Skip to content

Commit 918fc2a

Browse files
committed
fix(library): track live export writes during initial parsing
1 parent 3c0dd7f commit 918fc2a

18 files changed

Lines changed: 624 additions & 432 deletions
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Sebastian Beltran @bjohansebas
4+
*/
5+
6+
"use strict";
7+
8+
const RuntimeGlobals = require("../../runtime/RuntimeGlobals");
9+
const makeSerializable = require("../../util/makeSerializable");
10+
const NullDependency = require("../core/NullDependency");
11+
12+
/** @import { Range } from "../../javascript/JavascriptParser" */
13+
/** @import { ModuleId } from "../../graph/ChunkGraph" */
14+
/** @import Dependency, { DependencyKinds, UpdateHashContext } from "../../graph/Dependency" */
15+
/** @import Hash from "../../util/Hash" */
16+
/** @import { ReplaceSource } from "webpack-sources" */
17+
/** @import { DependencyTemplateContext } from "../../template/DependencyTemplate" */
18+
/** @typedef {{ id: string, names: string[], range: Range, kind: "expression" | "postfix" | "pattern", shorthand: boolean, defaultRange?: Range, originalName?: string }} BindingWrite */
19+
/** @typedef {{ id: ModuleId | null, name: string }} ExportBinding */
20+
/** @typedef {import("../../serialization/ObjectMiddleware").ObjectSerializerContext<[BindingWrite[]]>} ObjectSerializerContext */
21+
/** @typedef {import("../../serialization/ObjectMiddleware").ObjectDeserializerContext<[BindingWrite[]]>} ObjectDeserializerContext */
22+
23+
class ESMExportBindingDependency extends NullDependency {
24+
/** @param {BindingWrite[]} writes writes */
25+
constructor(writes) {
26+
super();
27+
this.writes = writes;
28+
}
29+
30+
/**
31+
* Updates the hash with the data contributed by this instance.
32+
* @param {Hash} hash hash to be updated
33+
* @param {UpdateHashContext} context context
34+
* @returns {void}
35+
*/
36+
updateHash(hash, { chunkGraph }) {
37+
const { moduleGraph } = chunkGraph;
38+
const module = moduleGraph.getParentModule(this);
39+
const meta = module && moduleGraph.getMetaIfExisting(module);
40+
const bindings = meta && meta.libraryExportBindings;
41+
if (!bindings) {
42+
hash.update("no export bindings");
43+
return;
44+
}
45+
for (const [name, binding] of bindings) {
46+
hash.update(`${name}:${JSON.stringify(binding)}`);
47+
}
48+
}
49+
50+
/**
51+
* Serializes this instance into the provided serializer context.
52+
* @param {ObjectSerializerContext} context context
53+
*/
54+
serialize(context) {
55+
context.write(this.writes);
56+
super.serialize(context);
57+
}
58+
59+
/**
60+
* Restores this instance from the provided deserializer context.
61+
* @param {ObjectDeserializerContext} context context
62+
*/
63+
deserialize(context) {
64+
this.writes = context.read();
65+
super.deserialize(context.rest);
66+
}
67+
}
68+
/** @type {Set<keyof DependencyKinds>} */
69+
ESMExportBindingDependency.KINDS = new Set([
70+
...NullDependency.KINDS,
71+
"esm-export-binding"
72+
]);
73+
74+
makeSerializable(
75+
ESMExportBindingDependency,
76+
"webpack/lib/dependencies/esm/ESMExportBindingDependency"
77+
);
78+
79+
ESMExportBindingDependency.Template = class extends NullDependency.Template {
80+
/**
81+
* Applies the plugin by registering its hooks on the compiler.
82+
* @param {Dependency} dependency the dependency for which the template should be applied
83+
* @param {ReplaceSource} source the current replace source which can be modified
84+
* @param {DependencyTemplateContext} templateContext the context object
85+
* @returns {void}
86+
*/
87+
apply(dependency, source, { module, moduleGraph, runtimeRequirements }) {
88+
const meta = module && moduleGraph.getMetaIfExisting(module);
89+
const bindings = meta && meta.libraryExportBindings;
90+
if (!bindings) return;
91+
const dep = /** @type {ESMExportBindingDependency} */ (dependency);
92+
/** @type {Map<number, string[]>} */
93+
const endings = new Map();
94+
/**
95+
* @param {number} position end
96+
* @param {string} text suffix
97+
* @returns {void}
98+
*/
99+
const end = (position, text) => {
100+
const existing = endings.get(position);
101+
if (existing) existing.unshift(text);
102+
else endings.set(position, [text]);
103+
};
104+
// Visit outer writes first so shared end positions close the inner one first.
105+
for (const write of dep.writes) {
106+
const binding = bindings.get(write.id);
107+
if (!binding) continue;
108+
runtimeRequirements.add(RuntimeGlobals.exportBinding);
109+
runtimeRequirements.add(RuntimeGlobals.requireScope);
110+
const call = `${RuntimeGlobals.exportBinding}(${JSON.stringify(binding.id)}, ${JSON.stringify(binding.name)}, `;
111+
if (write.kind !== "pattern") {
112+
source.insert(write.range[0], call);
113+
end(
114+
write.range[1],
115+
write.kind === "postfix" ? `, false, ${write.id})` : ")"
116+
);
117+
continue;
118+
}
119+
const value =
120+
write.id === "__webpack_value__"
121+
? "__webpack_value_1__"
122+
: "__webpack_value__";
123+
source.replace(
124+
write.range[0],
125+
write.range[1] - 1,
126+
`${write.shorthand ? `${write.originalName || write.id}: ` : ""}({ set value(${value}) { ${write.id} = ${value}; ${call}${value}); } }).value`
127+
);
128+
if (write.defaultRange) {
129+
const key = JSON.stringify(write.originalName || write.id);
130+
source.insert(
131+
write.defaultRange[0],
132+
`({${(write.originalName || write.id) === "__proto__" ? `[${key}]` : key}: `
133+
);
134+
end(write.defaultRange[1], `})[${key}]`);
135+
}
136+
}
137+
for (const [position, parts] of endings) {
138+
source.insert(position, parts.join(""));
139+
}
140+
}
141+
};
142+
143+
module.exports = ESMExportBindingDependency;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Sebastian Beltran @bjohansebas
4+
*/
5+
6+
"use strict";
7+
8+
const CompatibilityPlugin = require("../../javascript/CompatibilityPlugin");
9+
const ESMExportBindingDependency = require("./ESMExportBindingDependency");
10+
const { harmonySpecifierTag } = require("./ESMImportDependencyParserPlugin");
11+
12+
/** @import JavascriptParser, { Range, WriteStatement } from "../../javascript/JavascriptParser" */
13+
/** @import { Expression, Pattern, AssignmentExpression, Node } from "estree" */
14+
/** @import { BindingWrite } from "./ESMExportBindingDependency" */
15+
16+
const PLUGIN_NAME = "ESMExportBindingParserPlugin";
17+
const bindingTag = Symbol(PLUGIN_NAME);
18+
19+
class ESMExportBindingParserPlugin {
20+
/**
21+
* @param {JavascriptParser} parser parser
22+
* @returns {void}
23+
*/
24+
apply(parser) {
25+
/** @type {ESMExportBindingDependency | undefined} */
26+
let dependency;
27+
/** @type {WeakSet<Node>} */
28+
let seen;
29+
parser.hooks.program.tap(PLUGIN_NAME, () => {
30+
dependency = undefined;
31+
seen = new WeakSet();
32+
});
33+
/**
34+
* @param {string} id local name
35+
* @param {string} name export name
36+
* @returns {void}
37+
*/
38+
const tag = (id, name) => {
39+
if (parser.getTagData(id, harmonySpecifierTag)) return;
40+
const names = /** @type {string[] | undefined} */ (
41+
parser.getTagData(id, bindingTag)
42+
);
43+
if (names) names.push(name);
44+
else parser.tagVariable(id, bindingTag, [name]);
45+
};
46+
parser.hooks.exportSpecifier.tap(
47+
{ name: PLUGIN_NAME, stage: -100 },
48+
(_statement, id, name) => {
49+
tag(id, name);
50+
}
51+
);
52+
parser.hooks.exportExpression.tap(
53+
{ name: PLUGIN_NAME, stage: -100 },
54+
(_statement, expression) => {
55+
if (
56+
(expression.type === "FunctionDeclaration" ||
57+
expression.type === "ClassDeclaration") &&
58+
expression.id
59+
) {
60+
tag(expression.id.name, "default");
61+
}
62+
}
63+
);
64+
/**
65+
* @param {BindingWrite} write write
66+
* @returns {void}
67+
*/
68+
const add = (write) => {
69+
const renamed =
70+
/** @type {import("../../javascript/CompatibilityPlugin").CompatibilitySettings | undefined} */ (
71+
parser.getTagData(
72+
write.id,
73+
CompatibilityPlugin.nestedWebpackIdentifierTag
74+
)
75+
);
76+
if (renamed) {
77+
write.originalName = write.id;
78+
write.id = renamed.name;
79+
}
80+
if (!dependency) {
81+
dependency = new ESMExportBindingDependency([]);
82+
parser.state.module.addDependency(dependency);
83+
}
84+
dependency.writes.push(write);
85+
};
86+
/**
87+
* @param {Pattern | Expression} target target
88+
* @param {boolean=} shorthand shorthand
89+
* @param {Range=} defaultRange anonymous default
90+
* @returns {void}
91+
*/
92+
const pattern = (target, shorthand = false, defaultRange = undefined) => {
93+
switch (target.type) {
94+
case "Identifier": {
95+
const names = /** @type {string[] | undefined} */ (
96+
parser.getTagData(target.name, bindingTag)
97+
);
98+
if (names) {
99+
add({
100+
id: target.name,
101+
names,
102+
range: /** @type {Range} */ (target.range),
103+
kind: "pattern",
104+
shorthand,
105+
defaultRange
106+
});
107+
}
108+
break;
109+
}
110+
case "ArrayPattern":
111+
for (const element of target.elements) if (element) pattern(element);
112+
break;
113+
case "ObjectPattern":
114+
for (const property of target.properties) {
115+
if (property.type === "RestElement") pattern(property.argument);
116+
else pattern(property.value, property.shorthand);
117+
}
118+
break;
119+
case "RestElement":
120+
pattern(target.argument);
121+
break;
122+
case "AssignmentPattern": {
123+
const value = target.right;
124+
const anonymous =
125+
value.type === "ArrowFunctionExpression" ||
126+
((value.type === "FunctionExpression" ||
127+
value.type === "ClassExpression") &&
128+
!value.id);
129+
pattern(
130+
target.left,
131+
shorthand,
132+
anonymous ? /** @type {Range} */ (value.range) : undefined
133+
);
134+
break;
135+
}
136+
}
137+
};
138+
/**
139+
* @param {AssignmentExpression | WriteStatement} expression write
140+
* @returns {void}
141+
*/
142+
const record = (expression) => {
143+
if (seen.has(expression)) return;
144+
seen.add(expression);
145+
const target =
146+
expression.type === "UpdateExpression"
147+
? expression.argument
148+
: expression.left;
149+
if (target.type === "VariableDeclaration") return;
150+
if (
151+
target.type === "Identifier" &&
152+
(expression.type === "AssignmentExpression" ||
153+
expression.type === "UpdateExpression")
154+
) {
155+
const names = /** @type {string[] | undefined} */ (
156+
parser.getTagData(target.name, bindingTag)
157+
);
158+
if (names) {
159+
add({
160+
id: target.name,
161+
names,
162+
range: /** @type {Range} */ (expression.range),
163+
kind:
164+
expression.type === "UpdateExpression" && !expression.prefix
165+
? "postfix"
166+
: "expression",
167+
shorthand: false
168+
});
169+
}
170+
} else {
171+
pattern(target);
172+
}
173+
};
174+
parser.hooks.finish.tap(PLUGIN_NAME, () => {
175+
if (dependency) dependency.writes.sort((a, b) => a.range[0] - b.range[0]);
176+
});
177+
parser.hooks.assignment.tap(PLUGIN_NAME, record);
178+
parser.hooks.assign.for(bindingTag).tap(PLUGIN_NAME, (expression) => {
179+
// A destructuring write must not redefine an outer binding in this scope.
180+
if (expression.left.type.endsWith("Pattern")) return true;
181+
});
182+
parser.hooks.write.for(bindingTag).tap(PLUGIN_NAME, (expression) => {
183+
record(expression);
184+
});
185+
}
186+
}
187+
188+
module.exports = ESMExportBindingParserPlugin;

