This commit is contained in:
Xiao 2025-10-04 13:37:13 +03:00 committed by GitHub
commit 7a214cb7d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 814 additions and 22 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). * A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
*/ */
export type DevTool = (false | "eval") | string; 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. * 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). * A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
*/ */
devtool?: DevTool; devtool?: DevTool;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
dotenv?: Dotenv;
/** /**
* The entry point(s) of the compilation. * The entry point(s) of the compilation.
*/ */
@ -1108,6 +1116,19 @@ export interface FileCacheOptions {
*/ */
version?: string; 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;
}
/** /**
* Multiple entry bundles are created. The key is the entry name. The value can be a string, an array or an entry description object. * Multiple entry bundles are created. The key is the entry name. The value can be a string, an array or an entry description object.
*/ */
@ -3798,6 +3819,10 @@ export interface WebpackOptionsNormalized {
* A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map). * A developer tool to enhance debugging (false | eval | [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map).
*/ */
devtool?: DevTool; devtool?: DevTool;
/**
* Enable and configure the Dotenv plugin to load environment variables from .env files.
*/
dotenv?: Dotenv;
/** /**
* The entry point(s) of the compilation. * The entry point(s) of the compilation.
*/ */

View File

@ -354,20 +354,20 @@ class DefinePlugin {
* @returns {void} * @returns {void}
*/ */
apply(compiler) { 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( compiler.hooks.compilation.tap(
PLUGIN_NAME, PLUGIN_NAME,
(compilation, { normalModuleFactory }) => { (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"); const logger = compilation.getLogger("webpack.DefinePlugin");
compilation.dependencyTemplates.set( compilation.dependencyTemplates.set(
ConstDependency, ConstDependency,

429
lib/DotenvPlugin.js Normal file
View File

@ -0,0 +1,429 @@
/*
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
};
// 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;
};
/**
* Get list of env files to load based on mode
* 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
*/
const getEnvFilesForMode = (inputFileSystem, dir, mode) => {
if (dir) {
return [
/** default file */ ".env",
/** local file */ ".env.local",
/** mode file */ `.env.${mode}`,
/** mode local file */ `.env.${mode}.local`
].map((file) => join(inputFileSystem, dir, file));
}
return [];
};
/**
* 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.beforeRun.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);
}
/**
* 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;
}
if (rawDir === false) {
return "";
}
return "";
};
const dir = getDir();
// Get env files to load
const envFiles = 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); ).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) {
if (options.devtool.includes("source-map")) { if (options.devtool.includes("source-map")) {
const hidden = options.devtool.includes("hidden"); const hidden = options.devtool.includes("hidden");

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -629,6 +629,57 @@
"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.", "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" "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
}
]
}
}
},
"EmptyGeneratorOptions": { "EmptyGeneratorOptions": {
"description": "No generator options are supported for this module type.", "description": "No generator options are supported for this module type.",
"type": "object", "type": "object",
@ -5800,6 +5851,9 @@
"devtool": { "devtool": {
"$ref": "#/definitions/DevTool" "$ref": "#/definitions/DevTool"
}, },
"dotenv": {
"$ref": "#/definitions/Dotenv"
},
"entry": { "entry": {
"$ref": "#/definitions/EntryNormalized" "$ref": "#/definitions/EntryNormalized"
}, },
@ -5950,6 +6004,9 @@
"devtool": { "devtool": {
"$ref": "#/definitions/DevTool" "$ref": "#/definitions/DevTool"
}, },
"dotenv": {
"$ref": "#/definitions/Dotenv"
},
"entry": { "entry": {
"$ref": "#/definitions/Entry" "$ref": "#/definitions/Entry"
}, },

View File

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

View File

@ -79,7 +79,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration should be an object: - 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." -> Options object as provided by the user."
`) `)
); );
@ -88,7 +88,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration should be an object: - 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." -> Options object as provided by the user."
`) `)
); );
@ -251,7 +251,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "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: - 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. -> Options object as provided by the user.
For typos: please correct them. For typos: please correct them.
For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration. For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration.
@ -494,7 +494,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "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: - 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. -> Options object as provided by the user.
The 'debug' property was removed in webpack 2.0.0. 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. Loaders should be updated to allow passing this option via loader options in module.rules.
@ -542,7 +542,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration[1] should be an object: - 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." -> Options object as provided by the user."
`) `)
); );
@ -664,7 +664,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "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: - 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. -> Options object as provided by the user.
Did you mean module.rules?" Did you mean module.rules?"
`) `)
@ -679,7 +679,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "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: - 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. -> Options object as provided by the user.
Did you mean optimization.splitChunks?" Did you mean optimization.splitChunks?"
`) `)
@ -694,7 +694,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(` expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema. "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: - 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. -> Options object as provided by the user.
Did you mean module.noParse?" Did you mean module.noParse?"
`) `)

View File

@ -465,6 +465,64 @@ Object {
"multiple": false, "multiple": false,
"simpleType": "string", "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",
},
"entry": Object { "entry": Object {
"configs": Array [ "configs": Array [
Object { Object {

View File

@ -0,0 +1,13 @@
# 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

View File

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

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 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,54 @@
"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
}
}
];

68
types.d.ts vendored
View File

@ -3000,6 +3000,11 @@ declare interface Configuration {
*/ */
devtool?: string | false; 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. * The entry point(s) of the compilation.
*/ */
@ -4419,6 +4424,55 @@ declare interface DllReferencePluginOptionsManifest {
| "jsonp" | "jsonp"
| "system"; | "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[];
};
apply(compiler: Compiler): void;
/**
* 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[];
}
declare class DynamicEntryPlugin { declare class DynamicEntryPlugin {
constructor(context: string, entry: () => Promise<EntryStaticNormalized>); constructor(context: string, entry: () => Promise<EntryStaticNormalized>);
context: string; context: string;
@ -18313,6 +18367,11 @@ declare interface WebpackOptionsNormalized {
*/ */
devtool?: string | false; 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. * The entry point(s) of the compilation.
*/ */
@ -18514,7 +18573,13 @@ type WebpackOptionsNormalizedWithDefaults = WebpackOptionsNormalized & {
} & { watch: NonNullable<undefined | boolean> } & { } & { watch: NonNullable<undefined | boolean> } & {
performance: NonNullable<undefined | false | PerformanceOptions>; performance: NonNullable<undefined | false | PerformanceOptions>;
} & { recordsInputPath: NonNullable<undefined | string | false> } & { } & { recordsInputPath: NonNullable<undefined | string | false> } & {
recordsOutputPath: NonNullable<undefined | string | false>; recordsOutputPath:
| (string & {
dotenv: NonNullable<undefined | boolean | DotenvPluginOptions>;
})
| (false & {
dotenv: NonNullable<undefined | boolean | DotenvPluginOptions>;
});
}; };
/** /**
@ -19237,6 +19302,7 @@ declare namespace exports {
DllPlugin, DllPlugin,
DllReferencePlugin, DllReferencePlugin,
DynamicEntryPlugin, DynamicEntryPlugin,
DotenvPlugin,
EntryOptionPlugin, EntryOptionPlugin,
EntryPlugin, EntryPlugin,
EnvironmentPlugin, EnvironmentPlugin,