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 {Set<string>} fileDependencies file dependencies
  267. */
  268. /**
  269. * @typedef {object} TsconfigPathsData
  270. * @property {AliasOption[]} alias tsconfig file data
  271. * @property {string[]} modules tsconfig file data
  272. */
  273. /**
  274. * @typedef {object} BaseResolveRequest
  275. * @property {string | false} path path
  276. * @property {Context=} context content
  277. * @property {string=} descriptionFilePath description file path
  278. * @property {string=} descriptionFileRoot description file root
  279. * @property {JsonObject=} descriptionFileData description file data
  280. * @property {TsconfigPathsMap | null | undefined=} tsconfigPathsMap tsconfig paths map
  281. * @property {string=} relativePath relative path
  282. * @property {boolean=} ignoreSymlinks true when need to ignore symlinks, otherwise false
  283. * @property {boolean=} fullySpecified true when full specified, otherwise false
  284. * @property {string=} __innerRequest inner request for internal usage
  285. * @property {string=} __innerRequest_request inner request for internal usage
  286. * @property {string=} __innerRequest_relativePath inner relative path for internal usage
  287. */
  288. /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
  289. /**
  290. * String with special formatting
  291. * @typedef {string} StackEntry
  292. */
  293. /**
  294. * @template T
  295. * @typedef {{ add: (item: T) => void }} WriteOnlySet
  296. */
  297. /** @typedef {(request: ResolveRequest) => void} ResolveContextYield */
  298. /**
  299. * Resolve context
  300. * @typedef {object} ResolveContext
  301. * @property {WriteOnlySet<string>=} contextDependencies directories that was found on file system
  302. * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
  303. * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
  304. * @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
  305. * @property {((str: string) => void)=} log log function
  306. * @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
  307. */
  308. /** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
  309. /**
  310. * @typedef {object} KnownHooks
  311. * @property {SyncHook<[ResolveStepHook, ResolveRequest], void>} resolveStep resolve step hook
  312. * @property {SyncHook<[ResolveRequest, Error]>} noResolve no resolve hook
  313. * @property {ResolveStepHook} resolve resolve hook
  314. * @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result result hook
  315. */
  316. /**
  317. * @typedef {{ [key: string]: ResolveStepHook }} EnsuredHooks
  318. */
  319. /**
  320. * @param {string} str input string
  321. * @returns {string} in camel case
  322. */
  323. function toCamelCase(str) {
  324. return str.replace(/-([a-z])/g, (str) => str.slice(1).toUpperCase());
  325. }
  326. class Resolver {
  327. /**
  328. * @param {ResolveStepHook} hook hook
  329. * @param {ResolveRequest} request request
  330. * @returns {StackEntry} stack entry
  331. */
  332. static createStackEntry(hook, request) {
  333. return `${hook.name}: (${request.path}) ${request.request || ""}${
  334. request.query || ""
  335. }${request.fragment || ""}${request.directory ? " directory" : ""}${
  336. request.module ? " module" : ""
  337. }`;
  338. }
  339. /**
  340. * @param {FileSystem} fileSystem a filesystem
  341. * @param {ResolveOptions} options options
  342. */
  343. constructor(fileSystem, options) {
  344. this.fileSystem = fileSystem;
  345. this.options = options;
  346. /** @type {KnownHooks} */
  347. this.hooks = {
  348. resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
  349. noResolve: new SyncHook(["request", "error"], "noResolve"),
  350. resolve: new AsyncSeriesBailHook(
  351. ["request", "resolveContext"],
  352. "resolve",
  353. ),
  354. result: new AsyncSeriesHook(["result", "resolveContext"], "result"),
  355. };
  356. }
  357. /**
  358. * @param {string | ResolveStepHook} name hook name or hook itself
  359. * @returns {ResolveStepHook} the hook
  360. */
  361. ensureHook(name) {
  362. if (typeof name !== "string") {
  363. return name;
  364. }
  365. name = toCamelCase(name);
  366. if (name.startsWith("before")) {
  367. return /** @type {ResolveStepHook} */ (
  368. this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
  369. stage: -10,
  370. })
  371. );
  372. }
  373. if (name.startsWith("after")) {
  374. return /** @type {ResolveStepHook} */ (
  375. this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
  376. stage: 10,
  377. })
  378. );
  379. }
  380. /** @type {ResolveStepHook} */
  381. const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  382. if (!hook) {
  383. /** @type {KnownHooks & EnsuredHooks} */
  384. (this.hooks)[name] = new AsyncSeriesBailHook(
  385. ["request", "resolveContext"],
  386. name,
  387. );
  388. return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  389. }
  390. return hook;
  391. }
  392. /**
  393. * @param {string | ResolveStepHook} name hook name or hook itself
  394. * @returns {ResolveStepHook} the hook
  395. */
  396. getHook(name) {
  397. if (typeof name !== "string") {
  398. return name;
  399. }
  400. name = toCamelCase(name);
  401. if (name.startsWith("before")) {
  402. return /** @type {ResolveStepHook} */ (
  403. this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
  404. stage: -10,
  405. })
  406. );
  407. }
  408. if (name.startsWith("after")) {
  409. return /** @type {ResolveStepHook} */ (
  410. this.getHook(name[5].toLowerCase() + name.slice(6)).withOptions({
  411. stage: 10,
  412. })
  413. );
  414. }
  415. /** @type {ResolveStepHook} */
  416. const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  417. if (!hook) {
  418. throw new Error(`Hook ${name} doesn't exist`);
  419. }
  420. return hook;
  421. }
  422. /**
  423. * @param {Context} context context information object
  424. * @param {string} path context path
  425. * @param {string} request request string
  426. * @returns {string | false} result
  427. */
  428. resolveSync(context, path, request) {
  429. /** @type {Error | null | undefined} */
  430. let err;
  431. /** @type {string | false | undefined} */
  432. let result;
  433. let sync = false;
  434. this.resolve(context, path, request, {}, (_err, r) => {
  435. err = _err;
  436. result = r;
  437. sync = true;
  438. });
  439. if (!sync) {
  440. throw new Error(
  441. "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!",
  442. );
  443. }
  444. if (err) throw err;
  445. if (result === undefined) throw new Error("No result");
  446. return result;
  447. }
  448. /**
  449. * @param {Context} context context information object
  450. * @param {string} path context path
  451. * @param {string} request request string
  452. * @param {ResolveContext} resolveContext resolve context
  453. * @param {ResolveCallback} callback callback function
  454. * @returns {void}
  455. */
  456. resolve(context, path, request, resolveContext, callback) {
  457. if (!context || typeof context !== "object") {
  458. return callback(new Error("context argument is not an object"));
  459. }
  460. if (typeof path !== "string") {
  461. return callback(new Error("path argument is not a string"));
  462. }
  463. if (typeof request !== "string") {
  464. return callback(new Error("request argument is not a string"));
  465. }
  466. if (!resolveContext) {
  467. return callback(new Error("resolveContext argument is not set"));
  468. }
  469. /** @type {ResolveRequest} */
  470. const obj = {
  471. context,
  472. path,
  473. request,
  474. };
  475. /** @type {ResolveContextYield | undefined} */
  476. let yield_;
  477. let yieldCalled = false;
  478. /** @type {ResolveContextYield | undefined} */
  479. let finishYield;
  480. if (typeof resolveContext.yield === "function") {
  481. const old = resolveContext.yield;
  482. /**
  483. * @param {ResolveRequest} obj object
  484. */
  485. yield_ = (obj) => {
  486. old(obj);
  487. yieldCalled = true;
  488. };
  489. /**
  490. * @param {ResolveRequest} result result
  491. * @returns {void}
  492. */
  493. finishYield = (result) => {
  494. if (result) {
  495. /** @type {ResolveContextYield} */ (yield_)(result);
  496. }
  497. callback(null);
  498. };
  499. }
  500. const message = `resolve '${request}' in '${path}'`;
  501. /**
  502. * @param {ResolveRequest} result result
  503. * @returns {void}
  504. */
  505. const finishResolved = (result) =>
  506. callback(
  507. null,
  508. result.path === false
  509. ? false
  510. : `${result.path.replace(/#/g, "\0#")}${
  511. result.query ? result.query.replace(/#/g, "\0#") : ""
  512. }${result.fragment || ""}`,
  513. result,
  514. );
  515. /**
  516. * @param {string[]} log logs
  517. * @returns {void}
  518. */
  519. const finishWithoutResolve = (log) => {
  520. /**
  521. * @type {ErrorWithDetail}
  522. */
  523. const error = new Error(`Can't ${message}`);
  524. error.details = log.join("\n");
  525. this.hooks.noResolve.call(obj, error);
  526. return callback(error);
  527. };
  528. if (resolveContext.log) {
  529. // We need log anyway to capture it in case of an error
  530. const parentLog = resolveContext.log;
  531. /** @type {string[]} */
  532. const log = [];
  533. return this.doResolve(
  534. this.hooks.resolve,
  535. obj,
  536. message,
  537. {
  538. log: (msg) => {
  539. parentLog(msg);
  540. log.push(msg);
  541. },
  542. yield: yield_,
  543. fileDependencies: resolveContext.fileDependencies,
  544. contextDependencies: resolveContext.contextDependencies,
  545. missingDependencies: resolveContext.missingDependencies,
  546. stack: resolveContext.stack,
  547. },
  548. (err, result) => {
  549. if (err) return callback(err);
  550. if (yieldCalled || (result && yield_)) {
  551. return /** @type {ResolveContextYield} */ (finishYield)(
  552. /** @type {ResolveRequest} */ (result),
  553. );
  554. }
  555. if (result) return finishResolved(result);
  556. return finishWithoutResolve(log);
  557. },
  558. );
  559. }
  560. // Try to resolve assuming there is no error
  561. // We don't log stuff in this case
  562. return this.doResolve(
  563. this.hooks.resolve,
  564. obj,
  565. message,
  566. {
  567. log: undefined,
  568. yield: yield_,
  569. fileDependencies: resolveContext.fileDependencies,
  570. contextDependencies: resolveContext.contextDependencies,
  571. missingDependencies: resolveContext.missingDependencies,
  572. stack: resolveContext.stack,
  573. },
  574. (err, result) => {
  575. if (err) return callback(err);
  576. if (yieldCalled || (result && yield_)) {
  577. return /** @type {ResolveContextYield} */ (finishYield)(
  578. /** @type {ResolveRequest} */ (result),
  579. );
  580. }
  581. if (result) return finishResolved(result);
  582. // log is missing for the error details
  583. // so we redo the resolving for the log info
  584. // this is more expensive to the success case
  585. // is assumed by default
  586. /** @type {string[]} */
  587. const log = [];
  588. return this.doResolve(
  589. this.hooks.resolve,
  590. obj,
  591. message,
  592. {
  593. log: (msg) => log.push(msg),
  594. yield: yield_,
  595. stack: resolveContext.stack,
  596. },
  597. (err, result) => {
  598. if (err) return callback(err);
  599. // In a case that there is a race condition and yield will be called
  600. if (yieldCalled || (result && yield_)) {
  601. return /** @type {ResolveContextYield} */ (finishYield)(
  602. /** @type {ResolveRequest} */ (result),
  603. );
  604. }
  605. return finishWithoutResolve(log);
  606. },
  607. );
  608. },
  609. );
  610. }
  611. /**
  612. * @param {ResolveStepHook} hook hook
  613. * @param {ResolveRequest} request request
  614. * @param {null | string} message string
  615. * @param {ResolveContext} resolveContext resolver context
  616. * @param {(err?: null | Error, result?: ResolveRequest) => void} callback callback
  617. * @returns {void}
  618. */
  619. doResolve(hook, request, message, resolveContext, callback) {
  620. const stackEntry = Resolver.createStackEntry(hook, request);
  621. /** @type {Set<string> | undefined} */
  622. let newStack;
  623. if (resolveContext.stack) {
  624. newStack = new Set(resolveContext.stack);
  625. if (resolveContext.stack.has(stackEntry)) {
  626. /**
  627. * Prevent recursion
  628. * @type {Error & { recursion?: boolean }}
  629. */
  630. const recursionError = new Error(
  631. `Recursion in resolving\nStack:\n ${[...newStack].join("\n ")}`,
  632. );
  633. recursionError.recursion = true;
  634. if (resolveContext.log) {
  635. resolveContext.log("abort resolving because of recursion");
  636. }
  637. return callback(recursionError);
  638. }
  639. newStack.add(stackEntry);
  640. } else {
  641. // creating a set with new Set([item])
  642. // allocates a new array that has to be garbage collected
  643. // this is an EXTREMELY hot path, so let's avoid it
  644. newStack = new Set();
  645. newStack.add(stackEntry);
  646. }
  647. this.hooks.resolveStep.call(hook, request);
  648. if (hook.isUsed()) {
  649. const innerContext = createInnerContext(
  650. {
  651. log: resolveContext.log,
  652. yield: resolveContext.yield,
  653. fileDependencies: resolveContext.fileDependencies,
  654. contextDependencies: resolveContext.contextDependencies,
  655. missingDependencies: resolveContext.missingDependencies,
  656. stack: newStack,
  657. },
  658. message,
  659. );
  660. return hook.callAsync(request, innerContext, (err, result) => {
  661. if (err) return callback(err);
  662. if (result) return callback(null, result);
  663. callback();
  664. });
  665. }
  666. callback();
  667. }
  668. /**
  669. * @param {string} identifier identifier
  670. * @returns {ParsedIdentifier} parsed identifier
  671. */
  672. parse(identifier) {
  673. const part = {
  674. request: "",
  675. query: "",
  676. fragment: "",
  677. module: false,
  678. directory: false,
  679. file: false,
  680. internal: false,
  681. };
  682. const parsedIdentifier = parseIdentifier(identifier);
  683. if (!parsedIdentifier) return part;
  684. [part.request, part.query, part.fragment] = parsedIdentifier;
  685. if (part.request.length > 0) {
  686. part.internal = this.isPrivate(identifier);
  687. part.module = this.isModule(part.request);
  688. part.directory = this.isDirectory(part.request);
  689. if (part.directory) {
  690. part.request = part.request.slice(0, -1);
  691. }
  692. }
  693. return part;
  694. }
  695. /**
  696. * @param {string} path path
  697. * @returns {boolean} true, if the path is a module
  698. */
  699. isModule(path) {
  700. return getType(path) === PathType.Normal;
  701. }
  702. /**
  703. * @param {string} path path
  704. * @returns {boolean} true, if the path is private
  705. */
  706. isPrivate(path) {
  707. return getType(path) === PathType.Internal;
  708. }
  709. /**
  710. * @param {string} path a path
  711. * @returns {boolean} true, if the path is a directory path
  712. */
  713. isDirectory(path) {
  714. return path.endsWith("/");
  715. }
  716. /**
  717. * @param {string} path path
  718. * @param {string} request request
  719. * @returns {string} joined path
  720. */
  721. join(path, request) {
  722. return join(path, request);
  723. }
  724. /**
  725. * @param {string} path path
  726. * @returns {string} normalized path
  727. */
  728. normalize(path) {
  729. return normalize(path);
  730. }
  731. }
  732. module.exports = Resolver;