Skip to content
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

feat: add built in .env support #4392

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
Expand Up @@ -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.
Expand Down
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
Expand Up @@ -177,6 +177,7 @@ type WebpackDevServerOptions = DevServerConfig &
disableInterpret?: boolean;
extends?: string[];
argv: Argv;
envFile?: boolean;
};

type Callback<T extends unknown[]> = (...args: T) => void;
Expand Down Expand Up @@ -239,6 +240,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;
Expand Down Expand Up @@ -318,6 +324,7 @@ export {
CallableWebpackConfiguration,
Callback,
CLIPluginOptions,
DotenvPluginOptions,
CommandAction,
CommanderOption,
CommandOptions,
Expand Down
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;
}
}
24 changes: 24 additions & 0 deletions packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
RechoirError,
Argument,
Problem,
DotenvPluginOptions,
} from "./types";

import type webpackMerge from "webpack-merge";
Expand All @@ -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");
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -2336,6 +2359,7 @@ class WebpackCLI implements IWebpackCLI {
analyze: options.analyze,
isMultiCompiler: Array.isArray(config.options),
}),
new DotenvPlugin(),
);
};

Expand Down
Loading