| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Natsu @xiaoxiaojx
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const FileSystemInfo = require("./FileSystemInfo");
|
|---|
| 9 | const { join } = require("./util/fs");
|
|---|
| 10 |
|
|---|
| 11 | /** @typedef {import("../declarations/WebpackOptions").DotenvPluginOptions} DotenvPluginOptions */
|
|---|
| 12 | /** @typedef {import("./Compiler")} Compiler */
|
|---|
| 13 | /** @typedef {import("./CacheFacade").ItemCacheFacade} ItemCacheFacade */
|
|---|
| 14 | /** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
|
|---|
| 15 | /** @typedef {import("./FileSystemInfo").Snapshot} Snapshot */
|
|---|
| 16 |
|
|---|
| 17 | /** @typedef {Exclude<DotenvPluginOptions["prefix"], string | undefined>} Prefix */
|
|---|
| 18 | /** @typedef {Record<string, string>} Env */
|
|---|
| 19 |
|
|---|
| 20 | const DEFAULT_TEMPLATE = [
|
|---|
| 21 | ".env",
|
|---|
| 22 | ".env.local",
|
|---|
| 23 | ".env.[mode]",
|
|---|
| 24 | ".env.[mode].local"
|
|---|
| 25 | ];
|
|---|
| 26 |
|
|---|
| 27 | // Regex for parsing .env files
|
|---|
| 28 | // ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L49
|
|---|
| 29 | const LINE =
|
|---|
| 30 | /^\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$/gm;
|
|---|
| 31 |
|
|---|
| 32 | const PLUGIN_NAME = "DotenvPlugin";
|
|---|
| 33 |
|
|---|
| 34 | /**
|
|---|
| 35 | * Parse .env file content
|
|---|
| 36 | * ported from https://github.com/motdotla/dotenv/blob/master/lib/main.js#L49
|
|---|
| 37 | * @param {string | Buffer} src the source content to parse
|
|---|
| 38 | * @returns {Env} parsed environment variables object
|
|---|
| 39 | */
|
|---|
| 40 | function parse(src) {
|
|---|
| 41 | const obj = /** @type {Env} */ (Object.create(null));
|
|---|
| 42 |
|
|---|
| 43 | // Convert buffer to string
|
|---|
| 44 | let lines = src.toString();
|
|---|
| 45 |
|
|---|
| 46 | // Convert line breaks to same format
|
|---|
| 47 | lines = lines.replace(/\r\n?/g, "\n");
|
|---|
| 48 |
|
|---|
| 49 | /** @type {null | RegExpExecArray} */
|
|---|
| 50 | let match;
|
|---|
| 51 |
|
|---|
| 52 | while ((match = LINE.exec(lines)) !== null) {
|
|---|
| 53 | const key = match[1];
|
|---|
| 54 |
|
|---|
| 55 | // Default undefined or null to empty string
|
|---|
| 56 | let value = match[2] || "";
|
|---|
| 57 |
|
|---|
| 58 | // Remove whitespace
|
|---|
| 59 | value = value.trim();
|
|---|
| 60 |
|
|---|
| 61 | // Check if double quoted
|
|---|
| 62 | const maybeQuote = value[0];
|
|---|
| 63 |
|
|---|
| 64 | // Remove surrounding quotes
|
|---|
| 65 | value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
|
|---|
| 66 |
|
|---|
| 67 | // Expand newlines if double quoted
|
|---|
| 68 | if (maybeQuote === '"') {
|
|---|
| 69 | value = value.replace(/\\n/g, "\n");
|
|---|
| 70 | value = value.replace(/\\r/g, "\r");
|
|---|
| 71 | }
|
|---|
| 72 |
|
|---|
| 73 | // Add to object
|
|---|
| 74 | obj[key] = value;
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | return obj;
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | /**
|
|---|
| 81 | * Resolve escape sequences
|
|---|
| 82 | * ported from https://github.com/motdotla/dotenv-expand
|
|---|
| 83 | * @param {string} value value to resolve
|
|---|
| 84 | * @returns {string} resolved value
|
|---|
| 85 | */
|
|---|
| 86 | function _resolveEscapeSequences(value) {
|
|---|
| 87 | return value.replace(/\\\$/g, "$");
|
|---|
| 88 | }
|
|---|
| 89 |
|
|---|
| 90 | /**
|
|---|
| 91 | * Expand environment variable value
|
|---|
| 92 | * ported from https://github.com/motdotla/dotenv-expand
|
|---|
| 93 | * @param {string} value value to expand
|
|---|
| 94 | * @param {Record<string, string | undefined>} processEnv process.env object
|
|---|
| 95 | * @param {Env} runningParsed running parsed object
|
|---|
| 96 | * @returns {string} expanded value
|
|---|
| 97 | */
|
|---|
| 98 | function expandValue(value, processEnv, runningParsed) {
|
|---|
| 99 | const env = { ...runningParsed, ...processEnv }; // process.env wins
|
|---|
| 100 |
|
|---|
| 101 | const regex = /(?<!\\)\$\{([^{}]+)\}|(?<!\\)\$([a-z_]\w*)/gi;
|
|---|
| 102 |
|
|---|
| 103 | let result = value;
|
|---|
| 104 | /** @type {null | RegExpExecArray} */
|
|---|
| 105 | let match;
|
|---|
| 106 | /** @type {Set<string>} */
|
|---|
| 107 | const seen = new Set(); // self-referential checker
|
|---|
| 108 |
|
|---|
| 109 | while ((match = regex.exec(result)) !== null) {
|
|---|
| 110 | seen.add(result);
|
|---|
| 111 |
|
|---|
| 112 | const [template, bracedExpression, unbracedExpression] = match;
|
|---|
| 113 | const expression = bracedExpression || unbracedExpression;
|
|---|
| 114 |
|
|---|
| 115 | // match the operators `:+`, `+`, `:-`, and `-`
|
|---|
| 116 | const opRegex = /(:\+|\+|:-|-)/;
|
|---|
| 117 | // find first match
|
|---|
| 118 | const opMatch = expression.match(opRegex);
|
|---|
| 119 | const splitter = opMatch ? opMatch[0] : null;
|
|---|
| 120 |
|
|---|
| 121 | const r = expression.split(/** @type {string} */ (splitter));
|
|---|
| 122 | // const r = splitter ? expression.split(splitter) : [expression];
|
|---|
| 123 |
|
|---|
| 124 | /** @type {string} */
|
|---|
| 125 | let defaultValue;
|
|---|
| 126 | /** @type {undefined | null | string} */
|
|---|
| 127 | let value;
|
|---|
| 128 |
|
|---|
| 129 | const key = r.shift();
|
|---|
| 130 |
|
|---|
| 131 | if ([":+", "+"].includes(splitter || "")) {
|
|---|
| 132 | defaultValue = env[key || ""] ? r.join(splitter || "") : "";
|
|---|
| 133 | value = null;
|
|---|
| 134 | } else {
|
|---|
| 135 | defaultValue = r.join(splitter || "");
|
|---|
| 136 | value = env[key || ""];
|
|---|
| 137 | }
|
|---|
| 138 |
|
|---|
| 139 | if (value) {
|
|---|
| 140 | // self-referential check
|
|---|
| 141 | result = seen.has(value)
|
|---|
| 142 | ? result.replace(template, defaultValue)
|
|---|
| 143 | : result.replace(template, value);
|
|---|
| 144 | } else {
|
|---|
| 145 | result = result.replace(template, defaultValue);
|
|---|
| 146 | }
|
|---|
| 147 |
|
|---|
| 148 | // if the result equaled what was in process.env and runningParsed then stop expanding
|
|---|
| 149 | if (result === runningParsed[key || ""]) {
|
|---|
| 150 | break;
|
|---|
| 151 | }
|
|---|
| 152 |
|
|---|
| 153 | regex.lastIndex = 0; // reset regex search position to re-evaluate after each replacement
|
|---|
| 154 | }
|
|---|
| 155 |
|
|---|
| 156 | return result;
|
|---|
| 157 | }
|
|---|
| 158 |
|
|---|
| 159 | /**
|
|---|
| 160 | * Expand environment variables in parsed object
|
|---|
| 161 | * ported from https://github.com/motdotla/dotenv-expand
|
|---|
| 162 | * @param {{ parsed: Env, processEnv: Record<string, string | undefined> }} options expand options
|
|---|
| 163 | * @returns {{ parsed: Env }} expanded options
|
|---|
| 164 | */
|
|---|
| 165 | function expand(options) {
|
|---|
| 166 | // for use with progressive expansion
|
|---|
| 167 | const runningParsed = /** @type {Env} */ (Object.create(null));
|
|---|
| 168 | const processEnv = options.processEnv;
|
|---|
| 169 |
|
|---|
| 170 | // dotenv.config() ran before this so the assumption is process.env has already been set
|
|---|
| 171 | for (const key in options.parsed) {
|
|---|
| 172 | let value = options.parsed[key];
|
|---|
| 173 |
|
|---|
| 174 | // short-circuit scenario: process.env was already set prior to the file value
|
|---|
| 175 | value =
|
|---|
| 176 | Object.prototype.hasOwnProperty.call(processEnv, key) &&
|
|---|
| 177 | processEnv[key] !== value
|
|---|
| 178 | ? /** @type {string} */ (processEnv[key])
|
|---|
| 179 | : expandValue(value, processEnv, runningParsed);
|
|---|
| 180 |
|
|---|
| 181 | const resolvedValue = _resolveEscapeSequences(value);
|
|---|
| 182 |
|
|---|
| 183 | options.parsed[key] = resolvedValue;
|
|---|
| 184 | // for use with progressive expansion
|
|---|
| 185 | runningParsed[key] = resolvedValue;
|
|---|
| 186 | }
|
|---|
| 187 |
|
|---|
| 188 | // Part of `dotenv-expand` code, but we don't need it because of we don't modify `process.env`
|
|---|
| 189 | // for (const processKey in options.parsed) {
|
|---|
| 190 | // if (processEnv) {
|
|---|
| 191 | // processEnv[processKey] = options.parsed[processKey];
|
|---|
| 192 | // }
|
|---|
| 193 | // }
|
|---|
| 194 |
|
|---|
| 195 | return options;
|
|---|
| 196 | }
|
|---|
| 197 |
|
|---|
| 198 | /**
|
|---|
| 199 | * Format environment variables as DefinePlugin definitions
|
|---|
| 200 | * @param {Env} env environment variables
|
|---|
| 201 | * @returns {Record<string, string>} formatted definitions
|
|---|
| 202 | */
|
|---|
| 203 | const envToDefinitions = (env) => {
|
|---|
| 204 | const definitions = /** @type {Record<string, string>} */ ({});
|
|---|
| 205 |
|
|---|
| 206 | for (const [key, value] of Object.entries(env)) {
|
|---|
| 207 | const defValue = JSON.stringify(value);
|
|---|
| 208 | definitions[`process.env.${key}`] = defValue;
|
|---|
| 209 | definitions[`import.meta.env.${key}`] = defValue;
|
|---|
| 210 | }
|
|---|
| 211 |
|
|---|
| 212 | return definitions;
|
|---|
| 213 | };
|
|---|
| 214 |
|
|---|
| 215 | class DotenvPlugin {
|
|---|
| 216 | /**
|
|---|
| 217 | * Creates an instance of DotenvPlugin.
|
|---|
| 218 | * @param {DotenvPluginOptions=} options options object
|
|---|
| 219 | */
|
|---|
| 220 | constructor(options = {}) {
|
|---|
| 221 | /** @type {DotenvPluginOptions} */
|
|---|
| 222 | this.options = options;
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | /**
|
|---|
| 226 | * Applies the plugin by registering its hooks on the compiler.
|
|---|
| 227 | * @param {Compiler} compiler the compiler instance
|
|---|
| 228 | * @returns {void}
|
|---|
| 229 | */
|
|---|
| 230 | apply(compiler) {
|
|---|
| 231 | compiler.hooks.validate.tap(PLUGIN_NAME, () => {
|
|---|
| 232 | compiler.validate(
|
|---|
| 233 | () => {
|
|---|
| 234 | const { definitions } = require("../schemas/WebpackOptions.json");
|
|---|
| 235 |
|
|---|
| 236 | return {
|
|---|
| 237 | definitions,
|
|---|
| 238 | oneOf: [{ $ref: "#/definitions/DotenvPluginOptions" }]
|
|---|
| 239 | };
|
|---|
| 240 | },
|
|---|
| 241 | this.options,
|
|---|
| 242 | {
|
|---|
| 243 | name: "Dotenv Plugin",
|
|---|
| 244 | baseDataPath: "options"
|
|---|
| 245 | }
|
|---|
| 246 | );
|
|---|
| 247 | });
|
|---|
| 248 | const definePlugin = new compiler.webpack.DefinePlugin({});
|
|---|
| 249 | const prefixes = Array.isArray(this.options.prefix)
|
|---|
| 250 | ? this.options.prefix
|
|---|
| 251 | : [this.options.prefix || "WEBPACK_"];
|
|---|
| 252 | /** @type {string | false} */
|
|---|
| 253 | const dir =
|
|---|
| 254 | typeof this.options.dir === "string"
|
|---|
| 255 | ? this.options.dir
|
|---|
| 256 | : typeof this.options.dir === "undefined"
|
|---|
| 257 | ? compiler.context
|
|---|
| 258 | : this.options.dir;
|
|---|
| 259 |
|
|---|
| 260 | /** @type {undefined | Snapshot} */
|
|---|
| 261 | let snapshot;
|
|---|
| 262 |
|
|---|
| 263 | const cache = compiler.getCache(PLUGIN_NAME);
|
|---|
| 264 | const identifier = JSON.stringify(
|
|---|
| 265 | this.options.template || DEFAULT_TEMPLATE
|
|---|
| 266 | );
|
|---|
| 267 | const itemCache = cache.getItemCache(identifier, null);
|
|---|
| 268 |
|
|---|
| 269 | compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, async () => {
|
|---|
| 270 | const { parsed, snapshot: newSnapshot } = dir
|
|---|
| 271 | ? await this._loadEnv(compiler, itemCache, dir)
|
|---|
| 272 | : { parsed: {} };
|
|---|
| 273 | const env = this._getEnv(prefixes, parsed);
|
|---|
| 274 |
|
|---|
| 275 | definePlugin.definitions = envToDefinitions(env || {});
|
|---|
| 276 | snapshot = newSnapshot;
|
|---|
| 277 | });
|
|---|
| 278 |
|
|---|
| 279 | compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
|---|
| 280 | if (snapshot) {
|
|---|
| 281 | compilation.fileDependencies.addAll(snapshot.getFileIterable());
|
|---|
| 282 | compilation.missingDependencies.addAll(snapshot.getMissingIterable());
|
|---|
| 283 | }
|
|---|
| 284 | });
|
|---|
| 285 |
|
|---|
| 286 | definePlugin.apply(compiler);
|
|---|
| 287 | }
|
|---|
| 288 |
|
|---|
| 289 | /**
|
|---|
| 290 | * Get list of env files to load based on mode and template
|
|---|
| 291 | * Similar to Vite's getEnvFilesForMode
|
|---|
| 292 | * @private
|
|---|
| 293 | * @param {InputFileSystem} inputFileSystem the input file system
|
|---|
| 294 | * @param {string | false} dir the directory containing .env files
|
|---|
| 295 | * @param {string | undefined} mode the mode (e.g., 'production', 'development')
|
|---|
| 296 | * @returns {string[]} array of file paths to load
|
|---|
| 297 | */
|
|---|
| 298 | _getEnvFilesForMode(inputFileSystem, dir, mode) {
|
|---|
| 299 | if (!dir) {
|
|---|
| 300 | return [];
|
|---|
| 301 | }
|
|---|
| 302 |
|
|---|
| 303 | const templates = this.options.template || DEFAULT_TEMPLATE;
|
|---|
| 304 |
|
|---|
| 305 | return templates
|
|---|
| 306 | .map((pattern) => pattern.replace(/\[mode\]/g, mode || "development"))
|
|---|
| 307 | .map((file) => join(inputFileSystem, dir, file));
|
|---|
| 308 | }
|
|---|
| 309 |
|
|---|
| 310 | /**
|
|---|
| 311 | * Get parsed env variables from `.env` files
|
|---|
| 312 | * @private
|
|---|
| 313 | * @param {InputFileSystem} fs input file system
|
|---|
| 314 | * @param {string} dir dir to load `.env` files
|
|---|
| 315 | * @param {string} mode mode
|
|---|
| 316 | * @returns {Promise<{ parsed: Env, fileDependencies: string[], missingDependencies: string[] }>} parsed env variables and dependencies
|
|---|
| 317 | */
|
|---|
| 318 | async _getParsed(fs, dir, mode) {
|
|---|
| 319 | /** @type {string[]} */
|
|---|
| 320 | const fileDependencies = [];
|
|---|
| 321 | /** @type {string[]} */
|
|---|
| 322 | const missingDependencies = [];
|
|---|
| 323 |
|
|---|
| 324 | // Get env files to load
|
|---|
| 325 | const envFiles = this._getEnvFilesForMode(fs, dir, mode);
|
|---|
| 326 |
|
|---|
| 327 | // Read all files
|
|---|
| 328 | const contents = await Promise.all(
|
|---|
| 329 | envFiles.map((filePath) =>
|
|---|
| 330 | this._loadFile(fs, filePath).then(
|
|---|
| 331 | (content) => {
|
|---|
| 332 | fileDependencies.push(filePath);
|
|---|
| 333 | return content;
|
|---|
| 334 | },
|
|---|
| 335 | () => {
|
|---|
| 336 | // File doesn't exist, add to missingDependencies (this is normal)
|
|---|
| 337 | missingDependencies.push(filePath);
|
|---|
| 338 | return "";
|
|---|
| 339 | }
|
|---|
| 340 | )
|
|---|
| 341 | )
|
|---|
| 342 | );
|
|---|
| 343 |
|
|---|
| 344 | // Parse all files and merge (later files override earlier ones)
|
|---|
| 345 | // Similar to Vite's implementation
|
|---|
| 346 | const parsed = /** @type {Env} */ (Object.create(null));
|
|---|
| 347 |
|
|---|
| 348 | for (const content of contents) {
|
|---|
| 349 | if (!content) continue;
|
|---|
| 350 | const entries = parse(content);
|
|---|
| 351 | for (const key in entries) {
|
|---|
| 352 | parsed[key] = entries[key];
|
|---|
| 353 | }
|
|---|
| 354 | }
|
|---|
| 355 |
|
|---|
| 356 | return { parsed, fileDependencies, missingDependencies };
|
|---|
| 357 | }
|
|---|
| 358 |
|
|---|
| 359 | /**
|
|---|
| 360 | * Loads the provided compiler.
|
|---|
| 361 | * @private
|
|---|
| 362 | * @param {Compiler} compiler compiler
|
|---|
| 363 | * @param {ItemCacheFacade} itemCache item cache facade
|
|---|
| 364 | * @param {string} dir directory to read
|
|---|
| 365 | * @returns {Promise<{ parsed: Env, snapshot: Snapshot }>} parsed result and snapshot
|
|---|
| 366 | */
|
|---|
| 367 | async _loadEnv(compiler, itemCache, dir) {
|
|---|
| 368 | const fs = /** @type {InputFileSystem} */ (compiler.inputFileSystem);
|
|---|
| 369 | const fileSystemInfo = new FileSystemInfo(fs, {
|
|---|
| 370 | unmanagedPaths: compiler.unmanagedPaths,
|
|---|
| 371 | managedPaths: compiler.managedPaths,
|
|---|
| 372 | immutablePaths: compiler.immutablePaths,
|
|---|
| 373 | hashFunction: compiler.options.output.hashFunction
|
|---|
| 374 | });
|
|---|
| 375 |
|
|---|
| 376 | const result = await itemCache.getPromise();
|
|---|
| 377 |
|
|---|
| 378 | if (result) {
|
|---|
| 379 | const isSnapshotValid = await new Promise((resolve, reject) => {
|
|---|
| 380 | fileSystemInfo.checkSnapshotValid(result.snapshot, (error, isValid) => {
|
|---|
| 381 | if (error) {
|
|---|
| 382 | reject(error);
|
|---|
| 383 |
|
|---|
| 384 | return;
|
|---|
| 385 | }
|
|---|
| 386 |
|
|---|
| 387 | resolve(isValid);
|
|---|
| 388 | });
|
|---|
| 389 | });
|
|---|
| 390 |
|
|---|
| 391 | if (isSnapshotValid) {
|
|---|
| 392 | return { parsed: result.parsed, snapshot: result.snapshot };
|
|---|
| 393 | }
|
|---|
| 394 | }
|
|---|
| 395 |
|
|---|
| 396 | const { parsed, fileDependencies, missingDependencies } =
|
|---|
| 397 | await this._getParsed(
|
|---|
| 398 | fs,
|
|---|
| 399 | dir,
|
|---|
| 400 | /** @type {string} */
|
|---|
| 401 | (compiler.options.mode)
|
|---|
| 402 | );
|
|---|
| 403 |
|
|---|
| 404 | const startTime = Date.now();
|
|---|
| 405 | const newSnapshot = await new Promise((resolve, reject) => {
|
|---|
| 406 | fileSystemInfo.createSnapshot(
|
|---|
| 407 | startTime,
|
|---|
| 408 | fileDependencies,
|
|---|
| 409 | null,
|
|---|
| 410 | missingDependencies,
|
|---|
| 411 | // `.env` files are build dependencies
|
|---|
| 412 | compiler.options.snapshot.buildDependencies,
|
|---|
| 413 | (err, snapshot) => {
|
|---|
| 414 | if (err) return reject(err);
|
|---|
| 415 | resolve(snapshot);
|
|---|
| 416 | }
|
|---|
| 417 | );
|
|---|
| 418 | });
|
|---|
| 419 |
|
|---|
| 420 | await itemCache.storePromise({ parsed, snapshot: newSnapshot });
|
|---|
| 421 |
|
|---|
| 422 | return { parsed, snapshot: newSnapshot };
|
|---|
| 423 | }
|
|---|
| 424 |
|
|---|
| 425 | /**
|
|---|
| 426 | * Generate env variables
|
|---|
| 427 | * @private
|
|---|
| 428 | * @param {Prefix} prefixes expose only environment variables that start with these prefixes
|
|---|
| 429 | * @param {Env} parsed parsed env variables
|
|---|
| 430 | * @returns {Env} env variables
|
|---|
| 431 | */
|
|---|
| 432 | _getEnv(prefixes, parsed) {
|
|---|
| 433 | // Always expand environment variables (like Vite does)
|
|---|
| 434 | // Make a copy of process.env so that dotenv-expand doesn't modify global process.env
|
|---|
| 435 | const processEnv = { ...process.env };
|
|---|
| 436 | expand({ parsed, processEnv });
|
|---|
| 437 | const env = /** @type {Env} */ (Object.create(null));
|
|---|
| 438 |
|
|---|
| 439 | // Get all keys from parser and process.env
|
|---|
| 440 | const keys = [...Object.keys(parsed), ...Object.keys(process.env)];
|
|---|
| 441 |
|
|---|
| 442 | // Prioritize actual env variables from `process.env`, fallback to parsed
|
|---|
| 443 | for (const key of keys) {
|
|---|
| 444 | if (prefixes.some((prefix) => key.startsWith(prefix))) {
|
|---|
| 445 | env[key] =
|
|---|
| 446 | Object.prototype.hasOwnProperty.call(process.env, key) &&
|
|---|
| 447 | process.env[key]
|
|---|
| 448 | ? process.env[key]
|
|---|
| 449 | : parsed[key];
|
|---|
| 450 | }
|
|---|
| 451 | }
|
|---|
| 452 |
|
|---|
| 453 | return env;
|
|---|
| 454 | }
|
|---|
| 455 |
|
|---|
| 456 | /**
|
|---|
| 457 | * Load a file with proper path resolution
|
|---|
| 458 | * @private
|
|---|
| 459 | * @param {InputFileSystem} fs the input file system
|
|---|
| 460 | * @param {string} file the file to load
|
|---|
| 461 | * @returns {Promise<string>} the content of the file
|
|---|
| 462 | */
|
|---|
| 463 | _loadFile(fs, file) {
|
|---|
| 464 | return new Promise((resolve, reject) => {
|
|---|
| 465 | fs.readFile(file, (err, content) => {
|
|---|
| 466 | if (err) reject(err);
|
|---|
| 467 | else resolve(/** @type {Buffer} */ (content).toString() || "");
|
|---|
| 468 | });
|
|---|
| 469 | });
|
|---|
| 470 | }
|
|---|
| 471 | }
|
|---|
| 472 |
|
|---|
| 473 | module.exports = DotenvPlugin;
|
|---|