ContextModuleFactory.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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. } = options;
  279. if (!regExp || !resource) return callback(null, []);
  280. const addDirectoryChecked = (ctx, directory, visited, callback) => {
  281. fs.realpath(directory, (err, realPath) => {
  282. if (err) return callback(err);
  283. if (visited.has(realPath)) return callback(null, []);
  284. let recursionStack;
  285. addDirectory(
  286. ctx,
  287. directory,
  288. (_, dir, callback) => {
  289. if (recursionStack === undefined) {
  290. recursionStack = new Set(visited);
  291. recursionStack.add(realPath);
  292. }
  293. addDirectoryChecked(ctx, dir, recursionStack, callback);
  294. },
  295. callback
  296. );
  297. });
  298. };
  299. const addDirectory = (ctx, directory, addSubDirectory, callback) => {
  300. fs.readdir(directory, (err, files) => {
  301. if (err) return callback(err);
  302. const processedFiles = cmf.hooks.contextModuleFiles.call(
  303. /** @type {string[]} */ (files).map(file => file.normalize("NFC"))
  304. );
  305. if (!processedFiles || processedFiles.length === 0)
  306. return callback(null, []);
  307. asyncLib.map(
  308. processedFiles.filter(p => p.indexOf(".") !== 0),
  309. (segment, callback) => {
  310. const subResource = join(fs, directory, segment);
  311. if (!exclude || !subResource.match(exclude)) {
  312. fs.stat(subResource, (err, stat) => {
  313. if (err) {
  314. if (err.code === "ENOENT") {
  315. // ENOENT is ok here because the file may have been deleted between
  316. // the readdir and stat calls.
  317. return callback();
  318. } else {
  319. return callback(err);
  320. }
  321. }
  322. if (stat.isDirectory()) {
  323. if (!recursive) return callback();
  324. addSubDirectory(ctx, subResource, callback);
  325. } else if (
  326. stat.isFile() &&
  327. (!include || subResource.match(include))
  328. ) {
  329. const obj = {
  330. context: ctx,
  331. request:
  332. "." + subResource.slice(ctx.length).replace(/\\/g, "/")
  333. };
  334. this.hooks.alternativeRequests.callAsync(
  335. [obj],
  336. options,
  337. (err, alternatives) => {
  338. if (err) return callback(err);
  339. alternatives = alternatives
  340. .filter(obj => regExp.test(obj.request))
  341. .map(obj => {
  342. const dep = new ContextElementDependency(
  343. `${obj.request}${resourceQuery}${resourceFragment}`,
  344. obj.request,
  345. typePrefix,
  346. category,
  347. referencedExports,
  348. obj.context
  349. );
  350. dep.optional = true;
  351. return dep;
  352. });
  353. callback(null, alternatives);
  354. }
  355. );
  356. } else {
  357. callback();
  358. }
  359. });
  360. } else {
  361. callback();
  362. }
  363. },
  364. (err, result) => {
  365. if (err) return callback(err);
  366. if (!result) return callback(null, []);
  367. const flattenedResult = [];
  368. for (const item of result) {
  369. if (item) flattenedResult.push(...item);
  370. }
  371. callback(null, flattenedResult);
  372. }
  373. );
  374. });
  375. };
  376. const addSubDirectory = (ctx, dir, callback) =>
  377. addDirectory(ctx, dir, addSubDirectory, callback);
  378. const visitResource = (resource, callback) => {
  379. if (typeof fs.realpath === "function") {
  380. addDirectoryChecked(resource, resource, new Set(), callback);
  381. } else {
  382. addDirectory(resource, resource, addSubDirectory, callback);
  383. }
  384. };
  385. if (typeof resource === "string") {
  386. visitResource(resource, callback);
  387. } else {
  388. asyncLib.map(resource, visitResource, (err, result) => {
  389. if (err) return callback(err);
  390. // result dependencies should have unique userRequest
  391. // ordered by resolve result
  392. const temp = new Set();
  393. const res = [];
  394. for (let i = 0; i < result.length; i++) {
  395. const inner = result[i];
  396. for (const el of inner) {
  397. if (temp.has(el.userRequest)) continue;
  398. res.push(el);
  399. temp.add(el.userRequest);
  400. }
  401. }
  402. callback(null, res);
  403. });
  404. }
  405. }
  406. };