‎lib/dependencies/esm/ESMExportInitFragment.js‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,20 +251,17 @@ class ESMExportInitFragment extends InitFragment {
251251
)}\n/* harmony export */ });\n`;
252252
}
253253

254-
if (
255-
getData !== undefined &&
256-
(this.onDemand || context.runtimeTemplate.isModule())
257-
) {
254+
if (this.onDemand && getData !== undefined) {
258255
const data = getData();
259256
/** @type {Record<string, string>} */
260-
const finalNames = data.get("exportsFinalName") || {};
257+
const finalNames = {};
261258
for (const [key, { finalName }] of this.exportMap) {
262259
if (finalName !== undefined) {
263260
finalNames[/** @type {string} */ (key)] = finalName;
264261
}
265262
}
266263
data.set("exportsFinalName", finalNames);
267-
if (this.onDemand && definePart) {
264+
if (definePart) {
268265
const before = data.get("exportsSource");
269266
data.set(
270267
"exportsSource",
@@ -295,9 +292,7 @@ class ESMExportInitFragment extends InitFragment {
295292
runtimeRequirements.add(RuntimeGlobals.definePropertyGettersFromArray);
296293
definePart = `/* harmony export */ ${
297294
RuntimeGlobals.definePropertyGetters
298-
}(${this.exportsArgument}, [${this._defsValue.join(
299-
","
300-
)}\n/* harmony export */ ]);\n`;
295+
}(${this.exportsArgument}, [${this._defsValue.join(",")}\n/* harmony export */ ]);\n`;
301296

302297
if (this.onDemand && getData !== undefined) {
303298
const data = getData();

0 commit comments

Comments
 (0)