@ix-xs/node-comfort 1.2.0 → 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.
- package/CHANGELOG.md +190 -0
- package/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +501 -235
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- 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
|
+
};
|