Skip to content

Commit 47b4065

Browse files
authored
Merge pull request #15 from lostfictions/compat
Add edge runtime compatibility and allow custom formatters
2 parents 813efca + 983a07b commit 47b4065

File tree

12 files changed

+678
-370
lines changed

12 files changed

+678
-370
lines changed

README.md

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ optionally provide defaults (which can be matched against `NODE_ENV` values like
1818
`production` or `development`), as well as help strings that will be included in
1919
the error thrown when an env var is missing.
2020

21+
## Features
22+
23+
- No dependencies
24+
- Fully type-safe
25+
- Compatible with serverless environments (import `znv/compat` instead of `znv`)
26+
2127
## Status
2228

2329
Unstable: znv has not yet hit v1.0.0, and per semver there may be breaking
@@ -31,7 +37,7 @@ about final API design are welcome.
3137
- [Quickstart](#quickstart)
3238
- [Motivation](#motivation)
3339
- [Usage](#usage)
34-
- [`parseEnv`](#parseenvenvironment-schemas)
40+
- [`parseEnv`](#parseenvenvironment-schemas-reporterOrFormatters)
3541
- [Extra schemas](#extra-schemas)
3642
- [Coercion rules](#coercion-rules)
3743
- [Comparison to other libraries](#comparison-to-other-libraries)
@@ -43,6 +49,8 @@ about final API design are welcome.
4349
```bash
4450
npm i znv zod
4551
# or
52+
pnpm add znv zod
53+
# or
4654
yarn add znv zod
4755
```
4856

@@ -200,7 +208,7 @@ environments is not straightforward.
200208

201209
## Usage
202210

203-
### `parseEnv(environment, schemas)`
211+
### `parseEnv(environment, schemas, reporterOrFormatters?)`
204212

205213
Parse the given `environment` using the given `schemas`. Returns a read-only
206214
object that maps the keys of the `schemas` object to their respective parsed
@@ -209,6 +217,13 @@ values.
209217
Throws if any schema fails to parse its respective env var. The error aggregates
210218
all parsing failures for the schemas.
211219

220+
Optionally, you can pass a custom error reporter as the third parameter to
221+
`parseEnv` to customize how errors are displayed. The reporter is a function
222+
that receives error details and returns a `string`. Alternately, you can pass an
223+
object of _token formatters_ as the third parameter to `parseEnv`; this can be
224+
useful if you want to retain the default error reporting format but want to
225+
customize some aspects of it (for example, by redacting secrets).
226+
212227
#### `environment: Record<string, string | undefined>`
213228

214229
You usually want to pass in `process.env` as the first argument.
@@ -308,6 +323,66 @@ pass a `DetailedSpec` object that has the following fields:
308323
`NODE_ENV: z.enum(["production", "development", "test", "ci"])` to enforce
309324
that `NODE_ENV` is always defined and is one of those four expected values.
310325

326+
#### `reporterOrFormatters?: Reporter | TokenFormatters`
327+
328+
An optional error reporter or object of error token formatters, for customizing
329+
the displayed output when a validation error occurs.
330+
331+
- `Reporter: (errors: ErrorWithContext[], schemas: Schemas) => string`
332+
333+
A reporter is a function that takes a list of errors and the schemas you
334+
passed to `parseEnv` and returns a `string`. Each error has the following
335+
format:
336+
337+
```ts
338+
{
339+
/** The env var name. */
340+
key: string;
341+
/** The actual value present in `process.env[key]`, or undefined. */
342+
receivedValue: unknown;
343+
/** `ZodError` if Zod parsing failed, or `Error` if a preprocessor threw. */
344+
error: unknown;
345+
/** If a default was provided, whether the default value was used. */
346+
defaultUsed: boolean;
347+
/** If a default was provided, the given default value. */
348+
defaultValue: unknown;
349+
}
350+
```
351+
352+
- `TokenFormatters`
353+
354+
An object with the following structure:
355+
356+
```ts
357+
{
358+
/** Formatter for the env var name. */
359+
formatVarName?: (key: string) => string;
360+
361+
/** For parsed objects with errors, formatter for object keys. */
362+
formatObjKey?: (key: string) => string;
363+
364+
/** Formatter for the actual value we received for the env var. */
365+
formatReceivedValue?: (val: unknown) => string;
366+
367+
/** Formatter for the default value provided for the schema. */
368+
formatDefaultValue?: (val: unknown) => string;
369+
370+
/** Formatter for the error summary header. */
371+
formatHeader?: (header: string) => string;
372+
}
373+
```
374+
375+
For example, if you want to redact value names, you can invoke `parseEnv` like
376+
this:
377+
378+
```ts
379+
export const { SOME_VAL } = parseEnv(
380+
process.env,
381+
{ SOME_VAL: z.number().nonnegative() },
382+
{ formatReceivedValue: () => "<redacted>" },
383+
);
384+
```
385+
311386
### Extra schemas
312387

313388
znv exports a very small number of extra schemas for common env var types.

package.json

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
"name": "znv",
33
"version": "0.4.0",
44
"description": "Parse your environment with Zod schemas",
5-
"type": "module",
5+
"license": "MIT",
66
"keywords": [
77
"env",
88
"process.env",
99
"zod",
1010
"validation"
1111
],
12-
"main": "dist-cjs/index.js",
13-
"types": "dist/index.d.ts",
14-
"module": "dist/index.js",
1512
"author": "s <https://github.com/lostfictions>",
1613
"homepage": "https://github.com/lostfictions/znv",
1714
"repository": {
@@ -21,6 +18,10 @@
2118
"bugs": {
2219
"url": "https://github.com/lostfictions/znv/issues"
2320
},
21+
"type": "module",
22+
"main": "dist-cjs/index.js",
23+
"types": "dist/index.d.ts",
24+
"module": "dist/index.js",
2425
"files": [
2526
"dist/",
2627
"dist-cjs/"
@@ -35,9 +36,18 @@
3536
"types": "./dist-cjs/index.d.ts",
3637
"default": "./dist-cjs/index.js"
3738
}
39+
},
40+
"./compat": {
41+
"import": {
42+
"types": "./dist/compat.d.ts",
43+
"default": "./dist/compat.js"
44+
},
45+
"require": {
46+
"types": "./dist-cjs/compat.d.ts",
47+
"default": "./dist-cjs/compat.js"
48+
}
3849
}
3950
},
40-
"license": "MIT",
4151
"scripts": {
4252
"build": "run-s -l build:*",
4353
"build:clean": "rm -rf dist/ dist-cjs/",
@@ -53,22 +63,19 @@
5363
"jest": "jest --colors --watch",
5464
"prepublishOnly": "run-s -l test build"
5565
},
56-
"dependencies": {
57-
"colorette": "^2.0.19"
58-
},
5966
"peerDependencies": {
6067
"zod": "^3.13.2"
6168
},
6269
"devDependencies": {
63-
"@types/jest": "^29.5.4",
64-
"@types/node": "^16.18.24",
65-
"eslint": "^8.48.0",
66-
"eslint-config-lostfictions": "^6.0.0",
67-
"jest": "^29.6.4",
70+
"@types/jest": "^29.5.11",
71+
"@types/node": "^18.19.3",
72+
"eslint": "^8.55.0",
73+
"eslint-config-lostfictions": "^6.1.0",
74+
"jest": "^29.7.0",
6875
"npm-run-all": "^4.1.5",
69-
"prettier": "^3.0.3",
76+
"prettier": "^3.1.1",
7077
"ts-jest": "^29.1.1",
71-
"ts-node": "^10.9.1",
78+
"ts-node": "^10.9.2",
7279
"typescript": "^4.9.5",
7380
"zod": "~3.13.2"
7481
},

src/compat.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export { z } from "zod";
2+
export * from "./parse-env.js";
3+
export * from "./preprocessors.js";
4+
export * from "./extra-schemas.js";
5+
6+
import { parseEnvImpl, type ParseEnv } from "./parse-env.js";
7+
8+
/**
9+
* Parses the passed environment object using the provided map of Zod schemas
10+
* and returns the immutably-typed, parsed environment. Compatible with
11+
* serverless and browser environments.
12+
*/
13+
export const parseEnv: ParseEnv = (
14+
env,
15+
schemas,
16+
reporterOrTokenFormatters = {},
17+
) => parseEnvImpl(env, schemas, reporterOrTokenFormatters);

src/extra-schemas.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseEnv } from "./parse-env.js";
1+
import { parseEnv } from "./index.js";
22
import { deprecate } from "./extra-schemas.js";
33

44
describe("extra schemas", () => {

src/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,30 @@ export { z } from "zod";
22
export * from "./parse-env.js";
33
export * from "./preprocessors.js";
44
export * from "./extra-schemas.js";
5-
export type * from "./util.js";
5+
export type {
6+
DeepReadonly,
7+
DeepReadonlyArray,
8+
DeepReadonlyObject,
9+
} from "./util/type-helpers.js";
10+
11+
import { parseEnvImpl, type ParseEnv } from "./parse-env.js";
12+
import { cyan, green, red, yellow } from "./util/tty-colors.js";
13+
14+
// This entrypoint provides a colorized reporter by default; this requires tty
15+
// detection, which in turn relies on Node's built-in `tty` module.
16+
17+
/**
18+
* Parses the passed environment object using the provided map of Zod schemas
19+
* and returns the immutably-typed, parsed environment.
20+
*/
21+
export const parseEnv: ParseEnv = (
22+
env,
23+
schemas,
24+
reporterOrTokenFormatters = {
25+
formatVarName: yellow,
26+
formatObjKey: green,
27+
formatReceivedValue: cyan,
28+
formatDefaultValue: cyan,
29+
formatHeader: red,
30+
},
31+
) => parseEnvImpl(env, schemas, reporterOrTokenFormatters);

src/parse-env.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as z from "zod";
22

3-
import { parseEnv } from "./parse-env.js";
3+
import { parseEnv } from "./index.js";
44
import { port } from "./extra-schemas.js";
55

66
// FIXME: many of these don't need to be part of parseCore tests, or at minimum

src/parse-env.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import * as z from "zod";
22

33
import { getSchemaWithPreprocessor } from "./preprocessors.js";
4-
import { ErrorWithContext, reportErrors, errorMap } from "./reporter.js";
4+
import {
5+
makeDefaultReporter,
6+
errorMap,
7+
type TokenFormatters,
8+
type ErrorWithContext,
9+
type Reporter,
10+
} from "./reporter.js";
511

6-
import type { DeepReadonlyObject } from "./util.js";
12+
import type { DeepReadonlyObject } from "./util/type-helpers.js";
713

814
export type SimpleSchema<TOut = any, TIn = any> = z.ZodType<
915
TOut,
@@ -55,20 +61,20 @@ export type RestrictSchemas<T extends Schemas> = {
5561
[K in keyof T]: T[K] extends SimpleSchema
5662
? SimpleSchema
5763
: T[K] extends DetailedSpec
58-
? DetailedSpec<T[K]["schema"]> &
59-
Omit<Record<keyof T[K], never>, DetailedSpecKeys>
60-
: never;
64+
? DetailedSpec<T[K]["schema"]> &
65+
Omit<Record<keyof T[K], never>, DetailedSpecKeys>
66+
: never;
6167
};
6268

6369
export type ParsedSchema<T extends Schemas> = T extends any
6470
? {
6571
[K in keyof T]: T[K] extends SimpleSchema<infer TOut>
6672
? TOut
6773
: T[K] extends DetailedSpec
68-
? T[K]["schema"] extends SimpleSchema<infer TOut>
69-
? TOut
70-
: never
71-
: never;
74+
? T[K]["schema"] extends SimpleSchema<infer TOut>
75+
? TOut
76+
: never
77+
: never;
7278
}
7379
: never;
7480

@@ -100,14 +106,31 @@ export const inferSchemas = <T extends Schemas>(
100106
schemas: T & RestrictSchemas<T>,
101107
): T & RestrictSchemas<T> => schemas;
102108

109+
export type ParseEnv = <T extends Schemas>(
110+
env: Record<string, string | undefined>,
111+
schemas: T & RestrictSchemas<T>,
112+
reporterOrTokenFormatters?: Reporter | TokenFormatters,
113+
) => DeepReadonlyObject<ParsedSchema<T>>;
114+
103115
/**
104116
* Parses the passed environment object using the provided map of Zod schemas
105-
* and returns the immutably-typed, parsed environment..
117+
* and returns the immutably-typed, parsed environment.
118+
*
119+
* This version of `parseEnv` is intended for internal use and requires a
120+
* reporter or token formatters to be passed in. The versions exported in
121+
* `index.js` and `compat.js` provide defaults for this third parameter, making
122+
* it optional.
106123
*/
107-
export function parseEnv<T extends Schemas>(
124+
export function parseEnvImpl<T extends Schemas>(
108125
env: Record<string, string | undefined>,
109-
schemas: T & RestrictSchemas<T>,
126+
schemas: T,
127+
reporterOrTokenFormatters: Reporter | TokenFormatters,
110128
): DeepReadonlyObject<ParsedSchema<T>> {
129+
const reporter =
130+
typeof reporterOrTokenFormatters === "function"
131+
? reporterOrTokenFormatters
132+
: makeDefaultReporter(reporterOrTokenFormatters);
133+
111134
const parsed: Record<string, unknown> = {} as any;
112135

113136
const errors: ErrorWithContext[] = [];
@@ -173,7 +196,7 @@ export function parseEnv<T extends Schemas>(
173196
}
174197

175198
if (errors.length > 0) {
176-
throw new Error(reportErrors(errors, schemas));
199+
throw new Error(reporter(errors, schemas));
177200
}
178201

179202
return parsed as DeepReadonlyObject<ParsedSchema<T>>;

src/preprocessors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as z from "zod";
22

3-
import { assertNever } from "./util.js";
3+
import { assertNever } from "./util/type-helpers.js";
44

55
const { ZodFirstPartyTypeKind: TypeName } = z;
66

0 commit comments

Comments
 (0)