Resolver.js 44 KB

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