ContextModuleFactory.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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. this.resolverFactory = resolverFactory;
  93. }
  94. /**
  95. * @param {ModuleFactoryCreateData} data data object
  96. * @param {ModuleFactoryCallback} callback callback
  97. * @returns {void}
  98. */
  99. create(data, callback) {
  100. const context = data.context;
  101. const dependencies = /** @type {ContextDependency[]} */ (data.dependencies);
  102. const resolveOptions = data.resolveOptions;
  103. const dependency = dependencies[0];
  104. const fileDependencies = new LazySet();
  105. const missingDependencies = new LazySet();
  106. const contextDependencies = new LazySet();
  107. this.hooks.beforeResolve.callAsync(
  108. {
  109. context,
  110. dependencies,
  111. layer: data.contextInfo.issuerLayer,
  112. resolveOptions,
  113. fileDependencies,
  114. missingDependencies,
  115. contextDependencies,
  116. ...dependency.options
  117. },
  118. (err, beforeResolveResult) => {
  119. if (err) {
  120. return callback(err, {
  121. fileDependencies,
  122. missingDependencies,
  123. contextDependencies
  124. });
  125. }
  126. // Ignored
  127. if (!beforeResolveResult) {
  128. return callback(null, {
  129. fileDependencies,
  130. missingDependencies,
  131. contextDependencies
  132. });
  133. }
  134. const context = beforeResolveResult.context;
  135. const request = beforeResolveResult.request;
  136. const resolveOptions = beforeResolveResult.resolveOptions;
  137. /** @type {undefined | string[]} */
  138. let loaders;
  139. /** @type {undefined | string} */
  140. let resource;
  141. let loadersPrefix = "";
  142. const idx = request.lastIndexOf("!");
  143. if (idx >= 0) {
  144. let loadersRequest = request.slice(0, idx + 1);
  145. let i;
  146. for (
  147. i = 0;
  148. i < loadersRequest.length && loadersRequest[i] === "!";
  149. i++
  150. ) {
  151. loadersPrefix += "!";
  152. }
  153. loadersRequest = loadersRequest
  154. .slice(i)
  155. .replace(/!+$/, "")
  156. .replace(/!!+/g, "!");
  157. loaders = loadersRequest === "" ? [] : loadersRequest.split("!");
  158. resource = request.slice(idx + 1);
  159. } else {
  160. loaders = [];
  161. resource = request;
  162. }
  163. const contextResolver = this.resolverFactory.get(
  164. "context",
  165. dependencies.length > 0
  166. ? cachedSetProperty(
  167. resolveOptions || EMPTY_RESOLVE_OPTIONS,
  168. "dependencyType",
  169. dependencies[0].category
  170. )
  171. : resolveOptions
  172. );
  173. const loaderResolver = this.resolverFactory.get("loader");
  174. asyncLib.parallel(
  175. [
  176. (callback) => {
  177. const results = /** @type {ResolveRequest[]} */ ([]);
  178. /**
  179. * @param {ResolveRequest} obj obj
  180. * @returns {void}
  181. */
  182. const yield_ = (obj) => {
  183. results.push(obj);
  184. };
  185. contextResolver.resolve(
  186. {},
  187. context,
  188. resource,
  189. {
  190. fileDependencies,
  191. missingDependencies,
  192. contextDependencies,
  193. yield: yield_
  194. },
  195. (err) => {
  196. if (err) return callback(err);
  197. callback(null, results);
  198. }
  199. );
  200. },
  201. (callback) => {
  202. asyncLib.map(
  203. loaders,
  204. (loader, callback) => {
  205. loaderResolver.resolve(
  206. {},
  207. context,
  208. loader,
  209. {
  210. fileDependencies,
  211. missingDependencies,
  212. contextDependencies
  213. },
  214. (err, result) => {
  215. if (err) return callback(err);
  216. callback(null, result);
  217. }
  218. );
  219. },
  220. callback
  221. );
  222. }
  223. ],
  224. (err, result) => {
  225. if (err) {
  226. return callback(err, {
  227. fileDependencies,
  228. missingDependencies,
  229. contextDependencies
  230. });
  231. }
  232. let [contextResult, loaderResult] =
  233. /** @type {[ResolveRequest[], string[]]} */ (result);
  234. if (contextResult.length > 1) {
  235. const first = contextResult[0];
  236. contextResult = contextResult.filter((r) => r.path);
  237. if (contextResult.length === 0) contextResult.push(first);
  238. }
  239. this.hooks.afterResolve.callAsync(
  240. {
  241. addon:
  242. loadersPrefix +
  243. loaderResult.join("!") +
  244. (loaderResult.length > 0 ? "!" : ""),
  245. resource:
  246. contextResult.length > 1
  247. ? /** @type {string[]} */ (contextResult.map((r) => r.path))
  248. : /** @type {string} */ (contextResult[0].path),
  249. resolveDependencies: this.resolveDependencies.bind(this),
  250. resourceQuery: contextResult[0].query,
  251. resourceFragment: contextResult[0].fragment,
  252. ...beforeResolveResult
  253. },
  254. (err, result) => {
  255. if (err) {
  256. return callback(err, {
  257. fileDependencies,
  258. missingDependencies,
  259. contextDependencies
  260. });
  261. }
  262. // Ignored
  263. if (!result) {
  264. return callback(null, {
  265. fileDependencies,
  266. missingDependencies,
  267. contextDependencies
  268. });
  269. }
  270. return callback(null, {
  271. module: new ContextModule(result.resolveDependencies, result),
  272. fileDependencies,
  273. missingDependencies,
  274. contextDependencies
  275. });
  276. }
  277. );
  278. }
  279. );
  280. }
  281. );
  282. }
  283. /**
  284. * @param {InputFileSystem} fs file system
  285. * @param {ContextModuleOptions} options options
  286. * @param {ResolveDependenciesCallback} callback callback function
  287. * @returns {void}
  288. */
  289. resolveDependencies(fs, options, callback) {
  290. const cmf = this;
  291. const {
  292. resource,
  293. resourceQuery,
  294. resourceFragment,
  295. recursive,
  296. regExp,
  297. include,
  298. exclude,
  299. referencedExports,
  300. category,
  301. typePrefix,
  302. attributes
  303. } = options;
  304. if (!regExp || !resource) return callback(null, []);
  305. /**
  306. * @param {string} ctx context
  307. * @param {string} directory directory
  308. * @param {Set<string>} visited visited
  309. * @param {ResolveDependenciesCallback} callback callback
  310. */
  311. const addDirectoryChecked = (ctx, directory, visited, callback) => {
  312. /** @type {NonNullable<InputFileSystem["realpath"]>} */
  313. (fs.realpath)(directory, (err, _realPath) => {
  314. if (err) return callback(err);
  315. const realPath = /** @type {string} */ (_realPath);
  316. if (visited.has(realPath)) return callback(null, []);
  317. /** @type {Set<string> | undefined} */
  318. let recursionStack;
  319. addDirectory(
  320. ctx,
  321. directory,
  322. (_, dir, callback) => {
  323. if (recursionStack === undefined) {
  324. recursionStack = new Set(visited);
  325. recursionStack.add(realPath);
  326. }
  327. addDirectoryChecked(ctx, dir, recursionStack, callback);
  328. },
  329. callback
  330. );
  331. });
  332. };
  333. /**
  334. * @param {string} ctx context
  335. * @param {string} directory directory
  336. * @param {(context: string, subResource: string, callback: () => void) => void} addSubDirectory addSubDirectoryFn
  337. * @param {ResolveDependenciesCallback} callback callback
  338. * @returns {void}
  339. */
  340. const addDirectory = (ctx, directory, addSubDirectory, callback) => {
  341. fs.readdir(directory, (err, files) => {
  342. if (err) return callback(err);
  343. const processedFiles = cmf.hooks.contextModuleFiles.call(
  344. /** @type {string[]} */ (files).map((file) => file.normalize("NFC"))
  345. );
  346. if (!processedFiles || processedFiles.length === 0) {
  347. return callback(null, []);
  348. }
  349. asyncLib.map(
  350. processedFiles.filter((p) => p.indexOf(".") !== 0),
  351. (segment, callback) => {
  352. const subResource = join(fs, directory, segment);
  353. if (!exclude || !exclude.test(subResource)) {
  354. fs.stat(subResource, (err, _stat) => {
  355. if (err) {
  356. if (err.code === "ENOENT") {
  357. // ENOENT is ok here because the file may have been deleted between
  358. // the readdir and stat calls.
  359. return callback();
  360. }
  361. return callback(err);
  362. }
  363. const stat = /** @type {IStats} */ (_stat);
  364. if (stat.isDirectory()) {
  365. if (!recursive) return callback();
  366. addSubDirectory(ctx, subResource, callback);
  367. } else if (
  368. stat.isFile() &&
  369. (!include || include.test(subResource))
  370. ) {
  371. /** @type {{ context: string, request: string }} */
  372. const obj = {
  373. context: ctx,
  374. request: `.${subResource.slice(ctx.length).replace(/\\/g, "/")}`
  375. };
  376. this.hooks.alternativeRequests.callAsync(
  377. [obj],
  378. options,
  379. (err, alternatives) => {
  380. if (err) return callback(err);
  381. callback(
  382. null,
  383. /** @type {ContextAlternativeRequest[]} */
  384. (alternatives)
  385. .filter((obj) =>
  386. regExp.test(/** @type {string} */ (obj.request))
  387. )
  388. .map((obj) => {
  389. const dep = new ContextElementDependency(
  390. `${obj.request}${resourceQuery}${resourceFragment}`,
  391. obj.request,
  392. typePrefix,
  393. /** @type {string} */
  394. (category),
  395. referencedExports,
  396. obj.context,
  397. attributes
  398. );
  399. dep.optional = true;
  400. return dep;
  401. })
  402. );
  403. }
  404. );
  405. } else {
  406. callback();
  407. }
  408. });
  409. } else {
  410. callback();
  411. }
  412. },
  413. (err, result) => {
  414. if (err) return callback(err);
  415. if (!result) return callback(null, []);
  416. const flattenedResult = [];
  417. for (const item of result) {
  418. if (item) flattenedResult.push(...item);
  419. }
  420. callback(null, flattenedResult);
  421. }
  422. );
  423. });
  424. };
  425. /**
  426. * @param {string} ctx context
  427. * @param {string} dir dir
  428. * @param {ResolveDependenciesCallback} callback callback
  429. * @returns {void}
  430. */
  431. const addSubDirectory = (ctx, dir, callback) =>
  432. addDirectory(ctx, dir, addSubDirectory, callback);
  433. /**
  434. * @param {string} resource resource
  435. * @param {ResolveDependenciesCallback} callback callback
  436. */
  437. const visitResource = (resource, callback) => {
  438. if (typeof fs.realpath === "function") {
  439. addDirectoryChecked(resource, resource, new Set(), callback);
  440. } else {
  441. addDirectory(resource, resource, addSubDirectory, callback);
  442. }
  443. };
  444. if (typeof resource === "string") {
  445. visitResource(resource, callback);
  446. } else {
  447. asyncLib.map(resource, visitResource, (err, _result) => {
  448. if (err) return callback(err);
  449. const result = /** @type {ContextElementDependency[][]} */ (_result);
  450. // result dependencies should have unique userRequest
  451. // ordered by resolve result
  452. /** @type {Set<string>} */
  453. const temp = new Set();
  454. /** @type {ContextElementDependency[]} */
  455. const res = [];
  456. for (let i = 0; i < result.length; i++) {
  457. const inner = result[i];
  458. for (const el of inner) {
  459. if (temp.has(el.userRequest)) continue;
  460. res.push(el);
  461. temp.add(el.userRequest);
  462. }
  463. }
  464. callback(null, res);
  465. });
  466. }
  467. }
  468. }
  469. module.exports = ContextModuleFactory;