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: dot-env plugin #4113

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
93a45c8
test
info-arnav Mar 13, 2024
80d7233
Merge branch 'master' of https://github.com/info-arnav/webpack-cli
info-arnav Mar 13, 2024
4355ebc
add: add boilerplate script
info-arnav Mar 13, 2024
5704492
add: react boilerplate
info-arnav Mar 13, 2024
1591b8d
add: react boilerplate
info-arnav Mar 13, 2024
78c59c8
feat: dot env plugin
info-arnav Mar 14, 2024
e3c4bb4
dot-env
info-arnav Mar 14, 2024
14062c5
feat: dot-env support
info-arnav Mar 20, 2024
41385a0
feat: dot-env plugin
info-arnav Mar 23, 2024
7c1811f
feat: dot-env plugin
info-arnav Mar 23, 2024
06bc78d
Merge branch 'master' into dot-env
info-arnav Mar 25, 2024
ef9e163
fix: changed the directory and removed silent
info-arnav Mar 25, 2024
132d757
Merge branch 'dot-env' of https://github.com/info-arnav/webpack-cli i…
info-arnav Mar 25, 2024
ad0f842
fix: yarn.lock conflict
info-arnav Mar 25, 2024
8ee2144
feat: multiple prefix
info-arnav Mar 25, 2024
9fd7dca
feat: multiple prefix
info-arnav Mar 25, 2024
98499d3
feat: added comment for future aspect
info-arnav Mar 25, 2024
f4f53b8
fix: removed loadconfig from webpack-cli
info-arnav Mar 26, 2024
e471b42
feat: added the --dot-env arg
info-arnav Mar 27, 2024
1bb6371
fix :changed the dotenv-webpack-plugin directory
info-arnav Mar 27, 2024
e60dffb
fix: added test -> positive
info-arnav Mar 27, 2024
75ec38f
fix: added webpack logger
info-arnav Mar 27, 2024
4c53402
fix: added tests
info-arnav Mar 27, 2024
9d2cae1
fix: used Map for cache
info-arnav Mar 28, 2024
7ed4652
fix: private changed with # and cache removed
info-arnav Mar 28, 2024
efeff89
fix: cache implimented properly
info-arnav Mar 28, 2024
7491651
fix: type target string
info-arnav Mar 28, 2024
6269163
fix: reolved issues
info-arnav Mar 29, 2024
67d08db
Merge branch 'master' into dot-env
info-arnav Mar 29, 2024
5ae5ffa
fix: reolved all the issues, tests pending
info-arnav Apr 3, 2024
143344b
fix: renamed config to options
info-arnav Apr 3, 2024
f214c62
feat: added safe method
info-arnav Apr 3, 2024
da567e9
fix: replaced for-each with for..of
info-arnav Apr 4, 2024
903104b
fix: resolved all issues except hook
info-arnav Apr 6, 2024
61fc823
fix: removed async, and made small changes:
info-arnav Apr 6, 2024
b3049cf
fix: compilation is defined
info-arnav Apr 6, 2024
f45788a
fix: corrected types
info-arnav Apr 6, 2024
7c4e8c8
test: added tests for safe, expand and empty modes
info-arnav Apr 12, 2024
5a1e693
fix: resolved small changes
info-arnav Apr 12, 2024
969181e
Merge branch 'master' into dot-env
info-arnav Apr 12, 2024
a51a1fc
fix: removed compilation hook
info-arnav Apr 13, 2024
397d73a
Merge branch 'dot-env' of https://github.com/info-arnav/webpack-cli i…
info-arnav Apr 13, 2024
78eac39
adding a compilation hook inside the initialize hook
info-arnav Apr 21, 2024
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 .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"version": "0.2",
"language": "en,en-gb",
"words": [
"systemvars",
"Atsumu",
"autoprefixer",
"barbaz",
Expand Down
Empty file modified .husky/commit-msg
100755 → 100644
Empty file.
Empty file modified .husky/pre-commit
100755 → 100644
Empty file.
1 change: 1 addition & 0 deletions SERVE-OPTIONS-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Options:
--watch-files-reset Clear all items provided in 'watchFiles' configuration. Allows to configure list of globs/directories/files to watch for file changes.
--no-web-socket-server Disallows to set web socket server and options.
--web-socket-server-type <value> Allows to set web socket server and options (by default 'ws').
--dot-env Allows env support to webpack.

Global options:
--color Enable colors on console.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"get-port": "^5.1.1",
"husky": "^8.0.1",
"internal-ip": "^6.2.0",
"jest": "^29.4.1",
"jest": "^29.7.0",
"jest-watch-typeahead": "^2.2.2",
"lerna": "^6.0.1",
"lint-staged": "^13.0.3",
Expand Down
1 change: 1 addition & 0 deletions packages/webpack-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"colorette": "^2.0.14",
"commander": "^10.0.1",
"cross-spawn": "^7.0.3",
"dotenv": "^16.4.5",
"envinfo": "^7.10.0",
"fastest-levenshtein": "^1.0.12",
"import-local": "^3.0.2",
Expand Down
220 changes: 220 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,220 @@
import dotenv from "dotenv";
import { Compiler, DefinePlugin, cache } from "webpack";

interface EnvVariables {
[key: string]: string;
}
interface DotenvConfig {
info-arnav marked this conversation as resolved.
Show resolved Hide resolved
paths?: string[];
prefixes?: string[];
systemvars?: boolean;
allowEmptyValues?: boolean;
info-arnav marked this conversation as resolved.
Show resolved Hide resolved
expand?: boolean;
info-arnav marked this conversation as resolved.
Show resolved Hide resolved
ignoreStub?: boolean;
safe?: boolean;
}

const interpolate = (env: string, vars: EnvVariables): string => {
const matches = env.match(/\$([a-zA-Z0-9_]+)|\${([a-zA-Z0-9_]+)}/g) || [];
matches.forEach((match) => {
env = env.replace(match, interpolate(vars[match.replace(/\$|{|}/g, "")] || "", vars));
});
return env;
};

const isMainThreadElectron = (target: string | undefined): boolean =>
!!target && target.startsWith("electron") && target.endsWith("main");

export class Dotenv {
#options: DotenvConfig;
#inputFileSystem!: any;
#compiler!: Compiler;
#logger: any;
#cache!: any;
constructor(config: DotenvConfig) {
this.#options = {
prefixes: ["process.env.", "import.meta.env."],
allowEmptyValues: true,
expand: true,
safe: true,
paths: process.env.NODE_ENV
? [
".env",
...(process.env.NODE_ENV === "test" ? [] : [".env.local"]),
`.env.[mode]`,
`.env.[mode].local`,
]
: [],
...config,
};
}

public apply(compiler: Compiler): void {
this.#inputFileSystem = compiler.inputFileSystem;
this.#logger = compiler.getInfrastructureLogger("dotenv-webpack-plugin");
this.#compiler = compiler;
compiler.hooks.initialize.tap("dotenv-webpack-plugin", () => {
this.#execute();
// Not sure if this part is is a correct approach
compiler.hooks.compilation.tap("dotenv-webpack-plugin", async (compilation) => {
this.#cache = await compilation.getCache("dotenv-webpack-plugin-cache");
if (this.#cache) {
new DefinePlugin(this.#cache).apply(compiler);
} else {
this.#execute();
}
});
});
}

#execute() {
const variables = this.#gatherVariables();
const target: string =
typeof this.#compiler.options.target == "string" ? this.#compiler.options.target : "";
const data = this.#formatData({
variables,
target: target,
});
new DefinePlugin(data).apply(this.#compiler);
}

