Resolver.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  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. normalize,
  11. cachedJoin: join,
  12. getType,
  13. PathType,
  14. } = require("./util/path");
  15. /** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
  16. /** @typedef {Error & { details?: string }} ErrorWithDetail */
  17. /** @typedef {(err: ErrorWithDetail | null, res?: string | false, req?: ResolveRequest) => void} ResolveCallback */
  18. /**
  19. * @typedef {object} PossibleFileSystemError
  20. * @property {string=} code code
  21. * @property {number=} errno number
  22. * @property {string=} path path
  23. * @property {string=} syscall syscall
  24. */
  25. /**
  26. * @template T
  27. * @callback FileSystemCallback
  28. * @param {PossibleFileSystemError & Error | null} err
  29. * @param {T=} result
  30. */
  31. /**
  32. * @typedef {string | Buffer | URL} PathLike
  33. */
  34. /**
  35. * @typedef {PathLike | number} PathOrFileDescriptor
  36. */
  37. /**
  38. * @typedef {object} ObjectEncodingOptions
  39. * @property {BufferEncoding | null | undefined=} encoding encoding
  40. */
  41. /**
  42. * @typedef {ObjectEncodingOptions | BufferEncoding | undefined | null} EncodingOption
  43. */
  44. /** @typedef {(err: NodeJS.ErrnoException | null, result?: string) => void} StringCallback */
  45. /** @typedef {(err: NodeJS.ErrnoException | null, result?: Buffer) => void} BufferCallback */
  46. /** @typedef {(err: NodeJS.ErrnoException | null, result?: (string | Buffer)) => void} StringOrBufferCallback */
  47. /** @typedef {(err: NodeJS.ErrnoException | null, result?: IStats) => void} StatsCallback */
  48. /** @typedef {(err: NodeJS.ErrnoException | null, result?: IBigIntStats) => void} BigIntStatsCallback */
  49. /** @typedef {(err: NodeJS.ErrnoException | null, result?: (IStats | IBigIntStats)) => void} StatsOrBigIntStatsCallback */
  50. /** @typedef {(err: NodeJS.ErrnoException | Error | null, result?: JsonObject) => void} ReadJsonCallback */
  51. /**
  52. * @template T
  53. * @typedef {object} IStatsBase
  54. * @property {() => boolean} isFile is file
  55. * @property {() => boolean} isDirectory is directory
  56. * @property {() => boolean} isBlockDevice is block device
  57. * @property {() => boolean} isCharacterDevice is character device
  58. * @property {() => boolean} isSymbolicLink is symbolic link
  59. * @property {() => boolean} isFIFO is FIFO
  60. * @property {() => boolean} isSocket is socket
  61. * @property {T} dev dev
  62. * @property {T} ino ino
  63. * @property {T} mode mode
  64. * @property {T} nlink nlink
  65. * @property {T} uid uid
  66. * @property {T} gid gid
  67. * @property {T} rdev rdev
  68. * @property {T} size size
  69. * @property {T} blksize blksize
  70. * @property {T} blocks blocks
  71. * @property {T} atimeMs atime ms
  72. * @property {T} mtimeMs mtime ms
  73. * @property {T} ctimeMs ctime ms
  74. * @property {T} birthtimeMs birthtime ms
  75. * @property {Date} atime atime
  76. * @property {Date} mtime mtime
  77. * @property {Date} ctime ctime
  78. * @property {Date} birthtime birthtime
  79. */
  80. /**
  81. * @typedef {IStatsBase<number>} IStats
  82. */
  83. /**
  84. * @typedef {IStatsBase<bigint> & { atimeNs: bigint, mtimeNs: bigint, ctimeNs: bigint, birthtimeNs: bigint }} IBigIntStats
  85. */
  86. /* eslint-disable jsdoc/require-template */
  87. /**
  88. * @template {string | Buffer} [T=string]
  89. * @typedef {object} Dirent
  90. * @property {() => boolean} isFile true when is file, otherwise false
  91. * @property {() => boolean} isDirectory true when is directory, otherwise false
  92. * @property {() => boolean} isBlockDevice true when is block device, otherwise false
  93. * @property {() => boolean} isCharacterDevice true when is character device, otherwise false
  94. * @property {() => boolean} isSymbolicLink true when is symbolic link, otherwise false
  95. * @property {() => boolean} isFIFO true when is FIFO, otherwise false
  96. * @property {() => boolean} isSocket true when is socket, otherwise false
  97. * @property {T} name name
  98. * @property {string} parentPath path
  99. * @property {string=} path path
  100. */
  101. /* eslint-enable jsdoc/require-template */
  102. /**
  103. * @typedef {object} StatOptions
  104. * @property {(boolean | undefined)=} bigint need bigint values
  105. */
  106. /**
  107. * @typedef {object} StatSyncOptions
  108. * @property {(boolean | undefined)=} bigint need bigint values
  109. * @property {(boolean | undefined)=} throwIfNoEntry throw if no entry
  110. */
  111. /**
  112. * @typedef {{
  113. * (path: PathOrFileDescriptor, options: ({ encoding?: null | undefined, flag?: string | undefined } & import("events").Abortable) | undefined | null, callback: BufferCallback): void;
  114. * (path: PathOrFileDescriptor, options: ({ encoding: BufferEncoding, flag?: string | undefined } & import("events").Abortable) | BufferEncoding, callback: StringCallback): void;
  115. * (path: PathOrFileDescriptor, options: (ObjectEncodingOptions & { flag?: string | undefined } & import("events").Abortable) | BufferEncoding | undefined | null, callback: StringOrBufferCallback): void;
  116. * (path: PathOrFileDescriptor, callback: BufferCallback): void;
  117. * }} ReadFile
  118. */
  119. /**
  120. * @typedef {'buffer'| { encoding: 'buffer' }} BufferEncodingOption
  121. */
  122. /**
  123. * @typedef {{
  124. * (path: PathOrFileDescriptor, options?: { encoding?: null | undefined, flag?: string | undefined } | null): Buffer;
  125. * (path: PathOrFileDescriptor, options: { encoding: BufferEncoding, flag?: string | undefined } | BufferEncoding): string;
  126. * (path: PathOrFileDescriptor, options?: (ObjectEncodingOptions & { flag?: string | undefined }) | BufferEncoding | null): string | Buffer;
  127. * }} ReadFileSync
  128. */
  129. /**
  130. * @typedef {{
  131. * (path: PathLike, options: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void;
  132. * (path: PathLike, options: { encoding: 'buffer', withFileTypes?: false | undefined, recursive?: boolean | undefined } | 'buffer', callback: (err: NodeJS.ErrnoException | null, files?: Buffer[]) => void): void;
  133. * (path: PathLike, options: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[] | Buffer[]) => void): void;
  134. * (path: PathLike, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void;
  135. * (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files?: Dirent<string>[]) => void): void;
  136. * (path: PathLike, options: { encoding: 'buffer', withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files: Dirent<Buffer>[]) => void): void;
  137. * }} Readdir
  138. */
  139. /**
  140. * @typedef {{
  141. * (path: PathLike, options?: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined; } | BufferEncoding | null): string[];
  142. * (path: PathLike, options: { encoding: 'buffer', withFileTypes?: false | undefined, recursive?: boolean | undefined } | 'buffer'): Buffer[];
  143. * (path: PathLike, options?: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | null): string[] | Buffer[];
  144. * (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }): Dirent[];
  145. * (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }): Dirent<Buffer>[];
  146. * }} ReaddirSync
  147. */
  148. /**
  149. * @typedef {(pathOrFileDescription: PathOrFileDescriptor, callback: ReadJsonCallback) => void} ReadJson
  150. */
  151. /**
  152. * @typedef {(pathOrFileDescription: PathOrFileDescriptor) => JsonObject} ReadJsonSync
  153. */
  154. /**
  155. * @typedef {{
  156. * (path: PathLike, options: EncodingOption, callback: StringCallback): void;
  157. * (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void;
  158. * (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void;
  159. * (path: PathLike, callback: StringCallback): void;
  160. * }} Readlink
  161. */
  162. /**
  163. * @typedef {{
  164. * (path: PathLike, options?: EncodingOption): string;
  165. * (path: PathLike, options: BufferEncodingOption): Buffer;
  166. * (path: PathLike, options?: EncodingOption): string | Buffer;
  167. * }} ReadlinkSync
  168. */
  169. /**
  170. * @typedef {{
  171. * (path: PathLike, callback: StatsCallback): void;
  172. * (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void;
  173. * (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void;
  174. * (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void;
  175. * }} LStat
  176. */
  177. /**
  178. * @typedef {{
  179. * (path: PathLike, options?: undefined): IStats;
  180. * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined;
  181. * (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined;
  182. * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats;
  183. * (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats;
  184. * (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats;
  185. * (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined;
  186. * }} LStatSync
  187. */
  188. /**
  189. * @typedef {{
  190. * (path: PathLike, callback: StatsCallback): void;
  191. * (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void;
  192. * (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void;
  193. * (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void;
  194. * }} Stat
  195. */
  196. /**
  197. * @typedef {{
  198. * (path: PathLike, options?: undefined): IStats;
  199. * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined;
  200. * (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined;
  201. * (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats;
  202. * (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats;
  203. * (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats;
  204. * (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined;
  205. * }} StatSync
  206. */
  207. /**
  208. * @typedef {{
  209. * (path: PathLike, options: EncodingOption, callback: StringCallback): void;
  210. * (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void;
  211. * (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void;
  212. * (path: PathLike, callback: StringCallback): void;
  213. * }} RealPath
  214. */
  215. /**
  216. * @typedef {{
  217. * (path: PathLike, options?: EncodingOption): string;
  218. * (path: PathLike, options: BufferEncodingOption): Buffer;
  219. * (path: PathLike, options?: EncodingOption): string | Buffer;
  220. * }} RealPathSync
  221. */
  222. /**
  223. * @typedef {object} FileSystem
  224. * @property {ReadFile} readFile read file method
  225. * @property {Readdir} readdir readdir method
  226. * @property {ReadJson=} readJson read json method
  227. * @property {Readlink} readlink read link method
  228. * @property {LStat=} lstat lstat method
  229. * @property {Stat} stat stat method
  230. * @property {RealPath=} realpath realpath method
  231. */
  232. /**
  233. * @typedef {object} SyncFileSystem
  234. * @property {ReadFileSync} readFileSync read file sync method
  235. * @property {ReaddirSync} readdirSync read dir sync method
  236. * @property {ReadJsonSync=} readJsonSync read json sync method
  237. * @property {ReadlinkSync} readlinkSync read link sync method
  238. * @property {LStatSync=} lstatSync lstat sync method
  239. * @property {StatSync} statSync stat sync method
  240. * @property {RealPathSync=} realpathSync real path sync method
  241. */
  242. /**
  243. * @typedef {object} ParsedIdentifier
  244. * @property {string} request request
  245. * @property {string} query query
  246. * @property {string} fragment fragment
  247. * @property {boolean} directory is directory
  248. * @property {boolean} module is module
  249. * @property {boolean} file is file
  250. * @property {boolean} internal is internal
  251. */
  252. /** @typedef {string | number | boolean | null} JsonPrimitive */
  253. /** @typedef {JsonValue[]} JsonArray */
  254. /** @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue */
  255. /** @typedef {{[Key in string]: JsonValue} & {[Key in string]?: JsonValue | undefined}} JsonObject */
  256. // eslint-disable-next-line jsdoc/require-property
  257. /** @typedef {object} Context */
  258. /**
  259. * @typedef {object} BaseResolveRequest
  260. * @property {string | false} path path
  261. * @property {Context=} context content
  262. * @property {string=} descriptionFilePath description file path
  263. * @property {string=} descriptionFileRoot description file root
  264. * @property {JsonObject=} descriptionFileData description file data
  265. * @property {string=} relativePath relative path
  266. * @property {boolean=} ignoreSymlinks true when need to ignore symlinks, otherwise false
  267. * @property {boolean=} fullySpecified true when full specified, otherwise false
  268. * @property {string=} __innerRequest inner request for internal usage
  269. * @property {string=} __innerRequest_request inner request for internal usage
  270. * @property {string=} __innerRequest_relativePath inner relative path for internal usage
  271. */
  272. /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
  273. /**
  274. * String with special formatting
  275. * @typedef {string} StackEntry
  276. */
  277. /**
  278. * @template T
  279. * @typedef {{ add: (item: T) => void }} WriteOnlySet
  280. */
  281. /** @typedef {(request: ResolveRequest) => void} ResolveContextYield */
  282. /**
  283. * Resolve context
  284. * @typedef {object} ResolveContext
  285. * @property {WriteOnlySet<string>=} contextDependencies directories that was found on file system
  286. * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
  287. * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
  288. * @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
  289. * @property {((str: string) => void)=} log log function
  290. * @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
  291. */
  292. /** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
  293. /**
  294. * @typedef {object} KnownHooks
  295. * @property {SyncHook<[ResolveStepHook, ResolveRequest], void>} resolveStep resolve step hook
  296. * @property {SyncHook<[ResolveRequest, Error]>} noResolve no resolve hook
  297. * @property {ResolveStepHook} resolve resolve hook
  298. * @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result result hook
  299. */
  300. /**
  301. * @typedef {{[key: string]: ResolveStepHook}} EnsuredHooks
  302. */
  303. /**
  304. * @param {string} str input string
  305. * @returns {string} in camel case
  306. */
  307. function toCamelCase(str) {
  308. return str.replace(/-([a-z])/g, (str) => str.slice(1).toUpperCase());
  309. }
  310. class Resolver {
  311. /**
  312. * @param {ResolveStepHook} hook hook
  313. * @param {ResolveRequest} request request
  314. * @returns {StackEntry} stack entry
  315. */
  316. static createStackEntry(hook, request) {
  317. return `${hook.name}: (${request.path}) ${request.request || ""}${
  318. request.query || ""
  319. }${request.fragment || ""}${
  320. request.directory ? " directory" : ""
  321. }${request.module ? " module" : ""}`;
  322. }
  323. /**
  324. * @param {FileSystem} fileSystem a filesystem
  325. * @param {ResolveOptions} options options
  326. */
  327. constructor(fileSystem, options) {
  328. this.fileSystem = fileSystem;
  329. this.options = options;
  330. /** @type {KnownHooks} */
  331. this.hooks = {
  332. resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
  333. noResolve: new SyncHook(["request", "error"], "noResolve"),
  334. resolve: new AsyncSeriesBailHook(
  335. ["request", "resolveContext"],
  336. "resolve",
  337. ),
  338. result: new AsyncSeriesHook(["result", "resolveContext"], "result"),
  339. };
  340. }
  341. /**
  342. * @param {string | ResolveStepHook} name hook name or hook itself
  343. * @returns {ResolveStepHook} the hook
  344. */
  345. ensureHook(name) {
  346. if (typeof name !== "string") {
  347. return name;
  348. }
  349. name = toCamelCase(name);
  350. if (name.startsWith("before")) {
  351. return /** @type {ResolveStepHook} */ (
  352. this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
  353. stage: -10,
  354. })
  355. );
  356. }
  357. if (name.startsWith("after")) {
  358. return /** @type {ResolveStepHook} */ (
  359. this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
  360. stage: 10,
  361. })
  362. );
  363. }
  364. /** @type {ResolveStepHook} */
  365. const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  366. if (!hook) {
  367. /** @type {KnownHooks & EnsuredHooks} */
  368. (this.hooks)[name] = new AsyncSeriesBailHook(
  369. ["request", "resolveContext"],
  370. name,
  371. );
  372. return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  373. }
  374. return hook;
  375. }
  376. /**
  377. * @param {string | ResolveStepHook} name hook name or hook itself
  378. * @returns {ResolveStepHook} the hook
  379. */
  380. getHook(name) {
  381. if (typeof name !== "string") {
  382. return name;
  383. }
  384. name = toCamelCase(name);
  385. if (name.startsWith("before")) {
  386. return /** @type {ResolveStepHook} */ (
  387. this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
  388. stage: -10,
  389. })
  390. );
  391. }
  392. if (name.startsWith("after")) {
  393. return /** @type {ResolveStepHook} */ (
  394. this.getHook(name[5].toLowerCase() + name.slice(6)).withOptions({
  395. stage: 10,
  396. })
  397. );
  398. }
  399. /** @type {ResolveStepHook} */
  400. const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  401. if (!hook) {
  402. throw new Error(`Hook ${name} doesn't exist`);
  403. }
  404. return hook;
  405. }
  406. /**
  407. * @param {object} context context information object
  408. * @param {string} path context path
  409. * @param {string} request request string
  410. * @returns {string | false} result
  411. */
  412. resolveSync(context, path, request) {
  413. /** @type {Error | null | undefined} */
  414. let err;
  415. /** @type {string | false | undefined} */
  416. let result;
  417. let sync = false;
  418. this.resolve(context, path, request, {}, (_err, r) => {
  419. err = _err;
  420. result = r;
  421. sync = true;
  422. });
  423. if (!sync) {
  424. throw new Error(
  425. "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!",
  426. );
  427. }
  428. if (err) throw err;
  429. if (result === undefined) throw new Error("No result");
  430. return result;
  431. }
  432. /**
  433. * @param {object} context context information object
  434. * @param {string} path context path
  435. * @param {string} request request string
  436. * @param {ResolveContext} resolveContext resolve context
  437. * @param {ResolveCallback} callback callback function
  438. * @returns {void}
  439. */
  440. resolve(context, path, request, resolveContext, callback) {
  441. if (!context || typeof context !== "object") {
  442. return callback(new Error("context argument is not an object"));
  443. }
  444. if (typeof path !== "string") {
  445. return callback(new Error("path argument is not a string"));
  446. }
  447. if (typeof request !== "string") {
  448. return callback(new Error("request argument is not a string"));
  449. }
  450. if (!resolveContext) {
  451. return callback(new Error("resolveContext argument is not set"));
  452. }
  453. /** @type {ResolveRequest} */
  454. const obj = {
  455. context,
  456. path,
  457. request,
  458. };
  459. /** @type {ResolveContextYield | undefined} */
  460. let yield_;
  461. let yieldCalled = false;
  462. /** @type {ResolveContextYield | undefined} */
  463. let finishYield;
  464. if (typeof resolveContext.yield === "function") {
  465. const old = resolveContext.yield;
  466. /**
  467. * @param {ResolveRequest} obj object
  468. */
  469. yield_ = (obj) => {
  470. old(obj);
  471. yieldCalled = true;
  472. };
  473. /**
  474. * @param {ResolveRequest} result result
  475. * @returns {void}
  476. */
  477. finishYield = (result) => {
  478. if (result) {
  479. /** @type {ResolveContextYield} */ (yield_)(result);
  480. }
  481. callback(null);
  482. };
  483. }
  484. const message = `resolve '${request}' in '${path}'`;
  485. /**
  486. * @param {ResolveRequest} result result
  487. * @returns {void}
  488. */
  489. const finishResolved = (result) =>
  490. callback(
  491. null,
  492. result.path === false
  493. ? false
  494. : `${result.path.replace(/#/g, "\0#")}${
  495. result.query ? result.query.replace(/#/g, "\0#") : ""
  496. }${result.fragment || ""}`,
  497. result,
  498. );
  499. /**
  500. * @param {string[]} log logs
  501. * @returns {void}
  502. */
  503. const finishWithoutResolve = (log) => {
  504. /**
  505. * @type {ErrorWithDetail}
  506. */
  507. const error = new Error(`Can't ${message}`);
  508. error.details = log.join("\n");
  509. this.hooks.noResolve.call(obj, error);
  510. return callback(error);
  511. };
  512. if (resolveContext.log) {
  513. // We need log anyway to capture it in case of an error
  514. const parentLog = resolveContext.log;
  515. /** @type {string[]} */
  516. const log = [];
  517. return this.doResolve(
  518. this.hooks.resolve,
  519. obj,
  520. message,
  521. {
  522. log: (msg) => {
  523. parentLog(msg);
  524. log.push(msg);
  525. },
  526. yield: yield_,
  527. fileDependencies: resolveContext.fileDependencies,
  528. contextDependencies: resolveContext.contextDependencies,
  529. missingDependencies: resolveContext.missingDependencies,
  530. stack: resolveContext.stack,
  531. },
  532. (err, result) => {
  533. if (err) return callback(err);
  534. if (yieldCalled || (result && yield_)) {
  535. return /** @type {ResolveContextYield} */ (finishYield)(
  536. /** @type {ResolveRequest} */ (result),
  537. );
  538. }
  539. if (result) return finishResolved(result);
  540. return finishWithoutResolve(log);
  541. },
  542. );
  543. }
  544. // Try to resolve assuming there is no error
  545. // We don't log stuff in this case
  546. return this.doResolve(
  547. this.hooks.resolve,
  548. obj,
  549. message,
  550. {
  551. log: undefined,
  552. yield: yield_,
  553. fileDependencies: resolveContext.fileDependencies,
  554. contextDependencies: resolveContext.contextDependencies,
  555. missingDependencies: resolveContext.missingDependencies,
  556. stack: resolveContext.stack,
  557. },
  558. (err, result) => {
  559. if (err) return callback(err);
  560. if (yieldCalled || (result && yield_)) {
  561. return /** @type {ResolveContextYield} */ (finishYield)(
  562. /** @type {ResolveRequest} */ (result),
  563. );
  564. }
  565. if (result) return finishResolved(result);
  566. // log is missing for the error details
  567. // so we redo the resolving for the log info
  568. // this is more expensive to the success case
  569. // is assumed by default
  570. /** @type {string[]} */
  571. const log = [];
  572. return this.doResolve(
  573. this.hooks.resolve,
  574. obj,
  575. message,
  576. {
  577. log: (msg) => log.push(msg),
  578. yield: yield_,
  579. stack: resolveContext.stack,
  580. },
  581. (err, result) => {
  582. if (err) return callback(err);
  583. // In a case that there is a race condition and yield will be called
  584. if (yieldCalled || (result && yield_)) {
  585. return /** @type {ResolveContextYield} */ (finishYield)(
  586. /** @type {ResolveRequest} */ (result),
  587. );
  588. }
  589. return finishWithoutResolve(log);
  590. },
  591. );
  592. },
  593. );
  594. }
  595. /**
  596. * @param {ResolveStepHook} hook hook
  597. * @param {ResolveRequest} request request
  598. * @param {null|string} message string
  599. * @param {ResolveContext} resolveContext resolver context
  600. * @param {(err?: null|Error, result?: ResolveRequest) => void} callback callback
  601. * @returns {void}
  602. */
  603. doResolve(hook, request, message, resolveContext, callback) {
  604. const stackEntry = Resolver.createStackEntry(hook, request);
  605. /** @type {Set<string> | undefined} */
  606. let newStack;
  607. if (resolveContext.stack) {
  608. newStack = new Set(resolveContext.stack);
  609. if (resolveContext.stack.has(stackEntry)) {
  610. /**
  611. * Prevent recursion
  612. * @type {Error & {recursion?: boolean}}
  613. */
  614. const recursionError = new Error(
  615. `Recursion in resolving\nStack:\n ${[...newStack].join("\n ")}`,
  616. );
  617. recursionError.recursion = true;
  618. if (resolveContext.log) {
  619. resolveContext.log("abort resolving because of recursion");
  620. }
  621. return callback(recursionError);
  622. }
  623. newStack.add(stackEntry);
  624. } else {
  625. // creating a set with new Set([item])
  626. // allocates a new array that has to be garbage collected
  627. // this is an EXTREMELY hot path, so let's avoid it
  628. newStack = new Set();
  629. newStack.add(stackEntry);
  630. }
  631. this.hooks.resolveStep.call(hook, request);
  632. if (hook.isUsed()) {
  633. const innerContext = createInnerContext(
  634. {
  635. log: resolveContext.log,
  636. yield: resolveContext.yield,
  637. fileDependencies: resolveContext.fileDependencies,
  638. contextDependencies: resolveContext.contextDependencies,
  639. missingDependencies: resolveContext.missingDependencies,
  640. stack: newStack,
  641. },
  642. message,
  643. );
  644. return hook.callAsync(request, innerContext, (err, result) => {
  645. if (err) return callback(err);
  646. if (result) return callback(null, result);
  647. callback();
  648. });
  649. }
  650. callback();
  651. }
  652. /**
  653. * @param {string} identifier identifier
  654. * @returns {ParsedIdentifier} parsed identifier
  655. */
  656. parse(identifier) {
  657. const part = {
  658. request: "",
  659. query: "",
  660. fragment: "",
  661. module: false,
  662. directory: false,
  663. file: false,
  664. internal: false,
  665. };
  666. const parsedIdentifier = parseIdentifier(identifier);
  667. if (!parsedIdentifier) return part;
  668. [part.request, part.query, part.fragment] = parsedIdentifier;
  669. if (part.request.length > 0) {
  670. part.internal = this.isPrivate(identifier);
  671. part.module = this.isModule(part.request);
  672. part.directory = this.isDirectory(part.request);
  673. if (part.directory) {
  674. part.request = part.request.slice(0, -1);
  675. }
  676. }
  677. return part;
  678. }
  679. /**
  680. * @param {string} path path
  681. * @returns {boolean} true, if the path is a module
  682. */
  683. isModule(path) {
  684. return getType(path) === PathType.Normal;
  685. }
  686. /**
  687. * @param {string} path path
  688. * @returns {boolean} true, if the path is private
  689. */
  690. isPrivate(path) {
  691. return getType(path) === PathType.Internal;
  692. }
  693. /**
  694. * @param {string} path a path
  695. * @returns {boolean} true, if the path is a directory path
  696. */
  697. isDirectory(path) {
  698. return path.endsWith("/");
  699. }
  700. /**
  701. * @param {string} path path
  702. * @param {string} request request
  703. * @returns {string} joined path
  704. */
  705. join(path, request) {
  706. return join(path, request);
  707. }
  708. /**
  709. * @param {string} path path
  710. * @returns {string} normalized path
  711. */
  712. normalize(path) {
  713. return normalize(path);
  714. }
  715. }
  716. module.exports = Resolver;