Skip to content

Commit bd3d700

Browse files
authored
Rewrite relative import extensions with flag (#59767)
1 parent 9d98874 commit bd3d700

File tree

54 files changed

+3132
-89
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3132
-89
lines changed

src/compiler/checker.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ import {
248248
getAllJSDocTags,
249249
getAllowSyntheticDefaultImports,
250250
getAncestor,
251+
getAnyExtensionFromPath,
251252
getAssignedExpandoInitializer,
252253
getAssignmentDeclarationKind,
253254
getAssignmentDeclarationPropertyAccessKind,
@@ -259,6 +260,7 @@ import {
259260
getCombinedLocalAndExportSymbolFlags,
260261
getCombinedModifierFlags,
261262
getCombinedNodeFlags,
263+
getCommonSourceDirectoryOfConfig,
262264
getContainingClass,
263265
getContainingClassExcludingClassDecorators,
264266
getContainingClassStaticBlock,
@@ -352,6 +354,8 @@ import {
352354
getPropertyAssignmentAliasLikeExpression,
353355
getPropertyNameForPropertyNameNode,
354356
getPropertyNameFromType,
357+
getRelativePathFromDirectory,
358+
getRelativePathFromFile,
355359
getResolutionDiagnostic,
356360
getResolutionModeOverride,
357361
getResolveJsonModule,
@@ -414,6 +418,7 @@ import {
414418
hasSyntacticModifiers,
415419
hasType,
416420
HeritageClause,
421+
hostGetCanonicalFileName,
417422
Identifier,
418423
identifierToKeywordKind,
419424
IdentifierTypePredicate,
@@ -693,6 +698,7 @@ import {
693698
isParenthesizedTypeNode,
694699
isPartOfParameterDeclaration,
695700
isPartOfTypeNode,
701+
isPartOfTypeOnlyImportOrExportDeclaration,
696702
isPartOfTypeQuery,
697703
isPlainJsFile,
698704
isPrefixUnaryExpression,
@@ -994,6 +1000,7 @@ import {
9941000
ShorthandPropertyAssignment,
9951001
shouldAllowImportingTsExtension,
9961002
shouldPreserveConstEnums,
1003+
shouldRewriteModuleSpecifier,
9971004
Signature,
9981005
SignatureDeclaration,
9991006
SignatureFlags,
@@ -1007,6 +1014,7 @@ import {
10071014
skipTypeParentheses,
10081015
some,
10091016
SourceFile,
1017+
sourceFileMayBeEmitted,
10101018
SpreadAssignment,
10111019
SpreadElement,
10121020
startsWith,
@@ -4665,6 +4673,45 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
46654673
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
46664674
}
46674675
}
4676+
else if (
4677+
compilerOptions.rewriteRelativeImportExtensions
4678+
&& !(location.flags & NodeFlags.Ambient)
4679+
&& !isDeclarationFileName(moduleReference)
4680+
&& !isLiteralImportTypeNode(location)
4681+
&& !isPartOfTypeOnlyImportOrExportDeclaration(location)
4682+
) {
4683+
const shouldRewrite = shouldRewriteModuleSpecifier(moduleReference, compilerOptions);
4684+
if (!resolvedModule.resolvedUsingTsExtension && shouldRewrite) {
4685+
error(
4686+
errorNode,
4687+
Diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0,
4688+
getRelativePathFromFile(getNormalizedAbsolutePath(currentSourceFile.fileName, host.getCurrentDirectory()), resolvedModule.resolvedFileName, hostGetCanonicalFileName(host)),
4689+
);
4690+
}
4691+
else if (resolvedModule.resolvedUsingTsExtension && !shouldRewrite && sourceFileMayBeEmitted(sourceFile, host)) {
4692+
error(
4693+
errorNode,
4694+
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,
4695+
getAnyExtensionFromPath(moduleReference),
4696+
);
4697+
}
4698+
else if (resolvedModule.resolvedUsingTsExtension && shouldRewrite) {
4699+
const redirect = host.getResolvedProjectReferenceToRedirect(sourceFile.path);
4700+
if (redirect) {
4701+
const ignoreCase = !host.useCaseSensitiveFileNames();
4702+
const ownRootDir = host.getCommonSourceDirectory();
4703+
const otherRootDir = getCommonSourceDirectoryOfConfig(redirect.commandLine, ignoreCase);
4704+
const rootDirPath = getRelativePathFromDirectory(ownRootDir, otherRootDir, ignoreCase);
4705+
const outDirPath = getRelativePathFromDirectory(compilerOptions.outDir || ownRootDir, redirect.commandLine.options.outDir || otherRootDir, ignoreCase);
4706+
if (rootDirPath !== outDirPath) {
4707+
error(
4708+
errorNode,
4709+
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,
4710+
);
4711+
}
4712+
}
4713+
}
4714+
}
46684715

46694716
if (sourceFile.symbol) {
46704717
if (errorNode && resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
@@ -50871,6 +50918,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5087150918
return ["__propKey"];
5087250919
case ExternalEmitHelpers.AddDisposableResourceAndDisposeResources:
5087350920
return ["__addDisposableResource", "__disposeResources"];
50921+
case ExternalEmitHelpers.RewriteRelativeImportExtension:
50922+
return ["__rewriteRelativeImportExtension"];
5087450923
default:
5087550924
return Debug.fail("Unrecognized helper");
5087650925
}
@@ -52911,7 +52960,7 @@ function createBasicNodeBuilderModuleSpecifierResolutionHost(host: TypeCheckerHo
5291152960
getCurrentDirectory: () => host.getCurrentDirectory(),
5291252961
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
5291352962
getPackageJsonInfoCache: () => host.getPackageJsonInfoCache?.(),
52914-
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
52963+
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
5291552964
redirectTargetsMap: host.redirectTargetsMap,
5291652965
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
5291752966
isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName),

src/compiler/commandLineParser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,15 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
11771177
defaultValueDescription: false,
11781178
transpileOptionValue: undefined,
11791179
},
1180+
{
1181+
name: "rewriteRelativeImportExtensions",
1182+
type: "boolean",
1183+
affectsSemanticDiagnostics: true,
1184+
affectsBuildInfo: true,
1185+
category: Diagnostics.Modules,
1186+
description: Diagnostics.Rewrite_ts_tsx_mts_and_cts_file_extensions_in_relative_import_paths_to_their_JavaScript_equivalent_in_output_files,
1187+
defaultValueDescription: false,
1188+
},
11801189
{
11811190
name: "resolvePackageJsonExports",
11821191
type: "boolean",

src/compiler/diagnosticMessages.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3964,6 +3964,18 @@
39643964
"category": "Error",
39653965
"code": 2875
39663966
},
3967+
"This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to \"{0}\".": {
3968+
"category": "Error",
3969+
"code": 2876
3970+
},
3971+
"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.": {
3972+
"category": "Error",
3973+
"code": 2877
3974+
},
3975+
"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.": {
3976+
"category": "Error",
3977+
"code": 2878
3978+
},
39673979

39683980
"Import declaration '{0}' is using private name '{1}'.": {
39693981
"category": "Error",
@@ -5947,6 +5959,10 @@
59475959
"category": "Message",
59485960
"code": 6420
59495961
},
5962+
"Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files.": {
5963+
"category": "Message",
5964+
"code": 6421
5965+
},
59505966

59515967
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
59525968
"category": "Message",

src/compiler/factory/emitHelpers.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
isCallExpression,
2424
isComputedPropertyName,
2525
isIdentifier,
26+
JsxEmit,
2627
memoize,
2728
ObjectLiteralElementLike,
2829
ParameterDeclaration,
@@ -139,6 +140,8 @@ export interface EmitHelperFactory {
139140
// 'using' helpers
140141
createAddDisposableResourceHelper(envBinding: Expression, value: Expression, async: boolean): Expression;
141142
createDisposeResourcesHelper(envBinding: Expression): Expression;
143+
// --rewriteRelativeImportExtensions helpers
144+
createRewriteRelativeImportExtensionsHelper(expression: Expression): Expression;
142145
}
143146

144147
/** @internal */
@@ -189,6 +192,8 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
189192
// 'using' helpers
190193
createAddDisposableResourceHelper,
191194
createDisposeResourcesHelper,
195+
// --rewriteRelativeImportExtensions helpers
196+
createRewriteRelativeImportExtensionsHelper,
192197
};
193198

194199
/**
@@ -682,6 +687,17 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
682687
context.requestEmitHelper(disposeResourcesHelper);
683688
return factory.createCallExpression(getUnscopedHelperName("__disposeResources"), /*typeArguments*/ undefined, [envBinding]);
684689
}
690+
691+
function createRewriteRelativeImportExtensionsHelper(expression: Expression) {
692+
context.requestEmitHelper(rewriteRelativeImportExtensionsHelper);
693+
return factory.createCallExpression(
694+
getUnscopedHelperName("__rewriteRelativeImportExtension"),
695+
/*typeArguments*/ undefined,
696+
context.getCompilerOptions().jsx === JsxEmit.Preserve
697+
? [expression, factory.createTrue()]
698+
: [expression],
699+
);
700+
}
685701
}
686702

687703
/** @internal */
@@ -1422,6 +1438,21 @@ const disposeResourcesHelper: UnscopedEmitHelper = {
14221438
});`,
14231439
};
14241440

1441+
const rewriteRelativeImportExtensionsHelper: UnscopedEmitHelper = {
1442+
name: "typescript:rewriteRelativeImportExtensions",
1443+
importName: "__rewriteRelativeImportExtension",
1444+
scoped: false,
1445+
text: `
1446+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
1447+
if (typeof path === "string" && /^\\.\\.?\\//.test(path)) {
1448+
return path.replace(/\\.(tsx)$|((?:\\.d)?)((?:\\.[^./]+?)?)\\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
1449+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
1450+
});
1451+
}
1452+
return path;
1453+
};`,
1454+
};
1455+
14251456
/** @internal */
14261457
export const asyncSuperHelper: EmitHelper = {
14271458
name: "typescript:async-super",

src/compiler/moduleNameResolver.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
forEach,
3737
forEachAncestorDirectory,
3838
formatMessage,
39+
getAllowImportingTsExtensions,
3940
getAllowJSCompilerOption,
4041
getAnyExtensionFromPath,
4142
getBaseFileName,
@@ -1484,7 +1485,7 @@ export function resolveModuleName(moduleName: string, containingFile: string, co
14841485
* 'typings' entry or file 'index' with some supported extension
14851486
* - Classic loader will only try to interpret '/a/b/c' as file.
14861487
*/
1487-
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined;
1488+
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonValue?: string) => Resolved | undefined;
14881489

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

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

24892491
const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => {
2490-
const fromFile = loadFileNameFromPackageJsonField(extensions, candidate, onlyRecordFailures, state);
2492+
const fromFile = loadFileNameFromPackageJsonField(extensions, candidate, /*packageJsonValue*/ undefined, onlyRecordFailures, state);
24912493
if (fromFile) {
24922494
return noPackageId(fromFile);
24932495
}
@@ -2790,7 +2792,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
27902792
const finalPath = toAbsolutePath(pattern ? resolvedTarget.replace(/\*/g, subpath) : resolvedTarget + subpath);
27912793
const inputLink = tryLoadInputFileForPath(finalPath, subpath, combinePaths(scope.packageDirectory, "package.json"), isImports);
27922794
if (inputLink) return inputLink;
2793-
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state), state));
2795+
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, finalPath, target, /*onlyRecordFailures*/ false, state), state));
27942796
}
27952797
else if (typeof target === "object" && target !== null) { // eslint-disable-line no-restricted-syntax
27962798
if (!Array.isArray(target)) {
@@ -2936,7 +2938,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
29362938
if (!extensionIsOk(extensions, possibleExt)) continue;
29372939
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state));
29382940
if (state.host.fileExists(possibleInputWithInputExtension)) {
2939-
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state), state));
2941+
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*packageJsonValue*/ undefined, /*onlyRecordFailures*/ false, state), state));
29402942
}
29412943
}
29422944
}
@@ -3333,7 +3335,7 @@ function resolveFromTypeRoot(moduleName: string, state: ModuleResolutionState) {
33333335
// so this function doesn't check them to avoid propagating errors.
33343336
/** @internal */
33353337
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string): boolean {
3336-
return !!compilerOptions.allowImportingTsExtensions || !!fromFileName && isDeclarationFileName(fromFileName);
3338+
return getAllowImportingTsExtensions(compilerOptions) || !!fromFileName && isDeclarationFileName(fromFileName);
33373339
}
33383340

33393341
/**

0 commit comments

Comments
 (0)