Resolver.js 26 KB

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