ContextModuleFactory.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  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. const {
  16. globPatternBaseReachesDir,
  17. globUserRequest,
  18. isNonExhaustiveImportMetaGlobSkippedDir,
  19. resolveContextModuleGlobPattern
  20. } = require("./util/globUtils");
  21. /** @import { ResolvedContextModuleGlobPattern } from "./util/globUtils" */
  22. /** @typedef {(context: string, subResource: string, callback: () => void, resolvedGlobPatterns?: ResolvedContextModuleGlobPattern[]) => void} AddSubDirectoryFn */
  23. /** @import { ResolveRequest } from "enhanced-resolve" */
  24. /** @import { FileSystemDependencies } from "./Compilation" */
  25. /**
  26. * @import {
  27. * ContextModuleOptions,
  28. * ResolveDependenciesCallback,
  29. * ContextOptions
  30. * } from "./ContextModule"
  31. */
  32. /**
  33. * @import {
  34. * ModuleFactoryCreateData,
  35. * ModuleFactoryCallback
  36. * } from "./ModuleFactory"
  37. */
  38. /** @import ResolverFactory from "./ResolverFactory" */
  39. /** @import ContextDependency from "./dependencies/ContextDependency" */
  40. /**
  41. * Defines the shared type used by this module.
  42. * @template T
  43. * @typedef {import("./util/deprecation").FakeHook<T>} FakeHook<T>
  44. */
  45. /** @import { IStats, InputFileSystem } from "./util/fs" */
  46. /** @typedef {{ context: string, request: string }} ContextAlternativeRequest */
  47. /**
  48. * Defines the context resolve data type used by this module.
  49. * @typedef {object} ContextResolveData
  50. * @property {string} context
  51. * @property {string} request
  52. * @property {ModuleFactoryCreateData["resolveOptions"]} resolveOptions
  53. * @property {FileSystemDependencies} fileDependencies
  54. * @property {FileSystemDependencies} missingDependencies
  55. * @property {FileSystemDependencies} contextDependencies
  56. * @property {ContextDependency[]} dependencies
  57. */
  58. /** @typedef {ContextResolveData & ContextOptions} BeforeContextResolveData */
  59. /** @typedef {BeforeContextResolveData & { resource: string | string[], resourceQuery: string | undefined, resourceFragment: string | undefined, resolveDependencies: ContextModuleFactory["resolveDependencies"] }} AfterContextResolveData */
  60. const EMPTY_RESOLVE_OPTIONS = {};
  61. /**
  62. * Strips the query and fragment the elements of the context carry themselves, so
  63. * the remainder can be joined with an element request again.
  64. * @param {string | undefined} request the request of the context as written by the user
  65. * @param {string | undefined} resourceQuery query of the resolved context
  66. * @param {string | undefined} resourceFragment fragment of the resolved context
  67. * @returns {string | undefined} the request without query and fragment
  68. */
  69. const getContextRequest = (request, resourceQuery, resourceFragment) => {
  70. if (request === undefined) return undefined;
  71. const suffix = (resourceQuery || "") + (resourceFragment || "");
  72. return suffix && request.endsWith(suffix)
  73. ? request.slice(0, -suffix.length)
  74. : request;
  75. };
  76. class ContextModuleFactory extends ModuleFactory {
  77. /**
  78. * Creates an instance of ContextModuleFactory.
  79. * @param {ResolverFactory} resolverFactory resolverFactory
  80. */
  81. constructor(resolverFactory) {
  82. super();
  83. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[], ContextModuleOptions]>} */
  84. const alternativeRequests = new AsyncSeriesWaterfallHook([
  85. "modules",
  86. "options"
  87. ]);
  88. this.hooks = Object.freeze({
  89. /** @type {AsyncSeriesWaterfallHook<[BeforeContextResolveData], BeforeContextResolveData | false | void>} */
  90. beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
  91. /** @type {AsyncSeriesWaterfallHook<[AfterContextResolveData], AfterContextResolveData | false | void>} */
  92. afterResolve: new AsyncSeriesWaterfallHook(["data"]),
  93. /** @type {SyncWaterfallHook<[string[]]>} */
  94. contextModuleFiles: new SyncWaterfallHook(["files"]),
  95. /** @type {FakeHook<Pick<AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>, "tap" | "tapAsync" | "tapPromise" | "name">>} */
  96. alternatives: createFakeHook(
  97. {
  98. name: "alternatives",
  99. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["intercept"]} */
  100. intercept: (interceptor) => {
  101. throw new Error(
  102. "Intercepting fake hook ContextModuleFactory.hooks.alternatives is not possible, use ContextModuleFactory.hooks.alternativeRequests instead"
  103. );
  104. },
  105. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tap"]} */
  106. tap: (options, fn) => {
  107. alternativeRequests.tap(options, fn);
  108. },
  109. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tapAsync"]} */
  110. tapAsync: (options, fn) => {
  111. alternativeRequests.tapAsync(options, (items, _options, callback) =>
  112. fn(items, callback)
  113. );
  114. },
  115. /** @type {AsyncSeriesWaterfallHook<[ContextAlternativeRequest[]]>["tapPromise"]} */
  116. tapPromise: (options, fn) => {
  117. alternativeRequests.tapPromise(options, fn);
  118. }
  119. },
  120. "ContextModuleFactory.hooks.alternatives has deprecated in favor of ContextModuleFactory.hooks.alternativeRequests with an additional options argument.",
  121. "DEP_WEBPACK_CONTEXT_MODULE_FACTORY_ALTERNATIVES"
  122. ),
  123. alternativeRequests
  124. });
  125. /** @type {ResolverFactory} */
  126. this.resolverFactory = resolverFactory;
  127. }
  128. /**
  129. * Processes the provided data.
  130. * @param {ModuleFactoryCreateData} data data object
  131. * @param {ModuleFactoryCallback} callback callback
  132. * @returns {void}
  133. */
  134. create(data, callback) {
  135. const context = data.context;
  136. const dependencies = /** @type {ContextDependency[]} */ (data.dependencies);
  137. const resolveOptions = data.resolveOptions;
  138. const dependency = dependencies[0];
  139. /** @type {FileSystemDependencies} */
  140. const fileDependencies = new LazySet();
  141. /** @type {FileSystemDependencies} */
  142. const missingDependencies = new LazySet();
  143. /** @type {FileSystemDependencies} */
  144. const contextDependencies = new LazySet();
  145. this.hooks.beforeResolve.callAsync(
  146. {
  147. context,
  148. dependencies,
  149. layer: data.contextInfo.issuerLayer,
  150. resolveOptions,
  151. fileDependencies,
  152. missingDependencies,
  153. contextDependencies,
  154. ...dependency.options
  155. },
  156. (err, beforeResolveResult) => {
  157. if (err) {
  158. return callback(err, {
  159. fileDependencies,
  160. missingDependencies,
  161. contextDependencies
  162. });
  163. }
  164. // Ignored
  165. if (!beforeResolveResult) {
  166. return callback(null, {
  167. fileDependencies,
  168. missingDependencies,
  169. contextDependencies
  170. });
  171. }
  172. const context = beforeResolveResult.context;
  173. const request = beforeResolveResult.request;
  174. const resolveOptions = beforeResolveResult.resolveOptions;
  175. /** @type {undefined | string[]} */
  176. let loaders;
  177. /** @type {undefined | string} */
  178. let resource;
  179. let loadersPrefix = "";
  180. const idx = request.lastIndexOf("!");
  181. if (idx >= 0) {
  182. let loadersRequest = request.slice(0, idx + 1);
  183. /** @type {number} */
  184. let i;
  185. for (
  186. i = 0;
  187. i < loadersRequest.length && loadersRequest[i] === "!";
  188. i++
  189. ) {
  190. loadersPrefix += "!";
  191. }
  192. loadersRequest = loadersRequest
  193. .slice(i)
  194. .replace(/!+$/, "")
  195. .replace(/!{2,}/g, "!");
  196. loaders = loadersRequest === "" ? [] : loadersRequest.split("!");
  197. resource = request.slice(idx + 1);
  198. } else {
  199. loaders = [];
  200. resource = request;
  201. }
  202. const contextResolver = this.resolverFactory.get(
  203. "context",
  204. dependencies.length > 0
  205. ? cachedSetProperty(
  206. resolveOptions || EMPTY_RESOLVE_OPTIONS,
  207. "dependencyType",
  208. dependencies[0].category
  209. )
  210. : resolveOptions
  211. );
  212. const loaderResolver = this.resolverFactory.get("loader");
  213. asyncLib.parallel(
  214. [
  215. (callback) => {
  216. const results = /** @type {ResolveRequest[]} */ ([]);
  217. /**
  218. * Processes the provided obj.
  219. * @param {ResolveRequest} obj obj
  220. * @returns {void}
  221. */
  222. const yield_ = (obj) => {
  223. results.push(obj);
  224. };
  225. contextResolver.resolve(
  226. {},
  227. context,
  228. resource,
  229. {
  230. fileDependencies,
  231. missingDependencies,
  232. contextDependencies,
  233. yield: yield_
  234. },
  235. (err) => {
  236. if (err) return callback(err);
  237. callback(null, results);
  238. }
  239. );
  240. },
  241. (callback) => {
  242. asyncLib.map(
  243. loaders,
  244. (loader, callback) => {
  245. loaderResolver.resolve(
  246. {},
  247. context,
  248. loader,
  249. {
  250. fileDependencies,
  251. missingDependencies,
  252. contextDependencies
  253. },
  254. (err, result) => {
  255. if (err) return callback(err);
  256. callback(null, result);
  257. }
  258. );
  259. },
  260. callback
  261. );
  262. }
  263. ],
  264. (err, result) => {
  265. if (err) {
  266. return callback(err, {
  267. fileDependencies,
  268. missingDependencies,
  269. contextDependencies
  270. });
  271. }
  272. let [contextResult, loaderResult] =
  273. /** @type {[ResolveRequest[], string[]]} */ (result);
  274. if (contextResult.length > 1) {
  275. const first = contextResult[0];
  276. contextResult = contextResult.filter((r) => r.path);
  277. if (contextResult.length === 0) contextResult.push(first);
  278. }
  279. this.hooks.afterResolve.callAsync(
  280. {
  281. addon:
  282. loadersPrefix +
  283. loaderResult.join("!") +
  284. (loaderResult.length > 0 ? "!" : ""),
  285. resource:
  286. contextResult.length > 1
  287. ? /** @type {string[]} */ (contextResult.map((r) => r.path))
  288. : /** @type {string} */ (contextResult[0].path),
  289. resolveDependencies: this.resolveDependencies.bind(this),
  290. resourceQuery: contextResult[0].query,
  291. resourceFragment: contextResult[0].fragment,
  292. ...beforeResolveResult
  293. },
  294. (err, result) => {
  295. if (err) {
  296. return callback(err, {
  297. fileDependencies,
  298. missingDependencies,
  299. contextDependencies
  300. });
  301. }
  302. // Ignored
  303. if (!result) {
  304. return callback(null, {
  305. fileDependencies,
  306. missingDependencies,
  307. contextDependencies
  308. });
  309. }
  310. return callback(null, {
  311. module: new ContextModule(result.resolveDependencies, result),
  312. fileDependencies,
  313. missingDependencies,
  314. contextDependencies
  315. });
  316. }
  317. );
  318. }
  319. );
  320. }
  321. );
  322. }
  323. /**
  324. * Resolves dependencies.
  325. * @param {InputFileSystem} fs file system
  326. * @param {ContextModuleOptions} options options
  327. * @param {ResolveDependenciesCallback} callback callback function
  328. * @returns {void}
  329. */
  330. resolveDependencies(fs, options, callback) {
  331. const cmf = this;
  332. const {
  333. resource,
  334. resourceQuery,
  335. resourceFragment,
  336. recursive,
  337. regExp,
  338. patterns,
  339. requestContext,
  340. exhaustive,
  341. caseSensitive,
  342. include,
  343. exclude,
  344. referencedExports,
  345. category,
  346. typePrefix,
  347. attributes
  348. } = options;
  349. const isImportMetaGlob = Boolean(patterns && requestContext);
  350. if ((!regExp && !isImportMetaGlob) || !resource) return callback(null, []);
  351. // the request the user wrote (`#configs`, `./dir`, …) before it was resolved
  352. // to a directory — elements keep it so they can report an original request
  353. const contextRequest = getContextRequest(
  354. options.request,
  355. resourceQuery,
  356. resourceFragment
  357. );
  358. /**
  359. * Adds directory checked.
  360. * @param {string} ctx context
  361. * @param {string} directory directory
  362. * @param {Set<string>} visited visited
  363. * @param {ResolveDependenciesCallback} callback callback
  364. * @param {ResolvedContextModuleGlobPattern[] | undefined} resolvedGlobPatterns resolved glob patterns
  365. */
  366. const addDirectoryChecked = (
  367. ctx,
  368. directory,
  369. visited,
  370. callback,
  371. resolvedGlobPatterns
  372. ) => {
  373. /** @type {NonNullable<InputFileSystem["realpath"]>} */
  374. (fs.realpath)(directory, (err, _realPath) => {
  375. if (err) return callback(err);
  376. const realPath = /** @type {string} */ (_realPath);
  377. if (visited.has(realPath)) return callback(null, []);
  378. /** @type {Set<string> | undefined} */
  379. let recursionStack;
  380. addDirectory(
  381. ctx,
  382. directory,
  383. (_, dir, callback) => {
  384. if (recursionStack === undefined) {
  385. recursionStack = new Set(visited);
  386. recursionStack.add(realPath);
  387. }
  388. addDirectoryChecked(
  389. ctx,
  390. dir,
  391. recursionStack,
  392. callback,
  393. resolvedGlobPatterns
  394. );
  395. },
  396. callback,
  397. resolvedGlobPatterns
  398. );
  399. });
  400. };
  401. /**
  402. * Adds the provided ctx to the context module factory.
  403. * @param {string} ctx context
  404. * @param {string} directory directory
  405. * @param {AddSubDirectoryFn} addSubDirectory addSubDirectoryFn
  406. * @param {ResolveDependenciesCallback} callback callback
  407. * @param {ResolvedContextModuleGlobPattern[] | undefined} resolvedGlobPatterns resolved glob patterns
  408. * @returns {void}
  409. */
  410. const addDirectory = (
  411. ctx,
  412. directory,
  413. addSubDirectory,
  414. callback,
  415. resolvedGlobPatterns
  416. ) => {
  417. fs.readdir(directory, (err, files) => {
  418. if (err) return callback(err);
  419. const processedFiles = cmf.hooks.contextModuleFiles.call(
  420. /** @type {string[]} */ (files).map((file) => file.normalize("NFC"))
  421. );
  422. if (!processedFiles || processedFiles.length === 0) {
  423. return callback(null, []);
  424. }
  425. /** @type {ContextAlternativeRequest[]} */
  426. const fileObjs = [];
  427. /** @type {ContextElementDependency[]} */
  428. const globDeps = [];
  429. /** @type {Set<string>} */
  430. const globUserRequests = new Set();
  431. asyncLib.map(
  432. isImportMetaGlob
  433. ? processedFiles
  434. : processedFiles.filter((p) => p.indexOf(".") !== 0),
  435. (segment, callback) => {
  436. const subResource = join(fs, directory, segment);
  437. if (!exclude || !exclude.test(subResource)) {
  438. fs.stat(subResource, (err, _stat) => {
  439. if (err) {
  440. if (err.code === "ENOENT") {
  441. // ENOENT is ok here because the file may have been deleted between
  442. // the readdir and stat calls.
  443. return callback();
  444. }
  445. return callback(err);
  446. }
  447. const stat = /** @type {IStats} */ (_stat);
  448. if (stat.isDirectory()) {
  449. if (!recursive) return callback();
  450. if (
  451. isImportMetaGlob &&
  452. !exhaustive &&
  453. isNonExhaustiveImportMetaGlobSkippedDir(segment) &&
  454. !(
  455. resolvedGlobPatterns &&
  456. globPatternBaseReachesDir(
  457. resolvedGlobPatterns,
  458. subResource
  459. )
  460. )
  461. ) {
  462. return callback();
  463. }
  464. addSubDirectory(
  465. ctx,
  466. subResource,
  467. callback,
  468. resolvedGlobPatterns
  469. );
  470. } else if (
  471. stat.isFile() &&
  472. (!include || include.test(subResource))
  473. ) {
  474. if (
  475. isImportMetaGlob &&
  476. patterns &&
  477. requestContext &&
  478. resolvedGlobPatterns
  479. ) {
  480. const relativePath = `.${subResource
  481. .slice(ctx.length)
  482. .replace(/\\/g, "/")}`;
  483. const exposedUserRequest = globUserRequest(
  484. resolvedGlobPatterns,
  485. subResource,
  486. exhaustive === true,
  487. caseSensitive !== false
  488. );
  489. if (
  490. exposedUserRequest &&
  491. !globUserRequests.has(exposedUserRequest)
  492. ) {
  493. globUserRequests.add(exposedUserRequest);
  494. const dep = new ContextElementDependency(
  495. `${relativePath}${resourceQuery}${resourceFragment}`,
  496. exposedUserRequest,
  497. typePrefix,
  498. /** @type {string} */
  499. (category),
  500. referencedExports,
  501. ctx,
  502. attributes,
  503. contextRequest
  504. );
  505. dep.optional = true;
  506. globDeps.push(dep);
  507. }
  508. return callback();
  509. }
  510. // Collect for a single batched alternativeRequests call
  511. // per directory below. Calling the hook once per file
  512. // would pay per-call overhead (closure, resolverFactory
  513. // lookup, array allocations) for every file in the
  514. // context — which is the bulk of work on rebuilds.
  515. fileObjs.push({
  516. context: ctx,
  517. request: `.${subResource.slice(ctx.length).replace(/\\/g, "/")}`
  518. });
  519. callback();
  520. } else {
  521. callback();
  522. }
  523. });
  524. } else {
  525. callback();
  526. }
  527. },
  528. (err, result) => {
  529. if (err) return callback(err);
  530. /** @type {ContextElementDependency[]} */
  531. const flattenedResult = [];
  532. if (result) {
  533. for (const item of result) {
  534. if (item) flattenedResult.push(...item);
  535. }
  536. }
  537. if (isImportMetaGlob) {
  538. /** @type {Set<string>} */
  539. const mergedUserRequests = new Set();
  540. /** @type {ContextElementDependency[]} */
  541. const merged = [];
  542. for (const dep of [...flattenedResult, ...globDeps]) {
  543. if (mergedUserRequests.has(dep.userRequest)) continue;
  544. mergedUserRequests.add(dep.userRequest);
  545. merged.push(dep);
  546. }
  547. return callback(null, merged);
  548. }
  549. if (fileObjs.length === 0) {
  550. return callback(null, flattenedResult);
  551. }
  552. this.hooks.alternativeRequests.callAsync(
  553. fileObjs,
  554. options,
  555. (err, alternatives) => {
  556. if (err) return callback(err);
  557. for (const alt of /** @type {ContextAlternativeRequest[]} */ (
  558. alternatives
  559. )) {
  560. if (
  561. !(regExp instanceof RegExp) ||
  562. !regExp.test(/** @type {string} */ (alt.request))
  563. ) {
  564. continue;
  565. }
  566. const dep = new ContextElementDependency(
  567. `${alt.request}${resourceQuery}${resourceFragment}`,
  568. alt.request,
  569. typePrefix,
  570. /** @type {string} */
  571. (category),
  572. referencedExports,
  573. alt.context,
  574. attributes,
  575. contextRequest
  576. );
  577. dep.optional = true;
  578. flattenedResult.push(dep);
  579. }
  580. callback(null, flattenedResult);
  581. }
  582. );
  583. }
  584. );
  585. });
  586. };
  587. /**
  588. * Adds sub directory.
  589. * @param {string} ctx context
  590. * @param {string} dir dir
  591. * @param {ResolveDependenciesCallback} callback callback
  592. * @param {ResolvedContextModuleGlobPattern[] | undefined} resolvedGlobPatterns resolved glob patterns
  593. * @returns {void}
  594. */
  595. const addSubDirectory = (ctx, dir, callback, resolvedGlobPatterns) =>
  596. addDirectory(ctx, dir, addSubDirectory, callback, resolvedGlobPatterns);
  597. /**
  598. * Processes the provided resource.
  599. * @param {string} resource resource
  600. * @param {ResolveDependenciesCallback} callback callback
  601. */
  602. const visitResource = (resource, callback) => {
  603. /** @type {ResolvedContextModuleGlobPattern[] | undefined} */
  604. const resolvedGlobPatterns =
  605. isImportMetaGlob && patterns && requestContext
  606. ? patterns.map((pattern) =>
  607. resolveContextModuleGlobPattern(pattern, requestContext, resource)
  608. )
  609. : undefined;
  610. if (typeof fs.realpath === "function") {
  611. addDirectoryChecked(
  612. resource,
  613. resource,
  614. /** @type {Set<string>} */
  615. new Set(),
  616. callback,
  617. resolvedGlobPatterns
  618. );
  619. } else {
  620. addDirectory(
  621. resource,
  622. resource,
  623. addSubDirectory,
  624. callback,
  625. resolvedGlobPatterns
  626. );
  627. }
  628. };
  629. if (typeof resource === "string") {
  630. visitResource(resource, callback);
  631. } else {
  632. asyncLib.map(resource, visitResource, (err, _result) => {
  633. if (err) return callback(err);
  634. const result = /** @type {ContextElementDependency[][]} */ (_result);
  635. // result dependencies should have unique userRequest
  636. // ordered by resolve result
  637. /** @type {Set<string>} */
  638. const temp = new Set();
  639. /** @type {ContextElementDependency[]} */
  640. const res = [];
  641. for (let i = 0; i < result.length; i++) {
  642. const inner = result[i];
  643. for (const el of inner) {
  644. if (temp.has(el.userRequest)) continue;
  645. res.push(el);
  646. temp.add(el.userRequest);
  647. }
  648. }
  649. callback(null, res);
  650. });
  651. }
  652. }
  653. }
  654. module.exports = ContextModuleFactory;