UNPKG

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