feat: add DotenvPlugin

This commit is contained in:
xiaoxiaojx 2025-09-13 15:01:49 +08:00
parent 9f98d803c0
commit 08625a84d7
27 changed files with 901 additions and 21 deletions

View File

@ -47,6 +47,10 @@ export type DevServer =
* A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
*/
export type DevTool = (false | "eval") | string;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
export type Dotenv = boolean | DotenvPluginOptions;
/**
* The entry point(s) of the compilation.
*/
@ -884,6 +888,10 @@ export interface WebpackOptions {
* A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
*/
devtool?: DevTool;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
dotenv?: Dotenv;
/**
* The entry point(s) of the compilation.
*/
@ -1108,6 +1116,23 @@ export interface FileCacheOptions {
*/
version?: string;
}
/**
* Options for Dotenv plugin.
*/
export interface DotenvPluginOptions {
/**
* The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.
*/
dir?: boolean | string;
/**
* Only expose environment variables that start with these prefixes. Defaults to 'WEBPACK_'.
*/
prefix?: string[] | string;
/**
* Template patterns for .env file names. Use [mode] as placeholder for the webpack mode. Defaults to ['.env', '.env.local', '.env.[mode]', '.env.[mode].local'].
*/
template?: string[];
}
/**
* Multiple entry bundles are created. The key is the entry name. The value can be a string, an array or an entry description object.
*/
@ -3797,6 +3822,10 @@ export interface WebpackOptionsNormalized {
* A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
*/
devtool?: DevTool;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
dotenv?: Dotenv;
/**
* The entry point(s) of the compilation.
*/

View File

@ -354,20 +354,20 @@ class DefinePlugin {
* @returns {void}
*/
apply(compiler) {
const definitions = this.definitions;
/**
* @type {Map<string, Set<string>>}
*/
const finalByNestedKey = new Map();
/**
* @type {Map<string, Set<string>>}
*/
const nestedByFinalKey = new Map();
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
const definitions = this.definitions;
/**
* @type {Map<string, Set<string>>}
*/
const finalByNestedKey = new Map();
/**
* @type {Map<string, Set<string>>}
*/
const nestedByFinalKey = new Map();
const logger = compilation.getLogger("webpack.DefinePlugin");
compilation.dependencyTemplates.set(
ConstDependency,

426
lib/DotenvPlugin.js Normal file
View File

@ -0,0 +1,426 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
*/
"use strict";
const createSchemaValidation = require("./util/create-schema-validation");
const { join } = require("./util/fs");
/** @typedef {import("../declarations/WebpackOptions").DotenvPluginOptions} DotenvPluginOptions */
/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
/** @type {DotenvPluginOptions} */
const DEFAULT_OPTIONS = {
prefix: "WEBPACK_",
dir: true,
template: [".env", ".env.local", ".env.[mode]", ".env.[mode].local"]
};
// Regex for parsing .env files
// ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32
const LINE =
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
const PLUGIN_NAME = "DotenvPlugin";
const validate = createSchemaValidation(
undefined,
() => {
const { definitions } = require("../schemas/WebpackOptions.json");
return {
definitions,
oneOf: [{ $ref: "#/definitions/DotenvPluginOptions" }]
};
},
{
name: "Dotenv Plugin",
baseDataPath: "options"
}
);
/**
* Parse .env file content
* ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L49
* @param {string|Buffer} src the source content to parse
* @returns {Record<string, string>} parsed environment variables object
*/
function parse(src) {
const obj = /** @type {Record<string, string>} */ ({});
// Convert buffer to string
let lines = src.toString();
// Convert line breaks to same format
lines = lines.replace(/\r\n?/gm, "\n");
let match;
while ((match = LINE.exec(lines)) !== null) {
const key = match[1];
// Default undefined or null to empty string
let value = match[2] || "";
// Remove whitespace
value = value.trim();
// Check if double quoted
const maybeQuote = value[0];
// Remove surrounding quotes
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
// Expand newlines if double quoted
if (maybeQuote === '"') {
value = value.replace(/\\n/g, "\n");
value = value.replace(/\\r/g, "\r");
}
// Add to object
obj[key] = value;
}
return obj;
}
/**
* Resolve escape sequences
* ported from https://github.com/motdotla/dotenv-expand
* @param {string} value value to resolve
* @returns {string} resolved value
*/
function _resolveEscapeSequences(value) {
return value.replace(/\\\$/g, "$");
}
/**
* Expand environment variable value
* ported from https://github.com/motdotla/dotenv-expand
* @param {string} value value to expand
* @param {Record<string, string | undefined>} processEnv process.env object
* @param {Record<string, string>} runningParsed running parsed object
* @returns {string} expanded value
*/
function expandValue(value, processEnv, runningParsed) {
const env = { ...runningParsed, ...processEnv }; // process.env wins
const regex = /(?<!\\)\$\{([^{}]+)\}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g;
let result = value;
let match;
const seen = new Set(); // self-referential checker
while ((match = regex.exec(result)) !== null) {
seen.add(result);
const [template, bracedExpression, unbracedExpression] = match;
const expression = bracedExpression || unbracedExpression;
// match the operators `:+`, `+`, `:-`, and `-`
const opRegex = /(:\+|\+|:-|-)/;
// find first match
const opMatch = expression.match(opRegex);
const splitter = opMatch ? opMatch[0] : null;
const r = expression.split(/** @type {string} */ (splitter));
// const r = splitter ? expression.split(splitter) : [expression];
let defaultValue;
let value;
const key = r.shift();
if ([":+", "+"].includes(splitter || "")) {
defaultValue = env[key || ""] ? r.join(splitter || "") : "";
value = null;
} else {
defaultValue = r.join(splitter || "");
value = env[key || ""];
}
if (value) {
// self-referential check
result = seen.has(value)
? result.replace(template, defaultValue)
: result.replace(template, value);
} else {
result = result.replace(template, defaultValue);
}
// if the result equaled what was in process.env and runningParsed then stop expanding
if (result === runningParsed[key || ""]) {
break;
}
regex.lastIndex = 0; // reset regex search position to re-evaluate after each replacement
}
return result;
}
/**
* Expand environment variables in parsed object
* ported from https://github.com/motdotla/dotenv-expand
* @param {{ parsed: Record<string, string>, processEnv?: Record<string, string | undefined> }} options expand options
* @returns {{ parsed: Record<string, string> }} expanded options
*/
function expand(options) {
// for use with progressive expansion
const runningParsed = /** @type {Record<string, string>} */ ({});
let processEnv = process.env;
if (
options &&
options.processEnv !== null &&
options.processEnv !== undefined
) {
processEnv = options.processEnv;
}
// dotenv.config() ran before this so the assumption is process.env has already been set
for (const key in options.parsed) {
let value = options.parsed[key];
// short-circuit scenario: process.env was already set prior to the file value
value =
processEnv[key] && processEnv[key] !== value
? /** @type {string} */ (processEnv[key])
: expandValue(value, processEnv, runningParsed);
options.parsed[key] = _resolveEscapeSequences(value);
// for use with progressive expansion
runningParsed[key] = _resolveEscapeSequences(value);
}
for (const processKey in options.parsed) {
if (processEnv) {
processEnv[processKey] = options.parsed[processKey];
}
}
return options;
}
/**
* Resolve and validate env prefixes
* Similar to Vite's resolveEnvPrefix
* @param {string | string[] | undefined} rawPrefix raw prefix option
* @returns {string[]} normalized prefixes array
*/
const resolveEnvPrefix = (rawPrefix) => {
const prefixes = Array.isArray(rawPrefix)
? rawPrefix
: [rawPrefix || "WEBPACK_"];
// Check for empty prefix (security issue like Vite does)
if (prefixes.includes("")) {
throw new Error(
"prefix option contains value '', which could lead to unexpected exposure of sensitive information."
);
}
return prefixes;
};
/**
* Format environment variables as DefinePlugin definitions
* @param {Record<string, string>} env environment variables
* @returns {Record<string, string>} formatted definitions
*/
const envToDefinitions = (env) => {
const definitions = /** @type {Record<string, string>} */ ({});
for (const [key, value] of Object.entries(env)) {
// Always use process.env. prefix for DefinePlugin
definitions[`process.env.${key}`] = JSON.stringify(value);
}
return definitions;
};
class DotenvPlugin {
/**
* @param {DotenvPluginOptions=} options options object
*/
constructor(options = {}) {
validate(options);
this.config = { ...DEFAULT_OPTIONS, ...options };
}
/**
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const definePlugin = new compiler.webpack.DefinePlugin({});
/** @type {string[] | undefined} */
let fileDependenciesCache;
compiler.hooks.beforeCompile.tapAsync(PLUGIN_NAME, (_params, callback) => {
const inputFileSystem = /** @type {InputFileSystem} */ (
compiler.inputFileSystem
);
const context = compiler.context;
const mode = compiler.options.mode || "development";
this.loadEnv(
inputFileSystem,
mode,
context,
(err, env, fileDependencies) => {
if (err) return callback(err);
const definitions = envToDefinitions(env || {});
// update the definitions
definePlugin.definitions = definitions;
// update the file dependencies
fileDependenciesCache = fileDependencies;
callback();
}
);
});
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.fileDependencies.addAll(fileDependenciesCache || []);
});
definePlugin.apply(compiler);
}
/**
* Get list of env files to load based on mode and template
* Similar to Vite's getEnvFilesForMode
* @param {InputFileSystem} inputFileSystem the input file system
* @param {string} dir the directory containing .env files
* @param {string | undefined} mode the mode (e.g., 'production', 'development')
* @returns {string[]} array of file paths to load
*/
getEnvFilesForMode(inputFileSystem, dir, mode) {
if (!dir) {
return [];
}
const { template } = /** @type {DotenvPluginOptions} */ (this.config);
const templates = template || [];
return templates
.map((pattern) => pattern.replace(/\[mode\]/g, mode || "development"))
.map((file) => join(inputFileSystem, dir, file));
}
/**
* Load environment variables from .env files
* Similar to Vite's loadEnv implementation
* @param {InputFileSystem} fs the input file system
* @param {string | undefined} mode the mode
* @param {string} context the compiler context
* @param {(err: Error | null, env?: Record<string, string>, fileDependencies?: string[]) => void} callback callback function
* @returns {void}
*/
loadEnv(fs, mode, context, callback) {
const { dir: rawDir, prefix: rawPrefix } =
/** @type {DotenvPluginOptions} */ (this.config);
let prefixes;
try {
prefixes = resolveEnvPrefix(rawPrefix);
} catch (err) {
return callback(/** @type {Error} */ (err));
}
const getDir = () => {
if (typeof rawDir === "string") {
return join(fs, context, rawDir);
}
if (rawDir === true) {
return context;
}
return "";
};
/** @type {string} */
const dir = getDir();
// Get env files to load
const envFiles = this.getEnvFilesForMode(fs, dir, mode);
/** @type {string[]} */
const fileDependencies = [];
// Read all files
const readPromises = envFiles.map((filePath) =>
this.loadFile(fs, filePath).then(
(content) => {
fileDependencies.push(filePath);
return { content, filePath };
},
() =>
// File doesn't exist, skip it (this is normal)
({ content: "", filePath })
)
);
Promise.all(readPromises)
.then((results) => {
// Parse all files and merge (later files override earlier ones)
// Similar to Vite's implementation
const parsed = /** @type {Record<string, string>} */ ({});
for (const { content } of results) {
if (!content) continue;
const entries = parse(content);
for (const key in entries) {
parsed[key] = entries[key];
}
}
// Always expand environment variables (like Vite does)
// Make a copy of process.env so that dotenv-expand doesn't modify global process.env
const processEnv = { ...process.env };
expand({ parsed, processEnv });
// Filter by prefixes and prioritize process.env (like Vite)
const env = /** @type {Record<string, string>} */ ({});
// First, add filtered vars from parsed .env files
for (const [key, value] of Object.entries(parsed)) {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = value;
}
}
// Then, prioritize actual env variables starting with prefixes
// These are typically provided inline and should be prioritized (like Vite)
for (const key in process.env) {
if (prefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = /** @type {string} */ (process.env[key]);
}
}
callback(null, env, fileDependencies);
})
.catch((err) => {
callback(err);
});
}
/**
* Load a file with proper path resolution
* @param {InputFileSystem} fs the input file system
* @param {string} file the file to load
* @returns {Promise<string>} the content of the file
*/
loadFile(fs, file) {
return new Promise((resolve, reject) => {
fs.readFile(file, "utf8", (err, content) => {
if (err) reject(err);
else resolve(content || "");
});
});
}
}
module.exports = DotenvPlugin;

View File

@ -294,6 +294,14 @@ class WebpackOptionsApply extends OptionsApply {
).apply(compiler);
}
if (options.dotenv) {
const DotenvPlugin = require("./DotenvPlugin");
new DotenvPlugin(
typeof options.dotenv === "boolean" ? {} : options.dotenv
).apply(compiler);
}
if (options.devtool) {
if (options.devtool.includes("source-map")) {
const hidden = options.devtool.includes("hidden");

View File

@ -202,6 +202,7 @@ const {
* & { performance: NonNullable<WebpackOptionsNormalized["performance"]> }
* & { recordsInputPath: NonNullable<WebpackOptionsNormalized["recordsInputPath"]> }
* & { recordsOutputPath: NonNullable<WebpackOptionsNormalized["recordsOutputPath"]>
* & { dotenv: NonNullable<WebpackOptionsNormalized["dotenv"]> }
* }} WebpackOptionsNormalizedWithDefaults
*/

View File

@ -178,6 +178,7 @@ const getNormalizedWebpackOptions = (config) => ({
return { ...devServer };
}),
devtool: config.devtool,
dotenv: config.dotenv,
entry:
config.entry === undefined
? { main: {} }

View File

@ -232,6 +232,9 @@ module.exports = mergeExports(fn, {
get DynamicEntryPlugin() {
return require("./DynamicEntryPlugin");
},
get DotenvPlugin() {
return require("./DotenvPlugin");
},
get EntryOptionPlugin() {
return require("./EntryOptionPlugin");
},

File diff suppressed because one or more lines are too long

View File

@ -629,6 +629,66 @@
"description": "Module namespace to use when interpolating filename template string for the sources array in a generated SourceMap. Defaults to `output.library` if not set. It's useful for avoiding runtime collisions in sourcemaps from multiple webpack projects built as libraries.",
"type": "string"
},
"Dotenv": {
"description": "Enable and configure the Dotenv plugin to load environment variables from .env files.",
"cli": {
"exclude": false
},
"anyOf": [
{
"description": "Enable Dotenv plugin with default options.",
"type": "boolean"
},
{
"$ref": "#/definitions/DotenvPluginOptions"
}
]
},
"DotenvPluginOptions": {
"description": "Options for Dotenv plugin.",
"type": "object",
"additionalProperties": false,
"properties": {
"dir": {
"description": "The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.",
"anyOf": [
{
"type": "boolean"
},
{
"type": "string",
"minLength": 1
}
]
},
"prefix": {
"description": "Only expose environment variables that start with these prefixes. Defaults to 'WEBPACK_'.",
"anyOf": [
{
"type": "array",
"items": {
"description": "A prefix that environment variables must start with to be exposed.",
"type": "string",
"minLength": 1
}
},
{
"type": "string",
"minLength": 1
}
]
},
"template": {
"description": "Template patterns for .env file names. Use [mode] as placeholder for the webpack mode. Defaults to ['.env', '.env.local', '.env.[mode]', '.env.[mode].local'].",
"type": "array",
"items": {
"description": "A template pattern for .env file names.",
"type": "string",
"minLength": 1
}
}
}
},
"EmptyGeneratorOptions": {
"description": "No generator options are supported for this module type.",
"type": "object",
@ -5800,6 +5860,9 @@
"devtool": {
"$ref": "#/definitions/DevTool"
},
"dotenv": {
"$ref": "#/definitions/Dotenv"
},
"entry": {
"$ref": "#/definitions/EntryNormalized"
},
@ -5950,6 +6013,9 @@
"devtool": {
"$ref": "#/definitions/DevTool"
},
"dotenv": {
"$ref": "#/definitions/Dotenv"
},
"entry": {
"$ref": "#/definitions/Entry"
},

View File

@ -84,6 +84,7 @@ describe("snapshots", () => {
"dependencies": undefined,
"devServer": undefined,
"devtool": false,
"dotenv": undefined,
"entry": Object {
"main": Object {
"import": Array [

View File

@ -79,7 +79,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration should be an object:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user."
`)
);
@ -88,7 +88,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration should be an object:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user."
`)
);
@ -251,7 +251,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property 'postcss'. These properties are valid:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user.
For typos: please correct them.
For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.
@ -494,7 +494,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property 'debug'. These properties are valid:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user.
The 'debug' property was removed in webpack 2.0.0.
Loaders should be updated to allow passing this option via loader options in module.rules.
@ -542,7 +542,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration[1] should be an object:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user."
`)
);
@ -664,7 +664,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property 'rules'. These properties are valid:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user.
Did you mean module.rules?"
`)
@ -679,7 +679,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property 'splitChunks'. These properties are valid:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user.
Did you mean optimization.splitChunks?"
`)
@ -694,7 +694,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration has an unknown property 'noParse'. These properties are valid:
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, dotenv?, entry?, experiments?, extends?, externals?, externalsPresets?, externalsType?, ignoreWarnings?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, snapshot?, stats?, target?, watch?, watchOptions? }
-> Options object as provided by the user.
Did you mean module.noParse?"
`)

View File

@ -465,6 +465,90 @@ Object {
"multiple": false,
"simpleType": "string",
},
"dotenv": Object {
"configs": Array [
Object {
"description": "Enable Dotenv plugin with default options.",
"multiple": false,
"path": "dotenv",
"type": "boolean",
},
],
"description": "Enable Dotenv plugin with default options.",
"multiple": false,
"simpleType": "boolean",
},
"dotenv-dir": Object {
"configs": Array [
Object {
"description": "The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.",
"multiple": false,
"path": "dotenv.dir",
"type": "boolean",
},
Object {
"description": "The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.",
"multiple": false,
"path": "dotenv.dir",
"type": "string",
},
],
"description": "The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.",
"multiple": false,
"simpleType": "string",
},
"dotenv-prefix": Object {
"configs": Array [
Object {
"description": "A prefix that environment variables must start with to be exposed.",
"multiple": true,
"path": "dotenv.prefix[]",
"type": "string",
},
],
"description": "A prefix that environment variables must start with to be exposed.",
"multiple": true,
"simpleType": "string",
},
"dotenv-prefix-reset": Object {
"configs": Array [
Object {
"description": "Clear all items provided in 'dotenv.prefix' configuration. Only expose environment variables that start with these prefixes. Defaults to 'WEBPACK_'.",
"multiple": false,
"path": "dotenv.prefix",
"type": "reset",
},
],
"description": "Clear all items provided in 'dotenv.prefix' configuration. Only expose environment variables that start with these prefixes. Defaults to 'WEBPACK_'.",
"multiple": false,
"simpleType": "boolean",
},
"dotenv-template": Object {
"configs": Array [
Object {
"description": "A template pattern for .env file names.",
"multiple": true,
"path": "dotenv.template[]",
"type": "string",
},
],
"description": "A template pattern for .env file names.",
"multiple": true,
"simpleType": "string",
},
"dotenv-template-reset": Object {
"configs": Array [
Object {
"description": "Clear all items provided in 'dotenv.template' configuration. Template patterns for .env file names. Use [mode] as placeholder for the webpack mode. Defaults to ['.env', '.env.local', '.env.[mode]', '.env.[mode].local'].",
"multiple": false,
"path": "dotenv.template",
"type": "reset",
},
],
"description": "Clear all items provided in 'dotenv.template' configuration. Template patterns for .env file names. Use [mode] as placeholder for the webpack mode. Defaults to ['.env', '.env.local', '.env.[mode]', '.env.[mode].local'].",
"multiple": false,
"simpleType": "boolean",
},
"entry": Object {
"configs": Array [
Object {

View File

@ -0,0 +1,16 @@
# Basic test
WEBPACK_API_URL=https://api.example.com
WEBPACK_MODE=test
SECRET_KEY=should-not-be-exposed
PRIVATE_VAR=also-hidden
# Expand test
WEBPACK_BASE=example.com
WEBPACK_FULL_URL=${WEBPACK_API_URL}/v1
WEBPACK_PORT=${PORT:-3000}
# Mode-specific base value
WEBPACK_ENV=development
# Custom template test - base value
WEBPACK_OVERRIDE_VAR=base-value

View File

@ -0,0 +1,3 @@
# Custom template test - myLocal file
WEBPACK_CUSTOM_VAR=from-myLocal
WEBPACK_OVERRIDE_VAR=myLocal-value

View File

@ -0,0 +1,3 @@
# Production overrides
WEBPACK_API_URL=https://prod-api.example.com
WEBPACK_ENV=production

View File

@ -0,0 +1,2 @@
# Custom template test - production.myLocal file
WEBPACK_PROD_CUSTOM=from-production-myLocal

View File

@ -0,0 +1,10 @@
"use strict";
it("should expose only WEBPACK_ prefixed env vars", () => {
expect(process.env.WEBPACK_API_URL).toBe("https://api.example.com");
expect(process.env.WEBPACK_MODE).toBe("test");
// Non-prefixed vars should not be exposed
expect(typeof process.env.SECRET_KEY).toBe("undefined");
expect(typeof process.env.PRIVATE_VAR).toBe("undefined");
});

View File

@ -0,0 +1,7 @@
"use strict";
it("should load from custom dir", () => {
expect(process.env.WEBPACK_FROM_ENVS).toBe("loaded-from-envs-dir");
expect(process.env.WEBPACK_API_URL).toBe("https://custom.example.com");
});

View File

@ -0,0 +1,13 @@
"use strict";
it("should expose only APP_ and CONFIG_ prefixed vars", () => {
expect(process.env.APP_NAME).toBe("MyApp");
expect(process.env.CONFIG_TIMEOUT).toBe("5000");
// WEBPACK_ prefixed should not be exposed
expect(typeof process.env.WEBPACK_API_URL).toBe("undefined");
// Non-prefixed should not be exposed
expect(typeof process.env.SECRET_KEY).toBe("undefined");
});

View File

@ -0,0 +1,16 @@
"use strict";
it("should load env files based on custom template", () => {
// Should load from .env.myLocal (custom template)
expect(process.env.WEBPACK_CUSTOM_VAR).toBe("from-myLocal");
// Should load from .env.production.myLocal (custom mode-specific template)
expect(process.env.WEBPACK_PROD_CUSTOM).toBe("from-production-myLocal");
// Should also load from standard .env
expect(process.env.WEBPACK_API_URL).toBe("https://prod-api.example.com");
// Custom template files should override .env values
expect(process.env.WEBPACK_OVERRIDE_VAR).toBe("myLocal-value");
});

View File

@ -0,0 +1,16 @@
"use strict";
it("should not load any .env files when dir is false", () => {
// When dir: false, no .env files should be loaded
// Only environment variables that were already set in process.env should be available
// and only those with WEBPACK_ prefix should be exposed
// These should be undefined since no .env files are loaded
expect(typeof process.env.WEBPACK_API_URL).toBe("undefined");
expect(typeof process.env.WEBPACK_MODE).toBe("undefined");
expect(typeof process.env.SECRET_KEY).toBe("undefined");
expect(typeof process.env.PRIVATE_VAR).toBe("undefined");
// Only pre-existing process.env variables with WEBPACK_ prefix should be available
// (if any were set before webpack runs)
});

View File

@ -0,0 +1,2 @@
WEBPACK_FROM_ENVS=loaded-from-envs-dir
WEBPACK_API_URL=https://custom.example.com

View File

@ -0,0 +1,10 @@
"use strict";
it("should expand variables by default", () => {
expect(process.env.WEBPACK_BASE).toBe("example.com");
expect(process.env.WEBPACK_API_URL).toBe("https://api.example.com");
expect(process.env.WEBPACK_FULL_URL).toBe("https://api.example.com/v1");
// Test default value operator
expect(process.env.WEBPACK_PORT).toBe("3000");
});

View File

@ -0,0 +1,11 @@
"use strict";
it("should load .env.production and override .env values", () => {
// Value from .env.production should override .env
expect(process.env.WEBPACK_API_URL).toBe("https://prod-api.example.com");
expect(process.env.WEBPACK_ENV).toBe("production");
// Value only in .env
expect(process.env.WEBPACK_MODE).toBe("test");
});

View File

@ -0,0 +1,4 @@
APP_NAME=MyApp
CONFIG_TIMEOUT=5000
WEBPACK_API_URL=should-not-be-exposed
SECRET_KEY=also-hidden

View File

@ -0,0 +1,63 @@
"use strict";
/** @type {import("../../../../").Configuration[]} */
module.exports = [
// Test 1: Basic - default behavior with WEBPACK_ prefix
{
name: "basic",
mode: "development",
entry: "./basic.js",
dotenv: true
},
// Test 2: Expand - variables are always expanded
{
name: "expand",
mode: "development",
entry: "./expand.js",
dotenv: true
},
// Test 3: Custom dir - load from different directory
{
name: "custom-dir",
mode: "development",
entry: "./custom-envdir.js",
dotenv: {
dir: "./envs"
}
},
// Test 4: Custom prefixes - multiple prefixes
{
name: "custom-prefixes",
mode: "development",
entry: "./custom-prefixes.js",
dotenv: {
dir: "./prefixes-env",
prefix: ["APP_", "CONFIG_"]
}
},
// Test 5: Mode-specific - .env.[mode] overrides
{
name: "mode-specific",
mode: "production",
entry: "./mode-specific.js",
dotenv: true
},
// Test 6: Disabled dir - dir: false disables .env file loading
{
name: "disabled-dir",
mode: "development",
entry: "./disabled-dir.js",
dotenv: {
dir: false
}
},
// Test 7: Custom template - load files based on custom template patterns
{
name: "custom-template",
mode: "production",
entry: "./custom-template.js",
dotenv: {
template: [".env", ".env.myLocal", ".env.[mode]", ".env.[mode].myLocal"]
}
}
];

87
types.d.ts vendored
View File

@ -3000,6 +3000,11 @@ declare interface Configuration {
*/
devtool?: string | false;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
dotenv?: boolean | DotenvPluginOptions;
/**
* The entry point(s) of the compilation.
*/
@ -4419,6 +4424,74 @@ declare interface DllReferencePluginOptionsManifest {
| "jsonp"
| "system";
}
declare class DotenvPlugin {
constructor(options?: DotenvPluginOptions);
config: {
/**
* The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.
*/
dir?: string | boolean;
/**
* Only expose environment variables that start with these prefixes. Defaults to 'WEBPACK_'.
*/
prefix?: string | string[];
/**
* Template patterns for .env file names. Use [mode] as placeholder for the webpack mode. Defaults to ['.env', '.env.local', '.env.[mode]', '.env.[mode].local'].
*/
template?: string[];
};
apply(compiler: Compiler): void;
/**
* Get list of env files to load based on mode and template
* Similar to Vite's getEnvFilesForMode
*/
getEnvFilesForMode(
inputFileSystem: InputFileSystem,
dir: string,
mode?: string
): string[];
/**
* Load environment variables from .env files
* Similar to Vite's loadEnv implementation
*/
loadEnv(
fs: InputFileSystem,
mode: undefined | string,
context: string,
callback: (
err: null | Error,
env?: Record<string, string>,
fileDependencies?: string[]
) => void
): void;
/**
* Load a file with proper path resolution
*/
loadFile(fs: InputFileSystem, file: string): Promise<string>;
}
/**
* Options for Dotenv plugin.
*/
declare interface DotenvPluginOptions {
/**
* The directory from which .env files are loaded. Can be an absolute path, or a path relative to the project root. false will disable the .env file loading.
*/
dir?: string | boolean;
/**
* Only expose environment variables that start with these prefixes. Defaults to 'WEBPACK_'.
*/
prefix?: string | string[];
/**
* Template patterns for .env file names. Use [mode] as placeholder for the webpack mode. Defaults to ['.env', '.env.local', '.env.[mode]', '.env.[mode].local'].
*/
template?: string[];
}
declare class DynamicEntryPlugin {
constructor(context: string, entry: () => Promise<EntryStaticNormalized>);
context: string;
@ -18311,6 +18384,11 @@ declare interface WebpackOptionsNormalized {
*/
devtool?: string | false;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
dotenv?: boolean | DotenvPluginOptions;
/**
* The entry point(s) of the compilation.
*/
@ -18512,7 +18590,13 @@ type WebpackOptionsNormalizedWithDefaults = WebpackOptionsNormalized & {
} & { watch: NonNullable<undefined | boolean> } & {
performance: NonNullable<undefined | false | PerformanceOptions>;
} & { recordsInputPath: NonNullable<undefined | string | false> } & {
recordsOutputPath: NonNullable<undefined | string | false>;
recordsOutputPath:
| (string & {
dotenv: NonNullable<undefined | boolean | DotenvPluginOptions>;
})
| (false & {
dotenv: NonNullable<undefined | boolean | DotenvPluginOptions>;
});
};
/**
@ -19235,6 +19319,7 @@ declare namespace exports {
DllPlugin,
DllReferencePlugin,
DynamicEntryPlugin,
DotenvPlugin,
EntryOptionPlugin,
EntryPlugin,
EnvironmentPlugin,