Skip to content

Commit f98ba80

Browse files
authored
Merge pull request #24909 from weswigham/port-import-type-declaration-updates
Port import type declaration updates
2 parents 0db07e5 + 97c3c60 commit f98ba80

27 files changed

+583
-58
lines changed

src/compiler/checker.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4092,7 +4092,15 @@ namespace ts {
40924092
// ambient module, just use declaration/symbol name (fallthrough)
40934093
}
40944094
else {
4095-
return `"${getResolvedExternalModuleName(context.tracker.moduleResolverHost, file, getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration)))}"`;
4095+
const contextFile = getSourceFileOfNode(getOriginalNode(context!.enclosingDeclaration))!;
4096+
return `"${file.moduleName || moduleSpecifiers.getModuleSpecifiers(
4097+
symbol,
4098+
compilerOptions,
4099+
contextFile,
4100+
context!.tracker.moduleResolverHost!,
4101+
context!.tracker.moduleResolverHost!.getSourceFiles!(),
4102+
{ importModuleSpecifierPreference: "non-relative" }
4103+
)[0]}"`;
40964104
}
40974105
}
40984106
const declaration = symbol.declarations[0];

src/compiler/moduleNameResolver.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,6 @@ namespace ts {
132132
}
133133
}
134134

135-
export interface GetEffectiveTypeRootsHost {
136-
directoryExists?(directoryName: string): boolean;
137-
getCurrentDirectory?(): string;
138-
}
139135
export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
140136
if (options.typeRoots) {
141137
return options.typeRoots;

src/services/codefixes/moduleSpecifiers.ts renamed to src/compiler/moduleSpecifiers.ts

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
// Used by importFixes to synthesize import module specifiers.
22
/* @internal */
33
namespace ts.moduleSpecifiers {
4+
export interface ModuleSpecifierPreferences {
5+
importModuleSpecifierPreference?: "relative" | "non-relative";
6+
}
7+
48
// Note: fromSourceFile is just for usesJsExtensionOnImports
5-
export function getModuleSpecifier(program: Program, fromSourceFile: SourceFile, fromSourceFileName: string, toFileName: string, host: LanguageServiceHost, preferences: UserPreferences) {
6-
const info = getInfo(program.getCompilerOptions(), fromSourceFile, fromSourceFileName, host);
7-
const compilerOptions = program.getCompilerOptions();
9+
export function getModuleSpecifier(compilerOptions: CompilerOptions, fromSourceFile: SourceFile, fromSourceFileName: string, toFileName: string, host: ModuleSpecifierResolutionHost, preferences: ModuleSpecifierPreferences = {}) {
10+
const info = getInfo(compilerOptions, fromSourceFile, fromSourceFileName, host);
811
return getGlobalModuleSpecifier(toFileName, info, host, compilerOptions) ||
912
first(getLocalModuleSpecifiers(toFileName, info, compilerOptions, preferences));
1013
}
1114

1215
// For each symlink/original for a module, returns a list of ways to import that file.
1316
export function getModuleSpecifiers(
1417
moduleSymbol: Symbol,
15-
program: Program,
18+
compilerOptions: CompilerOptions,
1619
importingSourceFile: SourceFile,
17-
host: LanguageServiceHost,
18-
preferences: UserPreferences,
20+
host: ModuleSpecifierResolutionHost,
21+
files: ReadonlyArray<SourceFile>,
22+
preferences: ModuleSpecifierPreferences,
1923
): ReadonlyArray<ReadonlyArray<string>> {
2024
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol);
2125
if (ambient) return [[ambient]];
2226

23-
const compilerOptions = program.getCompilerOptions();
24-
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.fileName, host);
25-
const modulePaths = getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile());
27+
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.path, host);
28+
if (!files) {
29+
return Debug.fail("Files list must be present to resolve symlinks in specifier resolution");
30+
}
31+
const modulePaths = getAllModulePaths(files, getSourceFileOfNode(moduleSymbol.valueDeclaration), info.getCanonicalFileName, host);
2632

2733
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions));
2834
return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName =>
@@ -36,18 +42,18 @@ namespace ts.moduleSpecifiers {
3642
readonly sourceDirectory: string;
3743
}
3844
// importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
39-
function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: string, host: LanguageServiceHost): Info {
45+
function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: string, host: ModuleSpecifierResolutionHost): Info {
4046
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
4147
const addJsExtension = usesJsExtensionOnImports(importingSourceFile);
42-
const getCanonicalFileName = hostGetCanonicalFileName(host);
48+
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true);
4349
const sourceDirectory = getDirectoryPath(importingSourceFileName);
4450
return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory };
4551
}
4652

