ContextModuleFactory.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const asyncLib = require("neo-async");
  7. const { AsyncSeriesWaterfallHook, SyncWaterfallHook } = require("tapable");
  8. const ContextModule = require("./ContextModule");
  9. const ModuleFactory = require("./ModuleFactory");
  10. const ContextElementDependency = require("./dependencies/ContextElementDependency");
  11. const LazySet = require("./util/LazySet");
  12. const { cachedSetProperty } = require("./util/cleverMerge");
  13. const { createFakeHook } = require("./util/deprecation");
  14. const { join } = require("./util/fs");
  15. /** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */
  16. /** @typedef {import("./Compilation").FileSystemDependencies} FileSystemDependencies */
  17. /** @typedef {import("./ContextModule").ContextModuleOptions} ContextModuleOptions */
  18. /** @typedef {import("./ContextModule").ResolveDependenciesCallback} ResolveDependenciesCallback */
  19. /** @typedef {import("./ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
  20. /** @typedef {import("./ModuleFactory").ModuleFactoryCallback} ModuleFactoryCallback */
  21. /** @typedef {import("./ResolverFactory")} ResolverFactory */
  22. /** @typedef {import("./dependencies/ContextDependency")} ContextDependency */
  23. /** @typedef {import("./dependencies/ContextDependency").ContextOptions} ContextOptions */
  24. /**
  25. * @template T
  26. * @typedef {import("./util/deprecation").FakeHook<T>} FakeHook<T>
  27. */
  28. /** @typedef {import("./util/fs").IStats} IStats */
  29. /** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
  30. /** @typedef {{ context: string, request: string }} ContextAlternativeRequest */
  31. /**
  32. * @typedef {object} ContextResolveData
  33. * @property {string} context
  34. * @property {string} request
  35. * @property {ModuleFactoryCreateData["resolveOptions"]} resolveOptions
  36. * @property {FileSystemDependencies} fileDependencies
  37. * @property {FileSystemDependencies} missingDependencies
  38. * @property {FileSystemDependencies} contextDependencies
  39. * @property {ContextDependency[]} dependencies
  40. */
  41. /** @typedef {ContextResolveData & ContextOptions} BeforeContextResolveData */
  42. /** @typedef {BeforeContextResolveData & { resource: string | string[], resourceQuery: string | undefined, resourceFragment: string | undefined, resolveDependencies: ContextModuleFactory["resolveDependencies"] }} AfterContextResolveData */
  43. const EMPTY_RESOLVE_OPTIONS = {};
  44. class ContextModuleFactory extends ModuleFactory {
  45. /**
  46. * @param {ResolverFactory} resolverFactory resolverFactory
  47. */
  48. constructor(resolverFactory) {
  49. super();
  50. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[], ContextModuleOptions]>} */
  51. const alternativeRequests = new AsyncSeriesWaterfallHook([
  52. "modules",
  53. "options"
  54. ]);
  55. this.hooks = Object.freeze({
  56. /** @type {AsyncSeriesWaterfallHook<[BeforeContextResolveData], BeforeContextResolveData | false | void>} */
  57. beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
  58. /** @type {AsyncSeriesWaterfallHook<[AfterContextResolveData], AfterContextResolveData | false | void>} */
  59. afterResolve: new AsyncSeriesWaterfallHook(["data"]),
  60. /** @type {SyncWaterfallHook<[string[]]>} */
  61. contextModuleFiles: new SyncWaterfallHook(["files"]),
  62. /** @type {FakeHook<Pick<AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>, "tap" | "tapAsync" | "tapPromise" | "name">>} */
  63. alternatives: createFakeHook(
  64. {
  65. name: "alternatives",
  66. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["intercept"]} */
  67. intercept: (interceptor) => {
  68. throw new Error(
  69. "Intercepting fake hook ContextModuleFactory.hooks.alternatives is not possible, use ContextModuleFactory.hooks.alternativeRequests instead"
  70. );
  71. },
  72. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tap"]} */
  73. tap: (options, fn) => {
  74. alternativeRequests.tap(options, fn);
  75. },
  76. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tapAsync"]} */
  77. tapAsync: (options, fn) => {
  78. alternativeRequests.tapAsync(options, (items, _options, callback) =>
  79. fn(items, callback)
  80. );
  81. },
  82. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tapPromise"]} */
  83. tapPromise: (options, fn) => {
  84. alternativeRequests.tapPromise(options, fn);
  85. }
  86. },
  87. "ContextModuleFactory.hooks.alternatives has deprecated in favor of ContextModuleFactory.hooks.alternativeRequests with an additional options argument.",
  88. "DEP_WEBPACK_CONTEXT_MODULE_FACTORY_ALTERNATIVES"
  89. ),
  90. alternativeRequests
  91. });
  92. /** @type {ResolverFactory} */
  93. this.resolverFactory = resolverFactory;
  94. }
  95. /**
  96. * @param {ModuleFactoryCreateData} data data object
  97. * @param {ModuleFactoryCallback} callback callback
  98. * @returns {void}
  99. */
  100. create(data, callback) {
  101. const context = data.context;
  102. const dependencies = /** @type {ContextDependency[]} */ (data.dependencies);
  103. const resolveOptions = data.resolveOptions;
  104. const dependency = dependencies[0];
  105. /** @type {FileSystemDependencies} */
  106. const fileDependencies = new LazySet();
  107. /** @type {FileSystemDependencies} */
  108. const missingDependencies = new LazySet();
  109. /** @type {FileSystemDependencies} */
  110. const contextDependencies = new LazySet();
  111. this.hooks.beforeResolve.callAsync(
  112. {
  113. context,
  114. dependencies,
  115. layer: data.contextInfo.issuerLayer,
  116. resolveOptions,
  117. fileDependencies,
  118. missingDependencies,
  119. contextDependencies,
  120. ...dependency.options
  121. },
  122. (err, beforeResolveResult) => {
  123. if (err) {
  124. return callback(err, {
  125. fileDependencies,
  126. missingDependencies,
  127. contextDependencies
  128. });
  129. }
  130. // Ignored
  131. if (!beforeResolveResult) {
  132. return callback(null, {
  133. fileDependencies,
  134. missingDependencies,
  135. contextDependencies
  136. });
  137. }
  138. const context = beforeResolveResult.context;
  139. const request = beforeResolveResult.request;
  140. const resolveOptions = beforeResolveResult.resolveOptions;
  141. /** @type {undefined | string[]} */
  142. let loaders;
  143. /** @type {undefined | string} */
  144. let resource;
  145. let loadersPrefix = "";
  146. const idx = request.lastIndexOf("!");
  147. if (idx >= 0) {
  148. let loadersRequest = request.slice(0, idx + 1);
  149. /** @type {number} */
  150. let i;
  151. for (
  152. i = 0;
  153. i < loadersRequest.length && loadersRequest[i] === "!";
  154. i++
  155. ) {
  156. loadersPrefix += "!";
  157. }
  158. loadersRequest = loadersRequest
  159. .slice(i)
  160. .replace(/!+$/, "")
  161. .replace(/!{2,}/g, "!");
  162. loaders = loadersRequest === "" ? [] : loadersRequest.split("!");
  163. resource = request.slice(idx + 1);
  164. } else {
  165. loaders = [];
  166. resource = request;
  167. }
  168. const contextResolver = this.resolverFactory.get(
  169. "context",
  170. dependencies.length > 0
  171. ? cachedSetProperty(
  172. resolveOptions || EMPTY_RESOLVE_OPTIONS,
  173. "dependencyType",
  174. dependencies[0].category
  175. )
  176. : resolveOptions
  177. );
  178. const loaderResolver = this.resolverFactory.get("loader");
  179. asyncLib.parallel(
  180. [
  181. (callback) => {
  182. const results = /** @type {ResolveRequest[]} */ ([]);
  183. /**
  184. * @param {ResolveRequest} obj obj
  185. * @returns {void}
  186. */
  187. const yield_ = (obj) => {
  188. results.push(obj);
  189. };
  190. contextResolver.resolve(
  191. {},
  192. context,
  193. resource,
  194. {
  195. fileDependencies,
  196. missingDependencies,
  197. contextDependencies,
  198. yield: yield_
  199. },
  200. (err) => {
  201. if (err) return callback(err);
  202. callback(null, results);
  203. }
  204. );
  205. },
  206. (callback) => {
  207. asyncLib.map(
  208. loaders,
  209. (loader, callback) => {
  210. loaderResolver.resolve(
  211. {},
  212. context,
  213. loader,
  214. {
  215. fileDependencies,
  216. missingDependencies,
  217. contextDependencies
  218. },
  219. (err, result) => {
  220. if (err) return callback(err);
  221. callback(null, result);
  222. }
  223. );
  224. },
  225. callback
  226. );
  227. }
  228. ],
  229. (err, result) => {
  230. if (err) {
  231. return callback(err, {
  232. fileDependencies,
  233. missingDependencies,
  234. contextDependencies
  235. });
  236. }
  237. let [contextResult, loaderResult] =
  238. /** @type {[ResolveRequest[], string[]]} */ (result);
  239. if (contextResult.length > 1) {
  240. const first = contextResult[0];
  241. contextResult = contextResult.filter((r) => r.path);
  242. if (contextResult.length === 0) contextResult.push(first);
  243. }
  244. this.hooks.afterResolve.callAsync(
  245. {
  246. addon:
  247. loadersPrefix +
  248. loaderResult.join("!") +
  249. (loaderResult.length > 0 ? "!" : ""),
  250. resource:
  251. contextResult.length > 1
  252. ? /** @type {string[]} */ (contextResult.map((r) => r.path))
  253. : /** @type {string} */ (contextResult[0].path),
  254. resolveDependencies: this.resolveDependencies.bind(this),
  255. resourceQuery: contextResult[0].query,
  256. resourceFragment: contextResult[0].fragment,
  257. ...beforeResolveResult
  258. },
  259. (err, result) => {
  260. if (err) {
  261. return callback(err, {
  262. fileDependencies,
  263. missingDependencies,
  264. contextDependencies
  265. });
  266. }
  267. // Ignored
  268. if (!result) {
  269. return callback(null, {
  270. fileDependencies,
  271. missingDependencies,
  272. contextDependencies
  273. });
  274. }
  275. return callback(null, {
  276. module: new ContextModule(result.resolveDependencies, result),
  277. fileDependencies,
  278. missingDependencies,
  279. contextDependencies
  280. });
  281. }
  282. );
  283. }
  284. );
  285. }
  286. );
  287. }
  288. /**
  289. * @param {InputFileSystem} fs file system
  290. * @param {ContextModuleOptions} options options
  291. * @param {ResolveDependenciesCallback} callback callback function
  292. * @returns {void}
  293. */
  294. resolveDependencies(fs, options, callback) {
  295. const cmf = this;
  296. const {
  297. resource,
  298. resourceQuery,
  299. resourceFragment,
  300. recursive,
  301. regExp,
  302. include,
  303. exclude,
  304. referencedExports,
  305. category,
  306. typePrefix,
  307. attributes
  308. } = options;
  309. if (!regExp || !resource) return callback(null, []);
  310. /**
  311. * @param {string} ctx context
  312. * @param {string} directory directory
  313. * @param {Set<string>} visited visited
  314. * @param {ResolveDependenciesCallback} callback callback
  315. */
  316. const addDirectoryChecked = (ctx, directory, visited, callback) => {
  317. /** @type {NonNullable<InputFileSystem["realpath"]>} */
  318. (fs.realpath)(directory, (err, _realPath) => {
  319. if (err) return callback(err);
  320. const realPath = /** @type {string} */ (_realPath);
  321. if (visited.has(realPath)) return callback(null, []);
  322. /** @type {Set<string> | undefined} */
  323. let recursionStack;
  324. addDirectory(
  325. ctx,
  326. directory,
  327. (_, dir, callback) => {
  328. if (recursionStack === undefined) {
  329. recursionStack = new Set(visited);
  330. recursionStack.add(realPath);
  331. }
  332. addDirectoryChecked(ctx, dir, recursionStack, callback);
  333. },
  334. callback
  335. );
  336. });
  337. };
  338. /**
  339. * @param {string} ctx context
  340. * @param {string} directory directory
  341. * @param {(context: string, subResource: string, callback: () => void) => void} addSubDirectory addSubDirectoryFn
  342. * @param {ResolveDependenciesCallback} callback callback
  343. * @returns {void}
  344. */
  345. const addDirectory = (ctx, directory, addSubDirectory, callback) => {
  346. fs.readdir(directory, (err, files) => {
  347. if (err) return callback(err);
  348. const processedFiles = cmf.hooks.contextModuleFiles.call(
  349. /** @type {string[]} */ (files).map((file) => file.normalize("NFC"))
  350. );
  351. if (!processedFiles || processedFiles.length === 0) {
  352. return callback(null, []);
  353. }
  354. asyncLib.map(
  355. processedFiles.filter((p) => p.indexOf(".") !== 0),
  356. (segment, callback) => {
  357. const subResource = join(fs, directory, segment);
  358. if (!exclude || !exclude.test(subResource)) {
  359. fs.stat(subResource, (err, _stat) => {
  360. if (err) {
  361. if (err.code === "ENOENT") {
  362. // ENOENT is ok here because the file may have been deleted between
  363. // the readdir and stat calls.
  364. return callback();
  365. }
  366. return callback(err);
  367. }
  368. const stat = /** @type {IStats} */ (_stat);
  369. if (stat.isDirectory()) {
  370. if (!recursive) return callback();
  371. addSubDirectory(ctx, subResource, callback);
  372. } else if (
  373. stat.isFile() &&
  374. (!include || include.test(subResource))
  375. ) {
  376. /** @type {{ context: string, request: string }} */
  377. const obj = {
  378. context: ctx,
  379. request: `.${subResource.slice(ctx.length).replace(/\\/g, "/")}`
  380. };
  381. this.hooks.alternativeRequests.callAsync(
  382. [obj],
  383. options,
  384. (err, alternatives) => {
  385. if (err) return callback(err);
  386. callback(
  387. null,
  388. /** @type {ContextAlternativeRequest[]} */
  389. (alternatives)
  390. .filter((obj) =>
  391. regExp.test(/** @type {string} */ (obj.request))
  392. )
  393. .map((obj) => {
  394. const dep = new ContextElementDependency(
  395. `${obj.request}${resourceQuery}${resourceFragment}`,
  396. obj.request,
  397. typePrefix,
  398. /** @type {string} */
  399. (category),
  400. referencedExports,
  401. obj.context,
  402. attributes
  403. );
  404. dep.optional = true;
  405. return dep;
  406. })
  407. );
  408. }
  409. );
  410. } else {
  411. callback();
  412. }
  413. });
  414. } else {
  415. callback();
  416. }
  417. },
  418. (err, result) => {
  419. if (err) return callback(err);
  420. if (!result) return callback(null, []);
  421. /** @type {ContextElementDependency[]} */
  422. const flattenedResult = [];
  423. for (const item of result) {
  424. if (item) flattenedResult.push(...item);
  425. }
  426. callback(null, flattenedResult);
  427. }
  428. );
  429. });
  430. };
  431. /**
  432. * @param {string} ctx context
  433. * @param {string} dir dir
  434. * @param {ResolveDependenciesCallback} callback callback
  435. * @returns {void}
  436. */
  437. const addSubDirectory = (ctx, dir, callback) =>
  438. addDirectory(ctx, dir, addSubDirectory, callback);
  439. /**
  440. * @param {string} resource resource
  441. * @param {ResolveDependenciesCallback} callback callback
  442. */
  443. const visitResource = (resource, callback) => {
  444. if (typeof fs.realpath === "function") {
  445. addDirectoryChecked(
  446. resource,
  447. resource,
  448. /** @type {Set<string>} */
  449. new Set(),
  450. callback
  451. );
  452. } else {
  453. addDirectory(resource, resource, addSubDirectory, callback);
  454. }
  455. };
  456. if (typeof resource === "string") {
  457. visitResource(resource, callback);
  458. } else {
  459. asyncLib.map(resource, visitResource, (err, _result) => {
  460. if (err) return callback(err);
  461. const result = /** @type {ContextElementDependency[][]} */ (_result);
  462. // result dependencies should have unique userRequest
  463. // ordered by resolve result
  464. /** @type {Set<string>} */
  465. const temp = new Set();
  466. /** @type {ContextElementDependency[]} */
  467. const res = [];
  468. for (let i = 0; i < result.length; i++) {
  469. const inner = result[i];
  470. for (const el of inner) {
  471. if (temp.has(el.userRequest)) continue;
  472. res.push(el);
  473. temp.add(el.userRequest);
  474. }
  475. }
  476. callback(null, res);
  477. });
  478. }
  479. }
  480. }
  481. module.exports = ContextModuleFactory;