Skip to content

Commit ae9c671

Browse files
authored
(fix #50725, #50710) add file extensions in import statements (#51702)
* fixes #50725 * fixed 50710 * fixed broken test * clean up * variable rename * rename variable to newFileBaseName
1 parent 5bb204e commit ae9c671

File tree

4 files changed

+140
-22
lines changed

4 files changed

+140
-22
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -880,9 +880,9 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam
880880
return undefined;
881881
}
882882

883-
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs
884-
? removeExtensionAndIndexPostFix(shortest, ending, compilerOptions)
885-
: removeFileExtension(shortest);
883+
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Classic
884+
? removeFileExtension(shortest)
885+
: removeExtensionAndIndexPostFix(shortest, ending, compilerOptions);
886886
}
887887

888888
function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined {

src/services/refactors/moveToNewFile.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getModuleSpecifier } from "../../compiler/moduleSpecifiers";
12
import {
23
AnyImportOrRequireStatement,
34
append,
@@ -16,13 +17,13 @@ import {
1617
concatenate,
1718
contains,
1819
copyEntries,
20+
createModuleSpecifierResolutionHost,
1921
createTextRangeFromSpan,
2022
Debug,
2123
Declaration,
2224
DeclarationStatement,
2325
Diagnostics,
2426
emptyArray,
25-
ensurePathIsNonModuleName,
2627
EnumDeclaration,
2728
escapeLeadingUnderscores,
2829
Expression,
@@ -102,9 +103,9 @@ import {
102103
rangeContainsRange,
103104
RefactorContext,
104105
RefactorEditInfo,
105-
removeFileExtension,
106106
RequireOrImportCall,
107107
RequireVariableStatement,
108+
resolvePath,
108109
ScriptTarget,
109110
skipAlias,
110111
some,
@@ -191,13 +192,12 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes
191192

192193
const currentDirectory = getDirectoryPath(oldFile.fileName);
193194
const extension = extensionFromPath(oldFile.fileName);
194-
const newModuleName = makeUniqueModuleName(getNewModuleName(usage.oldFileImportsFromNewFile, usage.movedSymbols), extension, currentDirectory, host);
195-
const newFileNameWithExtension = newModuleName + extension;
195+
const newFileBasename = makeUniqueModuleName(getNewModuleName(usage.oldFileImportsFromNewFile, usage.movedSymbols), extension, currentDirectory, host);
196196

197197
// If previous file was global, this is easy.
198-
changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences));
198+
changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileBasename + extension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFileBasename, extension, preferences));
199199

200-
addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host));
200+
addNewFileToTsconfig(program, changes, oldFile.fileName, newFileBasename + extension, hostGetCanonicalFileName(host));
201201
}
202202

203203
interface StatementRange {
@@ -258,7 +258,7 @@ function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTrack
258258
}
259259

260260
function getNewStatementsAndRemoveFromOldFile(
261-
oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences,
261+
oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newModuleName: string, extension: string, preferences: UserPreferences,
262262
) {
263263
const checker = program.getTypeChecker();
264264
const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective);
@@ -269,16 +269,16 @@ function getNewStatementsAndRemoveFromOldFile(
269269

270270
const useEsModuleSyntax = !!oldFile.externalModuleIndicator;
271271
const quotePreference = getQuotePreference(oldFile, preferences);
272-
const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEsModuleSyntax, quotePreference);
272+
const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newModuleName + extension, program, host, useEsModuleSyntax, quotePreference);
273273
if (importsFromNewFile) {
274274
insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true);
275275
}
276276

277277
deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker);
278278
deleteMovedStatements(oldFile, toMove.ranges, changes);
279-
updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName);
279+
updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newModuleName, extension);
280280

281-
const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEsModuleSyntax, quotePreference);
281+
const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference);
282282
const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax);
283283
if (imports.length && body.length) {
284284
return [
@@ -309,7 +309,9 @@ function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[
309309
}
310310
}
311311

312-
function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program: Program, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string): void {
312+
function updateImportsInOtherFiles(
313+
changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string, extension: string
314+
): void {
313315
const checker = program.getTypeChecker();
314316
for (const sourceFile of program.getSourceFiles()) {
315317
if (sourceFile === oldFile) continue;
@@ -324,7 +326,9 @@ function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program:
324326
return !!symbol && movedSymbols.has(symbol);
325327
};
326328
deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file
327-
const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName);
329+
330+
const pathToNewFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), newModuleName + extension);
331+
const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFileWithExtension, createModuleSpecifierResolutionHost(program, host));
328332
const newImportDeclaration = filterImport(importNode, factory.createStringLiteral(newModuleSpecifier), shouldMove);
329333
if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration);
330334

@@ -431,7 +435,15 @@ type SupportedImportStatement =
431435
| ImportEqualsDeclaration
432436
| VariableStatement;
433437

