Spaces:
Running
Running
/* | |
MIT License http://www.opensource.org/licenses/mit-license.php | |
Author Tobias Koppers @sokra | |
*/ | |
; | |
const versions = require("process").versions; | |
const Resolver = require("./Resolver"); | |
const { getType, PathType } = require("./util/path"); | |
const SyncAsyncFileSystemDecorator = require("./SyncAsyncFileSystemDecorator"); | |
const AliasFieldPlugin = require("./AliasFieldPlugin"); | |
const AliasPlugin = require("./AliasPlugin"); | |
const AppendPlugin = require("./AppendPlugin"); | |
const ConditionalPlugin = require("./ConditionalPlugin"); | |
const DescriptionFilePlugin = require("./DescriptionFilePlugin"); | |
const DirectoryExistsPlugin = require("./DirectoryExistsPlugin"); | |
const ExportsFieldPlugin = require("./ExportsFieldPlugin"); | |
const ExtensionAliasPlugin = require("./ExtensionAliasPlugin"); | |
const FileExistsPlugin = require("./FileExistsPlugin"); | |
const ImportsFieldPlugin = require("./ImportsFieldPlugin"); | |
const JoinRequestPartPlugin = require("./JoinRequestPartPlugin"); | |
const JoinRequestPlugin = require("./JoinRequestPlugin"); | |
const MainFieldPlugin = require("./MainFieldPlugin"); | |
const ModulesInHierarchicalDirectoriesPlugin = require("./ModulesInHierarchicalDirectoriesPlugin"); | |
const ModulesInRootPlugin = require("./ModulesInRootPlugin"); | |
const NextPlugin = require("./NextPlugin"); | |
const ParsePlugin = require("./ParsePlugin"); | |
const PnpPlugin = require("./PnpPlugin"); | |
const RestrictionsPlugin = require("./RestrictionsPlugin"); | |
const ResultPlugin = require("./ResultPlugin"); | |
const RootsPlugin = require("./RootsPlugin"); | |
const SelfReferencePlugin = require("./SelfReferencePlugin"); | |
const SymlinkPlugin = require("./SymlinkPlugin"); | |
const TryNextPlugin = require("./TryNextPlugin"); | |
const UnsafeCachePlugin = require("./UnsafeCachePlugin"); | |
const UseFilePlugin = require("./UseFilePlugin"); | |
/** @typedef {import("./AliasPlugin").AliasOption} AliasOptionEntry */ | |
/** @typedef {import("./ExtensionAliasPlugin").ExtensionAliasOption} ExtensionAliasOption */ | |
/** @typedef {import("./PnpPlugin").PnpApiImpl} PnpApi */ | |
/** @typedef {import("./Resolver").EnsuredHooks} EnsuredHooks */ | |
/** @typedef {import("./Resolver").FileSystem} FileSystem */ | |
/** @typedef {import("./Resolver").KnownHooks} KnownHooks */ | |
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */ | |
/** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */ | |
/** @typedef {string|string[]|false} AliasOptionNewRequest */ | |
/** @typedef {{[k: string]: AliasOptionNewRequest}} AliasOptions */ | |
/** @typedef {{[k: string]: string|string[] }} ExtensionAliasOptions */ | |
/** @typedef {false | 0 | "" | null | undefined} Falsy */ | |
/** @typedef {{apply: function(Resolver): void} | (function(this: Resolver, Resolver): void) | Falsy} Plugin */ | |
/** | |
* @typedef {Object} UserResolveOptions | |
* @property {(AliasOptions | AliasOptionEntry[])=} alias A list of module alias configurations or an object which maps key to value | |
* @property {(AliasOptions | AliasOptionEntry[])=} fallback A list of module alias configurations or an object which maps key to value, applied only after modules option | |
* @property {ExtensionAliasOptions=} extensionAlias An object which maps extension to extension aliases | |
* @property {(string | string[])[]=} aliasFields A list of alias fields in description files | |
* @property {(function(ResolveRequest): boolean)=} cachePredicate A function which decides whether a request should be cached or not. An object is passed with at least `path` and `request` properties. | |
* @property {boolean=} cacheWithContext Whether or not the unsafeCache should include request context as part of the cache key. | |
* @property {string[]=} descriptionFiles A list of description files to read from | |
* @property {string[]=} conditionNames A list of exports field condition names. | |
* @property {boolean=} enforceExtension Enforce that a extension from extensions must be used | |
* @property {(string | string[])[]=} exportsFields A list of exports fields in description files | |
* @property {(string | string[])[]=} importsFields A list of imports fields in description files | |
* @property {string[]=} extensions A list of extensions which should be tried for files | |
* @property {FileSystem} fileSystem The file system which should be used | |
* @property {(object | boolean)=} unsafeCache Use this cache object to unsafely cache the successful requests | |
* @property {boolean=} symlinks Resolve symlinks to their symlinked location | |
* @property {Resolver=} resolver A prepared Resolver to which the plugins are attached | |
* @property {string[] | string=} modules A list of directories to resolve modules from, can be absolute path or folder name | |
* @property {(string | string[] | {name: string | string[], forceRelative: boolean})[]=} mainFields A list of main fields in description files | |
* @property {string[]=} mainFiles A list of main files in directories | |
* @property {Plugin[]=} plugins A list of additional resolve plugins which should be applied | |
* @property {PnpApi | null=} pnpApi A PnP API that should be used - null is "never", undefined is "auto" | |
* @property {string[]=} roots A list of root paths | |
* @property {boolean=} fullySpecified The request is already fully specified and no extensions or directories are resolved for it | |
* @property {boolean=} resolveToContext Resolve to a context instead of a file | |
* @property {(string|RegExp)[]=} restrictions A list of resolve restrictions | |
* @property {boolean=} useSyncFileSystemCalls Use only the sync constraints of the file system calls | |
* @property {boolean=} preferRelative Prefer to resolve module requests as relative requests before falling back to modules | |
* @property {boolean=} preferAbsolute Prefer to resolve server-relative urls as absolute paths before falling back to resolve in roots | |
*/ | |
/** | |
* @typedef {Object} ResolveOptions | |
* @property {AliasOptionEntry[]} alias | |
* @property {AliasOptionEntry[]} fallback | |
* @property {Set<string | string[]>} aliasFields | |
* @property {ExtensionAliasOption[]} extensionAlias | |
* @property {(function(ResolveRequest): boolean)} cachePredicate | |
* @property {boolean} cacheWithContext | |
* @property {Set<string>} conditionNames A list of exports field condition names. | |
* @property {string[]} descriptionFiles | |
* @property {boolean} enforceExtension | |
* @property {Set<string | string[]>} exportsFields | |
* @property {Set<string | string[]>} importsFields | |
* @property {Set<string>} extensions | |
* @property {FileSystem} fileSystem | |
* @property {object | false} unsafeCache | |
* @property {boolean} symlinks | |
* @property {Resolver=} resolver | |
* @property {Array<string | string[]>} modules | |
* @property {{name: string[], forceRelative: boolean}[]} mainFields | |
* @property {Set<string>} mainFiles | |
* @property {Plugin[]} plugins | |
* @property {PnpApi | null} pnpApi | |
* @property {Set<string>} roots | |
* @property {boolean} fullySpecified | |
* @property {boolean} resolveToContext | |
* @property {Set<string|RegExp>} restrictions | |
* @property {boolean} preferRelative | |
* @property {boolean} preferAbsolute | |
*/ | |
/** | |
* @param {PnpApi | null=} option option | |
* @returns {PnpApi | null} processed option | |
*/ | |
function processPnpApiOption(option) { | |
if ( | |
option === undefined && | |
/** @type {NodeJS.ProcessVersions & {pnp: string}} */ versions.pnp | |
) { | |
// @ts-ignore | |
return require("pnpapi"); // eslint-disable-line node/no-missing-require | |
} | |
return option || null; | |
} | |
/** | |
* @param {AliasOptions | AliasOptionEntry[] | undefined} alias alias | |
* @returns {AliasOptionEntry[]} normalized aliases | |
*/ | |
function normalizeAlias(alias) { | |
return typeof alias === "object" && !Array.isArray(alias) && alias !== null | |
? Object.keys(alias).map(key => { | |
/** @type {AliasOptionEntry} */ | |
const obj = { name: key, onlyModule: false, alias: alias[key] }; | |
if (/\$$/.test(key)) { | |
obj.onlyModule = true; | |
obj.name = key.slice(0, -1); | |
} | |
return obj; | |
}) | |
: /** @type {Array<AliasOptionEntry>} */ (alias) || []; | |
} | |
/** | |
* @param {UserResolveOptions} options input options | |
* @returns {ResolveOptions} output options | |
*/ | |
function createOptions(options) { | |
const mainFieldsSet = new Set(options.mainFields || ["main"]); | |
/** @type {ResolveOptions["mainFields"]} */ | |
const mainFields = []; | |
for (const item of mainFieldsSet) { | |
if (typeof item === "string") { | |
mainFields.push({ | |
name: [item], | |
forceRelative: true | |
}); | |
} else if (Array.isArray(item)) { | |
mainFields.push({ | |
name: item, | |
forceRelative: true | |
}); | |
} else { | |
mainFields.push({ | |
name: Array.isArray(item.name) ? item.name : [item.name], | |
forceRelative: item.forceRelative | |
}); | |
} | |
} | |
return { | |
alias: normalizeAlias(options.alias), | |
fallback: normalizeAlias(options.fallback), | |
aliasFields: new Set(options.aliasFields), | |
cachePredicate: | |
options.cachePredicate || | |
function () { | |
return true; | |
}, | |
cacheWithContext: | |
typeof options.cacheWithContext !== "undefined" | |
? options.cacheWithContext | |
: true, | |
exportsFields: new Set(options.exportsFields || ["exports"]), | |
importsFields: new Set(options.importsFields || ["imports"]), | |
conditionNames: new Set(options.conditionNames), | |
descriptionFiles: Array.from( | |
new Set(options.descriptionFiles || ["package.json"]) | |
), | |
enforceExtension: | |
options.enforceExtension === undefined | |
? options.extensions && options.extensions.includes("") | |
? true | |
: false | |
: options.enforceExtension, | |
extensions: new Set(options.extensions || [".js", ".json", ".node"]), | |
extensionAlias: options.extensionAlias | |
? Object.keys(options.extensionAlias).map(k => ({ | |
extension: k, | |
alias: /** @type {ExtensionAliasOptions} */ (options.extensionAlias)[ | |
k | |
] | |
})) | |
: [], | |
fileSystem: options.useSyncFileSystemCalls | |
? new SyncAsyncFileSystemDecorator( | |
/** @type {SyncFileSystem} */ ( | |
/** @type {unknown} */ (options.fileSystem) | |
) | |
) | |
: options.fileSystem, | |
unsafeCache: | |
options.unsafeCache && typeof options.unsafeCache !== "object" | |
? {} | |
: options.unsafeCache || false, | |
symlinks: typeof options.symlinks !== "undefined" ? options.symlinks : true, | |
resolver: options.resolver, | |
modules: mergeFilteredToArray( | |
Array.isArray(options.modules) | |
? options.modules | |
: options.modules | |
? [options.modules] | |
: ["node_modules"], | |
item => { | |
const type = getType(item); | |
return type === PathType.Normal || type === PathType.Relative; | |
} | |
), | |
mainFields, | |
mainFiles: new Set(options.mainFiles || ["index"]), | |
plugins: options.plugins || [], | |
pnpApi: processPnpApiOption(options.pnpApi), | |
roots: new Set(options.roots || undefined), | |
fullySpecified: options.fullySpecified || false, | |
resolveToContext: options.resolveToContext || false, | |
preferRelative: options.preferRelative || false, | |
preferAbsolute: options.preferAbsolute || false, | |
restrictions: new Set(options.restrictions) | |
}; | |
} | |
/** | |
* @param {UserResolveOptions} options resolve options | |
* @returns {Resolver} created resolver | |
*/ | |
exports.createResolver = function (options) { | |
const normalizedOptions = createOptions(options); | |
const { | |
alias, | |
fallback, | |
aliasFields, | |
cachePredicate, | |
cacheWithContext, | |
conditionNames, | |
descriptionFiles, | |
enforceExtension, | |
exportsFields, | |
extensionAlias, | |
importsFields, | |
extensions, | |
fileSystem, | |
fullySpecified, | |
mainFields, | |
mainFiles, | |
modules, | |
plugins: userPlugins, | |
pnpApi, | |
resolveToContext, | |
preferRelative, | |
preferAbsolute, | |
symlinks, | |
unsafeCache, | |
resolver: customResolver, | |
restrictions, | |
roots | |
} = normalizedOptions; | |
const plugins = userPlugins.slice(); | |
const resolver = customResolver | |
? customResolver | |
: new Resolver(fileSystem, normalizedOptions); | |
//// pipeline //// | |
resolver.ensureHook("resolve"); | |
resolver.ensureHook("internalResolve"); | |
resolver.ensureHook("newInternalResolve"); | |
resolver.ensureHook("parsedResolve"); | |
resolver.ensureHook("describedResolve"); | |
resolver.ensureHook("rawResolve"); | |
resolver.ensureHook("normalResolve"); | |
resolver.ensureHook("internal"); | |
resolver.ensureHook("rawModule"); | |
resolver.ensureHook("module"); | |
resolver.ensureHook("resolveAsModule"); | |
resolver.ensureHook("undescribedResolveInPackage"); | |
resolver.ensureHook("resolveInPackage"); | |
resolver.ensureHook("resolveInExistingDirectory"); | |
resolver.ensureHook("relative"); | |
resolver.ensureHook("describedRelative"); | |
resolver.ensureHook("directory"); | |
resolver.ensureHook("undescribedExistingDirectory"); | |
resolver.ensureHook("existingDirectory"); | |
resolver.ensureHook("undescribedRawFile"); | |
resolver.ensureHook("rawFile"); | |
resolver.ensureHook("file"); | |
resolver.ensureHook("finalFile"); | |
resolver.ensureHook("existingFile"); | |
resolver.ensureHook("resolved"); | |
// TODO remove in next major | |
// cspell:word Interal | |
// Backward-compat | |
// @ts-ignore | |
resolver.hooks.newInteralResolve = resolver.hooks.newInternalResolve; | |
// resolve | |
for (const { source, resolveOptions } of [ | |
{ source: "resolve", resolveOptions: { fullySpecified } }, | |
{ source: "internal-resolve", resolveOptions: { fullySpecified: false } } | |
]) { | |
if (unsafeCache) { | |
plugins.push( | |
new UnsafeCachePlugin( | |
source, | |
cachePredicate, | |
/** @type {import("./UnsafeCachePlugin").Cache} */ (unsafeCache), | |
cacheWithContext, | |
`new-${source}` | |
) | |
); | |
plugins.push( | |
new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve") | |
); | |
} else { | |
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve")); | |
} | |
} | |
// parsed-resolve | |
plugins.push( | |
new DescriptionFilePlugin( | |
"parsed-resolve", | |
descriptionFiles, | |
false, | |
"described-resolve" | |
) | |
); | |
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve")); | |
// described-resolve | |
plugins.push(new NextPlugin("described-resolve", "raw-resolve")); | |
if (fallback.length > 0) { | |
plugins.push( | |
new AliasPlugin("described-resolve", fallback, "internal-resolve") | |
); | |
} | |
// raw-resolve | |
if (alias.length > 0) { | |
plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve")); | |
} | |
aliasFields.forEach(item => { | |
plugins.push(new AliasFieldPlugin("raw-resolve", item, "internal-resolve")); | |
}); | |
extensionAlias.forEach(item => | |
plugins.push( | |
new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve") | |
) | |
); | |
plugins.push(new NextPlugin("raw-resolve", "normal-resolve")); | |
// normal-resolve | |
if (preferRelative) { | |
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); | |
} | |
plugins.push( | |
new ConditionalPlugin( | |
"after-normal-resolve", | |
{ module: true }, | |
"resolve as module", | |
false, | |
"raw-module" | |
) | |
); | |
plugins.push( | |
new ConditionalPlugin( | |
"after-normal-resolve", | |
{ internal: true }, | |
"resolve as internal import", | |
false, | |
"internal" | |
) | |
); | |
if (preferAbsolute) { | |
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); | |
} | |
if (roots.size > 0) { | |
plugins.push(new RootsPlugin("after-normal-resolve", roots, "relative")); | |
} | |
if (!preferRelative && !preferAbsolute) { | |
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); | |
} | |
// internal | |
importsFields.forEach(importsField => { | |
plugins.push( | |
new ImportsFieldPlugin( | |
"internal", | |
conditionNames, | |
importsField, | |
"relative", | |
"internal-resolve" | |
) | |
); | |
}); | |
// raw-module | |
exportsFields.forEach(exportsField => { | |
plugins.push( | |
new SelfReferencePlugin("raw-module", exportsField, "resolve-as-module") | |
); | |
}); | |
modules.forEach(item => { | |
if (Array.isArray(item)) { | |
if (item.includes("node_modules") && pnpApi) { | |
plugins.push( | |
new ModulesInHierarchicalDirectoriesPlugin( | |
"raw-module", | |
item.filter(i => i !== "node_modules"), | |
"module" | |
) | |
); | |
plugins.push( | |
new PnpPlugin("raw-module", pnpApi, "undescribed-resolve-in-package") | |
); | |
} else { | |
plugins.push( | |
new ModulesInHierarchicalDirectoriesPlugin( | |
"raw-module", | |
item, | |
"module" | |
) | |
); | |
} | |
} else { | |
plugins.push(new ModulesInRootPlugin("raw-module", item, "module")); | |
} | |
}); | |
// module | |
plugins.push(new JoinRequestPartPlugin("module", "resolve-as-module")); | |
// resolve-as-module | |
if (!resolveToContext) { | |
plugins.push( | |
new ConditionalPlugin( | |
"resolve-as-module", | |
{ directory: false, request: "." }, | |
"single file module", | |
true, | |
"undescribed-raw-file" | |
) | |
); | |
} | |
plugins.push( | |
new DirectoryExistsPlugin( | |
"resolve-as-module", | |
"undescribed-resolve-in-package" | |
) | |
); | |
// undescribed-resolve-in-package | |
plugins.push( | |
new DescriptionFilePlugin( | |
"undescribed-resolve-in-package", | |
descriptionFiles, | |
false, | |
"resolve-in-package" | |
) | |
); | |
plugins.push( | |
new NextPlugin("after-undescribed-resolve-in-package", "resolve-in-package") | |
); | |
// resolve-in-package | |
exportsFields.forEach(exportsField => { | |
plugins.push( | |
new ExportsFieldPlugin( | |
"resolve-in-package", | |
conditionNames, | |
exportsField, | |
"relative" | |
) | |
); | |
}); | |
plugins.push( | |
new NextPlugin("resolve-in-package", "resolve-in-existing-directory") | |
); | |
// resolve-in-existing-directory | |
plugins.push( | |
new JoinRequestPlugin("resolve-in-existing-directory", "relative") | |
); | |
// relative | |
plugins.push( | |
new DescriptionFilePlugin( | |
"relative", | |
descriptionFiles, | |
true, | |
"described-relative" | |
) | |
); | |
plugins.push(new NextPlugin("after-relative", "described-relative")); | |
// described-relative | |
if (resolveToContext) { | |
plugins.push(new NextPlugin("described-relative", "directory")); | |
} else { | |
plugins.push( | |
new ConditionalPlugin( | |
"described-relative", | |
{ directory: false }, | |
null, | |
true, | |
"raw-file" | |
) | |
); | |
plugins.push( | |
new ConditionalPlugin( | |
"described-relative", | |
{ fullySpecified: false }, | |
"as directory", | |
true, | |
"directory" | |
) | |
); | |
} | |
// directory | |
plugins.push( | |
new DirectoryExistsPlugin("directory", "undescribed-existing-directory") | |
); | |
if (resolveToContext) { | |
// undescribed-existing-directory | |
plugins.push(new NextPlugin("undescribed-existing-directory", "resolved")); | |
} else { | |
// undescribed-existing-directory | |
plugins.push( | |
new DescriptionFilePlugin( | |
"undescribed-existing-directory", | |
descriptionFiles, | |
false, | |
"existing-directory" | |
) | |
); | |
mainFiles.forEach(item => { | |
plugins.push( | |
new UseFilePlugin( | |
"undescribed-existing-directory", | |
item, | |
"undescribed-raw-file" | |
) | |
); | |
}); | |
// described-existing-directory | |
mainFields.forEach(item => { | |
plugins.push( | |
new MainFieldPlugin( | |
"existing-directory", | |
item, | |
"resolve-in-existing-directory" | |
) | |
); | |
}); | |
mainFiles.forEach(item => { | |
plugins.push( | |
new UseFilePlugin("existing-directory", item, "undescribed-raw-file") | |
); | |
}); | |
// undescribed-raw-file | |
plugins.push( | |
new DescriptionFilePlugin( | |
"undescribed-raw-file", | |
descriptionFiles, | |
true, | |
"raw-file" | |
) | |
); | |
plugins.push(new NextPlugin("after-undescribed-raw-file", "raw-file")); | |
// raw-file | |
plugins.push( | |
new ConditionalPlugin( | |
"raw-file", | |
{ fullySpecified: true }, | |
null, | |
false, | |
"file" | |
) | |
); | |
if (!enforceExtension) { | |
plugins.push(new TryNextPlugin("raw-file", "no extension", "file")); | |
} | |
extensions.forEach(item => { | |
plugins.push(new AppendPlugin("raw-file", item, "file")); | |
}); | |
// file | |
if (alias.length > 0) | |
plugins.push(new AliasPlugin("file", alias, "internal-resolve")); | |
aliasFields.forEach(item => { | |
plugins.push(new AliasFieldPlugin("file", item, "internal-resolve")); | |
}); | |
plugins.push(new NextPlugin("file", "final-file")); | |
// final-file | |
plugins.push(new FileExistsPlugin("final-file", "existing-file")); | |
// existing-file | |
if (symlinks) | |
plugins.push(new SymlinkPlugin("existing-file", "existing-file")); | |
plugins.push(new NextPlugin("existing-file", "resolved")); | |
} | |
const resolved = | |
/** @type {KnownHooks & EnsuredHooks} */ | |
(resolver.hooks).resolved; | |
// resolved | |
if (restrictions.size > 0) { | |
plugins.push(new RestrictionsPlugin(resolved, restrictions)); | |
} | |
plugins.push(new ResultPlugin(resolved)); | |
//// RESOLVER //// | |
for (const plugin of plugins) { | |
if (typeof plugin === "function") { | |
/** @type {function(this: Resolver, Resolver): void} */ | |
(plugin).call(resolver, resolver); | |
} else if (plugin) { | |
plugin.apply(resolver); | |
} | |
} | |
return resolver; | |
}; | |
/** | |
* Merging filtered elements | |
* @param {string[]} array source array | |
* @param {function(string): boolean} filter predicate | |
* @returns {Array<string | string[]>} merge result | |
*/ | |
function mergeFilteredToArray(array, filter) { | |
/** @type {Array<string | string[]>} */ | |
const result = []; | |
const set = new Set(array); | |
for (const item of set) { | |
if (filter(item)) { | |
const lastElement = | |
result.length > 0 ? result[result.length - 1] : undefined; | |
if (Array.isArray(lastElement)) { | |
lastElement.push(item); | |
} else { | |
result.push([item]); | |
} | |
} else { | |
result.push(item); | |
} | |
} | |
return result; | |
} | |