source: frontend/node_modules/webpack/lib/DotenvPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 13.1 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Natsu @xiaoxiaojx
4*/
5
6"use strict";
7
8const FileSystemInfo = require("./FileSystemInfo");
9const { 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
20const 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
29const LINE =
30 /^\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$/gm;
31
32const 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 */
40function 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 */
86function _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 */
98function 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 */
165function 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 */
203const 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
215class 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
473module.exports = DotenvPlugin;
Note: See TracBrowser for help on using the repository browser.