Compare commits

...

4 Commits

Author SHA1 Message Date
xiaoxiaojx 08625a84d7 feat: add DotenvPlugin 2025-10-04 23:16:08 +08:00
Alexander Akait 9f98d803c0
fix: javascript parser options types (#19980)
Github Actions / lint (push) Has been cancelled Details
Github Actions / validate-legacy-node (push) Has been cancelled Details
Github Actions / benchmark (1/4) (push) Has been cancelled Details
Github Actions / benchmark (2/4) (push) Has been cancelled Details
Github Actions / benchmark (3/4) (push) Has been cancelled Details
Github Actions / benchmark (4/4) (push) Has been cancelled Details
Github Actions / basic (push) Has been cancelled Details
Github Actions / unit (push) Has been cancelled Details
Github Actions / integration (10.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (10.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (10.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (12.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (14.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (16.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (18.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (20.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (20.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (lts/*, ubuntu-latest, a, 1) (push) Has been cancelled Details
Github Actions / integration (lts/*, ubuntu-latest, b, 1) (push) Has been cancelled Details
Update examples / examples (push) Has been cancelled Details
2025-10-04 14:58:38 +03:00
dependabot[bot] 8804459884
chore(deps): bump CodSpeedHQ/action from 4.0.1 to 4.1.0 (#19977)
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.0.1 to 4.1.0.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](653fdc30e6...3959e9e296)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-04 13:32:45 +03:00
Alexander Akait bc91301142
chore: update codespeed (#19959) 2025-10-04 13:26:48 +03:00
32 changed files with 1140 additions and 145 deletions

View File

@ -97,7 +97,7 @@ jobs:
- run: yarn link webpack --frozen-lockfile - run: yarn link webpack --frozen-lockfile
- name: Run benchmarks - name: Run benchmarks
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1 uses: CodSpeedHQ/action@3959e9e296ef25296e93e32afcc97196f966e57f # v4.1.0
with: with:
run: yarn benchmark --ci run: yarn benchmark --ci
mode: "instrumentation" mode: "instrumentation"

View File

@ -284,6 +284,7 @@
"url's", "url's",
"valign", "valign",
"valtype", "valtype",
"walltime",
"wasi", "wasi",
"wasm", "wasm",
"watchings", "watchings",
@ -304,7 +305,6 @@
"commithash", "commithash",
"formaters", "formaters",
"akait", "akait",
"Akait",
"evenstensberg", "evenstensberg",
"Stensberg", "Stensberg",
"ovflowd", "ovflowd",

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,23 @@ 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;
/**
* 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. * Multiple entry bundles are created. The key is the entry name. The value can be a string, an array or an entry description object.
*/ */
@ -3349,7 +3374,6 @@ export interface JavascriptParserOptions {
* Set the inner regular expression for partial dynamic dependencies. * Set the inner regular expression for partial dynamic dependencies.
*/ */
wrappedContextRegExp?: RegExp; wrappedContextRegExp?: RegExp;
[k: string]: any;
} }
/** /**
* Generator options for json modules. * Generator options for json modules.
@ -3798,6 +3822,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,

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); ).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");
}, },

View File

@ -110,7 +110,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.27.1", "@babel/core": "^7.27.1",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "^7.27.1",
"@codspeed/core": "^4.0.1", "@codspeed/core": "^5.0.1",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@eslint/markdown": "^7.3.0", "@eslint/markdown": "^7.3.0",
"@stylistic/eslint-plugin": "^5.4.0", "@stylistic/eslint-plugin": "^5.4.0",

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.", "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
}
]
},
"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": { "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",
@ -1786,7 +1846,7 @@
"JavascriptParserOptions": { "JavascriptParserOptions": {
"description": "Parser options for javascript modules.", "description": "Parser options for javascript modules.",
"type": "object", "type": "object",
"additionalProperties": true, "additionalProperties": false,
"properties": { "properties": {
"amd": { "amd": {
"$ref": "#/definitions/Amd" "$ref": "#/definitions/Amd"
@ -5800,6 +5860,9 @@
"devtool": { "devtool": {
"$ref": "#/definitions/DevTool" "$ref": "#/definitions/DevTool"
}, },
"dotenv": {
"$ref": "#/definitions/Dotenv"
},
"entry": { "entry": {
"$ref": "#/definitions/EntryNormalized" "$ref": "#/definitions/EntryNormalized"
}, },
@ -5950,6 +6013,9 @@
"devtool": { "devtool": {
"$ref": "#/definitions/DevTool" "$ref": "#/definitions/DevTool"
}, },
"dotenv": {
"$ref": "#/definitions/Dotenv"
},
"entry": { "entry": {
"$ref": "#/definitions/Entry" "$ref": "#/definitions/Entry"
}, },

View File

@ -3,6 +3,15 @@ import fs from "fs/promises";
import { Session } from "inspector"; import { Session } from "inspector";
import path from "path"; import path from "path";
import { fileURLToPath, pathToFileURL } from "url"; import { fileURLToPath, pathToFileURL } from "url";
import {
InstrumentHooks,
getCodspeedRunnerMode,
getGitDir,
getV8Flags,
mongoMeasurement,
setupCore,
teardownCore
} from "@codspeed/core";
import { simpleGit } from "simple-git"; import { simpleGit } from "simple-git";
import { Bench, hrtimeNow } from "tinybench"; import { Bench, hrtimeNow } from "tinybench";
@ -12,32 +21,6 @@ const git = simpleGit(rootPath);
const REV_LIST_REGEXP = /^([a-f0-9]+)\s*([a-f0-9]+)\s*([a-f0-9]+)?\s*$/; const REV_LIST_REGEXP = /^([a-f0-9]+)\s*([a-f0-9]+)\s*([a-f0-9]+)?\s*$/;
const getV8Flags = () => {
const nodeVersionMajor = Number.parseInt(
process.version.slice(1).split(".")[0],
10
);
const flags = [
"--hash-seed=1",
"--random-seed=1",
"--no-opt",
"--predictable",
"--predictable-gc-schedule",
"--interpreted-frames-native-stack",
"--allow-natives-syntax",
"--expose-gc",
"--no-concurrent-sweeping",
"--max-old-space-size=4096"
];
if (nodeVersionMajor < 18) {
flags.push("--no-randomize-hashes");
}
if (nodeVersionMajor < 20) {
flags.push("--no-scavenge-task");
}
return flags;
};
const checkV8Flags = () => { const checkV8Flags = () => {
const requiredFlags = getV8Flags(); const requiredFlags = getV8Flags();
const actualFlags = process.execArgv; const actualFlags = process.execArgv;
@ -248,6 +231,8 @@ for (const baselineInfo of baselineRevisions) {
} }
} }
const baseOutputPath = path.join(__dirname, "js", "benchmark");
function buildConfiguration( function buildConfiguration(
test, test,
baseline, baseline,
@ -385,105 +370,239 @@ const scenarios = [
} }
]; ];
const baseOutputPath = path.join(__dirname, "js", "benchmark"); function getStackTrace(belowFn) {
const oldLimit = Error.stackTraceLimit;
Error.stackTraceLimit = Infinity;
const dummyObject = {};
const v8Handler = Error.prepareStackTrace;
Error.prepareStackTrace = (dummyObject, v8StackTrace) => v8StackTrace;
Error.captureStackTrace(dummyObject, belowFn || getStackTrace);
const v8StackTrace = dummyObject.stack;
Error.prepareStackTrace = v8Handler;
Error.stackTraceLimit = oldLimit;
return v8StackTrace;
}
function getCallingFile() {
const stack = getStackTrace();
let callingFile = stack[2].getFileName(); // [here, withCodSpeed, actual caller]
const gitDir = getGitDir(callingFile);
if (gitDir === undefined) {
throw new Error("Could not find a git repository");
}
if (callingFile.startsWith("file://")) {
callingFile = fileURLToPath(callingFile);
}
return path.relative(gitDir, callingFile);
}
const taskUriMap = new WeakMap();
function getOrCreateUriMap(bench) {
let uriMap = taskUriMap.get(bench);
if (!uriMap) {
uriMap = new Map();
taskUriMap.set(bench, uriMap);
}
return uriMap;
}
function getTaskUri(bench, taskName, rootCallingFile) {
const uriMap = taskUriMap.get(bench);
return uriMap?.get(taskName) || `${rootCallingFile}::${taskName}`;
}
const withCodSpeed = async (/** @type {import("tinybench").Bench} */ bench) => { const withCodSpeed = async (/** @type {import("tinybench").Bench} */ bench) => {
const { Measurement, getGitDir, mongoMeasurement, setupCore, teardownCore } = const codspeedRunnerMode = getCodspeedRunnerMode();
await import("@codspeed/core");
if (!Measurement.isInstrumented()) { if (codspeedRunnerMode === "disabled") {
const rawRun = bench.run;
bench.run = async () => {
console.warn(
`[CodSpeed] ${bench.tasks.length} benches detected but no instrumentation found, falling back to tinybench`
);
return await rawRun.bind(bench)();
};
return bench; return bench;
} }
const getStackTrace = (belowFn) => {
const oldLimit = Error.stackTraceLimit;
Error.stackTraceLimit = Infinity;
const dummyObject = {};
const v8Handler = Error.prepareStackTrace;
Error.prepareStackTrace = (dummyObject, v8StackTrace) => v8StackTrace;
Error.captureStackTrace(dummyObject, belowFn || getStackTrace);
const v8StackTrace = dummyObject.stack;
Error.prepareStackTrace = v8Handler;
Error.stackTraceLimit = oldLimit;
return v8StackTrace;
};
const getCallingFile = () => {
const stack = getStackTrace();
let callingFile = stack[2].getFileName(); // [here, withCodSpeed, actual caller]
const gitDir = getGitDir(callingFile);
if (gitDir === undefined) {
throw new Error("Could not find a git repository");
}
if (callingFile.startsWith("file://")) {
callingFile = fileURLToPath(callingFile);
}
return path.relative(gitDir, callingFile);
};
const rawAdd = bench.add; const rawAdd = bench.add;
const uriMap = getOrCreateUriMap(bench);
bench.add = (name, fn, opts) => { bench.add = (name, fn, opts) => {
const callingFile = getCallingFile(); const callingFile = getCallingFile();
const uri = `${callingFile}::${name}`; let uri = callingFile;
const options = { ...opts, uri }; if (bench.name !== undefined) {
return rawAdd.bind(bench)(name, fn, options); uri += `::${bench.name}`;
}
uri += `::${name}`;
uriMap.set(name, uri);
return rawAdd.bind(bench)(name, fn, opts);
}; };
const rootCallingFile = getCallingFile(); const rootCallingFile = getCallingFile();
bench.run = async function run() {
const iterations = bench.opts.iterations - 1; if (codspeedRunnerMode === "instrumented") {
console.log("[CodSpeed] running"); const setupBenchRun = () => {
setupCore(); setupCore();
for (const task of bench.tasks) { console.log(
await bench.opts.setup?.(task, "run"); "[CodSpeed] running with @codspeed/tinybench (instrumented mode)"
await task.fnOpts.beforeAll?.call(task); );
const samples = []; };
async function iteration() { const finalizeBenchRun = () => {
try { teardownCore();
await task.fnOpts.beforeEach?.call(task, "run"); console.log(`[CodSpeed] Done running ${bench.tasks.length} benches.`);
const start = bench.opts.now(); return bench.tasks;
await task.fn(); };
samples.push(bench.opts.now() - start || 0);
await task.fnOpts.afterEach?.call(this, "run"); const wrapFunctionWithFrame = (fn, isAsync) => {
} catch (err) { if (isAsync) {
if (bench.opts.throws) { return async function __codspeed_root_frame__() {
throw err; await fn();
} };
}
return function __codspeed_root_frame__() {
fn();
};
};
const logTaskCompletion = (uri, status) => {
console.log(`[CodSpeed] ${status} ${uri}`);
};
const taskCompletionMessage = () =>
InstrumentHooks.isInstrumented() ? "Measured" : "Checked";
const iterationAsync = async (task) => {
try {
await task.fnOpts.beforeEach?.call(task, "run");
const start = bench.opts.now();
await task.fn();
const end = bench.opts.now() - start || 0;
await task.fnOpts.afterEach?.call(this, "run");
return [start, end];
} catch (err) {
if (bench.opts.throws) {
throw err;
} }
} }
while (samples.length < iterations) { };
await iteration();
}
// Codspeed Measure
const uri =
task.opts && "uri" in task.options
? task.opts.uri
: `${rootCallingFile}::${task.name}`;
await task.fnOpts.beforeEach?.call(task);
await mongoMeasurement.start(uri);
await (async function __codspeed_root_frame__() {
Measurement.startInstrumentation();
await task.fn();
Measurement.stopInstrumentation(uri);
})();
await mongoMeasurement.stop(uri);
await task.fnOpts.afterEach?.call(task);
console.log(`[Codspeed] ✔ Measured ${uri}`);
await task.fnOpts.afterAll?.call(task);
const wrapWithInstrumentHooksAsync = async (fn, uri) => {
InstrumentHooks.startBenchmark();
const result = await fn();
InstrumentHooks.stopBenchmark();
InstrumentHooks.setExecutedBenchmark(process.pid, uri);
return result;
};
const runTaskAsync = async (task, uri) => {
const { fnOpts, fn } = task;
// Custom setup
await bench.opts.setup?.(task, "run");
await fnOpts?.beforeAll?.call(task, "run");
// Custom warmup
// We don't run `optimizeFunction` because our function is never optimized, instead we just warmup webpack
const samples = [];
while (samples.length < bench.opts.iterations - 1) {
samples.push(await iterationAsync(task));
}
await fnOpts?.beforeEach?.call(task, "run");
await mongoMeasurement.start(uri);
global.gc?.();
await wrapWithInstrumentHooksAsync(wrapFunctionWithFrame(fn, true), uri);
await mongoMeasurement.stop(uri);
await fnOpts?.afterEach?.call(task, "run");
console.log(`[Codspeed] ✔ Measured ${uri}`);
await fnOpts?.afterAll?.call(task, "run");
// Custom teardown
await bench.opts.teardown?.(task, "run"); await bench.opts.teardown?.(task, "run");
task.processRunResult({ latencySamples: samples });
} logTaskCompletion(uri, taskCompletionMessage());
teardownCore(); };
console.log(`[CodSpeed] Done running ${bench.tasks.length} benches.`);
return bench.tasks; const iteration = (task) => {
}; try {
task.fnOpts.beforeEach?.call(task, "run");
const start = bench.opts.now();
task.fn();
const end = bench.opts.now() - start || 0;
task.fnOpts.afterEach?.call(this, "run");
return [start, end];
} catch (err) {
if (bench.opts.throws) {
throw err;
}
}
};
const wrapWithInstrumentHooks = (fn, uri) => {
InstrumentHooks.startBenchmark();
const result = fn();
InstrumentHooks.stopBenchmark();
InstrumentHooks.setExecutedBenchmark(process.pid, uri);
return result;
};
const runTaskSync = (task, uri) => {
const { fnOpts, fn } = task;
// Custom setup
bench.opts.setup?.(task, "run");
fnOpts?.beforeAll?.call(task, "run");
// Custom warmup
const samples = [];
while (samples.length < bench.opts.iterations - 1) {
samples.push(iteration(task));
}
fnOpts?.beforeEach?.call(task, "run");
wrapWithInstrumentHooks(wrapFunctionWithFrame(fn, false), uri);
fnOpts?.afterEach?.call(task, "run");
console.log(`[Codspeed] ✔ Measured ${uri}`);
fnOpts?.afterAll?.call(task, "run");
// Custom teardown
bench.opts.teardown?.(task, "run");
logTaskCompletion(uri, taskCompletionMessage());
};
const finalizeAsyncRun = () => {
finalizeBenchRun();
};
const finalizeSyncRun = () => {
finalizeBenchRun();
};
bench.run = async () => {
setupBenchRun();
for (const task of bench.tasks) {
const uri = getTaskUri(task.bench, task.name, rootCallingFile);
await runTaskAsync(task, uri);
}
return finalizeAsyncRun();
};
bench.runSync = () => {
setupBenchRun();
for (const task of bench.tasks) {
const uri = getTaskUri(task.bench, task.name, rootCallingFile);
runTaskSync(task, uri);
}
return finalizeSyncRun();
};
} else if (codspeedRunnerMode === "walltime") {
// We don't need it
}
return bench; return bench;
}; };
@ -495,7 +614,6 @@ const bench = await withCodSpeed(
warmupIterations: 2, warmupIterations: 2,
iterations: 8, iterations: 8,
setup(task, mode) { setup(task, mode) {
global.gc();
console.log(`Setup (${mode} mode): ${task.name}`); console.log(`Setup (${mode} mode): ${task.name}`);
}, },
teardown(task, mode) { teardown(task, mode) {

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,90 @@ 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",
},
"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 { "entry": Object {
"configs": Array [ "configs": Array [
Object { 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"]
}
}
];

89
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,74 @@ 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[];
/**
* 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 { declare class DynamicEntryPlugin {
constructor(context: string, entry: () => Promise<EntryStaticNormalized>); constructor(context: string, entry: () => Promise<EntryStaticNormalized>);
context: string; context: string;
@ -8195,8 +8268,6 @@ declare class JavascriptParser extends ParserClass {
* Parser options for javascript modules. * Parser options for javascript modules.
*/ */
declare interface JavascriptParserOptions { declare interface JavascriptParserOptions {
[index: string]: any;
/** /**
* Set the value of `require.amd` and `define.amd`. Or disable AMD support. * Set the value of `require.amd` and `define.amd`. Or disable AMD support.
*/ */
@ -18313,6 +18384,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 +18590,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 +19319,7 @@ declare namespace exports {
DllPlugin, DllPlugin,
DllReferencePlugin, DllReferencePlugin,
DynamicEntryPlugin, DynamicEntryPlugin,
DotenvPlugin,
EntryOptionPlugin, EntryOptionPlugin,
EntryPlugin, EntryPlugin,
EnvironmentPlugin, EnvironmentPlugin,

View File

@ -332,14 +332,14 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@codspeed/core@^4.0.1": "@codspeed/core@^5.0.1":
version "4.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/@codspeed/core/-/core-4.0.1.tgz#91049cce17b8c1d1b4b6cbc481f5ddc1145d6e1e" resolved "https://registry.yarnpkg.com/@codspeed/core/-/core-5.0.1.tgz#6145c898a86a6d56a169611c3e9657a8b97c7642"
integrity sha512-fJ53arfgtzCDZa8DuGJhpTZ3Ll9A1uW5nQ2jSJnfO4Hl5MRD2cP8P4vPvIUAGbdbjwCxR1jat6cW8OloMJkJXw== integrity sha512-4g5ZyFAin8QywK4+0FK1uXG3GLRPu0oc3xbP+7OUhhFxbwpzFuaJtKmnTofMqLy9/pHH6Bl/7H0/DTVH3cpFkA==
dependencies: dependencies:
axios "^1.4.0" axios "^1.4.0"
find-up "^6.3.0" find-up "^6.3.0"
form-data "^4.0.0" form-data "^4.0.4"
node-gyp-build "^4.6.0" node-gyp-build "^4.6.0"
"@cspell/cspell-bundled-dicts@9.1.3": "@cspell/cspell-bundled-dicts@9.1.3":
@ -3878,7 +3878,7 @@ fork-ts-checker-webpack-plugin@^9.0.2:
semver "^7.3.5" semver "^7.3.5"
tapable "^2.2.1" tapable "^2.2.1"
form-data@^4.0.0, form-data@^4.0.4: form-data@^4.0.4:
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==