@mr-aftab-ahmad-khan/envy 0.1.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 +9 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/dist/astro.cjs +153 -0
- package/dist/astro.cjs.map +1 -0
- package/dist/astro.d.cts +8 -0
- package/dist/astro.d.ts +8 -0
- package/dist/astro.js +124 -0
- package/dist/astro.js.map +1 -0
- package/dist/cli.cjs +231 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +230 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +144 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +153 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +8 -0
- package/dist/next.d.ts +8 -0
- package/dist/next.js +124 -0
- package/dist/next.js.map +1 -0
- package/dist/vite.cjs +153 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +8 -0
- package/dist/vite.d.ts +8 -0
- package/dist/vite.js +124 -0
- package/dist/vite.js.map +1 -0
- package/package.json +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — 2026-05-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Zod-validated `createEnv({ server, client, clientPrefix, runtimeEnv })` with read-only proxy and client/server isolation.
|
|
8
|
+
- Framework adapters: `envy/next`, `envy/vite`, `envy/astro`.
|
|
9
|
+
- CLI: `envy validate` and `envy generate-example` for `.env.example` scaffolding.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 envy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# envy
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@mr-aftab-ahmad-khan/envy)
|
|
4
|
+
[](https://bundlephobia.com/package/envy)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
**Type-safe environment variables for any JavaScript runtime.** `envy` is a framework-agnostic, Zod-powered loader that turns `process.env` into a strongly-typed, validated object at boot. `env.PORT` is a `number`. `env.DATABASE_URL` is a non-empty URL string. Defaults flow into the inferred type. Missing or malformed variables are reported all-at-once before your app starts.
|
|
9
|
+
|
|
10
|
+
Unlike `dotenv` (which gives you `string | undefined` everywhere) or `t3-env` (which is glued to Next.js + React), `envy` is a tiny zero-runtime-dependency core with optional adapters for **Next.js**, **Vite**, and **Astro**, a CLI for CI validation and `.env.example` generation, and a strict server/client boundary that throws at runtime if you ever try to read a secret in the browser.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @mr-aftab-ahmad-khan/envy zod
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @mr-aftab-ahmad-khan/envy zod
|
|
20
|
+
# or
|
|
21
|
+
yarn add @mr-aftab-ahmad-khan/envy zod
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`zod` is a peer dependency — `envy` itself has no runtime dependencies.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
|
|
34
|
+
export const env = createEnv({
|
|
35
|
+
server: {
|
|
36
|
+
DATABASE_URL: z.string().url(),
|
|
37
|
+
PORT: z.coerce.number().default(3000),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(env.DATABASE_URL); // string
|
|
42
|
+
console.log(env.PORT); // number
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If `DATABASE_URL` is missing, the process exits with a clear error listing every problem at once.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Core Usage Examples
|
|
50
|
+
|
|
51
|
+
### 1. Basic server-only env
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
55
|
+
import { z } from "zod";
|
|
56
|
+
|
|
57
|
+
export const env = createEnv({
|
|
58
|
+
server: {
|
|
59
|
+
DATABASE_URL: z.string().url(),
|
|
60
|
+
PORT: z.coerce.number(),
|
|
61
|
+
DEBUG: z.coerce.boolean(),
|
|
62
|
+
API_BASE: z.string().url(),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
env.DATABASE_URL; // string
|
|
67
|
+
env.PORT; // number
|
|
68
|
+
env.DEBUG; // boolean
|
|
69
|
+
env.API_BASE; // string
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Defaults and coercion
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
76
|
+
import { z } from "zod";
|
|
77
|
+
|
|
78
|
+
export const env = createEnv({
|
|
79
|
+
server: {
|
|
80
|
+
PORT: z.coerce.number().default(3000),
|
|
81
|
+
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
82
|
+
FEATURE_X_ENABLED: z.coerce.boolean().default(false),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
env.PORT; // number (3000 if PORT is missing)
|
|
87
|
+
env.LOG_LEVEL; // "debug" | "info" | "warn" | "error"
|
|
88
|
+
env.FEATURE_X_ENABLED; // boolean
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Custom onValidationError hook
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createEnv, EnvValidationError } from "@mr-aftab-ahmad-khan/envy";
|
|
95
|
+
import { z } from "zod";
|
|
96
|
+
|
|
97
|
+
export const env = createEnv({
|
|
98
|
+
server: { DATABASE_URL: z.string().url() },
|
|
99
|
+
onValidationError: (err: EnvValidationError) => {
|
|
100
|
+
console.error("envy: cannot start, missing required env");
|
|
101
|
+
for (const v of err.invalid) {
|
|
102
|
+
console.error(` - ${v.name}`);
|
|
103
|
+
}
|
|
104
|
+
process.exit(1);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. skipValidation in Vitest setup
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// vitest.setup.ts
|
|
113
|
+
import { beforeAll } from "vitest";
|
|
114
|
+
|
|
115
|
+
beforeAll(() => {
|
|
116
|
+
process.env.DATABASE_URL = "postgres://localhost/test";
|
|
117
|
+
process.env.SKIP_ENV_VALIDATION = "1";
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// src/env.ts
|
|
121
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
122
|
+
import { z } from "zod";
|
|
123
|
+
|
|
124
|
+
export const env = createEnv({
|
|
125
|
+
server: { DATABASE_URL: z.string().url() },
|
|
126
|
+
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 5. Shared env in a utility module
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// src/env.ts
|
|
134
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy/next";
|
|
135
|
+
import { z } from "zod";
|
|
136
|
+
|
|
137
|
+
export const env = createEnv({
|
|
138
|
+
server: { DATABASE_URL: z.string().url() },
|
|
139
|
+
client: { NEXT_PUBLIC_APP_URL: z.string().url() },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// src/lib/api.ts (imported by both server and client code)
|
|
143
|
+
import { env } from "../env";
|
|
144
|
+
|
|
145
|
+
export function getApiBase() {
|
|
146
|
+
return env.NEXT_PUBLIC_APP_URL; // safe everywhere
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 6. Combining multiple env files via runtimeEnv
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
154
|
+
import { z } from "zod";
|
|
155
|
+
|
|
156
|
+
const stage = process.env.NODE_ENV ?? "development";
|
|
157
|
+
const overlay = stage === "production" ? process.env : { ...process.env, DEBUG: "true" };
|
|
158
|
+
|
|
159
|
+
export const env = createEnv({
|
|
160
|
+
server: {
|
|
161
|
+
DATABASE_URL: z.string().url(),
|
|
162
|
+
DEBUG: z.coerce.boolean().default(false),
|
|
163
|
+
},
|
|
164
|
+
runtimeEnv: overlay,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Framework Integration Examples
|
|
171
|
+
|
|
172
|
+
### Next.js App Router
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// src/env.ts
|
|
176
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy/next";
|
|
177
|
+
import { z } from "zod";
|
|
178
|
+
|
|
179
|
+
export const env = createEnv({
|
|
180
|
+
server: {
|
|
181
|
+
DATABASE_URL: z.string().url(),
|
|
182
|
+
NEXTAUTH_SECRET: z.string().min(32),
|
|
183
|
+
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
|
|
184
|
+
},
|
|
185
|
+
client: {
|
|
186
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
187
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// app/page.tsx (Server Component)
|
|
194
|
+
import { env } from "@/env";
|
|
195
|
+
|
|
196
|
+
export default function Page() {
|
|
197
|
+
const url = process.env.NODE_ENV === "production"
|
|
198
|
+
? env.NEXT_PUBLIC_APP_URL
|
|
199
|
+
: "http://localhost:3000";
|
|
200
|
+
return <main>{url}</main>;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// app/components/StripeButton.tsx
|
|
206
|
+
"use client";
|
|
207
|
+
import { env } from "@/env";
|
|
208
|
+
|
|
209
|
+
export function StripeButton() {
|
|
210
|
+
return <button data-key={env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}>Pay</button>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Vite + React
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// src/env.ts
|
|
218
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy/vite";
|
|
219
|
+
import { z } from "zod";
|
|
220
|
+
|
|
221
|
+
export const env = createEnv({
|
|
222
|
+
client: {
|
|
223
|
+
VITE_API_BASE: z.string().url(),
|
|
224
|
+
VITE_SENTRY_DSN: z.string().url().optional(),
|
|
225
|
+
},
|
|
226
|
+
runtimeEnv: import.meta.env,
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
// src/App.tsx
|
|
232
|
+
import { env } from "./env";
|
|
233
|
+
|
|
234
|
+
export function App() {
|
|
235
|
+
return <pre>{env.VITE_API_BASE}</pre>;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Plain Express
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// src/env.ts
|
|
243
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
244
|
+
import { z } from "zod";
|
|
245
|
+
|
|
246
|
+
export const env = createEnv({
|
|
247
|
+
server: {
|
|
248
|
+
PORT: z.coerce.number().default(3000),
|
|
249
|
+
DATABASE_URL: z.string().url(),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// src/server.ts
|
|
254
|
+
import express from "express";
|
|
255
|
+
import { env } from "./env";
|
|
256
|
+
|
|
257
|
+
const app = express();
|
|
258
|
+
app.listen(env.PORT, () => console.log(`listening on ${env.PORT}`));
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Configuration Reference
|
|
264
|
+
|
|
265
|
+
| Option | Type | Default | Description |
|
|
266
|
+
| ------------------------ | -------------------------------------- | ------------------ | --------------------------------------------------------------------------- |
|
|
267
|
+
| `server` | `Record<string, ZodTypeAny>` | `{}` | Server-only schemas. Never accessible on the client. |
|
|
268
|
+
| `client` | `Record<string, ZodTypeAny>` | `{}` | Client-exposed schemas. Must match `clientPrefix` (when set by an adapter). |
|
|
269
|
+
| `runtimeEnv` | `Record<string, string \| undefined>` | `process.env` | Source of raw values. |
|
|
270
|
+
| `clientPrefix` | `string` | `undefined` | Prefix required on every `client` key. Set by adapters. |
|
|
271
|
+
| `skipValidation` | `boolean` | `false` | Bypass Zod parsing; values returned as-is. |
|
|
272
|
+
| `onValidationError` | `(err: EnvValidationError) => void` | `undefined` | Called instead of throwing. |
|
|
273
|
+
| `isServer` | `boolean` | `typeof window === "undefined"` | Controls whether server vars can be read. |
|
|
274
|
+
| `emptyStringAsUndefined` | `boolean` | `true` | Coerce `""` to `undefined` so defaults fire. |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Error Handling
|
|
279
|
+
|
|
280
|
+
When validation fails, `envy` throws an `EnvValidationError`:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
Invalid environment variables (2 issues):
|
|
284
|
+
- DATABASE_URL: Invalid url (received: "not-a-url")
|
|
285
|
+
- PORT: Expected number, received string (received: "abc")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Catch it programmatically:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { createEnv, EnvValidationError } from "@mr-aftab-ahmad-khan/envy";
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const env = createEnv({ /* ... */ });
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (err instanceof EnvValidationError) {
|
|
297
|
+
for (const v of err.invalid) {
|
|
298
|
+
console.error(v.name, v.received, v.issues);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`EnvValidationError` shape:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
class EnvValidationError extends Error {
|
|
309
|
+
readonly invalid: ReadonlyArray<{
|
|
310
|
+
name: string;
|
|
311
|
+
received: unknown;
|
|
312
|
+
issues: ReadonlyArray<ZodIssue>;
|
|
313
|
+
}>;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## TypeScript Types
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import type {
|
|
323
|
+
CreateEnvOptions,
|
|
324
|
+
Env,
|
|
325
|
+
InferEnv,
|
|
326
|
+
InvalidVar,
|
|
327
|
+
ZodRecord,
|
|
328
|
+
} from "@mr-aftab-ahmad-khan/envy";
|
|
329
|
+
|
|
330
|
+
const env = createEnv({ server: { PORT: z.coerce.number() } });
|
|
331
|
+
|
|
332
|
+
type MyEnv = InferEnv<typeof env>;
|
|
333
|
+
// { readonly PORT: number }
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## CLI Reference
|
|
339
|
+
|
|
340
|
+
### `envy validate`
|
|
341
|
+
|
|
342
|
+
Validates the current environment against your schema. Exits with code `1` on failure — perfect for CI.
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
envy validate
|
|
346
|
+
envy validate --schema src/env.ts --env .env.production
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
| Flag | Default | Description |
|
|
350
|
+
| -------------- | ----------- | ------------------------------------------ |
|
|
351
|
+
| `--schema, -s` | `src/env.ts`| Path to schema module (must export `schema = { server, client }`) |
|
|
352
|
+
| `--env, -e` | `.env` | Path to `.env` file |
|
|
353
|
+
|
|
354
|
+
### `envy generate-example`
|
|
355
|
+
|
|
356
|
+
Generates a `.env.example` from the schema, using `.default()` values or sensible placeholders.
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
envy generate-example
|
|
360
|
+
envy generate-example --out .env.example.local
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| Flag | Default | Description |
|
|
364
|
+
| ----------- | -------------- | -------------------------- |
|
|
365
|
+
| `--out, -o` | `.env.example` | Output path |
|
|
366
|
+
|
|
367
|
+
Schemas must be exported as:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
// src/env.ts
|
|
371
|
+
import { z } from "zod";
|
|
372
|
+
|
|
373
|
+
export const schema = {
|
|
374
|
+
server: { DATABASE_URL: z.string().url() },
|
|
375
|
+
client: { NEXT_PUBLIC_URL: z.string().url() },
|
|
376
|
+
};
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Real-World Recipe — Full Next.js Production Setup
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
// src/env.ts
|
|
385
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy/next";
|
|
386
|
+
import { z } from "zod";
|
|
387
|
+
|
|
388
|
+
export const schema = {
|
|
389
|
+
server: {
|
|
390
|
+
DATABASE_URL: z.string().url().describe("postgres://..."),
|
|
391
|
+
NEXTAUTH_SECRET: z.string().min(32),
|
|
392
|
+
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
|
|
393
|
+
},
|
|
394
|
+
client: {
|
|
395
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
396
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const env = createEnv(schema);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# .env.local
|
|
405
|
+
DATABASE_URL=postgres://app:pass@localhost:5432/app
|
|
406
|
+
NEXTAUTH_SECRET=ab92e7da6c0f4d39b1c6a8a2f0b1c4e3
|
|
407
|
+
STRIPE_SECRET_KEY=sk_test_abc123
|
|
408
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
409
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_abc123
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
# .env.example (generated by `envy generate-example`)
|
|
414
|
+
# --- server-only ---
|
|
415
|
+
DATABASE_URL=postgres://...
|
|
416
|
+
NEXTAUTH_SECRET=your-value-here
|
|
417
|
+
STRIPE_SECRET_KEY=your-value-here
|
|
418
|
+
|
|
419
|
+
# --- exposed to client ---
|
|
420
|
+
NEXT_PUBLIC_APP_URL=https://example.com
|
|
421
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your-value-here
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```json
|
|
425
|
+
// package.json
|
|
426
|
+
{
|
|
427
|
+
"scripts": {
|
|
428
|
+
"build": "envy validate && next build",
|
|
429
|
+
"start": "next start"
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
// app/page.tsx (Server Component)
|
|
436
|
+
import { env } from "@/env";
|
|
437
|
+
import Stripe from "stripe";
|
|
438
|
+
|
|
439
|
+
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
|
|
440
|
+
|
|
441
|
+
export default async function Page() {
|
|
442
|
+
const balance = await stripe.balance.retrieve();
|
|
443
|
+
return <pre>{JSON.stringify(balance, null, 2)}</pre>;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
// app/checkout/CheckoutButton.tsx
|
|
449
|
+
"use client";
|
|
450
|
+
import { env } from "@/env";
|
|
451
|
+
|
|
452
|
+
export function CheckoutButton() {
|
|
453
|
+
return <button data-pk={env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}>Pay</button>;
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```yaml
|
|
458
|
+
# .github/workflows/deploy.yml
|
|
459
|
+
name: deploy
|
|
460
|
+
on: [push]
|
|
461
|
+
jobs:
|
|
462
|
+
build:
|
|
463
|
+
runs-on: ubuntu-latest
|
|
464
|
+
steps:
|
|
465
|
+
- uses: actions/checkout@v4
|
|
466
|
+
- uses: actions/setup-node@v4
|
|
467
|
+
with: { node-version: 20 }
|
|
468
|
+
- run: npm ci
|
|
469
|
+
- run: npx envy validate
|
|
470
|
+
env:
|
|
471
|
+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
472
|
+
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
|
473
|
+
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
474
|
+
NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }}
|
|
475
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
|
476
|
+
- run: npm run build
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Migration Guide from dotenv
|
|
482
|
+
|
|
483
|
+
**Before — `dotenv`:**
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import "dotenv/config";
|
|
487
|
+
|
|
488
|
+
const port = parseInt(process.env.PORT ?? "3000", 10);
|
|
489
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
490
|
+
if (!dbUrl) throw new Error("DATABASE_URL missing");
|
|
491
|
+
|
|
492
|
+
app.listen(port);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**After — `envy`:**
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import { createEnv } from "@mr-aftab-ahmad-khan/envy";
|
|
499
|
+
import { z } from "zod";
|
|
500
|
+
|
|
501
|
+
const env = createEnv({
|
|
502
|
+
server: {
|
|
503
|
+
PORT: z.coerce.number().default(3000),
|
|
504
|
+
DATABASE_URL: z.string().url(),
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
app.listen(env.PORT);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- `process.env.PORT` (`string | undefined`) → `env.PORT` (`number`).
|
|
512
|
+
- Missing `DATABASE_URL` fails before listen with every error printed at once.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Comparison Table
|
|
517
|
+
|
|
518
|
+
| Feature | dotenv | t3-env | **envy** |
|
|
519
|
+
| ----------------------- | :----: | :----: | :------: |
|
|
520
|
+
| TypeScript types | ❌ | ✅ | ✅ |
|
|
521
|
+
| Framework agnostic | ✅ | ❌ | ✅ |
|
|
522
|
+
| Zod validation | ❌ | ✅ | ✅ |
|
|
523
|
+
| CLI tooling | ❌ | ❌ | ✅ |
|
|
524
|
+
| Client/server boundary | ❌ | ✅ | ✅ |
|
|
525
|
+
| Bundle size | 6 KB | 14 KB | **3 KB** |
|
|
526
|
+
| Custom error handler | ❌ | ❌ | ✅ |
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## License
|
|
531
|
+
|
|
532
|
+
MIT
|
package/dist/astro.cjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/astro.ts
|
|
21
|
+
var astro_exports = {};
|
|
22
|
+
__export(astro_exports, {
|
|
23
|
+
EnvValidationError: () => EnvValidationError,
|
|
24
|
+
InvalidAccessError: () => InvalidAccessError,
|
|
25
|
+
createEnv: () => createEnv2
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(astro_exports);
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
var EnvValidationError = class _EnvValidationError extends Error {
|
|
31
|
+
invalid;
|
|
32
|
+
constructor(invalid) {
|
|
33
|
+
super(_EnvValidationError.formatMessage(invalid));
|
|
34
|
+
this.name = "EnvValidationError";
|
|
35
|
+
this.invalid = invalid;
|
|
36
|
+
Object.setPrototypeOf(this, _EnvValidationError.prototype);
|
|
37
|
+
}
|
|
38
|
+
static formatMessage(invalid) {
|
|
39
|
+
const lines = [
|
|
40
|
+
`Invalid environment variables (${invalid.length} issue${invalid.length === 1 ? "" : "s"}):`
|
|
41
|
+
];
|
|
42
|
+
for (const v of invalid) {
|
|
43
|
+
const received = v.received === void 0 ? "<missing>" : JSON.stringify(v.received);
|
|
44
|
+
for (const issue of v.issues) {
|
|
45
|
+
lines.push(` - ${v.name}: ${issue.message} (received: ${received})`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var InvalidAccessError = class _InvalidAccessError extends Error {
|
|
52
|
+
constructor(varName) {
|
|
53
|
+
super(
|
|
54
|
+
`Attempted to access server-only env var "${varName}" on the client. Move it to the "server" object only if access is required server-side.`
|
|
55
|
+
);
|
|
56
|
+
this.name = "InvalidAccessError";
|
|
57
|
+
Object.setPrototypeOf(this, _InvalidAccessError.prototype);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/core.ts
|
|
62
|
+
var DEFAULT_RUNTIME_ENV = typeof process !== "undefined" && process.env ? process.env : {};
|
|
63
|
+
function isServerDefault() {
|
|
64
|
+
return typeof window === "undefined";
|
|
65
|
+
}
|
|
66
|
+
function createEnv(opts) {
|
|
67
|
+
const {
|
|
68
|
+
server = {},
|
|
69
|
+
client = {},
|
|
70
|
+
runtimeEnv = DEFAULT_RUNTIME_ENV,
|
|
71
|
+
clientPrefix,
|
|
72
|
+
skipValidation = false,
|
|
73
|
+
onValidationError,
|
|
74
|
+
isServer = isServerDefault(),
|
|
75
|
+
emptyStringAsUndefined = true
|
|
76
|
+
} = opts;
|
|
77
|
+
if (clientPrefix) {
|
|
78
|
+
for (const key of Object.keys(client)) {
|
|
79
|
+
if (!key.startsWith(clientPrefix)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Client variable "${key}" must start with prefix "${clientPrefix}".`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const key of Object.keys(server)) {
|
|
86
|
+
if (key.startsWith(clientPrefix)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Server variable "${key}" must NOT start with the client prefix "${clientPrefix}".`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const parsed = {};
|
|
94
|
+
const invalid = [];
|
|
95
|
+
const validate = (name, schema) => {
|
|
96
|
+
let raw = runtimeEnv[name];
|
|
97
|
+
if (emptyStringAsUndefined && raw === "") raw = void 0;
|
|
98
|
+
const result = schema.safeParse(raw);
|
|
99
|
+
if (result.success) {
|
|
100
|
+
parsed[name] = result.data;
|
|
101
|
+
} else {
|
|
102
|
+
invalid.push({ name, received: raw, issues: result.error.issues });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
if (!skipValidation) {
|
|
106
|
+
for (const [name, schema] of Object.entries(server)) validate(name, schema);
|
|
107
|
+
for (const [name, schema] of Object.entries(client)) validate(name, schema);
|
|
108
|
+
if (invalid.length > 0) {
|
|
109
|
+
const err = new EnvValidationError(invalid);
|
|
110
|
+
if (onValidationError) {
|
|
111
|
+
onValidationError(err);
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
for (const name of Object.keys(server)) parsed[name] = runtimeEnv[name];
|
|
118
|
+
for (const name of Object.keys(client)) parsed[name] = runtimeEnv[name];
|
|
119
|
+
}
|
|
120
|
+
const serverKeys = new Set(Object.keys(server));
|
|
121
|
+
const proxy = new Proxy(parsed, {
|
|
122
|
+
get(target, prop) {
|
|
123
|
+
if (typeof prop !== "string") return Reflect.get(target, prop);
|
|
124
|
+
if (!isServer && serverKeys.has(prop)) {
|
|
125
|
+
throw new InvalidAccessError(prop);
|
|
126
|
+
}
|
|
127
|
+
return target[prop];
|
|
128
|
+
},
|
|
129
|
+
set() {
|
|
130
|
+
throw new Error("Env is read-only");
|
|
131
|
+
},
|
|
132
|
+
deleteProperty() {
|
|
133
|
+
throw new Error("Env is read-only");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return proxy;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/adapters/astro.ts
|
|
140
|
+
function createEnv2(opts) {
|
|
141
|
+
return createEnv({
|
|
142
|
+
...opts,
|
|
143
|
+
clientPrefix: "PUBLIC_",
|
|
144
|
+
isServer: typeof window === "undefined"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
148
|
+
0 && (module.exports = {
|
|
149
|
+
EnvValidationError,
|
|
150
|
+
InvalidAccessError,
|
|
151
|
+
createEnv
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=astro.cjs.map
|