Skip to content

Commit 22f77c9

Browse files
author
Andy Hanson
committed
getEditsForFileRename: Avoid changing import specifier ending
1 parent 9df8831 commit 22f77c9

File tree

3 files changed

+115
-38
lines changed

3 files changed

+115
-38
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 85 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,50 @@ namespace ts.moduleSpecifiers {
55
readonly importModuleSpecifierPreference?: "relative" | "non-relative";
66
}
77

8+
const enum RelativePreference { Relative, NonRelative, Auto }
9+
const enum Ending {
10+
Minimal, // Import "foo.ts" as "foo", "foo/index.ts" as "foo"
11+
Index, // Import "foo.ts" as "foo", "foo/index.ts" as "foo/index"
12+
JsExtension, // Import "foo.ts" as "foo.js", "foo/index.ts" as "foo/index.js"
13+
}
14+
15+
// Processed preferences
16+
interface Preferences {
17+
readonly relativePreference: RelativePreference;
18+
readonly ending: Ending;
19+
}
20+
21+
function getPreferences({ importModuleSpecifierPreference }: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences {
22+
return {
23+
relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : RelativePreference.Auto,
24+
ending: usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension
25+
: getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal,
26+
};
27+
}
28+
29+
function getPreferencesForUpdate(_: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences {
30+
return {
31+
relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative,
32+
ending: fileExtensionIs(oldImportSpecifier, Extension.Js) ? Ending.JsExtension
33+
: getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") || endsWith(oldImportSpecifier, "index.js") ? Ending.Index : Ending.Minimal,
34+
};
35+
}
36+
37+
export function updateModuleSpecifier(
38+
compilerOptions: CompilerOptions,
39+
importingSourceFileName: Path,
40+
toFileName: string,
41+
host: ModuleSpecifierResolutionHost,
42+
files: ReadonlyArray<SourceFile>,
43+
preferences: ModuleSpecifierPreferences = {},
44+
redirectTargetsMap: RedirectTargetsMap,
45+
oldImportSpecifier: string,
46+
): string | undefined {
47+
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferencesForUpdate(preferences, compilerOptions, oldImportSpecifier));
48+
if (res === oldImportSpecifier) return undefined;
49+
return res;
50+
}
51+
852
// Note: importingSourceFile is just for usesJsExtensionOnImports
953
export function getModuleSpecifier(
1054
compilerOptions: CompilerOptions,
@@ -16,9 +60,21 @@ namespace ts.moduleSpecifiers {
1660
preferences: ModuleSpecifierPreferences = {},
1761
redirectTargetsMap: RedirectTargetsMap,
1862
): string {
19-
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFileName, host);
63+
return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferences(preferences, compilerOptions, importingSourceFile));
64+
}
65+
66+
function getModuleSpecifierWorker(
67+
compilerOptions: CompilerOptions,
68+
importingSourceFileName: Path,
69+
toFileName: string,
70+
host: ModuleSpecifierResolutionHost,
71+
files: ReadonlyArray<SourceFile>,
72+
redirectTargetsMap: RedirectTargetsMap,
73+
preferences: Preferences
74+
): string {
75+
const info = getInfo(importingSourceFileName, host);
2076
const modulePaths = getAllModulePaths(files, importingSourceFileName, toFileName, info.getCanonicalFileName, host, redirectTargetsMap);
21-
return firstDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions)) ||
77+
return firstDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions, preferences.ending)) ||
2278
first(getLocalModuleSpecifiers(toFileName, info, compilerOptions, preferences));
2379
}
2480

@@ -41,60 +97,47 @@ namespace ts.moduleSpecifiers {
4197
importingSourceFile: SourceFile,
4298
host: ModuleSpecifierResolutionHost,
4399
files: ReadonlyArray<SourceFile>,
44-
preferences: ModuleSpecifierPreferences,
100+
userPreferences: ModuleSpecifierPreferences,
45101
redirectTargetsMap: RedirectTargetsMap,
46102
): ReadonlyArray<ReadonlyArray<string>> {
47103
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol);
48104
if (ambient) return [[ambient]];
49105

50-
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.path, host);
106+
const info = getInfo(importingSourceFile.path, host);
51107
if (!files) {
52108
return Debug.fail("Files list must be present to resolve symlinks in specifier resolution");
53109
}
54110
const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration);
55111
const modulePaths = getAllModulePaths(files, importingSourceFile.path, moduleSourceFile.fileName, info.getCanonicalFileName, host, redirectTargetsMap);
56112

57-
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions));
113+
const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);
114+
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions, preferences.ending));
58115
return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName =>
59116
getLocalModuleSpecifiers(moduleFileName, info, compilerOptions, preferences));
60117
}
61118

62119
interface Info {
63-
readonly moduleResolutionKind: ModuleResolutionKind;
64-
readonly addJsExtension: boolean;
65120
readonly getCanonicalFileName: GetCanonicalFileName;
66121
readonly sourceDirectory: Path;
67122
}
68123
// importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
69-
function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info {
70-
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
71-
const addJsExtension = usesJsExtensionOnImports(importingSourceFile);
124+
function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info {
72125
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true);
73126
const sourceDirectory = getDirectoryPath(importingSourceFileName);
74-
return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory };
127+
return { getCanonicalFileName, sourceDirectory };
75128
}
76129

