Skip to content

feat: add built in .env support #4392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OPTIONS.md
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ Options:
--config-name <name...> Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.
-m, --merge Merge two or more configurations using 'webpack-merge'.
--disable-interpret Disable interpret for loading the config file.
--env-file Load environment variables from .env files for access within the configuration.
--env <value...> Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".
--node-env <value> Sets process.env.NODE_ENV to the specified value for access within the configuration.(Deprecated: Use '--config-node-env' instead)
--config-node-env <value> Sets process.env.NODE_ENV to the specified value for access within the configuration.
98 changes: 98 additions & 0 deletions packages/dotenv-webpack-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Dotenv Webpack Plugin

`DotenvWebpackPlugin` is a webpack plugin to allow consumers to load environment variables from `.env` files. It does a text replace in the resulting bundle for any instances of `process.env` and `import.meta.env`.

## Installation

```bash
npm install dotenv-webpack-plugin --save-dev
```

```bash
yarn add dotenv-webpack-plugin --dev
```

```bash
pnpm add dotenv-webpack-plugin -D
```

## Basic Usage

`DotenvWebpackPlugin` exposes env variables under `process.env` and `import.meta.env` objects as strings automatically.

To prevent accidentally leaking env variables to the client, only variables prefixed with `WEBPACK_` are exposed to Webpack-bundled code. e.g. for the following `.env` file:

```bash
WEBPACK_SOME_KEY=1234567890
SECRET_KEY=abcdefg
```

Only `WEBPACK_SOME_KEY` is exposed to Webpack-bundled code as `import.meta.env.WEBPACK_SOME_KEY` and `process.env.WEBPACK_SOME_KEY`, but `SECRET_KEY` is not.

```javascript
const DotenvWebpackPlugin = require("dotenv-webpack-plugin");

module.exports = {
// Existing configuration options...
plugins: [new DotenvWebpackPlugin()],
};
```

## `.env` Files

Environment variables are loaded from the following files in your [environment directory]():

```
.env # loaded in all cases
.env.local # loaded in all cases, ignored by git
.env.[mode] # only loaded in specified mode
.env.[mode].local # only loaded in specified mode, ignored by git
```

> Mode-specific env variables (e.g., `.env.production`) will override conflicting variables from generic environment files (e.g., `.env`). Variables that are only defined in `.env` or `.env.local` will remain available to the client.

## Using a configuration

You can pass a configuration object to `dotenv-webpack-plugin` to customize its behavior.

### `dir`

Type:

```ts
type dir = string;
```

Default: `""`

Specifies the directory where the plugin should look for environment files. By default, it looks in the root directory.

```js
new DotenvWebpackPlugin({
dir: "./config/env",
});
```

### `prefix`

Type:

```ts
type prefix = string | string[];
```

Default: `undefined`

Defines which environment variables should be exposed to the client code based on their prefix. This is a critical security feature to prevent sensitive information from being exposed.

```js
// Single prefix
new DotenvWebpackPlugin({
prefix: "PUBLIC_",
});

// Multiple prefixes
new DotenvWebpackPlugin({
prefix: ["PUBLIC_", "SHARED_"],
});
```
50 changes: 50 additions & 0 deletions packages/webpack-cli/src/plugins/dotenv-webpack-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Compiler, DefinePlugin } from "webpack";
import type { DotenvPluginOptions } from "../types";
import { EnvLoader } from "../utils/env-loader";

export class DotenvPlugin {
logger!: ReturnType<Compiler["getInfrastructureLogger"]>;
options: DotenvPluginOptions;

constructor(options: DotenvPluginOptions = {}) {
this.options = options;
}
apply(compiler: Compiler) {
this.logger = compiler.getInfrastructureLogger("DotenvPlugin");

try {
const env = EnvLoader.loadEnvFiles({
mode: process.env.NODE_ENV,
prefix: this.options.prefix,
dir: this.options.dir,
});
const envObj = JSON.stringify(env);

const runtimeEnvObject = `(() => {
const env = ${envObj};
// Make it read-only
return Object.freeze(env);
})()`;

const definitions = {
"process.env": envObj,
...Object.fromEntries(
Object.entries(env).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
),
"import.meta.env": runtimeEnvObject,
...Object.fromEntries(
Object.entries(env).map(([key, value]) => [
`import.meta.env.${key}`,
JSON.stringify(value),
]),
),
};

new DefinePlugin(definitions).apply(compiler);
} catch (error) {
this.logger.error(error);
}
}
}

