• 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 getModulesPackageName(
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, compilerOptions.packageManagerType);
75        return firstDefined(modulePaths,
76            modulePath => tryGetModuleNameAsExternalModule(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,compilerOptions.packageManagerType);
88        return firstDefined(modulePaths, modulePath => tryGetModuleNameAsExternalModule(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, compilerOptions.packageManagerType);
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 or oh_modules to a package.json's or oh-package.json5's "types" entry
126        //   2. Specifiers generated using "paths" from tsconfig
127        //   3. Non-relative specfiers resulting from a path through node_modules or oh_modules(e.g. "@foo/bar/path/to/file")
128        //   4. Relative paths
129        let modulesSpecifiers: string[] | undefined;
130        let pathsSpecifiers: string[] | undefined;
131        let relativeSpecifiers: string[] | undefined;
132        for (const modulePath of modulePaths) {
133            const specifier = tryGetModuleNameAsExternalModule(modulePath, info, host, compilerOptions);
134            modulesSpecifiers = append(modulesSpecifiers, 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 modulesSpecifiers!;
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' or 'oh_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 or oh_modules, not one we can access directly.
150                    // If some path to the file was in node_modules or oh_modules but another was not, this likely indicates that
151                    // we have a monorepo structure with symlinks. In this case, the non-node_modules or oh_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 or oh_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            modulesSpecifiers?.length ? modulesSpecifiers :
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            const packageManagerType = compilerOptions.packageManagerType;
222            const nearestTargetPackageJson = getNearestAncestorDirectoryWithPackageJson(host, getDirectoryPath(modulePath), packageManagerType);
223            const nearestSourcePackageJson = getNearestAncestorDirectoryWithPackageJson(host, sourceDirectory, packageManagerType);
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, packageManagerType?: string) {
263        if (host.getNearestAncestorDirectoryWithPackageJson) {
264            return host.getNearestAncestorDirectoryWithPackageJson(fileName);
265        }
266        return !!forEachAncestorDirectory(fileName, directory => {
267            return host.fileExists(combinePaths(directory, getPackageJsonByPMType(packageManagerType))) ? 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        isOHModules?: boolean
278    ): T | undefined {
279        const getCanonicalFileName = hostGetCanonicalFileName(host);
280        const cwd = host.getCurrentDirectory();
281        const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined;
282        const importedPath = toPath(importedFileName, cwd, getCanonicalFileName);
283        const redirects = host.redirectTargetsMap.get(importedPath) || emptyArray;
284        const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];
285        const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
286        let shouldFilterIgnoredPaths = !every(targets, containsIgnoredPath);
287
288        if (!preferSymlinks) {
289            // Symlinks inside ignored paths are already filtered out of the symlink cache,
290            // so we only need to remove them from the realpath filenames.
291            const result = forEach(targets, p => !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) && cb(p, referenceRedirect === p));
292            if (result) return result;
293        }
294        const links = host.getSymlinkCache
295            ? host.getSymlinkCache()
296            : discoverProbableSymlinks(host.getSourceFiles(), getCanonicalFileName, cwd, isOHModules);
297
298        const symlinkedDirectories = links.getSymlinkedDirectoriesByRealpath();
299        const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd);
300        const result = symlinkedDirectories && forEachAncestorDirectory(getDirectoryPath(fullImportedFileName), realPathDirectory => {
301            const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName)));
302            if (!symlinkDirectories) return undefined; // Continue to ancestor directory
303
304            // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts)
305            if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) {
306                return false; // Stop search, each ancestor directory will also hit this condition
307            }
308
309            return forEach(targets, target => {
310                if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) {
311                    return;
312                }
313
314                const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName);
315                for (const symlinkDirectory of symlinkDirectories) {
316                    const option = resolvePath(symlinkDirectory, relative);
317                    const result = cb(option, target === referenceRedirect);
318                    shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths
319                    if (result) return result;
320                }
321            });
322        });
323        return result || (preferSymlinks
324            ? forEach(targets, p => shouldFilterIgnoredPaths && containsIgnoredPath(p) ? undefined : cb(p, p === referenceRedirect))
325            : undefined);
326    }
327
328    interface ModulePath {
329        path: string;
330        isInNodeModules: boolean;
331        isRedirect: boolean;
332    }
333
334    /**
335     * Looks for existing imports that use symlinks to this module.
336     * Symlinks will be returned first so they are preferred over the real path.
337     */
338    function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost, packageManagerType?: string): readonly ModulePath[] {
339        const cwd = host.getCurrentDirectory();
340        const getCanonicalFileName = hostGetCanonicalFileName(host);
341        const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>();
342        let importedFileFromNodeModules = false;
343        const isOHModules: boolean =isOhpm(packageManagerType);
344        forEachFileNameOfModule(
345            importingFileName,
346            importedFileName,
347            host,
348            /*preferSymlinks*/ true,
349            (path, isRedirect) => {
350                const isInNodeModules = isOhpm(packageManagerType) ? pathContainsOHModules(path) : pathContainsNodeModules(path);
351                allFileNames.set(path, { path: getCanonicalFileName(path), isRedirect, isInNodeModules });
352                importedFileFromNodeModules = importedFileFromNodeModules || isInNodeModules;
353                // don't return value, so we collect everything
354            },
355            isOHModules
356        );
357
358        // Sort by paths closest to importing file Name directory
359        const sortedPaths: ModulePath[] = [];
360        for (
361            let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName));
362            allFileNames.size !== 0;
363        ) {
364            const directoryStart = ensureTrailingDirectorySeparator(directory);
365            let pathsInDirectory: ModulePath[] | undefined;
366            allFileNames.forEach(({ path, isRedirect, isInNodeModules }, fileName) => {
367                if (startsWith(path, directoryStart)) {
368                    (pathsInDirectory ||= []).push({ path: fileName, isRedirect, isInNodeModules });
369                    allFileNames.delete(fileName);
370                }
371            });
372            if (pathsInDirectory) {
373                if (pathsInDirectory.length > 1) {
374                    pathsInDirectory.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);
375                }
376                sortedPaths.push(...pathsInDirectory);
377            }
378            const newDirectory = getDirectoryPath(directory);
379            if (newDirectory === directory) break;
380            directory = newDirectory;
381        }
382        if (allFileNames.size) {
383            const remainingPaths = arrayFrom(allFileNames.values());
384            if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);
385            sortedPaths.push(...remainingPaths);
386        }
387        return sortedPaths;
388    }
389
390    function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeChecker): string | undefined {
391        const decl = find(moduleSymbol.declarations,
392            d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name)))
393        ) as (ModuleDeclaration & { name: StringLiteral }) | undefined;
394        if (decl) {
395            return decl.name.text;
396        }
397
398        // the module could be a namespace, which is export through "export=" from an ambient module.
399        /**
400         * declare module "m" {
401         *     namespace ns {
402         *         class c {}
403         *     }
404         *     export = ns;
405         * }
406         */
407        // `import {c} from "m";` is valid, in which case, `moduleSymbol` is "ns", but the module name should be "m"
408        const ambientModuleDeclareCandidates = mapDefined(moduleSymbol.declarations,
409            d => {
410                if (!isModuleDeclaration(d)) return;
411                const topNamespace = getTopNamespace(d);
412                if (!(topNamespace?.parent?.parent
413                    && isModuleBlock(topNamespace.parent) && isAmbientModule(topNamespace.parent.parent) && isSourceFile(topNamespace.parent.parent.parent))) return;
414                const exportAssignment = ((topNamespace.parent.parent.symbol.exports?.get("export=" as __String)?.valueDeclaration as ExportAssignment)?.expression as PropertyAccessExpression | Identifier);
415                if (!exportAssignment) return;
416                const exportSymbol = checker.getSymbolAtLocation(exportAssignment);
417                if (!exportSymbol) return;
418                const originalExportSymbol = exportSymbol?.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(exportSymbol) : exportSymbol;
419                if (originalExportSymbol === d.symbol) return topNamespace.parent.parent;
420
421                function getTopNamespace(namespaceDeclaration: ModuleDeclaration) {
422                    while (namespaceDeclaration.flags & NodeFlags.NestedNamespace) {
423                        namespaceDeclaration = namespaceDeclaration.parent as ModuleDeclaration;
424                    }
425                    return namespaceDeclaration;
426                }
427            }
428        );
429        const ambientModuleDeclare = ambientModuleDeclareCandidates[0] as (AmbientModuleDeclaration & { name: StringLiteral }) | undefined;
430        if (ambientModuleDeclare) {
431            return ambientModuleDeclare.name.text;
432        }
433    }
434
435    function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<readonly string[]>): string | undefined {
436        for (const key in paths) {
437            for (const patternText of paths[key]) {
438                const pattern = removeFileExtension(normalizePath(patternText));
439                const indexOfStar = pattern.indexOf("*");
440                if (indexOfStar !== -1) {
441                    const prefix = pattern.substr(0, indexOfStar);
442                    const suffix = pattern.substr(indexOfStar + 1);
443                    if (relativeToBaseUrl.length >= prefix.length + suffix.length &&
444                        startsWith(relativeToBaseUrl, prefix) &&
445                        endsWith(relativeToBaseUrl, suffix) ||
446                        !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) {
447                        const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length);
448                        return key.replace("*", matchedStar);
449                    }
450                }
451                else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) {
452                    return key;
453                }
454            }
455        }
456    }
457
458    function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined {
459        const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName);
460        if (normalizedTargetPath === undefined) {
461            return undefined;
462        }
463
464        const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
465        const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath;
466        return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs
467            ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions)
468            : removeFileExtension(relativePath);
469    }
470
471    function tryGetModuleNameAsExternalModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {
472        if (!host.fileExists || !host.readFile) {
473            return undefined;
474        }
475        const parts: ModulePathParts = getModulePathParts(path, isOhpm(options.packageManagerType) ? ohModulesPathPart : nodeModulesPathPart)!;
476        if (!parts) {
477            return undefined;
478        }
479
480        // Simplify the full file path to something that can be resolved by Node.
481
482        let moduleSpecifier = path;
483        let isPackageRootPath = false;
484        if (!packageNameOnly) {
485            let packageRootIndex = parts.packageRootIndex;
486            let moduleFileNameForExtensionless: string | undefined;
487            while (true) {
488                // If the module could be imported by a directory name, use that directory's name
489                const { moduleFileToTry, packageRootPath } = tryDirectoryWithPackageJson(packageRootIndex);
490                if (packageRootPath) {
491                    moduleSpecifier = packageRootPath;
492                    isPackageRootPath = true;
493                    break;
494                }
495                if (!moduleFileNameForExtensionless) moduleFileNameForExtensionless = moduleFileToTry;
496
497                // try with next level of directory
498                packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1);
499                if (packageRootIndex === -1) {
500                    moduleSpecifier = getExtensionlessFileName(moduleFileNameForExtensionless);
501                    break;
502                }
503            }
504        }
505
506        if (isRedirect && !isPackageRootPath) {
507            return undefined;
508        }
509
510        const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation();
511        // Get a path that's relative to node_modules, oh_modules or the importing file's path
512        // if node_modules or oh_modules folder is in this folder or any of its parent folders, no need to keep it.
513        const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex));
514        if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) {
515            return undefined;
516        }
517
518        // If the module was found in @types, get the actual Node package name
519        const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1);
520        const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
521        // For classic resolution, only allow importing from or oh_modules/@types, not other node_modules or oh_modules
522        return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName;
523
524        function tryDirectoryWithPackageJson(packageRootIndex: number) {
525            const packageRootPath = path.substring(0, packageRootIndex);
526            const packageJsonPath = combinePaths(packageRootPath, getPackageJsonByPMType(options.packageManagerType));
527            let moduleFileToTry = path;
528            if (host.fileExists(packageJsonPath)) {
529                const isOHModules: boolean = isOhpm(options.packageManagerType);
530                const packageJsonContent = isOHModules ? JSON5.parse(host.readFile!(packageJsonPath)!) : JSON.parse(host.readFile!(packageJsonPath)!);
531                const versionPaths = packageJsonContent.typesVersions
532                    ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
533                    : undefined;
534                if (versionPaths) {
535                    const subModuleName = path.slice(packageRootPath.length + 1);
536                    const fromPaths = tryGetModuleNameFromPaths(
537                        removeFileExtension(subModuleName),
538                        removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),
539                        versionPaths.paths
540                    );
541                    if (fromPaths !== undefined) {
542                        moduleFileToTry = combinePaths(packageRootPath, fromPaths);
543                    }
544                }
545
546                // If the file is the main module, it can be imported by the package name
547                const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
548                if (isString(mainFileRelative)) {
549                    const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName);
550                    if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(moduleFileToTry))) {
551                        return { packageRootPath, moduleFileToTry };
552                    }
553                }
554            }
555            return { moduleFileToTry };
556        }
557
558        function getExtensionlessFileName(path: string): string {
559            // We still have a file name - remove the extension
560            const fullModulePathWithoutExtension = removeFileExtension(path);
561
562            // If the file is /index, it can be imported by its directory name
563            // IFF there is not _also_ a file by the same name
564            if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) {
565                return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
566            }
567
568            return fullModulePathWithoutExtension;
569        }
570    }
571
572    function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) {
573        if (!host.fileExists) return;
574        // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
575        const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]);
576        for (const e of extensions) {
577            const fullPath = path + e;
578            if (host.fileExists(fullPath)) {
579                return fullPath;
580            }
581        }
582    }
583
584    interface ModulePathParts {
585        readonly topLevelNodeModulesIndex: number;
586        readonly topLevelPackageNameIndex: number;
587        readonly packageRootIndex: number;
588        readonly fileNameIndex: number;
589    }
590    function getModulePathParts(fullPath: string, modulesPathPart: string): ModulePathParts | undefined {
591        // If fullPath can't be valid module file within node_modules or oh_modules, returns undefined.
592        // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js
593        // Returns indices:                       ^            ^                                                      ^             ^
594
595        let topLevelNodeModulesIndex = 0;
596        let topLevelPackageNameIndex = 0;
597        let packageRootIndex = 0;
598        let fileNameIndex = 0;
599
600        const enum States {
601            BeforeNodeModules,
602            NodeModules,
603            Scope,
604            PackageContent
605        }
606
607        let partStart = 0;
608        let partEnd = 0;
609        let state = States.BeforeNodeModules;
610
611        while (partEnd >= 0) {
612            partStart = partEnd;
613            partEnd = fullPath.indexOf("/", partStart + 1);
614            switch (state) {
615                case States.BeforeNodeModules:
616                    if (fullPath.indexOf(modulesPathPart, partStart) === partStart) {
617                        topLevelNodeModulesIndex = partStart;
618                        topLevelPackageNameIndex = partEnd;
619                        state = States.NodeModules;
620                    }
621                    break;
622                case States.NodeModules:
623                case States.Scope:
624                    if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") {
625                        state = States.Scope;
626                    }
627                    else {
628                        packageRootIndex = partEnd;
629                        state = States.PackageContent;
630                    }
631                    break;
632                case States.PackageContent:
633                    if (fullPath.indexOf(modulesPathPart, partStart) === partStart) {
634                        state = States.NodeModules;
635                    }
636                    else {
637                        state = States.PackageContent;
638                    }
639                    break;
640            }
641        }
642
643        fileNameIndex = partStart;
644
645        return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined;
646    }
647
648    function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined {
649        return firstDefined(rootDirs, rootDir => {
650            const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217
651            return isPathRelativeToParent(relativePath) ? undefined : relativePath;
652        });
653    }
654
655    function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string {
656        if (fileExtensionIs(fileName, Extension.Json)) return fileName;
657        const noExtension = removeFileExtension(fileName);
658        switch (ending) {
659            case Ending.Minimal:
660                return removeSuffix(noExtension, "/index");
661            case Ending.Index:
662                return noExtension;
663            case Ending.JsExtension:
664                return noExtension + getJSExtensionForFile(fileName, options);
665            default:
666                return Debug.assertNever(ending);
667        }
668    }
669
670    function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension {
671        const ext = extensionFromPath(fileName);
672        switch (ext) {
673            case Extension.Ts:
674            case Extension.Dts:
675            case Extension.Ets:
676            case Extension.Dets:
677                return Extension.Js;
678            case Extension.Tsx:
679                return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js;
680            case Extension.Js:
681            case Extension.Jsx:
682            case Extension.Json:
683                return ext;
684            case Extension.TsBuildInfo:
685                return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`);
686            default:
687                return Debug.assertNever(ext);
688        }
689    }
690
691    function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined {
692        const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
693        return isRootedDiskPath(relativePath) ? undefined : relativePath;
694    }
695
696    function isPathRelativeToParent(path: string): boolean {
697        return startsWith(path, "..");
698    }
699}
700