ContextModuleFactory.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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.substr(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. .substr(i)
  133. .replace(/!+$/, "")
  134. .replace(/!!+/g, "!");
  135. if (loadersRequest === "") {
  136. loaders = [];
  137. } else {
  138. loaders = loadersRequest.split("!");
  139. }
  140. resource = request.substr(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. contextResolver.resolve(
  160. {},
  161. context,
  162. resource,
  163. {
  164. fileDependencies,
  165. missingDependencies,
  166. contextDependencies
  167. },
  168. (err, result) => {
  169. if (err) return callback(err);
  170. callback(null, result);
  171. }
  172. );
  173. },
  174. callback => {
  175. asyncLib.map(
  176. loaders,
  177. (loader, callback) => {
  178. loaderResolver.resolve(
  179. {},
  180. context,
  181. loader,
  182. {
  183. fileDependencies,
  184. missingDependencies,
  185. contextDependencies
  186. },
  187. (err, result) => {
  188. if (err) return callback(err);
  189. callback(null, result);
  190. }
  191. );
  192. },
  193. callback
  194. );
  195. }
  196. ],
  197. (err, result) => {
  198. if (err) {
  199. return callback(err, {
  200. fileDependencies,
  201. missingDependencies,
  202. contextDependencies
  203. });
  204. }
  205. this.hooks.afterResolve.callAsync(
  206. {
  207. addon:
  208. loadersPrefix +
  209. result[1].join("!") +
  210. (result[1].length > 0 ? "!" : ""),
  211. resource: result[0],
  212. resolveDependencies: this.resolveDependencies.bind(this),
  213. ...beforeResolveResult
  214. },
  215. (err, result) => {
  216. if (err) {
  217. return callback(err, {
  218. fileDependencies,
  219. missingDependencies,
  220. contextDependencies
  221. });
  222. }
  223. // Ignored
  224. if (!result) {
  225. return callback(null, {
  226. fileDependencies,
  227. missingDependencies,
  228. contextDependencies
  229. });
  230. }
  231. return callback(null, {
  232. module: new ContextModule(result.resolveDependencies, result),
  233. fileDependencies,
  234. missingDependencies,
  235. contextDependencies
  236. });
  237. }
  238. );
  239. }
  240. );
  241. }
  242. );
  243. }
  244. /**
  245. * @param {InputFileSystem} fs file system
  246. * @param {ContextModuleOptions} options options
  247. * @param {ResolveDependenciesCallback} callback callback function
  248. * @returns {void}
  249. */
  250. resolveDependencies(fs, options, callback) {
  251. const cmf = this;
  252. const {
  253. resource,
  254. resourceQuery,
  255. resourceFragment,
  256. recursive,
  257. regExp,
  258. include,
  259. exclude,
  260. referencedExports,
  261. category,
  262. typePrefix
  263. } = options;
  264. if (!regExp || !resource) return callback(null, []);
  265. const addDirectoryChecked = (directory, visited, callback) => {
  266. fs.realpath(directory, (err, realPath) => {
  267. if (err) return callback(err);
  268. if (visited.has(realPath)) return callback(null, []);
  269. let recursionStack;
  270. addDirectory(
  271. directory,
  272. (dir, callback) => {
  273. if (recursionStack === undefined) {
  274. recursionStack = new Set(visited);
  275. recursionStack.add(realPath);
  276. }
  277. addDirectoryChecked(dir, recursionStack, callback);
  278. },
  279. callback
  280. );
  281. });
  282. };
  283. const addDirectory = (directory, addSubDirectory, callback) => {
  284. fs.readdir(directory, (err, files) => {
  285. if (err) return callback(err);
  286. const processedFiles = cmf.hooks.contextModuleFiles.call(
  287. /** @type {string[]} */ (files).map(file => file.normalize("NFC"))
  288. );
  289. if (!processedFiles || processedFiles.length === 0)
  290. return callback(null, []);
  291. asyncLib.map(
  292. processedFiles.filter(p => p.indexOf(".") !== 0),
  293. (segment, callback) => {
  294. const subResource = join(fs, directory, segment);
  295. if (!exclude || !subResource.match(exclude)) {
  296. fs.stat(subResource, (err, stat) => {
  297. if (err) {
  298. if (err.code === "ENOENT") {
  299. // ENOENT is ok here because the file may have been deleted between
  300. // the readdir and stat calls.
  301. return callback();
  302. } else {
  303. return callback(err);
  304. }
  305. }
  306. if (stat.isDirectory()) {
  307. if (!recursive) return callback();
  308. addSubDirectory(subResource, callback);
  309. } else if (
  310. stat.isFile() &&
  311. (!include || subResource.match(include))
  312. ) {
  313. const obj = {
  314. context: resource,
  315. request:
  316. "." +
  317. subResource.substr(resource.length).replace(/\\/g, "/")
  318. };
  319. this.hooks.alternativeRequests.callAsync(
  320. [obj],
  321. options,
  322. (err, alternatives) => {
  323. if (err) return callback(err);
  324. alternatives = alternatives
  325. .filter(obj => regExp.test(obj.request))
  326. .map(obj => {
  327. const dep = new ContextElementDependency(
  328. obj.request + resourceQuery + resourceFragment,
  329. obj.request,
  330. typePrefix,
  331. category,
  332. referencedExports
  333. );
  334. dep.optional = true;
  335. return dep;
  336. });
  337. callback(null, alternatives);
  338. }
  339. );
  340. } else {
  341. callback();
  342. }
  343. });
  344. } else {
  345. callback();
  346. }
  347. },
  348. (err, result) => {
  349. if (err) return callback(err);
  350. if (!result) return callback(null, []);
  351. const flattenedResult = [];
  352. for (const item of result) {
  353. if (item) flattenedResult.push(...item);
  354. }
  355. callback(null, flattenedResult);
  356. }
  357. );
  358. });
  359. };
  360. if (typeof fs.realpath === "function") {
  361. addDirectoryChecked(resource, new Set(), callback);
  362. } else {
  363. const addSubDirectory = (dir, callback) =>
  364. addDirectory(dir, addSubDirectory, callback);
  365. addDirectory(resource, addSubDirectory, callback);
  366. }
  367. }
  368. };