ContextModuleFactory.js 14 KB

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