Skip to content

Commit 5928218

Browse files
fix(esm): preserve import.meta alias mutations (#22618)
* fix(esm): preserve import.meta alias mutations * fix(esm): preserve shared import.meta properties * fix(esm): keep every unknown import.meta access on the module's own object Direct accesses read and wrote the bundle's shared import.meta unless an alias had written the name first, so direct writes leaked into other modules and a value an alias deleted came back. Route them all to the module's object, created once per parse. * test: build the import.meta alias configurations without flatMap Node 10 has no Array.prototype.flatMap, so the case's config threw before any build there. --------- Co-authored-by: alexander-akait <[email protected]>
1 parent d80f612 commit 5928218

6 files changed

Lines changed: 196 additions & 73 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Keep unknown `import.meta` properties per module, alike for aliases and direct use.

‎lib/dependencies/import-meta/ImportMetaPlugin.js‎

Lines changed: 93 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ const WEBPACK_VERSION = Number.parseInt(
6565
* @import Parser, {
6666
* Range,
6767
* Members,
68-
* DestructuringAssignmentProperty
68+
* DestructuringAssignmentProperty,
69+
* JavascriptParserState
6970
* } from "../../javascript/JavascriptParser"
7071
*/
7172
/** @import { RawRuntimeRequirements } from "../core/NullDependency" */
@@ -191,6 +192,12 @@ const importMetaVarName = (parser) => {
191192
return `__webpack_import_meta_${index}__`;
192193
};
193194

195+
/**
196+
* The binding of each parse's synthesized `import.meta`, so it is set up once per module.
197+
* @type {WeakMap<JavascriptParserState, string>}
198+
*/
199+
const importMetaObjects = new WeakMap();
200+
194201
/**
195202
* Checks whether the callee is the `import.meta.resolve` member.
196203
* @param {Expression | Super} callee call callee
@@ -407,12 +414,86 @@ class ImportMetaPlugin {
407414
JSON.stringify(getUrl(parser.state.module));
408415
const importMetaWebpackVersion = () =>
409416
JSON.stringify(WEBPACK_VERSION);
417+
/**
418+
* Initializes the module's shared import.meta object on demand.
419+
* @param {Expression} expression expression using the object
420+
* @returns {string} the object's local binding
421+
*/
422+
const getImportMetaObject = (expression) => {
423+
const existing = importMetaObjects.get(parser.state);
424+
if (existing !== undefined) return existing;
425+
const varName = importMetaVarName(parser);
426+
const knownProps = [];
427+
if (isImportMetaFieldEnabled(importMeta, "url")) {
428+
knownProps.push(`url: ${importMetaUrl()}`);
429+
}
430+
if (isImportMetaFieldEnabled(importMeta, "webpack")) {
431+
knownProps.push(`webpack: ${importMetaWebpackVersion()}`);
432+
}
433+
if (isImportMetaFieldEnabled(importMeta, "main")) {
434+
knownProps.push(`main: ${renderImportMetaMain(parser, false)}`);
435+
}
436+
if (isImportMetaFieldEnabled(importMeta, "env")) {
437+
addDefinitionValueDependencies();
438+
knownProps.push(
439+
`env: ${renderDefinition(envNode, IMPORT_META_ENV)}`
440+
);
441+
}
442+
for (const [prop, node] of customProps) {
443+
addDefinitionValueDependencies();
444+
knownProps.push(
445+
`${DefinePlugin.toPropertyKey(prop)}: ${renderDefinition(
446+
node,
447+
`${IMPORT_META}.${prop}`
448+
)}`
449+
);
450+
}
451+
const plainInitCode = `var ${varName} = {${knownProps.join(
452+
", "
453+
)}};\n`;
454+
// Each module owns its `import.meta`: it copies the bundle's one
455+
// and inherits from it, so later host writes stay visible.
456+
const initCode = preserveUnknown(importMeta)
457+
? `var ${varName} = Object.assign(Object.create(import.meta), import.meta, {${knownProps.join(
458+
", "
459+
)}});\n`
460+
: plainInitCode;
461+
/** @type {RawRuntimeRequirements} */
462+
const initRuntimeRequirements = [
463+
RuntimeGlobals.moduleCache,
464+
RuntimeGlobals.entryModuleId,
465+
RuntimeGlobals.module
466+
];
467+
const initCodeRequirements =
468+
DefinePlugin.getRuntimeRequirements(initCode);
469+
if (initCodeRequirements) {
470+
initRuntimeRequirements.push(...initCodeRequirements);
471+
}
472+
const initDep = new ModuleInitFragmentDependency(
473+
initCode,
474+
initRuntimeRequirements,
475+
varName,
476+
// native `import.meta` does not parse in the build-time vm
477+
initCode === plainInitCode ? undefined : plainInitCode
478+
);
479+
initDep.loc = parser.getLocation(expression);
480+
parser.state.module.addPresentationalDependency(initDep);
481+
importMetaObjects.set(parser.state, varName);
482+
return varName;
483+
};
410484
/**
411485
* Import meta unknown property.
412486
* @param {Members} members members
413-
* @returns {string} error message
487+
* @param {Expression} expression expression using the property
488+
* @returns {string} property access
414489
*/
415-
const importMetaUnknownProperty = (members) => {
490+
const importMetaUnknownProperty = (members, expression) => {
491+
if (
492+
preserveUnknown(importMeta) &&
493+
!IMPORT_META_FIELDS.has(members[0])
494+
) {
495+
return `${getImportMetaObject(expression)}${propertyAccess(members, 0)}`;
496+
}
416497
if (
417498
preserveUnknown(importMeta) ||
418499
(IMPORT_META_FIELDS.has(members[0]) &&
@@ -426,12 +507,11 @@ class ImportMetaPlugin {
426507
};
427508
/**
428509
* @param {string} field field name
510+
* @param {Expression} expression expression using the property
429511
* @returns {string} generated property
430512
*/
431-
const importMetaRuntimeProperty = (field) =>
432-
`[${JSON.stringify(field)}]: ${importMetaUnknownProperty([
433-
field
434-
])},`;
513+
const importMetaRuntimeProperty = (field, expression) =>
514+
`[${JSON.stringify(field)}]: ${importMetaUnknownProperty([field], expression)},`;
435515
/**
436516
* Import meta runtime property for build-time execution.
437517
* @param {string} field field name
@@ -472,64 +552,7 @@ class ImportMetaPlugin {
472552
const referencedPropertiesInDestructuring =
473553
parser.destructuringAssignmentPropertiesFor(metaProperty);
474554
if (!referencedPropertiesInDestructuring) {
475-
const varName = importMetaVarName(parser);
476-
const knownProps = [];
477-
if (isImportMetaFieldEnabled(importMeta, "url")) {
478-
knownProps.push(`url: ${importMetaUrl()}`);
479-
}
480-
if (isImportMetaFieldEnabled(importMeta, "webpack")) {
481-
knownProps.push(`webpack: ${importMetaWebpackVersion()}`);
482-
}
483-
if (isImportMetaFieldEnabled(importMeta, "main")) {
484-
knownProps.push(
485-
`main: ${renderImportMetaMain(parser, false)}`
486-
);
487-
}
488-
if (isImportMetaFieldEnabled(importMeta, "env")) {
489-
addDefinitionValueDependencies();
490-
knownProps.push(
491-
`env: ${renderDefinition(envNode, IMPORT_META_ENV)}`
492-
);
493-
}
494-
for (const [prop, node] of customProps) {
495-
addDefinitionValueDependencies();
496-
knownProps.push(
497-
`${DefinePlugin.toPropertyKey(prop)}: ${renderDefinition(
498-
node,
499-
`${IMPORT_META}.${prop}`
500-
)}`
501-
);
502-
}
503-
const plainInitCode = `var ${varName} = {${knownProps.join(
504-
", "
505-
)}};\n`;
506-
// Each module owns its `import.meta`: it copies the bundle's one
507-
// and inherits from it, so later host writes stay visible.
508-
const initCode = preserveUnknown(importMeta)
509-
? `var ${varName} = Object.assign(Object.create(import.meta), import.meta, {${knownProps.join(
510-
", "
511-
)}});\n`
512-
: plainInitCode;
513-
/** @type {RawRuntimeRequirements} */
514-
const initRuntimeRequirements = [
515-
RuntimeGlobals.moduleCache,
516-
RuntimeGlobals.entryModuleId,
517-
RuntimeGlobals.module
518-
];
519-
const initCodeRequirements =
520-
DefinePlugin.getRuntimeRequirements(initCode);
521-
if (initCodeRequirements) {
522-
initRuntimeRequirements.push(...initCodeRequirements);
523-
}
524-
const initDep = new ModuleInitFragmentDependency(
525-
initCode,
526-
initRuntimeRequirements,
527-
varName,
528-
// native `import.meta` does not parse in the build-time vm
529-
initCode === plainInitCode ? undefined : plainInitCode
530-
);
531-
initDep.loc = parser.getLocation(metaProperty);
532-
parser.state.module.addPresentationalDependency(initDep);
555+
const varName = getImportMetaObject(metaProperty);
533556
const dep = new ConstDependency(
534557
varName,
535558
/** @type {Range} */ (metaProperty.range),
@@ -551,12 +574,11 @@ class ImportMetaPlugin {
551574
strBuildTime += code;
552575
};
553576
/**
554-
* Appends a property reading native `import.meta`, which
555-
* does not parse in the build-time vm.
577+
* Appends a runtime property with its build-time fallback.
556578
* @param {string} field field name
557579
*/
558580
const appendRuntimeProperty = (field) => {
559-
str += importMetaRuntimeProperty(field);
581+
str += importMetaRuntimeProperty(field, metaProperty);
560582
strBuildTime += importMetaBuildTimeProperty(field);
561583
};
562584
for (const prop of referencedPropertiesInDestructuring) {
@@ -882,10 +904,9 @@ class ImportMetaPlugin {
882904
(IMPORT_META_FIELDS.has(members[0]) &&
883905
!isImportMetaFieldEnabled(importMeta, members[0]))
884906
) {
885-
// keep the native access verbatim for emitted output, but
886-
// native `import.meta` does not parse in the build-time vm
907+
// Runtime properties are unavailable in the build-time vm.
887908
const dep = new BuildTimeConstDependency(
888-
`${IMPORT_META}${propertyAccess(members, 0)}`,
909+
importMetaUnknownProperty(members, expr),
889910
`${Template.toNormalComment(
890911
`unsupported import.meta.${members.join(".")}`
891912
)} undefined${propertyAccess(members, 1)}`,
@@ -896,7 +917,7 @@ class ImportMetaPlugin {
896917
return true;
897918
}
898919
const dep = new ConstDependency(
899-
importMetaUnknownProperty(members),
920+
importMetaUnknownProperty(members, expr),
900921
/** @type {Range} */ (expr.range)
901922
);
902923
dep.loc = parser.getLocation(expr);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { meta as otherMeta, readCustom, readDirect } from "./other";
2+
3+
const meta = import.meta;
4+
5+
it("should read alias mutations through direct and destructured accesses", () => {
6+
meta.custom = 42;
7+
expect(import.meta.custom).toBe(42);
8+
expect(import.meta["custom"]).toBe(42);
9+
expect(typeof import.meta.custom).toBe("number");
10+
const { custom } = import.meta;
11+
expect(custom).toBe(42);
12+
expect(meta).toBe(import.meta);
13+
});
14+
15+
it("should observe direct writes and deletions through the alias", () => {
16+
meta.count = 1;
17+
import.meta.count++;
18+
expect(meta.count).toBe(2);
19+
import.meta.count = 3;
20+
expect(meta.count).toBe(3);
21+
delete import.meta.count;
22+
expect(meta.count).toBeUndefined();
23+
meta.count = 4;
24+
delete meta.count;
25+
expect(import.meta.count).toBeUndefined();
26+
});
27+
28+
it("should use the same receiver for methods on import.meta", () => {
29+
meta.read = function () {
30+
return this;
31+
};
32+
expect(import.meta.read()).toBe(meta);
33+
});
34+
35+
it("should keep alias mutations local to the module", () => {
36+
meta.custom = 42;
37+
expect(otherMeta.custom).toBeUndefined();
38+
expect(readCustom()).toBeUndefined();
39+
otherMeta.custom = 7;
40+
expect(readCustom()).toBe(7);
41+
expect(import.meta.custom).toBe(42);
42+
});
43+
44+
it("should keep direct mutations local to the module", () => {
45+
import.meta.direct = "HELLO";
46+
expect(meta.direct).toBe("HELLO");
47+
expect(Object.keys(import.meta)).toContain("direct");
48+
expect(otherMeta.direct).toBeUndefined();
49+
expect(readDirect()).toBeUndefined();
50+
delete import.meta.direct;
51+
expect(meta.direct).toBeUndefined();
52+
});
53+
54+
it("should not bring back a value an alias deleted", () => {
55+
import.meta.restored = "direct";
56+
meta.restored = "alias";
57+
delete meta.restored;
58+
expect(import.meta.restored).toBeUndefined();
59+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const meta = import.meta;
2+
3+
/**
4+
* @returns {number | undefined} the custom value belonging to this module
5+
*/
6+
export function readCustom() {
7+
return import.meta.custom;
8+
}
9+
10+
/**
11+
* @returns {string | undefined} the direct value belonging to this module
12+
*/
13+
export function readDirect() {
14+
return import.meta.direct;
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use strict";
2+
3+
/** @import { JavascriptParserOptions } from "../../../../declarations/WebpackOptions" */
4+
5+
/** @type {JavascriptParserOptions["importMeta"][]} */
6+
const importMetaOptions = ["preserve-unknown", {}];
7+
8+
/** @type {import("../../../../types").Configuration[]} */
9+
const configurations = [];
10+
11+
for (const target of ["node", "web", "webworker", "universal"]) {
12+
for (const importMeta of importMetaOptions) {
13+
configurations.push({
14+
target,
15+
output: { module: true },
16+
module: {
17+
parser: { "javascript/auto": { importMeta } }
18+
}
19+
});
20+
}
21+
}
22+
23+
module.exports = configurations;

‎test/configCases/module/import-meta-distinct/index.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ it("should keep unknown properties observable", () => {
2222
import.meta.UNKNOWN_PROPERTY = "HELLO";
2323
const own = import.meta;
2424
expect(own.UNKNOWN_PROPERTY).toBe("HELLO");
25-
expect(getMeta().UNKNOWN_PROPERTY).toBe("HELLO");
25+
expect(getMeta().UNKNOWN_PROPERTY).toBeUndefined();
2626
});

0 commit comments

Comments
 (0)