ContextModuleFactory.js 12 KB

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