module.exports = DotenvPlugin;
7 changes: 7 additions & 0 deletions packages/webpack-cli/src/types.ts
Original file line number Diff line number Diff line change
@@ -177,6 +177,7 @@ type WebpackDevServerOptions = DevServerConfig &
disableInterpret?: boolean;
extends?: string[];
argv: Argv;
envFile?: boolean;
};

type Callback<T extends unknown[]> = (...args: T) => void;
@@ -244,6 +245,11 @@ interface CLIPluginOptions {
analyze?: boolean;
}

interface DotenvPluginOptions {
prefix?: string | string[];
dir?: string;
}

type BasicPrimitive = string | boolean | number;
type Instantiable<InstanceType = unknown, ConstructorParameters extends unknown[] = unknown[]> = {
new (...args: ConstructorParameters): InstanceType;
@@ -323,6 +329,7 @@ export {
CallableWebpackConfiguration,
Callback,
CLIPluginOptions,
DotenvPluginOptions,
CommandAction,
CommanderOption,
CommandOptions,
54 changes: 54 additions & 0 deletions packages/webpack-cli/src/utils/env-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import path from "path";
import { normalize } from "path";
// eslint-disable-next-line
import { loadEnvFile } from "process";

export interface EnvLoaderOptions {
mode?: string;
dir?: string;
prefix?: string | string[];
}

export class EnvLoader {
static getEnvFilePaths(mode = "development", envDir = process.cwd()): string[] {
return [
`.env`, // default file
`.env.local`, // local file
`.env.${mode}`, // mode file
`.env.${mode}.local`, // mode local file
].map((file) => normalize(path.join(envDir, file)));
}

static loadEnvFiles(options: EnvLoaderOptions = {}): Record<string, string> {
const { mode = process.env.NODE_ENV, dir = process.cwd(), prefix } = options;

const normalizedPrefixes = prefix ? (Array.isArray(prefix) ? prefix : [prefix]) : ["WEBPACK_"];

if (mode === "local") {
throw new Error(
'"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.',
);
}

const envFiles = this.getEnvFilePaths(mode, dir);
const env: Record<string, string> = {};

// Load all env files
envFiles.forEach((filePath) => {
try {
loadEnvFile(filePath);
} catch {
// Skip if file doesn't exist
}
});

// Filter env vars based on prefix
for (const [key, value] of Object.entries(process.env)) {
if (normalizedPrefixes.some((prefix) => key.startsWith(prefix))) {
env[key] = value as string;
}
}

return env;
}
}
26 changes: 26 additions & 0 deletions packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ import type {
RechoirError,
Argument,
Problem,
DotenvPluginOptions,
} from "./types";

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

import { type CLIPlugin as CLIPluginClass } from "./plugins/cli-plugin";
import { type DotenvPlugin as DotenvPluginClass } from "./plugins/dotenv-webpack-plugin";
import { EnvLoader } from "./utils/env-loader";

const fs = require("fs");
const { Readable } = require("stream");
@@ -1019,6 +1022,18 @@ class WebpackCLI implements IWebpackCLI {
description: "Print compilation progress during build.",
helpLevel: "minimum",
},
{
name: "env-file",
configs: [
{
type: "enum",
values: [true],
},
],
description:
"Load environment variables from .env files for access within the configuration.",
helpLevel: "minimum",
},

// Output options
{
@@ -1791,6 +1806,10 @@ class WebpackCLI implements IWebpackCLI {
}

async loadConfig(options: Partial<WebpackDevServerOptions>) {
if (options.envFile) {
EnvLoader.loadEnvFiles();
}

const disableInterpret =
typeof options.disableInterpret !== "undefined" && options.disableInterpret;

@@ -2170,6 +2189,10 @@ class WebpackCLI implements IWebpackCLI {
"./plugins/cli-plugin",
);

const DotenvPlugin = await this.tryRequireThenImport<
Instantiable<DotenvPluginClass, [DotenvPluginOptions?]>
>("./plugins/dotenv-webpack-plugin");

const internalBuildConfig = (item: WebpackConfiguration) => {
const originalWatchValue = item.watch;

@@ -2341,6 +2364,9 @@ class WebpackCLI implements IWebpackCLI {
analyze: options.analyze,
isMultiCompiler: Array.isArray(config.options),
}),
new DotenvPlugin({
prefixes: ["WEBPACK_"],
}),
);
}
};
195 changes: 98 additions & 97 deletions yarn.lock

Large diffs are not rendered by default.