LazyCompilationPlugin.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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 { registerNotSerializable } = require("../util/serialization");
  20. /** @typedef {import("../config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */
  21. /** @typedef {import("../Compilation")} Compilation */
  22. /** @typedef {import("../Compiler")} Compiler */
  23. /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
  24. /** @typedef {import("../Module").BuildCallback} BuildCallback */
  25. /** @typedef {import("../Module").BuildMeta} BuildMeta */
  26. /** @typedef {import("../Module").CodeGenerationContext} CodeGenerationContext */
  27. /** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
  28. /** @typedef {import("../Module").LibIdentOptions} LibIdentOptions */
  29. /** @typedef {import("../Module").LibIdent} LibIdent */
  30. /** @typedef {import("../Module").NeedBuildCallback} NeedBuildCallback */
  31. /** @typedef {import("../Module").NeedBuildContext} NeedBuildContext */
  32. /** @typedef {import("../Module").SourceTypes} SourceTypes */
  33. /** @typedef {import("../ModuleFactory").ModuleFactoryCallback} ModuleFactoryCallback */
  34. /** @typedef {import("../ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
  35. /** @typedef {import("../RequestShortener")} RequestShortener */
  36. /** @typedef {import("../ResolverFactory").ResolverWithOptions} ResolverWithOptions */
  37. /** @typedef {import("../dependencies/HarmonyImportDependency")} HarmonyImportDependency */
  38. /** @typedef {import("../util/Hash")} Hash */
  39. /** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
  40. /** @typedef {{ client: string, data: string, active: boolean }} ModuleResult */
  41. /**
  42. * @typedef {object} BackendApi
  43. * @property {(callback: (err?: (Error | null)) => void) => void} dispose
  44. * @property {(module: Module) => ModuleResult} module
  45. */
  46. const HMR_DEPENDENCY_TYPES = new Set([
  47. "import.meta.webpackHot.accept",
  48. "import.meta.webpackHot.decline",
  49. "module.hot.accept",
  50. "module.hot.decline"
  51. ]);
  52. /**
  53. * @param {Options["test"]} test test option
  54. * @param {Module} module the module
  55. * @returns {boolean | null | string} true, if the module should be selected
  56. */
  57. const checkTest = (test, module) => {
  58. if (test === undefined) return true;
  59. if (typeof test === "function") {
  60. return test(module);
  61. }
  62. if (typeof test === "string") {
  63. const name = module.nameForCondition();
  64. return name && name.startsWith(test);
  65. }
  66. if (test instanceof RegExp) {
  67. const name = module.nameForCondition();
  68. return name && test.test(name);
  69. }
  70. return false;
  71. };
  72. class LazyCompilationDependency extends Dependency {
  73. /**
  74. * @param {LazyCompilationProxyModule} proxyModule proxy module
  75. */
  76. constructor(proxyModule) {
  77. super();
  78. this.proxyModule = proxyModule;
  79. }
  80. get category() {
  81. return "esm";
  82. }
  83. get type() {
  84. return "lazy import()";
  85. }
  86. /**
  87. * @returns {string | null} an identifier to merge equal requests
  88. */
  89. getResourceIdentifier() {
  90. return this.proxyModule.originalModule.identifier();
  91. }
  92. }
  93. registerNotSerializable(LazyCompilationDependency);
  94. class LazyCompilationProxyModule extends Module {
  95. /**
  96. * @param {string} context context
  97. * @param {Module} originalModule an original module
  98. * @param {string} request request
  99. * @param {ModuleResult["client"]} client client
  100. * @param {ModuleResult["data"]} data data
  101. * @param {ModuleResult["active"]} active true when active, otherwise false
  102. */
  103. constructor(context, originalModule, request, client, data, active) {
  104. super(
  105. WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY,
  106. context,
  107. originalModule.layer
  108. );
  109. this.originalModule = originalModule;
  110. this.request = request;
  111. this.client = client;
  112. this.data = data;
  113. this.active = active;
  114. }
  115. /**
  116. * @returns {string} a unique identifier of the module
  117. */
  118. identifier() {
  119. return `${WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY}|${this.originalModule.identifier()}`;
  120. }
  121. /**
  122. * @param {RequestShortener} requestShortener the request shortener
  123. * @returns {string} a user readable identifier of the module
  124. */
  125. readableIdentifier(requestShortener) {
  126. return `${WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY} ${this.originalModule.readableIdentifier(
  127. requestShortener
  128. )}`;
  129. }
  130. /**
  131. * Assuming this module is in the cache. Update the (cached) module with
  132. * the fresh module from the factory. Usually updates internal references
  133. * and properties.
  134. * @param {Module} module fresh module
  135. * @returns {void}
  136. */
  137. updateCacheModule(module) {
  138. super.updateCacheModule(module);
  139. const m = /** @type {LazyCompilationProxyModule} */ (module);
  140. this.originalModule = m.originalModule;
  141. this.request = m.request;
  142. this.client = m.client;
  143. this.data = m.data;
  144. this.active = m.active;
  145. }
  146. /**
  147. * @param {LibIdentOptions} options options
  148. * @returns {LibIdent | null} an identifier for library inclusion
  149. */
  150. libIdent(options) {
  151. return `${this.originalModule.libIdent(
  152. options
  153. )}!${WEBPACK_MODULE_TYPE_LAZY_COMPILATION_PROXY}`;
  154. }
  155. /**
  156. * @param {NeedBuildContext} context context info
  157. * @param {NeedBuildCallback} callback callback function, returns true, if the module needs a rebuild
  158. * @returns {void}
  159. */
  160. needBuild(context, callback) {
  161. callback(null, !this.buildInfo || this.buildInfo.active !== this.active);
  162. }
  163. /**
  164. * @param {WebpackOptions} options webpack options
  165. * @param {Compilation} compilation the compilation
  166. * @param {ResolverWithOptions} resolver the resolver
  167. * @param {InputFileSystem} fs the file system
  168. * @param {BuildCallback} callback callback function
  169. * @returns {void}
  170. */
  171. build(options, compilation, resolver, fs, callback) {
  172. this.buildInfo = {
  173. active: this.active
  174. };
  175. /** @type {BuildMeta} */
  176. this.buildMeta = {};
  177. this.clearDependenciesAndBlocks();
  178. const dep = new CommonJsRequireDependency(this.client);
  179. this.addDependency(dep);
  180. if (this.active) {
  181. const dep = new LazyCompilationDependency(this);
  182. const block = new AsyncDependenciesBlock({});
  183. block.addDependency(dep);
  184. this.addBlock(block);
  185. }
  186. callback();
  187. }
  188. /**
  189. * @returns {SourceTypes} types available (do not mutate)
  190. */
  191. getSourceTypes() {
  192. return JAVASCRIPT_TYPES;
  193. }
  194. /**
  195. * @param {string=} type the source type for which the size should be estimated
  196. * @returns {number} the estimated size of the module (must be non-zero)
  197. */
  198. size(type) {
  199. return 200;
  200. }
  201. /**
  202. * @param {CodeGenerationContext} context context for code generation
  203. * @returns {CodeGenerationResult} result
  204. */
  205. codeGeneration({ runtimeTemplate, chunkGraph, moduleGraph }) {
  206. const sources = new Map();
  207. const runtimeRequirements = new Set();
  208. runtimeRequirements.add(RuntimeGlobals.module);
  209. const clientDep = /** @type {CommonJsRequireDependency} */ (
  210. this.dependencies[0]
  211. );
  212. const clientModule = moduleGraph.getModule(clientDep);
  213. const block = this.blocks[0];
  214. const client = Template.asString([
  215. `var client = ${runtimeTemplate.moduleExports({
  216. module: clientModule,
  217. chunkGraph,
  218. request: clientDep.userRequest,
  219. runtimeRequirements
  220. })}`,
  221. `var data = ${JSON.stringify(this.data)};`
  222. ]);
  223. const keepActive = Template.asString([
  224. `var dispose = client.keepAlive({ data: data, active: ${JSON.stringify(
  225. Boolean(block)
  226. )}, module: module, onError: onError });`
  227. ]);
  228. let source;
  229. if (block) {
  230. const dep = block.dependencies[0];
  231. const module = /** @type {Module} */ (moduleGraph.getModule(dep));
  232. source = Template.asString([
  233. client,
  234. `module.exports = ${runtimeTemplate.moduleNamespacePromise({
  235. chunkGraph,
  236. block,
  237. module,
  238. request: this.request,
  239. dependency: dep,
  240. strict: false, // TODO this should be inherited from the original module
  241. message: "import()",
  242. runtimeRequirements
  243. })};`,
  244. "if (module.hot) {",
  245. Template.indent([
  246. "module.hot.accept();",
  247. `module.hot.accept(${JSON.stringify(
  248. chunkGraph.getModuleId(module)
  249. )}, function() { module.hot.invalidate(); });`,
  250. "module.hot.dispose(function(data) { delete data.resolveSelf; dispose(data); });",
  251. "if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports);"
  252. ]),
  253. "}",
  254. "function onError() { /* ignore */ }",
  255. keepActive
  256. ]);
  257. } else {
  258. source = Template.asString([
  259. client,
  260. "var resolveSelf, onError;",
  261. "module.exports = new Promise(function(resolve, reject) { resolveSelf = resolve; onError = reject; });",
  262. "if (module.hot) {",
  263. Template.indent([
  264. "module.hot.accept();",
  265. "if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports);",
  266. "module.hot.dispose(function(data) { data.resolveSelf = resolveSelf; dispose(data); });"
  267. ]),
  268. "}",
  269. keepActive
  270. ]);
  271. }
  272. sources.set(JAVASCRIPT_TYPE, new RawSource(source));
  273. return {
  274. sources,
  275. runtimeRequirements
  276. };
  277. }
  278. /**
  279. * @param {Hash} hash the hash used to track dependencies
  280. * @param {UpdateHashContext} context context
  281. * @returns {void}
  282. */
  283. updateHash(hash, context) {
  284. super.updateHash(hash, context);
  285. hash.update(this.active ? "active" : "");
  286. hash.update(JSON.stringify(this.data));
  287. }
  288. }
  289. registerNotSerializable(LazyCompilationProxyModule);
  290. class LazyCompilationDependencyFactory extends ModuleFactory {
  291. constructor() {
  292. super();
  293. }
  294. /**
  295. * @param {ModuleFactoryCreateData} data data object
  296. * @param {ModuleFactoryCallback} callback callback
  297. * @returns {void}
  298. */
  299. create(data, callback) {
  300. const dependency =
  301. /** @type {LazyCompilationDependency} */
  302. (data.dependencies[0]);
  303. callback(null, {
  304. module: dependency.proxyModule.originalModule
  305. });
  306. }
  307. }
  308. /**
  309. * @callback BackendHandler
  310. * @param {Compiler} compiler compiler
  311. * @param {(err: Error | null, backendApi?: BackendApi) => void} callback callback
  312. * @returns {void}
  313. */
  314. /**
  315. * @callback PromiseBackendHandler
  316. * @param {Compiler} compiler compiler
  317. * @returns {Promise<BackendApi>} backend
  318. */
  319. /** @typedef {BackendHandler | PromiseBackendHandler} BackEnd */
  320. /** @typedef {(module: Module) => boolean} TestFn */
  321. /**
  322. * @typedef {object} Options options
  323. * @property {BackEnd} backend the backend
  324. * @property {boolean=} entries
  325. * @property {boolean=} imports
  326. * @property {RegExp | string | TestFn=} test additional filter for lazy compiled entrypoint modules
  327. */
  328. const PLUGIN_NAME = "LazyCompilationPlugin";
  329. class LazyCompilationPlugin {
  330. /**
  331. * @param {Options} options options
  332. */
  333. constructor({ backend, entries, imports, test }) {
  334. this.backend = backend;
  335. this.entries = entries;
  336. this.imports = imports;
  337. this.test = test;
  338. }
  339. /**
  340. * Apply the plugin
  341. * @param {Compiler} compiler the compiler instance
  342. * @returns {void}
  343. */
  344. apply(compiler) {
  345. /** @type {BackendApi} */
  346. let backend;
  347. compiler.hooks.beforeCompile.tapAsync(PLUGIN_NAME, (params, callback) => {
  348. if (backend !== undefined) return callback();
  349. const promise = this.backend(compiler, (err, result) => {
  350. if (err) return callback(err);
  351. backend = /** @type {BackendApi} */ (result);
  352. callback();
  353. });
  354. if (promise && promise.then) {
  355. promise.then((b) => {
  356. backend = b;
  357. callback();
  358. }, callback);
  359. }
  360. });
  361. compiler.hooks.thisCompilation.tap(
  362. PLUGIN_NAME,
  363. (compilation, { normalModuleFactory }) => {
  364. normalModuleFactory.hooks.module.tap(
  365. PLUGIN_NAME,
  366. (module, createData, resolveData) => {
  367. if (
  368. resolveData.dependencies.every((dep) =>
  369. HMR_DEPENDENCY_TYPES.has(dep.type)
  370. )
  371. ) {
  372. // for HMR only resolving, try to determine if the HMR accept/decline refers to
  373. // an import() or not
  374. const hmrDep = resolveData.dependencies[0];
  375. const originModule =
  376. /** @type {Module} */
  377. (compilation.moduleGraph.getParentModule(hmrDep));
  378. const isReferringToDynamicImport = originModule.blocks.some(
  379. (block) =>
  380. block.dependencies.some(
  381. (dep) =>
  382. dep.type === "import()" &&
  383. /** @type {HarmonyImportDependency} */ (dep).request ===
  384. hmrDep.request
  385. )
  386. );
  387. if (!isReferringToDynamicImport) return module;
  388. } else if (
  389. !resolveData.dependencies.every(
  390. (dep) =>
  391. HMR_DEPENDENCY_TYPES.has(dep.type) ||
  392. (this.imports &&
  393. (dep.type === "import()" ||
  394. dep.type === "import() context element")) ||
  395. (this.entries && dep.type === "entry")
  396. )
  397. ) {
  398. return module;
  399. }
  400. if (
  401. /webpack[/\\]hot[/\\]|webpack-dev-server[/\\]client|webpack-hot-middleware[/\\]client/.test(
  402. resolveData.request
  403. ) ||
  404. !checkTest(this.test, module)
  405. ) {
  406. return module;
  407. }
  408. const moduleInfo = backend.module(module);
  409. if (!moduleInfo) return module;
  410. const { client, data, active } = moduleInfo;
  411. return new LazyCompilationProxyModule(
  412. compiler.context,
  413. module,
  414. resolveData.request,
  415. client,
  416. data,
  417. active
  418. );
  419. }
  420. );
  421. compilation.dependencyFactories.set(
  422. LazyCompilationDependency,
  423. new LazyCompilationDependencyFactory()
  424. );
  425. }
  426. );
  427. compiler.hooks.shutdown.tapAsync(PLUGIN_NAME, (callback) => {
  428. backend.dispose(callback);
  429. });
  430. }
  431. }
  432. module.exports = LazyCompilationPlugin;