Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite relative import extensions with flag #59767

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2439577
Rewrite relative import extensions with flag
andrewbranch Aug 26, 2024
2b06e62
Add error for unsafe CJS-style resolution
andrewbranch Sep 6, 2024
a398ad1
Update baselines
andrewbranch Sep 10, 2024
921f9ae
Add error for using .ts in non-rewritable paths
andrewbranch Sep 10, 2024
ac8a2fa
Silence error for referencing TS file in node_modules
andrewbranch Sep 10, 2024
6e62350
Fix CJS error message interpolation
andrewbranch Sep 10, 2024
c8ba95d
Update baseline
andrewbranch Sep 10, 2024
fce0a2d
Use sourceFileMayBeEmitted
andrewbranch Sep 10, 2024
1558c7f
Merge branch 'main' into rewrite-extensions
andrewbranch Sep 10, 2024
0d95ce2
Add error for referencing referenced project input file
andrewbranch Sep 10, 2024
b88dddc
Add test with different but parallel rootDir/outDir
andrewbranch Sep 11, 2024
4867001
Test all import/export declaration forms for emit
andrewbranch Sep 11, 2024
62dab48
Move transformations to module, rewrite dynamic import
andrewbranch Sep 17, 2024
d87beef
Merge branch 'main' into rewrite-extensions
andrewbranch Sep 17, 2024
3a52370
Make shim for import call / require
andrewbranch Sep 18, 2024
84fa9ff
Process all import calls
andrewbranch Sep 18, 2024
32b3be0
Shim require, but don’t shim if the argument is statically determinab…
andrewbranch Sep 18, 2024
094c03c
Fix existing tests
andrewbranch Sep 18, 2024
b1974e0
Fix shimming in CommonJS transform
andrewbranch Sep 19, 2024
a125bfd
Merge branch 'main' into rewrite-extensions
andrewbranch Sep 19, 2024
5a1d785
Update baselines after merge
andrewbranch Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ import {
getAllJSDocTags,
getAllowSyntheticDefaultImports,
getAncestor,
getAnyExtensionFromPath,
getAssignedExpandoInitializer,
getAssignmentDeclarationKind,
getAssignmentDeclarationPropertyAccessKind,
Expand All @@ -260,6 +261,7 @@ import {
getCombinedLocalAndExportSymbolFlags,
getCombinedModifierFlags,
getCombinedNodeFlags,
getCommonSourceDirectoryOfConfig,
getContainingClass,
getContainingClassExcludingClassDecorators,
getContainingClassStaticBlock,
Expand Down Expand Up @@ -352,6 +354,8 @@ import {
getPropertyAssignmentAliasLikeExpression,
getPropertyNameForPropertyNameNode,
getPropertyNameFromType,
getRelativePathFromDirectory,
getRelativePathFromFile,
getResolutionDiagnostic,
getResolutionModeOverride,
getResolveJsonModule,
Expand Down Expand Up @@ -414,6 +418,7 @@ import {
hasSyntacticModifiers,
hasType,
HeritageClause,
hostGetCanonicalFileName,
Identifier,
identifierToKeywordKind,
IdentifierTypePredicate,
Expand Down Expand Up @@ -692,6 +697,7 @@ import {
isParenthesizedTypeNode,
isPartOfParameterDeclaration,
isPartOfTypeNode,
isPartOfTypeOnlyImportOrExportDeclaration,
isPartOfTypeQuery,
isPlainJsFile,
isPrefixUnaryExpression,
Expand Down Expand Up @@ -992,6 +998,7 @@ import {
ShorthandPropertyAssignment,
shouldAllowImportingTsExtension,
shouldPreserveConstEnums,
shouldRewriteModuleSpecifier,
Signature,
SignatureDeclaration,
SignatureFlags,
Expand All @@ -1005,6 +1012,7 @@ import {
skipTypeParentheses,
some,
SourceFile,
sourceFileMayBeEmitted,
SpreadAssignment,
SpreadElement,
startsWith,
Expand Down Expand Up @@ -4657,6 +4665,45 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
}
}
else if (
compilerOptions.rewriteRelativeImportExtensions
&& !(location.flags & NodeFlags.Ambient)
&& !isDeclarationFileName(moduleReference)
&& !isLiteralImportTypeNode(location)
&& !isPartOfTypeOnlyImportOrExportDeclaration(location)
) {
const shouldRewrite = shouldRewriteModuleSpecifier(moduleReference, compilerOptions);
if (!resolvedModule.resolvedUsingTsExtension && shouldRewrite) {
error(
errorNode,
Diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0,
getRelativePathFromFile(getNormalizedAbsolutePath(currentSourceFile.fileName, host.getCurrentDirectory()), resolvedModule.resolvedFileName, hostGetCanonicalFileName(host)),
);
}
else if (resolvedModule.resolvedUsingTsExtension && !shouldRewrite && sourceFileMayBeEmitted(sourceFile, host)) {
error(
errorNode,
Diagnostics.This_import_uses_a_0_extension_to_resolve_to_an_input_TypeScript_file_but_will_not_be_rewritten_during_emit_because_it_is_not_a_relative_path,
getAnyExtensionFromPath(moduleReference),
);
}
else if (resolvedModule.resolvedUsingTsExtension && shouldRewrite) {
const redirect = host.getResolvedProjectReferenceToRedirect(sourceFile.path);
if (redirect) {
const ignoreCase = !host.useCaseSensitiveFileNames();
const ownRootDir = host.getCommonSourceDirectory();
const otherRootDir = getCommonSourceDirectoryOfConfig(redirect.commandLine, ignoreCase);
const rootDirPath = getRelativePathFromDirectory(ownRootDir, otherRootDir, ignoreCase);
const outDirPath = getRelativePathFromDirectory(compilerOptions.outDir || ownRootDir, redirect.commandLine.options.outDir || otherRootDir, ignoreCase);
if (rootDirPath !== outDirPath) {
error(
errorNode,
Diagnostics.This_import_path_is_unsafe_to_rewrite_because_it_resolves_to_another_project_and_the_relative_path_between_the_projects_output_files_is_not_the_same_as_the_relative_path_between_its_input_files,
);
}
}
}
}

if (sourceFile.symbol) {
if (errorNode && resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
Expand Down Expand Up @@ -52724,7 +52771,7 @@ function createBasicNodeBuilderModuleSpecifierResolutionHost(host: TypeCheckerHo
getCurrentDirectory: () => host.getCurrentDirectory(),
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
getPackageJsonInfoCache: () => host.getPackageJsonInfoCache?.(),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
redirectTargetsMap: host.redirectTargetsMap,
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName),
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,15 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
defaultValueDescription: false,
transpileOptionValue: undefined,
},
{
name: "rewriteRelativeImportExtensions",
type: "boolean",
affectsSemanticDiagnostics: true,
affectsBuildInfo: true,
category: Diagnostics.Modules,
description: Diagnostics.Rewrite_ts_tsx_mts_and_cts_file_extensions_in_relative_import_paths_to_their_JavaScript_equivalent_in_output_files,
defaultValueDescription: false,
},
{
name: "resolvePackageJsonExports",
type: "boolean",
Expand Down
17 changes: 17 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3948,6 +3948,19 @@
"category": "Error",
"code": 2873
},
"This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to \"{0}\".": {
"category": "Error",
"code": 2874
},
"This import uses a '{0}' extension to resolve to an input TypeScript file, but will not be rewritten during emit because it is not a relative path.": {
"category": "Error",
"code": 2875
},
"This import path is unsafe to rewrite because it resolves to another project, and the relative path between the projects' output files is not the same as the relative path between its input files.": {
"category": "Error",
"code": 2876
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
"code": 4000
Expand Down Expand Up @@ -5930,6 +5943,10 @@
"category": "Message",
"code": 6420
},
"Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files.": {
"category": "Message",
"code": 6421
},

"The expected type comes from property '{0}' which is declared here on type '{1}'": {
"category": "Message",
Expand Down
38 changes: 38 additions & 0 deletions src/compiler/factory/emitHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isCallExpression,
isComputedPropertyName,
isIdentifier,
JsxEmit,
memoize,
ObjectLiteralElementLike,
ParameterDeclaration,
Expand Down Expand Up @@ -139,6 +140,8 @@ export interface EmitHelperFactory {
// 'using' helpers
createAddDisposableResourceHelper(envBinding: Expression, value: Expression, async: boolean): Expression;
createDisposeResourcesHelper(envBinding: Expression): Expression;
// --rewriteRelativeImportExtensions helpers
createRewriteRelativeImportExtensionsHelper(expression: Expression): Expression;
}

/** @internal */
Expand Down Expand Up @@ -189,6 +192,8 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
// 'using' helpers
createAddDisposableResourceHelper,
createDisposeResourcesHelper,
// --rewriteRelativeImportExtensions helpers
createRewriteRelativeImportExtensionsHelper,
};

/**
Expand Down Expand Up @@ -682,6 +687,17 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
context.requestEmitHelper(disposeResourcesHelper);
return factory.createCallExpression(getUnscopedHelperName("__disposeResources"), /*typeArguments*/ undefined, [envBinding]);
}

function createRewriteRelativeImportExtensionsHelper(expression: Expression) {
context.requestEmitHelper(rewriteRelativeImportExtensionsHelper);
return factory.createCallExpression(
getUnscopedHelperName("__rewriteRelativeImportExtension"),
/*typeArguments*/ undefined,
context.getCompilerOptions().jsx === JsxEmit.Preserve
? [expression, factory.createTrue()]
: [expression],
);
}
}

/** @internal */
Expand Down Expand Up @@ -1422,6 +1438,28 @@ const disposeResourcesHelper: UnscopedEmitHelper = {
});`,
};

const rewriteRelativeImportExtensionsHelper: UnscopedEmitHelper = {
name: "typescript:rewriteRelativeImportExtensions",
importName: "__rewriteRelativeImportExtension",
scoped: false,
text: `
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
if (typeof path === "string" && path[0] === "." && (path[1] === "/" || path[1] === "." && path[2] === "/")) {
if (path.substring(path.length - 4) === ".tsx") {
return path.substring(0, path.length - 4) + (preserveJsx ? ".jsx" : ".js");
}
if (path.substring(path.length - 3) === ".ts") {
var dot = path.lastIndexOf(".", path.length - 4);
if (dot >= 0 && (path.substring(dot - 2, dot) === ".d" || path.substring(dot, dot + 2) === ".d")) {
return path;
}
}
return path.replace(/(?<!\\.d)\\.[cm]ts$/, function (ext) { return ext === ".mts" ? ".mjs" : ".cjs"; });
}
return path;
};`,
};

/** @internal */
export const asyncSuperHelper: EmitHelper = {
name: "typescript:async-super",
Expand Down
18 changes: 10 additions & 8 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
forEach,
forEachAncestorDirectory,
formatMessage,
getAllowImportingTsExtensions,
getAllowJSCompilerOption,
getAnyExtensionFromPath,
getBaseFileName,
Expand Down Expand Up @@ -1484,7 +1485,7 @@ export function resolveModuleName(moduleName: string, containingFile: string, co
* 'typings' entry or file 'index' with some supported extension
* - Classic loader will only try to interpret '/a/b/c' as file.
*/
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined;
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonValue?: string) => Resolved | undefined;

/**
* Any module resolution kind can be augmented with optional settings: 'baseUrl', 'paths' and 'rootDirs' - they are used to
Expand Down Expand Up @@ -2094,13 +2095,14 @@ function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidat
* module specifiers written in source files - and so it always allows the
* candidate to end with a TS extension (but will also try substituting a JS extension for a TS extension).
*/
function loadFileNameFromPackageJsonField(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
function loadFileNameFromPackageJsonField(extensions: Extensions, candidate: string, packageJsonValue: string | undefined, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
if (
extensions & Extensions.TypeScript && fileExtensionIsOneOf(candidate, supportedTSImplementationExtensions) ||
extensions & Extensions.Declaration && fileExtensionIsOneOf(candidate, supportedDeclarationExtensions)
) {
const result = tryFile(candidate, onlyRecordFailures, state);
return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension, resolvedUsingTsExtension: undefined } : undefined;
const ext = tryExtractTSExtension(candidate) as Extension;
return result !== undefined ? { path: candidate, ext, resolvedUsingTsExtension: packageJsonValue ? !endsWith(packageJsonValue, ext) : undefined } : undefined;
}

if (state.isConfigLookup && extensions === Extensions.Json && fileExtensionIs(candidate, Extension.Json)) {
Expand Down Expand Up @@ -2316,7 +2318,7 @@ function loadEntrypointsFromExportMap(
}
const resolvedTarget = combinePaths(scope.packageDirectory, target);
const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.());
const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state);
const result = loadFileNameFromPackageJsonField(extensions, finalPath, target, /*onlyRecordFailures*/ false, state);
if (result) {
entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path);
return true;
Expand Down Expand Up @@ -2487,7 +2489,7 @@ function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: st
}

const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => {
const fromFile = loadFileNameFromPackageJsonField(extensions, candidate, onlyRecordFailures, state);
const fromFile = loadFileNameFromPackageJsonField(extensions, candidate, /*packageJsonValue*/ undefined, onlyRecordFailures, state);
if (fromFile) {
return noPackageId(fromFile);
}
Expand Down Expand Up @@ -2790,7 +2792,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
const finalPath = toAbsolutePath(pattern ? resolvedTarget.replace(/\*/g, subpath) : resolvedTarget + subpath);
const inputLink = tryLoadInputFileForPath(finalPath, subpath, combinePaths(scope.packageDirectory, "package.json"), isImports);
if (inputLink) return inputLink;
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state), state));
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, target, /*onlyRecordFailures*/ false, state), state));
}
else if (typeof target === "object" && target !== null) { // eslint-disable-line no-restricted-syntax
if (!Array.isArray(target)) {
Expand Down Expand Up @@ -2936,7 +2938,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
if (!extensionIsOk(extensions, possibleExt)) continue;
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state));
if (state.host.fileExists(possibleInputWithInputExtension)) {
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state), state));
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state));
}
}
}
Expand Down Expand Up @@ -3333,7 +3335,7 @@ function resolveFromTypeRoot(moduleName: string, state: ModuleResolutionState) {
// so this function doesn't check them to avoid propagating errors.
/** @internal */
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string): boolean {
return !!compilerOptions.allowImportingTsExtensions || !!fromFileName && isDeclarationFileName(fromFileName);
return getAllowImportingTsExtensions(compilerOptions) || !!fromFileName && isDeclarationFileName(fromFileName);
}

/**
Expand Down
Loading