| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- */
- "use strict";
- const { AsyncSeriesBailHook, AsyncSeriesHook, SyncHook } = require("tapable");
- const createInnerContext = require("./createInnerContext");
- const { parseIdentifier } = require("./util/identifier");
- const {
- PathType,
- createCachedBasename,
- createCachedDirname,
- createCachedJoin,
- getType,
- normalize,
- } = require("./util/path");
- /* eslint-disable jsdoc/check-alignment */
- // TODO in the next major release use only `Promise.withResolvers()`
- const _withResolvers =
- // eslint-disable-next-line n/no-unsupported-features/es-syntax
- Promise.withResolvers
- ? /**
- * @param {Resolver} self resolver
- * @param {Context} context context information object
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext} resolveContext resolve context
- * @returns {Promise<string | false>} result
- */
- (self, context, path, request, resolveContext) => {
- // eslint-disable-next-line n/no-unsupported-features/es-syntax
- const { promise, resolve, reject } = Promise.withResolvers();
- self.resolve(context, path, request, resolveContext, (err, res) => {
- if (err) reject(err);
- else resolve(/** @type {string | false} */ (res));
- });
- return promise;
- }
- : /**
- * @param {Resolver} self resolver
- * @param {Context} context context information object
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext} resolveContext resolve context
- * @returns {Promise<string | false>} result
- */
- (self, context, path, request, resolveContext) =>
- new Promise((resolve, reject) => {
- self.resolve(context, path, request, resolveContext, (err, res) => {
- if (err) reject(err);
- else resolve(/** @type {string | false} */ (res));
- });
- });
- /* eslint-enable jsdoc/check-alignment */
- /** @typedef {import("./AliasUtils").AliasOption} AliasOption */
- /** @typedef {import("./util/path").CachedJoin} CachedJoin */
- /** @typedef {import("./util/path").CachedDirname} CachedDirname */
- /** @typedef {import("./util/path").CachedBasename} CachedBasename */
- /**
- * @typedef {object} JoinCacheEntry
- * @property {CachedJoin["fn"]} fn cached join function
- * @property {CachedJoin["cache"]} cache the underlying cache map
- */
- /**
- * @typedef {object} DirnameCacheEntry
- * @property {CachedDirname["fn"]} fn cached dirname function
- * @property {CachedDirname["cache"]} cache the underlying cache map
- */
- /**
- * @typedef {object} BasenameCacheEntry
- * @property {CachedBasename["fn"]} fn cached dirname function
- * @property {CachedBasename["cache"]} cache the underlying cache map
- */
- /**
- * @typedef {object} PathCacheFunctions
- * @property {JoinCacheEntry} join cached join
- * @property {DirnameCacheEntry} dirname cached dirname
- * @property {BasenameCacheEntry} basename cached basename
- */
- /** @type {WeakMap<FileSystem, PathCacheFunctions>} */
- const _pathCacheByFs = new WeakMap();
- const HASH_ESCAPE_RE = /#/g;
- /** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
- /**
- * @typedef {object} KnownContext
- * @property {string[]=} environments environments
- */
- // eslint-disable-next-line jsdoc/reject-any-type
- /** @typedef {KnownContext & Record<any, any>} Context */
- /** @typedef {Error & { details?: string }} ErrorWithDetail */
- /** @typedef {(err: ErrorWithDetail | null, res?: string | false, req?: ResolveRequest) => void} ResolveCallback */
- /**
- * @typedef {object} PossibleFileSystemError
- * @property {string=} code code
- * @property {number=} errno number
- * @property {string=} path path
- * @property {string=} syscall syscall
- */
- /**
- * @template T
- * @callback FileSystemCallback
- * @param {PossibleFileSystemError & Error | null} err
- * @param {T=} result
- */
- /**
- * @typedef {string | Buffer | URL} PathLike
- */
- /**
- * @typedef {PathLike | number} PathOrFileDescriptor
- */
- /**
- * @typedef {object} ObjectEncodingOptions
- * @property {BufferEncoding | null | undefined=} encoding encoding
- */
- /**
- * @typedef {ObjectEncodingOptions | BufferEncoding | undefined | null} EncodingOption
- */
- /** @typedef {(err: NodeJS.ErrnoException | null, result?: string) => void} StringCallback */
- /** @typedef {(err: NodeJS.ErrnoException | null, result?: Buffer) => void} BufferCallback */
- /** @typedef {(err: NodeJS.ErrnoException | null, result?: (string | Buffer)) => void} StringOrBufferCallback */
- /** @typedef {(err: NodeJS.ErrnoException | null, result?: IStats) => void} StatsCallback */
- /** @typedef {(err: NodeJS.ErrnoException | null, result?: IBigIntStats) => void} BigIntStatsCallback */
- /** @typedef {(err: NodeJS.ErrnoException | null, result?: (IStats | IBigIntStats)) => void} StatsOrBigIntStatsCallback */
- /** @typedef {(err: NodeJS.ErrnoException | Error | null, result?: JsonObject) => void} ReadJsonCallback */
- /**
- * @template T
- * @typedef {object} IStatsBase
- * @property {() => boolean} isFile is file
- * @property {() => boolean} isDirectory is directory
- * @property {() => boolean} isBlockDevice is block device
- * @property {() => boolean} isCharacterDevice is character device
- * @property {() => boolean} isSymbolicLink is symbolic link
- * @property {() => boolean} isFIFO is FIFO
- * @property {() => boolean} isSocket is socket
- * @property {T} dev dev
- * @property {T} ino ino
- * @property {T} mode mode
- * @property {T} nlink nlink
- * @property {T} uid uid
- * @property {T} gid gid
- * @property {T} rdev rdev
- * @property {T} size size
- * @property {T} blksize blksize
- * @property {T} blocks blocks
- * @property {T} atimeMs atime ms
- * @property {T} mtimeMs mtime ms
- * @property {T} ctimeMs ctime ms
- * @property {T} birthtimeMs birthtime ms
- * @property {Date} atime atime
- * @property {Date} mtime mtime
- * @property {Date} ctime ctime
- * @property {Date} birthtime birthtime
- */
- /**
- * @typedef {IStatsBase<number>} IStats
- */
- /**
- * @typedef {IStatsBase<bigint> & { atimeNs: bigint, mtimeNs: bigint, ctimeNs: bigint, birthtimeNs: bigint }} IBigIntStats
- */
- /**
- * @template {string | Buffer} [T=string]
- * @typedef {object} Dirent
- * @property {() => boolean} isFile true when is file, otherwise false
- * @property {() => boolean} isDirectory true when is directory, otherwise false
- * @property {() => boolean} isBlockDevice true when is block device, otherwise false
- * @property {() => boolean} isCharacterDevice true when is character device, otherwise false
- * @property {() => boolean} isSymbolicLink true when is symbolic link, otherwise false
- * @property {() => boolean} isFIFO true when is FIFO, otherwise false
- * @property {() => boolean} isSocket true when is socket, otherwise false
- * @property {T} name name
- * @property {string} parentPath path
- * @property {string=} path path
- */
- /**
- * @typedef {object} StatOptions
- * @property {(boolean | undefined)=} bigint need bigint values
- */
- /**
- * @typedef {object} StatSyncOptions
- * @property {(boolean | undefined)=} bigint need bigint values
- * @property {(boolean | undefined)=} throwIfNoEntry throw if no entry
- */
- /**
- * @typedef {{
- * (path: PathOrFileDescriptor, options: ({ encoding?: null | undefined, flag?: string | undefined } & import("events").Abortable) | undefined | null, callback: BufferCallback): void,
- * (path: PathOrFileDescriptor, options: ({ encoding: BufferEncoding, flag?: string | undefined } & import("events").Abortable) | BufferEncoding, callback: StringCallback): void,
- * (path: PathOrFileDescriptor, options: (ObjectEncodingOptions & { flag?: string | undefined } & import("events").Abortable) | BufferEncoding | undefined | null, callback: StringOrBufferCallback): void,
- * (path: PathOrFileDescriptor, callback: BufferCallback): void,
- * }} ReadFile
- */
- /**
- * @typedef {"buffer" | { encoding: "buffer" }} BufferEncodingOption
- */
- /**
- * @typedef {{
- * (path: PathOrFileDescriptor, options?: { encoding?: null | undefined, flag?: string | undefined } | null): Buffer,
- * (path: PathOrFileDescriptor, options: { encoding: BufferEncoding, flag?: string | undefined } | BufferEncoding): string,
- * (path: PathOrFileDescriptor, options?: (ObjectEncodingOptions & { flag?: string | undefined }) | BufferEncoding | null): string | Buffer,
- * }} ReadFileSync
- */
- /**
- * @typedef {{
- * (path: PathLike, options: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void,
- * (path: PathLike, options: { encoding: "buffer", withFileTypes?: false | undefined, recursive?: boolean | undefined } | "buffer", callback: (err: NodeJS.ErrnoException | null, files?: Buffer[]) => void): void,
- * (path: PathLike, options: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[] | Buffer[]) => void): void,
- * (path: PathLike, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void,
- * (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files?: Dirent<string>[]) => void): void,
- * (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files: Dirent<Buffer>[]) => void): void,
- * }} Readdir
- */
- /**
- * @typedef {{
- * (path: PathLike, options?: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | null): string[],
- * (path: PathLike, options: { encoding: "buffer", withFileTypes?: false | undefined, recursive?: boolean | undefined } | "buffer"): Buffer[],
- * (path: PathLike, options?: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | null): string[] | Buffer[],
- * (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }): Dirent[],
- * (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }): Dirent<Buffer>[],
- * }} ReaddirSync
- */
- /**
- * @typedef {(pathOrFileDescription: PathOrFileDescriptor, callback: ReadJsonCallback) => void} ReadJson
- */
- /**
- * @typedef {(pathOrFileDescription: PathOrFileDescriptor) => JsonObject} ReadJsonSync
- */
- /**
- * @typedef {{
- * (path: PathLike, options: EncodingOption, callback: StringCallback): void,
- * (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void,
- * (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void,
- * (path: PathLike, callback: StringCallback): void,
- * }} Readlink
- */
- /**
- * @typedef {{
- * (path: PathLike, options?: EncodingOption): string,
- * (path: PathLike, options: BufferEncodingOption): Buffer,
- * (path: PathLike, options?: EncodingOption): string | Buffer,
- * }} ReadlinkSync
- */
- /**
- * @typedef {{
- * (path: PathLike, callback: StatsCallback): void,
- * (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void,
- * (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void,
- * (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void,
- * }} LStat
- */
- /**
- * @typedef {{
- * (path: PathLike, options?: undefined): IStats,
- * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined,
- * (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined,
- * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats,
- * (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats,
- * (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats,
- * (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined,
- * }} LStatSync
- */
- /**
- * @typedef {{
- * (path: PathLike, callback: StatsCallback): void,
- * (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void,
- * (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void,
- * (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void,
- * }} Stat
- */
- /**
- * @typedef {{
- * (path: PathLike, options?: undefined): IStats,
- * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined,
- * (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined,
- * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats,
- * (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats,
- * (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats,
- * (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined,
- * }} StatSync
- */
- /**
- * @typedef {{
- * (path: PathLike, options: EncodingOption, callback: StringCallback): void,
- * (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void,
- * (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void,
- * (path: PathLike, callback: StringCallback): void,
- * }} RealPath
- */
- /**
- * @typedef {{
- * (path: PathLike, options?: EncodingOption): string,
- * (path: PathLike, options: BufferEncodingOption): Buffer,
- * (path: PathLike, options?: EncodingOption): string | Buffer,
- * }} RealPathSync
- */
- /**
- * @typedef {object} FileSystem
- * @property {ReadFile} readFile read file method
- * @property {Readdir} readdir readdir method
- * @property {ReadJson=} readJson read json method
- * @property {Readlink} readlink read link method
- * @property {LStat=} lstat lstat method
- * @property {Stat} stat stat method
- * @property {RealPath=} realpath realpath method
- */
- /**
- * @typedef {object} SyncFileSystem
- * @property {ReadFileSync} readFileSync read file sync method
- * @property {ReaddirSync} readdirSync read dir sync method
- * @property {ReadJsonSync=} readJsonSync read json sync method
- * @property {ReadlinkSync} readlinkSync read link sync method
- * @property {LStatSync=} lstatSync lstat sync method
- * @property {StatSync} statSync stat sync method
- * @property {RealPathSync=} realpathSync real path sync method
- */
- /**
- * @typedef {object} ParsedIdentifier
- * @property {string} request request
- * @property {string} query query
- * @property {string} fragment fragment
- * @property {boolean} directory is directory
- * @property {boolean} module is module
- * @property {boolean} file is file
- * @property {boolean} internal is internal
- */
- /** @typedef {string | number | boolean | null} JsonPrimitive */
- /** @typedef {JsonValue[]} JsonArray */
- /** @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue */
- /** @typedef {{ [Key in string]?: JsonValue | undefined }} JsonObject */
- /**
- * @typedef {object} TsconfigPathsMap
- * @property {TsconfigPathsData} main main tsconfig paths data
- * @property {string} mainContext main tsconfig base URL (absolute path)
- * @property {{ [baseUrl: string]: TsconfigPathsData }} refs referenced tsconfig paths data mapped by baseUrl
- * @property {{ [context: string]: TsconfigPathsData }} allContexts all contexts (main + refs) for quick lookup
- * @property {string[]} contextList precomputed `Object.keys(allContexts)` — read-only; used on the `_selectPathsDataForContext` hot path
- * @property {Set<string>} fileDependencies file dependencies
- */
- /**
- * @typedef {object} TsconfigPathsData
- * @property {import("./AliasUtils").CompiledAliasOption[]} alias tsconfig file data
- * @property {string[]} modules tsconfig file data
- */
- /**
- * @typedef {object} BaseResolveRequest
- * @property {string | false} path path
- * @property {Context=} context content
- * @property {string=} descriptionFilePath description file path
- * @property {string=} descriptionFileRoot description file root
- * @property {JsonObject=} descriptionFileData description file data
- * @property {TsconfigPathsMap | null | undefined=} tsconfigPathsMap tsconfig paths map
- * @property {string=} relativePath relative path
- * @property {boolean=} ignoreSymlinks true when need to ignore symlinks, otherwise false
- * @property {boolean=} fullySpecified true when full specified, otherwise false
- * @property {string=} __innerRequest inner request for internal usage
- * @property {string=} __innerRequest_request inner request for internal usage
- * @property {string=} __innerRequest_relativePath inner relative path for internal usage
- */
- /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
- /**
- * @template T
- * @typedef {{ add: (item: T) => void }} WriteOnlySet
- */
- /** @typedef {(request: ResolveRequest) => void} ResolveContextYield */
- /**
- * Singly-linked stack entry that also exposes a Set-like API
- * (`has`, `size`, iteration). Each `doResolve` call prepends a new
- * `StackEntry` that points at the previous tip via `.parent`, so pushing
- * is O(1) in time and memory. Recursion detection walks the linked list
- * (O(n)) but the stack is typically shallow, so this is cheaper overall
- * than cloning a `Set` per call.
- */
- class StackEntry {
- /**
- * @param {ResolveStepHook} hook hook
- * @param {ResolveRequest} request request
- * @param {StackEntry=} parent previous tip
- * @param {Set<string>=} preSeeded entries pre-seeded via the legacy `Set<string>` API
- */
- constructor(hook, request, parent, preSeeded) {
- this.name = hook.name;
- this.path = request.path;
- this.request = request.request || "";
- this.query = request.query || "";
- this.fragment = request.fragment || "";
- this.directory = Boolean(request.directory);
- this.module = Boolean(request.module);
- /** @type {StackEntry | undefined} */
- this.parent = parent;
- /**
- * Strings seeded by callers that still pass `stack: new Set([...])`.
- * Propagated through the chain so deeper `doResolve` calls still see
- * them during recursion checks. `undefined` in the common case so
- * there is no extra work on the hot path.
- * @type {Set<string> | undefined}
- */
- this.preSeeded = preSeeded;
- }
- /**
- * Walk the linked list looking for an entry with the same request shape.
- * Set-compatible: callers that used `stack.has(entry)` keep working.
- *
- * NOTE: kept monomorphic on purpose. An earlier draft accepted a string
- * query too (so pre-5.21 plugins keeping their own `Set<string>` of
- * seen entries could probe the live stack with the formatted form),
- * but adding the second shape regressed `doResolve`'s heap profile by
- * ~1 MiB / 200 resolves on stack-churn — V8 keeps a polymorphic
- * call-site state for `parent.has(stackEntry)` once `has` has two
- * argument shapes. Plugins that need string membership can reach for
- * `[...stack].find(e => e.includes(formattedString))` via the
- * `String`-method proxies on `StackEntry` instead.
- * @param {StackEntry} query entry to look for
- * @returns {boolean} whether the stack already contains an equivalent entry
- */
- has(query) {
- /** @type {StackEntry | undefined} */
- let node = this;
- while (node) {
- if (
- node.name === query.name &&
- node.path === query.path &&
- node.request === query.request &&
- node.query === query.query &&
- node.fragment === query.fragment &&
- node.directory === query.directory &&
- node.module === query.module
- ) {
- return true;
- }
- node = node.parent;
- }
- return this.preSeeded !== undefined && this.preSeeded.has(query.toString());
- }
- /**
- * Number of entries on the stack (oldest-to-newest length).
- * @returns {number} size
- */
- get size() {
- let count = this.preSeeded ? this.preSeeded.size : 0;
- /** @type {StackEntry | undefined} */
- let node = this;
- while (node) {
- count++;
- node = node.parent;
- }
- return count;
- }
- /**
- * Iterate entries from oldest (root) to newest (tip), matching how a
- * `Set` that was populated in insertion order would iterate. Pre-seeded
- * legacy `Set<string>` entries come first so error-message output stays
- * ordered oldest-to-newest.
- *
- * Yields each entry as its formatted `toString()` form. Plugins written
- * against the pre-5.21 `Set<string>` shape — e.g.
- * `[...resolveContext.stack].find(a => a.includes("module:"))` — keep
- * working unchanged because each yielded value is a plain string with
- * all of `String.prototype` available natively. Resolves that never
- * iterate the stack pay nothing; iteration costs one `toString()`
- * allocation per stack frame.
- * @returns {IterableIterator<string>} iterator
- */
- *[Symbol.iterator]() {
- if (this.preSeeded !== undefined) {
- for (const entry of this.preSeeded) yield entry;
- }
- /** @type {StackEntry[]} */
- const entries = [];
- /** @type {StackEntry | undefined} */
- let node = this;
- while (node) {
- entries.push(node);
- node = node.parent;
- }
- for (let i = entries.length - 1; i >= 0; i--) yield entries[i].toString();
- }
- /**
- * Human-readable form used in recursion error messages, logs, and the
- * iterator above. Not memoized: caching would require an extra slot on
- * every `StackEntry`, which costs heap even on resolves that never look
- * at the formatted form.
- * @returns {string} formatted entry
- */
- toString() {
- return `${this.name}: (${this.path}) ${this.request}${this.query}${
- this.fragment
- }${this.directory ? " directory" : ""}${this.module ? " module" : ""}`;
- }
- }
- /**
- * Resolve context
- * @typedef {object} ResolveContext
- * @property {WriteOnlySet<string>=} contextDependencies directories that was found on file system
- * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
- * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
- * @property {StackEntry | Set<string>=} stack tip of the resolver call stack (a singly-linked list with Set-like API). For instance, `resolve → parsedResolve → describedResolve`. Accepts a legacy `Set<string>` for back-compat with older callers; it is normalized internally without a hot-path branch.
- * @property {((str: string) => void)=} log log function
- * @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
- */
- /** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
- /**
- * @typedef {object} KnownHooks
- * @property {SyncHook<[ResolveStepHook, ResolveRequest], void>} resolveStep resolve step hook
- * @property {SyncHook<[ResolveRequest, Error]>} noResolve no resolve hook
- * @property {ResolveStepHook} resolve resolve hook
- * @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result result hook
- */
- /**
- * @typedef {{ [key: string]: ResolveStepHook }} EnsuredHooks
- */
- /**
- * @param {string} str input string
- * @returns {string} in camel case
- */
- function toCamelCase(str) {
- return str.replace(/-([a-z])/g, (str) => str.slice(1).toUpperCase());
- }
- class Resolver {
- /**
- * @param {ResolveStepHook} hook hook
- * @param {ResolveRequest} request request
- * @param {StackEntry=} parent previous tip of the stack
- * @param {Set<string>=} preSeeded entries pre-seeded via the legacy `Set<string>` API
- * @returns {StackEntry} stack entry
- */
- static createStackEntry(hook, request, parent, preSeeded) {
- return new StackEntry(hook, request, parent, preSeeded);
- }
- /**
- * @param {FileSystem} fileSystem a filesystem
- * @param {ResolveOptions} options options
- */
- constructor(fileSystem, options) {
- /** @type {FileSystem} */
- this.fileSystem = fileSystem;
- /** @type {ResolveOptions} */
- this.options = options;
- let pathCache = _pathCacheByFs.get(fileSystem);
- if (!pathCache) {
- pathCache = {
- join: createCachedJoin(),
- dirname: createCachedDirname(),
- basename: createCachedBasename(),
- };
- _pathCacheByFs.set(fileSystem, pathCache);
- }
- /** @type {PathCacheFunctions} */
- this.pathCache = pathCache;
- /** @type {KnownHooks} */
- this.hooks = {
- resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
- noResolve: new SyncHook(["request", "error"], "noResolve"),
- resolve: new AsyncSeriesBailHook(
- ["request", "resolveContext"],
- "resolve",
- ),
- result: new AsyncSeriesHook(["result", "resolveContext"], "result"),
- };
- }
- /**
- * @param {string | ResolveStepHook} name hook name or hook itself
- * @returns {ResolveStepHook} the hook
- */
- ensureHook(name) {
- if (typeof name !== "string") {
- return name;
- }
- name = toCamelCase(name);
- if (name.startsWith("before")) {
- return /** @type {ResolveStepHook} */ (
- this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
- stage: -10,
- })
- );
- }
- if (name.startsWith("after")) {
- return /** @type {ResolveStepHook} */ (
- this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
- stage: 10,
- })
- );
- }
- /** @type {ResolveStepHook} */
- const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
- if (!hook) {
- /** @type {KnownHooks & EnsuredHooks} */
- (this.hooks)[name] = new AsyncSeriesBailHook(
- ["request", "resolveContext"],
- name,
- );
- return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
- }
- return hook;
- }
- /**
- * @param {string | ResolveStepHook} name hook name or hook itself
- * @returns {ResolveStepHook} the hook
- */
- getHook(name) {
- if (typeof name !== "string") {
- return name;
- }
- name = toCamelCase(name);
- if (name.startsWith("before")) {
- return /** @type {ResolveStepHook} */ (
- this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
- stage: -10,
- })
- );
- }
- if (name.startsWith("after")) {
- return /** @type {ResolveStepHook} */ (
- this.getHook(name[5].toLowerCase() + name.slice(6)).withOptions({
- stage: 10,
- })
- );
- }
- /** @type {ResolveStepHook} */
- const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
- if (!hook) {
- throw new Error(`Hook ${name} doesn't exist`);
- }
- return hook;
- }
- /**
- * @overload
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext=} resolveContext resolve context
- * @returns {string | false} result
- */
- /**
- * @overload
- * @param {Context} context context information object
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext=} resolveContext resolve context
- * @returns {string | false} result
- */
- /**
- * @param {Context | string} context context information object or context path when no context is provided
- * @param {string | ResolveContext=} path context path or resolve context when no context is provided
- * @param {string | ResolveContext=} request request string or resolve context when no context is provided
- * @param {ResolveContext=} resolveContext resolve context
- * @returns {string | false} result
- */
- resolveSync(context, path, request, resolveContext) {
- /** @type {Error | null | undefined} */
- let err;
- /** @type {string | false | undefined} */
- let result;
- let sync = false;
- // `|| {}` so the underlying `resolve()` hits its 5-arg fast path
- // (skips the overload-shifting prologue) regardless of whether the
- // caller supplied a resolveContext.
- this.resolve(
- /** @type {Context} */ (context),
- /** @type {string} */ (path),
- /** @type {string} */ (request),
- /** @type {ResolveContext} */ (resolveContext) || {},
- (_err, r) => {
- err = _err;
- result = r;
- sync = true;
- },
- );
- if (!sync) {
- throw new Error(
- "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!",
- );
- }
- if (err) throw err;
- if (result === undefined) throw new Error("No result");
- return result;
- }
- /**
- * @overload
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext=} resolveContext resolve context
- * @returns {Promise<string | false>} result
- */
- /**
- * @overload
- * @param {Context} context context information object
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext=} resolveContext resolve context
- * @returns {Promise<string | false>} result
- */
- /**
- * @param {Context | string} context context information object or context path when no context is provided
- * @param {string | ResolveContext=} path context path or resolve context when no context is provided
- * @param {string | ResolveContext=} request request string or resolve context when no context is provided
- * @param {ResolveContext=} resolveContext resolve context
- * @returns {Promise<string | false>} result
- */
- resolvePromise(context, path, request, resolveContext) {
- // `|| {}` ensures the 5-arg fast path inside `resolve()` is reached
- // even when the caller doesn't pass a resolveContext.
- return _withResolvers(
- this,
- /** @type {Context} */ (context),
- /** @type {string} */ (path),
- /** @type {string} */ (request),
- /** @type {ResolveContext} */ (resolveContext) || {},
- );
- }
- /**
- * @overload
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveCallback} callback callback function
- * @returns {void}
- */
- /**
- * @overload
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext} resolveContext resolve context
- * @param {ResolveCallback} callback callback function
- * @returns {void}
- */
- /**
- * @overload
- * @param {Context} context context information object
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveCallback} callback callback function
- * @returns {void}
- */
- /**
- * @overload
- * @param {Context} context context information object
- * @param {string} path context path
- * @param {string} request request string
- * @param {ResolveContext} resolveContext resolve context
- * @param {ResolveCallback} callback callback function
- * @returns {void}
- */
- /**
- * @param {Context | string} context context information object or context path when no context is provided
- * @param {string | ResolveContext | ResolveCallback=} path context path or (when no context) resolve context or callback
- * @param {string | ResolveContext | ResolveCallback=} request request string or (when no context) resolve context or callback
- * @param {ResolveContext | ResolveCallback=} resolveContext resolve context or callback when no resolve context is provided
- * @param {ResolveCallback=} callback callback function
- * @returns {void}
- */
- resolve(context, path, request, resolveContext, callback) {
- // Fast path for the common 5-arg call (`resolver.resolve(ctx, from,
- // req, resolveCtx, cb)`) — every call from `resolveSync` /
- // `resolvePromise` plus the vast majority of direct API callers.
- // PR #536 added runtime overload-shifting to support optional
- // `context` / `resolveContext`; that adds several `typeof` checks
- // per resolve which show up as a measurable instruction-count
- // regression on every benchmark that calls into this method. Skip
- // the shifting entirely when all 5 args are already well-typed.
- if (
- typeof callback === "function" &&
- typeof context === "object" &&
- context !== null &&
- typeof resolveContext === "object" &&
- resolveContext !== null
- ) {
- // proceed straight to per-arg validation below
- } else {
- // Slow path: shift positional args based on what was supplied.
- // Shift when context is omitted (first positional arg is the path string).
- if (typeof context === "string") {
- // Keep an already-supplied callback (resolveSync / resolvePromise
- // always pass one in the 5th position).
- if (typeof callback !== "function") {
- callback = /** @type {ResolveCallback | undefined} */ (
- resolveContext
- );
- }
- resolveContext =
- /** @type {ResolveContext | ResolveCallback | undefined} */ (request);
- request = /** @type {string} */ (path);
- path = context;
- context = {};
- }
- // 4-arg form: the resolveContext slot holds the callback.
- if (typeof resolveContext === "function") {
- callback = resolveContext;
- resolveContext = {};
- } else if (!resolveContext || typeof resolveContext !== "object") {
- resolveContext = {};
- }
- if (typeof callback !== "function") {
- throw new TypeError("callback argument is not a function");
- }
- if (!context || typeof context !== "object") {
- context = {};
- }
- }
- if (typeof path !== "string") {
- return callback(new Error("path argument is not a string"));
- }
- if (typeof request !== "string") {
- return callback(new Error("request argument is not a string"));
- }
- /** @type {ResolveRequest} */
- const obj = {
- context,
- path,
- request,
- };
- /** @type {ResolveContextYield | undefined} */
- let yield_;
- let yieldCalled = false;
- /** @type {ResolveContextYield | undefined} */
- let finishYield;
- if (typeof resolveContext.yield === "function") {
- const old = resolveContext.yield;
- /**
- * @param {ResolveRequest} obj object
- */
- yield_ = (obj) => {
- old(obj);
- yieldCalled = true;
- };
- /**
- * @param {ResolveRequest} result result
- * @returns {void}
- */
- finishYield = (result) => {
- if (result) {
- /** @type {ResolveContextYield} */ (yield_)(result);
- }
- callback(null);
- };
- }
- const message = `resolve '${request}' in '${path}'`;
- /**
- * @param {ResolveRequest} result result
- * @returns {void}
- */
- const finishResolved = (result) => {
- const resultPath = result.path;
- if (resultPath === false) return callback(null, false, result);
- const escapedPath = resultPath.includes("#")
- ? resultPath.replace(HASH_ESCAPE_RE, "\0#")
- : resultPath;
- const resultQuery = result.query;
- let escapedQuery;
- if (resultQuery) {
- escapedQuery = resultQuery.includes("#")
- ? resultQuery.replace(HASH_ESCAPE_RE, "\0#")
- : resultQuery;
- } else {
- escapedQuery = "";
- }
- return callback(
- null,
- `${escapedPath}${escapedQuery}${result.fragment || ""}`,
- result,
- );
- };
- /**
- * @param {string[]} log logs
- * @returns {void}
- */
- const finishWithoutResolve = (log) => {
- /**
- * @type {ErrorWithDetail}
- */
- const error = new Error(`Can't ${message}`);
- error.details = log.join("\n");
- this.hooks.noResolve.call(obj, error);
- return callback(error);
- };
- if (resolveContext.log) {
- // We need log anyway to capture it in case of an error
- const parentLog = resolveContext.log;
- /** @type {string[]} */
- const log = [];
- return this.doResolve(
- this.hooks.resolve,
- obj,
- message,
- {
- log: (msg) => {
- parentLog(msg);
- log.push(msg);
- },
- yield: yield_,
- fileDependencies: resolveContext.fileDependencies,
- contextDependencies: resolveContext.contextDependencies,
- missingDependencies: resolveContext.missingDependencies,
- stack: resolveContext.stack,
- },
- (err, result) => {
- if (err) return callback(err);
- if (yieldCalled || (result && yield_)) {
- return /** @type {ResolveContextYield} */ (finishYield)(
- /** @type {ResolveRequest} */ (result),
- );
- }
- if (result) return finishResolved(result);
- return finishWithoutResolve(log);
- },
- );
- }
- // Try to resolve assuming there is no error
- // We don't log stuff in this case
- return this.doResolve(
- this.hooks.resolve,
- obj,
- message,
- {
- log: undefined,
- yield: yield_,
- fileDependencies: resolveContext.fileDependencies,
- contextDependencies: resolveContext.contextDependencies,
- missingDependencies: resolveContext.missingDependencies,
- stack: resolveContext.stack,
- },
- (err, result) => {
- if (err) return callback(err);
- if (yieldCalled || (result && yield_)) {
- return /** @type {ResolveContextYield} */ (finishYield)(
- /** @type {ResolveRequest} */ (result),
- );
- }
- if (result) return finishResolved(result);
- // log is missing for the error details
- // so we redo the resolving for the log info
- // this is more expensive to the success case
- // is assumed by default
- /** @type {string[]} */
- const log = [];
- return this.doResolve(
- this.hooks.resolve,
- obj,
- message,
- {
- log: (msg) => log.push(msg),
- yield: yield_,
- stack: resolveContext.stack,
- },
- (err, result) => {
- if (err) return callback(err);
- // In a case that there is a race condition and yield will be called
- if (yieldCalled || (result && yield_)) {
- return /** @type {ResolveContextYield} */ (finishYield)(
- /** @type {ResolveRequest} */ (result),
- );
- }
- return finishWithoutResolve(log);
- },
- );
- },
- );
- }
- /**
- * @param {ResolveStepHook} hook hook
- * @param {ResolveRequest} request request
- * @param {null | string} message string
- * @param {ResolveContext} resolveContext resolver context
- * @param {(err?: null | Error, result?: ResolveRequest) => void} callback callback
- * @returns {void}
- */
- doResolve(hook, request, message, resolveContext, callback) {
- const rawStack = resolveContext.stack;
- /** @type {StackEntry | undefined} */
- let parent;
- /** @type {Set<string> | undefined} */
- let preSeeded;
- if (rawStack instanceof StackEntry) {
- parent = rawStack;
- preSeeded = rawStack.preSeeded;
- } else if (rawStack) {
- // TODO in the next major remove `Set<string>` support in favor of `StackEntry`
- // Legacy `stack: new Set<string>()` API: don't link the Set into
- // the parent chain (it would pollute iteration and field-compare
- // walks). Carry the strings on the StackEntry itself instead so
- // deeper `doResolve` calls keep seeing pre-seeded entries.
- preSeeded = /** @type {Set<string>} */ (rawStack);
- }
- // Prepend a new linked-list node. O(1) allocation, no Set clone.
- const stackEntry = Resolver.createStackEntry(
- hook,
- request,
- parent,
- preSeeded,
- );
- // When `parent` exists, its `has()` already consults `preSeeded`
- // (inherited from the same chain), so we only need the direct Set
- // lookup on the very first `doResolve` call (no parent yet).
- if (
- parent !== undefined
- ? parent.has(stackEntry)
- : preSeeded !== undefined && preSeeded.has(stackEntry.toString())
- ) {
- /**
- * Prevent recursion
- * @type {Error & { recursion?: boolean }}
- */
- const recursionError = new Error(
- `Recursion in resolving\nStack:\n ${[...stackEntry].join("\n ")}`,
- );
- recursionError.recursion = true;
- if (resolveContext.log) {
- resolveContext.log("abort resolving because of recursion");
- }
- return callback(recursionError);
- }
- this.hooks.resolveStep.call(hook, request);
- if (hook.isUsed()) {
- // Pass `resolveContext` and the override fields (stack, message)
- // directly instead of constructing an intermediate options-object
- // literal — `createInnerContext` reads from the parent and
- // allocates exactly one inner context per step. See the comment
- // on `createInnerContext` itself for the allocation rationale.
- const innerContext = createInnerContext(
- resolveContext,
- stackEntry,
- message,
- );
- return hook.callAsync(request, innerContext, (err, result) => {
- if (err) return callback(err);
- if (result) return callback(null, result);
- callback();
- });
- }
- callback();
- }
- /**
- * @param {string} identifier identifier
- * @returns {ParsedIdentifier} parsed identifier
- */
- parse(identifier) {
- /** @type {ParsedIdentifier} */
- const part = {
- request: "",
- query: "",
- fragment: "",
- module: false,
- directory: false,
- file: false,
- internal: false,
- };
- const parsedIdentifier = parseIdentifier(identifier);
- if (!parsedIdentifier) return part;
- [part.request, part.query, part.fragment] = parsedIdentifier;
- if (part.request.length > 0) {
- // `getType` looks at the prefix of its input and the prefix is
- // identical between `identifier` and `part.request` in every
- // non-`\0`-escape case (slicing off `?query` / `#fragment` doesn't
- // touch the head). `parseIdentifier`'s common fast path returns
- // the same `identifier` reference as `parsedIdentifier[0]`, so a
- // pointer-equality check detects the case where we can compute
- // `getType` once and use it for both `module` and `internal`. The
- // `\0#…` escape path produces a fresh `part.request` and falls
- // through to the second `getType(identifier)` call to preserve
- // the original `internal` flag.
- const requestType = getType(part.request);
- part.module = requestType === PathType.Normal;
- part.internal =
- identifier === part.request
- ? requestType === PathType.Internal
- : getType(identifier) === PathType.Internal;
- // `isDirectory` is just `endsWith("/")` — inline so `parse()`
- // doesn't pay for the extra method dispatch on every resolve.
- part.directory = part.request.endsWith("/");
- if (part.directory) {
- part.request = part.request.slice(0, -1);
- }
- }
- return part;
- }
- /**
- * @param {string} path path
- * @returns {boolean} true, if the path is a module
- */
- isModule(path) {
- return getType(path) === PathType.Normal;
- }
- /**
- * @param {string} path path
- * @returns {boolean} true, if the path is private
- */
- isPrivate(path) {
- return getType(path) === PathType.Internal;
- }
- /**
- * @param {string} path a path
- * @returns {boolean} true, if the path is a directory path
- */
- isDirectory(path) {
- return path.endsWith("/");
- }
- /**
- * @param {string} path path
- * @returns {string} normalized path
- */
- normalize(path) {
- return normalize(path);
- }
- /**
- * @param {string} path path
- * @param {string} request request
- * @returns {string} joined path
- */
- join(path, request) {
- return this.pathCache.join.fn(path, request);
- }
- /**
- * @param {string} path path
- * @returns {string} parent directory
- */
- dirname(path) {
- return this.pathCache.dirname.fn(path);
- }
- /**
- * @param {string} path the path to evaluate
- * @param {string=} suffix an extension to remove from the result
- * @returns {string} the last portion of a path
- */
- basename(path, suffix) {
- return this.pathCache.basename.fn(path, suffix);
- }
- }
- module.exports = Resolver;
|