@geekmidas/envkit 0.5.0 → 0.7.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/dist/{EnvironmentParser-DlWHnhDY.cjs → EnvironmentParser-DJdW7vOL.cjs} +2 -2
- package/dist/{EnvironmentParser-DlWHnhDY.cjs.map → EnvironmentParser-DJdW7vOL.cjs.map} +1 -1
- package/dist/{EnvironmentParser-CBLsPUyQ.mjs → EnvironmentParser-zMblItla.mjs} +2 -2
- package/dist/{EnvironmentParser-CBLsPUyQ.mjs.map → EnvironmentParser-zMblItla.mjs.map} +1 -1
- package/dist/EnvironmentParser.cjs +2 -2
- package/dist/EnvironmentParser.mjs +2 -2
- package/dist/SnifferEnvironmentParser.cjs +58 -2
- package/dist/SnifferEnvironmentParser.cjs.map +1 -1
- package/dist/SnifferEnvironmentParser.d.cts +54 -1
- package/dist/SnifferEnvironmentParser.d.cts.map +1 -1
- package/dist/SnifferEnvironmentParser.d.mts +54 -1
- package/dist/SnifferEnvironmentParser.d.mts.map +1 -1
- package/dist/SnifferEnvironmentParser.mjs +58 -3
- package/dist/SnifferEnvironmentParser.mjs.map +1 -1
- package/dist/{formatter-fz8V7x6i.mjs → formatter-BRRrxQi3.mjs} +2 -4
- package/dist/formatter-BRRrxQi3.mjs.map +1 -0
- package/dist/formatter-Cox0NGxT.d.mts.map +1 -1
- package/dist/formatter-D85aIkpd.d.cts.map +1 -1
- package/dist/{formatter-w8Tsccw4.cjs → formatter-HxePpSy2.cjs} +2 -4
- package/dist/formatter-HxePpSy2.cjs.map +1 -0
- package/dist/formatter.cjs +1 -1
- package/dist/formatter.mjs +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/src/SnifferEnvironmentParser.ts +97 -0
- package/src/__tests__/SnifferEnvironmentParser.spec.ts +105 -1
- package/src/__tests__/formatter.spec.ts +17 -0
- package/src/formatter.ts +4 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/formatter-fz8V7x6i.mjs.map +0 -1
- package/dist/formatter-w8Tsccw4.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatter-D85aIkpd.d.cts","names":[],"sources":["../src/formatter.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAgDgB,UAhDC,aAAA,CAgDe;EAAA;EAAA,MACtB,CAAA,EAAA,OAAA;;AACkB;
|
|
1
|
+
{"version":3,"file":"formatter-D85aIkpd.d.cts","names":[],"sources":["../src/formatter.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAgDgB,UAhDC,aAAA,CAgDe;EAAA;EAAA,MACtB,CAAA,EAAA,OAAA;;AACkB;AAuF5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAzFgB,gBAAA,QACR,CAAA,CAAE,oBACA;;;;iBAuFM,aAAA,CAAA"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
-
const zod_v4 = require_chunk.__toESM(require("zod/v4"));
|
|
3
1
|
|
|
4
2
|
//#region src/formatter.ts
|
|
5
3
|
/**
|
|
@@ -58,7 +56,7 @@ function formatParseError(error, options = {}) {
|
|
|
58
56
|
const invalidVars = [];
|
|
59
57
|
for (const issue of error.issues) {
|
|
60
58
|
let envName = "";
|
|
61
|
-
if (issue.path.length > 0) envName =
|
|
59
|
+
if (issue.path.length > 0) envName = issue.path.map(String).join(".");
|
|
62
60
|
else {
|
|
63
61
|
const match = issue.message.match(/Environment variable "([^"]+)"/);
|
|
64
62
|
if (match?.[1]) envName = match[1];
|
|
@@ -122,4 +120,4 @@ Object.defineProperty(exports, 'isDevelopment', {
|
|
|
122
120
|
return isDevelopment;
|
|
123
121
|
}
|
|
124
122
|
});
|
|
125
|
-
//# sourceMappingURL=formatter-
|
|
123
|
+
//# sourceMappingURL=formatter-HxePpSy2.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter-HxePpSy2.cjs","names":["error: z.ZodError","options: FormatOptions","missingVars: Array<{ name: string; message: string }>","invalidVars: Array<{ name: string; value: unknown; message: string }>","lines: string[]","message: string"],"sources":["../src/formatter.ts"],"sourcesContent":["import type { z } from 'zod/v4';\n\n/**\n * Options for formatting parse errors.\n */\nexport interface FormatOptions {\n\t/** Whether to use colors in output. Defaults to auto-detect TTY. */\n\tcolors?: boolean;\n}\n\n/**\n * ANSI color codes for terminal output.\n */\nconst colors = {\n\treset: '\\x1b[0m',\n\tred: '\\x1b[31m',\n\tyellow: '\\x1b[33m',\n\tcyan: '\\x1b[36m',\n\tdim: '\\x1b[2m',\n\tbold: '\\x1b[1m',\n};\n\n/**\n * Formats a ZodError into a user-friendly string for development.\n *\n * @param error - The ZodError to format\n * @param options - Formatting options\n * @returns Formatted error message\n *\n * @example\n * ```typescript\n * try {\n * config.parse();\n * } catch (error) {\n * if (error instanceof ZodError) {\n * console.error(formatParseError(error));\n * }\n * }\n * ```\n *\n * Output:\n * ```\n * Environment Configuration Failed\n *\n * Missing Variables:\n * DATABASE_URL - Required\n * JWT_SECRET - Required\n *\n * Invalid Values:\n * NODE_ENV = \"invalid\"\n * Expected: \"development\" | \"staging\" | \"production\"\n * ```\n */\nexport function formatParseError(\n\terror: z.ZodError,\n\toptions: FormatOptions = {},\n): string {\n\tconst useColors =\n\t\toptions.colors ?? (process.stdout?.isTTY && process.env.NO_COLOR == null);\n\n\tconst c = useColors\n\t\t? colors\n\t\t: { reset: '', red: '', yellow: '', cyan: '', dim: '', bold: '' };\n\n\tconst missingVars: Array<{ name: string; message: string }> = [];\n\tconst invalidVars: Array<{ name: string; value: unknown; message: string }> =\n\t\t[];\n\n\tfor (const issue of error.issues) {\n\t\t// Extract environment variable name from path or message\n\t\tlet envName = '';\n\t\tif (issue.path.length > 0) {\n\t\t\t// Join the full path with '.' to show nested config keys and env var name\n\t\t\tenvName = issue.path.map(String).join('.');\n\t\t} else {\n\t\t\t// Try to extract from message like 'Environment variable \"NAME\": ...'\n\t\t\tconst match = issue.message.match(/Environment variable \"([^\"]+)\"/);\n\t\t\tif (match?.[1]) {\n\t\t\t\tenvName = match[1];\n\t\t\t}\n\t\t}\n\n\t\t// Determine if this is a missing or invalid value\n\t\t// Use type guard for received property\n\t\tconst received = 'received' in issue ? issue.received : undefined;\n\t\tconst isMissing =\n\t\t\tissue.code === 'invalid_type' &&\n\t\t\t(received === 'undefined' || received === 'null');\n\n\t\tif (isMissing) {\n\t\t\tmissingVars.push({\n\t\t\t\tname: envName || 'Unknown',\n\t\t\t\tmessage: cleanMessage(issue.message),\n\t\t\t});\n\t\t} else {\n\t\t\tinvalidVars.push({\n\t\t\t\tname: envName || 'Unknown',\n\t\t\t\tvalue: received,\n\t\t\t\tmessage: cleanMessage(issue.message),\n\t\t\t});\n\t\t}\n\t}\n\n\tconst lines: string[] = [];\n\n\tlines.push('');\n\tlines.push(`${c.red}${c.bold}Environment Configuration Failed${c.reset}`);\n\tlines.push('');\n\n\tif (missingVars.length > 0) {\n\t\tlines.push(`${c.yellow}Missing Variables:${c.reset}`);\n\t\tfor (const v of missingVars) {\n\t\t\tlines.push(` ${c.cyan}${v.name}${c.reset} ${c.dim}- Required${c.reset}`);\n\t\t}\n\t\tlines.push('');\n\t}\n\n\tif (invalidVars.length > 0) {\n\t\tlines.push(`${c.yellow}Invalid Values:${c.reset}`);\n\t\tfor (const v of invalidVars) {\n\t\t\tconst valueStr =\n\t\t\t\tv.value !== undefined ? ` = ${JSON.stringify(v.value)}` : '';\n\t\t\tlines.push(` ${c.cyan}${v.name}${c.reset}${valueStr}`);\n\t\t\tlines.push(` ${c.dim}${v.message}${c.reset}`);\n\t\t}\n\t\tlines.push('');\n\t}\n\n\treturn lines.join('\\n');\n}\n\n/**\n * Cleans up a Zod error message by removing redundant prefixes.\n */\nfunction cleanMessage(message: string): string {\n\t// Remove \"Environment variable \"NAME\": \" prefix if present\n\treturn message.replace(/^Environment variable \"[^\"]+\": /, '');\n}\n\n/**\n * Checks if the current environment is development.\n */\nexport function isDevelopment(): boolean {\n\tconst nodeEnv = process.env.NODE_ENV?.toLowerCase();\n\treturn nodeEnv == null || nodeEnv === 'development' || nodeEnv === 'dev';\n}\n"],"mappings":";;;;;AAaA,MAAM,SAAS;CACd,OAAO;CACP,KAAK;CACL,QAAQ;CACR,MAAM;CACN,KAAK;CACL,MAAM;AACN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCD,SAAgB,iBACfA,OACAC,UAAyB,CAAE,GAClB;CACT,MAAM,YACL,QAAQ,WAAW,QAAQ,QAAQ,SAAS,QAAQ,IAAI,YAAY;CAErE,MAAM,IAAI,YACP,SACA;EAAE,OAAO;EAAI,KAAK;EAAI,QAAQ;EAAI,MAAM;EAAI,KAAK;EAAI,MAAM;CAAI;CAElE,MAAMC,cAAwD,CAAE;CAChE,MAAMC,cACL,CAAE;AAEH,MAAK,MAAM,SAAS,MAAM,QAAQ;EAEjC,IAAI,UAAU;AACd,MAAI,MAAM,KAAK,SAAS,EAEvB,WAAU,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,IAAI;OACpC;GAEN,MAAM,QAAQ,MAAM,QAAQ,MAAM,iCAAiC;AACnE,OAAI,QAAQ,GACX,WAAU,MAAM;EAEjB;EAID,MAAM,WAAW,cAAc,QAAQ,MAAM;EAC7C,MAAM,YACL,MAAM,SAAS,mBACd,aAAa,eAAe,aAAa;AAE3C,MAAI,UACH,aAAY,KAAK;GAChB,MAAM,WAAW;GACjB,SAAS,aAAa,MAAM,QAAQ;EACpC,EAAC;MAEF,aAAY,KAAK;GAChB,MAAM,WAAW;GACjB,OAAO;GACP,SAAS,aAAa,MAAM,QAAQ;EACpC,EAAC;CAEH;CAED,MAAMC,QAAkB,CAAE;AAE1B,OAAM,KAAK,GAAG;AACd,OAAM,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,KAAK,kCAAkC,EAAE,MAAM,EAAE;AACzE,OAAM,KAAK,GAAG;AAEd,KAAI,YAAY,SAAS,GAAG;AAC3B,QAAM,MAAM,EAAE,EAAE,OAAO,oBAAoB,EAAE,MAAM,EAAE;AACrD,OAAK,MAAM,KAAK,YACf,OAAM,MAAM,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,YAAY,EAAE,MAAM,EAAE;AAE1E,QAAM,KAAK,GAAG;CACd;AAED,KAAI,YAAY,SAAS,GAAG;AAC3B,QAAM,MAAM,EAAE,EAAE,OAAO,iBAAiB,EAAE,MAAM,EAAE;AAClD,OAAK,MAAM,KAAK,aAAa;GAC5B,MAAM,WACL,EAAE,oBAAuB,KAAK,KAAK,UAAU,EAAE,MAAM,CAAC,IAAI;AAC3D,SAAM,MAAM,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE;AACvD,SAAM,MAAM,MAAM,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE;EAChD;AACD,QAAM,KAAK,GAAG;CACd;AAED,QAAO,MAAM,KAAK,KAAK;AACvB;;;;AAKD,SAAS,aAAaC,SAAyB;AAE9C,QAAO,QAAQ,QAAQ,mCAAmC,GAAG;AAC7D;;;;AAKD,SAAgB,gBAAyB;CACxC,MAAM,UAAU,QAAQ,IAAI,UAAU,aAAa;AACnD,QAAO,WAAW,QAAQ,YAAY,iBAAiB,YAAY;AACnE"}
|
package/dist/formatter.cjs
CHANGED
package/dist/formatter.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_EnvironmentBuilder = require('./EnvironmentBuilder-Djr1VsWM.cjs');
|
|
2
|
-
const require_formatter = require('./formatter-
|
|
3
|
-
const require_EnvironmentParser = require('./EnvironmentParser-
|
|
2
|
+
const require_formatter = require('./formatter-HxePpSy2.cjs');
|
|
3
|
+
const require_EnvironmentParser = require('./EnvironmentParser-DJdW7vOL.cjs');
|
|
4
4
|
|
|
5
5
|
exports.ConfigParser = require_EnvironmentParser.ConfigParser;
|
|
6
6
|
exports.EnvironmentBuilder = require_EnvironmentBuilder.EnvironmentBuilder;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EnvironmentBuilder, environmentCase } from "./EnvironmentBuilder-BSuHZm0y.mjs";
|
|
2
|
-
import { formatParseError, isDevelopment } from "./formatter-
|
|
3
|
-
import { ConfigParser, EnvironmentParser } from "./EnvironmentParser-
|
|
2
|
+
import { formatParseError, isDevelopment } from "./formatter-BRRrxQi3.mjs";
|
|
3
|
+
import { ConfigParser, EnvironmentParser } from "./EnvironmentParser-zMblItla.mjs";
|
|
4
4
|
|
|
5
5
|
export { ConfigParser, EnvironmentBuilder, EnvironmentParser, environmentCase, formatParseError, isDevelopment };
|
package/package.json
CHANGED
|
@@ -207,3 +207,100 @@ class SnifferConfigParser<
|
|
|
207
207
|
return '';
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Result of sniffing with fire-and-forget handling.
|
|
213
|
+
*/
|
|
214
|
+
export interface SniffResult {
|
|
215
|
+
/** Environment variables that were accessed during sniffing */
|
|
216
|
+
envVars: string[];
|
|
217
|
+
/** Error thrown during sniffing (env vars may still be captured) */
|
|
218
|
+
error?: Error;
|
|
219
|
+
/** Unhandled promise rejections captured during sniffing */
|
|
220
|
+
unhandledRejections: Error[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Options for sniffing with fire-and-forget handling.
|
|
225
|
+
*/
|
|
226
|
+
export interface SniffOptions {
|
|
227
|
+
/**
|
|
228
|
+
* Time in milliseconds to wait for fire-and-forget promises to settle.
|
|
229
|
+
* Some libraries like better-auth create async operations that may reject
|
|
230
|
+
* after the initial event loop tick.
|
|
231
|
+
* @default 100
|
|
232
|
+
*/
|
|
233
|
+
settleTimeMs?: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Executes a sniffing operation with fire-and-forget handling.
|
|
238
|
+
*
|
|
239
|
+
* This function:
|
|
240
|
+
* 1. Captures unhandled promise rejections during the operation
|
|
241
|
+
* 2. Waits for async operations to settle before returning
|
|
242
|
+
* 3. Gracefully handles errors without throwing
|
|
243
|
+
*
|
|
244
|
+
* Use this when sniffing environment variables from code that may:
|
|
245
|
+
* - Throw synchronous errors
|
|
246
|
+
* - Create fire-and-forget promises that reject
|
|
247
|
+
* - Have async initialization that may fail
|
|
248
|
+
*
|
|
249
|
+
* @param sniffer - The SnifferEnvironmentParser instance to use
|
|
250
|
+
* @param operation - The async operation to execute (e.g., service.register)
|
|
251
|
+
* @param options - Optional configuration
|
|
252
|
+
* @returns SniffResult with env vars and any errors encountered
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const sniffer = new SnifferEnvironmentParser();
|
|
257
|
+
* const result = await sniffWithFireAndForget(sniffer, async () => {
|
|
258
|
+
* await service.register({ envParser: sniffer });
|
|
259
|
+
* });
|
|
260
|
+
* console.log('Env vars:', result.envVars);
|
|
261
|
+
* console.log('Error:', result.error);
|
|
262
|
+
* console.log('Unhandled rejections:', result.unhandledRejections);
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
export async function sniffWithFireAndForget(
|
|
266
|
+
sniffer: SnifferEnvironmentParser,
|
|
267
|
+
operation: () => unknown | Promise<unknown>,
|
|
268
|
+
options: SniffOptions = {},
|
|
269
|
+
): Promise<SniffResult> {
|
|
270
|
+
const { settleTimeMs = 100 } = options;
|
|
271
|
+
const unhandledRejections: Error[] = [];
|
|
272
|
+
|
|
273
|
+
// Capture unhandled rejections during sniffing (fire-and-forget promises)
|
|
274
|
+
// Libraries like better-auth create async operations that may reject after
|
|
275
|
+
// the initial event loop tick
|
|
276
|
+
const captureRejection = (reason: unknown) => {
|
|
277
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
278
|
+
unhandledRejections.push(err);
|
|
279
|
+
};
|
|
280
|
+
process.on('unhandledRejection', captureRejection);
|
|
281
|
+
|
|
282
|
+
let error: Error | undefined;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = operation();
|
|
286
|
+
|
|
287
|
+
// Handle async result
|
|
288
|
+
if (result && typeof result === 'object' && 'then' in result) {
|
|
289
|
+
await Promise.resolve(result).catch((e) => {
|
|
290
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
295
|
+
} finally {
|
|
296
|
+
// Wait for fire-and-forget promises to settle
|
|
297
|
+
await new Promise((resolve) => setTimeout(resolve, settleTimeMs));
|
|
298
|
+
process.off('unhandledRejection', captureRejection);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
envVars: sniffer.getEnvironmentVariables(),
|
|
303
|
+
error,
|
|
304
|
+
unhandledRejections,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { z } from 'zod/v4';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SnifferEnvironmentParser,
|
|
5
|
+
sniffWithFireAndForget,
|
|
6
|
+
} from '../SnifferEnvironmentParser';
|
|
4
7
|
|
|
5
8
|
describe('SnifferEnvironmentParser', () => {
|
|
6
9
|
describe('Environment variable tracking', () => {
|
|
@@ -384,3 +387,104 @@ describe('SnifferEnvironmentParser', () => {
|
|
|
384
387
|
});
|
|
385
388
|
});
|
|
386
389
|
});
|
|
390
|
+
|
|
391
|
+
describe('sniffWithFireAndForget', () => {
|
|
392
|
+
it('should capture environment variables from synchronous operations', async () => {
|
|
393
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
394
|
+
|
|
395
|
+
const result = await sniffWithFireAndForget(sniffer, () => {
|
|
396
|
+
sniffer.create((get) => ({
|
|
397
|
+
dbUrl: get('DATABASE_URL').string(),
|
|
398
|
+
apiKey: get('API_KEY').string(),
|
|
399
|
+
}));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(result.envVars).toEqual(['API_KEY', 'DATABASE_URL']);
|
|
403
|
+
expect(result.error).toBeUndefined();
|
|
404
|
+
expect(result.unhandledRejections).toEqual([]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should capture environment variables from async operations', async () => {
|
|
408
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
409
|
+
|
|
410
|
+
const result = await sniffWithFireAndForget(sniffer, async () => {
|
|
411
|
+
await Promise.resolve();
|
|
412
|
+
sniffer.create((get) => ({
|
|
413
|
+
redisUrl: get('REDIS_URL').string(),
|
|
414
|
+
}));
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(result.envVars).toEqual(['REDIS_URL']);
|
|
418
|
+
expect(result.error).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should capture error when operation throws synchronously', async () => {
|
|
422
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
423
|
+
|
|
424
|
+
const result = await sniffWithFireAndForget(sniffer, () => {
|
|
425
|
+
sniffer.create((get) => ({
|
|
426
|
+
value: get('CAPTURED_BEFORE_ERROR').string(),
|
|
427
|
+
}));
|
|
428
|
+
throw new Error('Sync error');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Should still capture env vars accessed before the error
|
|
432
|
+
expect(result.envVars).toEqual(['CAPTURED_BEFORE_ERROR']);
|
|
433
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
434
|
+
expect(result.error?.message).toBe('Sync error');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should capture error when async operation rejects', async () => {
|
|
438
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
439
|
+
|
|
440
|
+
const result = await sniffWithFireAndForget(sniffer, async () => {
|
|
441
|
+
sniffer.create((get) => ({
|
|
442
|
+
value: get('CAPTURED_BEFORE_REJECT').string(),
|
|
443
|
+
}));
|
|
444
|
+
throw new Error('Async rejection');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(result.envVars).toEqual(['CAPTURED_BEFORE_REJECT']);
|
|
448
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
449
|
+
expect(result.error?.message).toBe('Async rejection');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should convert non-Error throws to Error objects', async () => {
|
|
453
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
454
|
+
|
|
455
|
+
const result = await sniffWithFireAndForget(sniffer, () => {
|
|
456
|
+
throw 'string error';
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
460
|
+
expect(result.error?.message).toBe('string error');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should use custom settle time', async () => {
|
|
464
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
465
|
+
const startTime = Date.now();
|
|
466
|
+
|
|
467
|
+
await sniffWithFireAndForget(
|
|
468
|
+
sniffer,
|
|
469
|
+
() => {},
|
|
470
|
+
{ settleTimeMs: 50 },
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const elapsed = Date.now() - startTime;
|
|
474
|
+
// Should wait at least 50ms but not much longer
|
|
475
|
+
expect(elapsed).toBeGreaterThanOrEqual(50);
|
|
476
|
+
expect(elapsed).toBeLessThan(200);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should return empty arrays when no vars accessed and no errors', async () => {
|
|
480
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
481
|
+
|
|
482
|
+
const result = await sniffWithFireAndForget(sniffer, () => {
|
|
483
|
+
// Do nothing
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(result.envVars).toEqual([]);
|
|
487
|
+
expect(result.error).toBeUndefined();
|
|
488
|
+
expect(result.unhandledRejections).toEqual([]);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -102,6 +102,23 @@ describe('formatParseError', () => {
|
|
|
102
102
|
expect(formatted).toContain('Expected number, received string');
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it('should join nested paths with dot', () => {
|
|
106
|
+
const error = new z.ZodError([
|
|
107
|
+
createIssue({
|
|
108
|
+
code: 'invalid_type',
|
|
109
|
+
expected: 'string',
|
|
110
|
+
received: 'undefined',
|
|
111
|
+
path: ['databaseUrl', 'DATABASE_URL'],
|
|
112
|
+
message: 'Environment variable "DATABASE_URL": Required',
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const formatted = formatParseError(error, { colors: false });
|
|
117
|
+
|
|
118
|
+
// Should show the full path joined with '.'
|
|
119
|
+
expect(formatted).toContain('databaseUrl.DATABASE_URL');
|
|
120
|
+
});
|
|
121
|
+
|
|
105
122
|
it('should extract env name from message when path is empty', () => {
|
|
106
123
|
const error = new z.ZodError([
|
|
107
124
|
createIssue({
|
package/src/formatter.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from 'zod/v4';
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for formatting parse errors.
|
|
@@ -70,7 +70,8 @@ export function formatParseError(
|
|
|
70
70
|
// Extract environment variable name from path or message
|
|
71
71
|
let envName = '';
|
|
72
72
|
if (issue.path.length > 0) {
|
|
73
|
-
|
|
73
|
+
// Join the full path with '.' to show nested config keys and env var name
|
|
74
|
+
envName = issue.path.map(String).join('.');
|
|
74
75
|
} else {
|
|
75
76
|
// Try to extract from message like 'Environment variable "NAME": ...'
|
|
76
77
|
const match = issue.message.match(/Environment variable "([^"]+)"/);
|
|
@@ -103,9 +104,7 @@ export function formatParseError(
|
|
|
103
104
|
const lines: string[] = [];
|
|
104
105
|
|
|
105
106
|
lines.push('');
|
|
106
|
-
lines.push(
|
|
107
|
-
`${c.red}${c.bold}Environment Configuration Failed${c.reset}`,
|
|
108
|
-
);
|
|
107
|
+
lines.push(`${c.red}${c.bold}Environment Configuration Failed${c.reset}`);
|
|
109
108
|
lines.push('');
|
|
110
109
|
|
|
111
110
|
if (missingVars.length > 0) {
|