ContextModuleFactory.js 12 KB

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