diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 84031881431..c21a26c7dcb 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -880,9 +880,9 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam return undefined; } - return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs - ? removeExtensionAndIndexPostFix(shortest, ending, compilerOptions) - : removeFileExtension(shortest); + return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Classic + ? removeFileExtension(shortest) + : removeExtensionAndIndexPostFix(shortest, ending, compilerOptions); } function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 937c31edadd..dd4bc63d6be 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -1,3 +1,4 @@ +import { getModuleSpecifier } from "../../compiler/moduleSpecifiers"; import { AnyImportOrRequireStatement, append, @@ -16,13 +17,13 @@ import { concatenate, contains, copyEntries, + createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, Declaration, DeclarationStatement, Diagnostics, emptyArray, - ensurePathIsNonModuleName, EnumDeclaration, escapeLeadingUnderscores, Expression, @@ -102,9 +103,9 @@ import { rangeContainsRange, RefactorContext, RefactorEditInfo, - removeFileExtension, RequireOrImportCall, RequireVariableStatement, + resolvePath, ScriptTarget, skipAlias, some, @@ -191,13 +192,12 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes const currentDirectory = getDirectoryPath(oldFile.fileName); const extension = extensionFromPath(oldFile.fileName); - const newModuleName = makeUniqueModuleName(getNewModuleName(usage.oldFileImportsFromNewFile, usage.movedSymbols), extension, currentDirectory, host); - const newFileNameWithExtension = newModuleName + extension; + const newFileBasename = makeUniqueModuleName(getNewModuleName(usage.oldFileImportsFromNewFile, usage.movedSymbols), extension, currentDirectory, host); // If previous file was global, this is easy. - changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences)); + changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileBasename + extension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFileBasename, extension, preferences)); - addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host)); + addNewFileToTsconfig(program, changes, oldFile.fileName, newFileBasename + extension, hostGetCanonicalFileName(host)); } interface StatementRange { @@ -258,7 +258,7 @@ function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTrack } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences, + oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newModuleName: string, extension: string, preferences: UserPreferences, ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); @@ -269,16 +269,16 @@ function getNewStatementsAndRemoveFromOldFile( const useEsModuleSyntax = !!oldFile.externalModuleIndicator; const quotePreference = getQuotePreference(oldFile, preferences); - const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEsModuleSyntax, quotePreference); + const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newModuleName + extension, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true); } deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); deleteMovedStatements(oldFile, toMove.ranges, changes); - updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName); + updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newModuleName, extension); - const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEsModuleSyntax, quotePreference); + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); if (imports.length && body.length) { return [ @@ -309,7 +309,9 @@ function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[ } } -function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program: Program, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string): void { +function updateImportsInOtherFiles( + changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string, extension: string +): void { const checker = program.getTypeChecker(); for (const sourceFile of program.getSourceFiles()) { if (sourceFile === oldFile) continue; @@ -324,7 +326,9 @@ function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program: return !!symbol && movedSymbols.has(symbol); }; deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file - const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName); + + const pathToNewFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), newModuleName + extension); + const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFileWithExtension, createModuleSpecifierResolutionHost(program, host)); const newImportDeclaration = filterImport(importNode, factory.createStringLiteral(newModuleSpecifier), shouldMove); if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); @@ -431,7 +435,15 @@ type SupportedImportStatement = | ImportEqualsDeclaration | VariableStatement; -function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined { +function createOldFileImportsFromNewFile( + sourceFile: SourceFile, + newFileNeedExport: ReadonlySymbolSet, + newFileNameWithExtension: string, + program: Program, + host: LanguageServiceHost, + useEs6Imports: boolean, + quotePreference: QuotePreference +): AnyImportOrRequireStatement | undefined { let defaultImport: Identifier | undefined; const imports: string[] = []; newFileNeedExport.forEach(symbol => { @@ -442,20 +454,31 @@ function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, n imports.push(symbol.name); } }); - return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference); + return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference); } -function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined { - path = ensurePathIsNonModuleName(path); +function makeImportOrRequire( + sourceFile: SourceFile, + defaultImport: Identifier | undefined, + imports: readonly string[], + newFileNameWithExtension: string, + program: Program, + host: LanguageServiceHost, + useEs6Imports: boolean, + quotePreference: QuotePreference +): AnyImportOrRequireStatement | undefined { + const pathToNewFile = resolvePath(getDirectoryPath(sourceFile.path), newFileNameWithExtension); + const pathToNewFileWithCorrectExtension = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFile, createModuleSpecifierResolutionHost(program, host)); + if (useEs6Imports) { const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i))); - return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference); + return makeImportIfNecessary(defaultImport, specifiers, pathToNewFileWithCorrectExtension, quotePreference); } else { Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module. const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); return bindingElements.length - ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(path))) as RequireVariableStatement + ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(pathToNewFileWithCorrectExtension))) as RequireVariableStatement : undefined; } } @@ -564,6 +587,8 @@ function getNewFileImportsAndAddExportInOldFile( newFileImportsFromOldFile: ReadonlySymbolSet, changes: textChanges.ChangeTracker, checker: TypeChecker, + program: Program, + host: LanguageServiceHost, useEsModuleSyntax: boolean, quotePreference: QuotePreference, ): readonly SupportedImportStatement[] { @@ -600,7 +625,7 @@ function getNewFileImportsAndAddExportInOldFile( } }); - append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEsModuleSyntax, quotePreference)); + append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, getBaseFileName(oldFile.fileName), program, host, useEsModuleSyntax, quotePreference)); return copiedOldImports; } diff --git a/tests/cases/fourslash/getEditsForFileRename_keepFileExtensions.ts b/tests/cases/fourslash/getEditsForFileRename_keepFileExtensions.ts new file mode 100644 index 00000000000..acc02153b0b --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_keepFileExtensions.ts @@ -0,0 +1,23 @@ +/// + +// @Filename: /tsconfig.json +////{ +//// "compilerOptions": { +//// "module": "Node16", +//// "rootDirs": ["src"] +//// } +////} + +// @Filename: /src/person.ts +////export const name = 0; + +// @Filename: /src/index.ts +////import {name} from "./person.js"; + +verify.getEditsForFileRename({ + oldPath: '/src/person.ts', + newPath: '/src/vip.ts', + newFileContents: { + '/src/index.ts': 'import {name} from "./vip.js";', + }, +}); diff --git a/tests/cases/fourslash/moveToNewFile_importFileExtensions.ts b/tests/cases/fourslash/moveToNewFile_importFileExtensions.ts new file mode 100644 index 00000000000..852dccb95ca --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_importFileExtensions.ts @@ -0,0 +1,70 @@ +/// + +// @Filename: /tsconfig.json +////{ +//// "compilerOptions": { +//// "moduleResolution": "Node16", +//// } +////} + +// @Filename: /package.json +//// { "type": "module" } + +// @Filename: /src/main.ts +////[|export function someLibFn(): string { +//// return main(); +////}|] +//// +////function main(): string { +//// return "hello world!"; +////} +////console.log(someLibFn()); + +// @Filename: /other.ts +////import { someLibFn } from "./src/main.js"; +//// +////function someOtherFn(): string { +//// return someLibFn(); +////} + +// @Filename: /act/action.ts +////import { someLibFn } from "../src/main.js"; +//// +////function doAction(): string { +//// return someLibFn(); +////} + + +verify.moveToNewFile({ + newFileContents: { + "/src/main.ts": +`import { someLibFn } from "./someLibFn.js"; + +export function main(): string { + return "hello world!"; +} +console.log(someLibFn());`, + + "/src/someLibFn.ts": +`import { main } from "./main.js"; + +export function someLibFn(): string { + return main(); +} +`, + + "/other.ts": +`import { someLibFn } from "./src/someLibFn.js"; + +function someOtherFn(): string { + return someLibFn(); +}`, + + "/act/action.ts": +`import { someLibFn } from "../src/someLibFn.js"; + +function doAction(): string { + return someLibFn(); +}`, + } +}); \ No newline at end of file