celery-env 0.1.0 → 0.1.1
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/README.md +52 -15
- package/docs/BENCHMARKS.md +76 -10
- package/docs/CLI.md +1 -1
- package/docs/COMPARISON.md +43 -7
- package/docs/GETTING_STARTED.md +27 -11
- package/docs/README.md +1 -1
- package/docs/SCHEMA.md +5 -4
- package/package.json +18 -7
- package/src/compiler.js +90 -44
- package/src/index.d.ts +1 -1
- package/src/index.js +48 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>Type-safe process.env validation that compiles to tiny standalone JavaScript.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -22,10 +22,18 @@ messages, and fast startup.
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
+
Generated mode:
|
|
26
|
+
|
|
25
27
|
```sh
|
|
26
28
|
npm install -D celery-env
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
Runtime mode:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install celery-env
|
|
35
|
+
```
|
|
36
|
+
|
|
29
37
|
Other package managers:
|
|
30
38
|
|
|
31
39
|
```sh
|
|
@@ -74,6 +82,9 @@ import { loadEnv } from "./env.mjs";
|
|
|
74
82
|
export const env = loadEnv(process.env);
|
|
75
83
|
```
|
|
76
84
|
|
|
85
|
+
Celery validates the env object you pass. Load `.env` with your platform,
|
|
86
|
+
shell, or `dotenv` before calling `loadEnv`.
|
|
87
|
+
|
|
77
88
|
That is the main path: write `env.schema.mjs`, generate `src/env.mjs`, and use
|
|
78
89
|
the typed result everywhere else.
|
|
79
90
|
|
|
@@ -91,6 +102,15 @@ If you already use Zod for forms, API payloads, or general data validation,
|
|
|
91
102
|
keep using it there. Celery is for the narrower `process.env` problem where
|
|
92
103
|
defaults, examples, generated files, and startup cost matter.
|
|
93
104
|
|
|
105
|
+
## Quick Comparison
|
|
106
|
+
|
|
107
|
+
| Choose | When |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| Celery generated mode | You want a committed standalone validator, generated TypeScript declarations, generated `.env.example`, no production dependency, or lower cold-start cost. |
|
|
110
|
+
| Celery runtime mode | You like Celery's schema API but cannot add generation yet. |
|
|
111
|
+
| Zod | You need general object, form, API, or nested data validation. |
|
|
112
|
+
| Envalid / Envsafe / env-var | You want mature runtime-only env validation with no generated files. |
|
|
113
|
+
|
|
94
114
|
## Why Use It
|
|
95
115
|
|
|
96
116
|
- **Generated validator**: no schema walk during app startup.
|
|
@@ -118,20 +138,37 @@ export const env = parseEnv(schema, process.env);
|
|
|
118
138
|
|
|
119
139
|
## Benchmarks
|
|
120
140
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
Snapshot: Node v26.3.0, macOS arm64, Apple M3, generated on 2026-06-24. Higher
|
|
142
|
+
ops/sec is better; lower milliseconds and bytes are better. Generated-mode
|
|
143
|
+
numbers exclude compile/generation cost.
|
|
144
|
+
|
|
145
|
+
| Tool / mode | Real schemas ops/sec | Real `process.env` ops/sec | Invalid ops/sec | Cold first validation | Gzip snapshot |
|
|
146
|
+
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
147
|
+
| Celery generated | 1,411,473 | 228,570 | 112,361 | 1.849 ms | 526 B |
|
|
148
|
+
| Celery runtime | 776,241 | 185,124 | 96,846 | 2.598 ms | 2,779 B |
|
|
149
|
+
| Zod | 516,820 | 141,126 | 39,760 | 33.999 ms | 20,894 B |
|
|
150
|
+
| Valibot | 454,925 | 109,584 | 84,841 | 6.925 ms | 2,055 B |
|
|
151
|
+
| Envalid | 125,202 | 85,436 | 16,731 | 9.598 ms | 7,318 B |
|
|
152
|
+
| Envsafe | 940,564 | 184,818 | 19,359 | 5.694 ms | 3,292 B |
|
|
153
|
+
| env-var | 51,287 | 44,727 | 31,922 | 7.679 ms | 2,969 B |
|
|
154
|
+
| T3 Env Core | 316,627 | 168,740 | 10,264 | 32.366 ms | 19,531 B |
|
|
155
|
+
|
|
156
|
+
The benchmark corpus includes realistic API, web, worker, list-heavy, and
|
|
157
|
+
JSON-heavy env schemas. Results are workload-specific; real `process.env` access
|
|
158
|
+
narrows some gaps compared with frozen plain env objects.
|
|
159
|
+
|
|
160
|
+
Current npm package metadata:
|
|
161
|
+
|
|
162
|
+
| Package | Version checked | Runtime deps | Unpacked npm size | Files |
|
|
163
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
164
|
+
| `celery-env` | 0.1.1 | 0 | 94.1 kB | 20 |
|
|
165
|
+
| `zod` | 4.4.3 | 0 | 4.56 MB | 718 |
|
|
166
|
+
| `valibot` | 1.4.1 | 0 | 1.84 MB | 9 |
|
|
167
|
+
| `envalid` | 8.2.0 | 1 | 88.8 kB | 39 |
|
|
168
|
+
| `envsafe` | 2.0.3 | 0 | 91.4 kB | 27 |
|
|
169
|
+
| `env-var` | 7.5.0 | 0 | 42.9 kB | 30 |
|
|
170
|
+
|
|
171
|
+
Metadata checked with `npm view` on 2026-06-25.
|
|
135
172
|
|
|
136
173
|
## Documentation
|
|
137
174
|
|
package/docs/BENCHMARKS.md
CHANGED
|
@@ -6,16 +6,34 @@ not the local benchmark lab.
|
|
|
6
6
|
|
|
7
7
|
## Current Headline
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Snapshot: Node v26.3.0, V8 14.6.202.34-node.20, macOS arm64, Apple M3,
|
|
10
|
+
generated on 2026-06-24. Higher ops/sec is better; lower milliseconds and bytes
|
|
11
|
+
are better. Generated-mode numbers exclude compile/generation cost.
|
|
10
12
|
|
|
11
|
-
|
|
|
12
|
-
| --- | ---: |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
13
|
+
| Tool / mode | Primary use | Generated no-dep validator | Real schemas ops/sec geom mean | Real `process.env` ops/sec geom mean | Invalid ops/sec geom mean | Cold first validation | Gzip snapshot |
|
|
14
|
+
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |
|
|
15
|
+
| Celery generated | Env config | Yes | 1,411,473 | 228,570 | 112,361 | 1.849 ms | 526 B |
|
|
16
|
+
| Celery runtime | Env config | No | 776,241 | 185,124 | 96,846 | 2.598 ms | 2,779 B |
|
|
17
|
+
| Zod | General validation | No | 516,820 | 141,126 | 39,760 | 33.999 ms | 20,894 B |
|
|
18
|
+
| Valibot | General validation | No | 454,925 | 109,584 | 84,841 | 6.925 ms | 2,055 B |
|
|
19
|
+
| Envalid | Env validation | No | 125,202 | 85,436 | 16,731 | 9.598 ms | 7,318 B |
|
|
20
|
+
| Envsafe | Env validation | No | 940,564 | 184,818 | 19,359 | 5.694 ms | 3,292 B |
|
|
21
|
+
| env-var | Env accessors | No | 51,287 | 44,727 | 31,922 | 7.679 ms | 2,969 B |
|
|
22
|
+
| T3 Env Core | Typed env schema | No | 316,627 | 168,740 | 10,264 | 32.366 ms | 19,531 B |
|
|
23
|
+
|
|
24
|
+
Summary against the best external per-case baseline:
|
|
25
|
+
|
|
26
|
+
| Metric | Celery generated | Best external per-case baseline | Result |
|
|
27
|
+
| --- | ---: | ---: | ---: |
|
|
28
|
+
| Valid real-schema geometric mean | 1,411,473 ops/sec | 940,564 ops/sec | 1.50x |
|
|
29
|
+
| Real `process.env` geometric mean | 228,570 ops/sec | 200,908 ops/sec | 1.14x |
|
|
30
|
+
| Invalid real-schema geometric mean | 112,361 ops/sec | 84,841 ops/sec | 1.32x |
|
|
31
|
+
| Cold first validation | 1.849 ms | 4.466 ms | 2.42x faster |
|
|
32
|
+
| Shipped gzip snapshot | 526 B | 1,130 B | 2.15x smaller |
|
|
33
|
+
|
|
34
|
+
These are results from the env-validation corpus. They are useful for choosing
|
|
35
|
+
a tool, but they are not a guarantee for every schema, host runtime, or
|
|
36
|
+
deployment shape.
|
|
19
37
|
|
|
20
38
|
## What Is Measured
|
|
21
39
|
|
|
@@ -32,11 +50,59 @@ The benchmark corpus includes Zod, Valibot, Envalid, Envsafe, env-var, T3 Env
|
|
|
32
50
|
Core, Valienv, env-schema, env-type-validator, safe-env-vars, and Convict where
|
|
33
51
|
the benchmark applies.
|
|
34
52
|
|
|
53
|
+
Snapshot dependency versions:
|
|
54
|
+
|
|
55
|
+
| Package | Version |
|
|
56
|
+
| --- | ---: |
|
|
57
|
+
| `zod` | 4.4.3 |
|
|
58
|
+
| `valibot` | 1.4.1 |
|
|
59
|
+
| `envalid` | 8.2.0 |
|
|
60
|
+
| `envsafe` | 2.0.3 |
|
|
61
|
+
| `env-var` | 7.5.0 |
|
|
62
|
+
| `@t3-oss/env-core` | 0.13.11 |
|
|
63
|
+
| `valienv` | 1.1.0 |
|
|
64
|
+
| `env-schema` | 7.0.0 |
|
|
65
|
+
| `env-type-validator` | 1.0.1 |
|
|
66
|
+
| `safe-env-vars` | 1.0.8 |
|
|
67
|
+
| `convict` | 6.2.5 |
|
|
68
|
+
|
|
69
|
+
## Package Metadata
|
|
70
|
+
|
|
71
|
+
Package footprint is separate from runtime speed. Current npm metadata checked
|
|
72
|
+
on 2026-06-25:
|
|
73
|
+
|
|
74
|
+
| Package | Version Checked | Runtime Deps | Unpacked npm Size | Files |
|
|
75
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
76
|
+
| `celery-env` | 0.1.1 | 0 | 94.1 kB | 20 |
|
|
77
|
+
| `zod` | 4.4.3 | 0 | 4.56 MB | 718 |
|
|
78
|
+
| `valibot` | 1.4.1 | 0 | 1.84 MB | 9 |
|
|
79
|
+
| `envalid` | 8.2.0 | 1 | 88.8 kB | 39 |
|
|
80
|
+
| `envsafe` | 2.0.3 | 0 | 91.4 kB | 27 |
|
|
81
|
+
| `env-var` | 7.5.0 | 0 | 42.9 kB | 30 |
|
|
82
|
+
|
|
83
|
+
Celery's generated validator size can be much smaller than the package size
|
|
84
|
+
because generated mode ships only the emitted validator in production code.
|
|
85
|
+
|
|
86
|
+
## Reproducibility
|
|
87
|
+
|
|
88
|
+
The published package intentionally does not ship the local benchmark lab or
|
|
89
|
+
competitor dependencies. Public docs keep the benchmark summary and claim rules
|
|
90
|
+
so the npm package stays small and dependency-free.
|
|
91
|
+
|
|
92
|
+
When refreshing claims, record:
|
|
93
|
+
|
|
94
|
+
- `celery-env` version or commit;
|
|
95
|
+
- Node version, operating system, CPU, and date;
|
|
96
|
+
- competitor package versions;
|
|
97
|
+
- whether the row uses frozen env objects or real `process.env`;
|
|
98
|
+
- whether errors are aggregate or fail-fast;
|
|
99
|
+
- generated validator gzip size separately from npm package size.
|
|
100
|
+
|
|
35
101
|
## Claim Rules
|
|
36
102
|
|
|
37
103
|
Use precise benchmark claims:
|
|
38
104
|
|
|
39
|
-
- Good: "1.50x over the best external
|
|
105
|
+
- Good: "1.50x over the best external per-case baseline on the real-schema corpus."
|
|
40
106
|
- Good: "526 gzip bytes for the measured small generated validator."
|
|
41
107
|
- Avoid: "Celery is always 50x faster."
|
|
42
108
|
|
package/docs/CLI.md
CHANGED
|
@@ -53,7 +53,7 @@ Add a script:
|
|
|
53
53
|
```json
|
|
54
54
|
{
|
|
55
55
|
"scripts": {
|
|
56
|
-
"env:generate": "celery-env generate --schema env.schema.mjs --out src/env.mjs --types src/env.d.ts --example .env.example --minify"
|
|
56
|
+
"env:generate": "celery-env generate --schema env.schema.mjs --out src/env.mjs --types src/env.d.ts --example .env.example --minify --force"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
```
|
package/docs/COMPARISON.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Celery is not a replacement for general validation libraries. It is a focused
|
|
4
4
|
tool for environment configuration.
|
|
5
5
|
|
|
6
|
+
## Quick Decision Guide
|
|
7
|
+
|
|
8
|
+
| Choose | When |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| Celery generated mode | You want a committed standalone validator, generated TypeScript declarations, generated `.env.example`, no production dependency, or lower cold-start cost. |
|
|
11
|
+
| Celery runtime mode | You like Celery's schema API but cannot add generation yet. |
|
|
12
|
+
| Zod | You already need general object validation, nested data validation, or shared schemas beyond `process.env`. |
|
|
13
|
+
| Envalid / Envsafe / env-var | You want a mature runtime-only env validator with no generated files or build step. |
|
|
14
|
+
| Stay with your current tool | Env validation is not on the startup path and generated artifacts would add workflow friction. |
|
|
15
|
+
|
|
6
16
|
## Celery vs Zod
|
|
7
17
|
|
|
8
18
|
Zod is excellent for general TypeScript validation: forms, API payloads,
|
|
@@ -38,15 +48,41 @@ Celery's main difference is generated mode:
|
|
|
38
48
|
- TypeScript declarations can be generated from the same schema;
|
|
39
49
|
- `.env.example` can be generated from schema metadata.
|
|
40
50
|
|
|
41
|
-
##
|
|
51
|
+
## Generated vs Runtime Mode
|
|
52
|
+
|
|
53
|
+
| Question | Generated mode | Runtime mode |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| Production dependency on `celery-env` | No | Yes |
|
|
56
|
+
| Build or generate step | Yes | No |
|
|
57
|
+
| Startup cost | Lowest | Schema parsed at runtime |
|
|
58
|
+
| Generated `.d.ts` | Yes | Use `InferEnv` |
|
|
59
|
+
| Generated `.env.example` | Yes | Only if you run the CLI |
|
|
60
|
+
| Best fit | Services, serverless, production apps | Scripts, prototypes, no-build projects |
|
|
61
|
+
|
|
62
|
+
## Choose Another Tool When
|
|
42
63
|
|
|
43
|
-
|
|
|
64
|
+
| Need | Better Fit |
|
|
44
65
|
| --- | --- |
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
66
|
+
| General object, form, API, or nested JSON validation | Zod |
|
|
67
|
+
| Runtime-only env validation with no generated artifacts | Envalid / Envsafe / env-var |
|
|
68
|
+
| Built-in `.env` file loading | dotenv plus a validator |
|
|
69
|
+
| Maximum ecosystem maturity | Zod or established env validators |
|
|
70
|
+
| No schema execution during build or generation | A static config format or runtime-only validator |
|
|
71
|
+
|
|
72
|
+
## Package Footprint
|
|
73
|
+
|
|
74
|
+
This table is npm package metadata, not benchmark speed:
|
|
75
|
+
|
|
76
|
+
| Package | Version Checked | Runtime Deps | Unpacked npm Size | Files |
|
|
77
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
78
|
+
| `celery-env` | 0.1.1 | 0 | 94.1 kB | 20 |
|
|
79
|
+
| `zod` | 4.4.3 | 0 | 4.56 MB | 718 |
|
|
80
|
+
| `valibot` | 1.4.1 | 0 | 1.84 MB | 9 |
|
|
81
|
+
| `envalid` | 8.2.0 | 1 | 88.8 kB | 39 |
|
|
82
|
+
| `envsafe` | 2.0.3 | 0 | 91.4 kB | 27 |
|
|
83
|
+
| `env-var` | 7.5.0 | 0 | 42.9 kB | 30 |
|
|
84
|
+
|
|
85
|
+
Checked with `npm view` on 2026-06-25.
|
|
50
86
|
|
|
51
87
|
## What Celery Does Not Do
|
|
52
88
|
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
This guide
|
|
4
|
-
environment variables.
|
|
3
|
+
This guide shows the generated-validator path for a Node project.
|
|
5
4
|
|
|
6
5
|
## 1. Install
|
|
7
6
|
|
|
@@ -15,12 +14,12 @@ yarn add -D celery-env
|
|
|
15
14
|
bun add -d celery-env
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Generated validators do not need `celery-env` at runtime, so most apps install
|
|
18
|
+
it as a dev dependency.
|
|
20
19
|
|
|
21
20
|
## 2. Create A Schema
|
|
22
21
|
|
|
23
|
-
Create `env.schema.mjs
|
|
22
|
+
Create `env.schema.mjs`:
|
|
24
23
|
|
|
25
24
|
```js
|
|
26
25
|
import { bool, defineEnv, int, oneOf, str, url } from "celery-env";
|
|
@@ -63,7 +62,7 @@ npx celery-env generate \
|
|
|
63
62
|
--minify
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
The command writes:
|
|
67
66
|
|
|
68
67
|
| File | Purpose |
|
|
69
68
|
| --- | --- |
|
|
@@ -71,12 +70,12 @@ This creates:
|
|
|
71
70
|
| `src/env.d.ts` | Types for editors and TypeScript. |
|
|
72
71
|
| `.env.example` | Documented env template. |
|
|
73
72
|
|
|
74
|
-
Add
|
|
73
|
+
Add a script so generation is repeatable:
|
|
75
74
|
|
|
76
75
|
```json
|
|
77
76
|
{
|
|
78
77
|
"scripts": {
|
|
79
|
-
"env:generate": "celery-env generate --schema env.schema.mjs --out src/env.mjs --types src/env.d.ts --example .env.example --minify"
|
|
78
|
+
"env:generate": "celery-env generate --schema env.schema.mjs --out src/env.mjs --types src/env.d.ts --example .env.example --minify --force"
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
```
|
|
@@ -89,12 +88,14 @@ import { loadEnv } from "./env.mjs";
|
|
|
89
88
|
export const env = loadEnv(process.env);
|
|
90
89
|
```
|
|
91
90
|
|
|
91
|
+
Celery validates the env object you pass. Load `.env` with your platform,
|
|
92
|
+
shell, or `dotenv` before calling `loadEnv`.
|
|
93
|
+
|
|
92
94
|
Use `env` everywhere else instead of reading `process.env` directly.
|
|
93
95
|
|
|
94
|
-
## 5.
|
|
96
|
+
## 5. Optional: Shape App Config
|
|
95
97
|
|
|
96
|
-
If your app
|
|
97
|
-
and adapt it in one small file:
|
|
98
|
+
Keep the generated env flat. If your app wants nested config, adapt it once:
|
|
98
99
|
|
|
99
100
|
```js
|
|
100
101
|
import { loadEnv } from "./env.mjs";
|
|
@@ -126,6 +127,21 @@ For most apps, commit all of these:
|
|
|
126
127
|
Committing generated files keeps deployments simple because production does not
|
|
127
128
|
need to run the generator.
|
|
128
129
|
|
|
130
|
+
## Keep Generated Files In Sync
|
|
131
|
+
|
|
132
|
+
Regenerate after changing `env.schema.mjs`:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
npm run env:generate
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
In CI, verify generated files are current:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
npm run env:generate
|
|
142
|
+
git diff --exit-code src/env.mjs src/env.d.ts .env.example
|
|
143
|
+
```
|
|
144
|
+
|
|
129
145
|
## Next Steps
|
|
130
146
|
|
|
131
147
|
- Read [Schema API](SCHEMA.md) when adding validators.
|
package/docs/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Start here if you are new to Celery or new to typed env validation.
|
|
|
9
9
|
3. [CLI](CLI.md): `init`, `generate`, and generation flags.
|
|
10
10
|
4. [TypeScript](TYPESCRIPT.md): generated types and `InferEnv`.
|
|
11
11
|
5. [Runtime Mode](RUNTIME.md): use Celery without generation.
|
|
12
|
-
6. [Comparison](COMPARISON.md): when to
|
|
12
|
+
6. [Comparison](COMPARISON.md): when to choose Celery, Zod, Envalid, Envsafe, or runtime-only env validators.
|
|
13
13
|
7. [Troubleshooting](TROUBLESHOOTING.md): common setup and generation issues.
|
|
14
14
|
8. [Migration Guide](MIGRATION.md): add Celery to an existing project.
|
|
15
15
|
9. [Benchmarks](BENCHMARKS.md): what is measured and what claims are safe.
|
package/docs/SCHEMA.md
CHANGED
|
@@ -41,8 +41,8 @@ options describe defaults, examples, and environment-specific behavior.
|
|
|
41
41
|
| `example` | Example value used in generated `.env.example`. |
|
|
42
42
|
| `docs` | Longer documentation text for generated metadata. |
|
|
43
43
|
|
|
44
|
-
`testDefault` wins over `devDefault`, and
|
|
45
|
-
|
|
44
|
+
`testDefault` wins over `devDefault`, and both env-specific defaults win over
|
|
45
|
+
`default`.
|
|
46
46
|
|
|
47
47
|
## Missing Values
|
|
48
48
|
|
|
@@ -70,8 +70,9 @@ int({ min: 1, max: 65535 })
|
|
|
70
70
|
num({ min: 0, max: 1 })
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
By default, numeric parsing follows JavaScript `Number()`.
|
|
74
|
-
to reject values such as
|
|
73
|
+
By default, numeric parsing follows JavaScript `Number()`. For ports, limits,
|
|
74
|
+
rates, and other config values, prefer `strict: true` to reject values such as
|
|
75
|
+
hex and exponent notation:
|
|
75
76
|
|
|
76
77
|
```js
|
|
77
78
|
int({ strict: true })
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "celery-env",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Type-safe process.env validation that generates zero-dependency standalone validators.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -55,20 +55,31 @@
|
|
|
55
55
|
"size": "node scripts/size.mjs",
|
|
56
56
|
"security:scan": "node scripts/security-scan.mjs && npm run prepublishOnly",
|
|
57
57
|
"validate:publish": "node scripts/validate-publish.mjs",
|
|
58
|
-
"
|
|
58
|
+
"smoke:pack": "node scripts/smoke-pack.mjs",
|
|
59
|
+
"prepublishOnly": "npm test && npm run size && npm run validate:publish && npm run smoke:pack",
|
|
59
60
|
"demo:generate": "node src/cli.js --schema examples/env.schema.mjs --out .tmp/generated/env.mjs --types .tmp/generated/env.d.ts"
|
|
60
61
|
},
|
|
61
62
|
"keywords": [
|
|
62
63
|
"env",
|
|
63
|
-
"
|
|
64
|
+
"env-var",
|
|
65
|
+
"env-vars",
|
|
64
66
|
"environment",
|
|
67
|
+
"environment-variables",
|
|
68
|
+
"process-env",
|
|
69
|
+
"env-validation",
|
|
70
|
+
"env-validator",
|
|
71
|
+
"validation",
|
|
65
72
|
"config",
|
|
73
|
+
"configuration",
|
|
74
|
+
"config-validation",
|
|
66
75
|
"schema",
|
|
76
|
+
"type-safe",
|
|
67
77
|
"typescript",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
78
|
+
"typed-env",
|
|
79
|
+
"node",
|
|
70
80
|
"dotenv",
|
|
71
|
-
"cli"
|
|
81
|
+
"cli",
|
|
82
|
+
"serverless"
|
|
72
83
|
],
|
|
73
84
|
"license": "MIT",
|
|
74
85
|
"engines": {
|
package/src/compiler.js
CHANGED
|
@@ -20,6 +20,7 @@ export function generateValidator(schema, options = {}) {
|
|
|
20
20
|
if (rule.t === LIST && needsListTempVar(rule)) needsListTemp = true;
|
|
21
21
|
body.push(...emitRule(key, rule, `_${i}`, ctx));
|
|
22
22
|
}
|
|
23
|
+
if (!ctx.f) envError(ctx);
|
|
23
24
|
const lines = ctx.g;
|
|
24
25
|
lines.push(`export function ${fn}(${param}) {`);
|
|
25
26
|
if (!ctx.f) lines.push(" let r;");
|
|
@@ -28,7 +29,7 @@ export function generateValidator(schema, options = {}) {
|
|
|
28
29
|
if (needsListTemp) lines.push(" let x;");
|
|
29
30
|
lines.push(...body);
|
|
30
31
|
|
|
31
|
-
if (!ctx.f) lines.push(" if (r)
|
|
32
|
+
if (!ctx.f) lines.push(" if (r) R(r);");
|
|
32
33
|
lines.push(
|
|
33
34
|
` return ${returnObject(entries)};`,
|
|
34
35
|
"}",
|
|
@@ -48,13 +49,14 @@ function generateObjectValidator(entries, fn, param, ctx, options) {
|
|
|
48
49
|
if (rule.t === LIST && needsListTempVar(rule)) needsListTemp = true;
|
|
49
50
|
body.push(...emitRule(key, rule, `o${prop(key)}`, ctx));
|
|
50
51
|
}
|
|
52
|
+
if (!ctx.f) envError(ctx);
|
|
51
53
|
const lines = ctx.g;
|
|
52
54
|
lines.push(`export function ${fn}(${param}) {`);
|
|
53
55
|
if (!ctx.f) lines.push(" let r;");
|
|
54
56
|
lines.push(" let v;", " const o = {};");
|
|
55
57
|
if (needsListTemp) lines.push(" let x;");
|
|
56
58
|
lines.push(...body);
|
|
57
|
-
if (!ctx.f) lines.push(" if (r)
|
|
59
|
+
if (!ctx.f) lines.push(" if (r) R(r);");
|
|
58
60
|
lines.push(" return o;", "}", `export default ${fn};`, "");
|
|
59
61
|
if (options.minify) return lines.map((line) => line.trim()).join("");
|
|
60
62
|
return lines.join("\n");
|
|
@@ -66,6 +68,7 @@ function generateSplitValidator(entries, fn, param, ctx, options) {
|
|
|
66
68
|
for (let start = 0; start < entries.length; start += 32) {
|
|
67
69
|
lines.push(...emitSplitChunk(entries, start, Math.min(start + 32, entries.length), n++, ctx));
|
|
68
70
|
}
|
|
71
|
+
if (!ctx.f) envError(ctx);
|
|
69
72
|
|
|
70
73
|
lines.push(`export function ${fn}(${param}) {`);
|
|
71
74
|
if (!ctx.f) lines.push(" let r;");
|
|
@@ -73,7 +76,7 @@ function generateSplitValidator(entries, fn, param, ctx, options) {
|
|
|
73
76
|
for (let i = 0; i < n; i++) {
|
|
74
77
|
lines.push(ctx.f ? ` _c${i}(env, a);` : ` r = _c${i}(env, a, r);`);
|
|
75
78
|
}
|
|
76
|
-
if (!ctx.f) lines.push(" if (r)
|
|
79
|
+
if (!ctx.f) lines.push(" if (r) R(r);");
|
|
77
80
|
lines.push(
|
|
78
81
|
` return ${returnObject(entries, "a")};`,
|
|
79
82
|
"}",
|
|
@@ -221,6 +224,8 @@ function assertSchema(schema) {
|
|
|
221
224
|
const entries = Object.entries(schema);
|
|
222
225
|
for (const [key, rule] of entries) {
|
|
223
226
|
if (!(rule && typeof rule === "object" && H(rule, "t") && typeof rule.t === "number")) throw new TypeError(`${key}: invalid celery-env spec`);
|
|
227
|
+
assertRuleOptions(key, rule);
|
|
228
|
+
if (rule.t === LIST && !(rule.item && typeof rule.item === "object" && H(rule.item, "t") && typeof rule.item.t === "number")) throw new TypeError(`${key}: list item is not a celery-env spec`);
|
|
224
229
|
if (rule.t === LIST && rule.item.t === LIST) throw new TypeError(`${key}: nested list generation is not supported`);
|
|
225
230
|
if (rule.requiredWhen != null && typeof rule.requiredWhen !== "function") throw new TypeError(`${key}: requiredWhen must be a function`);
|
|
226
231
|
for (const field of ["default", "devDefault", "testDefault"]) {
|
|
@@ -232,6 +237,28 @@ function assertSchema(schema) {
|
|
|
232
237
|
return entries;
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
function assertRuleOptions(key, rule) {
|
|
241
|
+
for (const field of ["min", "max"]) {
|
|
242
|
+
if (H(rule, field) && !Number.isFinite(rule[field])) throw new TypeError(`${key}: ${field} must be a finite number`);
|
|
243
|
+
}
|
|
244
|
+
if (H(rule, "strict") && typeof rule.strict !== "boolean") throw new TypeError(`${key}: strict must be a boolean`);
|
|
245
|
+
if (H(rule, "optional") && typeof rule.optional !== "boolean") throw new TypeError(`${key}: optional must be a boolean`);
|
|
246
|
+
if (H(rule, "startsWith") && typeof rule.startsWith !== "string") throw new TypeError(`${key}: startsWith must be a string`);
|
|
247
|
+
if (H(rule, "includes") && typeof rule.includes !== "string") throw new TypeError(`${key}: includes must be a string`);
|
|
248
|
+
if (H(rule, "protocols") && (!Array.isArray(rule.protocols) || rule.protocols.some(p => typeof p !== "string" || p === "" || p.includes(":")))) {
|
|
249
|
+
throw new TypeError(`${key}: protocols must be protocol names without ":"`);
|
|
250
|
+
}
|
|
251
|
+
if (rule.t === URL_T && H(rule, "protocols") && (!Array.isArray(rule.ps) || rule.ps.length !== rule.protocols.length || rule.ps.some((p, i) => p !== `${rule.protocols[i]}:`))) {
|
|
252
|
+
throw new TypeError(`${key}: malformed URL protocol spec`);
|
|
253
|
+
}
|
|
254
|
+
if (H(rule, "separator") && typeof rule.separator !== "string") throw new TypeError(`${key}: separator must be a string`);
|
|
255
|
+
if (H(rule, "trim") && typeof rule.trim !== "boolean") throw new TypeError(`${key}: trim must be a boolean`);
|
|
256
|
+
if (rule.t === ENUM && (!Array.isArray(rule.values) || !rule.values.length || rule.values.some(v => typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" || typeof v === "number" && !Number.isFinite(v)))) {
|
|
257
|
+
throw new TypeError(`${key}: oneOf values must be strings, finite numbers, or booleans`);
|
|
258
|
+
}
|
|
259
|
+
if (rule.t === LIST && rule.item && typeof rule.item === "object") assertRuleOptions(`${key} item`, rule.item);
|
|
260
|
+
}
|
|
261
|
+
|
|
235
262
|
function validDefault(rule, value) {
|
|
236
263
|
if (value === undefined) return rule.optional === true;
|
|
237
264
|
switch (rule.t) {
|
|
@@ -299,7 +326,7 @@ function emitterContext(options) {
|
|
|
299
326
|
if (optimize && optimize !== "default" && optimize !== "speed") throw new TypeError(`Unknown optimize mode: ${optimize}`);
|
|
300
327
|
const t = options.splitLarge === false ? 1/0 : options.splitLargeThreshold ?? 512;
|
|
301
328
|
if (!Number.isInteger(t) && t !== 1/0) throw new TypeError("splitLargeThreshold must be an integer");
|
|
302
|
-
return { e: 0, f: options.failFast === true, g: [], h: 0, j: 0, s: optimize > "default", t };
|
|
329
|
+
return { e: 0, f: options.failFast === true, g: [], h: 0, j: 0, r: 0, s: optimize > "default", t };
|
|
303
330
|
}
|
|
304
331
|
|
|
305
332
|
function functionName(options) {
|
|
@@ -352,6 +379,10 @@ function envFacade(ctx) {
|
|
|
352
379
|
return "E(env)";
|
|
353
380
|
}
|
|
354
381
|
|
|
382
|
+
function envError(ctx) {
|
|
383
|
+
if(!ctx.r++)ctx.g.push(`function R(r){const e=Error("Invalid environment:\\n- "+r.join("\\n- "));e.name="EnvError";e.errors=r;throw e}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
355
386
|
function simpleMissing(rule) {
|
|
356
387
|
return !hasDefault(rule) && !rule.requiredWhen;
|
|
357
388
|
}
|
|
@@ -369,12 +400,12 @@ function emitMissing(key, rule, pad, target, out, ctx) {
|
|
|
369
400
|
if (H(rule, "default")) {
|
|
370
401
|
out.push(`${pad}${used ? "else " : ""}${target} = ${literal(rule.default)};`);
|
|
371
402
|
} else if (rule.optional && rule.requiredWhen) {
|
|
372
|
-
out.push(`${pad}${used ? "else " : ""}if (${requiredWhenExpr(rule)}(${envFacade(ctx)}) === true) ${
|
|
403
|
+
out.push(`${pad}${used ? "else " : ""}if (${requiredWhenExpr(rule)}(${envFacade(ctx)}) === true) ${errMsg(key, " is required", ctx)};`);
|
|
373
404
|
if (target > "b") out.push(`${pad}else ${target} = undefined;`);
|
|
374
405
|
} else if (rule.optional) {
|
|
375
406
|
out.push(`${pad}${used ? "else " : ""}${target} = undefined;`);
|
|
376
407
|
} else {
|
|
377
|
-
out.push(`${pad}${used ? "else " : ""}${
|
|
408
|
+
out.push(`${pad}${used ? "else " : ""}${errMsg(key, " is required", ctx)};`);
|
|
378
409
|
}
|
|
379
410
|
}
|
|
380
411
|
|
|
@@ -403,16 +434,16 @@ function emitPresent(key, rule, pad, target, ctx, value = "v") {
|
|
|
403
434
|
|
|
404
435
|
function emitJson(key, pad, target, value, ctx) {
|
|
405
436
|
if(!ctx.j++)ctx.g.push(`function J(v){return v[0]=="{"&&v[v.length-1]!="}"||v[0]=="["&&v[v.length-1]!="]"}`);
|
|
406
|
-
const er=
|
|
437
|
+
const er=errMsg(key, " must be valid JSON",ctx);
|
|
407
438
|
return [`${pad}try { if (J(${value})) throw 0; ${target} = JSON.parse(${value}); } catch { ${er}; }`];
|
|
408
439
|
}
|
|
409
440
|
|
|
410
441
|
function emitString(key, rule, pad, target, value, ctx) {
|
|
411
442
|
const out = [];
|
|
412
|
-
if (rule.min != null && !skipMin(rule)) out.push(`${pad}if (${value}.length < ${num(rule.min)}) ${
|
|
413
|
-
if (rule.max != null) out.push(`${out.length ? pad + "else " : pad}if (${value}.length > ${num(rule.max)}) ${
|
|
414
|
-
if (rule.startsWith != null) out.push(`${out.length ? pad + "else " : pad}if (!${value}.startsWith(${literal(rule.startsWith)})) ${
|
|
415
|
-
if (rule.includes != null) out.push(`${out.length ? pad + "else " : pad}if (!${value}.includes(${literal(rule.includes)})) ${
|
|
443
|
+
if (rule.min != null && !skipMin(rule)) out.push(`${pad}if (${value}.length < ${num(rule.min)}) ${errMsg(key, ` must have length >= ${rule.min}`, ctx)};`);
|
|
444
|
+
if (rule.max != null) out.push(`${out.length ? pad + "else " : pad}if (${value}.length > ${num(rule.max)}) ${errMsg(key, ` must have length <= ${rule.max}`, ctx)};`);
|
|
445
|
+
if (rule.startsWith != null) out.push(`${out.length ? pad + "else " : pad}if (!${value}.startsWith(${literal(rule.startsWith)})) ${errMsg(key, ` must start with ${rule.startsWith}`, ctx)};`);
|
|
446
|
+
if (rule.includes != null) out.push(`${out.length ? pad + "else " : pad}if (!${value}.includes(${literal(rule.includes)})) ${errMsg(key, ` must include ${rule.includes}`, ctx)};`);
|
|
416
447
|
out.push(`${out.length ? pad + "else " : pad}${target} = ${value};`);
|
|
417
448
|
return out;
|
|
418
449
|
}
|
|
@@ -427,14 +458,14 @@ function emitNumber(key, rule, pad, target, integer, value, ctx) {
|
|
|
427
458
|
const out = [];
|
|
428
459
|
if (rule.strict) {
|
|
429
460
|
const re = integer ? "/^[+-]?\\d+$/" : "/^[+-]?(?:\\d+\\.?\\d*|\\.\\d+)$/";
|
|
430
|
-
out.push(`${pad}if (!${re}.test(${value})) ${
|
|
461
|
+
out.push(`${pad}if (!${re}.test(${value})) ${errMsg(key, ` must be ${integer ? "a strict integer" : "a strict number"}`, ctx)};`);
|
|
431
462
|
out.push(`${pad}else {`);
|
|
432
463
|
pad += " ";
|
|
433
464
|
}
|
|
434
465
|
out.push(`${pad}${value} = +${value};`);
|
|
435
|
-
out.push(`${pad}if (${integer ? int32Bounded(rule) ? `(${value} | 0) !== ${value}` : `!Number.isInteger(${value})` : `!isFinite(${value})`}) ${
|
|
436
|
-
if (rule.min != null) out.push(`${pad}else if (${value} < ${num(rule.min)}) ${
|
|
437
|
-
if (rule.max != null) out.push(`${pad}else if (${value} > ${num(rule.max)}) ${
|
|
466
|
+
out.push(`${pad}if (${integer ? int32Bounded(rule) ? `(${value} | 0) !== ${value}` : `!Number.isInteger(${value})` : `!isFinite(${value})`}) ${errMsg(key, ` must be ${integer ? "an integer" : "a number"}`, ctx)};`);
|
|
467
|
+
if (rule.min != null) out.push(`${pad}else if (${value} < ${num(rule.min)}) ${errMsg(key, ` must be >= ${rule.min}`, ctx)};`);
|
|
468
|
+
if (rule.max != null) out.push(`${pad}else if (${value} > ${num(rule.max)}) ${errMsg(key, ` must be <= ${rule.max}`, ctx)};`);
|
|
438
469
|
out.push(`${pad}else ${target} = ${value};`);
|
|
439
470
|
if (rule.strict) out.push(`${pad.slice(0, -2)}}`);
|
|
440
471
|
return out;
|
|
@@ -454,13 +485,13 @@ function emitStrictNumScalar(key, rule, pad, target, value, ctx) {
|
|
|
454
485
|
`${pad} else if (c < 48 || c > 57) break;`,
|
|
455
486
|
`${pad} else d = 1;`,
|
|
456
487
|
`${pad} }`,
|
|
457
|
-
`${pad} if (!d || q !== ${value}.length) ${
|
|
488
|
+
`${pad} if (!d || q !== ${value}.length) ${errMsg(key, " must be a strict number", ctx)};`,
|
|
458
489
|
`${pad} else {`,
|
|
459
490
|
`${pad} ${value} = +${value};`,
|
|
460
|
-
`${pad} if (!isFinite(${value})) ${
|
|
491
|
+
`${pad} if (!isFinite(${value})) ${errMsg(key, " must be a number", ctx)};`
|
|
461
492
|
];
|
|
462
|
-
if (rule.min != null) out.push(`${pad} else if (${value} < ${num(rule.min)}) ${
|
|
463
|
-
if (rule.max != null) out.push(`${pad} else if (${value} > ${num(rule.max)}) ${
|
|
493
|
+
if (rule.min != null) out.push(`${pad} else if (${value} < ${num(rule.min)}) ${errMsg(key, ` must be >= ${rule.min}`, ctx)};`);
|
|
494
|
+
if (rule.max != null) out.push(`${pad} else if (${value} > ${num(rule.max)}) ${errMsg(key, ` must be <= ${rule.max}`, ctx)};`);
|
|
464
495
|
out.push(
|
|
465
496
|
`${pad} else ${target} = ${value};`,
|
|
466
497
|
`${pad} }`,
|
|
@@ -477,7 +508,7 @@ function emitStrictIntScalar(key, rule, pad, target, value, ctx) {
|
|
|
477
508
|
`${pad} let c = ${value}.charCodeAt(q);`,
|
|
478
509
|
`${pad} let g = 1;`,
|
|
479
510
|
`${pad} if (c === 43 || c === 45) { g = c === 45 ? -1 : 1; q++; }`,
|
|
480
|
-
`${pad} if (q === z) ${
|
|
511
|
+
`${pad} if (q === z) ${errMsg(key, " must be a strict integer", ctx)};`,
|
|
481
512
|
`${pad} else {`,
|
|
482
513
|
`${pad} let n = 0;`,
|
|
483
514
|
`${pad} for (; q < z; q++) {`,
|
|
@@ -485,13 +516,13 @@ function emitStrictIntScalar(key, rule, pad, target, value, ctx) {
|
|
|
485
516
|
`${pad} if (c < 48 || c > 57) break;`,
|
|
486
517
|
`${pad} n = n * 10 + c - 48;`,
|
|
487
518
|
`${pad} }`,
|
|
488
|
-
`${pad} if (q !== z) ${
|
|
519
|
+
`${pad} if (q !== z) ${errMsg(key, " must be a strict integer", ctx)};`,
|
|
489
520
|
`${pad} else {`,
|
|
490
521
|
`${pad} n *= g;`,
|
|
491
|
-
`${pad} if ((n | 0) !== n) ${
|
|
522
|
+
`${pad} if ((n | 0) !== n) ${errMsg(key, " must be an integer", ctx)};`
|
|
492
523
|
];
|
|
493
|
-
if (rule.min != null) out.push(`${pad} else if (n < ${num(rule.min)}) ${
|
|
494
|
-
if (rule.max != null) out.push(`${pad} else if (n > ${num(rule.max)}) ${
|
|
524
|
+
if (rule.min != null) out.push(`${pad} else if (n < ${num(rule.min)}) ${errMsg(key, ` must be >= ${rule.min}`, ctx)};`);
|
|
525
|
+
if (rule.max != null) out.push(`${pad} else if (n > ${num(rule.max)}) ${errMsg(key, ` must be <= ${rule.max}`, ctx)};`);
|
|
495
526
|
out.push(
|
|
496
527
|
`${pad} else ${target} = n;`,
|
|
497
528
|
`${pad} }`,
|
|
@@ -506,14 +537,14 @@ function emitEnum(key, rule, pad, target, value, ctx) {
|
|
|
506
537
|
const checks = rule.values.map(v=>`${value} === ${literal(v)}`).join(" || ");
|
|
507
538
|
return [
|
|
508
539
|
`${pad}if (${checks}) ${target} = ${value};`,
|
|
509
|
-
`${pad}else ${
|
|
540
|
+
`${pad}else ${errMsg(key, ` must be one of ${rule.values.join(", ")}`, ctx)};`
|
|
510
541
|
];
|
|
511
542
|
}
|
|
512
543
|
const out = [`${pad}switch (${value}) {`];
|
|
513
544
|
for (const v of rule.values) {
|
|
514
545
|
out.push(`${pad} case ${literal(String(v))}: ${target} = ${literal(v)}; break;`);
|
|
515
546
|
}
|
|
516
|
-
out.push(`${pad} default: ${
|
|
547
|
+
out.push(`${pad} default: ${errMsg(key, ` must be one of ${rule.values.join(", ")}`, ctx)};`);
|
|
517
548
|
out.push(`${pad}}`);
|
|
518
549
|
return out;
|
|
519
550
|
}
|
|
@@ -521,9 +552,9 @@ function emitEnum(key, rule, pad, target, value, ctx) {
|
|
|
521
552
|
function emitUrl(key, rule, pad, target, value, ctx) {
|
|
522
553
|
if (rule.protocols) {
|
|
523
554
|
const cases = rule.ps.map((protocol) => `case ${literal(protocol)}:`).join(" ");
|
|
524
|
-
return [`${pad}try { switch (new URL(${value}).protocol) { ${cases} ${target} = ${value}; break; default: ${
|
|
555
|
+
return [`${pad}try { switch (new URL(${value}).protocol) { ${cases} ${target} = ${value}; break; default: ${errMsg(key, ` must use protocol ${rule.protocols.join(", ")}`, ctx)}; } } catch { ${errMsg(key, " must be a URL", ctx)}; }`];
|
|
525
556
|
}
|
|
526
|
-
return [`${pad}try { new URL(${value}); ${target} = ${value}; } catch { ${
|
|
557
|
+
return [`${pad}try { new URL(${value}); ${target} = ${value}; } catch { ${errMsg(key, " must be a URL", ctx)}; }`];
|
|
527
558
|
}
|
|
528
559
|
|
|
529
560
|
function emitList(key, rule, pad, target, ctx) {
|
|
@@ -589,10 +620,11 @@ function emitSetList(key, rule, pad, target, ctx) {
|
|
|
589
620
|
const n = `S${ctx.g.length}`;
|
|
590
621
|
ctx.g.push(`const ${n}=new Set(${literal(item.values)});`);
|
|
591
622
|
const out = segmentListHeader(rule, pad, ctx);
|
|
623
|
+
const keyAt = listKey(key);
|
|
592
624
|
out.push(
|
|
593
|
-
`${pad} if (a === z) ${
|
|
625
|
+
`${pad} if (a === z) ${errMsg(keyAt, " is required", ctx)};`,
|
|
594
626
|
`${pad} else if (${n}.has(x = v.slice(a, z))) l[i] = x;`,
|
|
595
|
-
`${pad} else ${
|
|
627
|
+
`${pad} else ${errMsg(keyAt, ` must be one of ${item.values.join(", ")}`, ctx)};`
|
|
596
628
|
);
|
|
597
629
|
return finishSegmentList(out, pad, target, ctx);
|
|
598
630
|
}
|
|
@@ -600,14 +632,15 @@ function emitSetList(key, rule, pad, target, ctx) {
|
|
|
600
632
|
function emitFastStrictIntList(key, rule, pad, target, ctx) {
|
|
601
633
|
const item = rule.item;
|
|
602
634
|
const out = segmentListHeader(rule, pad, ctx);
|
|
635
|
+
const keyAt = listKey(key);
|
|
603
636
|
out.push(
|
|
604
|
-
`${pad} if (a === z) ${"default" in item || item.optional ? `l[i] = ${literal(item.default)}` :
|
|
637
|
+
`${pad} if (a === z) ${"default" in item || item.optional ? `l[i] = ${literal(item.default)}` : errMsg(keyAt, " must be a strict integer", ctx)};`,
|
|
605
638
|
`${pad} else {`,
|
|
606
639
|
`${pad} let q = a;`,
|
|
607
640
|
`${pad} let c = v.charCodeAt(q);`,
|
|
608
641
|
`${pad} let g = 1;`,
|
|
609
642
|
`${pad} if (c === 43 || c === 45) { g = c === 45 ? -1 : 1; q++; }`,
|
|
610
|
-
`${pad} if (q === z) ${
|
|
643
|
+
`${pad} if (q === z) ${errMsg(keyAt, " must be a strict integer", ctx)};`,
|
|
611
644
|
`${pad} else {`,
|
|
612
645
|
`${pad} let n = 0;`,
|
|
613
646
|
`${pad} for (; q < z; q++) {`,
|
|
@@ -615,13 +648,13 @@ function emitFastStrictIntList(key, rule, pad, target, ctx) {
|
|
|
615
648
|
`${pad} if (c < 48 || c > 57) break;`,
|
|
616
649
|
`${pad} n = n * 10 + c - 48;`,
|
|
617
650
|
`${pad} }`,
|
|
618
|
-
`${pad} if (q !== z) ${
|
|
651
|
+
`${pad} if (q !== z) ${errMsg(keyAt, " must be a strict integer", ctx)};`,
|
|
619
652
|
`${pad} else {`,
|
|
620
653
|
`${pad} n *= g;`,
|
|
621
|
-
`${pad} if ((n | 0) !== n) ${
|
|
654
|
+
`${pad} if ((n | 0) !== n) ${errMsg(keyAt, " must be an integer", ctx)};`
|
|
622
655
|
);
|
|
623
|
-
if (item.min != null) out.push(`${pad} else if (n < ${num(item.min)}) ${
|
|
624
|
-
if (item.max != null) out.push(`${pad} else if (n > ${num(item.max)}) ${
|
|
656
|
+
if (item.min != null) out.push(`${pad} else if (n < ${num(item.min)}) ${errMsg(keyAt, ` must be >= ${item.min}`, ctx)};`);
|
|
657
|
+
if (item.max != null) out.push(`${pad} else if (n > ${num(item.max)}) ${errMsg(keyAt, ` must be <= ${item.max}`, ctx)};`);
|
|
625
658
|
out.push(
|
|
626
659
|
`${pad} else l[i] = n;`,
|
|
627
660
|
`${pad} }`,
|
|
@@ -637,26 +670,26 @@ function emitFastStrictIntList(key, rule, pad, target, ctx) {
|
|
|
637
670
|
function emitSegmentStringList(key, rule, pad, target, ctx) {
|
|
638
671
|
const item = rule.item;
|
|
639
672
|
const out = segmentListHeader(rule, pad, ctx);
|
|
640
|
-
const
|
|
673
|
+
const keyAt = listKey(key);
|
|
641
674
|
const len = "z - a";
|
|
642
675
|
let checked = false;
|
|
643
676
|
if (item.min != null && !skipMin(item)) {
|
|
644
|
-
out.push(`${pad} if (${len} < ${num(item.min)}) ${
|
|
677
|
+
out.push(`${pad} if (${len} < ${num(item.min)}) ${errMsg(keyAt, ` must have length >= ${item.min}`, ctx)};`);
|
|
645
678
|
checked = true;
|
|
646
679
|
}
|
|
647
680
|
if (item.max != null) {
|
|
648
|
-
out.push(`${checked ? pad + " else " : pad + " "}if (${len} > ${num(item.max)}) ${
|
|
681
|
+
out.push(`${checked ? pad + " else " : pad + " "}if (${len} > ${num(item.max)}) ${errMsg(keyAt, ` must have length <= ${item.max}`, ctx)};`);
|
|
649
682
|
checked = true;
|
|
650
683
|
}
|
|
651
684
|
if (item.startsWith != null) {
|
|
652
685
|
const prefix = literal(item.startsWith);
|
|
653
|
-
out.push(`${checked ? pad + " else " : pad + " "}if (${len} < ${item.startsWith.length} || !v.startsWith(${prefix}, a)) ${
|
|
686
|
+
out.push(`${checked ? pad + " else " : pad + " "}if (${len} < ${item.startsWith.length} || !v.startsWith(${prefix}, a)) ${errMsg(keyAt, ` must start with ${item.startsWith}`, ctx)};`);
|
|
654
687
|
checked = true;
|
|
655
688
|
}
|
|
656
689
|
if (item.includes != null) {
|
|
657
690
|
const needle = literal(item.includes);
|
|
658
691
|
const needleLen = item.includes.length;
|
|
659
|
-
out.push(`${checked ? pad + " else " : pad + " "}if (((x = v.indexOf(${needle}, a)) < 0) || x + ${needleLen} > z) ${
|
|
692
|
+
out.push(`${checked ? pad + " else " : pad + " "}if (((x = v.indexOf(${needle}, a)) < 0) || x + ${needleLen} > z) ${errMsg(keyAt, ` must include ${item.includes}`, ctx)};`);
|
|
660
693
|
checked = true;
|
|
661
694
|
}
|
|
662
695
|
out.push(`${checked ? pad + " else " : pad + " "}l[i] = v.slice(a, z);`);
|
|
@@ -707,7 +740,7 @@ function listAssign(pad, target) {
|
|
|
707
740
|
}
|
|
708
741
|
|
|
709
742
|
function emitListItem(key, rule, pad, ctx) {
|
|
710
|
-
key =
|
|
743
|
+
key = listKey(key);
|
|
711
744
|
if (rule.optional || !simpleMissing(rule)) {
|
|
712
745
|
const out = [`${pad}if (x === "") {`];
|
|
713
746
|
emitMissing(key, rule, `${pad} `, "l[i]", out, ctx);
|
|
@@ -721,7 +754,7 @@ function emitBool(key, pad, target, value, ctx) {
|
|
|
721
754
|
return [
|
|
722
755
|
`${pad}if (${value}==="true"||${value}==="1"||${value}==="yes"||${value}==="on") ${target} = true;`,
|
|
723
756
|
`${pad}else if (${value}==="false"||${value}==="0"||${value}==="no"||${value}==="off") ${target} = false;`,
|
|
724
|
-
`${pad}else ${
|
|
757
|
+
`${pad}else ${errMsg(key, " must be a boolean", ctx)};`
|
|
725
758
|
];
|
|
726
759
|
}
|
|
727
760
|
|
|
@@ -747,7 +780,20 @@ function innerTypeFor(rule) {
|
|
|
747
780
|
}
|
|
748
781
|
|
|
749
782
|
function err(message, ctx) {
|
|
750
|
-
return
|
|
783
|
+
return errExpr(literal(message), ctx);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function errMsg(key, suffix, ctx) {
|
|
787
|
+
return typeof key === "string" ? err(`${key}${suffix}`, ctx) : errExpr(`${key.expr}+${literal(suffix)}`, ctx);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function errExpr(message, ctx) {
|
|
791
|
+
envError(ctx);
|
|
792
|
+
return ctx.f ? `R([${message}])` : `(r ??= []).push(${message})`;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function listKey(key) {
|
|
796
|
+
return { expr: `${literal(key)}+"["+i+"]"` };
|
|
751
797
|
}
|
|
752
798
|
|
|
753
799
|
function literal(value) {
|
package/src/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type Spec<T> = { readonly [specBrand]: T };
|
|
|
7
7
|
export type InferSpec<T> = T extends Spec<infer V> ? V : never;
|
|
8
8
|
export type InferEnv<T extends Record<string, Spec<unknown>>> = { readonly [K in keyof T]: InferSpec<T[K]> };
|
|
9
9
|
export type EnvResult<T extends Record<string, Spec<unknown>>> = InferEnv<T>;
|
|
10
|
-
type Output<T, O> = O extends { default:
|
|
10
|
+
type Output<T, O> = O extends { default: unknown } | { devDefault: unknown } | { testDefault: unknown } ? T : O extends { optional: true } ? T | undefined : T;
|
|
11
11
|
export function defineEnv<T extends Record<string, Spec<unknown>>>(schema: T): Readonly<T>;
|
|
12
12
|
export function str<O extends StringOptions | undefined = undefined>(options?: O): Spec<Output<string, O>>;
|
|
13
13
|
export function int<O extends NumberOptions | undefined = undefined>(options?: O): Spec<Output<number, O>>;
|
package/src/index.js
CHANGED
|
@@ -26,23 +26,34 @@ export function defineEnv(schema) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function str(options) {
|
|
29
|
+
assertBaseOptions("str", options);
|
|
30
|
+
assertStringOptions("str", options);
|
|
29
31
|
return spec(STR, options);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function int(options) {
|
|
35
|
+
assertBaseOptions("int", options);
|
|
36
|
+
assertNumberOptions("int", options);
|
|
33
37
|
return spec(INT, options);
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export function num(options) {
|
|
41
|
+
assertBaseOptions("num", options);
|
|
42
|
+
assertNumberOptions("num", options);
|
|
37
43
|
return spec(NUM, options);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export function bool(options) {
|
|
47
|
+
assertBaseOptions("bool", options);
|
|
41
48
|
return spec(BOOL, options);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
export function oneOf(values, options) {
|
|
45
52
|
if (!Array.isArray(values) || !values.length) throw new TypeError("oneOf() expects a non-empty array");
|
|
53
|
+
if (values.some(v => typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean" || typeof v === "number" && !Number.isFinite(v))) {
|
|
54
|
+
throw new TypeError("oneOf() values must be strings, finite numbers, or booleans");
|
|
55
|
+
}
|
|
56
|
+
assertBaseOptions("oneOf", options);
|
|
46
57
|
const mixed = values.some(v=>typeof v!="string");
|
|
47
58
|
const strings = mixed ? values.map(String) : values.slice();
|
|
48
59
|
const rule = {...options,values,strings};
|
|
@@ -51,15 +62,24 @@ export function oneOf(values, options) {
|
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export function url(options) {
|
|
65
|
+
assertBaseOptions("url", options);
|
|
66
|
+
assertStringOptions("url", options);
|
|
67
|
+
if (options && H(options, "protocols") && (!Array.isArray(options.protocols) || options.protocols.some(p => typeof p !== "string" || p === "" || p.includes(":")))) {
|
|
68
|
+
throw new TypeError("url() protocols must be protocol names without \":\"");
|
|
69
|
+
}
|
|
54
70
|
return spec(URL_T, options && H(options, "protocols") ? {...options,ps:options.protocols.map(p=>p+":")} : options);
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
export function json(options) {
|
|
74
|
+
assertBaseOptions("json", options);
|
|
58
75
|
return spec(JSON_T, options);
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
export function list(item, options) {
|
|
62
79
|
if (!isCelerySpec(item)) throw new TypeError("list() expects a celery-env spec item");
|
|
80
|
+
assertBaseOptions("list", options);
|
|
81
|
+
if (options && H(options, "separator") && typeof options.separator !== "string") throw new TypeError("list() separator must be a string");
|
|
82
|
+
if (options && H(options, "trim") && typeof options.trim !== "boolean") throw new TypeError("list() trim must be a boolean");
|
|
63
83
|
return spec(LIST, {separator:",",...options,item});
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -92,6 +112,34 @@ function schemaEntries(schema) {
|
|
|
92
112
|
return entries;
|
|
93
113
|
}
|
|
94
114
|
|
|
115
|
+
function assertBaseOptions(name, options) {
|
|
116
|
+
if (options === undefined) return;
|
|
117
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) throw new TypeError(`${name}() options must be an object`);
|
|
118
|
+
if (H(options, "optional") && typeof options.optional !== "boolean") throw new TypeError(`${name}() optional must be a boolean`);
|
|
119
|
+
if (H(options, "requiredWhen") && options.requiredWhen != null && typeof options.requiredWhen !== "function") throw new TypeError(`${name}() requiredWhen must be a function`);
|
|
120
|
+
if (H(options, "desc") && typeof options.desc !== "string") throw new TypeError(`${name}() desc must be a string`);
|
|
121
|
+
if (H(options, "docs") && typeof options.docs !== "string") throw new TypeError(`${name}() docs must be a string`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assertStringOptions(name, options) {
|
|
125
|
+
if (!options) return;
|
|
126
|
+
assertLimit(name, options, "min");
|
|
127
|
+
assertLimit(name, options, "max");
|
|
128
|
+
if (H(options, "startsWith") && typeof options.startsWith !== "string") throw new TypeError(`${name}() startsWith must be a string`);
|
|
129
|
+
if (H(options, "includes") && typeof options.includes !== "string") throw new TypeError(`${name}() includes must be a string`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function assertNumberOptions(name, options) {
|
|
133
|
+
if (!options) return;
|
|
134
|
+
assertLimit(name, options, "min");
|
|
135
|
+
assertLimit(name, options, "max");
|
|
136
|
+
if (H(options, "strict") && typeof options.strict !== "boolean") throw new TypeError(`${name}() strict must be a boolean`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertLimit(name, options, key) {
|
|
140
|
+
if (H(options, key) && !Number.isFinite(options[key])) throw new TypeError(`${name}() ${key} must be a finite number`);
|
|
141
|
+
}
|
|
142
|
+
|
|
95
143
|
function readValue(key, rule, value, e, i, env) {
|
|
96
144
|
if (value == null || value === "") {
|
|
97
145
|
if (H(rule, "testDefault") || H(rule, "devDefault")) {
|