#gatherVariables(compilation?: any): EnvVariables {
const { allowEmptyValues, safe } = this.#options;
const vars: EnvVariables = this.#initializeVars();

const { env, blueprint } = this.#getEnvs(compilation);

Object.keys(blueprint).forEach((key) => {
const value = Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : env[key];

if (
(typeof value === "undefined" || value === null || (!allowEmptyValues && value === "")) &&
safe
) {
compilation?.errors.push(new Error(`Missing environment variable: ${key}`));
} else {
vars[key] = value;
}
if (safe) {
Object.keys(env).forEach((key) => {
if (!Object.prototype.hasOwnProperty.call(vars, key)) {
vars[key] = env[key];
}
});
}
});

return vars;
}

#initializeVars(): EnvVariables {
return this.#options.systemvars
? Object.fromEntries(Object.entries(process.env).map(([key, value]) => [key, value ?? ""]))
: {};
}

#getEnvs(compilation?: any): { env: EnvVariables; blueprint: EnvVariables } {
const { paths, safe } = this.#options;

const env: EnvVariables = {};
let blueprint: EnvVariables = {};

for (const path of paths || []) {
const fileContent = this.#loadFile(
path.replace("[mode]", `${process.env.NODE_ENV || this.#compiler.options.mode}`),
compilation,
);
Object.assign(env, dotenv.parse(fileContent));
}
blueprint = env;
if (safe) {
for (const path of paths || []) {
const exampleContent = this.#loadFile(
`${path.replace(
"[mode]",
`${process.env.NODE_ENV || this.#compiler.options.mode}`,
)}.example`,
);
blueprint = { ...blueprint, ...dotenv.parse(exampleContent) };
}
}
return { env, blueprint };
}

#formatData({
variables = {},
target,
}: {
variables: EnvVariables;
target: string;
}): Record<string, string> {
const { expand, prefixes } = this.#options;

const preprocessedVariables: EnvVariables = Object.keys(variables).reduce(
(obj: EnvVariables, key: string) => {
let value = variables[key];
if (expand) {
if (value.startsWith("\\$")) {
value = value.substring(1);
} else if (value.includes("\\$")) {
value = value.replace(/\\\$/g, "$");
} else {
value = interpolate(value, variables);
}
}
obj[key] = JSON.stringify(value);
return obj;
},
{},
);

const formatted: Record<string, string> = {};
if (prefixes) {
prefixes.forEach((prefix) => {
Object.entries(preprocessedVariables).forEach(([key, value]) => {
formatted[`${prefix}${key}`] = value;
});
});
}

const shouldStubEnv =
prefixes?.includes("process.env.") && this.#shouldStub({ target, prefix: "process.env." });
info-arnav marked this conversation as resolved.
Show resolved Hide resolved
if (shouldStubEnv) {
formatted["process.env"] = '"MISSING_ENV_VAR"';
}

return formatted;
}

#shouldStub({
target: targetInput,
prefix,
}: {
target: string | string[] | undefined;
prefix: string;
}): boolean {
const targets: string[] = Array.isArray(targetInput) ? targetInput : [targetInput || ""];

return targets.every(
(target) =>
prefix === "process.env." &&
this.#options.ignoreStub !== true &&
(this.#options.ignoreStub === false ||
(!target.includes("node") && !isMainThreadElectron(target))),
);
}

#loadFile(filePath: string, compilation?: any): Buffer | string {
try {
const fileContent = this.#inputFileSystem.readFileSync(filePath);
compilation?.buildDependencies.add(filePath);
return fileContent;
} catch (err: any) {
compilation?.missingDependencies.add(filePath);
this.#logger.log(`Unable to upload ${filePath} file due:\n ${err.toString()}`);
return "{}";
info-arnav marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

module.exports = Dotenv;
1 change: 1 addition & 0 deletions packages/webpack-cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { type stringifyStream } from "@discoveryjs/json-ext";
*/

interface IWebpackCLI {
dotEnv: boolean;
colors: WebpackCLIColors;
logger: WebpackCLILogger;
isColorSupportChanged: boolean | undefined;
Expand Down
20 changes: 19 additions & 1 deletion packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,14 @@ class WebpackCLI implements IWebpackCLI {
colors: WebpackCLIColors;
logger: WebpackCLILogger;
isColorSupportChanged: boolean | undefined;
dotEnv: boolean;
builtInOptionsCache: WebpackCLIBuiltInOption[] | undefined;
webpack!: typeof webpack;
program: WebpackCLICommand;
constructor() {
this.colors = this.createColors();
this.logger = this.getLogger();

this.dotEnv = false;
// Initialize program
this.program = program;
this.program.name("webpack");
Expand Down Expand Up @@ -1381,6 +1382,17 @@ class WebpackCLI implements IWebpackCLI {
"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
);

// webpack-cli add dot-env plugin

this.program.option(
"--dot-env",
"Integrates dotenv configuration into your webpack configuration.",
);

this.program.on("option:dot-env", function () {
cli.dotEnv = true;
});

// webpack-cli has it's own logic for showing suggestions
this.program.showSuggestionAfterError(false);

Expand Down Expand Up @@ -2359,6 +2371,12 @@ class WebpackCLI implements IWebpackCLI {
isMultiCompiler: Array.isArray(config.options),
}),
);

// Add dotenv plugin to the config
if (this.dotEnv) {
const Dotenv = require("./plugins/dotenv-webpack-plugin");
item.plugins.unshift(new Dotenv());
}
};

if (Array.isArray(config.options)) {
Expand Down
Empty file modified test/api/capitalizeFirstLetter.test.js
100755 → 100644
Empty file.
Empty file modified test/api/generators/scaffold-utils.test.js
100755 → 100644
Empty file.
50 changes: 50 additions & 0 deletions test/dotenv/dotenv-empty.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use strict";

const { run } = require("../utils/test-utils");
const path = require("path");
const fs = require("fs");

describe("dotenv", () => {
const testDirectory = path.join(__dirname, "test-dot-env");
const outputFile = path.join(testDirectory, "output.js");

beforeAll(async () => {
if (!fs.existsSync(testDirectory)) {
fs.mkdirSync(testDirectory);
}
await fs.promises.writeFile(
path.join(testDirectory, "webpack.config.js"),
`
const path = require('path');
module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname),
filename: 'output.js'
},
};
`,
);
await fs.promises.writeFile(
path.join(testDirectory, "index.js"),
"module.exports = import.meta.env.TEST_VARIABLE;",
);
await fs.promises.writeFile(path.join(testDirectory, ".env"), "TEST_VARIABLE=");
});

afterAll(() => {
fs.unlinkSync(path.join(testDirectory, "webpack.config.js"));
fs.unlinkSync(path.join(testDirectory, "index.js"));
fs.unlinkSync(path.join(testDirectory, ".env"));
if (fs.existsSync(outputFile)) {
fs.unlinkSync(outputFile);
}
fs.rmdirSync(testDirectory);
});

it("should refer to the example file", async () => {
await run(testDirectory, ["--dot-env"]);
const output = fs.readFileSync(outputFile, "utf-8");
expect(output).toContain('exports=""');
});
});
Loading