Resolver.js 14 KB

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