Resolver.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  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} FileSystemStats
  20. * @property {function(): boolean} isDirectory
  21. * @property {function(): boolean} isFile
  22. */
  23. /**
  24. * @typedef {Object} FileSystemDirent
  25. * @property {Buffer | string} name
  26. * @property {function(): boolean} isDirectory
  27. * @property {function(): boolean} isFile
  28. */
  29. /**
  30. * @typedef {Object} PossibleFileSystemError
  31. * @property {string=} code
  32. * @property {number=} errno
  33. * @property {string=} path
  34. * @property {string=} syscall
  35. */
  36. /**
  37. * @template T
  38. * @callback FileSystemCallback
  39. * @param {PossibleFileSystemError & Error | null | undefined} err
  40. * @param {T=} result
  41. */
  42. /** @typedef {function((NodeJS.ErrnoException | null)=, (string | Buffer)[] | import("fs").Dirent[]=): void} DirentArrayCallback */
  43. /**
  44. * @typedef {Object} ReaddirOptions
  45. * @property {BufferEncoding | null | 'buffer'} [encoding]
  46. * @property {boolean | undefined} [withFileTypes=false]
  47. */
  48. /**
  49. * @typedef {Object} FileSystem
  50. * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readFile
  51. * @property {function(string, (ReaddirOptions | BufferEncoding | null | undefined | 'buffer' | DirentArrayCallback)=, DirentArrayCallback=): void} readdir
  52. * @property {((function(string, FileSystemCallback<object>): void) & function(string, object, FileSystemCallback<object>): void)=} readJson
  53. * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readlink
  54. * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void=} lstat
  55. * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} stat
  56. */
  57. /**
  58. * @typedef {Object} SyncFileSystem
  59. * @property {function(string, object=): Buffer | string} readFileSync
  60. * @property {function(string, object=): (Buffer | string)[] | FileSystemDirent[]} readdirSync
  61. * @property {(function(string, object=): object)=} readJsonSync
  62. * @property {function(string, object=): Buffer | string} readlinkSync
  63. * @property {function(string, object=): FileSystemStats=} lstatSync
  64. * @property {function(string, object=): FileSystemStats} statSync
  65. */
  66. /**
  67. * @typedef {Object} ParsedIdentifier
  68. * @property {string} request
  69. * @property {string} query
  70. * @property {string} fragment
  71. * @property {boolean} directory
  72. * @property {boolean} module
  73. * @property {boolean} file
  74. * @property {boolean} internal
  75. */
  76. /** @typedef {string | number | boolean | null} JsonPrimitive */
  77. /** @typedef {JsonValue[]} JsonArray */
  78. /** @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue */
  79. /** @typedef {{[Key in string]: JsonValue} & {[Key in string]?: JsonValue | undefined}} JsonObject */
  80. /**
  81. * @typedef {Object} BaseResolveRequest
  82. * @property {string | false} path
  83. * @property {object=} context
  84. * @property {string=} descriptionFilePath
  85. * @property {string=} descriptionFileRoot
  86. * @property {JsonObject=} descriptionFileData
  87. * @property {string=} relativePath
  88. * @property {boolean=} ignoreSymlinks
  89. * @property {boolean=} fullySpecified
  90. * @property {string=} __innerRequest
  91. * @property {string=} __innerRequest_request
  92. * @property {string=} __innerRequest_relativePath
  93. */
  94. /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
  95. /**
  96. * String with special formatting
  97. * @typedef {string} StackEntry
  98. */
  99. /**
  100. * @template T
  101. * @typedef {{ add: (item: T) => void }} WriteOnlySet
  102. */
  103. /** @typedef {(function (ResolveRequest): void)} ResolveContextYield */
  104. /**
  105. * Resolve context
  106. * @typedef {Object} ResolveContext
  107. * @property {WriteOnlySet<string>=} contextDependencies
  108. * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
  109. * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
  110. * @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
  111. * @property {(function(string): void)=} log log function
  112. * @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
  113. */
  114. /** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
  115. /**
  116. * @typedef {Object} KnownHooks
  117. * @property {SyncHook<[ResolveStepHook, ResolveRequest], void>} resolveStep
  118. * @property {SyncHook<[ResolveRequest, Error]>} noResolve
  119. * @property {ResolveStepHook} resolve
  120. * @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result
  121. */
  122. /**
  123. * @typedef {{[key: string]: ResolveStepHook}} EnsuredHooks
  124. */
  125. /**
  126. * @param {string} str input string
  127. * @returns {string} in camel case
  128. */
  129. function toCamelCase(str) {
  130. return str.replace(/-([a-z])/g, str => str.slice(1).toUpperCase());
  131. }
  132. class Resolver {
  133. /**
  134. * @param {ResolveStepHook} hook hook
  135. * @param {ResolveRequest} request request
  136. * @returns {StackEntry} stack entry
  137. */
  138. static createStackEntry(hook, request) {
  139. return (
  140. hook.name +
  141. ": (" +
  142. request.path +
  143. ") " +
  144. (request.request || "") +
  145. (request.query || "") +
  146. (request.fragment || "") +
  147. (request.directory ? " directory" : "") +
  148. (request.module ? " module" : "")
  149. );
  150. }
  151. /**
  152. * @param {FileSystem} fileSystem a filesystem
  153. * @param {ResolveOptions} options options
  154. */
  155. constructor(fileSystem, options) {
  156. this.fileSystem = fileSystem;
  157. this.options = options;
  158. /** @type {KnownHooks} */
  159. this.hooks = {
  160. resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
  161. noResolve: new SyncHook(["request", "error"], "noResolve"),
  162. resolve: new AsyncSeriesBailHook(
  163. ["request", "resolveContext"],
  164. "resolve"
  165. ),
  166. result: new AsyncSeriesHook(["result", "resolveContext"], "result")
  167. };
  168. }
  169. /**
  170. * @param {string | ResolveStepHook} name hook name or hook itself
  171. * @returns {ResolveStepHook} the hook
  172. */
  173. ensureHook(name) {
  174. if (typeof name !== "string") {
  175. return name;
  176. }
  177. name = toCamelCase(name);
  178. if (/^before/.test(name)) {
  179. return /** @type {ResolveStepHook} */ (
  180. this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
  181. stage: -10
  182. })
  183. );
  184. }
  185. if (/^after/.test(name)) {
  186. return /** @type {ResolveStepHook} */ (
  187. this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
  188. stage: 10
  189. })
  190. );
  191. }
  192. /** @type {ResolveStepHook} */
  193. const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  194. if (!hook) {
  195. /** @type {KnownHooks & EnsuredHooks} */
  196. (this.hooks)[name] = new AsyncSeriesBailHook(
  197. ["request", "resolveContext"],
  198. name
  199. );
  200. return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  201. }
  202. return hook;
  203. }
  204. /**
  205. * @param {string | ResolveStepHook} name hook name or hook itself
  206. * @returns {ResolveStepHook} the hook
  207. */
  208. getHook(name) {
  209. if (typeof name !== "string") {
  210. return name;
  211. }
  212. name = toCamelCase(name);
  213. if (/^before/.test(name)) {
  214. return /** @type {ResolveStepHook} */ (
  215. this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
  216. stage: -10
  217. })
  218. );
  219. }
  220. if (/^after/.test(name)) {
  221. return /** @type {ResolveStepHook} */ (
  222. this.getHook(name[5].toLowerCase() + name.slice(6)).withOptions({
  223. stage: 10
  224. })
  225. );
  226. }
  227. /** @type {ResolveStepHook} */
  228. const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
  229. if (!hook) {
  230. throw new Error(`Hook ${name} doesn't exist`);
  231. }
  232. return hook;
  233. }
  234. /**
  235. * @param {object} context context information object
  236. * @param {string} path context path
  237. * @param {string} request request string
  238. * @returns {string | false} result
  239. */
  240. resolveSync(context, path, request) {
  241. /** @type {Error | null | undefined} */
  242. let err = undefined;
  243. /** @type {string | false | undefined} */
  244. let result = undefined;
  245. let sync = false;
  246. this.resolve(context, path, request, {}, (e, r) => {
  247. err = e;
  248. result = r;
  249. sync = true;
  250. });
  251. if (!sync) {
  252. throw new Error(
  253. "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!"
  254. );
  255. }
  256. if (err) throw err;
  257. if (result === undefined) throw new Error("No result");
  258. return result;
  259. }
  260. /**
  261. * @param {object} context context information object
  262. * @param {string} path context path
  263. * @param {string} request request string
  264. * @param {ResolveContext} resolveContext resolve context
  265. * @param {ResolveCallback} callback callback function
  266. * @returns {void}
  267. */
  268. resolve(context, path, request, resolveContext, callback) {
  269. if (!context || typeof context !== "object")
  270. return callback(new Error("context argument is not an object"));
  271. if (typeof path !== "string")
  272. return callback(new Error("path argument is not a string"));
  273. if (typeof request !== "string")
  274. return callback(new Error("request argument is not a string"));
  275. if (!resolveContext)
  276. return callback(new Error("resolveContext argument is not set"));
  277. /** @type {ResolveRequest} */
  278. const obj = {
  279. context: context,
  280. path: path,
  281. request: request
  282. };
  283. /** @type {ResolveContextYield | undefined} */
  284. let yield_;
  285. let yieldCalled = false;
  286. /** @type {ResolveContextYield | undefined} */
  287. let finishYield;
  288. if (typeof resolveContext.yield === "function") {
  289. const old = resolveContext.yield;
  290. /**
  291. * @param {ResolveRequest} obj object
  292. */
  293. yield_ = obj => {
  294. old(obj);
  295. yieldCalled = true;
  296. };
  297. /**
  298. * @param {ResolveRequest} result result
  299. * @returns {void}
  300. */
  301. finishYield = result => {
  302. if (result) {
  303. /** @type {ResolveContextYield} */ (yield_)(result);
  304. }
  305. callback(null);
  306. };
  307. }
  308. const message = `resolve '${request}' in '${path}'`;
  309. /**
  310. * @param {ResolveRequest} result result
  311. * @returns {void}
  312. */
  313. const finishResolved = result => {
  314. return callback(
  315. null,
  316. result.path === false
  317. ? false
  318. : `${result.path.replace(/#/g, "\0#")}${
  319. result.query ? result.query.replace(/#/g, "\0#") : ""
  320. }${result.fragment || ""}`,
  321. result
  322. );
  323. };
  324. /**
  325. * @param {string[]} log logs
  326. * @returns {void}
  327. */
  328. const finishWithoutResolve = log => {
  329. /**
  330. * @type {ErrorWithDetail}
  331. */
  332. const error = new Error("Can't " + message);
  333. error.details = log.join("\n");
  334. this.hooks.noResolve.call(obj, error);
  335. return callback(error);
  336. };
  337. if (resolveContext.log) {
  338. // We need log anyway to capture it in case of an error
  339. const parentLog = resolveContext.log;
  340. /** @type {string[]} */
  341. const log = [];
  342. return this.doResolve(
  343. this.hooks.resolve,
  344. obj,
  345. message,
  346. {
  347. log: msg => {
  348. parentLog(msg);
  349. log.push(msg);
  350. },
  351. yield: yield_,
  352. fileDependencies: resolveContext.fileDependencies,
  353. contextDependencies: resolveContext.contextDependencies,
  354. missingDependencies: resolveContext.missingDependencies,
  355. stack: resolveContext.stack
  356. },
  357. (err, result) => {
  358. if (err) return callback(err);
  359. if (yieldCalled || (result && yield_)) {
  360. return /** @type {ResolveContextYield} */ (finishYield)(
  361. /** @type {ResolveRequest} */ (result)
  362. );
  363. }
  364. if (result) return finishResolved(result);
  365. return finishWithoutResolve(log);
  366. }
  367. );
  368. } else {
  369. // Try to resolve assuming there is no error
  370. // We don't log stuff in this case
  371. return this.doResolve(
  372. this.hooks.resolve,
  373. obj,
  374. message,
  375. {
  376. log: undefined,
  377. yield: yield_,
  378. fileDependencies: resolveContext.fileDependencies,
  379. contextDependencies: resolveContext.contextDependencies,
  380. missingDependencies: resolveContext.missingDependencies,
  381. stack: resolveContext.stack
  382. },
  383. (err, result) => {
  384. if (err) return callback(err);
  385. if (yieldCalled || (result && yield_)) {
  386. return /** @type {ResolveContextYield} */ (finishYield)(
  387. /** @type {ResolveRequest} */ (result)
  388. );
  389. }
  390. if (result) return finishResolved(result);
  391. // log is missing for the error details
  392. // so we redo the resolving for the log info
  393. // this is more expensive to the success case
  394. // is assumed by default
  395. /** @type {string[]} */
  396. const log = [];
  397. return this.doResolve(
  398. this.hooks.resolve,
  399. obj,
  400. message,
  401. {
  402. log: msg => log.push(msg),
  403. yield: yield_,
  404. stack: resolveContext.stack
  405. },
  406. (err, result) => {
  407. if (err) return callback(err);
  408. // In a case that there is a race condition and yield will be called
  409. if (yieldCalled || (result && yield_)) {
  410. return /** @type {ResolveContextYield} */ (finishYield)(
  411. /** @type {ResolveRequest} */ (result)
  412. );
  413. }
  414. return finishWithoutResolve(log);
  415. }
  416. );
  417. }
  418. );
  419. }
  420. }
  421. /**
  422. * @param {ResolveStepHook} hook hook
  423. * @param {ResolveRequest} request request
  424. * @param {null|string} message string
  425. * @param {ResolveContext} resolveContext resolver context
  426. * @param {(err?: null|Error, result?: ResolveRequest) => void} callback callback
  427. * @returns {void}
  428. */
  429. doResolve(hook, request, message, resolveContext, callback) {
  430. const stackEntry = Resolver.createStackEntry(hook, request);
  431. /** @type {Set<string> | undefined} */
  432. let newStack;
  433. if (resolveContext.stack) {
  434. newStack = new Set(resolveContext.stack);
  435. if (resolveContext.stack.has(stackEntry)) {
  436. /**
  437. * Prevent recursion
  438. * @type {Error & {recursion?: boolean}}
  439. */
  440. const recursionError = new Error(
  441. "Recursion in resolving\nStack:\n " +
  442. Array.from(newStack).join("\n ")
  443. );
  444. recursionError.recursion = true;
  445. if (resolveContext.log)
  446. resolveContext.log("abort resolving because of recursion");
  447. return callback(recursionError);
  448. }
  449. newStack.add(stackEntry);
  450. } else {
  451. newStack = new Set([stackEntry]);
  452. }
  453. this.hooks.resolveStep.call(hook, request);
  454. if (hook.isUsed()) {
  455. const innerContext = createInnerContext(
  456. {
  457. log: resolveContext.log,
  458. yield: resolveContext.yield,
  459. fileDependencies: resolveContext.fileDependencies,
  460. contextDependencies: resolveContext.contextDependencies,
  461. missingDependencies: resolveContext.missingDependencies,
  462. stack: newStack
  463. },
  464. message
  465. );
  466. return hook.callAsync(request, innerContext, (err, result) => {
  467. if (err) return callback(err);
  468. if (result) return callback(null, result);
  469. callback();
  470. });
  471. } else {
  472. callback();
  473. }
  474. }
  475. /**
  476. * @param {string} identifier identifier
  477. * @returns {ParsedIdentifier} parsed identifier
  478. */
  479. parse(identifier) {
  480. const part = {
  481. request: "",
  482. query: "",
  483. fragment: "",
  484. module: false,
  485. directory: false,
  486. file: false,
  487. internal: false
  488. };
  489. const parsedIdentifier = parseIdentifier(identifier);
  490. if (!parsedIdentifier) return part;
  491. [part.request, part.query, part.fragment] = parsedIdentifier;
  492. if (part.request.length > 0) {
  493. part.internal = this.isPrivate(identifier);
  494. part.module = this.isModule(part.request);
  495. part.directory = this.isDirectory(part.request);
  496. if (part.directory) {
  497. part.request = part.request.slice(0, -1);
  498. }
  499. }
  500. return part;
  501. }
  502. /**
  503. * @param {string} path path
  504. * @returns {boolean} true, if the path is a module
  505. */
  506. isModule(path) {
  507. return getType(path) === PathType.Normal;
  508. }
  509. /**
  510. * @param {string} path path
  511. * @returns {boolean} true, if the path is private
  512. */
  513. isPrivate(path) {
  514. return getType(path) === PathType.Internal;
  515. }
  516. /**
  517. * @param {string} path a path
  518. * @returns {boolean} true, if the path is a directory path
  519. */
  520. isDirectory(path) {
  521. return path.endsWith("/");
  522. }
  523. /**
  524. * @param {string} path path
  525. * @param {string} request request
  526. * @returns {string} joined path
  527. */
  528. join(path, request) {
  529. return join(path, request);
  530. }
  531. /**
  532. * @param {string} path path
  533. * @returns {string} normalized path
  534. */
  535. normalize(path) {
  536. return normalize(path);
  537. }
  538. }
  539. module.exports = Resolver;