7fc1cb4b36
* Make getSourceOfProjectReferenceRedirect take a Path * Add useCaseSensitiveFileNames to ModuleResolutionHost ...so that path comparisons can use it during module resolution. * Re-enable realpathSync.native for case-insensitive file systems
1721 lines
94 KiB
TypeScript
1721 lines
94 KiB
TypeScript
namespace ts {
|
|
/* @internal */
|
|
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void;
|
|
export function trace(host: ModuleResolutionHost): void {
|
|
host.trace!(formatMessage.apply(undefined, arguments));
|
|
}
|
|
|
|
/* @internal */
|
|
export function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean {
|
|
return !!compilerOptions.traceResolution && host.trace !== undefined;
|
|
}
|
|
|
|
function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExtension | undefined): Resolved | undefined {
|
|
let packageId: PackageId | undefined;
|
|
if (r && packageInfo) {
|
|
const packageJsonContent = packageInfo.packageJsonContent as PackageJson;
|
|
if (typeof packageJsonContent.name === "string" && typeof packageJsonContent.version === "string") {
|
|
packageId = {
|
|
name: packageJsonContent.name,
|
|
subModuleName: r.path.slice(packageInfo.packageDirectory.length + directorySeparator.length),
|
|
version: packageJsonContent.version
|
|
};
|
|
}
|
|
}
|
|
return r && { path: r.path, extension: r.ext, packageId };
|
|
}
|
|
|
|
function noPackageId(r: PathAndExtension | undefined): Resolved | undefined {
|
|
return withPackageId(/*packageInfo*/ undefined, r);
|
|
}
|
|
|
|
function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined {
|
|
if (r) {
|
|
Debug.assert(r.packageId === undefined);
|
|
return { path: r.path, ext: r.extension };
|
|
}
|
|
}
|
|
|
|
/** Result of trying to resolve a module. */
|
|
interface Resolved {
|
|
path: string;
|
|
extension: Extension;
|
|
packageId: PackageId | undefined;
|
|
/**
|
|
* When the resolved is not created from cache, the value is
|
|
* - string if it is symbolic link to the resolved `path`
|
|
* - undefined if `path` is not a symbolic link
|
|
* When the resolved is created using value from cache of ResolvedModuleWithFailedLookupLocations, the value is:
|
|
* - string if it is symbolic link to the resolved `path`
|
|
* - true if `path` is not a symbolic link - this indicates that the `originalPath` calculation is already done and needs to be skipped
|
|
* Note: This is a file name with preserved original casing, not a normalized `Path`.
|
|
*/
|
|
originalPath?: string | true;
|
|
}
|
|
|
|
/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */
|
|
interface PathAndExtension {
|
|
path: string;
|
|
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
|
|
ext: Extension;
|
|
}
|
|
|
|
/**
|
|
* Kinds of file that we are currently looking for.
|
|
* Typically there is one pass with Extensions.TypeScript, then a second pass with Extensions.JavaScript.
|
|
*/
|
|
enum Extensions {
|
|
TypeScript, /** '.ts', '.tsx', or '.d.ts' */
|
|
JavaScript, /** '.js' or '.jsx' */
|
|
Json, /** '.json' */
|
|
TSConfig, /** '.json' with `tsconfig` used instead of `index` */
|
|
DtsOnly /** Only '.d.ts' */
|
|
}
|
|
|
|
interface PathAndPackageId {
|
|
readonly fileName: string;
|
|
readonly packageId: PackageId | undefined;
|
|
}
|
|
/** Used with `Extensions.DtsOnly` to extract the path from TypeScript results. */
|
|
function resolvedTypeScriptOnly(resolved: Resolved | undefined): PathAndPackageId | undefined {
|
|
if (!resolved) {
|
|
return undefined;
|
|
}
|
|
Debug.assert(extensionIsTS(resolved.extension));
|
|
return { fileName: resolved.path, packageId: resolved.packageId };
|
|
}
|
|
|
|
function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean | undefined, failedLookupLocations: string[], resultFromCache: ResolvedModuleWithFailedLookupLocations | undefined): ResolvedModuleWithFailedLookupLocations {
|
|
if (resultFromCache) {
|
|
resultFromCache.failedLookupLocations.push(...failedLookupLocations);
|
|
return resultFromCache;
|
|
}
|
|
return {
|
|
resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId },
|
|
failedLookupLocations
|
|
};
|
|
}
|
|
|
|
interface ModuleResolutionState {
|
|
host: ModuleResolutionHost;
|
|
compilerOptions: CompilerOptions;
|
|
traceEnabled: boolean;
|
|
failedLookupLocations: Push<string>;
|
|
resultFromCache?: ResolvedModuleWithFailedLookupLocations;
|
|
packageJsonInfoCache: PackageJsonInfoCache | undefined;
|
|
}
|
|
|
|
/** Just the fields that we use for module resolution. */
|
|
/*@internal*/
|
|
interface PackageJsonPathFields {
|
|
typings?: string;
|
|
types?: string;
|
|
typesVersions?: MapLike<MapLike<string[]>>;
|
|
main?: string;
|
|
tsconfig?: string;
|
|
}
|
|
|
|
interface PackageJson extends PackageJsonPathFields {
|
|
name?: string;
|
|
version?: string;
|
|
}
|
|
|
|
function readPackageJsonField<TMatch, K extends MatchingKeys<PackageJson, string | undefined>>(jsonContent: PackageJson, fieldName: K, typeOfTag: "string", state: ModuleResolutionState): PackageJson[K] | undefined;
|
|
function readPackageJsonField<K extends MatchingKeys<PackageJson, object | undefined>>(jsonContent: PackageJson, fieldName: K, typeOfTag: "object", state: ModuleResolutionState): PackageJson[K] | undefined;
|
|
function readPackageJsonField<K extends keyof PackageJson>(jsonContent: PackageJson, fieldName: K, typeOfTag: "string" | "object", state: ModuleResolutionState): PackageJson[K] | undefined {
|
|
if (!hasProperty(jsonContent, fieldName)) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_does_not_have_a_0_field, fieldName);
|
|
}
|
|
return;
|
|
}
|
|
const value = jsonContent[fieldName];
|
|
if (typeof value !== typeOfTag || value === null) { // eslint-disable-line no-null/no-null
|
|
if (state.traceEnabled) {
|
|
// eslint-disable-next-line no-null/no-null
|
|
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, fieldName, typeOfTag, value === null ? "null" : typeof value);
|
|
}
|
|
return;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function readPackageJsonPathField<K extends "typings" | "types" | "main" | "tsconfig">(jsonContent: PackageJson, fieldName: K, baseDirectory: string, state: ModuleResolutionState): PackageJson[K] | undefined {
|
|
const fileName = readPackageJsonField(jsonContent, fieldName, "string", state);
|
|
if (fileName === undefined) {
|
|
return;
|
|
}
|
|
if (!fileName) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_had_a_falsy_0_field, fieldName);
|
|
}
|
|
return;
|
|
}
|
|
const path = normalizePath(combinePaths(baseDirectory, fileName));
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
function readPackageJsonTypesFields(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) {
|
|
return readPackageJsonPathField(jsonContent, "typings", baseDirectory, state)
|
|
|| readPackageJsonPathField(jsonContent, "types", baseDirectory, state);
|
|
}
|
|
|
|
function readPackageJsonTSConfigField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) {
|
|
return readPackageJsonPathField(jsonContent, "tsconfig", baseDirectory, state);
|
|
}
|
|
|
|
function readPackageJsonMainField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) {
|
|
return readPackageJsonPathField(jsonContent, "main", baseDirectory, state);
|
|
}
|
|
|
|
function readPackageJsonTypesVersionsField(jsonContent: PackageJson, state: ModuleResolutionState) {
|
|
const typesVersions = readPackageJsonField(jsonContent, "typesVersions", "object", state);
|
|
if (typesVersions === undefined) return;
|
|
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_has_a_typesVersions_field_with_version_specific_path_mappings);
|
|
}
|
|
|
|
return typesVersions;
|
|
}
|
|
|
|
/*@internal*/
|
|
interface VersionPaths {
|
|
version: string;
|
|
paths: MapLike<string[]>;
|
|
}
|
|
|
|
function readPackageJsonTypesVersionPaths(jsonContent: PackageJson, state: ModuleResolutionState): VersionPaths | undefined {
|
|
const typesVersions = readPackageJsonTypesVersionsField(jsonContent, state);
|
|
if (typesVersions === undefined) return;
|
|
|
|
if (state.traceEnabled) {
|
|
for (const key in typesVersions) {
|
|
if (hasProperty(typesVersions, key) && !VersionRange.tryParse(key)) {
|
|
trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_is_not_a_valid_semver_range, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = getPackageJsonTypesVersionsPaths(typesVersions);
|
|
if (!result) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_does_not_have_a_typesVersions_entry_that_matches_version_0, versionMajorMinor);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const { version: bestVersionKey, paths: bestVersionPaths } = result;
|
|
if (typeof bestVersionPaths !== "object") {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, `typesVersions['${bestVersionKey}']`, "object", typeof bestVersionPaths);
|
|
}
|
|
return;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
let typeScriptVersion: Version | undefined;
|
|
|
|
/* @internal */
|
|
export function getPackageJsonTypesVersionsPaths(typesVersions: MapLike<MapLike<string[]>>) {
|
|
if (!typeScriptVersion) typeScriptVersion = new Version(version);
|
|
|
|
for (const key in typesVersions) {
|
|
if (!hasProperty(typesVersions, key)) continue;
|
|
|
|
const keyRange = VersionRange.tryParse(key);
|
|
if (keyRange === undefined) {
|
|
continue;
|
|
}
|
|
|
|
// return the first entry whose range matches the current compiler version.
|
|
if (keyRange.test(typeScriptVersion)) {
|
|
return { version: key, paths: typesVersions[key] };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
|
|
if (options.typeRoots) {
|
|
return options.typeRoots;
|
|
}
|
|
|
|
let currentDirectory: string | undefined;
|
|
if (options.configFilePath) {
|
|
currentDirectory = getDirectoryPath(options.configFilePath);
|
|
}
|
|
else if (host.getCurrentDirectory) {
|
|
currentDirectory = host.getCurrentDirectory();
|
|
}
|
|
|
|
if (currentDirectory !== undefined) {
|
|
return getDefaultTypeRoots(currentDirectory, host);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the path to every node_modules/@types directory from some ancestor directory.
|
|
* Returns undefined if there are none.
|
|
*/
|
|
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
|
|
if (!host.directoryExists) {
|
|
return [combinePaths(currentDirectory, nodeModulesAtTypes)];
|
|
// And if it doesn't exist, tough.
|
|
}
|
|
|
|
let typeRoots: string[] | undefined;
|
|
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
|
|
const atTypes = combinePaths(directory, nodeModulesAtTypes);
|
|
if (host.directoryExists!(atTypes)) {
|
|
(typeRoots || (typeRoots = [])).push(atTypes);
|
|
}
|
|
return undefined;
|
|
});
|
|
return typeRoots;
|
|
}
|
|
const nodeModulesAtTypes = combinePaths("node_modules", "@types");
|
|
|
|
function arePathsEqual(path1: string, path2: string, host: ModuleResolutionHost): boolean {
|
|
const useCaseSensitiveFileNames = typeof host.useCaseSensitiveFileNames === "function" ? host.useCaseSensitiveFileNames() : host.useCaseSensitiveFileNames;
|
|
return comparePaths(path1, path2, !useCaseSensitiveFileNames) === Comparison.EqualTo;
|
|
}
|
|
|
|
/**
|
|
* @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
|
|
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
|
|
* is assumed to be the same as root directory of the project.
|
|
*/
|
|
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
|
|
const traceEnabled = isTraceEnabled(options, host);
|
|
if (redirectedReference) {
|
|
options = redirectedReference.commandLine.options;
|
|
}
|
|
|
|
const containingDirectory = containingFile ? getDirectoryPath(containingFile) : undefined;
|
|
const perFolderCache = containingDirectory ? cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference) : undefined;
|
|
let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName);
|
|
if (result) {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1, typeReferenceDirectiveName, containingFile);
|
|
if (redirectedReference) trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName);
|
|
trace(host, Diagnostics.Resolution_for_type_reference_directive_0_was_found_in_cache_from_location_1, typeReferenceDirectiveName, containingDirectory);
|
|
traceResult(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const typeRoots = getEffectiveTypeRoots(options, host);
|
|
if (traceEnabled) {
|
|
if (containingFile === undefined) {
|
|
if (typeRoots === undefined) {
|
|
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_not_set, typeReferenceDirectiveName);
|
|
}
|
|
else {
|
|
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_1, typeReferenceDirectiveName, typeRoots);
|
|
}
|
|
}
|
|
else {
|
|
if (typeRoots === undefined) {
|
|
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_not_set, typeReferenceDirectiveName, containingFile);
|
|
}
|
|
else {
|
|
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, typeRoots);
|
|
}
|
|
}
|
|
if (redirectedReference) {
|
|
trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName);
|
|
}
|
|
}
|
|
|
|
const failedLookupLocations: string[] = [];
|
|
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache };
|
|
let resolved = primaryLookup();
|
|
let primary = true;
|
|
if (!resolved) {
|
|
resolved = secondaryLookup();
|
|
primary = false;
|
|
}
|
|
|
|
let resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective | undefined;
|
|
if (resolved) {
|
|
const { fileName, packageId } = resolved;
|
|
const resolvedFileName = options.preserveSymlinks ? fileName : realPath(fileName, host, traceEnabled);
|
|
resolvedTypeReferenceDirective = {
|
|
primary,
|
|
resolvedFileName,
|
|
originalPath: arePathsEqual(fileName, resolvedFileName, host) ? undefined : fileName,
|
|
packageId,
|
|
isExternalLibraryImport: pathContainsNodeModules(fileName),
|
|
};
|
|
}
|
|
result = { resolvedTypeReferenceDirective, failedLookupLocations };
|
|
perFolderCache?.set(typeReferenceDirectiveName, result);
|
|
if (traceEnabled) traceResult(result);
|
|
return result;
|
|
|
|
function traceResult(result: ResolvedTypeReferenceDirectiveWithFailedLookupLocations) {
|
|
if (!result.resolvedTypeReferenceDirective?.resolvedFileName) {
|
|
trace(host, Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName);
|
|
}
|
|
else if (result.resolvedTypeReferenceDirective.packageId) {
|
|
trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_with_Package_ID_2_primary_Colon_3, typeReferenceDirectiveName, result.resolvedTypeReferenceDirective.resolvedFileName, packageIdToString(result.resolvedTypeReferenceDirective.packageId), result.resolvedTypeReferenceDirective.primary);
|
|
}
|
|
else {
|
|
trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, result.resolvedTypeReferenceDirective.resolvedFileName, result.resolvedTypeReferenceDirective.primary);
|
|
}
|
|
}
|
|
|
|
function primaryLookup(): PathAndPackageId | undefined {
|
|
// Check primary library paths
|
|
if (typeRoots && typeRoots.length) {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Resolving_with_primary_search_path_0, typeRoots.join(", "));
|
|
}
|
|
return firstDefined(typeRoots, typeRoot => {
|
|
const candidate = combinePaths(typeRoot, typeReferenceDirectiveName);
|
|
const candidateDirectory = getDirectoryPath(candidate);
|
|
const directoryExists = directoryProbablyExists(candidateDirectory, host);
|
|
if (!directoryExists && traceEnabled) {
|
|
trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory);
|
|
}
|
|
return resolvedTypeScriptOnly(
|
|
loadNodeModuleFromDirectory(Extensions.DtsOnly, candidate,
|
|
!directoryExists, moduleResolutionState));
|
|
});
|
|
}
|
|
else {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths);
|
|
}
|
|
}
|
|
}
|
|
|
|
function secondaryLookup(): PathAndPackageId | undefined {
|
|
const initialLocationForSecondaryLookup = containingFile && getDirectoryPath(containingFile);
|
|
|
|
if (initialLocationForSecondaryLookup !== undefined) {
|
|
// check secondary locations
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
|
|
}
|
|
let result: Resolved | undefined;
|
|
if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) {
|
|
const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
|
|
result = searchResult && searchResult.value;
|
|
}
|
|
else {
|
|
const { path: candidate } = normalizePathAndParts(combinePaths(initialLocationForSecondaryLookup, typeReferenceDirectiveName));
|
|
result = nodeLoadModuleByRelativeName(Extensions.DtsOnly, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true);
|
|
}
|
|
return resolvedTypeScriptOnly(result);
|
|
}
|
|
else {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Containing_file_is_not_specified_and_root_directory_cannot_be_determined_skipping_lookup_in_node_modules_folder);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a set of options, returns the set of type directive names
|
|
* that should be included for this program automatically.
|
|
* This list could either come from the config file,
|
|
* or from enumerating the types root + initial secondary types lookup location.
|
|
* More type directives might appear in the program later as a result of loading actual source files;
|
|
* this list is only the set of defaults that are implicitly included.
|
|
*/
|
|
export function getAutomaticTypeDirectiveNames(options: CompilerOptions, host: ModuleResolutionHost): string[] {
|
|
// Use explicit type list from tsconfig.json
|
|
if (options.types) {
|
|
return options.types;
|
|
}
|
|
|
|
// Walk the primary type lookup locations
|
|
const result: string[] = [];
|
|
if (host.directoryExists && host.getDirectories) {
|
|
const typeRoots = getEffectiveTypeRoots(options, host);
|
|
if (typeRoots) {
|
|
for (const root of typeRoots) {
|
|
if (host.directoryExists(root)) {
|
|
for (const typeDirectivePath of host.getDirectories(root)) {
|
|
const normalized = normalizePath(typeDirectivePath);
|
|
const packageJsonPath = combinePaths(root, normalized, "package.json");
|
|
// `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types.
|
|
// See `createNotNeededPackageJSON` in the types-publisher` repo.
|
|
// eslint-disable-next-line no-null/no-null
|
|
const isNotNeededPackage = host.fileExists(packageJsonPath) && (readJson(packageJsonPath, host) as PackageJson).typings === null;
|
|
if (!isNotNeededPackage) {
|
|
const baseFileName = getBaseFileName(normalized);
|
|
|
|
// At this stage, skip results with leading dot.
|
|
if (baseFileName.charCodeAt(0) !== CharacterCodes.dot) {
|
|
// Return just the type directive names
|
|
result.push(baseFileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export interface TypeReferenceDirectiveResolutionCache extends PerDirectoryResolutionCache<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>, PackageJsonInfoCache {
|
|
}
|
|
|
|
/**
|
|
* Cached resolutions per containing directory.
|
|
* This assumes that any module id will have the same resolution for sibling files located in the same folder.
|
|
*/
|
|
export interface PerDirectoryResolutionCache<T> {
|
|
getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference): Map<T>;
|
|
clear(): void;
|
|
/**
|
|
* Updates with the current compilerOptions the cache will operate with.
|
|
* This updates the redirects map as well if needed so module resolutions are cached if they can across the projects
|
|
*/
|
|
update(options: CompilerOptions): void;
|
|
}
|
|
|
|
export interface ModuleResolutionCache extends PerDirectoryResolutionCache<ResolvedModuleWithFailedLookupLocations>, NonRelativeModuleNameResolutionCache, PackageJsonInfoCache {
|
|
getPackageJsonInfoCache(): PackageJsonInfoCache;
|
|
}
|
|
|
|
/**
|
|
* Stored map from non-relative module name to a table: directory -> result of module lookup in this directory
|
|
* We support only non-relative module names because resolution of relative module names is usually more deterministic and thus less expensive.
|
|
*/
|
|
export interface NonRelativeModuleNameResolutionCache extends PackageJsonInfoCache {
|
|
getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache;
|
|
}
|
|
|
|
export interface PackageJsonInfoCache {
|
|
/*@internal*/ getPackageJsonInfo(packageJsonPath: string): PackageJsonInfo | boolean | undefined;
|
|
/*@internal*/ setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean): void;
|
|
/*@internal*/ entries(): [Path, PackageJsonInfo | boolean][];
|
|
clear(): void;
|
|
}
|
|
|
|
export interface PerModuleNameCache {
|
|
get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined;
|
|
set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void;
|
|
}
|
|
|
|
/*@internal*/
|
|
export interface CacheWithRedirects<T> {
|
|
getOwnMap: () => ESMap<string, T>;
|
|
redirectsMap: ESMap<Path, ESMap<string, T>>;
|
|
getOrCreateMapOfCacheRedirects(redirectedReference: ResolvedProjectReference | undefined): ESMap<string, T>;
|
|
clear(): void;
|
|
setOwnOptions(newOptions: CompilerOptions): void;
|
|
setOwnMap(newOwnMap: ESMap<string, T>): void;
|
|
}
|
|
|
|
/*@internal*/
|
|
export function createCacheWithRedirects<T>(options?: CompilerOptions): CacheWithRedirects<T> {
|
|
let ownMap: ESMap<string, T> = new Map();
|
|
const redirectsMap = new Map<Path, ESMap<string, T>>();
|
|
return {
|
|
getOwnMap,
|
|
redirectsMap,
|
|
getOrCreateMapOfCacheRedirects,
|
|
clear,
|
|
setOwnOptions,
|
|
setOwnMap
|
|
};
|
|
|
|
function getOwnMap() {
|
|
return ownMap;
|
|
}
|
|
|
|
function setOwnOptions(newOptions: CompilerOptions) {
|
|
options = newOptions;
|
|
}
|
|
|
|
function setOwnMap(newOwnMap: ESMap<string, T>) {
|
|
ownMap = newOwnMap;
|
|
}
|
|
|
|
function getOrCreateMapOfCacheRedirects(redirectedReference: ResolvedProjectReference | undefined) {
|
|
if (!redirectedReference) {
|
|
return ownMap;
|
|
}
|
|
const path = redirectedReference.sourceFile.path;
|
|
let redirects = redirectsMap.get(path);
|
|
if (!redirects) {
|
|
// Reuse map if redirected reference map uses same resolution
|
|
redirects = !options || optionsHaveModuleResolutionChanges(options, redirectedReference.commandLine.options) ? new Map() : ownMap;
|
|
redirectsMap.set(path, redirects);
|
|
}
|
|
return redirects;
|
|
}
|
|
|
|
function clear() {
|
|
ownMap.clear();
|
|
redirectsMap.clear();
|
|
}
|
|
}
|
|
|
|
function createPackageJsonInfoCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): PackageJsonInfoCache {
|
|
let cache: ESMap<Path, PackageJsonInfo | boolean> | undefined;
|
|
return { getPackageJsonInfo, setPackageJsonInfo, clear, entries };
|
|
function getPackageJsonInfo(packageJsonPath: string) {
|
|
return cache?.get(toPath(packageJsonPath, currentDirectory, getCanonicalFileName));
|
|
}
|
|
function setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean) {
|
|
(cache ||= new Map()).set(toPath(packageJsonPath, currentDirectory, getCanonicalFileName), info);
|
|
}
|
|
function clear() {
|
|
cache = undefined;
|
|
}
|
|
function entries() {
|
|
const iter = cache?.entries();
|
|
return iter ? arrayFrom(iter) : [];
|
|
}
|
|
}
|
|
|
|
function getOrCreateCache<T>(cacheWithRedirects: CacheWithRedirects<T>, redirectedReference: ResolvedProjectReference | undefined, key: string, create: () => T): T {
|
|
const cache = cacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference);
|
|
let result = cache.get(key);
|
|
if (!result) {
|
|
result = create();
|
|
cache.set(key, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function updateRedirectsMap<T>(
|
|
options: CompilerOptions,
|
|
directoryToModuleNameMap: CacheWithRedirects<ESMap<string, T>>,
|
|
moduleNameToDirectoryMap?: CacheWithRedirects<PerModuleNameCache>
|
|
) {
|
|
if (!options.configFile) return;
|
|
if (directoryToModuleNameMap.redirectsMap.size === 0) {
|
|
// The own map will be for projectCompilerOptions
|
|
Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.redirectsMap.size === 0);
|
|
Debug.assert(directoryToModuleNameMap.getOwnMap().size === 0);
|
|
Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.getOwnMap().size === 0);
|
|
directoryToModuleNameMap.redirectsMap.set(options.configFile.path, directoryToModuleNameMap.getOwnMap());
|
|
moduleNameToDirectoryMap?.redirectsMap.set(options.configFile.path, moduleNameToDirectoryMap.getOwnMap());
|
|
}
|
|
else {
|
|
// Set correct own map
|
|
Debug.assert(!moduleNameToDirectoryMap || moduleNameToDirectoryMap.redirectsMap.size > 0);
|
|
const ref: ResolvedProjectReference = {
|
|
sourceFile: options.configFile,
|
|
commandLine: { options } as ParsedCommandLine
|
|
};
|
|
directoryToModuleNameMap.setOwnMap(directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref));
|
|
moduleNameToDirectoryMap?.setOwnMap(moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref));
|
|
}
|
|
directoryToModuleNameMap.setOwnOptions(options);
|
|
moduleNameToDirectoryMap?.setOwnOptions(options);
|
|
}
|
|
|
|
function createPerDirectoryResolutionCache<T>(currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, directoryToModuleNameMap: CacheWithRedirects<ESMap<string, T>>): PerDirectoryResolutionCache<T> {
|
|
return {
|
|
getOrCreateCacheForDirectory,
|
|
clear,
|
|
update,
|
|
};
|
|
|
|
function clear() {
|
|
directoryToModuleNameMap.clear();
|
|
}
|
|
|
|
function update(options: CompilerOptions) {
|
|
updateRedirectsMap(options, directoryToModuleNameMap);
|
|
}
|
|
|
|
function getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference) {
|
|
const path = toPath(directoryName, currentDirectory, getCanonicalFileName);
|
|
return getOrCreateCache<ESMap<string, T>>(directoryToModuleNameMap, redirectedReference, path, () => new Map());
|
|
}
|
|
}
|
|
|
|
export function createModuleResolutionCache(
|
|
currentDirectory: string,
|
|
getCanonicalFileName: (s: string) => string,
|
|
options?: CompilerOptions
|
|
): ModuleResolutionCache;
|
|
/*@internal*/
|
|
export function createModuleResolutionCache(
|
|
currentDirectory: string,
|
|
getCanonicalFileName: GetCanonicalFileName,
|
|
options: undefined,
|
|
directoryToModuleNameMap: CacheWithRedirects<ESMap<string, ResolvedModuleWithFailedLookupLocations>>,
|
|
moduleNameToDirectoryMap: CacheWithRedirects<PerModuleNameCache>,
|
|
): ModuleResolutionCache;
|
|
export function createModuleResolutionCache(
|
|
currentDirectory: string,
|
|
getCanonicalFileName: GetCanonicalFileName,
|
|
options?: CompilerOptions,
|
|
directoryToModuleNameMap?: CacheWithRedirects<ESMap<string, ResolvedModuleWithFailedLookupLocations>>,
|
|
moduleNameToDirectoryMap?: CacheWithRedirects<PerModuleNameCache>,
|
|
): ModuleResolutionCache {
|
|
const preDirectoryResolutionCache = createPerDirectoryResolutionCache(currentDirectory, getCanonicalFileName, directoryToModuleNameMap ||= createCacheWithRedirects(options));
|
|
moduleNameToDirectoryMap ||= createCacheWithRedirects(options);
|
|
const packageJsonInfoCache = createPackageJsonInfoCache(currentDirectory, getCanonicalFileName);
|
|
|
|
return {
|
|
...packageJsonInfoCache,
|
|
...preDirectoryResolutionCache,
|
|
getOrCreateCacheForModuleName,
|
|
clear,
|
|
update,
|
|
getPackageJsonInfoCache: () => packageJsonInfoCache,
|
|
};
|
|
|
|
function clear() {
|
|
preDirectoryResolutionCache.clear();
|
|
moduleNameToDirectoryMap!.clear();
|
|
packageJsonInfoCache.clear();
|
|
}
|
|
|
|
function update(options: CompilerOptions) {
|
|
updateRedirectsMap(options, directoryToModuleNameMap!, moduleNameToDirectoryMap);
|
|
}
|
|
|
|
function getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache {
|
|
Debug.assert(!isExternalModuleNameRelative(nonRelativeModuleName));
|
|
return getOrCreateCache(moduleNameToDirectoryMap!, redirectedReference, nonRelativeModuleName, createPerModuleNameCache);
|
|
}
|
|
|
|
function createPerModuleNameCache(): PerModuleNameCache {
|
|
const directoryPathMap = new Map<string, ResolvedModuleWithFailedLookupLocations>();
|
|
|
|
return { get, set };
|
|
|
|
function get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined {
|
|
return directoryPathMap.get(toPath(directory, currentDirectory, getCanonicalFileName));
|
|
}
|
|
|
|
/**
|
|
* At first this function add entry directory -> module resolution result to the table.
|
|
* Then it computes the set of parent folders for 'directory' that should have the same module resolution result
|
|
* and for every parent folder in set it adds entry: parent -> module resolution. .
|
|
* Lets say we first directory name: /a/b/c/d/e and resolution result is: /a/b/bar.ts.
|
|
* Set of parent folders that should have the same result will be:
|
|
* [
|
|
* /a/b/c/d, /a/b/c, /a/b
|
|
* ]
|
|
* this means that request for module resolution from file in any of these folder will be immediately found in cache.
|
|
*/
|
|
function set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void {
|
|
const path = toPath(directory, currentDirectory, getCanonicalFileName);
|
|
// if entry is already in cache do nothing
|
|
if (directoryPathMap.has(path)) {
|
|
return;
|
|
}
|
|
directoryPathMap.set(path, result);
|
|
|
|
const resolvedFileName = result.resolvedModule &&
|
|
(result.resolvedModule.originalPath || result.resolvedModule.resolvedFileName);
|
|
// find common prefix between directory and resolved file name
|
|
// this common prefix should be the shortest path that has the same resolution
|
|
// directory: /a/b/c/d/e
|
|
// resolvedFileName: /a/b/foo.d.ts
|
|
// commonPrefix: /a/b
|
|
// for failed lookups cache the result for every directory up to root
|
|
const commonPrefix = resolvedFileName && getCommonPrefix(path, resolvedFileName);
|
|
let current = path;
|
|
while (current !== commonPrefix) {
|
|
const parent = getDirectoryPath(current);
|
|
if (parent === current || directoryPathMap.has(parent)) {
|
|
break;
|
|
}
|
|
directoryPathMap.set(parent, result);
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
function getCommonPrefix(directory: Path, resolution: string) {
|
|
const resolutionDirectory = toPath(getDirectoryPath(resolution), currentDirectory, getCanonicalFileName);
|
|
|
|
// find first position where directory and resolution differs
|
|
let i = 0;
|
|
const limit = Math.min(directory.length, resolutionDirectory.length);
|
|
while (i < limit && directory.charCodeAt(i) === resolutionDirectory.charCodeAt(i)) {
|
|
i++;
|
|
}
|
|
if (i === directory.length && (resolutionDirectory.length === i || resolutionDirectory[i] === directorySeparator)) {
|
|
return directory;
|
|
}
|
|
const rootLength = getRootLength(directory);
|
|
if (i < rootLength) {
|
|
return undefined;
|
|
}
|
|
const sep = directory.lastIndexOf(directorySeparator, i - 1);
|
|
if (sep === -1) {
|
|
return undefined;
|
|
}
|
|
return directory.substr(0, Math.max(sep, rootLength));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createTypeReferenceDirectiveResolutionCache(
|
|
currentDirectory: string,
|
|
getCanonicalFileName: (s: string) => string,
|
|
options?: CompilerOptions,
|
|
packageJsonInfoCache?: PackageJsonInfoCache,
|
|
): TypeReferenceDirectiveResolutionCache;
|
|
/*@internal*/
|
|
export function createTypeReferenceDirectiveResolutionCache(
|
|
currentDirectory: string,
|
|
getCanonicalFileName: GetCanonicalFileName,
|
|
options: undefined,
|
|
packageJsonInfoCache: PackageJsonInfoCache | undefined,
|
|
directoryToModuleNameMap: CacheWithRedirects<ESMap<string, ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>,
|
|
): TypeReferenceDirectiveResolutionCache;
|
|
export function createTypeReferenceDirectiveResolutionCache(
|
|
currentDirectory: string,
|
|
getCanonicalFileName: GetCanonicalFileName,
|
|
options?: CompilerOptions,
|
|
packageJsonInfoCache?: PackageJsonInfoCache | undefined,
|
|
directoryToModuleNameMap?: CacheWithRedirects<ESMap<string, ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>,
|
|
): TypeReferenceDirectiveResolutionCache {
|
|
const preDirectoryResolutionCache = createPerDirectoryResolutionCache(currentDirectory, getCanonicalFileName, directoryToModuleNameMap ||= createCacheWithRedirects(options));
|
|
packageJsonInfoCache ||= createPackageJsonInfoCache(currentDirectory, getCanonicalFileName);
|
|
|
|
return {
|
|
...packageJsonInfoCache,
|
|
...preDirectoryResolutionCache,
|
|
clear,
|
|
};
|
|
|
|
function clear() {
|
|
preDirectoryResolutionCache.clear();
|
|
packageJsonInfoCache!.clear();
|
|
}
|
|
}
|
|
|
|
export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined {
|
|
const containingDirectory = getDirectoryPath(containingFile);
|
|
const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory);
|
|
return perFolderCache && perFolderCache.get(moduleName);
|
|
}
|
|
|
|
export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations {
|
|
const traceEnabled = isTraceEnabled(compilerOptions, host);
|
|
if (redirectedReference) {
|
|
compilerOptions = redirectedReference.commandLine.options;
|
|
}
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Resolving_module_0_from_1, moduleName, containingFile);
|
|
if (redirectedReference) {
|
|
trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName);
|
|
}
|
|
}
|
|
const containingDirectory = getDirectoryPath(containingFile);
|
|
const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference);
|
|
let result = perFolderCache && perFolderCache.get(moduleName);
|
|
|
|
if (result) {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory);
|
|
}
|
|
}
|
|
else {
|
|
let moduleResolution = compilerOptions.moduleResolution;
|
|
if (moduleResolution === undefined) {
|
|
moduleResolution = getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic;
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Module_resolution_kind_is_not_specified_using_0, ModuleResolutionKind[moduleResolution]);
|
|
}
|
|
}
|
|
else {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, ModuleResolutionKind[moduleResolution]);
|
|
}
|
|
}
|
|
|
|
perfLogger.logStartResolveModule(moduleName /* , containingFile, ModuleResolutionKind[moduleResolution]*/);
|
|
switch (moduleResolution) {
|
|
case ModuleResolutionKind.NodeJs:
|
|
result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
|
|
break;
|
|
case ModuleResolutionKind.Classic:
|
|
result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
|
|
break;
|
|
default:
|
|
return Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`);
|
|
}
|
|
if (result && result.resolvedModule) perfLogger.logInfoEvent(`Module "${moduleName}" resolved to "${result.resolvedModule.resolvedFileName}"`);
|
|
perfLogger.logStopResolveModule((result && result.resolvedModule) ? "" + result.resolvedModule.resolvedFileName : "null");
|
|
|
|
if (perFolderCache) {
|
|
perFolderCache.set(moduleName, result);
|
|
if (!isExternalModuleNameRelative(moduleName)) {
|
|
// put result in per-module name cache
|
|
cache!.getOrCreateCacheForModuleName(moduleName, redirectedReference).set(containingDirectory, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (traceEnabled) {
|
|
if (result.resolvedModule) {
|
|
if (result.resolvedModule.packageId) {
|
|
trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1_with_Package_ID_2, moduleName, result.resolvedModule.resolvedFileName, packageIdToString(result.resolvedModule.packageId));
|
|
}
|
|
else {
|
|
trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.resolvedModule.resolvedFileName);
|
|
}
|
|
}
|
|
else {
|
|
trace(host, Diagnostics.Module_name_0_was_not_resolved, moduleName);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Every module resolution kind can has its specific understanding how to load module from a specific path on disk
|
|
* I.e. for path '/a/b/c':
|
|
* - Node loader will first to try to check if '/a/b/c' points to a file with some supported extension and if this fails
|
|
* it will try to load module from directory: directory '/a/b/c' should exist and it should have either 'package.json' with
|
|
* 'typings' entry or file 'index' with some supported extension
|
|
* - Classic loader will only try to interpret '/a/b/c' as file.
|
|
*/
|
|
type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined;
|
|
|
|
/**
|
|
* Any module resolution kind can be augmented with optional settings: 'baseUrl', 'paths' and 'rootDirs' - they are used to
|
|
* mitigate differences between design time structure of the project and its runtime counterpart so the same import name
|
|
* can be resolved successfully by TypeScript compiler and runtime module loader.
|
|
* If these settings are set then loading procedure will try to use them to resolve module name and it can of failure it will
|
|
* fallback to standard resolution routine.
|
|
*
|
|
* - baseUrl - this setting controls how non-relative module names are resolved. If this setting is specified then non-relative
|
|
* names will be resolved relative to baseUrl: i.e. if baseUrl is '/a/b' then candidate location to resolve module name 'c/d' will
|
|
* be '/a/b/c/d'
|
|
* - paths - this setting can only be used when baseUrl is specified. allows to tune how non-relative module names
|
|
* will be resolved based on the content of the module name.
|
|
* Structure of 'paths' compiler options
|
|
* 'paths': {
|
|
* pattern-1: [...substitutions],
|
|
* pattern-2: [...substitutions],
|
|
* ...
|
|
* pattern-n: [...substitutions]
|
|
* }
|
|
* Pattern here is a string that can contain zero or one '*' character. During module resolution module name will be matched against
|
|
* all patterns in the list. Matching for patterns that don't contain '*' means that module name must be equal to pattern respecting the case.
|
|
* If pattern contains '*' then to match pattern "<prefix>*<suffix>" module name must start with the <prefix> and end with <suffix>.
|
|
* <MatchedStar> denotes part of the module name between <prefix> and <suffix>.
|
|
* If module name can be matches with multiple patterns then pattern with the longest prefix will be picked.
|
|
* After selecting pattern we'll use list of substitutions to get candidate locations of the module and the try to load module
|
|
* from the candidate location.
|
|
* Substitution is a string that can contain zero or one '*'. To get candidate location from substitution we'll pick every
|
|
* substitution in the list and replace '*' with <MatchedStar> string. If candidate location is not rooted it
|
|
* will be converted to absolute using baseUrl.
|
|
* For example:
|
|
* baseUrl: /a/b/c
|
|
* "paths": {
|
|
* // match all module names
|
|
* "*": [
|
|
* "*", // use matched name as is,
|
|
* // <matched name> will be looked as /a/b/c/<matched name>
|
|
*
|
|
* "folder1/*" // substitution will convert matched name to 'folder1/<matched name>',
|
|
* // since it is not rooted then final candidate location will be /a/b/c/folder1/<matched name>
|
|
* ],
|
|
* // match module names that start with 'components/'
|
|
* "components/*": [ "/root/components/*" ] // substitution will convert /components/folder1/<matched name> to '/root/components/folder1/<matched name>',
|
|
* // it is rooted so it will be final candidate location
|
|
* }
|
|
*
|
|
* 'rootDirs' allows the project to be spreaded across multiple locations and resolve modules with relative names as if
|
|
* they were in the same location. For example lets say there are two files
|
|
* '/local/src/content/file1.ts'
|
|
* '/shared/components/contracts/src/content/protocols/file2.ts'
|
|
* After bundling content of '/shared/components/contracts/src' will be merged with '/local/src' so
|
|
* if file1 has the following import 'import {x} from "./protocols/file2"' it will be resolved successfully in runtime.
|
|
* 'rootDirs' provides the way to tell compiler that in order to get the whole project it should behave as if content of all
|
|
* root dirs were merged together.
|
|
* I.e. for the example above 'rootDirs' will have two entries: [ '/local/src', '/shared/components/contracts/src' ].
|
|
* Compiler will first convert './protocols/file2' into absolute path relative to the location of containing file:
|
|
* '/local/src/content/protocols/file2' and try to load it - failure.
|
|
* Then it will search 'rootDirs' looking for a longest matching prefix of this absolute path and if such prefix is found - absolute path will
|
|
* be converted to a path relative to found rootDir entry './content/protocols/file2' (*). As a last step compiler will check all remaining
|
|
* entries in 'rootDirs', use them to build absolute path out of (*) and try to resolve module from this location.
|
|
*/
|
|
function tryLoadModuleUsingOptionalResolutionSettings(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader,
|
|
state: ModuleResolutionState): Resolved | undefined {
|
|
|
|
const resolved = tryLoadModuleUsingPathsIfEligible(extensions, moduleName, loader, state);
|
|
if (resolved) return resolved.value;
|
|
|
|
if (!isExternalModuleNameRelative(moduleName)) {
|
|
return tryLoadModuleUsingBaseUrl(extensions, moduleName, loader, state);
|
|
}
|
|
else {
|
|
return tryLoadModuleUsingRootDirs(extensions, moduleName, containingDirectory, loader, state);
|
|
}
|
|
}
|
|
|
|
function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) {
|
|
const { baseUrl, paths, configFile } = state.compilerOptions;
|
|
if (paths && !pathIsRelative(moduleName)) {
|
|
if (state.traceEnabled) {
|
|
if (baseUrl) {
|
|
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName);
|
|
}
|
|
trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName);
|
|
}
|
|
const baseDirectory = getPathsBasePath(state.compilerOptions, state.host)!; // Always defined when 'paths' is defined
|
|
const pathPatterns = configFile?.configFileSpecs ? configFile.configFileSpecs.pathPatterns ||= tryParsePatterns(paths) : undefined;
|
|
return tryLoadModuleUsingPaths(extensions, moduleName, baseDirectory, paths, pathPatterns, loader, /*onlyRecordFailures*/ false, state);
|
|
}
|
|
}
|
|
|
|
function tryLoadModuleUsingRootDirs(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader,
|
|
state: ModuleResolutionState): Resolved | undefined {
|
|
|
|
if (!state.compilerOptions.rootDirs) {
|
|
return undefined;
|
|
}
|
|
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, moduleName);
|
|
}
|
|
|
|
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
|
|
|
|
let matchedRootDir: string | undefined;
|
|
let matchedNormalizedPrefix: string | undefined;
|
|
for (const rootDir of state.compilerOptions.rootDirs) {
|
|
// rootDirs are expected to be absolute
|
|
// in case of tsconfig.json this will happen automatically - compiler will expand relative names
|
|
// using location of tsconfig.json as base location
|
|
let normalizedRoot = normalizePath(rootDir);
|
|
if (!endsWith(normalizedRoot, directorySeparator)) {
|
|
normalizedRoot += directorySeparator;
|
|
}
|
|
const isLongestMatchingPrefix =
|
|
startsWith(candidate, normalizedRoot) &&
|
|
(matchedNormalizedPrefix === undefined || matchedNormalizedPrefix.length < normalizedRoot.length);
|
|
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix);
|
|
}
|
|
|
|
if (isLongestMatchingPrefix) {
|
|
matchedNormalizedPrefix = normalizedRoot;
|
|
matchedRootDir = rootDir;
|
|
}
|
|
}
|
|
if (matchedNormalizedPrefix) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix);
|
|
}
|
|
const suffix = candidate.substr(matchedNormalizedPrefix.length);
|
|
|
|
// first - try to load from a initial location
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate);
|
|
}
|
|
const resolvedFileName = loader(extensions, candidate, !directoryProbablyExists(containingDirectory, state.host), state);
|
|
if (resolvedFileName) {
|
|
return resolvedFileName;
|
|
}
|
|
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Trying_other_entries_in_rootDirs);
|
|
}
|
|
// then try to resolve using remaining entries in rootDirs
|
|
for (const rootDir of state.compilerOptions.rootDirs) {
|
|
if (rootDir === matchedRootDir) {
|
|
// skip the initially matched entry
|
|
continue;
|
|
}
|
|
const candidate = combinePaths(normalizePath(rootDir), suffix);
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate);
|
|
}
|
|
const baseDirectory = getDirectoryPath(candidate);
|
|
const resolvedFileName = loader(extensions, candidate, !directoryProbablyExists(baseDirectory, state.host), state);
|
|
if (resolvedFileName) {
|
|
return resolvedFileName;
|
|
}
|
|
}
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Module_resolution_using_rootDirs_has_failed);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function tryLoadModuleUsingBaseUrl(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined {
|
|
const { baseUrl } = state.compilerOptions;
|
|
if (!baseUrl) {
|
|
return undefined;
|
|
}
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName);
|
|
}
|
|
const candidate = normalizePath(combinePaths(baseUrl, moduleName));
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Resolving_module_name_0_relative_to_base_url_1_2, moduleName, baseUrl, candidate);
|
|
}
|
|
return loader(extensions, candidate, !directoryProbablyExists(getDirectoryPath(candidate), state.host), state);
|
|
}
|
|
|
|
/**
|
|
* Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations.
|
|
* No way to do this with `require()`: https://github.com/nodejs/node/issues/5963
|
|
* Throws an error if the module can't be resolved.
|
|
*/
|
|
/* @internal */
|
|
export function resolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string {
|
|
const { resolvedModule, failedLookupLocations } = tryResolveJSModuleWorker(moduleName, initialDir, host);
|
|
if (!resolvedModule) {
|
|
throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`);
|
|
}
|
|
return resolvedModule.resolvedFileName;
|
|
}
|
|
|
|
/* @internal */
|
|
export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost) {
|
|
return tryResolveJSModuleWorker(moduleName, initialDir, host).resolvedModule;
|
|
}
|
|
|
|
const jsOnlyExtensions = [Extensions.JavaScript];
|
|
const tsExtensions = [Extensions.TypeScript, Extensions.JavaScript];
|
|
const tsPlusJsonExtensions = [...tsExtensions, Extensions.Json];
|
|
const tsconfigExtensions = [Extensions.TSConfig];
|
|
function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
|
|
return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions, /*redirectedReferences*/ undefined);
|
|
}
|
|
|
|
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations;
|
|
/* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures
|
|
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations {
|
|
return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions), redirectedReference);
|
|
}
|
|
|
|
function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ResolvedProjectReference | undefined): ResolvedModuleWithFailedLookupLocations {
|
|
const traceEnabled = isTraceEnabled(compilerOptions, host);
|
|
|
|
const failedLookupLocations: string[] = [];
|
|
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache };
|
|
|
|
const result = forEach(extensions, ext => tryResolve(ext));
|
|
return createResolvedModuleWithFailedLookupLocations(result?.value?.resolved, result?.value?.isExternalLibraryImport, failedLookupLocations, state.resultFromCache);
|
|
|
|
function tryResolve(extensions: Extensions): SearchResult<{ resolved: Resolved, isExternalLibraryImport: boolean }> {
|
|
const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => nodeLoadModuleByRelativeName(extensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ true);
|
|
const resolved = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loader, state);
|
|
if (resolved) {
|
|
return toSearchResult({ resolved, isExternalLibraryImport: pathContainsNodeModules(resolved.path) });
|
|
}
|
|
|
|
if (!isExternalModuleNameRelative(moduleName)) {
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]);
|
|
}
|
|
const resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference);
|
|
if (!resolved) return undefined;
|
|
|
|
let resolvedValue = resolved.value;
|
|
if (!compilerOptions.preserveSymlinks && resolvedValue && !resolvedValue.originalPath) {
|
|
const path = realPath(resolvedValue.path, host, traceEnabled);
|
|
const originalPath = arePathsEqual(path, resolvedValue.path, host) ? undefined : resolvedValue.path;
|
|
resolvedValue = { ...resolvedValue, path, originalPath };
|
|
}
|
|
// For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files.
|
|
return { value: resolvedValue && { resolved: resolvedValue, isExternalLibraryImport: true } };
|
|
}
|
|
else {
|
|
const { path: candidate, parts } = normalizePathAndParts(combinePaths(containingDirectory, moduleName));
|
|
const resolved = nodeLoadModuleByRelativeName(extensions, candidate, /*onlyRecordFailures*/ false, state, /*considerPackageJson*/ true);
|
|
// Treat explicit "node_modules" import as an external library import.
|
|
return resolved && toSearchResult({ resolved, isExternalLibraryImport: contains(parts, "node_modules") });
|
|
}
|
|
}
|
|
}
|
|
|
|
function realPath(path: string, host: ModuleResolutionHost, traceEnabled: boolean): string {
|
|
if (!host.realpath) {
|
|
return path;
|
|
}
|
|
|
|
const real = normalizePath(host.realpath(path));
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Resolving_real_path_for_0_result_1, path, real);
|
|
}
|
|
Debug.assert(host.fileExists(real), `${path} linked to nonexistent file ${real}`);
|
|
return real;
|
|
}
|
|
|
|
function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson: boolean): Resolved | undefined {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]);
|
|
}
|
|
if (!hasTrailingDirectorySeparator(candidate)) {
|
|
if (!onlyRecordFailures) {
|
|
const parentOfCandidate = getDirectoryPath(candidate);
|
|
if (!directoryProbablyExists(parentOfCandidate, state.host)) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate);
|
|
}
|
|
onlyRecordFailures = true;
|
|
}
|
|
}
|
|
const resolvedFromFile = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state);
|
|
if (resolvedFromFile) {
|
|
const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile.path) : undefined;
|
|
const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined;
|
|
return withPackageId(packageInfo, resolvedFromFile);
|
|
}
|
|
}
|
|
if (!onlyRecordFailures) {
|
|
const candidateExists = directoryProbablyExists(candidate, state.host);
|
|
if (!candidateExists) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate);
|
|
}
|
|
onlyRecordFailures = true;
|
|
}
|
|
}
|
|
return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson);
|
|
}
|
|
|
|
/*@internal*/
|
|
export const nodeModulesPathPart = "/node_modules/";
|
|
/*@internal*/
|
|
export function pathContainsNodeModules(path: string): boolean {
|
|
return stringContains(path, nodeModulesPathPart);
|
|
}
|
|
|
|
/**
|
|
* This will be called on the successfully resolved path from `loadModuleFromFile`.
|
|
* (Not needed for `loadModuleFromNodeModules` as that looks up the `package.json` as part of resolution.)
|
|
*
|
|
* packageDirectory is the directory of the package itself.
|
|
* For `blah/node_modules/foo/index.d.ts` this is packageDirectory: "foo"
|
|
* For `/node_modules/foo/bar.d.ts` this is packageDirectory: "foo"
|
|
* For `/node_modules/@types/foo/bar/index.d.ts` this is packageDirectory: "@types/foo"
|
|
* For `/node_modules/foo/bar/index.d.ts` this is packageDirectory: "foo"
|
|
*/
|
|
/* @internal */
|
|
export function parseNodeModuleFromPath(resolved: string): string | undefined {
|
|
const path = normalizePath(resolved);
|
|
const idx = path.lastIndexOf(nodeModulesPathPart);
|
|
if (idx === -1) {
|
|
return undefined;
|
|
}
|
|
|
|
const indexAfterNodeModules = idx + nodeModulesPathPart.length;
|
|
let indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules);
|
|
if (path.charCodeAt(indexAfterNodeModules) === CharacterCodes.at) {
|
|
indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName);
|
|
}
|
|
return path.slice(0, indexAfterPackageName);
|
|
}
|
|
|
|
function moveToNextDirectorySeparatorIfAvailable(path: string, prevSeparatorIndex: number): number {
|
|
const nextSeparatorIndex = path.indexOf(directorySeparator, prevSeparatorIndex + 1);
|
|
return nextSeparatorIndex === -1 ? prevSeparatorIndex : nextSeparatorIndex;
|
|
}
|
|
|
|
function loadModuleFromFileNoPackageId(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined {
|
|
return noPackageId(loadModuleFromFile(extensions, candidate, onlyRecordFailures, state));
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary
|
|
* in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations.
|
|
*/
|
|
function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
|
|
if (extensions === Extensions.Json || extensions === Extensions.TSConfig) {
|
|
const extensionLess = tryRemoveExtension(candidate, Extension.Json);
|
|
return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, onlyRecordFailures, state);
|
|
}
|
|
|
|
// First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts"
|
|
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, onlyRecordFailures, state);
|
|
if (resolvedByAddingExtension) {
|
|
return resolvedByAddingExtension;
|
|
}
|
|
|
|
// If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one;
|
|
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
|
|
if (hasJSFileExtension(candidate)) {
|
|
const extensionless = removeFileExtension(candidate);
|
|
if (state.traceEnabled) {
|
|
const extension = candidate.substring(extensionless.length);
|
|
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
|
|
}
|
|
return tryAddingExtensions(extensionless, extensions, onlyRecordFailures, state);
|
|
}
|
|
}
|
|
|
|
/** Try to return an existing file that adds one of the `extensions` to `candidate`. */
|
|
function tryAddingExtensions(candidate: string, extensions: Extensions, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
|
|
if (!onlyRecordFailures) {
|
|
// check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing
|
|
const directory = getDirectoryPath(candidate);
|
|
if (directory) {
|
|
onlyRecordFailures = !directoryProbablyExists(directory, state.host);
|
|
}
|
|
}
|
|
|
|
switch (extensions) {
|
|
case Extensions.DtsOnly:
|
|
return tryExtension(Extension.Dts);
|
|
case Extensions.TypeScript:
|
|
return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts);
|
|
case Extensions.JavaScript:
|
|
return tryExtension(Extension.Js) || tryExtension(Extension.Jsx);
|
|
case Extensions.TSConfig:
|
|
case Extensions.Json:
|
|
return tryExtension(Extension.Json);
|
|
}
|
|
|
|
function tryExtension(ext: Extension): PathAndExtension | undefined {
|
|
const path = tryFile(candidate + ext, onlyRecordFailures, state);
|
|
return path === undefined ? undefined : { path, ext };
|
|
}
|
|
}
|
|
|
|
/** Return the file if it exists. */
|
|
function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined {
|
|
if (!onlyRecordFailures) {
|
|
if (state.host.fileExists(fileName)) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName);
|
|
}
|
|
return fileName;
|
|
}
|
|
else {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.File_0_does_not_exist, fileName);
|
|
}
|
|
}
|
|
}
|
|
state.failedLookupLocations.push(fileName);
|
|
return undefined;
|
|
}
|
|
|
|
function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true) {
|
|
const packageInfo = considerPackageJson ? getPackageJsonInfo(candidate, onlyRecordFailures, state) : undefined;
|
|
const packageJsonContent = packageInfo && packageInfo.packageJsonContent;
|
|
const versionPaths = packageInfo && packageInfo.versionPaths;
|
|
return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths));
|
|
}
|
|
|
|
/*@internal*/
|
|
interface PackageJsonInfo {
|
|
packageDirectory: string;
|
|
packageJsonContent: PackageJsonPathFields;
|
|
versionPaths: VersionPaths | undefined;
|
|
}
|
|
|
|
function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined {
|
|
const { host, traceEnabled } = state;
|
|
const packageJsonPath = combinePaths(packageDirectory, "package.json");
|
|
if (onlyRecordFailures) {
|
|
state.failedLookupLocations.push(packageJsonPath);
|
|
return undefined;
|
|
}
|
|
|
|
const existing = state.packageJsonInfoCache?.getPackageJsonInfo(packageJsonPath);
|
|
if (existing !== undefined) {
|
|
if (typeof existing !== "boolean") {
|
|
if (traceEnabled) trace(host, Diagnostics.File_0_exists_according_to_earlier_cached_lookups, packageJsonPath);
|
|
return existing;
|
|
}
|
|
else {
|
|
if (existing && traceEnabled) trace(host, Diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, packageJsonPath);
|
|
state.failedLookupLocations.push(packageJsonPath);
|
|
return undefined;
|
|
}
|
|
}
|
|
const directoryExists = directoryProbablyExists(packageDirectory, host);
|
|
if (directoryExists && host.fileExists(packageJsonPath)) {
|
|
const packageJsonContent = readJson(packageJsonPath, host) as PackageJson;
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath);
|
|
}
|
|
const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state);
|
|
const result = { packageDirectory, packageJsonContent, versionPaths };
|
|
state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result);
|
|
return result;
|
|
}
|
|
else {
|
|
if (directoryExists && traceEnabled) {
|
|
trace(host, Diagnostics.File_0_does_not_exist, packageJsonPath);
|
|
}
|
|
state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, directoryExists);
|
|
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
|
|
state.failedLookupLocations.push(packageJsonPath);
|
|
}
|
|
}
|
|
|
|
function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, jsonContent: PackageJsonPathFields | undefined, versionPaths: VersionPaths | undefined): PathAndExtension | undefined {
|
|
let packageFile: string | undefined;
|
|
if (jsonContent) {
|
|
switch (extensions) {
|
|
case Extensions.JavaScript:
|
|
case Extensions.Json:
|
|
packageFile = readPackageJsonMainField(jsonContent, candidate, state);
|
|
break;
|
|
case Extensions.TypeScript:
|
|
// When resolving typescript modules, try resolving using main field as well
|
|
packageFile = readPackageJsonTypesFields(jsonContent, candidate, state) || readPackageJsonMainField(jsonContent, candidate, state);
|
|
break;
|
|
case Extensions.DtsOnly:
|
|
packageFile = readPackageJsonTypesFields(jsonContent, candidate, state);
|
|
break;
|
|
case Extensions.TSConfig:
|
|
packageFile = readPackageJsonTSConfigField(jsonContent, candidate, state);
|
|
break;
|
|
default:
|
|
return Debug.assertNever(extensions);
|
|
}
|
|
}
|
|
|
|
const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => {
|
|
const fromFile = tryFile(candidate, onlyRecordFailures, state);
|
|
if (fromFile) {
|
|
const resolved = resolvedIfExtensionMatches(extensions, fromFile);
|
|
if (resolved) {
|
|
return noPackageId(resolved);
|
|
}
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.File_0_has_an_unsupported_extension_so_skipping_it, fromFile);
|
|
}
|
|
}
|
|
|
|
// Even if extensions is DtsOnly, we can still look up a .ts file as a result of package.json "types"
|
|
const nextExtensions = extensions === Extensions.DtsOnly ? Extensions.TypeScript : extensions;
|
|
// Don't do package.json lookup recursively, because Node.js' package lookup doesn't.
|
|
return nodeLoadModuleByRelativeName(nextExtensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ false);
|
|
};
|
|
|
|
const onlyRecordFailuresForPackageFile = packageFile ? !directoryProbablyExists(getDirectoryPath(packageFile), state.host) : undefined;
|
|
const onlyRecordFailuresForIndex = onlyRecordFailures || !directoryProbablyExists(candidate, state.host);
|
|
const indexPath = combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index");
|
|
|
|
if (versionPaths && (!packageFile || containsPath(candidate, packageFile))) {
|
|
const moduleName = getRelativePathFromDirectory(candidate, packageFile || indexPath, /*ignoreCase*/ false);
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.version, version, moduleName);
|
|
}
|
|
const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, /*pathPatterns*/ undefined, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state);
|
|
if (result) {
|
|
return removeIgnoredPackageId(result.value);
|
|
}
|
|
}
|
|
|
|
// It won't have a `packageId` set, because we disabled `considerPackageJson`.
|
|
const packageFileResult = packageFile && removeIgnoredPackageId(loader(extensions, packageFile, onlyRecordFailuresForPackageFile!, state));
|
|
if (packageFileResult) return packageFileResult;
|
|
|
|
return loadModuleFromFile(extensions, indexPath, onlyRecordFailuresForIndex, state);
|
|
}
|
|
|
|
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
|
|
function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined {
|
|
const ext = tryGetExtensionFromPath(path);
|
|
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined;
|
|
}
|
|
|
|
/** True if `extension` is one of the supported `extensions`. */
|
|
function extensionIsOk(extensions: Extensions, extension: Extension): boolean {
|
|
switch (extensions) {
|
|
case Extensions.JavaScript:
|
|
return extension === Extension.Js || extension === Extension.Jsx;
|
|
case Extensions.TSConfig:
|
|
case Extensions.Json:
|
|
return extension === Extension.Json;
|
|
case Extensions.TypeScript:
|
|
return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Dts;
|
|
case Extensions.DtsOnly:
|
|
return extension === Extension.Dts;
|
|
}
|
|
}
|
|
|
|
/* @internal */
|
|
export function parsePackageName(moduleName: string): { packageName: string, rest: string } {
|
|
let idx = moduleName.indexOf(directorySeparator);
|
|
if (moduleName[0] === "@") {
|
|
idx = moduleName.indexOf(directorySeparator, idx + 1);
|
|
}
|
|
return idx === -1 ? { packageName: moduleName, rest: "" } : { packageName: moduleName.slice(0, idx), rest: moduleName.slice(idx + 1) };
|
|
}
|
|
|
|
function loadModuleFromNearestNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: NonRelativeModuleNameResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult<Resolved> {
|
|
return loadModuleFromNearestNodeModulesDirectoryWorker(extensions, moduleName, directory, state, /*typesScopeOnly*/ false, cache, redirectedReference);
|
|
}
|
|
|
|
function loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName: string, directory: string, state: ModuleResolutionState): SearchResult<Resolved> {
|
|
// Extensions parameter here doesn't actually matter, because typesOnly ensures we're just doing @types lookup, which is always DtsOnly.
|
|
return loadModuleFromNearestNodeModulesDirectoryWorker(Extensions.DtsOnly, moduleName, directory, state, /*typesScopeOnly*/ true, /*cache*/ undefined, /*redirectedReference*/ undefined);
|
|
}
|
|
|
|
function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: NonRelativeModuleNameResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult<Resolved> {
|
|
const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, redirectedReference);
|
|
return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => {
|
|
if (getBaseFileName(ancestorDirectory) !== "node_modules") {
|
|
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, ancestorDirectory, state);
|
|
if (resolutionFromCache) {
|
|
return resolutionFromCache;
|
|
}
|
|
return toSearchResult(loadModuleFromImmediateNodeModulesDirectory(extensions, moduleName, ancestorDirectory, state, typesScopeOnly));
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean): Resolved | undefined {
|
|
const nodeModulesFolder = combinePaths(directory, "node_modules");
|
|
const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host);
|
|
if (!nodeModulesFolderExists && state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder);
|
|
}
|
|
|
|
const packageResult = typesScopeOnly ? undefined : loadModuleFromSpecificNodeModulesDirectory(extensions, moduleName, nodeModulesFolder, nodeModulesFolderExists, state);
|
|
if (packageResult) {
|
|
return packageResult;
|
|
}
|
|
if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) {
|
|
const nodeModulesAtTypes = combinePaths(nodeModulesFolder, "@types");
|
|
let nodeModulesAtTypesExists = nodeModulesFolderExists;
|
|
if (nodeModulesFolderExists && !directoryProbablyExists(nodeModulesAtTypes, state.host)) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes);
|
|
}
|
|
nodeModulesAtTypesExists = false;
|
|
}
|
|
return loadModuleFromSpecificNodeModulesDirectory(Extensions.DtsOnly, mangleScopedPackageNameWithTrace(moduleName, state), nodeModulesAtTypes, nodeModulesAtTypesExists, state);
|
|
}
|
|
}
|
|
|
|
function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState): Resolved | undefined {
|
|
const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName));
|
|
|
|
// First look for a nested package.json, as in `node_modules/foo/bar/package.json`.
|
|
let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state);
|
|
if (packageInfo) {
|
|
const fromFile = loadModuleFromFile(extensions, candidate, !nodeModulesDirectoryExists, state);
|
|
if (fromFile) {
|
|
return noPackageId(fromFile);
|
|
}
|
|
|
|
const fromDirectory = loadNodeModuleFromDirectoryWorker(
|
|
extensions,
|
|
candidate,
|
|
!nodeModulesDirectoryExists,
|
|
state,
|
|
packageInfo.packageJsonContent,
|
|
packageInfo.versionPaths
|
|
);
|
|
return withPackageId(packageInfo, fromDirectory);
|
|
}
|
|
|
|
const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => {
|
|
const pathAndExtension =
|
|
loadModuleFromFile(extensions, candidate, onlyRecordFailures, state) ||
|
|
loadNodeModuleFromDirectoryWorker(
|
|
extensions,
|
|
candidate,
|
|
onlyRecordFailures,
|
|
state,
|
|
packageInfo && packageInfo.packageJsonContent,
|
|
packageInfo && packageInfo.versionPaths
|
|
);
|
|
return withPackageId(packageInfo, pathAndExtension);
|
|
};
|
|
|
|
const { packageName, rest } = parsePackageName(moduleName);
|
|
if (rest !== "") { // If "rest" is empty, we just did this search above.
|
|
const packageDirectory = combinePaths(nodeModulesDirectory, packageName);
|
|
|
|
// Don't use a "types" or "main" from here because we're not loading the root, but a subdirectory -- just here for the packageId and path mappings.
|
|
packageInfo = getPackageJsonInfo(packageDirectory, !nodeModulesDirectoryExists, state);
|
|
if (packageInfo && packageInfo.versionPaths) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, packageInfo.versionPaths.version, version, rest);
|
|
}
|
|
const packageDirectoryExists = nodeModulesDirectoryExists && directoryProbablyExists(packageDirectory, state.host);
|
|
const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, /*pathPatterns*/ undefined, loader, !packageDirectoryExists, state);
|
|
if (fromPaths) {
|
|
return fromPaths.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return loader(extensions, candidate, !nodeModulesDirectoryExists, state);
|
|
}
|
|
|
|
function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike<string[]>, pathPatterns: readonly (string | Pattern)[] | undefined, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult<Resolved> {
|
|
pathPatterns ||= tryParsePatterns(paths);
|
|
const matchedPattern = matchPatternOrExact(pathPatterns, moduleName);
|
|
if (matchedPattern) {
|
|
const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName);
|
|
const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern);
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText);
|
|
}
|
|
const resolved = forEach(paths[matchedPatternText], subst => {
|
|
const path = matchedStar ? subst.replace("*", matchedStar) : subst;
|
|
// When baseUrl is not specified, the command line parser resolves relative paths to the config file location.
|
|
const candidate = normalizePath(combinePaths(baseDirectory, path));
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path);
|
|
}
|
|
// A path mapping may have an extension, in contrast to an import, which should omit it.
|
|
const extension = tryGetExtensionFromPath(subst);
|
|
if (extension !== undefined) {
|
|
const path = tryFile(candidate, onlyRecordFailures, state);
|
|
if (path !== undefined) {
|
|
return noPackageId({ path, ext: extension });
|
|
}
|
|
}
|
|
return loader(extensions, candidate, onlyRecordFailures || !directoryProbablyExists(getDirectoryPath(candidate), state.host), state);
|
|
});
|
|
return { value: resolved };
|
|
}
|
|
}
|
|
|
|
/** Double underscores are used in DefinitelyTyped to delimit scoped packages. */
|
|
const mangledScopedPackageSeparator = "__";
|
|
|
|
/** For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. */
|
|
function mangleScopedPackageNameWithTrace(packageName: string, state: ModuleResolutionState): string {
|
|
const mangled = mangleScopedPackageName(packageName);
|
|
if (state.traceEnabled && mangled !== packageName) {
|
|
trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled);
|
|
}
|
|
return mangled;
|
|
}
|
|
|
|
/* @internal */
|
|
export function getTypesPackageName(packageName: string): string {
|
|
return `@types/${mangleScopedPackageName(packageName)}`;
|
|
}
|
|
|
|
/* @internal */
|
|
export function mangleScopedPackageName(packageName: string): string {
|
|
if (startsWith(packageName, "@")) {
|
|
const replaceSlash = packageName.replace(directorySeparator, mangledScopedPackageSeparator);
|
|
if (replaceSlash !== packageName) {
|
|
return replaceSlash.slice(1); // Take off the "@"
|
|
}
|
|
}
|
|
return packageName;
|
|
}
|
|
|
|
/* @internal */
|
|
export function getPackageNameFromTypesPackageName(mangledName: string): string {
|
|
const withoutAtTypePrefix = removePrefix(mangledName, "@types/");
|
|
if (withoutAtTypePrefix !== mangledName) {
|
|
return unmangleScopedPackageName(withoutAtTypePrefix);
|
|
}
|
|
return mangledName;
|
|
}
|
|
|
|
/* @internal */
|
|
export function unmangleScopedPackageName(typesPackageName: string): string {
|
|
return stringContains(typesPackageName, mangledScopedPackageSeparator) ?
|
|
"@" + typesPackageName.replace(mangledScopedPackageSeparator, directorySeparator) :
|
|
typesPackageName;
|
|
}
|
|
|
|
function tryFindNonRelativeModuleNameInCache(cache: PerModuleNameCache | undefined, moduleName: string, containingDirectory: string, state: ModuleResolutionState): SearchResult<Resolved> {
|
|
const result = cache && cache.get(containingDirectory);
|
|
if (result) {
|
|
if (state.traceEnabled) {
|
|
trace(state.host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory);
|
|
}
|
|
state.resultFromCache = result;
|
|
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, originalPath: result.resolvedModule.originalPath || true, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } };
|
|
}
|
|
}
|
|
|
|
export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations {
|
|
const traceEnabled = isTraceEnabled(compilerOptions, host);
|
|
const failedLookupLocations: string[] = [];
|
|
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache };
|
|
const containingDirectory = getDirectoryPath(containingFile);
|
|
|
|
const resolved = tryResolve(Extensions.TypeScript) || tryResolve(Extensions.JavaScript);
|
|
// No originalPath because classic resolution doesn't resolve realPath
|
|
return createResolvedModuleWithFailedLookupLocations(resolved && resolved.value, /*isExternalLibraryImport*/ false, failedLookupLocations, state.resultFromCache);
|
|
|
|
function tryResolve(extensions: Extensions): SearchResult<Resolved> {
|
|
const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, state);
|
|
if (resolvedUsingSettings) {
|
|
return { value: resolvedUsingSettings };
|
|
}
|
|
|
|
if (!isExternalModuleNameRelative(moduleName)) {
|
|
const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, redirectedReference);
|
|
// Climb up parent directories looking for a module.
|
|
const resolved = forEachAncestorDirectory(containingDirectory, directory => {
|
|
const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, directory, state);
|
|
if (resolutionFromCache) {
|
|
return resolutionFromCache;
|
|
}
|
|
const searchName = normalizePath(combinePaths(directory, moduleName));
|
|
return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, /*onlyRecordFailures*/ false, state));
|
|
});
|
|
if (resolved) {
|
|
return resolved;
|
|
}
|
|
if (extensions === Extensions.TypeScript) {
|
|
// If we didn't find the file normally, look it up in @types.
|
|
return loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state);
|
|
}
|
|
}
|
|
else {
|
|
const candidate = normalizePath(combinePaths(containingDirectory, moduleName));
|
|
return toSearchResult(loadModuleFromFileNoPackageId(extensions, candidate, /*onlyRecordFailures*/ false, state));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A host may load a module from a global cache of typings.
|
|
* This is the minumum code needed to expose that functionality; the rest is in the host.
|
|
*/
|
|
/* @internal */
|
|
export function loadModuleFromGlobalCache(moduleName: string, projectName: string | undefined, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string, packageJsonInfoCache: PackageJsonInfoCache): ResolvedModuleWithFailedLookupLocations {
|
|
const traceEnabled = isTraceEnabled(compilerOptions, host);
|
|
if (traceEnabled) {
|
|
trace(host, Diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, projectName, moduleName, globalCache);
|
|
}
|
|
const failedLookupLocations: string[] = [];
|
|
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache };
|
|
const resolved = loadModuleFromImmediateNodeModulesDirectory(Extensions.DtsOnly, moduleName, globalCache, state, /*typesScopeOnly*/ false);
|
|
return createResolvedModuleWithFailedLookupLocations(resolved, /*isExternalLibraryImport*/ true, failedLookupLocations, state.resultFromCache);
|
|
}
|
|
|
|
/**
|
|
* Represents result of search. Normally when searching among several alternatives we treat value `undefined` as indicator
|
|
* that search fails and we should try another option.
|
|
* However this does not allow us to represent final result that should be used instead of further searching (i.e. a final result that was found in cache).
|
|
* SearchResult is used to deal with this issue, its values represents following outcomes:
|
|
* - undefined - not found, continue searching
|
|
* - { value: undefined } - not found - stop searching
|
|
* - { value: <some-value> } - found - stop searching
|
|
*/
|
|
type SearchResult<T> = { value: T | undefined } | undefined;
|
|
|
|
/**
|
|
* Wraps value to SearchResult.
|
|
* @returns undefined if value is undefined or { value } otherwise
|
|
*/
|
|
function toSearchResult<T>(value: T | undefined): SearchResult<T> {
|
|
return value !== undefined ? { value } : undefined;
|
|
}
|
|
}
|