Skip to content

Commit fd02fa0

Browse files
committed
feat: initial implementation of .env support
- Load env vars at build time and run time
1 parent 6246663 commit fd02fa0

File tree

6 files changed

+2816
-1636
lines changed

6 files changed

+2816
-1636
lines changed

packages/webpack-cli/src/bootstrap.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { type IWebpackCLI } from "./types";
2+
import { EnvLoader } from "./utils/env-loader";
23

34
const WebpackCLI = require("./webpack-cli");
45

56
const runCLI = async (args: Parameters<IWebpackCLI["run"]>[0]) => {
7+
// Load environment variables from .env files
8+
try {
9+
EnvLoader.loadEnvFiles();
10+
} catch (error) {
11+
console.warn("Warning: Error loading environment variables:", error);
12+
}
13+
614
// Create a new instance of the CLI object
715
const cli: IWebpackCLI = new WebpackCLI();
816

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Compiler, DefinePlugin } from "webpack";
2+
import type { DotenvPluginOptions } from "../types";
3+
import { EnvLoader } from "../utils/env-loader";
4+
5+
export class DotenvPlugin {
6+
logger!: ReturnType<Compiler["getInfrastructureLogger"]>;
7+
options: DotenvPluginOptions;
8+
9+
constructor(options: DotenvPluginOptions) {
10+
this.options = options;
11+
}
12+
13+
apply(compiler: Compiler) {
14+
this.logger = compiler.getInfrastructureLogger("DotenvPlugin");
15+
16+
try {
17+
const env = EnvLoader.loadEnvFiles({
18+
mode: process.env.NODE_ENV,
19+
prefixes: this.options.prefixes,
20+
});
21+
22+
new DefinePlugin(
23+
Object.fromEntries(
24+
Object.entries(env).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
25+
),
26+
).apply(compiler);
27+
} catch (error) {
28+
this.logger.error(error);
29+
}
30+
}
31+
}
32+
33+
module.exports = DotenvPlugin;

packages/webpack-cli/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ interface CLIPluginOptions {
239239
analyze?: boolean;
240240
}
241241

242+
interface DotenvPluginOptions {
243+
prefixes?: string | string[];
244+
}
245+
242246
type BasicPrimitive = string | boolean | number;
243247
type Instantiable<InstanceType = unknown, ConstructorParameters extends unknown[] = unknown[]> = {
244248
new (...args: ConstructorParameters): InstanceType;
@@ -318,6 +322,7 @@ export {
318322
CallableWebpackConfiguration,
319323
Callback,
320324
CLIPluginOptions,
325+
DotenvPluginOptions,
321326
CommandAction,
322327
CommanderOption,
323328
CommandOptions,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import path from "path";
2+
import { normalize } from "path";
3+
// eslint-disable-next-line
4+
import { loadEnvFile } from "process";
5+
6+
export interface EnvLoaderOptions {
7+
mode?: string;
8+
envDir?: string;
9+
prefixes?: string | string[];
10+
}
11+
12+
export class EnvLoader {
13+
static getEnvFilePaths(mode = "development", envDir = process.cwd()): string[] {
14+
return [
15+
`.env`, // default file
16+
`.env.local`, // local file
17+
`.env.${mode}`, // mode file
18+
`.env.${mode}.local`, // mode local file
19+
].map((file) => normalize(path.join(envDir, file)));
20+
}
21+
22+
static loadEnvFiles(options: EnvLoaderOptions = {}): Record<string, string> {
23+
const {
24+
mode = process.env.NODE_ENV || "development",
25+
envDir = process.cwd(),
26+
prefixes,
27+
} = options;
28+
29+
const normalizedPrefixes = prefixes
30+
? Array.isArray(prefixes)
31+
? prefixes
32+
: [prefixes]
33+
: undefined;
34+
35+
if (mode === "local") {
36+
throw new Error(
37+
'"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.',
38+
);
39+
}
40+
41+
const envFiles = this.getEnvFilePaths(mode, envDir);
42+
const env: Record<string, string> = {};
43+
44+
// Load all env files
45+
envFiles.forEach((filePath) => {
46+
try {
47+
loadEnvFile(filePath);
48+
} catch {
49+
// Skip if file doesn't exist
50+
}
51+
});
52+
53+
// If prefixes are specified, filter environment variables
54+
if (normalizedPrefixes?.length) {
55+
for (const [key, value] of Object.entries(process.env)) {
56+
if (normalizedPrefixes.some((prefix) => key.startsWith(prefix))) {
57+
env[key] = value as string;
58+
}
59+
}
60+
return env;
61+
}
62+
63+
// Return all environment variables if no prefixes specified
64+
return { ...process.env } as Record<string, string>;
65+
}
66+
}

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {
3939
RechoirError,
4040
Argument,
4141
Problem,
42+
DotenvPluginOptions,
4243
} from "./types";
4344

4445
import type webpackMerge from "webpack-merge";
@@ -55,6 +56,7 @@ import { type stringifyChunked } from "@discoveryjs/json-ext";
5556
import { type Help, type ParseOptions } from "commander";
5657

5758
import { type CLIPlugin as CLIPluginClass } from "./plugins/cli-plugin";
59+
import { type DotenvPlugin as DotenvPluginClass } from "./plugins/dotenv-plugin";
5860

5961
const fs = require("fs");
6062
const { Readable } = require("stream");
@@ -2170,6 +2172,11 @@ class WebpackCLI implements IWebpackCLI {
21702172
"./plugins/cli-plugin",
21712173
);
21722174

2175+
const DotenvPlugin =
2176+
await this.tryRequireThenImport<Instantiable<DotenvPluginClass, [DotenvPluginOptions]>>(
2177+
"./plugins/dotenv-plugin",
2178+
);
2179+
21732180
const internalBuildConfig = (item: WebpackConfiguration) => {
21742181
const originalWatchValue = item.watch;
21752182

@@ -2343,6 +2350,9 @@ class WebpackCLI implements IWebpackCLI {
23432350
analyze: options.analyze,
23442351
isMultiCompiler: Array.isArray(config.options),
23452352
}),
2353+
new DotenvPlugin({
2354+
prefixes: ["WEBPACK_"],
2355+
}),
23462356
);
23472357
}
23482358
};

0 commit comments

Comments
 (0)