support import.meta.hot

This commit is contained in:
Ivan Kopeykin 2020-06-22 18:49:57 +03:00
parent 8aaff95a1b
commit 104845a419
20 changed files with 317 additions and 90 deletions

View File

@ -4,6 +4,7 @@
"words": [
"webpack",
"webpack's",
"endregion",
"entrypoint",
"entrypoints",
"splitted",

View File

@ -12,9 +12,11 @@ const Compilation = require("./Compilation");
const HotUpdateChunk = require("./HotUpdateChunk");
const NormalModule = require("./NormalModule");
const RuntimeGlobals = require("./RuntimeGlobals");
const HMRApiDependency = require("./dependencies/HMRApiDependency");
const ImportMetaHotAcceptDependency = require("./dependencies/ImportMetaHotAcceptDependency");
const ImportMetaHotDeclineDependency = require("./dependencies/ImportMetaHotDeclineDependency");
const ModuleHotAcceptDependency = require("./dependencies/ModuleHotAcceptDependency");
const ModuleHotDeclineDependency = require("./dependencies/ModuleHotDeclineDependency");
const ModuleHotDependency = require("./dependencies/ModuleHotDependency");
const HotModuleReplacementRuntimeModule = require("./hmr/HotModuleReplacementRuntimeModule");
const JavascriptParser = require("./javascript/JavascriptParser");
const {
@ -82,12 +84,83 @@ class HotModuleReplacementPlugin {
}
);
const addParserPlugins = (parser, parserOptions) => {
const createAcceptHandler = (parser, paramDependency) => {
const {
hotAcceptCallback,
hotAcceptWithoutCallback
} = HotModuleReplacementPlugin.getParserHooks(parser);
return expr => {
const dep = new HMRApiDependency(expr.callee.range, "accept");
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
if (expr.arguments.length >= 1) {
const arg = parser.evaluateExpression(expr.arguments[0]);
let params = [];
let requests = [];
if (arg.isString()) {
params = [arg];
} else if (arg.isArray()) {
params = arg.items.filter(param => param.isString());
}
if (params.length > 0) {
params.forEach((param, idx) => {
const request = param.string;
const dep = new paramDependency(request, param.range);
dep.optional = true;
dep.loc = Object.create(expr.loc);
dep.loc.index = idx;
parser.state.module.addDependency(dep);
requests.push(request);
});
if (expr.arguments.length > 1) {
hotAcceptCallback.call(expr.arguments[1], requests);
parser.walkExpression(expr.arguments[1]); // other args are ignored
return true;
} else {
hotAcceptWithoutCallback.call(expr, requests);
return true;
}
}
}
parser.walkExpressions(expr.arguments);
return true;
};
};
const createDeclineHandler = (parser, paramDependency) => expr => {
const dep = new HMRApiDependency(expr.callee.range, "decline");
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
if (expr.arguments.length === 1) {
const arg = parser.evaluateExpression(expr.arguments[0]);
let params = [];
if (arg.isString()) {
params = [arg];
} else if (arg.isArray()) {
params = arg.items.filter(param => param.isString());
}
params.forEach((param, idx) => {
const dep = new paramDependency(param.string, param.range);
dep.optional = true;
dep.loc = Object.create(expr.loc);
dep.loc.index = idx;
parser.state.module.addDependency(dep);
});
}
return true;
};
const createHMRExpressionHandler = parser => expr => {
const dep = new HMRApiDependency(expr.range);
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
return true;
};
const addParserPlugins = (parser, parserOptions) => {
const hmrExpressionHandler = createHMRExpressionHandler(parser);
parser.hooks.expression
.for("__webpack_hash__")
.tap(
@ -99,6 +172,8 @@ class HotModuleReplacementPlugin {
parser.hooks.evaluateTypeof
.for("__webpack_hash__")
.tap("HotModuleReplacementPlugin", evaluateToString("string"));
//#region module.hot.* API
parser.hooks.evaluateIdentifier.for("module.hot").tap(
{
name: "HotModuleReplacementPlugin",
@ -115,77 +190,48 @@ class HotModuleReplacementPlugin {
);
parser.hooks.call
.for("module.hot.accept")
.tap("HotModuleReplacementPlugin", expr => {
const dep = new ModuleHotDependency(expr.callee.range, "accept");
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
if (expr.arguments.length >= 1) {
const arg = parser.evaluateExpression(expr.arguments[0]);
let params = [];
let requests = [];
if (arg.isString()) {
params = [arg];
} else if (arg.isArray()) {
params = arg.items.filter(param => param.isString());
}
if (params.length > 0) {
params.forEach((param, idx) => {
const request = param.string;
const dep = new ModuleHotAcceptDependency(request, param.range);
dep.optional = true;
dep.loc = Object.create(expr.loc);
dep.loc.index = idx;
parser.state.module.addDependency(dep);
requests.push(request);
});
if (expr.arguments.length > 1) {
hotAcceptCallback.call(expr.arguments[1], requests);
parser.walkExpression(expr.arguments[1]); // other args are ignored
return true;
} else {
hotAcceptWithoutCallback.call(expr, requests);
return true;
}
}
}
parser.walkExpressions(expr.arguments);
return true;
});
.tap(
"HotModuleReplacementPlugin",
createAcceptHandler(parser, ModuleHotAcceptDependency)
);
parser.hooks.call
.for("module.hot.decline")
.tap("HotModuleReplacementPlugin", expr => {
const dep = new ModuleHotDependency(expr.callee.range, "decline");
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
if (expr.arguments.length === 1) {
const arg = parser.evaluateExpression(expr.arguments[0]);
let params = [];
if (arg.isString()) {
params = [arg];
} else if (arg.isArray()) {
params = arg.items.filter(param => param.isString());
}
params.forEach((param, idx) => {
const dep = new ModuleHotDeclineDependency(
param.string,
param.range
);
dep.optional = true;
dep.loc = Object.create(expr.loc);
dep.loc.index = idx;
parser.state.module.addDependency(dep);
});
}
return true;
});
.tap(
"HotModuleReplacementPlugin",
createDeclineHandler(parser, ModuleHotDeclineDependency)
);
parser.hooks.expression
.for("module.hot")
.tap("HotModuleReplacementPlugin", hmrExpressionHandler);
//#endregion
//#region import.meta.hot.* API
parser.hooks.evaluateIdentifier
.for("import.meta.hot")
.tap("HotModuleReplacementPlugin", expr => {
const dep = new ModuleHotDependency(expr.range);
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
return true;
return evaluateToIdentifier(
"import.meta.hot",
"import.meta",
() => ["hot"],
true
)(expr);
});
parser.hooks.call
.for("import.meta.hot.accept")
.tap(
"HotModuleReplacementPlugin",
createAcceptHandler(parser, ImportMetaHotAcceptDependency)
);
parser.hooks.call
.for("import.meta.hot.decline")
.tap(
"HotModuleReplacementPlugin",
createDeclineHandler(parser, ImportMetaHotDeclineDependency)
);
parser.hooks.expression
.for("import.meta.hot")
.tap("HotModuleReplacementPlugin", hmrExpressionHandler);
//#endregion
};
compiler.hooks.compilation.tap(
@ -195,6 +241,7 @@ class HotModuleReplacementPlugin {
// It should not affect child compilations
if (compilation.compiler !== compiler) return;
//#region module.hot.* API
compilation.dependencyFactories.set(
ModuleHotAcceptDependency,
normalModuleFactory
@ -203,7 +250,6 @@ class HotModuleReplacementPlugin {
ModuleHotAcceptDependency,
new ModuleHotAcceptDependency.Template()
);
compilation.dependencyFactories.set(
ModuleHotDeclineDependency,
normalModuleFactory
@ -212,10 +258,30 @@ class HotModuleReplacementPlugin {
ModuleHotDeclineDependency,
new ModuleHotDeclineDependency.Template()
);
//#endregion
//#region import.meta.hot.* API
compilation.dependencyFactories.set(
ImportMetaHotAcceptDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
ImportMetaHotAcceptDependency,
new ImportMetaHotAcceptDependency.Template()
);
compilation.dependencyFactories.set(
ImportMetaHotDeclineDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
ImportMetaHotDeclineDependency,
new ImportMetaHotDeclineDependency.Template()
);
//#endregion
compilation.dependencyTemplates.set(
ModuleHotDependency,
new ModuleHotDependency.Template()
HMRApiDependency,
new HMRApiDependency.Template()
);
compilation.hooks.record.tap(

View File

@ -13,7 +13,7 @@ const NullDependency = require("./NullDependency");
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
class ModuleHotDependency extends NullDependency {
class HMRApiDependency extends NullDependency {
constructor(range, apiMethod) {
super();
@ -22,7 +22,7 @@ class ModuleHotDependency extends NullDependency {
}
get type() {
return "module.hot";
return "module.hot and import.meta.hot";
}
serialize(context) {
@ -44,12 +44,9 @@ class ModuleHotDependency extends NullDependency {
}
}
makeSerializable(
ModuleHotDependency,
"webpack/lib/dependencies/ModuleHotDependency"
);
makeSerializable(HMRApiDependency, "webpack/lib/dependencies/HMRApiDependency");
ModuleHotDependency.Template = class ModuleHotDependencyTemplate extends NullDependency.Template {
HMRApiDependency.Template = class HMRApiDependencyTemplate extends NullDependency.Template {
/**
* @param {Dependency} dependency the dependency for which the template should be applied
* @param {ReplaceSource} source the current replace source which can be modified
@ -57,7 +54,7 @@ ModuleHotDependency.Template = class ModuleHotDependencyTemplate extends NullDep
* @returns {void}
*/
apply(dependency, source, { module, runtimeRequirements }) {
const dep = /** @type {ModuleHotDependency} */ (dependency);
const dep = /** @type {HMRApiDependency} */ (dependency);
runtimeRequirements.add(RuntimeGlobals.module);
source.replace(
dep.range[0],
@ -67,4 +64,4 @@ ModuleHotDependency.Template = class ModuleHotDependencyTemplate extends NullDep
}
};
module.exports = ModuleHotDependency;
module.exports = HMRApiDependency;

View File

@ -0,0 +1,35 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const makeSerializable = require("../util/makeSerializable");
const ModuleDependency = require("./ModuleDependency");
const ModuleDependencyTemplateAsId = require("./ModuleDependencyTemplateAsId");
class ImportMetaHotAcceptDependency extends ModuleDependency {
constructor(request, range) {
super(request);
this.range = range;
this.weak = true;
}
get type() {
return "import.meta.hot.accept";
}
get category() {
return "esm";
}
}
makeSerializable(
ImportMetaHotAcceptDependency,
"webpack/lib/dependencies/ImportMetaHotAcceptDependency"
);
ImportMetaHotAcceptDependency.Template = ModuleDependencyTemplateAsId;
module.exports = ImportMetaHotAcceptDependency;

View File

@ -0,0 +1,36 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const makeSerializable = require("../util/makeSerializable");
const ModuleDependency = require("./ModuleDependency");
const ModuleDependencyTemplateAsId = require("./ModuleDependencyTemplateAsId");
class ImportMetaHotDeclineDependency extends ModuleDependency {
constructor(request, range) {
super(request);
this.range = range;
this.weak = true;
}
get type() {
return "import.meta.hot.decline";
}
get category() {
return "esm";
}
}
makeSerializable(
ImportMetaHotDeclineDependency,
"webpack/lib/dependencies/ImportMetaHotDeclineDependency"
);
ImportMetaHotDeclineDependency.Template = ModuleDependencyTemplateAsId;
module.exports = ImportMetaHotDeclineDependency;

View File

@ -30,6 +30,7 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
/** @typedef {import("estree").Literal} LiteralNode */
/** @typedef {import("estree").LogicalExpression} LogicalExpressionNode */
/** @typedef {import("estree").MemberExpression} MemberExpressionNode */
/** @typedef {import("estree").MetaProperty} MetaPropertyNode */
/** @typedef {import("estree").MethodDefinition} MethodDefinitionNode */
/** @typedef {import("estree").ModuleDeclaration} ModuleDeclarationNode */
/** @typedef {import("estree").Node} AnyNode */
@ -105,6 +106,8 @@ const getRootName = expression => {
return expression.name;
case "ThisExpression":
return "this";
case "MetaProperty":
return "import.meta";
default:
return undefined;
}
@ -3157,6 +3160,7 @@ class JavascriptParser extends Parser {
};
}
case "Identifier":
case "MetaProperty":
case "ThisExpression": {
if (!possibleTypes.has("expression")) return undefined;
const rootName = getRootName(object);

View File

@ -108,8 +108,12 @@ module.exports = {
require("../dependencies/ModuleHotAcceptDependency"),
"dependencies/ModuleHotDeclineDependency": () =>
require("../dependencies/ModuleHotDeclineDependency"),
"dependencies/ModuleHotDependency": () =>
require("../dependencies/ModuleHotDependency"),
"dependencies/ImportMetaHotAcceptDependency": () =>
require("../dependencies/ImportMetaHotAcceptDependency"),
"dependencies/ImportMetaHotDeclineDependency": () =>
require("../dependencies/ImportMetaHotDeclineDependency"),
"dependencies/HMRApiDependency": () =>
require("../dependencies/HMRApiDependency"),
"dependencies/ProvidedDependency": () =>
require("../dependencies/ProvidedDependency"),
"dependencies/PureExpressionDependency": () =>

View File

@ -1,3 +1,5 @@
export var value = 1;
---
export var value = 2;
---
export var value = 3;

View File

@ -1,4 +1,4 @@
it("should import a changed chunk", (done) => {
it("should import a changed chunk using module.hot.accept API", (done) => {
import("./chunk").then((chunk) => {
expect(chunk.value).toBe(1);
import("./chunk2").then((chunk2) => {
@ -16,3 +16,22 @@ it("should import a changed chunk", (done) => {
}).catch(done);
}).catch(done);
});
it("should import a changed chunk using import.meta.hot.accept API", (done) => {
import("./chunk").then((chunk) => {
expect(chunk.value).toBe(2);
import("./chunk2").then((chunk2) => {
expect(chunk2.value).toBe(2);
NEXT(require("../../update")(done));
import.meta.hot.accept(["./chunk", "./chunk2"], () => {
import("./chunk").then((chunk) => {
expect(chunk.value).toBe(3);
import("./chunk2").then((chunk2) => {
expect(chunk2.value).toBe(3);
done();
}).catch(done);
}).catch(done);
});
}).catch(done);
}).catch(done);
});

View File

@ -1,6 +1,6 @@
import vendor from "vendor";
module.hot.accept("vendor");
it("should hot update a splitted initial chunk", function (done) {
import.meta.hot.accept("vendor");
it("should hot update a splitted initial chunk using import.meta.hot.* API", function (done) {
expect(vendor).toBe("1");
NEXT(
require("../../update")(done, true, () => {
@ -9,3 +9,14 @@ it("should hot update a splitted initial chunk", function (done) {
})
);
});
it("should hot update a splitted initial chunk using module.hot.* API", function (done) {
expect(vendor).toBe("2");
module.hot.accept("vendor");
NEXT(
require("../../update")(done, true, () => {
expect(vendor).toBe("3");
done();
})
);
});

View File

@ -1,3 +1,5 @@
module.exports = "1";
---
module.exports = "2";
module.exports = "2";
---
module.exports = "3";

View File

@ -1,8 +1,8 @@
import x from "./module";
it("should have correct this context in accept handler", (done) => {
it("should have correct this context in module.hot.accept handler", (done) => {
expect(x).toEqual("ok1");
(function() {
module.hot.accept("./module", () => {
expect(x).toEqual("ok2");
@ -13,3 +13,17 @@ it("should have correct this context in accept handler", (done) => {
NEXT(require("../../update")(done));
});
it("should have correct this context in import.meta.hot.accept handler", (done) => {
expect(x).toEqual("ok2");
(function() {
import.meta.hot.accept("./module", () => {
expect(x).toEqual("ok3");
expect(this).toEqual({ ok: true });
done();
});
}).call({ ok: true });
NEXT(require("../../update")(done));
});

View File

@ -1,3 +1,5 @@
export default "ok1";
---
export default "ok2";
---
export default "ok3";

View File

@ -2,7 +2,7 @@ import b from "./b";
export default b;
if(module.hot) {
module.hot.decline("./b");
module.hot.accept();
if(import.meta.hot) {
import.meta.hot.decline("./b");
import.meta.hot.accept();
}

View File

@ -0,0 +1,14 @@
import {val} from "./module";
it("should fail import a changed chunk using module.hot.accept API", (done) => {
expect(val).toBe(1);
NEXT(require("../../update")((err) => {
try {
expect(err.message).toMatch(/Aborted because \.\/node_modules\/dep1\/file.js is not accepted/);
expect(err.message).toMatch(/Update propagation: \.\/node_modules\/dep1\/file.js -> \.\/node_modules\/dep1\/exports\.js -> \.\/module\.js -> \.\/index.js/);
done();
} catch(e) {
done(e);
}
}));
});

View File

@ -0,0 +1,5 @@
import {value} from "dep1";
export const val = value;
module.hot.accept("dep1");

View File

@ -0,0 +1 @@
export {value} from "./file";

View File

@ -0,0 +1,3 @@
export var value = 1;
---
export var value = 2;

View File

@ -0,0 +1,5 @@
(() => {
throw new Error("should not resolve");
})();
export default 1;

View File

@ -0,0 +1,6 @@
{
"exports": {
"import": "./exports.js",
"default": "./main.js"
}
}