Resolver.js 13 KB

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