Skip to content

Commit 05cac61

Browse files
authored
feat: enhance import.meta.env to support object access (#20153)
1 parent 8eff7d9 commit 05cac61

File tree

8 files changed

+164
-6
lines changed

8 files changed

+164
-6
lines changed

lib/DefinePlugin.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"use strict";
77

8+
const { SyncWaterfallHook } = require("tapable");
89
const {
910
JAVASCRIPT_MODULE_TYPE_AUTO,
1011
JAVASCRIPT_MODULE_TYPE_DYNAMIC,
@@ -32,6 +33,7 @@ const createHash = require("./util/createHash");
3233
/** @typedef {import("./javascript/JavascriptParser").DestructuringAssignmentProperties} DestructuringAssignmentProperties */
3334
/** @typedef {import("./javascript/JavascriptParser").Range} Range */
3435
/** @typedef {import("./logging/Logger").Logger} Logger */
36+
/** @typedef {import("./Compilation")} Compilation */
3537

3638
/** @typedef {null | undefined | RegExp | EXPECTED_FUNCTION | string | number | boolean | bigint | undefined} CodeValuePrimitive */
3739
/** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive | RuntimeValue>} CodeValue */
@@ -330,7 +332,30 @@ const WEBPACK_REQUIRE_FUNCTION_REGEXP = new RegExp(
330332
);
331333
const WEBPACK_REQUIRE_IDENTIFIER_REGEXP = new RegExp(RuntimeGlobals.require);
332334

335+
/**
336+
* @typedef {object} DefinePluginHooks
337+
* @property {SyncWaterfallHook<[Record<string, CodeValue>]>} definitions
338+
*/
339+
340+
/** @type {WeakMap<Compilation, DefinePluginHooks>} */
341+
const compilationHooksMap = new WeakMap();
342+
333343
class DefinePlugin {
344+
/**
345+
* @param {Compilation} compilation the compilation
346+
* @returns {DefinePluginHooks} the attached hooks
347+
*/
348+
static getCompilationHooks(compilation) {
349+
let hooks = compilationHooksMap.get(compilation);
350+
if (hooks === undefined) {
351+
hooks = {
352+
definitions: new SyncWaterfallHook(["definitions"])
353+
};
354+
compilationHooksMap.set(compilation, hooks);
355+
}
356+
return hooks;
357+
}
358+
334359
/**
335360
* Create a new define plugin
336361
* @param {Record<string, CodeValue>} definitions A map of global object definitions
@@ -358,6 +383,12 @@ class DefinePlugin {
358383
PLUGIN_NAME,
359384
(compilation, { normalModuleFactory }) => {
360385
const definitions = this.definitions;
386+
const hooks = DefinePlugin.getCompilationHooks(compilation);
387+
388+
hooks.definitions.tap(PLUGIN_NAME, (previousDefinitions) => ({
389+
...previousDefinitions,
390+
...definitions
391+
}));
361392

362393
/**
363394
* @type {Map<string, Set<string>>}

lib/dependencies/ImportMetaPlugin.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
const { pathToFileURL } = require("url");
99
const { SyncBailHook } = require("tapable");
1010
const Compilation = require("../Compilation");
11+
const DefinePlugin = require("../DefinePlugin");
1112
const ModuleDependencyWarning = require("../ModuleDependencyWarning");
1213
const {
1314
JAVASCRIPT_MODULE_TYPE_AUTO,
@@ -43,6 +44,31 @@ const getCriticalDependencyWarning = memoize(() =>
4344

4445
const PLUGIN_NAME = "ImportMetaPlugin";
4546

47+
/**
48+
* Collect import.meta.env definitions from DefinePlugin and build JSON string
49+
* @param {Compilation} compilation the compilation
50+
* @returns {string} env object as JSON string
51+
*/
52+
const collectImportMetaEnvDefinitions = (compilation) => {
53+
const definePluginHooks = DefinePlugin.getCompilationHooks(compilation);
54+
const definitions = definePluginHooks.definitions.call({});
55+
if (!definitions) {
56+
return "{}";
57+
}
58+
59+
/** @type {string[]} */
60+
const pairs = [];
61+
for (const key of Object.keys(definitions)) {
62+
if (key.startsWith("import.meta.env.")) {
63+
const envKey = key.slice("import.meta.env.".length);
64+
const value = definitions[key];
65+
pairs.push(`${JSON.stringify(envKey)}:${value}`);
66+
}
67+
}
68+
69+
return `{${pairs.join(",")}}`;
70+
};
71+
4672
/**
4773
* @typedef {object} ImportMetaPluginHooks
4874
* @property {SyncBailHook<[DestructuringAssignmentProperty], string | void>} propertyInDestructuring
@@ -294,6 +320,37 @@ class ImportMetaPlugin {
294320
.for("import.meta.main")
295321
.tap(PLUGIN_NAME, evaluateToString("boolean"));
296322

323+
// import.meta.env
324+
parser.hooks.typeof
325+
.for("import.meta.env")
326+
.tap(
327+
PLUGIN_NAME,
328+
toConstantDependency(parser, JSON.stringify("object"))
329+
);
330+
parser.hooks.expression
331+
.for("import.meta.env")
332+
.tap(PLUGIN_NAME, (expr) => {
333+
const envCode = collectImportMetaEnvDefinitions(compilation);
334+
const dep = new ConstDependency(
335+
envCode,
336+
/** @type {Range} */ (expr.range)
337+
);
338+
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
339+
parser.state.module.addPresentationalDependency(dep);
340+
return true;
341+
});
342+
parser.hooks.evaluateTypeof
343+
.for("import.meta.env")
344+
.tap(PLUGIN_NAME, evaluateToString("object"));
345+
parser.hooks.evaluateIdentifier
346+
.for("import.meta.env")
347+
.tap(PLUGIN_NAME, (expr) =>
348+
new BasicEvaluatedExpression()
349+
.setTruthy()
350+
.setSideEffects(false)
351+
.setRange(/** @type {Range} */ (expr.range))
352+
);
353+
297354
// Unknown properties
298355
parser.hooks.unhandledExpressionMemberChain
299356
.for("import.meta")

