source: frontend/node_modules/enhanced-resolve/lib/Resolver.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 41.7 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const { AsyncSeriesBailHook, AsyncSeriesHook, SyncHook } = require("tapable");
9const createInnerContext = require("./createInnerContext");
10const { parseIdentifier } = require("./util/identifier");
11const {
12 PathType,
13 createCachedBasename,
14 createCachedDirname,
15 createCachedJoin,
16 getType,
17 normalize,
18} = require("./util/path");
19
20/* eslint-disable jsdoc/check-alignment */
21// TODO in the next major release use only `Promise.withResolvers()`
22const _withResolvers =
23 // eslint-disable-next-line n/no-unsupported-features/es-syntax
24 Promise.withResolvers
25 ? /**
26 * @param {Resolver} self resolver
27 * @param {Context} context context information object
28 * @param {string} path context path
29 * @param {string} request request string
30 * @param {ResolveContext} resolveContext resolve context
31 * @returns {Promise<string | false>} result
32 */
33 (self, context, path, request, resolveContext) => {
34 // eslint-disable-next-line n/no-unsupported-features/es-syntax
35 const { promise, resolve, reject } = Promise.withResolvers();
36 self.resolve(context, path, request, resolveContext, (err, res) => {
37 if (err) reject(err);
38 else resolve(/** @type {string | false} */ (res));
39 });
40 return promise;
41 }
42 : /**
43 * @param {Resolver} self resolver
44 * @param {Context} context context information object
45 * @param {string} path context path
46 * @param {string} request request string
47 * @param {ResolveContext} resolveContext resolve context
48 * @returns {Promise<string | false>} result
49 */
50 (self, context, path, request, resolveContext) =>
51 new Promise((resolve, reject) => {
52 self.resolve(context, path, request, resolveContext, (err, res) => {
53 if (err) reject(err);
54 else resolve(/** @type {string | false} */ (res));
55 });
56 });
57/* eslint-enable jsdoc/check-alignment */
58
59/** @typedef {import("./AliasUtils").AliasOption} AliasOption */
60/** @typedef {import("./util/path").CachedJoin} CachedJoin */
61/** @typedef {import("./util/path").CachedDirname} CachedDirname */
62/** @typedef {import("./util/path").CachedBasename} CachedBasename */
63
64/**
65 * @typedef {object} JoinCacheEntry
66 * @property {CachedJoin["fn"]} fn cached join function
67 * @property {CachedJoin["cache"]} cache the underlying cache map
68 */
69
70/**
71 * @typedef {object} DirnameCacheEntry
72 * @property {CachedDirname["fn"]} fn cached dirname function
73 * @property {CachedDirname["cache"]} cache the underlying cache map
74 */
75
76/**
77 * @typedef {object} BasenameCacheEntry
78 * @property {CachedBasename["fn"]} fn cached dirname function
79 * @property {CachedBasename["cache"]} cache the underlying cache map
80 */
81
82/**
83 * @typedef {object} PathCacheFunctions
84 * @property {JoinCacheEntry} join cached join
85 * @property {DirnameCacheEntry} dirname cached dirname
86 * @property {BasenameCacheEntry} basename cached basename
87 */
88
89/** @type {WeakMap<FileSystem, PathCacheFunctions>} */
90const _pathCacheByFs = new WeakMap();
91
92const HASH_ESCAPE_RE = /#/g;
93
94/** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
95
96/**
97 * @typedef {object} KnownContext
98 * @property {string[]=} environments environments
99 */
100
101// eslint-disable-next-line jsdoc/reject-any-type
102/** @typedef {KnownContext & Record<any, any>} Context */
103
104/** @typedef {Error & { details?: string }} ErrorWithDetail */
105
106/** @typedef {(err: ErrorWithDetail | null, res?: string | false, req?: ResolveRequest) => void} ResolveCallback */
107
108/**
109 * @typedef {object} PossibleFileSystemError
110 * @property {string=} code code
111 * @property {number=} errno number
112 * @property {string=} path path
113 * @property {string=} syscall syscall
114 */
115
116/**
117 * @template T
118 * @callback FileSystemCallback
119 * @param {PossibleFileSystemError & Error | null} err
120 * @param {T=} result
121 */
122
123/**
124 * @typedef {string | Buffer | URL} PathLike
125 */
126
127/**
128 * @typedef {PathLike | number} PathOrFileDescriptor
129 */
130
131/**
132 * @typedef {object} ObjectEncodingOptions
133 * @property {BufferEncoding | null | undefined=} encoding encoding
134 */
135
136/**
137 * @typedef {ObjectEncodingOptions | BufferEncoding | undefined | null} EncodingOption
138 */
139
140/** @typedef {(err: NodeJS.ErrnoException | null, result?: string) => void} StringCallback */
141/** @typedef {(err: NodeJS.ErrnoException | null, result?: Buffer) => void} BufferCallback */
142/** @typedef {(err: NodeJS.ErrnoException | null, result?: (string | Buffer)) => void} StringOrBufferCallback */
143/** @typedef {(err: NodeJS.ErrnoException | null, result?: IStats) => void} StatsCallback */
144/** @typedef {(err: NodeJS.ErrnoException | null, result?: IBigIntStats) => void} BigIntStatsCallback */
145/** @typedef {(err: NodeJS.ErrnoException | null, result?: (IStats | IBigIntStats)) => void} StatsOrBigIntStatsCallback */
146/** @typedef {(err: NodeJS.ErrnoException | Error | null, result?: JsonObject) => void} ReadJsonCallback */
147
148/**
149 * @template T
150 * @typedef {object} IStatsBase
151 * @property {() => boolean} isFile is file
152 * @property {() => boolean} isDirectory is directory
153 * @property {() => boolean} isBlockDevice is block device
154 * @property {() => boolean} isCharacterDevice is character device
155 * @property {() => boolean} isSymbolicLink is symbolic link
156 * @property {() => boolean} isFIFO is FIFO
157 * @property {() => boolean} isSocket is socket
158 * @property {T} dev dev
159 * @property {T} ino ino
160 * @property {T} mode mode
161 * @property {T} nlink nlink
162 * @property {T} uid uid
163 * @property {T} gid gid
164 * @property {T} rdev rdev
165 * @property {T} size size
166 * @property {T} blksize blksize
167 * @property {T} blocks blocks
168 * @property {T} atimeMs atime ms
169 * @property {T} mtimeMs mtime ms
170 * @property {T} ctimeMs ctime ms
171 * @property {T} birthtimeMs birthtime ms
172 * @property {Date} atime atime
173 * @property {Date} mtime mtime
174 * @property {Date} ctime ctime
175 * @property {Date} birthtime birthtime
176 */
177
178/**
179 * @typedef {IStatsBase<number>} IStats
180 */
181
182/**
183 * @typedef {IStatsBase<bigint> & { atimeNs: bigint, mtimeNs: bigint, ctimeNs: bigint, birthtimeNs: bigint }} IBigIntStats
184 */
185
186/**
187 * @template {string | Buffer} [T=string]
188 * @typedef {object} Dirent
189 * @property {() => boolean} isFile true when is file, otherwise false
190 * @property {() => boolean} isDirectory true when is directory, otherwise false
191 * @property {() => boolean} isBlockDevice true when is block device, otherwise false
192 * @property {() => boolean} isCharacterDevice true when is character device, otherwise false
193 * @property {() => boolean} isSymbolicLink true when is symbolic link, otherwise false
194 * @property {() => boolean} isFIFO true when is FIFO, otherwise false
195 * @property {() => boolean} isSocket true when is socket, otherwise false
196 * @property {T} name name
197 * @property {string} parentPath path
198 * @property {string=} path path
199 */
200
201/**
202 * @typedef {object} StatOptions
203 * @property {(boolean | undefined)=} bigint need bigint values
204 */
205
206/**
207 * @typedef {object} StatSyncOptions
208 * @property {(boolean | undefined)=} bigint need bigint values
209 * @property {(boolean | undefined)=} throwIfNoEntry throw if no entry
210 */
211
212/**
213 * @typedef {{
214 * (path: PathOrFileDescriptor, options: ({ encoding?: null | undefined, flag?: string | undefined } & import("events").Abortable) | undefined | null, callback: BufferCallback): void,
215 * (path: PathOrFileDescriptor, options: ({ encoding: BufferEncoding, flag?: string | undefined } & import("events").Abortable) | BufferEncoding, callback: StringCallback): void,
216 * (path: PathOrFileDescriptor, options: (ObjectEncodingOptions & { flag?: string | undefined } & import("events").Abortable) | BufferEncoding | undefined | null, callback: StringOrBufferCallback): void,
217 * (path: PathOrFileDescriptor, callback: BufferCallback): void,
218 * }} ReadFile
219 */
220
221/**
222 * @typedef {"buffer" | { encoding: "buffer" }} BufferEncodingOption
223 */
224
225/**
226 * @typedef {{
227 * (path: PathOrFileDescriptor, options?: { encoding?: null | undefined, flag?: string | undefined } | null): Buffer,
228 * (path: PathOrFileDescriptor, options: { encoding: BufferEncoding, flag?: string | undefined } | BufferEncoding): string,
229 * (path: PathOrFileDescriptor, options?: (ObjectEncodingOptions & { flag?: string | undefined }) | BufferEncoding | null): string | Buffer,
230 * }} ReadFileSync
231 */
232
233/**
234 * @typedef {{
235 * (path: PathLike, options: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void,
236 * (path: PathLike, options: { encoding: "buffer", withFileTypes?: false | undefined, recursive?: boolean | undefined } | "buffer", callback: (err: NodeJS.ErrnoException | null, files?: Buffer[]) => void): void,
237 * (path: PathLike, options: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[] | Buffer[]) => void): void,
238 * (path: PathLike, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void,
239 * (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files?: Dirent<string>[]) => void): void,
240 * (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files: Dirent<Buffer>[]) => void): void,
241 * }} Readdir
242 */
243
244/**
245 * @typedef {{
246 * (path: PathLike, options?: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | null): string[],
247 * (path: PathLike, options: { encoding: "buffer", withFileTypes?: false | undefined, recursive?: boolean | undefined } | "buffer"): Buffer[],
248 * (path: PathLike, options?: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | null): string[] | Buffer[],
249 * (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }): Dirent[],
250 * (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }): Dirent<Buffer>[],
251 * }} ReaddirSync
252 */
253
254/**
255 * @typedef {(pathOrFileDescription: PathOrFileDescriptor, callback: ReadJsonCallback) => void} ReadJson
256 */
257
258/**
259 * @typedef {(pathOrFileDescription: PathOrFileDescriptor) => JsonObject} ReadJsonSync
260 */
261
262/**
263 * @typedef {{
264 * (path: PathLike, options: EncodingOption, callback: StringCallback): void,
265 * (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void,
266 * (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void,
267 * (path: PathLike, callback: StringCallback): void,
268 * }} Readlink
269 */
270
271/**
272 * @typedef {{
273 * (path: PathLike, options?: EncodingOption): string,
274 * (path: PathLike, options: BufferEncodingOption): Buffer,
275 * (path: PathLike, options?: EncodingOption): string | Buffer,
276 * }} ReadlinkSync
277 */
278
279/**
280 * @typedef {{
281 * (path: PathLike, callback: StatsCallback): void,
282 * (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void,
283 * (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void,
284 * (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void,
285 * }} LStat
286 */
287
288/**
289 * @typedef {{
290 * (path: PathLike, options?: undefined): IStats,
291 * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined,
292 * (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined,
293 * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats,
294 * (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats,
295 * (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats,
296 * (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined,
297 * }} LStatSync
298 */
299
300/**
301 * @typedef {{
302 * (path: PathLike, callback: StatsCallback): void,
303 * (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void,
304 * (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void,
305 * (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void,
306 * }} Stat
307 */
308
309/**
310 * @typedef {{
311 * (path: PathLike, options?: undefined): IStats,
312 * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined,
313 * (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined,
314 * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats,
315 * (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats,
316 * (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats,
317 * (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined,
318 * }} StatSync
319 */
320
321/**
322 * @typedef {{
323 * (path: PathLike, options: EncodingOption, callback: StringCallback): void,
324 * (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void,
325 * (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void,
326 * (path: PathLike, callback: StringCallback): void,
327 * }} RealPath
328 */
329
330/**
331 * @typedef {{
332 * (path: PathLike, options?: EncodingOption): string,
333 * (path: PathLike, options: BufferEncodingOption): Buffer,
334 * (path: PathLike, options?: EncodingOption): string | Buffer,
335 * }} RealPathSync
336 */
337
338/**
339 * @typedef {object} FileSystem
340 * @property {ReadFile} readFile read file method
341 * @property {Readdir} readdir readdir method
342 * @property {ReadJson=} readJson read json method
343 * @property {Readlink} readlink read link method
344 * @property {LStat=} lstat lstat method
345 * @property {Stat} stat stat method
346 * @property {RealPath=} realpath realpath method
347 */
348
349/**
350 * @typedef {object} SyncFileSystem
351 * @property {ReadFileSync} readFileSync read file sync method
352 * @property {ReaddirSync} readdirSync read dir sync method
353 * @property {ReadJsonSync=} readJsonSync read json sync method
354 * @property {ReadlinkSync} readlinkSync read link sync method
355 * @property {LStatSync=} lstatSync lstat sync method
356 * @property {StatSync} statSync stat sync method
357 * @property {RealPathSync=} realpathSync real path sync method
358 */
359
360/**
361 * @typedef {object} ParsedIdentifier
362 * @property {string} request request
363 * @property {string} query query
364 * @property {string} fragment fragment
365 * @property {boolean} directory is directory
366 * @property {boolean} module is module
367 * @property {boolean} file is file
368 * @property {boolean} internal is internal
369 */
370
371/** @typedef {string | number | boolean | null} JsonPrimitive */
372/** @typedef {JsonValue[]} JsonArray */
373/** @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue */
374/** @typedef {{ [Key in string]?: JsonValue | undefined }} JsonObject */
375
376/**
377 * @typedef {object} TsconfigPathsMap
378 * @property {TsconfigPathsData} main main tsconfig paths data
379 * @property {string} mainContext main tsconfig base URL (absolute path)
380 * @property {{ [baseUrl: string]: TsconfigPathsData }} refs referenced tsconfig paths data mapped by baseUrl
381 * @property {{ [context: string]: TsconfigPathsData }} allContexts all contexts (main + refs) for quick lookup
382 * @property {string[]} contextList precomputed `Object.keys(allContexts)` — read-only; used on the `_selectPathsDataForContext` hot path
383 * @property {Set<string>} fileDependencies file dependencies
384 */
385
386/**
387 * @typedef {object} TsconfigPathsData
388 * @property {import("./AliasUtils").CompiledAliasOptions} alias tsconfig file data
389 * @property {string[]} modules tsconfig file data
390 */
391
392/**
393 * @typedef {object} BaseResolveRequest
394 * @property {string | false} path path
395 * @property {Context=} context content
396 * @property {string=} descriptionFilePath description file path
397 * @property {string=} descriptionFileRoot description file root
398 * @property {JsonObject=} descriptionFileData description file data
399 * @property {TsconfigPathsMap | null | undefined=} tsconfigPathsMap tsconfig paths map
400 * @property {string=} relativePath relative path
401 * @property {boolean=} ignoreSymlinks true when need to ignore symlinks, otherwise false
402 * @property {boolean=} fullySpecified true when full specified, otherwise false
403 * @property {string=} __innerRequest inner request for internal usage
404 * @property {string=} __innerRequest_request inner request for internal usage
405 * @property {string=} __innerRequest_relativePath inner relative path for internal usage
406 */
407
408/** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
409
410/**
411 * @template T
412 * @typedef {{ add: (item: T) => void }} WriteOnlySet
413 */
414
415/** @typedef {(request: ResolveRequest) => void} ResolveContextYield */
416
417/**
418 * Singly-linked stack entry that also exposes a Set-like API
419 * (`has`, `size`, iteration). Each `doResolve` call prepends a new
420 * `StackEntry` that points at the previous tip via `.parent`, so pushing
421 * is O(1) in time and memory. Recursion detection walks the linked list
422 * (O(n)) but the stack is typically shallow, so this is cheaper overall
423 * than cloning a `Set` per call.
424 */
425class StackEntry {
426 /**
427 * @param {ResolveStepHook} hook hook
428 * @param {ResolveRequest} request request
429 * @param {StackEntry=} parent previous tip
430 * @param {Set<string>=} preSeeded entries pre-seeded via the legacy `Set<string>` API
431 */
432 constructor(hook, request, parent, preSeeded) {
433 this.name = hook.name;
434 this.path = request.path;
435 this.request = request.request || "";
436 this.query = request.query || "";
437 this.fragment = request.fragment || "";
438 this.directory = Boolean(request.directory);
439 this.module = Boolean(request.module);
440 /** @type {StackEntry | undefined} */
441 this.parent = parent;
442 /**
443 * Strings seeded by callers that still pass `stack: new Set([...])`.
444 * Propagated through the chain so deeper `doResolve` calls still see
445 * them during recursion checks. `undefined` in the common case so
446 * there is no extra work on the hot path.
447 * @type {Set<string> | undefined}
448 */
449 this.preSeeded = preSeeded;
450 }
451
452 /**
453 * Walk the linked list looking for an entry with the same request shape.
454 * Set-compatible: callers that used `stack.has(entry)` keep working.
455 *
456 * NOTE: kept monomorphic on purpose. An earlier draft accepted a string
457 * query too (so pre-5.21 plugins keeping their own `Set<string>` of
458 * seen entries could probe the live stack with the formatted form),
459 * but adding the second shape regressed `doResolve`'s heap profile by
460 * ~1 MiB / 200 resolves on stack-churn — V8 keeps a polymorphic
461 * call-site state for `parent.has(stackEntry)` once `has` has two
462 * argument shapes. Plugins that need string membership can reach for
463 * `[...stack].find(e => e.includes(formattedString))` via the
464 * `String`-method proxies on `StackEntry` instead.
465 * @param {StackEntry} query entry to look for
466 * @returns {boolean} whether the stack already contains an equivalent entry
467 */
468 has(query) {
469 /** @type {StackEntry | undefined} */
470 let node = this;
471 while (node) {
472 if (
473 node.name === query.name &&
474 node.path === query.path &&
475 node.request === query.request &&
476 node.query === query.query &&
477 node.fragment === query.fragment &&
478 node.directory === query.directory &&
479 node.module === query.module
480 ) {
481 return true;
482 }
483 node = node.parent;
484 }
485 return this.preSeeded !== undefined && this.preSeeded.has(query.toString());
486 }
487
488 /**
489 * Number of entries on the stack (oldest-to-newest length).
490 * @returns {number} size
491 */
492 get size() {
493 let count = this.preSeeded ? this.preSeeded.size : 0;
494 /** @type {StackEntry | undefined} */
495 let node = this;
496 while (node) {
497 count++;
498 node = node.parent;
499 }
500 return count;
501 }
502
503 /**
504 * Iterate entries from oldest (root) to newest (tip), matching how a
505 * `Set` that was populated in insertion order would iterate. Pre-seeded
506 * legacy `Set<string>` entries come first so error-message output stays
507 * ordered oldest-to-newest.
508 *
509 * Yields each entry as its formatted `toString()` form. Plugins written
510 * against the pre-5.21 `Set<string>` shape — e.g.
511 * `[...resolveContext.stack].find(a => a.includes("module:"))` — keep
512 * working unchanged because each yielded value is a plain string with
513 * all of `String.prototype` available natively. Resolves that never
514 * iterate the stack pay nothing; iteration costs one `toString()`
515 * allocation per stack frame.
516 * @returns {IterableIterator<string>} iterator
517 */
518 *[Symbol.iterator]() {
519 if (this.preSeeded !== undefined) {
520 for (const entry of this.preSeeded) yield entry;
521 }
522 /** @type {StackEntry[]} */
523 const entries = [];
524 /** @type {StackEntry | undefined} */
525 let node = this;
526 while (node) {
527 entries.push(node);
528 node = node.parent;
529 }
530 for (let i = entries.length - 1; i >= 0; i--) yield entries[i].toString();
531 }
532
533 /**
534 * Human-readable form used in recursion error messages, logs, and the
535 * iterator above. Not memoized: caching would require an extra slot on
536 * every `StackEntry`, which costs heap even on resolves that never look
537 * at the formatted form.
538 * @returns {string} formatted entry
539 */
540 toString() {
541 return `${this.name}: (${this.path}) ${this.request}${this.query}${
542 this.fragment
543 }${this.directory ? " directory" : ""}${this.module ? " module" : ""}`;
544 }
545}
546
547/**
548 * Resolve context
549 * @typedef {object} ResolveContext
550 * @property {WriteOnlySet<string>=} contextDependencies directories that was found on file system
551 * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
552 * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
553 * @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.
554 * @property {((str: string) => void)=} log log function
555 * @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
556 */
557
558/** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
559
560/**
561 * @typedef {object} KnownHooks
562 * @property {SyncHook<[ResolveStepHook, ResolveRequest], void>} resolveStep resolve step hook
563 * @property {SyncHook<[ResolveRequest, Error]>} noResolve no resolve hook
564 * @property {ResolveStepHook} resolve resolve hook
565 * @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result result hook
566 */
567
568/**
569 * @typedef {{ [key: string]: ResolveStepHook }} EnsuredHooks
570 */
571
572/**
573 * @param {string} str input string
574 * @returns {string} in camel case
575 */
576function toCamelCase(str) {
577 return str.replace(/-([a-z])/g, (str) => str.slice(1).toUpperCase());
578}
579
580class Resolver {
581 /**
582 * @param {ResolveStepHook} hook hook
583 * @param {ResolveRequest} request request
584 * @param {StackEntry=} parent previous tip of the stack
585 * @param {Set<string>=} preSeeded entries pre-seeded via the legacy `Set<string>` API
586 * @returns {StackEntry} stack entry
587 */
588 static createStackEntry(hook, request, parent, preSeeded) {
589 return new StackEntry(hook, request, parent, preSeeded);
590 }
591
592 /**
593 * @param {FileSystem} fileSystem a filesystem
594 * @param {ResolveOptions} options options
595 */
596 constructor(fileSystem, options) {
597 /** @type {FileSystem} */
598 this.fileSystem = fileSystem;
599 /** @type {ResolveOptions} */
600 this.options = options;
601 let pathCache = _pathCacheByFs.get(fileSystem);
602 if (!pathCache) {
603 pathCache = {
604 join: createCachedJoin(),
605 dirname: createCachedDirname(),
606 basename: createCachedBasename(),
607 };
608 _pathCacheByFs.set(fileSystem, pathCache);
609 }
610 /** @type {PathCacheFunctions} */
611 this.pathCache = pathCache;
612 /** @type {KnownHooks} */
613 this.hooks = {
614 resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
615 noResolve: new SyncHook(["request", "error"], "noResolve"),
616 resolve: new AsyncSeriesBailHook(
617 ["request", "resolveContext"],
618 "resolve",
619 ),
620 result: new AsyncSeriesHook(["result", "resolveContext"], "result"),
621 };
622 }
623
624 /**
625 * @param {string | ResolveStepHook} name hook name or hook itself
626 * @returns {ResolveStepHook} the hook
627 */
628 ensureHook(name) {
629 if (typeof name !== "string") {
630 return name;
631 }
632 name = toCamelCase(name);
633 if (name.startsWith("before")) {
634 return /** @type {ResolveStepHook} */ (
635 this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
636 stage: -10,
637 })
638 );
639 }
640 if (name.startsWith("after")) {
641 return /** @type {ResolveStepHook} */ (
642 this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
643 stage: 10,
644 })
645 );
646 }
647 /** @type {ResolveStepHook} */
648 const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
649 if (!hook) {
650 /** @type {KnownHooks & EnsuredHooks} */
651 (this.hooks)[name] = new AsyncSeriesBailHook(
652 ["request", "resolveContext"],
653 name,
654 );
655
656 return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
657 }
658 return hook;
659 }
660
661 /**
662 * @param {string | ResolveStepHook} name hook name or hook itself
663 * @returns {ResolveStepHook} the hook
664 */
665 getHook(name) {
666 if (typeof name !== "string") {
667 return name;
668 }
669 name = toCamelCase(name);
670 if (name.startsWith("before")) {
671 return /** @type {ResolveStepHook} */ (
672 this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
673 stage: -10,
674 })
675 );
676 }
677 if (name.startsWith("after")) {
678 return /** @type {ResolveStepHook} */ (
679 this.getHook(name[5].toLowerCase() + name.slice(6)).withOptions({
680 stage: 10,
681 })
682 );
683 }
684 /** @type {ResolveStepHook} */
685 const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
686 if (!hook) {
687 throw new Error(`Hook ${name} doesn't exist`);
688 }
689 return hook;
690 }
691
692 /**
693 * @overload
694 * @param {string} path context path
695 * @param {string} request request string
696 * @param {ResolveContext=} resolveContext resolve context
697 * @returns {string | false} result
698 */
699 /**
700 * @overload
701 * @param {Context} context context information object
702 * @param {string} path context path
703 * @param {string} request request string
704 * @param {ResolveContext=} resolveContext resolve context
705 * @returns {string | false} result
706 */
707 /**
708 * @param {Context | string} context context information object or context path when no context is provided
709 * @param {string | ResolveContext=} path context path or resolve context when no context is provided
710 * @param {string | ResolveContext=} request request string or resolve context when no context is provided
711 * @param {ResolveContext=} resolveContext resolve context
712 * @returns {string | false} result
713 */
714 resolveSync(context, path, request, resolveContext) {
715 /** @type {Error | null | undefined} */
716 let err;
717 /** @type {string | false | undefined} */
718 let result;
719 let sync = false;
720 // `|| {}` so the underlying `resolve()` hits its 5-arg fast path
721 // (skips the overload-shifting prologue) regardless of whether the
722 // caller supplied a resolveContext.
723 this.resolve(
724 /** @type {Context} */ (context),
725 /** @type {string} */ (path),
726 /** @type {string} */ (request),
727 /** @type {ResolveContext} */ (resolveContext) || {},
728 (_err, r) => {
729 err = _err;
730 result = r;
731 sync = true;
732 },
733 );
734 if (!sync) {
735 throw new Error(
736 "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!",
737 );
738 }
739 if (err) throw err;
740 if (result === undefined) throw new Error("No result");
741 return result;
742 }
743
744 /**
745 * @overload
746 * @param {string} path context path
747 * @param {string} request request string
748 * @param {ResolveContext=} resolveContext resolve context
749 * @returns {Promise<string | false>} result
750 */
751 /**
752 * @overload
753 * @param {Context} context context information object
754 * @param {string} path context path
755 * @param {string} request request string
756 * @param {ResolveContext=} resolveContext resolve context
757 * @returns {Promise<string | false>} result
758 */
759 /**
760 * @param {Context | string} context context information object or context path when no context is provided
761 * @param {string | ResolveContext=} path context path or resolve context when no context is provided
762 * @param {string | ResolveContext=} request request string or resolve context when no context is provided
763 * @param {ResolveContext=} resolveContext resolve context
764 * @returns {Promise<string | false>} result
765 */
766 resolvePromise(context, path, request, resolveContext) {
767 // `|| {}` ensures the 5-arg fast path inside `resolve()` is reached
768 // even when the caller doesn't pass a resolveContext.
769 return _withResolvers(
770 this,
771 /** @type {Context} */ (context),
772 /** @type {string} */ (path),
773 /** @type {string} */ (request),
774 /** @type {ResolveContext} */ (resolveContext) || {},
775 );
776 }
777
778 /**
779 * @overload
780 * @param {string} path context path
781 * @param {string} request request string
782 * @param {ResolveCallback} callback callback function
783 * @returns {void}
784 */
785 /**
786 * @overload
787 * @param {string} path context path
788 * @param {string} request request string
789 * @param {ResolveContext} resolveContext resolve context
790 * @param {ResolveCallback} callback callback function
791 * @returns {void}
792 */
793 /**
794 * @overload
795 * @param {Context} context context information object
796 * @param {string} path context path
797 * @param {string} request request string
798 * @param {ResolveCallback} callback callback function
799 * @returns {void}
800 */
801 /**
802 * @overload
803 * @param {Context} context context information object
804 * @param {string} path context path
805 * @param {string} request request string
806 * @param {ResolveContext} resolveContext resolve context
807 * @param {ResolveCallback} callback callback function
808 * @returns {void}
809 */
810 /**
811 * @param {Context | string} context context information object or context path when no context is provided
812 * @param {string | ResolveContext | ResolveCallback=} path context path or (when no context) resolve context or callback
813 * @param {string | ResolveContext | ResolveCallback=} request request string or (when no context) resolve context or callback
814 * @param {ResolveContext | ResolveCallback=} resolveContext resolve context or callback when no resolve context is provided
815 * @param {ResolveCallback=} callback callback function
816 * @returns {void}
817 */
818 resolve(context, path, request, resolveContext, callback) {
819 // Fast path for the common 5-arg call (`resolver.resolve(ctx, from,
820 // req, resolveCtx, cb)`) — every call from `resolveSync` /
821 // `resolvePromise` plus the vast majority of direct API callers.
822 // PR #536 added runtime overload-shifting to support optional
823 // `context` / `resolveContext`; that adds several `typeof` checks
824 // per resolve which show up as a measurable instruction-count
825 // regression on every benchmark that calls into this method. Skip
826 // the shifting entirely when all 5 args are already well-typed.
827 if (
828 typeof callback === "function" &&
829 typeof context === "object" &&
830 context !== null &&
831 typeof resolveContext === "object" &&
832 resolveContext !== null
833 ) {
834 // proceed straight to per-arg validation below
835 } else {
836 // Slow path: shift positional args based on what was supplied.
837 // Shift when context is omitted (first positional arg is the path string).
838 if (typeof context === "string") {
839 // Keep an already-supplied callback (resolveSync / resolvePromise
840 // always pass one in the 5th position).
841 if (typeof callback !== "function") {
842 callback = /** @type {ResolveCallback | undefined} */ (
843 resolveContext
844 );
845 }
846 resolveContext =
847 /** @type {ResolveContext | ResolveCallback | undefined} */ (request);
848 request = /** @type {string} */ (path);
849 path = context;
850 context = {};
851 }
852 // 4-arg form: the resolveContext slot holds the callback.
853 if (typeof resolveContext === "function") {
854 callback = resolveContext;
855 resolveContext = {};
856 } else if (!resolveContext || typeof resolveContext !== "object") {
857 resolveContext = {};
858 }
859 if (typeof callback !== "function") {
860 throw new TypeError("callback argument is not a function");
861 }
862 if (!context || typeof context !== "object") {
863 context = {};
864 }
865 }
866 if (typeof path !== "string") {
867 return callback(new Error("path argument is not a string"));
868 }
869 if (typeof request !== "string") {
870 return callback(new Error("request argument is not a string"));
871 }
872
873 /** @type {ResolveRequest} */
874 const obj = {
875 context,
876 path,
877 request,
878 };
879
880 /** @type {ResolveContextYield | undefined} */
881 let yield_;
882 let yieldCalled = false;
883 /** @type {ResolveContextYield | undefined} */
884 let finishYield;
885 if (typeof resolveContext.yield === "function") {
886 const old = resolveContext.yield;
887 /**
888 * @param {ResolveRequest} obj object
889 */
890 yield_ = (obj) => {
891 old(obj);
892 yieldCalled = true;
893 };
894 /**
895 * @param {ResolveRequest} result result
896 * @returns {void}
897 */
898 finishYield = (result) => {
899 if (result) {
900 /** @type {ResolveContextYield} */ (yield_)(result);
901 }
902 callback(null);
903 };
904 }
905
906 const message = `resolve '${request}' in '${path}'`;
907
908 /**
909 * @param {ResolveRequest} result result
910 * @returns {void}
911 */
912 const finishResolved = (result) => {
913 const resultPath = result.path;
914 if (resultPath === false) return callback(null, false, result);
915 const escapedPath = resultPath.includes("#")
916 ? resultPath.replace(HASH_ESCAPE_RE, "\0#")
917 : resultPath;
918 const resultQuery = result.query;
919 let escapedQuery;
920 if (resultQuery) {
921 escapedQuery = resultQuery.includes("#")
922 ? resultQuery.replace(HASH_ESCAPE_RE, "\0#")
923 : resultQuery;
924 } else {
925 escapedQuery = "";
926 }
927 return callback(
928 null,
929 `${escapedPath}${escapedQuery}${result.fragment || ""}`,
930 result,
931 );
932 };
933
934 /**
935 * @param {string[]} log logs
936 * @returns {void}
937 */
938 const finishWithoutResolve = (log) => {
939 /**
940 * @type {ErrorWithDetail}
941 */
942 const error = new Error(`Can't ${message}`);
943 error.details = log.join("\n");
944 this.hooks.noResolve.call(obj, error);
945 return callback(error);
946 };
947
948 if (resolveContext.log) {
949 // We need log anyway to capture it in case of an error
950 const parentLog = resolveContext.log;
951 /** @type {string[]} */
952 const log = [];
953 return this.doResolve(
954 this.hooks.resolve,
955 obj,
956 message,
957 {
958 log: (msg) => {
959 parentLog(msg);
960 log.push(msg);
961 },
962 yield: yield_,
963 fileDependencies: resolveContext.fileDependencies,
964 contextDependencies: resolveContext.contextDependencies,
965 missingDependencies: resolveContext.missingDependencies,
966 stack: resolveContext.stack,
967 },
968 (err, result) => {
969 if (err) return callback(err);
970
971 if (yieldCalled || (result && yield_)) {
972 return /** @type {ResolveContextYield} */ (finishYield)(
973 /** @type {ResolveRequest} */ (result),
974 );
975 }
976
977 if (result) return finishResolved(result);
978
979 return finishWithoutResolve(log);
980 },
981 );
982 }
983 // Try to resolve assuming there is no error
984 // We don't log stuff in this case
985 return this.doResolve(
986 this.hooks.resolve,
987 obj,
988 message,
989 {
990 log: undefined,
991 yield: yield_,
992 fileDependencies: resolveContext.fileDependencies,
993 contextDependencies: resolveContext.contextDependencies,
994 missingDependencies: resolveContext.missingDependencies,
995 stack: resolveContext.stack,
996 },
997 (err, result) => {
998 if (err) return callback(err);
999
1000 if (yieldCalled || (result && yield_)) {
1001 return /** @type {ResolveContextYield} */ (finishYield)(
1002 /** @type {ResolveRequest} */ (result),
1003 );
1004 }
1005
1006 if (result) return finishResolved(result);
1007
1008 // log is missing for the error details
1009 // so we redo the resolving for the log info
1010 // this is more expensive to the success case
1011 // is assumed by default
1012 /** @type {string[]} */
1013 const log = [];
1014
1015 return this.doResolve(
1016 this.hooks.resolve,
1017 obj,
1018 message,
1019 {
1020 log: (msg) => log.push(msg),
1021 yield: yield_,
1022 stack: resolveContext.stack,
1023 },
1024 (err, result) => {
1025 if (err) return callback(err);
1026
1027 // In a case that there is a race condition and yield will be called
1028 if (yieldCalled || (result && yield_)) {
1029 return /** @type {ResolveContextYield} */ (finishYield)(
1030 /** @type {ResolveRequest} */ (result),
1031 );
1032 }
1033
1034 return finishWithoutResolve(log);
1035 },
1036 );
1037 },
1038 );
1039 }
1040
1041 /**
1042 * @param {ResolveStepHook} hook hook
1043 * @param {ResolveRequest} request request
1044 * @param {null | string} message string
1045 * @param {ResolveContext} resolveContext resolver context
1046 * @param {(err?: null | Error, result?: ResolveRequest) => void} callback callback
1047 * @returns {void}
1048 */
1049 doResolve(hook, request, message, resolveContext, callback) {
1050 const rawStack = resolveContext.stack;
1051 /** @type {StackEntry | undefined} */
1052 let parent;
1053 /** @type {Set<string> | undefined} */
1054 let preSeeded;
1055 if (rawStack instanceof StackEntry) {
1056 parent = rawStack;
1057 preSeeded = rawStack.preSeeded;
1058 } else if (rawStack) {
1059 // TODO in the next major remove `Set<string>` support in favor of `StackEntry`
1060 // Legacy `stack: new Set<string>()` API: don't link the Set into
1061 // the parent chain (it would pollute iteration and field-compare
1062 // walks). Carry the strings on the StackEntry itself instead so
1063 // deeper `doResolve` calls keep seeing pre-seeded entries.
1064 preSeeded = /** @type {Set<string>} */ (rawStack);
1065 }
1066 // Prepend a new linked-list node. O(1) allocation, no Set clone.
1067 const stackEntry = Resolver.createStackEntry(
1068 hook,
1069 request,
1070 parent,
1071 preSeeded,
1072 );
1073
1074 // When `parent` exists, its `has()` already consults `preSeeded`
1075 // (inherited from the same chain), so we only need the direct Set
1076 // lookup on the very first `doResolve` call (no parent yet).
1077 if (
1078 parent !== undefined
1079 ? parent.has(stackEntry)
1080 : preSeeded !== undefined && preSeeded.has(stackEntry.toString())
1081 ) {
1082 /**
1083 * Prevent recursion
1084 * @type {Error & { recursion?: boolean }}
1085 */
1086 const recursionError = new Error(
1087 `Recursion in resolving\nStack:\n ${[...stackEntry].join("\n ")}`,
1088 );
1089 recursionError.recursion = true;
1090 if (resolveContext.log) {
1091 resolveContext.log("abort resolving because of recursion");
1092 }
1093 return callback(recursionError);
1094 }
1095 this.hooks.resolveStep.call(hook, request);
1096
1097 if (hook.isUsed()) {
1098 // Pass `resolveContext` and the override fields (stack, message)
1099 // directly instead of constructing an intermediate options-object
1100 // literal — `createInnerContext` reads from the parent and
1101 // allocates exactly one inner context per step. See the comment
1102 // on `createInnerContext` itself for the allocation rationale.
1103 const innerContext = createInnerContext(
1104 resolveContext,
1105 stackEntry,
1106 message,
1107 );
1108 return hook.callAsync(request, innerContext, (err, result) => {
1109 if (err) return callback(err);
1110 if (result) return callback(null, result);
1111 callback();
1112 });
1113 }
1114 callback();
1115 }
1116
1117 /**
1118 * @param {string} identifier identifier
1119 * @returns {ParsedIdentifier} parsed identifier
1120 */
1121 parse(identifier) {
1122 /** @type {ParsedIdentifier} */
1123 const part = {
1124 request: "",
1125 query: "",
1126 fragment: "",
1127 module: false,
1128 directory: false,
1129 file: false,
1130 internal: false,
1131 };
1132
1133 const parsedIdentifier = parseIdentifier(identifier);
1134
1135 if (!parsedIdentifier) return part;
1136
1137 [part.request, part.query, part.fragment] = parsedIdentifier;
1138
1139 if (part.request.length > 0) {
1140 // `getType` looks at the prefix of its input and the prefix is
1141 // identical between `identifier` and `part.request` in every
1142 // non-`\0`-escape case (slicing off `?query` / `#fragment` doesn't
1143 // touch the head). `parseIdentifier`'s common fast path returns
1144 // the same `identifier` reference as `parsedIdentifier[0]`, so a
1145 // pointer-equality check detects the case where we can compute
1146 // `getType` once and use it for both `module` and `internal`. The
1147 // `\0#…` escape path produces a fresh `part.request` and falls
1148 // through to the second `getType(identifier)` call to preserve
1149 // the original `internal` flag.
1150 const requestType = getType(part.request);
1151 part.module = requestType === PathType.Normal;
1152 part.internal =
1153 identifier === part.request
1154 ? requestType === PathType.Internal
1155 : getType(identifier) === PathType.Internal;
1156 // `isDirectory` is just `endsWith("/")` — inline so `parse()`
1157 // doesn't pay for the extra method dispatch on every resolve.
1158 part.directory = part.request.endsWith("/");
1159 if (part.directory) {
1160 part.request = part.request.slice(0, -1);
1161 }
1162 }
1163
1164 return part;
1165 }
1166
1167 /**
1168 * @param {string} path path
1169 * @returns {boolean} true, if the path is a module
1170 */
1171 isModule(path) {
1172 return getType(path) === PathType.Normal;
1173 }
1174
1175 /**
1176 * @param {string} path path
1177 * @returns {boolean} true, if the path is private
1178 */
1179 isPrivate(path) {
1180 return getType(path) === PathType.Internal;
1181 }
1182
1183 /**
1184 * @param {string} path a path
1185 * @returns {boolean} true, if the path is a directory path
1186 */
1187 isDirectory(path) {
1188 return path.endsWith("/");
1189 }
1190
1191 /**
1192 * @param {string} path path
1193 * @returns {string} normalized path
1194 */
1195 normalize(path) {
1196 return normalize(path);
1197 }
1198
1199 /**
1200 * @param {string} path path
1201 * @param {string} request request
1202 * @returns {string} joined path
1203 */
1204 join(path, request) {
1205 return this.pathCache.join.fn(path, request);
1206 }
1207
1208 /**
1209 * @param {string} path path
1210 * @returns {string} parent directory
1211 */
1212 dirname(path) {
1213 return this.pathCache.dirname.fn(path);
1214 }
1215
1216 /**
1217 * @param {string} path the path to evaluate
1218 * @param {string=} suffix an extension to remove from the result
1219 * @returns {string} the last portion of a path
1220 */
1221 basename(path, suffix) {
1222 return this.pathCache.basename.fn(path, suffix);
1223 }
1224}
1225
1226module.exports = Resolver;
Note: See TracBrowser for help on using the repository browser.