4753
function getGlobalModuleSpecifier(
4854
moduleFileName: string,
4955
{ addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
50-
host: LanguageServiceHost,
56+
host: ModuleSpecifierResolutionHost,
5157
compilerOptions: CompilerOptions,
5258
) {
5359
return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension)
@@ -59,7 +65,7 @@ namespace ts.moduleSpecifiers {
5965
moduleFileName: string,
6066
{ moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
6167
compilerOptions: CompilerOptions,
62-
preferences: UserPreferences,
68+
preferences: ModuleSpecifierPreferences,
6369
) {
6470
const { baseUrl, paths } = compilerOptions;
6571

@@ -127,15 +133,57 @@ namespace ts.moduleSpecifiers {
127133
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? fileExtensionIs(text, Extension.Js) : undefined) || false;
128134
}
129135

136+
function discoverProbableSymlinks(files: ReadonlyArray<SourceFile>) {
137+
const symlinks = mapDefined(files, sf =>
138+
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
139+
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] : undefined));
140+
const result = createMap<string>();
141+
if (symlinks) {
142+
for (const [resolvedPath, originalPath] of symlinks) {
143+
const resolvedParts = getPathComponents(resolvedPath);
144+
const originalParts = getPathComponents(originalPath);
145+
while (resolvedParts[resolvedParts.length - 1] === originalParts[originalParts.length - 1]) {
146+
resolvedParts.pop();
147+
originalParts.pop();
148+
}
149+
result.set(getPathFromPathComponents(originalParts), getPathFromPathComponents(resolvedParts));
150+
}
151+
}
152+
return result;
153+
}
154+
155+
function getAllModulePathsUsingIndirectSymlinks(files: ReadonlyArray<SourceFile>, target: string, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost) {
156+
const links = discoverProbableSymlinks(files);
157+
const paths = arrayFrom(links.keys());
158+
let options: string[] | undefined;
159+
for (const path of paths) {
160+
const resolved = links.get(path)!;
161+
if (startsWith(target, resolved + "/")) {
162+
const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName);
163+
const option = resolvePath(path, relative);
164+
if (!host.fileExists || host.fileExists(option)) {
165+
if (!options) options = [];
166+
options.push(option);
167+
}
168+
}
169+
}
170+
const resolvedtarget = host.getCurrentDirectory ? resolvePath(host.getCurrentDirectory(), target) : target;
171+
if (options) {
172+
options.push(resolvedtarget); // Since these are speculative, we also include the original resolved name as a possibility
173+
return options;
174+
}
175+
return [resolvedtarget];
176+
}
177+
130178
/**
131179
* Looks for a existing imports that use symlinks to this module.
132180
* Only if no symlink is available, the real path will be used.
133181
*/
134-
function getAllModulePaths(program: Program, { fileName }: SourceFile): ReadonlyArray<string> {
135-
const symlinks = mapDefined(program.getSourceFiles(), sf =>
182+
function getAllModulePaths(files: ReadonlyArray<SourceFile>, { fileName }: SourceFile, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost): ReadonlyArray<string> {
183+
const symlinks = mapDefined(files, sf =>
136184
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
137185
res && res.resolvedFileName === fileName ? res.originalPath : undefined));
138-
return symlinks.length === 0 ? [fileName] : symlinks;
186+
return symlinks.length === 0 ? getAllModulePathsUsingIndirectSymlinks(files, fileName, getCanonicalFileName, host) : symlinks;
139187
}
140188

141189
function getRelativePathNParents(relativePath: string): number {
@@ -210,7 +258,7 @@ namespace ts.moduleSpecifiers {
210258
function tryGetModuleNameAsNodeModule(
211259
options: CompilerOptions,
212260
moduleFileName: string,
213-
host: LanguageServiceHost,
261+
host: ModuleSpecifierResolutionHost,
214262
getCanonicalFileName: (file: string) => string,
215263
sourceDirectory: string,
216264
): string | undefined {
@@ -255,7 +303,8 @@ namespace ts.moduleSpecifiers {
255303
const fullModulePathWithoutExtension = removeFileExtension(path);
256304

257305
// If the file is /index, it can be imported by its directory name
258-
if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index") {
306+
// IFF there is not _also_ a file by the same name
307+
if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) {
259308
return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
260309
}
261310

@@ -274,6 +323,17 @@ namespace ts.moduleSpecifiers {
274323
}
275324
}
276325

326+
function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) {
327+
// We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
328+
const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]);
329+
for (const e of extensions) {
330+
const fullPath = path + e;
331+
if (host.fileExists!(fullPath)) { // TODO: GH#18217
332+
return fullPath;
333+
}
334+
}
335+
}
336+
277337
function getNodeModulePathParts(fullPath: string) {
278338
// If fullPath can't be valid module file within node_modules, returns undefined.
279339
// Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js

src/compiler/program.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,9 @@ namespace ts {
11681168
writeFile: writeFileCallback || (
11691169
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
11701170
isEmitBlocked,
1171+
readFile: f => host.readFile(f),
1172+
fileExists: f => host.fileExists(f),
1173+
...(host.directoryExists ? { directoryExists: f => host.directoryExists!(f) } : {}),
11711174
};
11721175
}
11731176

src/compiler/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"builderState.ts",
4545
"builder.ts",
4646
"resolutionCache.ts",
47+
"moduleSpecifiers.ts",
4748
"watch.ts",
4849
"commandLineParser.ts",
4950
"tsc.ts"

src/compiler/types.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5011,8 +5011,9 @@ namespace ts {
50115011
}
50125012

50135013
/* @internal */
5014-
export interface EmitHost extends ScriptReferenceHost {
5014+
export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost {
50155015
getSourceFiles(): ReadonlyArray<SourceFile>;
5016+
getCurrentDirectory(): string;
50165017

50175018
/* @internal */
50185019
isSourceFileFromExternalLibrary(file: SourceFile): boolean;
@@ -5274,11 +5275,16 @@ namespace ts {
52745275
isAtStartOfLine(): boolean;
52755276
}
52765277

5277-
/* @internal */
5278-
export interface ModuleNameResolverHost {
5279-
getCanonicalFileName(f: string): string;
5280-
getCommonSourceDirectory(): string;
5281-
getCurrentDirectory(): string;
5278+
export interface GetEffectiveTypeRootsHost {
5279+
directoryExists?(directoryName: string): boolean;
5280+
getCurrentDirectory?(): string;
5281+
}
5282+
/** @internal */
5283+
export interface ModuleSpecifierResolutionHost extends GetEffectiveTypeRootsHost {
5284+
useCaseSensitiveFileNames?(): boolean;
5285+
fileExists?(path: string): boolean;
5286+
readFile?(path: string): string | undefined;
5287+
getSourceFiles?(): ReadonlyArray<SourceFile>; // Used for cached resolutions to find symlinks without traversing the fs (again)
52825288
}
52835289

52845290
/** @deprecated See comment on SymbolWriter */
@@ -5292,7 +5298,7 @@ namespace ts {
52925298
reportPrivateInBaseOfClassExpression?(propertyName: string): void;
52935299
reportInaccessibleUniqueSymbolError?(): void;
52945300
/* @internal */
5295-
moduleResolverHost?: ModuleNameResolverHost;
5301+
moduleResolverHost?: ModuleSpecifierResolutionHost;
52965302
/* @internal */
52975303
trackReferencedAmbientModule?(decl: ModuleDeclaration): void;
52985304
}

src/compiler/utilities.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,11 +2893,11 @@ namespace ts {
28932893
};
28942894
}
28952895

2896-
export function getResolvedExternalModuleName(host: ModuleNameResolverHost, file: SourceFile, referenceFile?: SourceFile): string {
2896+
export function getResolvedExternalModuleName(host: EmitHost, file: SourceFile, referenceFile?: SourceFile): string {
28972897
return file.moduleName || getExternalModuleNameFromPath(host, file.fileName, referenceFile && referenceFile.fileName);
28982898
}
28992899

2900-
export function getExternalModuleNameFromDeclaration(host: ModuleNameResolverHost, resolver: EmitResolver, declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode): string {
2900+
export function getExternalModuleNameFromDeclaration(host: EmitHost, resolver: EmitResolver, declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode): string | undefined {
29012901
const file = resolver.getExternalModuleFileFromDeclaration(declaration);
29022902
if (!file || file.isDeclarationFile) {
29032903
return undefined;
@@ -2908,7 +2908,7 @@ namespace ts {
29082908
/**
29092909
* Resolves a local path to a path which is absolute to the base of the emit
29102910
*/
2911-
export function getExternalModuleNameFromPath(host: ModuleNameResolverHost, fileName: string, referencePath?: string): string {
2911+
export function getExternalModuleNameFromPath(host: EmitHost, fileName: string, referencePath?: string): string {
29122912
const getCanonicalFileName = (f: string) => host.getCanonicalFileName(f);
29132913
const dir = toPath(referencePath ? getDirectoryPath(referencePath) : host.getCommonSourceDirectory(), host.getCurrentDirectory(), getCanonicalFileName);
29142914
const filePath = getNormalizedAbsolutePath(fileName, host.getCurrentDirectory());

src/harness/compilerRunner.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ class CompilerTest {
187187
this.otherFiles,
188188
this.harnessSettings,
189189
/*options*/ tsConfigOptions,
190-
/*currentDirectory*/ this.harnessSettings.currentDirectory);
190+
/*currentDirectory*/ this.harnessSettings.currentDirectory,
191+
testCaseContent.symlinks
192+
);
191193

192194
this.options = this.result.options;
193195
}

0 commit comments

Comments
 (0)