test/__snapshots__/ConfigCacheTestCases.longtest.js.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15012,15 +15012,15 @@ it(\\"should work import.meta.env with DotenvPlugin\\", () => {
1501215012
1501315013
it(\\"import.meta.env behaves like process.env\\", async (done) => {
1501415014
try {
15015-
const importMetaEnv = import.meta.env;
15015+
const importMetaEnv = {\\"AAA\\":\\"aaa\\",\\"WEBPACK_API_URL\\":\\"https://api.example.com\\",\\"NODE_ENV\\":\\"production\\"};
1501615016
importMetaEnv;
1501715017
const processEnv = process.env;
1501815018
processEnv;
15019-
const UNKNOWN_PROPERTY = import.meta.env.UNKNOWN_PROPERTY;
15019+
const UNKNOWN_PROPERTY = {\\"AAA\\":\\"aaa\\",\\"WEBPACK_API_URL\\":\\"https://api.example.com\\",\\"NODE_ENV\\":\\"production\\"}.UNKNOWN_PROPERTY;
1502015020
UNKNOWN_PROPERTY;
1502115021
const UNKNOWN_PROPERTY_2 = process.env.UNKNOWN_PROPERTY_2;
1502215022
UNKNOWN_PROPERTY_2;
15023-
typeof import.meta.env;
15023+
\\"object\\";
1502415024
typeof process.env;
1502515025
} catch (_e) {
1502615026
// ignore

test/__snapshots__/ConfigTestCases.basictest.js.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13803,15 +13803,15 @@ it(\\"should work import.meta.env with DotenvPlugin\\", () => {
1380313803
1380413804
it(\\"import.meta.env behaves like process.env\\", async (done) => {
1380513805
try {
13806-
const importMetaEnv = import.meta.env;
13806+
const importMetaEnv = {\\"AAA\\":\\"aaa\\",\\"WEBPACK_API_URL\\":\\"https://api.example.com\\",\\"NODE_ENV\\":\\"production\\"};
1380713807
importMetaEnv;
1380813808
const processEnv = process.env;
1380913809
processEnv;
13810-
const UNKNOWN_PROPERTY = import.meta.env.UNKNOWN_PROPERTY;
13810+
const UNKNOWN_PROPERTY = {\\"AAA\\":\\"aaa\\",\\"WEBPACK_API_URL\\":\\"https://api.example.com\\",\\"NODE_ENV\\":\\"production\\"}.UNKNOWN_PROPERTY;
1381113811
UNKNOWN_PROPERTY;
1381213812
const UNKNOWN_PROPERTY_2 = process.env.UNKNOWN_PROPERTY_2;
1381313813
UNKNOWN_PROPERTY_2;
13814-
typeof import.meta.env;
13814+
\\"object\\";
1381513815
typeof process.env;
1381613816
} catch (_e) {
1381713817
// ignore
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
WEBPACK_DOTENV_VAR=from_dotenv
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
it("should expose NODE_ENV from mode (WebpackOptionsApply)", () => {
2+
const env = import.meta.env;
3+
expect(env.NODE_ENV).toBe("production");
4+
});
5+
6+
it("should expose variables from EnvironmentPlugin", () => {
7+
const env = import.meta.env;
8+
expect(env.ENV_VAR_FROM_ENV).toBe("from_environment_plugin");
9+
});
10+
11+
it("should expose variables from DotenvPlugin", () => {
12+
const env = import.meta.env;
13+
expect(env.WEBPACK_DOTENV_VAR).toBe("from_dotenv");
14+
});
15+
16+
it("should expose variables from DefinePlugin", () => {
17+
const env = import.meta.env;
18+
expect(env.CUSTOM_VAR).toBe("custom_value");
19+
});
20+
21+
it("should support typeof import.meta.env", () => {
22+
expect(typeof import.meta.env).toBe("object");
23+
});
24+
25+
it("should evaluate typeof import.meta.env as 'object'", () => {
26+
const typeofEnv = typeof import.meta.env;
27+
expect(typeofEnv).toBe("object");
28+
});
29+
30+
it("should treat import.meta.env as truthy", () => {
31+
if (import.meta.env) {
32+
expect(true).toBe(true);
33+
} else {
34+
throw new Error("import.meta.env should be truthy");
35+
}
36+
});
37+
38+
39+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use strict";
2+
3+
const { DefinePlugin, EnvironmentPlugin } = require("../../../../");
4+
5+
/** @type {import("../../../../").Configuration} */
6+
module.exports = {
7+
// Test 1: NODE_ENV from mode (WebpackOptionsApply)
8+
mode: "production",
9+
// Test 3: DotenvPlugin from .env.test file
10+
dotenv: {
11+
template: [".env.test"]
12+
},
13+
plugins: [
14+
// Test 2: EnvironmentPlugin
15+
new EnvironmentPlugin({
16+
ENV_VAR_FROM_ENV: "from_environment_plugin"
17+
}),
18+
// Test 4: DefinePlugin
19+
new DefinePlugin({
20+
"import.meta.env.CUSTOM_VAR": JSON.stringify("custom_value")
21+
})
22+
]
23+
};

types.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3935,6 +3935,7 @@ declare class DefinePlugin {
39353935
* Apply the plugin
39363936
*/
39373937
apply(compiler: Compiler): void;
3938+
static getCompilationHooks(compilation: Compilation): DefinePluginHooks;
39383939
static runtimeValue(
39393940
fn: (value: {
39403941
module: NormalModule;
@@ -3944,6 +3945,12 @@ declare class DefinePlugin {
39443945
options?: true | string[] | RuntimeValueOptions
39453946
): RuntimeValue;
39463947
}
3948+
declare interface DefinePluginHooks {
3949+
definitions: SyncWaterfallHook<
3950+
[Record<string, CodeValue>],
3951+
Record<string, CodeValue>
3952+
>;
3953+
}
39473954
declare class DelegatedPlugin {
39483955
constructor(options: Options);
39493956
options: Options;

0 commit comments

Comments
 (0)