LazyCompilationPlugin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { RawSource } = require("webpack-sources");
  7. const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
  8. const Dependency = require("../Dependency");
  9. const Module = require("../Module");
  10. const ModuleFactory = require("../ModuleFactory");
  11. const { JAVASCRIPT_TYPES } = require("../ModuleSourceTypeConstants");
  12. const { JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants");
  13. const {
  14. WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY
  15. } = require("../ModuleTypeConstants");
  16. const RuntimeGlobals = require("../RuntimeGlobals");
  17. const Template = require("../Template");
  18. const CommonJsRequireDependency = require("../dependencies/CommonJsRequireDependency");
  19. const { resolveByProperty } = require("../util/cleverMerge");
  20. const { registerNotSerializable } = require("../util/serialization");
  21. /**
  22. * @import {
  23. * WebpackOptionsNormalizedWithDefaults as WebpackOptions
  24. * } from "../config/defaults"
  25. */
  26. /** @import Compilation from "../Compilation" */
  27. /** @import Compiler from "../Compiler" */
  28. /** @import { UpdateHashContext } from "../Dependency" */
  29. /**
  30. * @import {
  31. * BuildCallback,
  32. * BuildInfo,
  33. * BuildMeta,
  34. * CodeGenerationContext,
  35. * CodeGenerationResult,
  36. * LibIdentOptions,
  37. * LibIdent,
  38. * NeedBuildCallback,
  39. * NeedBuildContext,
  40. * SourceTypes,
  41. * Sources,
  42. * RuntimeRequirements
  43. * } from "../Module"
  44. */
  45. /**
  46. * @import {
  47. * ModuleFactoryCallback,
  48. * ModuleFactoryCreateData
  49. * } from "../ModuleFactory"
  50. */
  51. /** @import RequestShortener from "../RequestShortener" */
  52. /** @import { ResolverWithOptions } from "../ResolverFactory" */
  53. /** @import HarmonyImportDependency from "../dependencies/HarmonyImportDependency" */
  54. /** @import Hash from "../util/Hash" */
  55. /** @import { InputFileSystem } from "../util/fs" */
  56. /** @typedef {{ client: string, data: string, active: boolean }} ModuleResult */
  57. /**
  58. * Library wrappers of these types pass external modules as closure arguments
  59. * (e.g. `__WEBPACK_EXTERNAL_MODULE_react__`) baked into the entry chunk at
  60. * render time. When `lazyCompilation` activates a proxy for the first time,
  61. * any external dependency the lazily-built module pulls in lands in a hot
  62. * update chunk that lives outside the original wrapper closure, so the
  63. * factory body can't resolve its closure identifier and throws at runtime.
  64. * Reserving the externals up front (during the inactive build) folds them
  65. * into the initial wrapper, so the closure identifiers are already defined
  66. * when the activation update arrives.
  67. */
  68. const CLOSURE_LIBRARY_TYPES = new Set([
  69. "umd",
  70. "umd2",
  71. "amd",
  72. "amd-require",
  73. "system"
  74. ]);
  75. /**
  76. * `enabledLibraryTypes` covers both the global `output.library.type` and any
  77. * per-entry `entry.<name>.library.type`, so a UMD/AMD/System wrapper attached
  78. * to an individual entry is still detected.
  79. * @param {import("../../declarations/WebpackOptions").OutputNormalized} output normalized output option
  80. * @returns {boolean} true when at least one library wrapper passes externals as closure arguments
  81. */
  82. const hasClosureLibrary = (output) => {
  83. const enabled = output.enabledLibraryTypes;
  84. if (enabled) {
  85. for (const type of enabled) {
  86. if (CLOSURE_LIBRARY_TYPES.has(type)) return true;
  87. }
  88. }
  89. if (output.library && output.library.type) {
  90. return CLOSURE_LIBRARY_TYPES.has(output.library.type);
  91. }
  92. return false;
  93. };
  94. /**
  95. * Collects request strings from statically-enumerable externals (string,
  96. * object, and arrays of those). Function and RegExp forms are skipped because
  97. * their effective request set isn't knowable until something asks for it.
  98. *
  99. * Layer resolution mirrors `ExternalModuleFactoryPlugin.resolveLayer`: the
  100. * effective map for the proxy's layer is computed via the same
  101. * `resolveByProperty(..., "byLayer", layer)` helper that the externals system
  102. * uses, so `byLayer.default` fallback and function-form `byLayer` entries are
  103. * honored the same way.
  104. *
  105. * Entries whose effective value is `false` are skipped — `false` explicitly
  106. * disables externalization for that request, and reserving it would force the
  107. * real module into the entry chunk.
  108. * @param {import("../../declarations/WebpackOptions").Externals | undefined} externals normalized externals option
  109. * @param {string | null} layer issuer layer for which to resolve `byLayer`
  110. * @returns {Set<string>} requests to reserve in the entry chunk
  111. */
  112. const collectStaticExternalRequests = (externals, layer) => {
  113. /** @type {Set<string>} */
  114. const requests = new Set();
  115. if (!externals) return requests;
  116. /** @param {import("../../declarations/WebpackOptions").ExternalItem} item one item */
  117. const visit = (item) => {
  118. if (typeof item === "string") {
  119. requests.add(item);
  120. return;
  121. }
  122. if (!item || typeof item !== "object" || item instanceof RegExp) return;
  123. const resolved = /** @type {Record<string, unknown>} */ (
  124. resolveByProperty(
  125. /** @type {Record<string, unknown>} */ (item),
  126. "byLayer",
  127. layer
  128. )
  129. );
  130. for (const [request, value] of Object.entries(resolved)) {
  131. // `false` explicitly opts the request out of externalization; reserving
  132. // it would pull the actual module into the entry chunk.
  133. if (value === false) continue;
  134. requests.add(request);
  135. }
  136. };
  137. if (Array.isArray(externals)) {
  138. for (const item of externals) visit(item);
  139. } else {
  140. visit(externals);
  141. }
  142. return requests;
  143. };
  144. /**
  145. * Defines the backend api type used by this module.
  146. * @typedef {object} BackendApi
  147. * @property {(callback: (err?: (Error | null)) => void) => void} dispose
  148. * @property {(module: Module) => ModuleResult} module
  149. */
  150. const HMR_DEPENDENCY_TYPES = new Set([
  151. "import.meta.webpackHot.accept",
  152. "import.meta.webpackHot.decline",
  153. "module.hot.accept",
  154. "module.hot.decline"
  155. ]);
  156. /**
  157. * Checks true, if the module should be selected.
  158. * @param {Options["test"]} test test option
  159. * @param {Module} module the module
  160. * @returns {boolean | null | string} true, if the module should be selected
  161. */
  162. const checkTest = (test, module) => {
  163. if (test === undefined) return true;
  164. if (typeof test === "function") {
  165. return test(module);
  166. }
  167. if (typeof test === "string") {
  168. const name = module.nameForCondition();
  169. return name && name.startsWith(test);
  170. }
  171. if (test instanceof RegExp) {
  172. const name = module.nameForCondition();
  173. return name && test.test(name);
  174. }
  175. return false;
  176. };
  177. class LazyCompilationDependency extends Dependency {
  178. /**
  179. * Creates an instance of LazyCompilationDependency.
  180. * @param {LazyCompilationProxyModule} proxyModule proxy module
  181. */
  182. constructor(proxyModule) {
  183. super();
  184. /** @type {LazyCompilationProxyModule} */
  185. this.proxyModule = proxyModule;
  186. }
  187. get category() {
  188. return "esm";
  189. }
  190. get type() {
  191. return "lazy import()";
  192. }
  193. /**
  194. * Returns an identifier to merge equal requests.
  195. * @returns {string | null} an identifier to merge equal requests
  196. */
  197. getResourceIdentifier() {
  198. return this.proxyModule.originalModule.identifier();
  199. }
  200. }
  201. registerNotSerializable(LazyCompilationDependency);
  202. /**
  203. * Defines the build info properties specific to lazy compilation proxy modules.
  204. * @typedef {object} KnownLazyCompilationProxyModuleBuildInfo
  205. * @property {boolean=} active whether the proxied module was active when built
  206. */
  207. /** @typedef {BuildInfo & KnownLazyCompilationProxyModuleBuildInfo} LazyCompilationProxyModuleBuildInfo */
  208. class LazyCompilationProxyModule extends Module {
  209. /**
  210. * Creates an instance of LazyCompilationProxyModule.
  211. * @param {string} context context
  212. * @param {Module} originalModule an original module
  213. * @param {string} request request
  214. * @param {ModuleResult["client"]} client client
  215. * @param {ModuleResult["data"]} data data
  216. * @param {ModuleResult["active"]} active true when active, otherwise false
  217. */
  218. constructor(context, originalModule, request, client, data, active) {
  219. super(
  220. WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY,
  221. context,
  222. originalModule.layer
  223. );
  224. // Redeclared with the lazy compilation proxy specific shape
  225. /** @type {LazyCompilationProxyModuleBuildInfo | undefined} */
  226. this.buildInfo = undefined;
  227. /** @type {Module} */
  228. this.originalModule = originalModule;
  229. /** @type {string} */
  230. this.request = request;
  231. /** @type {string} */
  232. this.client = client;
  233. /** @type {string} */
  234. this.data = data;
  235. /** @type {boolean} */
  236. this.active = active;
  237. }
  238. /**
  239. * Returns the unique identifier used to reference this module.
  240. * @returns {string} a unique identifier of the module
  241. */
  242. identifier() {
  243. return `${WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY}|${this.originalModule.identifier()}`;
  244. }
  245. /**
  246. * Returns a human-readable identifier for this module.
  247. * @param {RequestShortener} requestShortener the request shortener
  248. * @returns {string} a user readable identifier of the module
  249. */
  250. readableIdentifier(requestShortener) {
  251. return `${WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY} ${this.originalModule.readableIdentifier(
  252. requestShortener
  253. )}`;
  254. }
  255. /**
  256. * Assuming this module is in the cache. Update the (cached) module with
  257. * the fresh module from the factory. Usually updates internal references
  258. * and properties.
  259. * @param {Module} module fresh module
  260. * @returns {void}
  261. */
  262. updateCacheModule(module) {
  263. super.updateCacheModule(module);
  264. const m = /** @type {LazyCompilationProxyModule} */ (module);
  265. this.originalModule = m.originalModule;
  266. this.request = m.request;
  267. this.client = m.client;
  268. this.data = m.data;
  269. this.active = m.active;
  270. }
  271. /**
  272. * Gets the library identifier.
  273. * @param {LibIdentOptions} options options
  274. * @returns {LibIdent | null} an identifier for library inclusion
  275. */
  276. libIdent(options) {
  277. return `${this.originalModule.libIdent(
  278. options
  279. )}!${WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY}`;
  280. }
  281. /**
  282. * Checks whether the module needs to be rebuilt for the current build state.
  283. * @param {NeedBuildContext} context context info
  284. * @param {NeedBuildCallback} callback callback function, returns true, if the module needs a rebuild
  285. * @returns {void}
  286. */
  287. needBuild(context, callback) {
  288. callback(null, !this.buildInfo || this.buildInfo.active !== this.active);
  289. }
  290. /**
  291. * Builds the module using the provided compilation context.
  292. * @param {WebpackOptions} options webpack options
  293. * @param {Compilation} compilation the compilation
  294. * @param {ResolverWithOptions} resolver the resolver
  295. * @param {InputFileSystem} fs the file system
  296. * @param {BuildCallback} callback callback function
  297. * @returns {void}
  298. */
  299. build(options, compilation, resolver, fs, callback) {
  300. this.buildInfo = {
  301. active: this.active
  302. };
  303. /** @type {BuildMeta} */
  304. this.buildMeta = {};
  305. this.clearDependenciesAndBlocks();
  306. const dep = new CommonJsRequireDependency(this.client);
  307. this.addDependency(dep);
  308. if (this.active) {
  309. const dep = new LazyCompilationDependency(this);
  310. const block = new AsyncDependenciesBlock({});
  311. block.addDependency(dep);
  312. this.addBlock(block);
  313. } else if (hasClosureLibrary(compilation.options.output)) {
  314. // Reserve statically-declared externals as dependencies of the inactive
  315. // proxy so the initial entry chunk's library wrapper already exposes
  316. // their closure identifiers (e.g. `__WEBPACK_EXTERNAL_MODULE_react__`).
  317. // Once the proxy activates and the lazily-built module references those
  318. // externals, the identifiers resolve normally instead of throwing.
  319. const requests = collectStaticExternalRequests(
  320. options.externals,
  321. this.layer
  322. );
  323. for (const request of requests) {
  324. this.addDependency(new CommonJsRequireDependency(request));
  325. }
  326. }
  327. callback();
  328. }
  329. /**
  330. * Returns the source types this module can generate.
  331. * @returns {SourceTypes} types available (do not mutate)
  332. */
  333. getSourceTypes() {
  334. return JAVASCRIPT_TYPES;
  335. }
  336. /**
  337. * Returns the estimated size for the requested source type.
  338. * @param {string=} type the source type for which the size should be estimated
  339. * @returns {number} the estimated size of the module (must be non-zero)
  340. */
  341. size(type) {
  342. return 200;
  343. }
  344. /**
  345. * Generates code and runtime requirements for this module.
  346. * @param {CodeGenerationContext} context context for code generation
  347. * @returns {CodeGenerationResult} result
  348. */
  349. codeGeneration({ runtimeTemplate, chunkGraph, moduleGraph }) {
  350. /** @type {Sources} */
  351. const sources = new Map();
  352. /** @type {RuntimeRequirements} */
  353. const runtimeRequirements = new Set();
  354. runtimeRequirements.add(RuntimeGlobals.module);
  355. const clientDep = /** @type {CommonJsRequireDependency} */ (
  356. this.dependencies[0]
  357. );
  358. const clientModule = moduleGraph.getModule(clientDep);
  359. const block = this.blocks[0];
  360. const cst = runtimeTemplate.renderConst();
  361. const lt = runtimeTemplate.renderLet();
  362. const client = Template.asString([
  363. `${cst} client = ${runtimeTemplate.moduleExports({
  364. module: clientModule,
  365. chunkGraph,
  366. request: clientDep.userRequest,
  367. runtimeRequirements
  368. })}`,
  369. `${cst} data = ${JSON.stringify(this.data)};`
  370. ]);
  371. const keepActive = Template.asString([
  372. `${cst} dispose = client.keepAlive({ data: data, active: ${JSON.stringify(
  373. Boolean(block)
  374. )}, module: module, onError: onError });`
  375. ]);
  376. /** @type {string} */
  377. let source;
  378. if (block) {
  379. const dep = block.dependencies[0];
  380. const module = /** @type {Module} */ (moduleGraph.getModule(dep));
  381. source = Template.asString([
  382. client,
  383. `module.exports = ${runtimeTemplate.moduleNamespacePromise({
  384. chunkGraph,
  385. block,
  386. module,
  387. request: this.request,
  388. dependency: dep,
  389. strict: false, // TODO this should be inherited from the original module
  390. message: "import()",
  391. runtimeRequirements,
  392. originModule: this
  393. })};`,
  394. "if (module.hot) {",
  395. Template.indent([
  396. "module.hot.accept();",
  397. `module.hot.accept(${JSON.stringify(
  398. chunkGraph.getModuleId(module)
  399. )}, function() { module.hot.invalidate(); });`,
  400. "module.hot.dispose(function(data) { delete data.resolveSelf; dispose(data); });",
  401. `if (${runtimeTemplate.optionalChaining("module.hot.data", "resolveSelf")}) module.hot.data.resolveSelf(module.exports);`
  402. ]),
  403. "}",
  404. "function onError() { /* ignore */ }",
  405. keepActive
  406. ]);
  407. } else {
  408. source = Template.asString([
  409. client,
  410. `${lt} resolveSelf, onError;`,
  411. "module.exports = new Promise(function(resolve, reject) { resolveSelf = resolve; onError = reject; });",
  412. "if (module.hot) {",
  413. Template.indent([
  414. "module.hot.accept();",
  415. `if (${runtimeTemplate.optionalChaining("module.hot.data", "resolveSelf")}) module.hot.data.resolveSelf(module.exports);`,
  416. "module.hot.dispose(function(data) { data.resolveSelf = resolveSelf; dispose(data); });"
  417. ]),
  418. "}",
  419. keepActive
  420. ]);
  421. }
  422. sources.set(JAVASCRIPT_TYPE, new RawSource(source));
  423. return {
  424. sources,
  425. runtimeRequirements
  426. };
  427. }
  428. /**
  429. * Updates the hash with the data contributed by this instance.
  430. * @param {Hash} hash the hash used to track dependencies
  431. * @param {UpdateHashContext} context context
  432. * @returns {void}
  433. */
  434. updateHash(hash, context) {
  435. super.updateHash(hash, context);
  436. hash.update(this.active ? "active" : "");
  437. hash.update(JSON.stringify(this.data));
  438. }
  439. }
  440. registerNotSerializable(LazyCompilationProxyModule);
  441. class LazyCompilationDependencyFactory extends ModuleFactory {
  442. constructor() {
  443. super();
  444. }
  445. /**
  446. * Processes the provided data.
  447. * @param {ModuleFactoryCreateData} data data object
  448. * @param {ModuleFactoryCallback} callback callback
  449. * @returns {void}
  450. */
  451. create(data, callback) {
  452. const dependency =
  453. /** @type {LazyCompilationDependency} */
  454. (data.dependencies[0]);
  455. callback(null, {
  456. module: dependency.proxyModule.originalModule
  457. });
  458. }
  459. }
  460. /**
  461. * Defines the backend handler callback.
  462. * @callback BackendHandler
  463. * @param {Compiler} compiler compiler
  464. * @param {(err: Error | null, backendApi?: BackendApi) => void} callback callback
  465. * @returns {void}
  466. */
  467. /**
  468. * Defines the promise backend handler callback.
  469. * @callback PromiseBackendHandler
  470. * @param {Compiler} compiler compiler
  471. * @returns {Promise<BackendApi>} backend
  472. */
  473. /** @typedef {BackendHandler | PromiseBackendHandler} BackEnd */
  474. /** @typedef {(module: Module) => boolean} TestFn */
  475. /**
  476. * Defines the options type used by this module.
  477. * @typedef {object} Options options
  478. * @property {BackEnd} backend the backend
  479. * @property {boolean=} entries
  480. * @property {boolean=} imports
  481. * @property {RegExp | string | TestFn=} test additional filter for lazy compiled entrypoint modules
  482. */
  483. const PLUGIN_NAME = "LazyCompilationPlugin";
  484. class LazyCompilationPlugin {
  485. /**
  486. * Creates an instance of LazyCompilationPlugin.
  487. * @param {Options} options options
  488. */
  489. constructor({ backend, entries, imports, test }) {
  490. /** @type {BackEnd} */
  491. this.backend = backend;
  492. /** @type {boolean | undefined} */
  493. this.entries = entries;
  494. /** @type {boolean | undefined} */
  495. this.imports = imports;
  496. /** @type {string | RegExp | TestFn | undefined} */
  497. this.test = test;
  498. }
  499. /**
  500. * Applies the plugin by registering its hooks on the compiler.
  501. * @param {Compiler} compiler the compiler instance
  502. * @returns {void}
  503. */
  504. apply(compiler) {
  505. /** @type {BackendApi} */
  506. let backend;
  507. compiler.hooks.beforeCompile.tapAsync(PLUGIN_NAME, (params, callback) => {
  508. if (backend !== undefined) return callback();
  509. const promise = this.backend(compiler, (err, result) => {
  510. if (err) return callback(err);
  511. backend = /** @type {BackendApi} */ (result);
  512. callback();
  513. });
  514. if (promise && promise.then) {
  515. promise.then((b) => {
  516. backend = b;
  517. callback();
  518. }, callback);
  519. }
  520. });
  521. compiler.hooks.thisCompilation.tap(
  522. PLUGIN_NAME,
  523. (compilation, { normalModuleFactory }) => {
  524. // A proxy already loaded in the client outlives the build that required
  525. // `ensureChunk`, so every runtime keeps it or the proxy calls a dropped global.
  526. compilation.hooks.additionalTreeRuntimeRequirements.tap(
  527. PLUGIN_NAME,
  528. (chunk, set) => {
  529. set.add(RuntimeGlobals.ensureChunk);
  530. }
  531. );
  532. normalModuleFactory.hooks.module.tap(
  533. PLUGIN_NAME,
  534. (module, createData, resolveData) => {
  535. if (
  536. resolveData.dependencies.every((dep) =>
  537. HMR_DEPENDENCY_TYPES.has(dep.type)
  538. )
  539. ) {
  540. // for HMR only resolving, try to determine if the HMR accept/decline refers to
  541. // an import() or not
  542. const hmrDep = resolveData.dependencies[0];
  543. const originModule =
  544. /** @type {Module} */
  545. (compilation.moduleGraph.getParentModule(hmrDep));
  546. const isReferringToDynamicImport = originModule.blocks.some(
  547. (block) =>
  548. block.dependencies.some(
  549. (dep) =>
  550. dep.type === "import()" &&
  551. /** @type {HarmonyImportDependency} */ (dep).request ===
  552. hmrDep.request
  553. )
  554. );
  555. if (!isReferringToDynamicImport) return module;
  556. } else if (
  557. !resolveData.dependencies.every(
  558. (dep) =>
  559. HMR_DEPENDENCY_TYPES.has(dep.type) ||
  560. (this.imports &&
  561. (dep.type === "import()" ||
  562. dep.type === "import() context element")) ||
  563. (this.entries && dep.type === "entry")
  564. )
  565. ) {
  566. return module;
  567. }
  568. if (
  569. /webpack[/\\]hot[/\\]|webpack-dev-server[/\\]client|webpack-hot-middleware[/\\]client/.test(
  570. resolveData.request
  571. ) ||
  572. !checkTest(this.test, module)
  573. ) {
  574. return module;
  575. }
  576. const moduleInfo = backend.module(module);
  577. if (!moduleInfo) return module;
  578. const { client, data, active } = moduleInfo;
  579. return new LazyCompilationProxyModule(
  580. compiler.context,
  581. module,
  582. resolveData.request,
  583. client,
  584. data,
  585. active
  586. );
  587. }
  588. );
  589. compilation.dependencyFactories.set(
  590. LazyCompilationDependency,
  591. new LazyCompilationDependencyFactory()
  592. );
  593. }
  594. );
  595. compiler.hooks.shutdown.tapAsync(PLUGIN_NAME, (callback) => {
  596. backend.dispose(callback);
  597. });
  598. }
  599. }
  600. module.exports = LazyCompilationPlugin;