diff --git a/cspell.json b/cspell.json index 904d24be1..6d30f60a6 100644 --- a/cspell.json +++ b/cspell.json @@ -315,7 +315,8 @@ "spacek", "thelarkinn", "behaviour", - "WHATWG" + "WHATWG", + "systemvars" ], "ignoreRegExpList": [ "/Author.+/", diff --git a/declarations/plugins/DotenvPlugin.d.ts b/declarations/plugins/DotenvPlugin.d.ts new file mode 100644 index 000000000..2d37dac44 --- /dev/null +++ b/declarations/plugins/DotenvPlugin.d.ts @@ -0,0 +1,36 @@ +/* + * This file was automatically generated. + * DO NOT MODIFY BY HAND. + * Run `yarn fix:special` to update + */ + +export interface DotenvPluginOptions { + /** + * Whether to allow empty strings in safe mode. If false, will throw an error if any env variables are empty (but only if safe mode is enabled). + */ + allowEmptyValues?: boolean; + /** + * Adds support for dotenv-defaults. If set to true, uses ./.env.defaults. If a string, uses that location for a defaults file. + */ + defaults?: boolean | string; + /** + * Allows your variables to be "expanded" for reusability within your .env file. + */ + expand?: boolean; + /** + * The path to your environment variables. This same path applies to the .env.example and .env.defaults files. + */ + path?: string; + /** + * The prefix to use before the name of your env variables. + */ + prefix?: string; + /** + * If true, load '.env.example' to verify the '.env' variables are all set. Can also be a string to a different file. + */ + safe?: boolean | string; + /** + * Set to true if you would rather load all system variables as well (useful for CI purposes). + */ + systemvars?: boolean; +} diff --git a/lib/DotenvPlugin.js b/lib/DotenvPlugin.js new file mode 100644 index 000000000..d5bcac90e --- /dev/null +++ b/lib/DotenvPlugin.js @@ -0,0 +1,345 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Natsu @xiaoxiaojx +*/ + +"use strict"; + +const createSchemaValidation = require("./util/create-schema-validation"); +const { isAbsolute, join } = require("./util/fs"); + +/** @typedef {import("./Compiler")} Compiler */ +/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */ +/** @typedef {import("../declarations/plugins/DotenvPlugin").DotenvPluginOptions} DotenvPluginOptions */ + +const DEFAULT_PATH = "./.env"; + +const DEFAULT_OPTIONS = { + path: DEFAULT_PATH, + prefix: "process.env." +}; + +const LINE = + /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; + +const PLUGIN_NAME = "DotenvPlugin"; + +const validate = createSchemaValidation( + require("../schemas/plugins/DotenvPlugin.check"), + () => require("../schemas/plugins/DotenvPlugin.json"), + { + name: "Dotenv Plugin", + baseDataPath: "options" + } +); + +/** + * @param {string} env environment variable value + * @param {Record} vars variables object + * @returns {string} interpolated value + */ +const interpolate = (env, vars) => { + const matches = env.match(/\$([a-zA-Z0-9_]+)|\${([a-zA-Z0-9_]+)}/g) || []; + + for (const match of matches) { + const key = match.replace(/\$|{|}/g, ""); + let variable = vars[key] || ""; + variable = interpolate(variable, vars); + env = env.replace(match, variable); + } + + return env; +}; + +/** + * ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L49 + * @param {string|Buffer} src the source content to parse + * @returns {Record} parsed environment variables object + */ +function parse(src) { + const obj = /** @type {Record} */ ({}); + + // 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; +} + +/** + * Parses objects like before, but with defaults! + * @param {string} src the original src + * @param {string=} defaultSrc the new-and-improved default source + * @returns {Record} the parsed results + */ +const mergeParse = (src, defaultSrc = "") => { + const parsedSrc = parse(src); + const parsedDefault = parse(defaultSrc); + + return { ...parsedDefault, ...parsedSrc }; +}; + +// ported from https://github.com/mrsteele/dotenv-webpack +class DotenvPlugin { + /** + * @param {DotenvPluginOptions=} options options object + */ + constructor(options = {}) { + validate(options); + this.config = { ...DEFAULT_OPTIONS, ...options }; + } + + /** + * Apply the plugin + * @param {Compiler} compiler the compiler instance + * @returns {void} + */ + apply(compiler) { + compiler.hooks.beforeCompile.tapAsync(PLUGIN_NAME, (_params, callback) => { + const inputFileSystem = /** @type {InputFileSystem} */ ( + compiler.inputFileSystem + ); + const context = compiler.context; + + this.gatherVariables(inputFileSystem, context, (err, variables) => { + if (err) return callback(err); + + const definitions = this.formatDefinitions(variables || {}); + const DefinePlugin = compiler.webpack.DefinePlugin; + + new DefinePlugin(definitions).apply(compiler); + callback(); + }); + }); + } + + /** + * @param {InputFileSystem} inputFileSystem the input file system + * @param {string} context the compiler context + * @param {(err: Error | null, variables?: Record) => void} callback callback function + * @returns {void} + */ + gatherVariables(inputFileSystem, context, callback) { + const { safe, allowEmptyValues } = /** @type {DotenvPluginOptions} */ ( + this.config + ); + const vars = /** @type {Record} */ (this.initializeVars()); + + this.getEnvs(inputFileSystem, context, (err, result) => { + if (err) return callback(err); + if (!result) { + return callback(new Error("Failed to get environment variables")); + } + + const { env, blueprint } = result; + + try { + for (const key of Object.keys(blueprint)) { + const value = Object.prototype.hasOwnProperty.call(vars, key) + ? vars[key] + : env[key]; + + const isMissing = + typeof value === "undefined" || + value === null || + (!allowEmptyValues && value === ""); + + if (safe && isMissing) { + throw new Error(`Missing environment variable: ${key}`); + } else { + vars[key] = value; + } + } + + // add the leftovers + if (safe) { + for (const key of Object.keys(env)) { + if (!Object.prototype.hasOwnProperty.call(vars, key)) { + vars[key] = env[key]; + } + } + } + + callback(null, vars); + } catch (error) { + callback(error instanceof Error ? error : new Error(String(error))); + } + }); + } + + initializeVars() { + const config = /** @type {DotenvPluginOptions} */ (this.config); + if (config.systemvars) { + const vars = /** @type {Record} */ ({}); + for (const key in process.env) { + if (process.env[key] !== undefined) { + vars[key] = /** @type {string} */ (process.env[key]); + } + } + return vars; + } + return /** @type {Record} */ ({}); + } + + /** + * @param {InputFileSystem} inputFileSystem the input file system + * @param {string} context the compiler context + * @param {(err: Error | null, result?: {env: Record, blueprint: Record}) => void} callback callback function + * @returns {void} + */ + getEnvs(inputFileSystem, context, callback) { + const { path, safe } = /** @type {DotenvPluginOptions} */ (this.config); + + // First load the main env file and defaults + this.loadFile( + { + file: path || DEFAULT_PATH, + inputFileSystem, + context + }, + (err, envContent) => { + if (err) return callback(err); + + this.getDefaults(inputFileSystem, context, (err, defaultsContent) => { + if (err) return callback(err); + + const env = mergeParse(envContent || "", defaultsContent || ""); + let blueprint = env; + + if (safe) { + let file = `${path || DEFAULT_PATH}.example`; + if (safe !== true) { + file = safe; + } + + this.loadFile( + { + file, + inputFileSystem, + context + }, + (err, blueprintContent) => { + if (err) return callback(err); + + blueprint = mergeParse(blueprintContent || ""); + callback(null, { env, blueprint }); + } + ); + } else { + callback(null, { env, blueprint }); + } + }); + } + ); + } + + /** + * @param {InputFileSystem} inputFileSystem the input file system + * @param {string} context the compiler context + * @param {(err: Error | null, content?: string) => void} callback callback function + * @returns {void} + */ + getDefaults(inputFileSystem, context, callback) { + const { path, defaults } = /** @type {DotenvPluginOptions} */ (this.config); + + if (defaults) { + const defaultsFile = + defaults === true ? `${path || DEFAULT_PATH}.defaults` : defaults; + this.loadFile( + { + file: defaultsFile, + inputFileSystem, + context + }, + callback + ); + } else { + callback(null, ""); + } + } + + /** + * Load a file with proper path resolution + * @param {object} options options object + * @param {string} options.file the file to load + * @param {InputFileSystem} options.inputFileSystem the input file system + * @param {string} options.context the compiler context for resolving relative paths + * @param {(err: Error | null, content?: string) => void} callback callback function + * @returns {void} + */ + loadFile({ file, inputFileSystem, context }, callback) { + // Resolve relative paths based on compiler context + const resolvedPath = isAbsolute(file) + ? file + : join(inputFileSystem, context, file); + + inputFileSystem.readFile(resolvedPath, "utf8", (err, content) => { + if (err) { + // File doesn't exist, return empty string + callback(null, ""); + } else { + callback(null, /** @type {string} */ (content)); + } + }); + } + + /** + * @param {Record} variables variables object + * @returns {Record} formatted data + */ + formatDefinitions(variables) { + const { expand, prefix } = /** @type {DotenvPluginOptions} */ (this.config); + const formatted = Object.keys(variables).reduce((obj, key) => { + const v = variables[key]; + const vKey = `${prefix}${key}`; + let vValue; + if (expand) { + if (v.slice(0, 2) === "\\$") { + vValue = v.slice(1); + } else if (v.indexOf("\\$") > 0) { + vValue = v.replace(/\\\$/g, "$"); + } else { + vValue = interpolate(v, variables); + } + } else { + vValue = v; + } + + obj[vKey] = JSON.stringify(vValue); + + return obj; + }, /** @type {Record} */ ({})); + + return formatted; + } +} + +module.exports = DotenvPlugin; diff --git a/lib/index.js b/lib/index.js index a8ef0a82d..ac910a701 100644 --- a/lib/index.js +++ b/lib/index.js @@ -232,6 +232,9 @@ module.exports = mergeExports(fn, { get DynamicEntryPlugin() { return require("./DynamicEntryPlugin"); }, + get DotenvPlugin() { + return require("./DotenvPlugin"); + }, get EntryOptionPlugin() { return require("./EntryOptionPlugin"); }, diff --git a/schemas/plugins/DotenvPlugin.check.d.ts b/schemas/plugins/DotenvPlugin.check.d.ts new file mode 100644 index 000000000..930ef155b --- /dev/null +++ b/schemas/plugins/DotenvPlugin.check.d.ts @@ -0,0 +1,7 @@ +/* + * This file was automatically generated. + * DO NOT MODIFY BY HAND. + * Run `yarn fix:special` to update + */ +declare const check: (options: import("../../declarations/plugins/DotenvPlugin").DotenvPluginOptions) => boolean; +export = check; diff --git a/schemas/plugins/DotenvPlugin.check.js b/schemas/plugins/DotenvPlugin.check.js new file mode 100644 index 000000000..971666324 --- /dev/null +++ b/schemas/plugins/DotenvPlugin.check.js @@ -0,0 +1,6 @@ +/* + * This file was automatically generated. + * DO NOT MODIFY BY HAND. + * Run `yarn fix:special` to update + */ +"use strict";function e(r,{instancePath:t="",parentData:s,parentDataProperty:o,rootData:a=r}={}){let n=null,l=0;if(0===l){if(!r||"object"!=typeof r||Array.isArray(r))return e.errors=[{params:{type:"object"}}],!1;{const t=l;for(const t in r)if("allowEmptyValues"!==t&&"defaults"!==t&&"expand"!==t&&"path"!==t&&"prefix"!==t&&"safe"!==t&&"systemvars"!==t)return e.errors=[{params:{additionalProperty:t}}],!1;if(t===l){if(void 0!==r.allowEmptyValues){const t=l;if("boolean"!=typeof r.allowEmptyValues)return e.errors=[{params:{type:"boolean"}}],!1;var i=t===l}else i=!0;if(i){if(void 0!==r.defaults){let t=r.defaults;const s=l,o=l;let a=!1;const f=l;if("boolean"!=typeof t){const e={params:{type:"boolean"}};null===n?n=[e]:n.push(e),l++}var p=f===l;if(a=a||p,!a){const e=l;if(l===e)if("string"==typeof t){if(t.length<1){const e={params:{}};null===n?n=[e]:n.push(e),l++}}else{const e={params:{type:"string"}};null===n?n=[e]:n.push(e),l++}p=e===l,a=a||p}if(!a){const r={params:{}};return null===n?n=[r]:n.push(r),l++,e.errors=n,!1}l=o,null!==n&&(o?n.length=o:n=null),i=s===l}else i=!0;if(i){if(void 0!==r.expand){const t=l;if("boolean"!=typeof r.expand)return e.errors=[{params:{type:"boolean"}}],!1;i=t===l}else i=!0;if(i){if(void 0!==r.path){let t=r.path;const s=l;if(l===s){if("string"!=typeof t)return e.errors=[{params:{type:"string"}}],!1;if(t.length<1)return e.errors=[{params:{}}],!1}i=s===l}else i=!0;if(i){if(void 0!==r.prefix){let t=r.prefix;const s=l;if(l===s){if("string"!=typeof t)return e.errors=[{params:{type:"string"}}],!1;if(t.length<1)return e.errors=[{params:{}}],!1}i=s===l}else i=!0;if(i){if(void 0!==r.safe){let t=r.safe;const s=l,o=l;let a=!1;const p=l;if("boolean"!=typeof t){const e={params:{type:"boolean"}};null===n?n=[e]:n.push(e),l++}var f=p===l;if(a=a||f,!a){const e=l;if(l===e)if("string"==typeof t){if(t.length<1){const e={params:{}};null===n?n=[e]:n.push(e),l++}}else{const e={params:{type:"string"}};null===n?n=[e]:n.push(e),l++}f=e===l,a=a||f}if(!a){const r={params:{}};return null===n?n=[r]:n.push(r),l++,e.errors=n,!1}l=o,null!==n&&(o?n.length=o:n=null),i=s===l}else i=!0;if(i)if(void 0!==r.systemvars){const t=l;if("boolean"!=typeof r.systemvars)return e.errors=[{params:{type:"boolean"}}],!1;i=t===l}else i=!0}}}}}}}}return e.errors=n,0===l}module.exports=e,module.exports.default=e; \ No newline at end of file diff --git a/schemas/plugins/DotenvPlugin.json b/schemas/plugins/DotenvPlugin.json new file mode 100644 index 000000000..24985179a --- /dev/null +++ b/schemas/plugins/DotenvPlugin.json @@ -0,0 +1,53 @@ +{ + "title": "DotenvPluginOptions", + "type": "object", + "additionalProperties": false, + "properties": { + "allowEmptyValues": { + "description": "Whether to allow empty strings in safe mode. If false, will throw an error if any env variables are empty (but only if safe mode is enabled).", + "type": "boolean" + }, + "defaults": { + "description": "Adds support for dotenv-defaults. If set to true, uses ./.env.defaults. If a string, uses that location for a defaults file.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "minLength": 1 + } + ] + }, + "expand": { + "description": "Allows your variables to be \"expanded\" for reusability within your .env file.", + "type": "boolean" + }, + "path": { + "description": "The path to your environment variables. This same path applies to the .env.example and .env.defaults files.", + "type": "string", + "minLength": 1 + }, + "prefix": { + "description": "The prefix to use before the name of your env variables.", + "type": "string", + "minLength": 1 + }, + "safe": { + "description": "If true, load '.env.example' to verify the '.env' variables are all set. Can also be a string to a different file.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "minLength": 1 + } + ] + }, + "systemvars": { + "description": "Set to true if you would rather load all system variables as well (useful for CI purposes).", + "type": "boolean" + } + } +} diff --git a/test/configCases/plugins/dotenv-plugin/.env b/test/configCases/plugins/dotenv-plugin/.env new file mode 100644 index 000000000..e3b5d510f --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/.env @@ -0,0 +1,7 @@ +MY_NODE_ENV=test +API_URL=https://api.example.com +DEBUG=true +PORT=3000 +SECRET_KEY=my-secret-key +EMPTY_VALUE= +INTERPOLATED_VAR=$MY_NODE_ENV-mode diff --git a/test/configCases/plugins/dotenv-plugin/.env.defaults b/test/configCases/plugins/dotenv-plugin/.env.defaults new file mode 100644 index 000000000..6b22457eb --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/.env.defaults @@ -0,0 +1,3 @@ +MY_NODE_ENV=development +PORT=8080 +DEFAULT_VALUE=default-from-defaults diff --git a/test/configCases/plugins/dotenv-plugin/.env.example b/test/configCases/plugins/dotenv-plugin/.env.example new file mode 100644 index 000000000..3815eb149 --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/.env.example @@ -0,0 +1,6 @@ +MY_NODE_ENV= +API_URL= +DEBUG= +PORT= +SECRET_KEY= +REQUIRED_VAR= diff --git a/test/configCases/plugins/dotenv-plugin/basic.js b/test/configCases/plugins/dotenv-plugin/basic.js new file mode 100644 index 000000000..6c3718dd7 --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/basic.js @@ -0,0 +1,10 @@ +"use strict"; + +it("should load basic .env variables", () => { + expect(process.env.MY_NODE_ENV).toBe("test"); + expect(process.env.API_URL).toBe("https://api.example.com"); + expect(process.env.DEBUG).toBe("true"); + expect(process.env.PORT).toBe("3000"); + expect(process.env.SECRET_KEY).toBe("my-secret-key"); + expect(process.env.EMPTY_VALUE).toBe(""); +}); \ No newline at end of file diff --git a/test/configCases/plugins/dotenv-plugin/custom-path.js b/test/configCases/plugins/dotenv-plugin/custom-path.js new file mode 100644 index 000000000..e3a7cbe7a --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/custom-path.js @@ -0,0 +1,11 @@ +"use strict"; + +it("should load from custom path", () => { + // When using .env.example as path, values should be empty (as defined in .env.example) + expect(process.env.MY_NODE_ENV).toBe(""); + expect(process.env.API_URL).toBe(""); + expect(process.env.DEBUG).toBe(""); + expect(process.env.PORT).toBe(""); + expect(process.env.SECRET_KEY).toBe(""); + expect(process.env.REQUIRED_VAR).toBe(""); +}); diff --git a/test/configCases/plugins/dotenv-plugin/custom-prefix.js b/test/configCases/plugins/dotenv-plugin/custom-prefix.js new file mode 100644 index 000000000..877eaf69b --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/custom-prefix.js @@ -0,0 +1,12 @@ +"use strict"; + +it("should use custom prefix", () => { + expect(MY_ENV.MY_NODE_ENV).toBe("test"); + expect(MY_ENV.API_URL).toBe("https://api.example.com"); + expect(MY_ENV.DEBUG).toBe("true"); + expect(MY_ENV.PORT).toBe("3000"); + expect(MY_ENV.SECRET_KEY).toBe("my-secret-key"); + + // process.env should not be defined with custom prefix + expect(typeof process.env.MY_NODE_ENV).toBe("undefined"); +}); diff --git a/test/configCases/plugins/dotenv-plugin/defaults.js b/test/configCases/plugins/dotenv-plugin/defaults.js new file mode 100644 index 000000000..513a39b4c --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/defaults.js @@ -0,0 +1,10 @@ +"use strict"; + +it("should load variables with defaults", () => { + // Main .env values should override defaults + expect(process.env.MY_NODE_ENV).toBe("test"); + expect(process.env.PORT).toBe("3000"); + + // Default values should be used when not in main .env + expect(process.env.DEFAULT_VALUE).toBe("default-from-defaults"); +}); diff --git a/test/configCases/plugins/dotenv-plugin/errors.js b/test/configCases/plugins/dotenv-plugin/errors.js new file mode 100644 index 000000000..12b4cbab0 --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/errors.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = [/Missing environment variable: /]; diff --git a/test/configCases/plugins/dotenv-plugin/expand.js b/test/configCases/plugins/dotenv-plugin/expand.js new file mode 100644 index 000000000..4151bd9fe --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/expand.js @@ -0,0 +1,6 @@ +"use strict"; + +it("should expand variables when expand is true", () => { + expect(process.env.INTERPOLATED_VAR).toBe("test-mode"); + expect(process.env.MY_NODE_ENV).toBe("test"); +}); diff --git a/test/configCases/plugins/dotenv-plugin/incomplete.env b/test/configCases/plugins/dotenv-plugin/incomplete.env new file mode 100644 index 000000000..fca1d5032 --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/incomplete.env @@ -0,0 +1,4 @@ +MY_NODE_ENV=test +API_URL=https://api.example.com +DEBUG=true +PORT=3000 diff --git a/test/configCases/plugins/dotenv-plugin/systemvars.js b/test/configCases/plugins/dotenv-plugin/systemvars.js new file mode 100644 index 000000000..cc74aea7a --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/systemvars.js @@ -0,0 +1,15 @@ +"use strict"; + +it("should include system variables when systemvars is true", () => { + // System variables should be available (we can't predict exact values, but PATH should exist) + expect(typeof process.env.PATH).toBe("string"); + expect(process.env.PATH.length).toBeGreaterThan(0); + + // .env variables should also be loaded + expect(process.env.MY_NODE_ENV).toBe("test"); + expect(process.env.API_URL).toBe("https://api.example.com"); + + // NODE_ENV might be set by the system/test environment + // We just check that it exists as a system variable + expect(typeof process.env.NODE_ENV).toBe("string"); +}); diff --git a/test/configCases/plugins/dotenv-plugin/warnings.js b/test/configCases/plugins/dotenv-plugin/warnings.js new file mode 100644 index 000000000..8dddbaacc --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/warnings.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = [[/Conflicting values for 'process.env.NODE_ENV'/]]; diff --git a/test/configCases/plugins/dotenv-plugin/webpack.config.js b/test/configCases/plugins/dotenv-plugin/webpack.config.js new file mode 100644 index 000000000..510c93e89 --- /dev/null +++ b/test/configCases/plugins/dotenv-plugin/webpack.config.js @@ -0,0 +1,67 @@ +"use strict"; + +const DotenvPlugin = require("../../../../lib/DotenvPlugin"); + +/** @type {import("../../../../").Configuration[]} */ +module.exports = [ + { + name: "basic", + entry: "./basic", + plugins: [new DotenvPlugin()] + }, + { + name: "with-defaults", + entry: "./defaults", + plugins: [ + new DotenvPlugin({ + defaults: true + }) + ] + }, + { + name: "with-expand", + entry: "./expand", + plugins: [ + new DotenvPlugin({ + expand: true + }) + ] + }, + { + name: "with-systemvars", + entry: "./systemvars", + plugins: [ + new DotenvPlugin({ + systemvars: true + }) + ] + }, + { + name: "custom-path", + entry: "./custom-path", + plugins: [ + new DotenvPlugin({ + path: "./.env.example" + }) + ] + }, + { + name: "custom-prefix", + entry: "./custom-prefix", + plugins: [ + new DotenvPlugin({ + prefix: "MY_ENV." + }) + ] + }, + { + name: "safe-mode-error", + entry: "./basic", // Use existing entry file + plugins: [ + new DotenvPlugin({ + path: "./incomplete.env", + safe: "./.env.example" + }) + ] + } +]; diff --git a/types.d.ts b/types.d.ts index e25fb870c..3a1fd8b23 100644 --- a/types.d.ts +++ b/types.d.ts @@ -4419,6 +4419,124 @@ declare interface DllReferencePluginOptionsManifest { | "jsonp" | "system"; } +declare class DotenvPlugin { + constructor(options?: DotenvPluginOptions); + config: { + /** + * Whether to allow empty strings in safe mode. If false, will throw an error if any env variables are empty (but only if safe mode is enabled). + */ + allowEmptyValues?: boolean; + /** + * Adds support for dotenv-defaults. If set to true, uses ./.env.defaults. If a string, uses that location for a defaults file. + */ + defaults?: string | boolean; + /** + * Allows your variables to be "expanded" for reusability within your .env file. + */ + expand?: boolean; + /** + * The path to your environment variables. This same path applies to the .env.example and .env.defaults files. + */ + path: string; + /** + * The prefix to use before the name of your env variables. + */ + prefix: string; + /** + * If true, load '.env.example' to verify the '.env' variables are all set. Can also be a string to a different file. + */ + safe?: string | boolean; + /** + * Set to true if you would rather load all system variables as well (useful for CI purposes). + */ + systemvars?: boolean; + }; + + /** + * Apply the plugin + */ + apply(compiler: Compiler): void; + gatherVariables( + inputFileSystem: InputFileSystem, + context: string, + callback: (err: null | Error, variables?: Record) => void + ): void; + initializeVars(): Record; + getEnvs( + inputFileSystem: InputFileSystem, + context: string, + callback: ( + err: null | Error, + result?: { + env: Record; + blueprint: Record; + } + ) => void + ): void; + getDefaults( + inputFileSystem: InputFileSystem, + context: string, + callback: (err: null | Error, content?: string) => void + ): void; + + /** + * Load a file with proper path resolution + */ + loadFile( + __0: { + /** + * the file to load + */ + file: string; + /** + * the input file system + */ + inputFileSystem: InputFileSystem; + /** + * the compiler context for resolving relative paths + */ + context: string; + }, + callback: (err: null | Error, content?: string) => void + ): void; + formatDefinitions(variables: Record): Record; +} +declare interface DotenvPluginOptions { + /** + * Whether to allow empty strings in safe mode. If false, will throw an error if any env variables are empty (but only if safe mode is enabled). + */ + allowEmptyValues?: boolean; + + /** + * Adds support for dotenv-defaults. If set to true, uses ./.env.defaults. If a string, uses that location for a defaults file. + */ + defaults?: string | boolean; + + /** + * Allows your variables to be "expanded" for reusability within your .env file. + */ + expand?: boolean; + + /** + * The path to your environment variables. This same path applies to the .env.example and .env.defaults files. + */ + path?: string; + + /** + * The prefix to use before the name of your env variables. + */ + prefix?: string; + + /** + * If true, load '.env.example' to verify the '.env' variables are all set. Can also be a string to a different file. + */ + safe?: string | boolean; + + /** + * Set to true if you would rather load all system variables as well (useful for CI purposes). + */ + systemvars?: boolean; +} declare class DynamicEntryPlugin { constructor(context: string, entry: () => Promise); context: string; @@ -19237,6 +19355,7 @@ declare namespace exports { DllPlugin, DllReferencePlugin, DynamicEntryPlugin, + DotenvPlugin, EntryOptionPlugin, EntryPlugin, EnvironmentPlugin,