434-
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
438+
function createOldFileImportsFromNewFile(
439+
sourceFile: SourceFile,
440+
newFileNeedExport: ReadonlySymbolSet,
441+
newFileNameWithExtension: string,
442+
program: Program,
443+
host: LanguageServiceHost,
444+
useEs6Imports: boolean,
445+
quotePreference: QuotePreference
446+
): AnyImportOrRequireStatement | undefined {
435447
let defaultImport: Identifier | undefined;
436448
const imports: string[] = [];
437449
newFileNeedExport.forEach(symbol => {
@@ -442,20 +454,31 @@ function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, n
442454
imports.push(symbol.name);
443455
}
444456
});
445-
return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference);
457+
return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference);
446458
}
447459

448-
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
449-
path = ensurePathIsNonModuleName(path);
460+
function makeImportOrRequire(
461+
sourceFile: SourceFile,
462+
defaultImport: Identifier | undefined,
463+
imports: readonly string[],
464+
newFileNameWithExtension: string,
465+
program: Program,
466+
host: LanguageServiceHost,
467+
useEs6Imports: boolean,
468+
quotePreference: QuotePreference
469+
): AnyImportOrRequireStatement | undefined {
470+
const pathToNewFile = resolvePath(getDirectoryPath(sourceFile.path), newFileNameWithExtension);
471+
const pathToNewFileWithCorrectExtension = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFile, createModuleSpecifierResolutionHost(program, host));
472+
450473
if (useEs6Imports) {
451474
const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i)));
452-
return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference);
475+
return makeImportIfNecessary(defaultImport, specifiers, pathToNewFileWithCorrectExtension, quotePreference);
453476
}
454477
else {
455478
Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module.
456479
const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i));
457480
return bindingElements.length
458-
? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(path))) as RequireVariableStatement
481+
? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(pathToNewFileWithCorrectExtension))) as RequireVariableStatement
459482
: undefined;
460483
}
461484
}
@@ -564,6 +587,8 @@ function getNewFileImportsAndAddExportInOldFile(
564587
newFileImportsFromOldFile: ReadonlySymbolSet,
565588
changes: textChanges.ChangeTracker,
566589
checker: TypeChecker,
590+
program: Program,
591+
host: LanguageServiceHost,
567592
useEsModuleSyntax: boolean,
568593
quotePreference: QuotePreference,
569594
): readonly SupportedImportStatement[] {
@@ -600,7 +625,7 @@ function getNewFileImportsAndAddExportInOldFile(
600625
}
601626
});
602627

603-
append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEsModuleSyntax, quotePreference));
628+
append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, getBaseFileName(oldFile.fileName), program, host, useEsModuleSyntax, quotePreference));
604629
return copiedOldImports;
605630
}
606631

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
// @Filename: /tsconfig.json
4+
////{
5+
//// "compilerOptions": {
6+
//// "module": "Node16",
7+
//// "rootDirs": ["src"]
8+
//// }
9+
////}
10+
11+
// @Filename: /src/person.ts
12+
////export const name = 0;
13+
14+
// @Filename: /src/index.ts
15+
////import {name} from "./person.js";
16+
17+
verify.getEditsForFileRename({
18+
oldPath: '/src/person.ts',
19+
newPath: '/src/vip.ts',
20+
newFileContents: {
21+
'/src/index.ts': 'import {name} from "./vip.js";',
22+
},
23+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
// @Filename: /tsconfig.json
4+
////{
5+
//// "compilerOptions": {
6+
//// "moduleResolution": "Node16",
7+
//// }
8+
////}
9+
10+
// @Filename: /package.json
11+
//// { "type": "module" }
12+
13+
// @Filename: /src/main.ts
14+
////[|export function someLibFn(): string {
15+
//// return main();
16+
////}|]
17+
////
18+
////function main(): string {
19+
//// return "hello world!";
20+
////}
21+
////console.log(someLibFn());
22+
23+
// @Filename: /other.ts
24+
////import { someLibFn } from "./src/main.js";
25+
////
26+
////function someOtherFn(): string {
27+
//// return someLibFn();
28+
////}
29+
30+
// @Filename: /act/action.ts
31+
////import { someLibFn } from "../src/main.js";
32+
////
33+
////function doAction(): string {
34+
//// return someLibFn();
35+
////}
36+
37+
38+
verify.moveToNewFile({
39+
newFileContents: {
40+
"/src/main.ts":
41+
`import { someLibFn } from "./someLibFn.js";
42+
43+
export function main(): string {
44+
return "hello world!";
45+
}
46+
console.log(someLibFn());`,
47+
48+
"/src/someLibFn.ts":
49+
`import { main } from "./main.js";
50+
51+
export function someLibFn(): string {
52+
return main();
53+
}
54+
`,
55+
56+
"/other.ts":
57+
`import { someLibFn } from "./src/someLibFn.js";
58+
59+
function someOtherFn(): string {
60+
return someLibFn();
61+
}`,
62+
63+
"/act/action.ts":
64+
`import { someLibFn } from "../src/someLibFn.js";
65+
66+
function doAction(): string {
67+
return someLibFn();
68+
}`,
69+
}
70+
});

0 commit comments

Comments
 (0)