mirror of https://github.com/webpack/webpack.git
add experimental lazy compilation
This commit is contained in:
parent
287707ce4d
commit
a1515fa9f0
|
@ -1041,6 +1041,30 @@ export interface Experiments {
|
|||
* Enable module and chunk layers.
|
||||
*/
|
||||
layers?: boolean;
|
||||
/**
|
||||
* Compile import()s only when they are accessed.
|
||||
*/
|
||||
lazyCompilation?:
|
||||
| boolean
|
||||
| {
|
||||
/**
|
||||
* A custom backend.
|
||||
*/
|
||||
backend?:
|
||||
| ((
|
||||
compiler: import("../lib/Compiler"),
|
||||
client: string,
|
||||
callback: (err?: Error, api?: any) => void
|
||||
) => void)
|
||||
| ((
|
||||
compiler: import("../lib/Compiler"),
|
||||
client: string
|
||||
) => Promise<any>);
|
||||
/**
|
||||
* A custom client.
|
||||
*/
|
||||
client?: string;
|
||||
};
|
||||
/**
|
||||
* Allow output javascript files as module source type.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
To run this example you need to install `webpack-dev-server` and run `webpack serve`.
|
||||
|
||||
# example.js
|
||||
|
||||
```javascript
|
||||
const libraries = {
|
||||
react: () => import("react"),
|
||||
acorn: () => import("acorn"),
|
||||
"core-js": () => import("core-js"),
|
||||
lodash: () => import("lodash"),
|
||||
xxhashjs: () => import("xxhashjs"),
|
||||
"all of them": () => import("./all")
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
document.body.style = "font-size: 16pt;";
|
||||
const pre = document.createElement("pre");
|
||||
pre.style = "height: 200px; overflow-y: auto";
|
||||
pre.innerText =
|
||||
"Click on a button to load the library with import(). The first click triggers a lazy compilation of the module.";
|
||||
for (const key of Object.keys(libraries)) {
|
||||
const button = document.createElement("button");
|
||||
const loadFn = libraries[key];
|
||||
button.innerText = key;
|
||||
button.onclick = async () => {
|
||||
pre.innerText = "Loading " + key + "...";
|
||||
const result = await loadFn();
|
||||
pre.innerText = `${key} = {\n ${Object.keys(result).join(",\n ")}\n}`;
|
||||
};
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
const button = document.createElement("button");
|
||||
button.innerText = "Load more...";
|
||||
button.onclick = async () => {
|
||||
pre.innerText = "Loading more...";
|
||||
await import("./more");
|
||||
pre.innerText = "More libraries available.";
|
||||
};
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(pre);
|
||||
};
|
||||
```
|
||||
|
||||
# webpack.config.js
|
||||
|
||||
```javascript
|
||||
const { HotModuleReplacementPlugin } = require("../../");
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
entry: {
|
||||
main: "./example.js"
|
||||
},
|
||||
cache: {
|
||||
type: "filesystem",
|
||||
idleTimeout: 5000
|
||||
},
|
||||
experiments: {
|
||||
lazyCompilation: true
|
||||
},
|
||||
devServer: {
|
||||
hot: true,
|
||||
publicPath: "/dist/"
|
||||
},
|
||||
plugins: [new HotModuleReplacementPlugin()]
|
||||
};
|
||||
```
|
|
@ -0,0 +1,8 @@
|
|||
export * from "react";
|
||||
export * from "react-dom";
|
||||
export * from "acorn";
|
||||
export * from "core-js";
|
||||
export * from "date-fns";
|
||||
export * from "lodash";
|
||||
export * from "lodash-es";
|
||||
export * from "xxhashjs";
|
|
@ -0,0 +1 @@
|
|||
require("../build-common");
|
|
@ -0,0 +1,36 @@
|
|||
const libraries = {
|
||||
react: () => import("react"),
|
||||
acorn: () => import("acorn"),
|
||||
"core-js": () => import("core-js"),
|
||||
lodash: () => import("lodash"),
|
||||
xxhashjs: () => import("xxhashjs"),
|
||||
"all of them": () => import("./all")
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
document.body.style = "font-size: 16pt;";
|
||||
const pre = document.createElement("pre");
|
||||
pre.style = "height: 200px; overflow-y: auto";
|
||||
pre.innerText =
|
||||
"Click on a button to load the library with import(). The first click triggers a lazy compilation of the module.";
|
||||
for (const key of Object.keys(libraries)) {
|
||||
const button = document.createElement("button");
|
||||
const loadFn = libraries[key];
|
||||
button.innerText = key;
|
||||
button.onclick = async () => {
|
||||
pre.innerText = "Loading " + key + "...";
|
||||
const result = await loadFn();
|
||||
pre.innerText = `${key} = {\n ${Object.keys(result).join(",\n ")}\n}`;
|
||||
};
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
const button = document.createElement("button");
|
||||
button.innerText = "Load more...";
|
||||
button.onclick = async () => {
|
||||
pre.innerText = "Loading more...";
|
||||
await import("./more");
|
||||
pre.innerText = "More libraries available.";
|
||||
};
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(pre);
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="dist/main.js"></script>
|
||||
</head>
|
||||
</html>
|
|
@ -0,0 +1,21 @@
|
|||
const libraries = {
|
||||
"react-dom": () => import("react-dom"),
|
||||
"date-fns": () => import("date-fns"),
|
||||
xxhashjs: () => import("xxhashjs"),
|
||||
"lodash-es": () => import("lodash-es")
|
||||
};
|
||||
|
||||
const pre = document.querySelector("pre");
|
||||
for (const key of Object.keys(libraries)) {
|
||||
const button = document.createElement("button");
|
||||
const loadFn = libraries[key];
|
||||
button.innerText = key;
|
||||
button.onclick = async () => {
|
||||
pre.innerText = "Loading " + key + "...";
|
||||
const result = await loadFn();
|
||||
pre.innerText = `${key} = {\n ${Object.keys(result).join(",\n ")}\n}`;
|
||||
};
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
export {};
|
|
@ -0,0 +1,13 @@
|
|||
To run this example you need to install `webpack-dev-server` and run `webpack serve`.
|
||||
|
||||
# example.js
|
||||
|
||||
```javascript
|
||||
_{{example.js}}_
|
||||
```
|
||||
|
||||
# webpack.config.js
|
||||
|
||||
```javascript
|
||||
_{{webpack.config.js}}_
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
const { HotModuleReplacementPlugin } = require("../../");
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
entry: {
|
||||
main: "./example.js"
|
||||
},
|
||||
cache: {
|
||||
type: "filesystem",
|
||||
idleTimeout: 5000
|
||||
},
|
||||
experiments: {
|
||||
lazyCompilation: true
|
||||
},
|
||||
devServer: {
|
||||
hot: true,
|
||||
publicPath: "/dist/"
|
||||
},
|
||||
plugins: [new HotModuleReplacementPlugin()]
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/* global __resourceQuery */
|
||||
|
||||
"use strict";
|
||||
|
||||
if (!module.hot) {
|
||||
throw new Error(
|
||||
"Environment doesn't support lazy compilation (requires Hot Module Replacement enabled)"
|
||||
);
|
||||
}
|
||||
|
||||
var urlBase = decodeURIComponent(__resourceQuery.slice(1));
|
||||
exports.keepAlive = function (key) {
|
||||
var response;
|
||||
require("http")
|
||||
.request(
|
||||
urlBase + key,
|
||||
{
|
||||
agent: false,
|
||||
headers: { accept: "text/event-stream" }
|
||||
},
|
||||
function (res) {
|
||||
response = res;
|
||||
}
|
||||
)
|
||||
.end();
|
||||
return function () {
|
||||
response.destroy();
|
||||
};
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/* global __resourceQuery */
|
||||
|
||||
"use strict";
|
||||
|
||||
if (typeof EventSource !== "function" || !module.hot) {
|
||||
throw new Error(
|
||||
"Environment doesn't support lazy compilation (requires EventSource and Hot Module Replacement enabled)"
|
||||
);
|
||||
}
|
||||
|
||||
var urlBase = decodeURIComponent(__resourceQuery.slice(1));
|
||||
var activeEventSource;
|
||||
var activeKeys = new Map();
|
||||
|
||||
var updateEventSource = function updateEventSource() {
|
||||
if (activeEventSource) activeEventSource.close();
|
||||
activeEventSource = new EventSource(
|
||||
urlBase + Array.from(activeKeys.keys()).join("@")
|
||||
);
|
||||
};
|
||||
|
||||
exports.keepAlive = function (key) {
|
||||
var value = activeKeys.get(key) || 0;
|
||||
activeKeys.set(key, value + 1);
|
||||
if (value === 0) {
|
||||
updateEventSource();
|
||||
}
|
||||
|
||||
return function () {
|
||||
setTimeout(function () {
|
||||
var value = activeKeys.get(key);
|
||||
if (value === 1) {
|
||||
activeKeys.delete(key);
|
||||
updateEventSource();
|
||||
} else {
|
||||
activeKeys.set(key, value - 1);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
};
|
|
@ -168,6 +168,8 @@ class Compiler {
|
|||
invalid: new SyncHook(["filename", "changeTime"]),
|
||||
/** @type {SyncHook<[]>} */
|
||||
watchClose: new SyncHook([]),
|
||||
/** @type {AsyncSeriesHook<[]>} */
|
||||
shutdown: new AsyncSeriesHook([]),
|
||||
|
||||
/** @type {SyncBailHook<[string, string, any[]], true>} */
|
||||
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
|
||||
|
@ -1075,7 +1077,10 @@ ${other}`);
|
|||
* @returns {void}
|
||||
*/
|
||||
close(callback) {
|
||||
this.hooks.shutdown.callAsync(err => {
|
||||
if (err) return callback(err);
|
||||
this.cache.shutdown(callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -244,6 +244,22 @@ class WebpackOptionsApply extends OptionsApply {
|
|||
}).apply(compiler);
|
||||
}
|
||||
|
||||
if (options.experiments.lazyCompilation) {
|
||||
const LazyCompilationPlugin = require("./hmr/LazyCompilationPlugin");
|
||||
new LazyCompilationPlugin({
|
||||
backend:
|
||||
(typeof options.experiments.lazyCompilation === "object" &&
|
||||
options.experiments.lazyCompilation.backend) ||
|
||||
require("./hmr/lazyCompilationBackend"),
|
||||
client:
|
||||
(typeof options.experiments.lazyCompilation === "object" &&
|
||||
options.experiments.lazyCompilation.client) ||
|
||||
`webpack/hot/lazy-compilation-${
|
||||
options.externalsPresets.node ? "node" : "web"
|
||||
}.js`
|
||||
}).apply(compiler);
|
||||
}
|
||||
|
||||
new EntryOptionPlugin().apply(compiler);
|
||||
compiler.hooks.entryOption.call(options.context, options.entry);
|
||||
|
||||
|
|
|
@ -11,7 +11,10 @@ const { formatSize } = require("../SizeFormatHelpers");
|
|||
const LazySet = require("../util/LazySet");
|
||||
const makeSerializable = require("../util/makeSerializable");
|
||||
const memoize = require("../util/memoize");
|
||||
const { createFileSerializer } = require("../util/serialization");
|
||||
const {
|
||||
createFileSerializer,
|
||||
NOT_SERIALIZABLE
|
||||
} = require("../util/serialization");
|
||||
|
||||
/** @typedef {import("../../declarations/WebpackOptions").SnapshotOptions} SnapshotOptions */
|
||||
/** @typedef {import("../Cache").Etag} Etag */
|
||||
|
@ -525,6 +528,7 @@ class PackContentItems {
|
|||
write(value);
|
||||
} catch (e) {
|
||||
rollback(s);
|
||||
if (e === NOT_SERIALIZABLE) continue;
|
||||
logger.warn(
|
||||
`Skipped not serializable cache item '${key}': ${e.message}`
|
||||
);
|
||||
|
|
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
MIT License http://www.opensource.org/licenses/mit-license.php
|
||||
Author Tobias Koppers @sokra
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { RawSource } = require("webpack-sources");
|
||||
const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
|
||||
const Dependency = require("../Dependency");
|
||||
const Module = require("../Module");
|
||||
const ModuleFactory = require("../ModuleFactory");
|
||||
const RuntimeGlobals = require("../RuntimeGlobals");
|
||||
const Template = require("../Template");
|
||||
const CommonJsRequireDependency = require("../dependencies/CommonJsRequireDependency");
|
||||
const { registerNotSerializable } = require("../util/serialization");
|
||||
|
||||
/** @typedef {import("../../declarations/WebpackOptions")} WebpackOptions */
|
||||
/** @typedef {import("../Compilation")} Compilation */
|
||||
/** @typedef {import("../Compiler")} Compiler */
|
||||
/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
|
||||
/** @typedef {import("../Module").BuildMeta} BuildMeta */
|
||||
/** @typedef {import("../Module").CodeGenerationContext} CodeGenerationContext */
|
||||
/** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
|
||||
/** @typedef {import("../Module").LibIdentOptions} LibIdentOptions */
|
||||
/** @typedef {import("../Module").NeedBuildContext} NeedBuildContext */
|
||||
/** @typedef {import("../ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
|
||||
/** @typedef {import("../ModuleFactory").ModuleFactoryResult} ModuleFactoryResult */
|
||||
/** @typedef {import("../RequestShortener")} RequestShortener */
|
||||
/** @typedef {import("../ResolverFactory").ResolverWithOptions} ResolverWithOptions */
|
||||
/** @typedef {import("../WebpackError")} WebpackError */
|
||||
/** @typedef {import("../util/Hash")} Hash */
|
||||
/** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
|
||||
|
||||
const TYPES = new Set(["javascript"]);
|
||||
|
||||
class LazyCompilationDependency extends Dependency {
|
||||
constructor(originalModule) {
|
||||
super();
|
||||
this.originalModule = originalModule;
|
||||
}
|
||||
|
||||
get category() {
|
||||
return "esm";
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "lazy import()";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string | null} an identifier to merge equal requests
|
||||
*/
|
||||
getResourceIdentifier() {
|
||||
return this.originalModule.identifier();
|
||||
}
|
||||
}
|
||||
|
||||
registerNotSerializable(LazyCompilationDependency);
|
||||
|
||||
class LazyCompilationProxyModule extends Module {
|
||||
constructor(originalModule, request, client, data, active) {
|
||||
super(
|
||||
"lazy-compilation-proxy",
|
||||
originalModule.context,
|
||||
originalModule.layer
|
||||
);
|
||||
this.originalModule = originalModule;
|
||||
this.request = request;
|
||||
this.client = client;
|
||||
this.data = data;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} a unique identifier of the module
|
||||
*/
|
||||
identifier() {
|
||||
return `lazy-compilation-proxy|${this.originalModule.identifier()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RequestShortener} requestShortener the request shortener
|
||||
* @returns {string} a user readable identifier of the module
|
||||
*/
|
||||
readableIdentifier(requestShortener) {
|
||||
return `lazy-compilation-proxy ${this.originalModule.readableIdentifier(
|
||||
requestShortener
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assuming this module is in the cache. Update the (cached) module with
|
||||
* the fresh module from the factory. Usually updates internal references
|
||||
* and properties.
|
||||
* @param {Module} module fresh module
|
||||
* @returns {void}
|
||||
*/
|
||||
updateCacheModule(module) {
|
||||
super.updateCacheModule(module);
|
||||
const m = /** @type {LazyCompilationProxyModule} */ (module);
|
||||
this.active = m.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LibIdentOptions} options options
|
||||
* @returns {string | null} an identifier for library inclusion
|
||||
*/
|
||||
libIdent(options) {
|
||||
return `${this.originalModule.libIdent(options)}!lazy-compilation-proxy`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NeedBuildContext} context context info
|
||||
* @param {function(WebpackError=, boolean=): void} callback callback function, returns true, if the module needs a rebuild
|
||||
* @returns {void}
|
||||
*/
|
||||
needBuild(context, callback) {
|
||||
callback(null, !this.buildInfo || this.buildInfo.active !== this.active);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WebpackOptions} options webpack options
|
||||
* @param {Compilation} compilation the compilation
|
||||
* @param {ResolverWithOptions} resolver the resolver
|
||||
* @param {InputFileSystem} fs the file system
|
||||
* @param {function(WebpackError=): void} callback callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
build(options, compilation, resolver, fs, callback) {
|
||||
this.buildInfo = {
|
||||
active: this.active
|
||||
};
|
||||
/** @type {BuildMeta} */
|
||||
this.buildMeta = {};
|
||||
this.clearDependenciesAndBlocks();
|
||||
const dep = new CommonJsRequireDependency(this.client);
|
||||
this.addDependency(dep);
|
||||
if (this.active) {
|
||||
const dep = new LazyCompilationDependency(this.originalModule);
|
||||
const block = new AsyncDependenciesBlock({});
|
||||
block.addDependency(dep);
|
||||
this.addBlock(block);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Set<string>} types available (do not mutate)
|
||||
*/
|
||||
getSourceTypes() {
|
||||
return TYPES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string=} type the source type for which the size should be estimated
|
||||
* @returns {number} the estimated size of the module (must be non-zero)
|
||||
*/
|
||||
size(type) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CodeGenerationContext} context context for code generation
|
||||
* @returns {CodeGenerationResult} result
|
||||
*/
|
||||
codeGeneration({ runtimeTemplate, chunkGraph, moduleGraph }) {
|
||||
const sources = new Map();
|
||||
const runtimeRequirements = new Set();
|
||||
runtimeRequirements.add(RuntimeGlobals.module);
|
||||
const clientDep = /** @type {CommonJsRequireDependency} */ (this
|
||||
.dependencies[0]);
|
||||
const clientModule = moduleGraph.getModule(clientDep);
|
||||
const block = this.blocks[0];
|
||||
const keepActive = Template.asString([
|
||||
`var client = ${runtimeTemplate.moduleExports({
|
||||
module: clientModule,
|
||||
chunkGraph,
|
||||
request: clientDep.userRequest,
|
||||
runtimeRequirements
|
||||
})}`,
|
||||
`var data = ${JSON.stringify(this.data)};`,
|
||||
`var dispose = client.keepAlive(data, ${JSON.stringify(
|
||||
!!block
|
||||
)}, module);`
|
||||
]);
|
||||
let source;
|
||||
if (block) {
|
||||
const dep = block.dependencies[0];
|
||||
const module = moduleGraph.getModule(dep);
|
||||
source = Template.asString([
|
||||
"module.hot.accept();",
|
||||
`module.hot.accept(${JSON.stringify(
|
||||
chunkGraph.getModuleId(module)
|
||||
)}, function() { module.hot.invalidate(); });`,
|
||||
"module.hot.dispose(function(data) { delete data.resolveSelf; dispose(data); });",
|
||||
`module.exports = ${runtimeTemplate.moduleNamespacePromise({
|
||||
chunkGraph,
|
||||
block,
|
||||
module,
|
||||
request: this.request,
|
||||
strict: false, // TODO this should be inherited from the original module
|
||||
message: "import()",
|
||||
runtimeRequirements
|
||||
})};`,
|
||||
"if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports);"
|
||||
]);
|
||||
} else {
|
||||
source = Template.asString([
|
||||
"module.hot.accept();",
|
||||
"var resolveSelf;",
|
||||
`module.exports = new Promise(function(resolve) { resolveSelf = resolve; });`,
|
||||
"if (module.hot.data && module.hot.data.resolveSelf) module.hot.data.resolveSelf(module.exports);",
|
||||
"module.hot.dispose(function(data) { data.resolveSelf = resolveSelf; dispose(data); });"
|
||||
]);
|
||||
}
|
||||
sources.set("javascript", new RawSource(keepActive + "\n\n" + source));
|
||||
return {
|
||||
sources,
|
||||
runtimeRequirements
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Hash} hash the hash used to track dependencies
|
||||
* @param {UpdateHashContext} context context
|
||||
* @returns {void}
|
||||
*/
|
||||
updateHash(hash, context) {
|
||||
super.updateHash(hash, context);
|
||||
hash.update(this.active ? "active" : "");
|
||||
hash.update(JSON.stringify(this.data));
|
||||
}
|
||||
}
|
||||
|
||||
registerNotSerializable(LazyCompilationProxyModule);
|
||||
|
||||
class LazyCompilationDependencyFactory extends ModuleFactory {
|
||||
constructor(factory) {
|
||||
super();
|
||||
this._factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ModuleFactoryCreateData} data data object
|
||||
* @param {function(Error=, ModuleFactoryResult=): void} callback callback
|
||||
* @returns {void}
|
||||
*/
|
||||
create(data, callback) {
|
||||
const dependency = /** @type {LazyCompilationDependency} */ (data
|
||||
.dependencies[0]);
|
||||
callback(null, {
|
||||
module: dependency.originalModule
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class LazyCompilationPlugin {
|
||||
/**
|
||||
* @param {Object} options options
|
||||
* @param {(function(Compiler, string, function(Error?, any?): void): void) | function(Compiler, string): Promise<any>} options.backend the backend
|
||||
* @param {string} options.client the client reference
|
||||
*/
|
||||
constructor({ backend, client }) {
|
||||
this.backend = backend;
|
||||
this.client = client;
|
||||
}
|
||||
/**
|
||||
* Apply the plugin
|
||||
* @param {Compiler} compiler the compiler instance
|
||||
* @returns {void}
|
||||
*/
|
||||
apply(compiler) {
|
||||
let backend;
|
||||
compiler.hooks.beforeCompile.tapAsync(
|
||||
"LazyCompilationPlugin",
|
||||
(params, callback) => {
|
||||
if (backend !== undefined) return callback();
|
||||
const promise = this.backend(compiler, this.client, (err, result) => {
|
||||
if (err) return callback(err);
|
||||
backend = result;
|
||||
callback();
|
||||
});
|
||||
if (promise && promise.then) {
|
||||
promise.then(b => {
|
||||
backend = b;
|
||||
callback();
|
||||
}, callback);
|
||||
}
|
||||
}
|
||||
);
|
||||
compiler.hooks.compilation.tap(
|
||||
"LazyCompilationPlugin",
|
||||
(compilation, { normalModuleFactory }) => {
|
||||
normalModuleFactory.hooks.module.tap(
|
||||
"LazyCompilationPlugin",
|
||||
(originalModule, createData, resolveData) => {
|
||||
if (
|
||||
resolveData.dependencies.every(dep => dep.type === "import()")
|
||||
) {
|
||||
const moduleInfo = backend.module(originalModule);
|
||||
if (!moduleInfo) return;
|
||||
const { client, data, active } = moduleInfo;
|
||||
|
||||
return new LazyCompilationProxyModule(
|
||||
originalModule,
|
||||
resolveData.request,
|
||||
client,
|
||||
data,
|
||||
active
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
compilation.dependencyFactories.set(
|
||||
LazyCompilationDependency,
|
||||
new LazyCompilationDependencyFactory()
|
||||
);
|
||||
}
|
||||
);
|
||||
compiler.hooks.shutdown.tapAsync("LazyCompilationPlugin", callback => {
|
||||
backend.dispose(callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LazyCompilationPlugin;
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
MIT License http://www.opensource.org/licenses/mit-license.php
|
||||
Author Tobias Koppers @sokra
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const http = require("http");
|
||||
|
||||
/** @typedef {import("../Compiler")} Compiler */
|
||||
|
||||
/**
|
||||
* @param {Compiler} compiler compiler
|
||||
* @param {string} client client reference
|
||||
* @param {function(Error?, any?): void} callback callback
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports = (compiler, client, callback) => {
|
||||
const logger = compiler.getInfrastructureLogger("LazyCompilationBackend");
|
||||
const activeModules = new Map();
|
||||
const prefix = "/lazy-compilation-using-";
|
||||
const server = http.createServer((req, res) => {
|
||||
const keys = req.url.slice(prefix.length).split("@");
|
||||
req.socket.on("close", () => {
|
||||
setTimeout(() => {
|
||||
for (const key of keys) {
|
||||
const oldValue = activeModules.get(key) || 0;
|
||||
activeModules.set(key, oldValue - 1);
|
||||
if (oldValue === 1) {
|
||||
logger.log(
|
||||
`${key} is no longer in use. Next compilation will skip this module.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}, 120000);
|
||||
});
|
||||
req.socket.setNoDelay(true);
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/event-stream",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
});
|
||||
res.write("\n");
|
||||
let moduleActivated = false;
|
||||
for (const key of keys) {
|
||||
const oldValue = activeModules.get(key) || 0;
|
||||
activeModules.set(key, oldValue + 1);
|
||||
if (oldValue === 0) {
|
||||
logger.log(`${key} is now in use and will be compiled.`);
|
||||
moduleActivated = true;
|
||||
}
|
||||
}
|
||||
if (moduleActivated && compiler.watching) compiler.watching.invalidate();
|
||||
});
|
||||
server.listen(err => {
|
||||
if (err) return callback(err);
|
||||
const addr = server.address();
|
||||
if (typeof addr === "string") throw new Error("addr must not be a string");
|
||||
const urlBase =
|
||||
addr.address === "::" || addr.address === "0.0.0.0"
|
||||
? `http://localhost:${addr.port}`
|
||||
: addr.family === "IPv6"
|
||||
? `http://[${addr.address}]:${addr.port}`
|
||||
: `http://${addr.address}:${addr.port}`;
|
||||
logger.log(
|
||||
`Server-Sent-Events server for lazy compilation open at ${urlBase}.`
|
||||
);
|
||||
callback(null, {
|
||||
dispose(callback) {
|
||||
server.close(callback);
|
||||
},
|
||||
module(originalModule) {
|
||||
const key = `${encodeURIComponent(
|
||||
originalModule.identifier().replace(/\\/g, "/").replace(/@/g, "_")
|
||||
).replace(/%(2F|3A|24|26|2B|2C|3B|3D|3A)/g, decodeURIComponent)}`;
|
||||
const active = activeModules.get(key) > 0;
|
||||
return {
|
||||
client: `webpack/hot/lazy-compilation-${
|
||||
compiler.options.externalsPresets.node ? "node" : "web"
|
||||
}.js?${encodeURIComponent(urlBase + prefix)}`,
|
||||
data: key,
|
||||
active
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
|
@ -367,12 +367,14 @@ class ObjectMiddleware extends SerializerMiddleware {
|
|||
try {
|
||||
process(value);
|
||||
} catch (e) {
|
||||
if (e !== NOT_SERIALIZABLE) {
|
||||
if (hasDebugInfoAttached === undefined)
|
||||
hasDebugInfoAttached = new WeakSet();
|
||||
if (!hasDebugInfoAttached.has(e)) {
|
||||
e.message += `\nwhile serializing ${stackToString(value)}`;
|
||||
hasDebugInfoAttached.add(e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -581,6 +581,29 @@
|
|||
"description": "Enable module and chunk layers.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"lazyCompilation": {
|
||||
"description": "Compile import()s only when they are accessed.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"backend": {
|
||||
"description": "A custom backend.",
|
||||
"instanceof": "Function",
|
||||
"tsType": "(((compiler: import('../lib/Compiler'), client: string, callback: (err?: Error, api?: any) => void) => void) | ((compiler: import('../lib/Compiler'), client: string) => Promise<any>))"
|
||||
},
|
||||
"client": {
|
||||
"description": "A custom client.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputModule": {
|
||||
"description": "Allow output javascript files as module source type.",
|
||||
"type": "boolean"
|
||||
|
|
|
@ -36,6 +36,11 @@ const describeCases = config => {
|
|||
return;
|
||||
}
|
||||
describe(testName, () => {
|
||||
let compiler;
|
||||
afterAll(callback => {
|
||||
compiler.close(callback);
|
||||
});
|
||||
|
||||
it(
|
||||
testName + " should compile",
|
||||
done => {
|
||||
|
@ -93,7 +98,7 @@ const describeCases = config => {
|
|||
new webpack.LoaderOptionsPlugin(fakeUpdateLoaderOptions)
|
||||
);
|
||||
if (!options.recordsPath) options.recordsPath = recordsPath;
|
||||
const compiler = webpack(options);
|
||||
compiler = webpack(options);
|
||||
compiler.run((err, stats) => {
|
||||
if (err) return done(err);
|
||||
const jsonStats = stats.toJson({
|
||||
|
@ -192,6 +197,7 @@ const describeCases = config => {
|
|||
Worker: require("./helpers/createFakeWorker")({
|
||||
outputDirectory
|
||||
}),
|
||||
EventSource: require("./helpers/EventSourceForNode"),
|
||||
location: {
|
||||
href: "https://test.cases/path/index.html",
|
||||
origin: "https://test.cases",
|
||||
|
@ -243,7 +249,7 @@ const describeCases = config => {
|
|||
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
||||
} else {
|
||||
const fn = vm.runInThisContext(
|
||||
"(function(require, module, exports, __dirname, __filename, it, beforeEach, afterEach, expect, self, window, fetch, document, importScripts, Worker, NEXT, STATS) {" +
|
||||
"(function(require, module, exports, __dirname, __filename, it, beforeEach, afterEach, expect, self, window, fetch, document, importScripts, Worker, EventSource, NEXT, STATS) {" +
|
||||
"global.expect = expect;" +
|
||||
'function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }' +
|
||||
fs.readFileSync(p, "utf-8") +
|
||||
|
@ -270,6 +276,7 @@ const describeCases = config => {
|
|||
window.document,
|
||||
window.importScripts,
|
||||
window.Worker,
|
||||
window.EventSource,
|
||||
_next,
|
||||
jsonStats
|
||||
);
|
||||
|
|
|
@ -367,6 +367,32 @@ Object {
|
|||
"multiple": false,
|
||||
"simpleType": "boolean",
|
||||
},
|
||||
"experiments-lazy-compilation": Object {
|
||||
"configs": Array [
|
||||
Object {
|
||||
"description": "Compile import()s only when they are accessed.",
|
||||
"multiple": false,
|
||||
"path": "experiments.lazyCompilation",
|
||||
"type": "boolean",
|
||||
},
|
||||
],
|
||||
"description": "Compile import()s only when they are accessed.",
|
||||
"multiple": false,
|
||||
"simpleType": "boolean",
|
||||
},
|
||||
"experiments-lazy-compilation-client": Object {
|
||||
"configs": Array [
|
||||
Object {
|
||||
"description": "A custom client.",
|
||||
"multiple": false,
|
||||
"path": "experiments.lazyCompilation.client",
|
||||
"type": "string",
|
||||
},
|
||||
],
|
||||
"description": "A custom client.",
|
||||
"multiple": false,
|
||||
"simpleType": "string",
|
||||
},
|
||||
"experiments-output-module": Object {
|
||||
"configs": Array [
|
||||
Object {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
MIT License http://www.opensource.org/licenses/mit-license.php
|
||||
Author Tobias Koppers @sokra
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
module.exports = class EventSource {
|
||||
constructor(url) {
|
||||
this.response = undefined;
|
||||
require("http")
|
||||
.request(
|
||||
url,
|
||||
{
|
||||
agent: false,
|
||||
headers: { accept: "text/event-stream" }
|
||||
},
|
||||
res => {
|
||||
this.response = res;
|
||||
res.on("error", err => {
|
||||
if (this.onerror) this.onerror(err);
|
||||
});
|
||||
}
|
||||
)
|
||||
.end();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.response.destroy();
|
||||
}
|
||||
|
||||
set onopen(value) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
set onmessage(value) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
it("should compile to lazy imported module", done => {
|
||||
let resolved;
|
||||
const promise = import("./module").then(r => (resolved = r));
|
||||
expect(resolved).toBe(undefined);
|
||||
setTimeout(() => {
|
||||
expect(resolved).toBe(undefined);
|
||||
NEXT(
|
||||
require("../../update")(done, true, () => {
|
||||
promise.then(result => {
|
||||
expect(result).toHaveProperty("default", 42);
|
||||
done();
|
||||
}, done);
|
||||
})
|
||||
);
|
||||
}, 1000);
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export default 42;
|
|
@ -0,0 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
/** @type {import("../../../../").Configuration} */
|
||||
module.exports = {
|
||||
experiments: {
|
||||
lazyCompilation: true
|
||||
}
|
||||
};
|
|
@ -1621,6 +1621,7 @@ declare class Compiler {
|
|||
failed: SyncHook<[Error]>;
|
||||
invalid: SyncHook<[null | string, number]>;
|
||||
watchClose: SyncHook<[]>;
|
||||
shutdown: AsyncSeriesHook<[]>;
|
||||
infrastructureLog: SyncBailHook<[string, string, any[]], true>;
|
||||
environment: SyncHook<[]>;
|
||||
afterEnvironment: SyncHook<[]>;
|
||||
|
@ -2954,6 +2955,28 @@ declare interface Experiments {
|
|||
*/
|
||||
layers?: boolean;
|
||||
|
||||
/**
|
||||
* Compile import()s only when they are accessed.
|
||||
*/
|
||||
lazyCompilation?:
|
||||
| boolean
|
||||
| {
|
||||
/**
|
||||
* A custom backend.
|
||||
*/
|
||||
backend?:
|
||||
| ((
|
||||
compiler: Compiler,
|
||||
client: string,
|
||||
callback: (err?: Error, api?: any) => void
|
||||
) => void)
|
||||
| ((compiler: Compiler, client: string) => Promise<any>);
|
||||
/**
|
||||
* A custom client.
|
||||
*/
|
||||
client?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow output javascript files as module source type.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue