| | |
| | | "use strict"; |
| | | |
| | | const FileSystemInfo = require("./FileSystemInfo"); |
| | | const createSchemaValidation = require("./util/create-schema-validation"); |
| | | const { join } = require("./util/fs"); |
| | | |
| | | /** @typedef {import("../declarations/WebpackOptions").DotenvPluginOptions} DotenvPluginOptions */ |
| | |
| | | /** @typedef {Exclude<DotenvPluginOptions["prefix"], string | undefined>} Prefix */ |
| | | /** @typedef {Record<string, string>} Env */ |
| | | |
| | | /** @type {DotenvPluginOptions} */ |
| | | const DEFAULT_OPTIONS = { |
| | | prefix: "WEBPACK_", |
| | | template: [".env", ".env.local", ".env.[mode]", ".env.[mode].local"] |
| | | }; |
| | | const DEFAULT_TEMPLATE = [ |
| | | ".env", |
| | | ".env.local", |
| | | ".env.[mode]", |
| | | ".env.[mode].local" |
| | | ]; |
| | | |
| | | // Regex for parsing .env files |
| | | // ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32 |
| | | // ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L49 |
| | | const LINE = |
| | | /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm; |
| | | /^\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$/gm; |
| | | |
| | | const PLUGIN_NAME = "DotenvPlugin"; |
| | | |
| | | const validate = createSchemaValidation( |
| | | undefined, |
| | | () => { |
| | | const { definitions } = require("../schemas/WebpackOptions.json"); |
| | | |
| | | return { |
| | | definitions, |
| | | oneOf: [{ $ref: "#/definitions/DotenvPluginOptions" }] |
| | | }; |
| | | }, |
| | | { |
| | | name: "Dotenv Plugin", |
| | | baseDataPath: "options" |
| | | } |
| | | ); |
| | | |
| | | /** |
| | | * Parse .env file content |
| | |
| | | * @returns {Env} parsed environment variables object |
| | | */ |
| | | function parse(src) { |
| | | const obj = /** @type {Env} */ ({}); |
| | | const obj = /** @type {Env} */ (Object.create(null)); |
| | | |
| | | // Convert buffer to string |
| | | let lines = src.toString(); |
| | | |
| | | // Convert line breaks to same format |
| | | lines = lines.replace(/\r\n?/gm, "\n"); |
| | | lines = lines.replace(/\r\n?/g, "\n"); |
| | | |
| | | /** @type {null | RegExpExecArray} */ |
| | | let match; |
| | | |
| | | while ((match = LINE.exec(lines)) !== null) { |
| | | const key = match[1]; |
| | | |
| | |
| | | function expandValue(value, processEnv, runningParsed) { |
| | | const env = { ...runningParsed, ...processEnv }; // process.env wins |
| | | |
| | | const regex = /(?<!\\)\$\{([^{}]+)\}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g; |
| | | const regex = /(?<!\\)\$\{([^{}]+)\}|(?<!\\)\$([a-z_]\w*)/gi; |
| | | |
| | | let result = value; |
| | | /** @type {null | RegExpExecArray} */ |
| | | let match; |
| | | /** @type {Set<string>} */ |
| | | const seen = new Set(); // self-referential checker |
| | | |
| | | while ((match = regex.exec(result)) !== null) { |
| | |
| | | const r = expression.split(/** @type {string} */ (splitter)); |
| | | // const r = splitter ? expression.split(splitter) : [expression]; |
| | | |
| | | /** @type {string} */ |
| | | let defaultValue; |
| | | /** @type {undefined | null | string} */ |
| | | let value; |
| | | |
| | | const key = r.shift(); |
| | |
| | | */ |
| | | function expand(options) { |
| | | // for use with progressive expansion |
| | | const runningParsed = /** @type {Env} */ ({}); |
| | | const runningParsed = /** @type {Env} */ (Object.create(null)); |
| | | const processEnv = options.processEnv; |
| | | |
| | | // dotenv.config() ran before this so the assumption is process.env has already been set |
| | |
| | | |
| | | // short-circuit scenario: process.env was already set prior to the file value |
| | | value = |
| | | processEnv[key] && processEnv[key] !== value |
| | | Object.prototype.hasOwnProperty.call(processEnv, key) && |
| | | processEnv[key] !== value |
| | | ? /** @type {string} */ (processEnv[key]) |
| | | : expandValue(value, processEnv, runningParsed); |
| | | |
| | |
| | | |
| | | class DotenvPlugin { |
| | | /** |
| | | * Creates an instance of DotenvPlugin. |
| | | * @param {DotenvPluginOptions=} options options object |
| | | */ |
| | | constructor(options = {}) { |
| | | validate(options); |
| | | this.options = { ...DEFAULT_OPTIONS, ...options }; |
| | | /** @type {DotenvPluginOptions} */ |
| | | this.options = options; |
| | | } |
| | | |
| | | /** |
| | | * Applies the plugin by registering its hooks on the compiler. |
| | | * @param {Compiler} compiler the compiler instance |
| | | * @returns {void} |
| | | */ |
| | | apply(compiler) { |
| | | compiler.hooks.validate.tap(PLUGIN_NAME, () => { |
| | | compiler.validate( |
| | | () => { |
| | | const { definitions } = require("../schemas/WebpackOptions.json"); |
| | | |
| | | return { |
| | | definitions, |
| | | oneOf: [{ $ref: "#/definitions/DotenvPluginOptions" }] |
| | | }; |
| | | }, |
| | | this.options, |
| | | { |
| | | name: "Dotenv Plugin", |
| | | baseDataPath: "options" |
| | | } |
| | | ); |
| | | }); |
| | | const definePlugin = new compiler.webpack.DefinePlugin({}); |
| | | const prefixes = Array.isArray(this.options.prefix) |
| | | ? this.options.prefix |
| | |
| | | let snapshot; |
| | | |
| | | const cache = compiler.getCache(PLUGIN_NAME); |
| | | const identifier = JSON.stringify(this.options.template); |
| | | const identifier = JSON.stringify( |
| | | this.options.template || DEFAULT_TEMPLATE |
| | | ); |
| | | const itemCache = cache.getItemCache(identifier, null); |
| | | |
| | | compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, async () => { |
| | |
| | | return []; |
| | | } |
| | | |
| | | const { template } = /** @type {DotenvPluginOptions} */ (this.options); |
| | | const templates = template || []; |
| | | const templates = this.options.template || DEFAULT_TEMPLATE; |
| | | |
| | | return templates |
| | | .map((pattern) => pattern.replace(/\[mode\]/g, mode || "development")) |
| | |
| | | * @param {InputFileSystem} fs input file system |
| | | * @param {string} dir dir to load `.env` files |
| | | * @param {string} mode mode |
| | | * @returns {Promise<{parsed: Env, fileDependencies: string[], missingDependencies: string[]}>} parsed env variables and dependencies |
| | | * @returns {Promise<{ parsed: Env, fileDependencies: string[], missingDependencies: string[] }>} parsed env variables and dependencies |
| | | */ |
| | | async _getParsed(fs, dir, mode) { |
| | | /** @type {string[]} */ |
| | |
| | | |
| | | // Parse all files and merge (later files override earlier ones) |
| | | // Similar to Vite's implementation |
| | | const parsed = /** @type {Env} */ ({}); |
| | | const parsed = /** @type {Env} */ (Object.create(null)); |
| | | |
| | | for (const content of contents) { |
| | | if (!content) continue; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Loads the provided compiler. |
| | | * @private |
| | | * @param {Compiler} compiler compiler |
| | | * @param {ItemCacheFacade} itemCache item cache facade |
| | |
| | | // Make a copy of process.env so that dotenv-expand doesn't modify global process.env |
| | | const processEnv = { ...process.env }; |
| | | expand({ parsed, processEnv }); |
| | | const env = /** @type {Env} */ ({}); |
| | | const env = /** @type {Env} */ (Object.create(null)); |
| | | |
| | | // Get all keys from parser and process.env |
| | | const keys = [...Object.keys(parsed), ...Object.keys(process.env)]; |
| | |
| | | // Prioritize actual env variables from `process.env`, fallback to parsed |
| | | for (const key of keys) { |
| | | if (prefixes.some((prefix) => key.startsWith(prefix))) { |
| | | env[key] = process.env[key] ? process.env[key] : parsed[key]; |
| | | env[key] = |
| | | Object.prototype.hasOwnProperty.call(process.env, key) && |
| | | process.env[key] |
| | | ? process.env[key] |
| | | : parsed[key]; |
| | | } |
| | | } |
| | | |