Resolver.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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("request 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. let finishYield;
  254. if (typeof resolveContext.yield === "function") {
  255. const old = resolveContext.yield;
  256. yield_ = obj => {
  257. old(obj);
  258. yieldCalled = true;
  259. };
  260. finishYield = result => {
  261. if (result) yield_(result);
  262. callback(null);
  263. };
  264. }
  265. const message = `resolve '${request}' in '${path}'`;
  266. const finishResolved = result => {
  267. return callback(
  268. null,
  269. result.path === false
  270. ? false
  271. : `${result.path.replace(/#/g, "\0#")}${
  272. result.query ? result.query.replace(/#/g, "\0#") : ""
  273. }${result.fragment || ""}`,
  274. result
  275. );
  276. };
  277. const finishWithoutResolve = log => {
  278. /**
  279. * @type {Error & {details?: string}}
  280. */
  281. const error = new Error("Can't " + message);
  282. error.details = log.join("\n");
  283. this.hooks.noResolve.call(obj, error);
  284. return callback(error);
  285. };
  286. if (resolveContext.log) {
  287. // We need log anyway to capture it in case of an error
  288. const parentLog = resolveContext.log;
  289. const log = [];
  290. return this.doResolve(
  291. this.hooks.resolve,
  292. obj,
  293. message,
  294. {
  295. log: msg => {
  296. parentLog(msg);
  297. log.push(msg);
  298. },
  299. yield: yield_,
  300. fileDependencies: resolveContext.fileDependencies,
  301. contextDependencies: resolveContext.contextDependencies,
  302. missingDependencies: resolveContext.missingDependencies,
  303. stack: resolveContext.stack
  304. },
  305. (err, result) => {
  306. if (err) return callback(err);
  307. if (yieldCalled || (result && yield_)) return finishYield(result);
  308. if (result) return finishResolved(result);
  309. return finishWithoutResolve(log);
  310. }
  311. );
  312. } else {
  313. // Try to resolve assuming there is no error
  314. // We don't log stuff in this case
  315. return this.doResolve(
  316. this.hooks.resolve,
  317. obj,
  318. message,
  319. {
  320. log: undefined,
  321. yield: yield_,
  322. fileDependencies: resolveContext.fileDependencies,
  323. contextDependencies: resolveContext.contextDependencies,
  324. missingDependencies: resolveContext.missingDependencies,
  325. stack: resolveContext.stack
  326. },
  327. (err, result) => {
  328. if (err) return callback(err);
  329. if (yieldCalled || (result && yield_)) return finishYield(result);
  330. if (result) return finishResolved(result);
  331. // log is missing for the error details
  332. // so we redo the resolving for the log info
  333. // this is more expensive to the success case
  334. // is assumed by default
  335. const log = [];
  336. return this.doResolve(
  337. this.hooks.resolve,
  338. obj,
  339. message,
  340. {
  341. log: msg => log.push(msg),
  342. yield: yield_,
  343. stack: resolveContext.stack
  344. },
  345. (err, result) => {
  346. if (err) return callback(err);
  347. // In a case that there is a race condition and yield will be called
  348. if (yieldCalled || (result && yield_)) return finishYield(result);
  349. return finishWithoutResolve(log);
  350. }
  351. );
  352. }
  353. );
  354. }
  355. }
  356. doResolve(hook, request, message, resolveContext, callback) {
  357. const stackEntry = Resolver.createStackEntry(hook, request);
  358. let newStack;
  359. if (resolveContext.stack) {
  360. newStack = new Set(resolveContext.stack);
  361. if (resolveContext.stack.has(stackEntry)) {
  362. /**
  363. * Prevent recursion
  364. * @type {Error & {recursion?: boolean}}
  365. */
  366. const recursionError = new Error(
  367. "Recursion in resolving\nStack:\n " +
  368. Array.from(newStack).join("\n ")
  369. );
  370. recursionError.recursion = true;
  371. if (resolveContext.log)
  372. resolveContext.log("abort resolving because of recursion");
  373. return callback(recursionError);
  374. }
  375. newStack.add(stackEntry);
  376. } else {
  377. newStack = new Set([stackEntry]);
  378. }
  379. this.hooks.resolveStep.call(hook, request);
  380. if (hook.isUsed()) {
  381. const innerContext = createInnerContext(
  382. {
  383. log: resolveContext.log,
  384. yield: resolveContext.yield,
  385. fileDependencies: resolveContext.fileDependencies,
  386. contextDependencies: resolveContext.contextDependencies,
  387. missingDependencies: resolveContext.missingDependencies,
  388. stack: newStack
  389. },
  390. message
  391. );
  392. return hook.callAsync(request, innerContext, (err, result) => {
  393. if (err) return callback(err);
  394. if (result) return callback(null, result);
  395. callback();
  396. });
  397. } else {
  398. callback();
  399. }
  400. }
  401. /**
  402. * @param {string} identifier identifier
  403. * @returns {ParsedIdentifier} parsed identifier
  404. */
  405. parse(identifier) {
  406. const part = {
  407. request: "",
  408. query: "",
  409. fragment: "",
  410. module: false,
  411. directory: false,
  412. file: false,
  413. internal: false
  414. };
  415. const parsedIdentifier = parseIdentifier(identifier);
  416. if (!parsedIdentifier) return part;
  417. [part.request, part.query, part.fragment] = parsedIdentifier;
  418. if (part.request.length > 0) {
  419. part.internal = this.isPrivate(identifier);
  420. part.module = this.isModule(part.request);
  421. part.directory = this.isDirectory(part.request);
  422. if (part.directory) {
  423. part.request = part.request.substr(0, part.request.length - 1);
  424. }
  425. }
  426. return part;
  427. }
  428. isModule(path) {
  429. return getType(path) === PathType.Normal;
  430. }
  431. isPrivate(path) {
  432. return getType(path) === PathType.Internal;
  433. }
  434. /**
  435. * @param {string} path a path
  436. * @returns {boolean} true, if the path is a directory path
  437. */
  438. isDirectory(path) {
  439. return path.endsWith("/");
  440. }
  441. join(path, request) {
  442. return join(path, request);
  443. }
  444. normalize(path) {
  445. return normalize(path);
  446. }
  447. }
  448. module.exports = Resolver;