@ix-xs/node-comfort 1.1.31 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. package/types/index.d.ts.map +0 -1
package/src/Env.js ADDED
@@ -0,0 +1,555 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Environment variables you can trust: `.env` loading, typed getters, and
5
+ * `validate()` to check your whole configuration at startup and report
6
+ * every problem at once.
7
+ *
8
+ * The `.env` file in the working directory is loaded when the package is
9
+ * required; variables that are already set are never overwritten. Set
10
+ * `NODE_COMFORT_DOTENV=false` to turn that off.
11
+ *
12
+ * @example
13
+ * const config = env.validate({
14
+ * PORT: { type: "port", default: 3000 },
15
+ * DATABASE_URL: { type: "url" }, // required
16
+ * CACHE_TTL: { type: "duration", default: "5m" }, // in ms
17
+ * });
18
+ * config.PORT; // a number, typed in your editor
19
+ */
20
+
21
+ const fs = require("node:fs");
22
+ const nodePath = require("node:path");
23
+ const { ValidationError } = require("./errors.js");
24
+
25
+ /**
26
+ * Options for `load()`.
27
+ * @typedef {object} EnvLoadOptions
28
+ * @property {boolean} [override] Overwrite variables that are already set.
29
+ * @property {boolean} [expand] Replace `${OTHER}` references. Defaults to `true`.
30
+ * @property {boolean} [required] Throw if a file is missing.
31
+ * @property {NodeJS.ProcessEnv} [target] Where to put the variables. Defaults to `process.env`.
32
+ */
33
+
34
+ /**
35
+ * Options for `parse()`.
36
+ * @typedef {object} EnvParseOptions
37
+ * @property {boolean} [expand] Replace `$VAR`, `${VAR}` and `${VAR:-default}`. Defaults to `true`.
38
+ * @property {Record<string, string | undefined>} [env] Other variables usable in expansions. Defaults to `process.env`.
39
+ */
40
+
41
+ /**
42
+ * Options shared by the typed getters.
43
+ * @template T
44
+ * @typedef {object} EnvGetterOptions
45
+ * @property {T} [default] Used when the variable is missing or empty. Without it, the variable is required.
46
+ * @property {boolean} [optional] Return `undefined` instead of throwing when it's missing.
47
+ * @property {NodeJS.ProcessEnv} [env] Where to read from. Defaults to `process.env`.
48
+ */
49
+
50
+ /**
51
+ * One variable in `validate()`. It's required unless it has a `default` or
52
+ * `optional: true`.
53
+ *
54
+ * @typedef {(
55
+ * { type: "string", default?: string, optional?: boolean, pattern?: RegExp, minLength?: number, description?: string } |
56
+ * { type: "number", default?: number, optional?: boolean, min?: number, max?: number, integer?: boolean, description?: string } |
57
+ * { type: "boolean", default?: boolean, optional?: boolean, description?: string } |
58
+ * { type: "port", default?: number, optional?: boolean, description?: string } |
59
+ * { type: "url", default?: string, optional?: boolean, protocols?: string[], description?: string } |
60
+ * { type: "email", default?: string, optional?: boolean, description?: string } |
61
+ * { type: "duration", default?: number | string, optional?: boolean, description?: string } |
62
+ * { type: "list", default?: string[], optional?: boolean, separator?: string, description?: string } |
63
+ * { type: "json", default?: unknown, optional?: boolean, description?: string } |
64
+ * { type: "enum", values: readonly string[], default?: string, optional?: boolean, description?: string }
65
+ * )} EnvVarSpec
66
+ */
67
+
68
+ /**
69
+ * The type a variable gets once read.
70
+ * @template {EnvVarSpec} S
71
+ * @typedef {S["type"] extends "number" | "port" | "duration" ? number : S["type"] extends "boolean" ? boolean : S["type"] extends "list" ? string[] : S["type"] extends "json" ? any : S extends { values: readonly (infer V)[] } ? V : string} EnvVarValue
72
+ */
73
+
74
+ /**
75
+ * What `validate()` returns.
76
+ * @template {Record<string, EnvVarSpec>} T
77
+ * @typedef {{ -readonly [K in keyof T]: T[K] extends { optional: true } ? EnvVarValue<T[K]> | undefined : EnvVarValue<T[K]> }} EnvConfig
78
+ */
79
+
80
+ /**
81
+ * @param {string} name
82
+ * @param {string} code
83
+ * @param {string} message
84
+ * @returns {never}
85
+ */
86
+ const _fail = (name, code, message) => {
87
+ throw new ValidationError([{ path: [name], code, message }], { message: `Environment variable ${name}: ${message}` });
88
+ };
89
+
90
+ /**
91
+ * @param {string} name
92
+ * @param {string} raw
93
+ * @param {EnvVarSpec} spec
94
+ * @returns {unknown}
95
+ */
96
+ const _convert = (name, raw, spec) => {
97
+ const value = raw.trim();
98
+ switch (spec.type) {
99
+ case "string": {
100
+ if (spec.minLength !== undefined && value.length < spec.minLength) _fail(name, "too_small", `must contain at least ${spec.minLength} characters`);
101
+ if (spec.pattern && !spec.pattern.test(value)) _fail(name, "invalid_string", `does not match ${spec.pattern}`);
102
+ return raw;
103
+ }
104
+ case "number": {
105
+ const n = Number(value);
106
+ if (value === "" || !Number.isFinite(n)) _fail(name, "invalid_type", `must be a number, got "${raw}"`);
107
+ if (spec.integer && !Number.isInteger(n)) _fail(name, "invalid_type", `must be an integer, got "${raw}"`);
108
+ if (spec.min !== undefined && n < spec.min) _fail(name, "too_small", `must be >= ${spec.min}, got ${n}`);
109
+ if (spec.max !== undefined && n > spec.max) _fail(name, "too_big", `must be <= ${spec.max}, got ${n}`);
110
+ return n;
111
+ }
112
+ case "port": {
113
+ const n = Number(value);
114
+ if (!/^\d+$/.test(value) || n < 0 || n > 65535) _fail(name, "invalid_type", `must be a port number (0-65535), got "${raw}"`);
115
+ return n;
116
+ }
117
+ case "boolean": {
118
+ const lower = value.toLowerCase();
119
+ if (["true", "1", "yes", "y", "on"].includes(lower)) return true;
120
+ if (["false", "0", "no", "n", "off", ""].includes(lower)) return false;
121
+ return _fail(name, "invalid_type", `must be a boolean (true/false, 1/0, yes/no, on/off), got "${raw}"`);
122
+ }
123
+ case "url": {
124
+ try {
125
+ const url = new URL(value);
126
+ if (spec.protocols && !spec.protocols.map((p) => p.replace(/:?$/, ":")).includes(url.protocol)) {
127
+ _fail(name, "invalid_string", `must use one of these protocols: ${spec.protocols.join(", ")}`);
128
+ }
129
+ return value;
130
+ } catch (error) {
131
+ if (error instanceof ValidationError) throw error;
132
+ return _fail(name, "invalid_string", `must be a valid URL, got "${raw}"`);
133
+ }
134
+ }
135
+ case "email": {
136
+ const { isEmail } = require("./Checker.js");
137
+ if (!isEmail(value)) _fail(name, "invalid_string", `must be an email address, got "${raw}"`);
138
+ return value;
139
+ }
140
+ case "duration": {
141
+ const { parseDuration } = require("./Time.js");
142
+ const ms = parseDuration(value);
143
+ if (ms === null) _fail(name, "invalid_type", `must be a duration such as "30s", "5m" or "1h", got "${raw}"`);
144
+ return ms;
145
+ }
146
+ case "list":
147
+ return value === "" ? [] : value.split(spec.separator ?? ",").map((item) => item.trim()).filter((item) => item !== "");
148
+ case "json":
149
+ try {
150
+ return JSON.parse(value);
151
+ } catch {
152
+ return _fail(name, "invalid_type", "must be valid JSON");
153
+ }
154
+ case "enum":
155
+ if (!spec.values.includes(value)) _fail(name, "invalid_enum_value", `must be one of: ${spec.values.join(", ")} (got "${raw}")`);
156
+ return value;
157
+ default:
158
+ return raw;
159
+ }
160
+ };
161
+
162
+ /**
163
+ * @param {string} name
164
+ * @param {EnvVarSpec & { optional?: boolean }} spec
165
+ * @param {NodeJS.ProcessEnv} source
166
+ * @returns {unknown}
167
+ */
168
+ const _read = (name, spec, source) => {
169
+ const raw = source[name];
170
+ if (raw === undefined || raw.trim() === "") {
171
+ if ("default" in spec && spec.default !== undefined) {
172
+ return spec.type === "duration" && typeof spec.default === "string" ? _convert(name, spec.default, spec) : spec.default;
173
+ }
174
+ if (spec.optional) return undefined;
175
+ return _fail(name, "required", "is required but is not set");
176
+ }
177
+ return _convert(name, raw, spec);
178
+ };
179
+
180
+ /**
181
+ * Parses the text of a `.env` file: quotes, multi-line values, `export`,
182
+ * comments, escapes, and `${VAR}` expansion (except in single quotes).
183
+ *
184
+ * @example
185
+ * env.parse('PORT=3000\nURL="http://localhost:${PORT}"');
186
+ * // { PORT: "3000", URL: "http://localhost:3000" }
187
+ *
188
+ * @param {string} content
189
+ * @param {EnvParseOptions} [options]
190
+ * @returns {Record<string, string>}
191
+ */
192
+ function parse(content, options = {}) {
193
+ /** @type {Record<string, string>} */
194
+ const out = {};
195
+ /** @type {Set<string>} */
196
+ const literal = new Set();
197
+ const text = String(content).replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
198
+ const regex = /^[ \t]*(?:export[ \t]+)?([\w.-]+)[ \t]*[=:][ \t]*(?:("(?:\\.|[^"\\])*"|'[^']*'|`[^`]*`)[ \t]*(?:#[^\n]*)?|([^\n]*))$/gm;
199
+ let match;
200
+ while ((match = regex.exec(text)) !== null) {
201
+ const [, key, quoted, bare] = match;
202
+ let value;
203
+ if (quoted !== undefined) {
204
+ const quote = quoted[0];
205
+ value = quoted.slice(1, -1);
206
+ if (quote === '"') value = value.replace(/\\([nrt"\\$])/g, (_, c) => ({ n: "\n", r: "\r", t: "\t" })[/** @type {"n"} */ (c)] ?? c);
207
+ if (quote === "'") literal.add(key);
208
+ } else {
209
+ value = (bare ?? "").replace(/(?:^|[ \t]+)#.*$/, "").trim();
210
+ }
211
+ out[key] = value;
212
+ }
213
+ if (options.expand !== false) {
214
+ const base = options.env ?? process.env;
215
+ const resolve = (/** @type {string} */ name, /** @type {Set<string>} */ seen) => {
216
+ if (seen.has(name)) return "";
217
+ if (name in out) return expandValue(out[name], new Set([...seen, name]), name);
218
+ return base[name] ?? "";
219
+ };
220
+ /**
221
+ * @param {string} value
222
+ * @param {Set<string>} seen
223
+ * @param {string} key
224
+ * @returns {string}
225
+ */
226
+ const expandValue = (value, seen, key) => {
227
+ if (literal.has(key)) return value;
228
+ return value.replace(/\\\$|\$\{([\w.]+)(?::?-([^}]*))?\}|\$([A-Za-z_]\w*)/g, (m, braced, fallback, bare) => {
229
+ if (m === "\\$") return "$";
230
+ const resolved = resolve(braced ?? bare, seen);
231
+ return resolved === "" && fallback !== undefined ? fallback : resolved;
232
+ });
233
+ };
234
+ for (const key of Object.keys(out)) out[key] = expandValue(out[key], new Set([key]), key);
235
+ }
236
+ return out;
237
+ }
238
+
239
+ /**
240
+ * Loads `.env` files into `process.env`. The first file to set a variable
241
+ * wins, and variables that are already set are kept, unless you pass
242
+ * `override`.
243
+ *
244
+ * @example
245
+ * env.load(); // ./.env
246
+ * env.load([".env.local", ".env"]); // local values win
247
+ * env.load(".env.test", { override: true });
248
+ *
249
+ * @param {string | string[]} [files=".env"] Relative to the working directory.
250
+ * @param {EnvLoadOptions} [options]
251
+ * @returns {Record<string, string>} Everything read from the files.
252
+ * @throws {Error} If `required` is set and a file is missing.
253
+ */
254
+ function load(files = ".env", options = {}) {
255
+ const target = options.target ?? process.env;
256
+ /** @type {Record<string, string>} */
257
+ const loaded = {};
258
+ for (const file of Array.isArray(files) ? files : [files]) {
259
+ const full = nodePath.resolve(process.cwd(), file);
260
+ if (!fs.existsSync(full)) {
261
+ if (options.required) throw new Error(`Environment file not found: ${full}`);
262
+ continue;
263
+ }
264
+ const values = parse(fs.readFileSync(full, "utf8"), { expand: options.expand, env: { ...target, ...loaded } });
265
+ for (const [key, value] of Object.entries(values)) {
266
+ if (!(key in loaded)) loaded[key] = value;
267
+ if (options.override || target[key] === undefined) target[key] = value;
268
+ }
269
+ }
270
+ return loaded;
271
+ }
272
+
273
+ /**
274
+ * Reads a variable, with a fallback. Never throws.
275
+ *
276
+ * @example
277
+ * env.get("REGION", "eu-west-1");
278
+ *
279
+ * @param {string} name
280
+ * @param {string} [fallback]
281
+ * @returns {string | undefined}
282
+ */
283
+ function get(name, fallback) {
284
+ return process.env[name] ?? fallback;
285
+ }
286
+
287
+ /**
288
+ * Is the variable set, even to an empty string?
289
+ *
290
+ * @param {string} name
291
+ * @returns {boolean}
292
+ */
293
+ function has(name) {
294
+ return process.env[name] !== undefined;
295
+ }
296
+
297
+ /**
298
+ * Reads a variable that must be set and not empty.
299
+ *
300
+ * @example
301
+ * const key = env.required("STRIPE_SECRET_KEY");
302
+ *
303
+ * @param {string} name
304
+ * @returns {string}
305
+ * @throws {ValidationError} If it's missing or empty.
306
+ */
307
+ function required(name) {
308
+ return /** @type {string} */ (_read(name, { type: "string" }, process.env));
309
+ }
310
+
311
+ /**
312
+ * Reads a string. Required unless you give a default or `optional`.
313
+ *
314
+ * @example
315
+ * env.string("APP_NAME", { default: "my-app" });
316
+ *
317
+ * @param {string} name
318
+ * @param {EnvGetterOptions<string> & { pattern?: RegExp, minLength?: number }} [options]
319
+ * @returns {string}
320
+ * @throws {ValidationError}
321
+ */
322
+ function string(name, options = {}) {
323
+ return /** @type {string} */ (_read(name, { ...options, type: "string" }, options.env ?? process.env));
324
+ }
325
+
326
+ /**
327
+ * Reads a number.
328
+ *
329
+ * @example
330
+ * env.number("WORKERS", { default: 4, min: 1, integer: true });
331
+ *
332
+ * @param {string} name
333
+ * @param {EnvGetterOptions<number> & { min?: number, max?: number, integer?: boolean }} [options]
334
+ * @returns {number}
335
+ * @throws {ValidationError}
336
+ */
337
+ function number(name, options = {}) {
338
+ return /** @type {number} */ (_read(name, { ...options, type: "number" }, options.env ?? process.env));
339
+ }
340
+
341
+ /**
342
+ * Reads a boolean: `true`/`false`, `1`/`0`, `yes`/`no` or `on`/`off`.
343
+ *
344
+ * @example
345
+ * if (env.bool("FEATURE_BETA", { default: false })) enableBeta();
346
+ *
347
+ * @param {string} name
348
+ * @param {EnvGetterOptions<boolean>} [options]
349
+ * @returns {boolean}
350
+ * @throws {ValidationError}
351
+ */
352
+ function bool(name, options = {}) {
353
+ return /** @type {boolean} */ (_read(name, { ...options, type: "boolean" }, options.env ?? process.env));
354
+ }
355
+
356
+ /**
357
+ * Reads a port number.
358
+ *
359
+ * @example
360
+ * server.listen(env.port("PORT", { default: 3000 }));
361
+ *
362
+ * @param {string} name
363
+ * @param {EnvGetterOptions<number>} [options]
364
+ * @returns {number}
365
+ * @throws {ValidationError}
366
+ */
367
+ function port(name, options = {}) {
368
+ return /** @type {number} */ (_read(name, { ...options, type: "port" }, options.env ?? process.env));
369
+ }
370
+
371
+ /**
372
+ * Reads an absolute URL.
373
+ *
374
+ * @example
375
+ * env.url("DATABASE_URL", { protocols: ["postgres", "postgresql"] });
376
+ *
377
+ * @param {string} name
378
+ * @param {EnvGetterOptions<string> & { protocols?: string[] }} [options]
379
+ * @returns {string}
380
+ * @throws {ValidationError}
381
+ */
382
+ function url(name, options = {}) {
383
+ return /** @type {string} */ (_read(name, { ...options, type: "url" }, options.env ?? process.env));
384
+ }
385
+
386
+ /**
387
+ * Reads a duration like `"30s"` or `"1h30m"` and returns milliseconds.
388
+ *
389
+ * @example
390
+ * env.duration("CACHE_TTL", { default: "10m" }); // 600000
391
+ *
392
+ * @param {string} name
393
+ * @param {EnvGetterOptions<number | string>} [options]
394
+ * @returns {number}
395
+ * @throws {ValidationError}
396
+ */
397
+ function duration(name, options = {}) {
398
+ return /** @type {number} */ (_read(name, { ...options, type: "duration" }, options.env ?? process.env));
399
+ }
400
+
401
+ /**
402
+ * Reads a comma-separated list. Items are trimmed and empty ones dropped.
403
+ *
404
+ * @example
405
+ * // ALLOWED_ORIGINS="https://a.com, https://b.com"
406
+ * env.list("ALLOWED_ORIGINS", { default: [] }); // ["https://a.com", "https://b.com"]
407
+ *
408
+ * @param {string} name
409
+ * @param {EnvGetterOptions<string[]> & { separator?: string }} [options]
410
+ * @returns {string[]}
411
+ * @throws {ValidationError}
412
+ */
413
+ function list(name, options = {}) {
414
+ return /** @type {string[]} */ (_read(name, { ...options, type: "list" }, options.env ?? process.env));
415
+ }
416
+
417
+ /**
418
+ * Reads a JSON value.
419
+ *
420
+ * @example
421
+ * const flags = env.json("FEATURE_FLAGS", { default: {} });
422
+ *
423
+ * @template [T=any]
424
+ * @param {string} name
425
+ * @param {EnvGetterOptions<T>} [options]
426
+ * @returns {T}
427
+ * @throws {ValidationError}
428
+ */
429
+ function json(name, options = {}) {
430
+ return /** @type {T} */ (_read(name, { ...options, type: "json" }, options.env ?? process.env));
431
+ }
432
+
433
+ /**
434
+ * Reads a variable that must be one of the given values. The result has
435
+ * their literal type.
436
+ *
437
+ * @example
438
+ * env.oneOf("LOG_LEVEL", ["debug", "info", "warn", "error"], { default: "info" });
439
+ *
440
+ * @template {string} V
441
+ * @param {string} name
442
+ * @param {readonly V[]} values
443
+ * @param {EnvGetterOptions<V>} [options]
444
+ * @returns {V}
445
+ * @throws {ValidationError}
446
+ */
447
+ function oneOf(name, values, options = {}) {
448
+ return /** @type {V} */ (_read(name, { ...options, type: "enum", values }, options.env ?? process.env));
449
+ }
450
+
451
+ /**
452
+ * Checks all your variables at once and returns a typed, frozen config. If
453
+ * anything is wrong, one `ValidationError` lists every problem, so a bad
454
+ * deployment fails at startup with the full picture.
455
+ *
456
+ * @example
457
+ * const config = env.validate({
458
+ * NODE_ENV: { type: "enum", values: ["development", "production", "test"], default: "development" },
459
+ * PORT: { type: "port", default: 3000 },
460
+ * DATABASE_URL: { type: "url", protocols: ["postgres"] },
461
+ * JWT_SECRET: { type: "string", minLength: 32 },
462
+ * SENTRY_DSN: { type: "url", optional: true },
463
+ * });
464
+ * // ValidationError: Validation failed with 2 issues:
465
+ * // • DATABASE_URL: is required but is not set
466
+ * // • JWT_SECRET: must contain at least 32 characters
467
+ *
468
+ * @template {Record<string, EnvVarSpec>} T
469
+ * @param {T} spec
470
+ * @param {{ env?: NodeJS.ProcessEnv }} [options]
471
+ * @returns {Readonly<EnvConfig<T>>}
472
+ * @throws {ValidationError}
473
+ */
474
+ function validate(spec, options = {}) {
475
+ const source = options.env ?? process.env;
476
+ /** @type {Record<string, unknown>} */
477
+ const out = {};
478
+ /** @type {import("./errors").ValidationIssue[]} */
479
+ const issues = [];
480
+ for (const [name, varSpec] of Object.entries(spec)) {
481
+ try {
482
+ out[name] = _read(name, varSpec, source);
483
+ } catch (error) {
484
+ if (error instanceof ValidationError) issues.push(...error.issues);
485
+ else throw error;
486
+ }
487
+ }
488
+ if (issues.length) throw new ValidationError(issues);
489
+ return /** @type {any} */ (Object.freeze(out));
490
+ }
491
+
492
+ /**
493
+ * `NODE_ENV`, or `"development"` when it's not set.
494
+ *
495
+ * @returns {string}
496
+ */
497
+ function mode() {
498
+ return process.env.NODE_ENV || "development";
499
+ }
500
+
501
+ /**
502
+ * Is `NODE_ENV` set to `"production"`?
503
+ * @returns {boolean}
504
+ */
505
+ function isProduction() {
506
+ return mode() === "production";
507
+ }
508
+
509
+ /**
510
+ * Is `NODE_ENV` `"development"`, or not set?
511
+ * @returns {boolean}
512
+ */
513
+ function isDevelopment() {
514
+ return mode() === "development";
515
+ }
516
+
517
+ /**
518
+ * Are we running tests? True when `NODE_ENV` is `"test"` or a test runner
519
+ * like `node --test` or Jest is detected.
520
+ * @returns {boolean}
521
+ */
522
+ function isTest() {
523
+ return mode() === "test" || process.env.NODE_TEST_CONTEXT !== undefined || process.env.JEST_WORKER_ID !== undefined;
524
+ }
525
+
526
+ if (String(process.env.NODE_COMFORT_DOTENV).toLowerCase() !== "false") {
527
+ try {
528
+ load(".env");
529
+ } catch {
530
+ // A broken .env file must never prevent the application from starting.
531
+ }
532
+ }
533
+
534
+ module.exports = {
535
+ parse,
536
+ load,
537
+ // not shorthand: Node's ES module export detection stops at a bare `get`
538
+ get: get,
539
+ has,
540
+ required,
541
+ string,
542
+ number,
543
+ bool,
544
+ port,
545
+ url,
546
+ duration,
547
+ list,
548
+ json,
549
+ oneOf,
550
+ validate,
551
+ mode,
552
+ isProduction,
553
+ isDevelopment,
554
+ isTest,
555
+ };