77-
function getGlobalModuleSpecifier(
78-
moduleFileName: string,
79-
{ addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
80-
host: ModuleSpecifierResolutionHost,
81-
compilerOptions: CompilerOptions,
82-
) {
83-
return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension)
130+
function getGlobalModuleSpecifier(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions, ending: Ending) {
131+
return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, ending)
84132
|| tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory);
85133
}
86134

87-
function getLocalModuleSpecifiers(
88-
moduleFileName: string,
89-
{ moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
90-
compilerOptions: CompilerOptions,
91-
preferences: ModuleSpecifierPreferences,
92-
): ReadonlyArray<string> {
135+
function getLocalModuleSpecifiers(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, compilerOptions: CompilerOptions, { ending, relativePreference }: Preferences): ReadonlyArray<string> {
93136
const { baseUrl, paths, rootDirs } = compilerOptions;
94137

95138
const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) ||
96-
removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension);
97-
if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") {
139+
removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending);
140+
if (!baseUrl || relativePreference === RelativePreference.Relative) {
98141
return [relativePath];
99142
}
100143

@@ -103,19 +146,19 @@ namespace ts.moduleSpecifiers {
103146
return [relativePath];
104147
}
105148

106-
const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension);
149+
const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending);
107150
if (paths) {
108151
const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths);
109152
if (fromPaths) {
110153
return [fromPaths];
111154
}
112155
}
113156

114-
if (preferences.importModuleSpecifierPreference === "non-relative") {
157+
if (relativePreference === RelativePreference.NonRelative) {
115158
return [importRelativeToBaseUrl];
116159
}
117160

118-
if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference);
161+
if (relativePreference !== RelativePreference.Auto) Debug.assertNever(relativePreference);
119162

120163
if (isPathRelativeToParent(relativeToBaseUrl)) {
121164
return [relativePath];
@@ -276,14 +319,14 @@ namespace ts.moduleSpecifiers {
276319
host: GetEffectiveTypeRootsHost,
277320
getCanonicalFileName: (file: string) => string,
278321
moduleFileName: string,
279-
addJsExtension: boolean,
322+
ending: Ending,
280323
): string | undefined {
281324
const roots = getEffectiveTypeRoots(options, host);
282325
return firstDefined(roots, unNormalizedTypeRoot => {
283326
const typeRoot = toPath(unNormalizedTypeRoot, /*basePath*/ undefined, getCanonicalFileName);
284327
if (startsWith(moduleFileName, typeRoot)) {
285328
// For a type definition, we can strip `/index` even with classic resolution.
286-
return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1), ModuleResolutionKind.NodeJs, addJsExtension);
329+
return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1), ending);
287330
}
288331
});
289332
}
@@ -428,13 +471,18 @@ namespace ts.moduleSpecifiers {
428471
});
429472
}
430473

431-
function removeExtensionAndIndexPostFix(fileName: string, moduleResolutionKind: ModuleResolutionKind, addJsExtension: boolean): string {
474+
function removeExtensionAndIndexPostFix(fileName: string, ending: Ending): string {
432475
const noExtension = removeFileExtension(fileName);
433-
return addJsExtension
434-
? noExtension + ".js"
435-
: moduleResolutionKind === ModuleResolutionKind.NodeJs
436-
? removeSuffix(noExtension, "/index")
437-
: noExtension;
476+
switch (ending) {
477+
case Ending.Minimal:
478+
return removeSuffix(noExtension, "/index");
479+
case Ending.Index:
480+
return noExtension;
481+
case Ending.JsExtension:
482+
return noExtension + ".js";
483+
default:
484+
return Debug.assertNever(ending);
485+
}
438486
}
439487

440488
function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined {

src/services/getEditsForFileRename.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ namespace ts {
156156

157157
// Need an update if the imported file moved, or the importing file moved and was using a relative path.
158158
return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text)))
159-
? moduleSpecifiers.getModuleSpecifier(program.getCompilerOptions(), sourceFile, newImportFromPath, toImport.newFileName, host, allFiles, preferences, program.redirectTargetsMap)
159+
? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), newImportFromPath, toImport.newFileName, host, allFiles, preferences, program.redirectTargetsMap, importLiteral.text)
160160
: undefined;
161161
});
162162
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
5+
// @Filename: /index.js
6+
////export const x = 0;
7+
8+
// @Filename: /a.js
9+
////import { x as x0 } from ".";
10+
////import { x as x1 } from "./index";
11+
////import { x as x2 } from "./index.js";
12+
13+
verify.getEditsForFileRename({
14+
oldPath: "/a.js",
15+
newPath: "/b.js",
16+
newFileContents: {}, // No change
17+
});
18+
19+
verify.getEditsForFileRename({
20+
oldPath: "/b.js",
21+
newPath: "/src/b.js",
22+
newFileContents: {
23+
"/b.js":
24+
`import { x as x0 } from "..";
25+
import { x as x1 } from "../index";
26+
import { x as x2 } from "../index.js";`,
27+
},
28+
});
29+

0 commit comments

Comments
 (0)