• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
2/* @internal */
3namespace ts.moduleSpecifiers {
4    const enum RelativePreference { Relative, NonRelative, Shortest, ExternalNonRelative }
5    // See UserPreferences#importPathEnding
6    const enum Ending { Minimal, Index, JsExtension }
7
8    // Processed preferences
9    interface Preferences {
10        readonly relativePreference: RelativePreference;
11        readonly ending: Ending;
12    }
13
14    function getPreferences({ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences {
15        return {
16            relativePreference:
17                importModuleSpecifierPreference === "relative" ? RelativePreference.Relative :
18                importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative :
19                importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative :
20                RelativePreference.Shortest,
21            ending: getEnding(),
22        };
23        function getEnding(): Ending {
24            switch (importModuleSpecifierEnding) {
25                case "minimal": return Ending.Minimal;
26                case "index": return Ending.Index;
27                case "js": return Ending.JsExtension;
28                default: return usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension
29                    : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal;
30            }
31        }
32    }
33
34    function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences {
35        return {
36            relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative,
37            ending: hasJSFileExtension(oldImportSpecifier) ?
38                Ending.JsExtension :
39                getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal,
40        };
41    }
42
43    export function updateModuleSpecifier(
44        compilerOptions: CompilerOptions,
45        importingSourceFileName: Path,
46        toFileName: string,
47        host: ModuleSpecifierResolutionHost,
48        oldImportSpecifier: string,
49    ): string | undefined {
50        const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier));
51        if (res === oldImportSpecifier) return undefined;
52        return res;
53    }
54
55    // Note: importingSourceFile is just for usesJsExtensionOnImports
56    export function getModuleSpecifier(
57        compilerOptions: CompilerOptions,
58        importingSourceFile: SourceFile,
59        importingSourceFileName: Path,
60        toFileName: string,
61        host: ModuleSpecifierResolutionHost,
62        preferences: UserPreferences = {},
63    ): string {
64        return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferences(preferences, compilerOptions, importingSourceFile));
65    }
66
67    export function getNodeModulesPackageName(
68        compilerOptions: CompilerOptions,
69        importingSourceFileName: Path,
70        nodeModulesFileName: string,
71        host: ModuleSpecifierResolutionHost,
72    ): string | undefined {
73        const info = getInfo(importingSourceFileName, host);
74        const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host);
75        return firstDefined(modulePaths,
76            modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true));
77    }
78
79    function getModuleSpecifierWorker(
80        compilerOptions: CompilerOptions,
81        importingSourceFileName: Path,
82        toFileName: string,
83        host: ModuleSpecifierResolutionHost,
84        preferences: Preferences
85    ): string {
86        const info = getInfo(importingSourceFileName, host);
87        const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host);
88        return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions)) ||
89            getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences);
90    }
91
92    /** Returns an import for each symlink and for the realpath. */
93    export function getModuleSpecifiers(
94        moduleSymbol: Symbol,
95        checker: TypeChecker,
96        compilerOptions: CompilerOptions,
97        importingSourceFile: SourceFile,
98        host: ModuleSpecifierResolutionHost,
99        userPreferences: UserPreferences,
100    ): readonly string[] {
101        const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
102        if (ambient) return [ambient];
103
104        const info = getInfo(importingSourceFile.path, host);
105        const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol));
106        const modulePaths = getAllModulePaths(importingSourceFile.path, moduleSourceFile.originalFileName, host);
107        const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);
108
109        const existingSpecifier = forEach(modulePaths, modulePath => forEach(
110            host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)),
111            reason => {
112                if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined;
113                const specifier = getModuleNameStringLiteralAt(importingSourceFile, reason.index).text;
114                // If the preference is for non relative and the module specifier is relative, ignore it
115                return preferences.relativePreference !== RelativePreference.NonRelative || !pathIsRelative(specifier) ?
116                    specifier :
117                    undefined;
118            }
119        ));
120        if (existingSpecifier) return [existingSpecifier];
121
122        const importedFileIsInNodeModules = some(modulePaths, p => p.isInNodeModules);
123
124        // Module specifier priority:
125        //   1. "Bare package specifiers" (e.g. "@foo/bar") resulting from a path through node_modules to a package.json's "types" entry
126        //   2. Specifiers generated using "paths" from tsconfig
127        //   3. Non-relative specfiers resulting from a path through node_modules (e.g. "@foo/bar/path/to/file")
128        //   4. Relative paths
129        let nodeModulesSpecifiers: string[] | undefined;
130        let pathsSpecifiers: string[] | undefined;
131        let relativeSpecifiers: string[] | undefined;
132        for (const modulePath of modulePaths) {
133            const specifier = tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions);
134            nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier);
135            if (specifier && modulePath.isRedirect) {
136                // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
137                // not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.
138                return nodeModulesSpecifiers!;
139            }
140
141            if (!specifier && !modulePath.isRedirect) {
142                const local = getLocalModuleSpecifier(modulePath.path, info, compilerOptions, host, preferences);
143                if (pathIsBareSpecifier(local)) {
144                    pathsSpecifiers = append(pathsSpecifiers, local);
145                }
146                else if (!importedFileIsInNodeModules || modulePath.isInNodeModules) {
147                    // Why this extra conditional, not just an `else`? If some path to the file contained
148                    // 'node_modules', but we can't create a non-relative specifier (e.g. "@foo/bar/path/to/file"),
149                    // that means we had to go through a *sibling's* node_modules, not one we can access directly.
150                    // If some path to the file was in node_modules but another was not, this likely indicates that
151                    // we have a monorepo structure with symlinks. In this case, the non-node_modules path is
152                    // probably the realpath, e.g. "../bar/path/to/file", but a relative path to another package
153                    // in a monorepo is probably not portable. So, the module specifier we actually go with will be
154                    // the relative path through node_modules, so that the declaration emitter can produce a
155                    // portability error. (See declarationEmitReexportedSymlinkReference3)
156                    relativeSpecifiers = append(relativeSpecifiers, local);
157                }
158            }
159        }
160
161        return pathsSpecifiers?.length ? pathsSpecifiers :
162            nodeModulesSpecifiers?.length ? nodeModulesSpecifiers :
163            Debug.checkDefined(relativeSpecifiers);
164    }
165
166    interface Info {
167        readonly getCanonicalFileName: GetCanonicalFileName;
168        readonly importingSourceFileName: Path
169        readonly sourceDirectory: Path;
170    }
171    // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
172    function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info {
173        const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true);
174        const sourceDirectory = getDirectoryPath(importingSourceFileName);
175        return { getCanonicalFileName, importingSourceFileName, sourceDirectory };
176    }
177
178    function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, { ending, relativePreference }: Preferences): string {
179        const { baseUrl, paths, rootDirs } = compilerOptions;
180        const { sourceDirectory, getCanonicalFileName } = info;
181
182        const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) ||
183            removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions);
184        if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) {
185            return relativePath;
186        }
187
188        const baseDirectory = getPathsBasePath(compilerOptions, host) || baseUrl!;
189        const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseDirectory, getCanonicalFileName);
190        if (!relativeToBaseUrl) {
191            return relativePath;
192        }
193
194        const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions);
195        const fromPaths = paths && tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths);
196        const nonRelative = fromPaths === undefined && baseUrl !== undefined ? importRelativeToBaseUrl : fromPaths;
197        if (!nonRelative) {
198            return relativePath;
199        }
200
201        if (relativePreference === RelativePreference.NonRelative) {
202            return nonRelative;
203        }
204
205        if (relativePreference === RelativePreference.ExternalNonRelative) {
206            const projectDirectory = host.getCurrentDirectory();
207            const modulePath = toPath(moduleFileName, projectDirectory, getCanonicalFileName);
208            const sourceIsInternal = startsWith(sourceDirectory, projectDirectory);
209            const targetIsInternal = startsWith(modulePath, projectDirectory);
210            if (sourceIsInternal && !targetIsInternal || !sourceIsInternal && targetIsInternal) {
211                // 1. The import path crosses the boundary of the tsconfig.json-containing directory.
212                //
213                //      src/
214                //        tsconfig.json
215                //        index.ts -------
216                //      lib/              | (path crosses tsconfig.json)
217                //        imported.ts <---
218                //
219                return nonRelative;
220            }
221
222            const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, getDirectoryPath(modulePath));
223            const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory);
224            if (nearestSourcePackageJson !== nearestTargetPackageJson) {
225                // 2. The importing and imported files are part of different packages.
226                //
227                //      packages/a/
228                //        package.json
229                //        index.ts --------
230                //      packages/b/        | (path crosses package.json)
231                //        package.json     |
232                //        component.ts <---
233                //
234                return nonRelative;
235            }
236
237            return relativePath;
238        }
239
240        if (relativePreference !== RelativePreference.Shortest) Debug.assertNever(relativePreference);
241
242        // Prefer a relative import over a baseUrl import if it has fewer components.
243        return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative;
244    }
245
246    export function countPathComponents(path: string): number {
247        let count = 0;
248        for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) {
249            if (path.charCodeAt(i) === CharacterCodes.slash) count++;
250        }
251        return count;
252    }
253
254    function usesJsExtensionOnImports({ imports }: SourceFile): boolean {
255        return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false;
256    }
257
258    function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) {
259        return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path);
260    }
261
262    function getNearestAncestorDirectoryWithPackageJson(host: ModuleSpecifierResolutionHost, fileName: string) {
263        if (host.getNearestAncestorDirectoryWithPackageJson) {
264            return host.getNearestAncestorDirectoryWithPackageJson(fileName);
265        }
266        return !!forEachAncestorDirectory(fileName, directory => {
267            return host.fileExists(combinePaths(directory, "package.json")) ? true : undefined;
268        });
269    }
270
271    export function forEachFileNameOfModule<T>(
272        importingFileName: string,
273        importedFileName: string,
274        host: ModuleSpecifierResolutionHost,
275        preferSymlinks: boolean,
276        cb: (fileName: string, isRedirect: boolean) => T | undefined
277    ): T | undefined {
278        const getCanonicalFileName = hostGetCanonicalFileName(host);
279        const cwd = host.getCurrentDirectory();
280        const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined;
281        const importedPath = toPath(importedFileName, cwd, getCanonicalFileName);
282        const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray;
283        const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];
284        const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
285        let shouldFilterIgnoredPaths = !every(targets, containsIgnoredPath);
286
287        if (!preferSymlinks) {
288            // Symlinks inside ignored paths are already filtered out of the symlink cache,
289            // so we only need to remove them from the realpath filenames.
290            const result = forEach(targets, p => !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) && cb(p, referenceRedirect === p));
291            if (result) return result;
292        }
293        const links = host.getSymlinkCache
294            ? host.getSymlinkCache()
295            : discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd);
296
297        const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath();
298        const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd);
299        const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => {
300            const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName)));
301            if (!symlinkDirectories) return undefined; // Continue to ancestor directory
302
303            // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts)
304            if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) {
305                return false; // Stop search, each ancestor directory will also hit this condition
306            }
307
308            return forEach(targets, target => {
309                if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) {
310                    return;
311                }
312
313                const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName);
314                for (const symlinkDirectory of symlinkDirectories) {
315                    const option = resolvePath(symlinkDirectory, relative);
316                    const result = cb(option, target === referenceRedirect);
317                    shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths
318                    if (result) return result;
319                }
320            });
321        });
322        return result || (preferSymlinks
323            ? forEach(targets, p => shouldFilterIgnoredPaths && containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect))
324            : undefined);
325    }
326
327    interface ModulePath {
328        path: string;
329        isInNodeModules: boolean;
330        isRedirect: boolean;
331    }
332
333    /**
334     * Looks for existing imports that use symlinks to this module.
335     * Symlinks will be returned first so they are preferred over the real path.
336     */
337    function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] {
338        const cwd = host.getCurrentDirectory();
339        const getCanonicalFileName = hostGetCanonicalFileName(host);
340        const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>();
341        let importedFileFromNodeModules = false;
342        forEachFileNameOfModule(
343            importingFileName,
344            importedFileName,
345            host,
346            /*preferSymlinks*/ true,
347            (path, isRedirect) => {
348                const isInNodeModules = pathContainsNodeModules(path);
349                allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules });
350                importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
351                // don't return value, so we collect everything
352            }
353        );
354
355        // Sort by paths closest to importing file Name directory
356        const sortedPaths: ModulePath[] = [];
357        for (
358            let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName));
359            allFileNames.size !== 0;
360        ) {
361            const directoryStart = ensureTrailingDirectorySeparator(directory);
362            let pathsInDirectory: ModulePath[] | undefined;
363            allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => {
364                if (startsWith(path, directoryStart)) {
365                    (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules });
366                    allFileNames.delete(fileName);
367                }
368            });
369            if (pathsInDirectory) {
370                if (pathsInDirectory.length > 1) {
371                    pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);
372                }
373                sortedPaths.push(...pathsInDirectory);
374            }
375            const newDirectory = getDirectoryPath(directory);
376            if (newDirectory === directory) break;
377            directory = newDirectory;
378        }
379        if (allFileNames.size) {
380            const remainingPaths = arrayFrom(allFileNames.values());
381            if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);
382            sortedPaths.push(...remainingPaths);
383        }
384        return sortedPaths;
385    }
386
387    function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeChecker): string | undefined {
388        const decl = find(moduleSymbol.declarations,
389            d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name)))
390        ) as (ModuleDeclaration & { name: StringLiteral }) | undefined;
391        if (decl) {
392            return decl.name.text;
393        }
394
395        // the module could be a namespace, which is export through "export=" from an ambient module.
396        /**
397         * declare module "m" {
398         *     namespace ns {
399         *         class c {}
400         *     }
401         *     export = ns;
402         * }
403         */
404        // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m"
405        const ambientModuleDeclareCandidates = mapDefined(moduleSymbol.declarations,
406            d => {
407                if (!isModuleDeclaration(d)) return;
408                const topNamespace = getTopNamespace(d);
409                if (!(topNamespace?.parent?.parent
410                    && isModuleBlock(topNamespace.parent) && isAmbientModule(topNamespace.parent.parent) && isSourceFile(topNamespace.parent.parent.parent))) return;
411                const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as __String)?.valueDeclaration as ExportAssignment)?.expression as PropertyAccessExpression | Identifier);
412                if (!exportAssignment) return;
413                const exportSymbol = checker.getSymbolAtLocation(exportAssignment);
414                if (!exportSymbol) return;
415                const originalExportSymbol = exportSymbol?.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol;
416                if (originalExportSymbol === d.symbol) return topNamespace.parent.parent;
417
418                function getTopNamespace(namespaceDeclaration: ModuleDeclaration) {
419                    while (namespaceDeclaration.flags & NodeFlags.NestedNamespace) {
420                        namespaceDeclaration = namespaceDeclaration.parent as ModuleDeclaration;
421                    }
422                    return namespaceDeclaration;
423                }
424            }
425        );
426        const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (AmbientModuleDeclaration & { name: StringLiteral }) | undefined;
427        if (ambientModuleDeclare) {
428            return ambientModuleDeclare.name.text;
429        }
430    }
431
432    function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<readonly string[]>): string | undefined {
433        for (const key in paths) {
434            for (const patternText of paths[key]) {
435                const pattern = removeFileExtension(normalizePath(patternText));
436                const indexOfStar = pattern.indexOf("*");
437                if (indexOfStar !== -1) {
438                    const prefix = pattern.substr(0, indexOfStar);
439                    const suffix = pattern.substr(indexOfStar + 1);
440                    if (relativeToBaseUrl.length >= prefix.length + suffix.length &&
441                        startsWith(relativeToBaseUrl, prefix) &&
442                        endsWith(relativeToBaseUrl, suffix) ||
443                        !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) {
444                        const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length);
445                        return key.replace("*", matchedStar);
446                    }
447                }
448                else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) {
449                    return key;
450                }
451            }
452        }
453    }
454
455    function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined {
456        const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName);
457        if (normalizedTargetPath === undefined) {
458            return undefined;
459        }
460
461        const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
462        const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath;
463        return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs
464            ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions)
465            : removeFileExtension(relativePath);
466    }
467
468    function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {
469        if (!host.fileExists || !host.readFile) {
470            return undefined;
471        }
472        const parts: NodeModulePathParts = getNodeModulePathParts(path)!;
473        if (!parts) {
474            return undefined;
475        }
476
477        // Simplify the full file path to something that can be resolved by Node.
478
479        let moduleSpecifier = path;
480        let isPackageRootPath = false;
481        if (!packageNameOnly) {
482            let packageRootIndex = parts.packageRootIndex;
483            let moduleFileNameForExtensionless: string | undefined;
484            while (true) {
485                // If the module could be imported by a directory name, use that directory's name
486                const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex);
487                if (packageRootPath) {
488                    moduleSpecifier = packageRootPath;
489                    isPackageRootPath = true;
490                    break;
491                }
492                if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry;
493
494                // try with next level of directory
495                packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1);
496                if (packageRootIndex === -1) {
497                    moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless);
498                    break;
499                }
500            }
501        }
502
503        if (isRedirect && !isPackageRootPath) {
504            return undefined;
505        }
506
507        const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
508        // Get a path that's relative to node_modules or the importing file's path
509        // if node_modules folder is in this folder or any of its parent folders, no need to keep it.
510        const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
511        if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
512            return undefined;
513        }
514
515        // If the module was found in @types, get the actual Node package name
516        const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
517        const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
518        // For classic resolution, only allow importing from node_modules/@types, not other node_modules
519        return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName;
520
521        function tryDirectoryWithPackageJson(packageRootIndex: number) {
522            const packageRootPath = path.substring(0, packageRootIndex);
523            const packageJsonPath = combinePaths(packageRootPath, "package.json");
524            let moduleFileToTry = path;
525            if (host.fileExists(packageJsonPath)) {
526                const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!);
527                const versionPaths = packageJsonContent.typesVersions
528                    ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
529                    : undefined;
530                if (versionPaths) {
531                    const subModuleName = path.slice(packageRootPath.length + 1);
532                    const fromPaths = tryGetModuleNameFromPaths(
533                        removeFileExtension(subModuleName),
534                        removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),
535                        versionPaths.paths
536                    );
537                    if (fromPaths !== undefined) {
538                        moduleFileToTry = combinePaths(packageRootPath, fromPaths);
539                    }
540                }
541
542                // If the file is the main module, it can be imported by the package name
543                const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
544                if (isString(mainFileRelative)) {
545                    const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName);
546                    if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) {
547                        return { packageRootPath, moduleFileToTry };
548                    }
549                }
550            }
551            return { moduleFileToTry };
552        }
553
554        function getExtensionlessFileName(path: string): string {
555            // We still have a file name - remove the extension
556            const fullModulePathWithoutExtension = removeFileExtension(path);
557
558            // If the file is /index, it can be imported by its directory name
559            // IFF there is not _also_ a file by the same name
560            if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) {
561                return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
562            }
563
564            return fullModulePathWithoutExtension;
565        }
566    }
567
568    function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) {
569        if (!host.fileExists) return;
570        // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
571        const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]);
572        for (const e of extensions) {
573            const fullPath = path + e;
574            if (host.fileExists(fullPath)) {
575                return fullPath;
576            }
577        }
578    }
579
580    interface NodeModulePathParts {
581        readonly topLevelNodeModulesIndex: number;
582        readonly topLevelPackageNameIndex: number;
583        readonly packageRootIndex: number;
584        readonly fileNameIndex: number;
585    }
586    function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined {
587        // If fullPath can't be valid module file within node_modules, returns undefined.
588        // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js
589        // Returns indices:                       ^            ^                                                      ^             ^
590
591        let topLevelNodeModulesIndex = 0;
592        let topLevelPackageNameIndex = 0;
593        let packageRootIndex = 0;
594        let fileNameIndex = 0;
595
596        const enum States {
597            BeforeNodeModules,
598            NodeModules,
599            Scope,
600            PackageContent
601        }
602
603        let partStart = 0;
604        let partEnd = 0;
605        let state = States.BeforeNodeModules;
606
607        while (partEnd >= 0) {
608            partStart = partEnd;
609            partEnd = fullPath.indexOf("/", partStart + 1);
610            switch (state) {
611                case States.BeforeNodeModules:
612                    if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) {
613                        topLevelNodeModulesIndex = partStart;
614                        topLevelPackageNameIndex = partEnd;
615                        state = States.NodeModules;
616                    }
617                    break;
618                case States.NodeModules:
619                case States.Scope:
620                    if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") {
621                        state = States.Scope;
622                    }
623                    else {
624                        packageRootIndex = partEnd;
625                        state = States.PackageContent;
626                    }
627                    break;
628                case States.PackageContent:
629                    if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) {
630                        state = States.NodeModules;
631                    }
632                    else {
633                        state = States.PackageContent;
634                    }
635                    break;
636            }
637        }
638
639        fileNameIndex = partStart;
640
641        return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined;
642    }
643
644    function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined {
645        return firstDefined(rootDirs, rootDir => {
646            const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217
647            return isPathRelativeToParent(relativePath) ? undefined : relativePath;
648        });
649    }
650
651    function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string {
652        if (fileExtensionIs(fileName, Extension.Json)) return fileName;
653        const noExtension = removeFileExtension(fileName);
654        switch (ending) {
655            case Ending.Minimal:
656                return removeSuffix(noExtension, "/index");
657            case Ending.Index:
658                return noExtension;
659            case Ending.JsExtension:
660                return noExtension + getJSExtensionForFile(fileName, options);
661            default:
662                return Debug.assertNever(ending);
663        }
664    }
665
666    function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension {
667        const ext = extensionFromPath(fileName);
668        switch (ext) {
669            case Extension.Ts:
670            case Extension.Dts:
671            case Extension.Ets:
672                return Extension.Js;
673            case Extension.Tsx:
674                return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js;
675            case Extension.Js:
676            case Extension.Jsx:
677            case Extension.Json:
678                return ext;
679            case Extension.TsBuildInfo:
680                return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`);
681            default:
682                return Debug.assertNever(ext);
683        }
684    }
685
686    function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined {
687        const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
688        return isRootedDiskPath(relativePath) ? undefined : relativePath;
689    }
690
691    function isPathRelativeToParent(path: string): boolean {
692        return startsWith(path, "..");
693    }
694}
695