bini-env 2.0.0 → 2.0.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 +240 -176
- package/dist/index.d.ts +19 -6
- package/dist/index.js +46 -92
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +8 -0
- package/dist/node.js +37 -0
- package/dist/node.js.map +1 -0
- package/dist/vite.d.ts +6 -0
- package/dist/vite.js +104 -0
- package/dist/vite.js.map +1 -0
- package/package.json +22 -11
- package/dist/index.d.mts +0 -15
- package/dist/index.mjs +0 -122
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -6,25 +6,26 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
9
|
-

|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Environment variable system + Vite plugin for Bini.js
|
|
12
12
|
|
|
13
|
-
Powered by [Hono](https://hono.dev)
|
|
13
|
+
Powered by [Hono](https://hono.dev). When you pass a Hono context, variables are read through Hono's `env(c)` adapter, so they resolve from the runtime binding of the platform you are on, whether that is Node.js, Bun, Deno, Vercel Edge, Netlify Edge, or Cloudflare Workers.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
- **Hono-native
|
|
20
|
-
- **
|
|
21
|
-
- **Zero-config dev secrets
|
|
22
|
-
- **Vite-native
|
|
23
|
-
- **`envDir`-aware
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
19
|
+
- **Hono-native.** `getEnv(c, key)` and `requireEnv(c, key)` read from the Hono request context through `hono/adapter`.
|
|
20
|
+
- **Works without a context too.** Pass a plain object, or `undefined` / `null`, and the functions read from that object or from the runtime's own environment (`process.env`, or `Deno.env`).
|
|
21
|
+
- **Zero-config dev secrets.** During `vite dev` and `vite preview`, `.env` values are mirrored into `process.env`, so Node-hosted Hono routes can read them with no manual loop.
|
|
22
|
+
- **Vite-native loading.** The plugin uses Vite's own `loadEnv`, so the standard `.env`, `.env.local`, `.env.[mode]`, and `.env.[mode].local` files apply.
|
|
23
|
+
- **`envDir`-aware.** A custom `envDir` or `root` in your `vite.config.ts` is respected, and `envDir: false` disables loading.
|
|
24
|
+
- **Public-prefix secret warnings.** A variable whose name looks like a secret but starts with `BINI_` or `VITE_` triggers a warning, because it is bundled into client code.
|
|
25
|
+
- **Dev banner.** Prints `ß Bini.js (dev|preview)` and lists detected `.env` files on server start.
|
|
26
|
+
- **Multi-variable helpers.** `checkEnv` and `requireEnvs` validate several variables at once and report every missing name together.
|
|
27
|
+
- **Edge-safe main entry.** The `bini-env` entry imports nothing from `node:` or `vite`. The Vite plugin lives in `bini-env/vite` and the file loader in `bini-env/node`, and a publish-time check (`check:edge`) verifies that `dist/index.js` has no `node:` or `vite` imports.
|
|
28
|
+
- **Typed.** Full TypeScript support, with exported `HonoContext`, `EnvSource`, `EnvInput`, and `EnvCheck` types.
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
@@ -38,27 +39,35 @@ npm install bini-env hono
|
|
|
38
39
|
yarn add bini-env hono
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
> `hono` is a required peer dependency.
|
|
42
|
+
> `hono` (`>=4.0.0`) is a required peer dependency. `vite` (`>=8.0.0`) is an optional peer dependency, needed only for the `bini-env/vite` plugin.
|
|
43
|
+
|
|
44
|
+
### Entry points
|
|
45
|
+
|
|
46
|
+
| Import | Contents | Runs on |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| `bini-env` | `getEnv`, `requireEnv`, `checkEnv`, `requireEnvs`, `biniLogger`, and the types | Any runtime (no `node:` or `vite` imports) |
|
|
49
|
+
| `bini-env/vite` | `biniEnv()` | Node, inside Vite |
|
|
50
|
+
| `bini-env/node` | `loadEnvFiles`, `envFilesFor` | Node |
|
|
42
51
|
|
|
43
52
|
---
|
|
44
53
|
|
|
45
54
|
## Quick Start
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
### 1. Register the Vite plugin
|
|
48
57
|
|
|
49
58
|
```ts
|
|
50
59
|
// vite.config.ts
|
|
51
60
|
import { defineConfig } from 'vite'
|
|
52
|
-
import { biniEnv } from 'bini-env'
|
|
61
|
+
import { biniEnv } from 'bini-env/vite'
|
|
53
62
|
|
|
54
63
|
export default defineConfig({
|
|
55
64
|
plugins: [biniEnv()]
|
|
56
65
|
})
|
|
57
66
|
```
|
|
58
67
|
|
|
59
|
-
`biniEnv()` takes no options
|
|
68
|
+
`biniEnv()` takes no options; there is nothing to configure.
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
### 2. Read env vars in your Hono handlers
|
|
62
71
|
|
|
63
72
|
```ts
|
|
64
73
|
// src/app/api/hello.ts
|
|
@@ -69,17 +78,14 @@ const app = new Hono()
|
|
|
69
78
|
|
|
70
79
|
app.post('/hello', async (c) => {
|
|
71
80
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// requireEnv throws if the var is missing — fail fast on required config
|
|
75
|
-
const apiKey = requireEnv(ctx, 'MY_API_KEY')
|
|
81
|
+
// requireEnv throws if the var is missing or empty: fail fast on required config
|
|
82
|
+
const apiKey = requireEnv(c, 'MY_API_KEY')
|
|
76
83
|
|
|
77
|
-
// getEnv returns undefined if missing
|
|
78
|
-
const appName = getEnv(
|
|
79
|
-
const timeout = parseInt(getEnv(
|
|
84
|
+
// getEnv returns undefined if the var is missing: use ?? to provide a default
|
|
85
|
+
const appName = getEnv(c, 'APP_NAME') ?? 'World'
|
|
86
|
+
const timeout = parseInt(getEnv(c, 'TIMEOUT_MS') ?? '5000')
|
|
80
87
|
|
|
81
88
|
return c.json({ message: `Hello, ${appName}!` })
|
|
82
|
-
|
|
83
89
|
} catch (error: any) {
|
|
84
90
|
if (error.message?.includes('[bini-env] Missing required')) {
|
|
85
91
|
return c.json({ error: error.message }, 500)
|
|
@@ -91,31 +97,28 @@ app.post('/hello', async (c) => {
|
|
|
91
97
|
export default app
|
|
92
98
|
```
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
No manual `loadEnv` loop in `vite.config.ts` is needed. `MY_API_KEY` just needs to exist in your `.env` file, and `requireEnv` will find it during dev and preview.
|
|
95
101
|
|
|
96
102
|
---
|
|
97
103
|
|
|
98
104
|
## Usage Pattern
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
Pass the Hono context `c` to every call.
|
|
101
107
|
|
|
102
108
|
```ts
|
|
103
109
|
app.post('/example', async (c) => {
|
|
104
110
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const dbUrl = requireEnv(ctx, 'DATABASE_URL')
|
|
109
|
-
const apiKey = requireEnv(ctx, 'STRIPE_SECRET_KEY')
|
|
111
|
+
// Required vars: the handler throws immediately if one is missing or empty
|
|
112
|
+
const dbUrl = requireEnv(c, 'DATABASE_URL')
|
|
113
|
+
const apiKey = requireEnv(c, 'STRIPE_SECRET_KEY')
|
|
110
114
|
|
|
111
|
-
// Optional vars
|
|
112
|
-
const model = getEnv(
|
|
113
|
-
const region = getEnv(
|
|
114
|
-
const maxRetries = parseInt(getEnv(
|
|
115
|
-
const debug = getEnv(
|
|
115
|
+
// Optional vars: fall back to sensible defaults
|
|
116
|
+
const model = getEnv(c, 'AI_MODEL') ?? 'gpt-4o'
|
|
117
|
+
const region = getEnv(c, 'AWS_REGION') ?? 'us-east-1'
|
|
118
|
+
const maxRetries = parseInt(getEnv(c, 'MAX_RETRIES') ?? '3')
|
|
119
|
+
const debug = getEnv(c, 'DEBUG_MODE') === 'true'
|
|
116
120
|
|
|
117
121
|
// ... rest of handler
|
|
118
|
-
|
|
119
122
|
} catch (error: any) {
|
|
120
123
|
if (error.message?.includes('[bini-env] Missing required')) {
|
|
121
124
|
return c.json({ error: error.message }, 500)
|
|
@@ -125,56 +128,58 @@ app.post('/example', async (c) => {
|
|
|
125
128
|
})
|
|
126
129
|
```
|
|
127
130
|
|
|
128
|
-
|
|
131
|
+
- `requireEnv(c, 'KEY')` for vars the handler cannot run without.
|
|
132
|
+
- `getEnv(c, 'KEY') ?? 'default'` for optional vars with sensible defaults.
|
|
133
|
+
- `checkEnv` and `requireEnvs` for several variables at once (see the [API Reference](#api-reference)).
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
2. `requireEnv(ctx, 'KEY')` — for vars the handler cannot run without
|
|
132
|
-
3. `getEnv(ctx, 'KEY') ?? 'default'` — for optional vars with sensible defaults
|
|
135
|
+
> **Empty values.** `getEnv` returns an empty string as-is (`KEY=` yields `''`), so `??` does not fall back to the default for it. `requireEnv` treats an empty value as missing.
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
### Reading without a Hono context
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
Every function accepts an `EnvInput`: a Hono context, a plain object, or `undefined` / `null`.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
getEnv(c, 'KEY') // Hono context: read through Hono's env(c) adapter
|
|
143
|
+
getEnv({ KEY: 'value' }, 'KEY') // plain object: read from it directly
|
|
144
|
+
getEnv(undefined, 'KEY') // no source: read from process.env, or Deno.env when there is no process.env
|
|
145
|
+
```
|
|
137
146
|
|
|
138
|
-
When
|
|
147
|
+
When no source is given and `Deno.env` cannot be read (for example without `--allow-env`), the lookup returns `undefined`.
|
|
139
148
|
|
|
140
|
-
|
|
149
|
+
---
|
|
141
150
|
|
|
142
|
-
It
|
|
151
|
+
## How It Works in Dev
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
ß Bini.js (dev)
|
|
146
|
-
➜ Environments: .env.local, .env
|
|
147
|
-
➜ Local: http://localhost:3000/
|
|
148
|
-
➜ Network: http://192.168.1.7:3000/
|
|
149
|
-
```
|
|
153
|
+
When you run `vite dev` or `vite preview` (Vite's `serve` command), `biniEnv()` loads your `.env*` files with Vite's own `loadEnv` and mirrors every variable it finds into `process.env`. This is what lets `getEnv` and `requireEnv` resolve server-side secrets inside Hono API routes on Node without any extra setup.
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
It reads `.env` files from the same place Vite does: the `envDir` you configured (a relative `envDir` is resolved against `root`), otherwise your `root`, otherwise the working directory. If you set `envDir: false` in your Vite config, `bini-env` skips loading and mirroring altogether.
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
The mirror runs once, in the plugin's `config` hook, and prints nothing when it succeeds. If loading fails for any reason, `bini-env` prints a warning and the dev server still boots; it never throws:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
10:45:55 (!) [bini-env] Failed to load .env files: <reason>
|
|
155
161
|
```
|
|
156
162
|
|
|
157
|
-
This happens automatically
|
|
163
|
+
This happens automatically; there is no flag to turn it off.
|
|
158
164
|
|
|
159
|
-
**Precedence:** a value already present in `process.env` (set by your OS, shell, or CI) always wins. `.env` file values only fill in variables that
|
|
165
|
+
**Precedence:** a value already present in `process.env` (set by your OS, shell, or CI) always wins. `.env` file values only fill in variables that are not already set; they never override real environment configuration.
|
|
160
166
|
|
|
161
167
|
**What gets mirrored:**
|
|
162
168
|
|
|
163
169
|
| Variable | Mirrored into `process.env`? |
|
|
164
|
-
|
|
170
|
+
| --- | --- |
|
|
165
171
|
| No prefix (`DATABASE_URL`) | ✅ Yes |
|
|
166
|
-
| `BINI_` / `VITE_` prefixed |
|
|
167
|
-
| Empty value (`KEY=`) |
|
|
168
|
-
| Already set in the OS/CI environment | ❌ No
|
|
172
|
+
| `BINI_` / `VITE_` prefixed | ✅ Yes (they are also exposed through `import.meta.env`) |
|
|
173
|
+
| Empty value (`KEY=`) | ✅ Yes, as an empty string. `requireEnv` still treats it as missing, so a blank placeholder fails loudly |
|
|
174
|
+
| Already set in the OS/CI environment | ❌ No. The existing value is kept |
|
|
169
175
|
|
|
170
176
|
**Scope:**
|
|
171
177
|
|
|
172
|
-
-
|
|
173
|
-
- **
|
|
174
|
-
-
|
|
175
|
-
- This behavior is always on — there is no option to disable it
|
|
178
|
+
- The mirror runs only when Vite's command is `serve` (`vite dev` and `vite preview`).
|
|
179
|
+
- It does **not** run for `vite build`: nothing is written to `process.env` at build time. The build only checks your env files for public-prefixed variables with secret-looking names (see [Security Best Practices](#security-best-practices)).
|
|
180
|
+
- There is no option to disable the mirror.
|
|
176
181
|
|
|
177
|
-
**Migrating from a manual loop:** if your project already has this in `vite.config.ts`, you can delete it
|
|
182
|
+
**Migrating from a manual loop:** if your project already has this in `vite.config.ts`, you can delete it, because `biniEnv()` now does it for you:
|
|
178
183
|
|
|
179
184
|
```ts
|
|
180
185
|
// vite.config.ts — no longer needed, remove this
|
|
@@ -193,47 +198,63 @@ export default defineConfig({
|
|
|
193
198
|
})
|
|
194
199
|
```
|
|
195
200
|
|
|
201
|
+
Note that the plugin also mirrors `BINI_` and `VITE_` prefixed variables, which the loop above skipped.
|
|
202
|
+
|
|
203
|
+
### Dev banner
|
|
204
|
+
|
|
205
|
+
When the dev or preview server starts, `biniEnv()` replaces the URL printout with a version that begins with the `ß Bini.js` banner and the `.env` files it found:
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
ß Bini.js (dev)
|
|
209
|
+
➜ Environments: .env.local, .env
|
|
210
|
+
➜ Local: http://localhost:3000/
|
|
211
|
+
➜ Network: http://192.168.1.7:3000/
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- The banner lists the files that exist among `.env.[mode].local`, `.env.[mode]`, `.env.local`, and `.env`, looked up in Vite's `root` (the `Environments` line is omitted when none exist). The `process.env` mirror itself uses `envDir`.
|
|
215
|
+
- Vite's `ready in …` line is hidden.
|
|
216
|
+
|
|
196
217
|
---
|
|
197
218
|
|
|
198
219
|
## Environment Prefixes
|
|
199
220
|
|
|
200
221
|
### The Truth About Prefixes
|
|
201
222
|
|
|
202
|
-
**Both `BINI_` and `VITE_` work exactly the same way.**
|
|
223
|
+
**Both `BINI_` and `VITE_` work exactly the same way.** `biniEnv()` has no options, so the prefix list is fixed.
|
|
203
224
|
|
|
204
225
|
| Prefix | Exposed to Browser? |
|
|
205
|
-
|
|
206
|
-
| `BINI_` | **Yes**
|
|
207
|
-
| `VITE_` | **Yes**
|
|
208
|
-
| No prefix | **No**
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `BINI_` | **Yes** |
|
|
228
|
+
| `VITE_` | **Yes** (Vite's own default prefix) |
|
|
229
|
+
| No prefix | **No** |
|
|
209
230
|
|
|
210
231
|
### What `biniEnv()` Actually Does
|
|
211
232
|
|
|
212
233
|
```ts
|
|
213
|
-
// biniEnv() always
|
|
234
|
+
// biniEnv() always returns this from its config hook:
|
|
214
235
|
envPrefix: ['BINI_', 'VITE_']
|
|
215
236
|
|
|
216
|
-
// This tells Vite: "Expose
|
|
217
|
-
// Everything else is mirrored into process.env during vite dev / preview
|
|
237
|
+
// This tells Vite: "Expose variables starting with BINI_ or VITE_ to import.meta.env"
|
|
218
238
|
```
|
|
219
239
|
|
|
220
240
|
**This means:**
|
|
221
|
-
|
|
222
|
-
- ✅ `
|
|
223
|
-
-
|
|
241
|
+
|
|
242
|
+
- ✅ `BINI_API_KEY` → available in the browser via `import.meta.env.BINI_API_KEY`
|
|
243
|
+
- ✅ `VITE_API_URL` → available in the browser via `import.meta.env.VITE_API_URL`
|
|
244
|
+
- ❌ `DATABASE_URL` → not exposed to the browser (no prefix); available server-side via `getEnv` / `requireEnv`
|
|
224
245
|
|
|
225
246
|
### How to Keep Secrets Secret
|
|
226
247
|
|
|
227
248
|
**Use no prefix for secrets:**
|
|
228
249
|
|
|
229
250
|
```env
|
|
230
|
-
# ✅ NOT exposed to browser
|
|
251
|
+
# ✅ NOT exposed to the browser; mirrored into process.env for server-side use
|
|
231
252
|
DATABASE_URL=postgres://...
|
|
232
253
|
STRIPE_SECRET_KEY=sk_live_...
|
|
233
254
|
AWS_SECRET_KEY=...
|
|
234
255
|
```
|
|
235
256
|
|
|
236
|
-
|
|
257
|
+
Anything without `BINI_` or `VITE_` in front of it is not exposed to the browser.
|
|
237
258
|
|
|
238
259
|
### Choosing the Right Prefix
|
|
239
260
|
|
|
@@ -250,102 +271,97 @@ VITE_ANALYTICS_ID=UA-XXXX
|
|
|
250
271
|
### Prefix Summary
|
|
251
272
|
|
|
252
273
|
| Prefix | Exposed to Browser? | Mirrored to `process.env` (dev/preview)? | Use For |
|
|
253
|
-
|
|
254
|
-
| No prefix | ❌ Never | ✅ Yes (unless
|
|
255
|
-
| `BINI_` | ✅
|
|
256
|
-
| `VITE_` | ✅
|
|
274
|
+
| --- | --- | --- | --- |
|
|
275
|
+
| No prefix | ❌ Never | ✅ Yes (unless already set) | **SECRETS**: API keys, database URLs, tokens |
|
|
276
|
+
| `BINI_` | ✅ Yes | ✅ Yes (unless already set) | Public config |
|
|
277
|
+
| `VITE_` | ✅ Yes | ✅ Yes (unless already set) | Public config (Vite's default) |
|
|
257
278
|
|
|
258
279
|
---
|
|
259
280
|
|
|
260
281
|
## Platform Support
|
|
261
282
|
|
|
262
|
-
`getEnv` and `requireEnv` delegate to Hono's `env(c)` adapter, which reads from the correct source on
|
|
283
|
+
With a Hono context, `getEnv` and `requireEnv` delegate to Hono's `env(c)` adapter, which reads from the correct source on each supported platform. Your code does not change between platforms.
|
|
263
284
|
|
|
264
285
|
| Platform | Runtime | Env source | How Hono reads it |
|
|
265
|
-
|
|
266
|
-
| Node.js | Node | System env / platform dashboard / `bini-env` dev mirror | `process.env` ✅ |
|
|
286
|
+
| --- | --- | --- | --- |
|
|
287
|
+
| Node.js | Node | System env / platform dashboard / `bini-env` dev mirror / `.env` files loaded by `bini-server` | `process.env` ✅ |
|
|
267
288
|
| Bun | Bun | System env / platform dashboard / `bini-env` dev mirror | `process.env` ✅ |
|
|
268
289
|
| Vercel Edge | V8 isolate | Project settings → Env Vars | `process.env` ✅ |
|
|
269
290
|
| Netlify Edge | Deno | Site settings → Env Vars | `Deno.env.get()` ✅ |
|
|
270
291
|
| Cloudflare Workers | V8 isolate | `wrangler.toml` / dashboard | CF bindings via `c.env` ✅ |
|
|
271
292
|
| Deno Deploy | Deno | Project settings → Env Vars | `Deno.env.get()` ✅ |
|
|
272
293
|
|
|
273
|
-
> **Cloudflare note:**
|
|
294
|
+
> **Cloudflare note:** secrets set via `wrangler secret put` are only available inside the fetch handler via `c.env`. `getEnv(c, key)` reads them as long as you pass `c`.
|
|
295
|
+
|
|
296
|
+
When you pass anything other than a Hono context, the lookup uses that object, or falls back to `process.env` / `Deno.env` as described in [Reading without a Hono context](#reading-without-a-hono-context).
|
|
274
297
|
|
|
275
298
|
---
|
|
276
299
|
|
|
277
300
|
## How It Works
|
|
278
301
|
|
|
279
|
-
### The plugin
|
|
302
|
+
### The plugin: `biniEnv()`
|
|
280
303
|
|
|
281
|
-
|
|
304
|
+
A Vite plugin (name `bini-env`, `enforce: 'pre'`) that does four things:
|
|
305
|
+
|
|
306
|
+
1. Sets Vite's `envPrefix` to `['BINI_', 'VITE_']`.
|
|
307
|
+
2. During `vite dev` and `vite preview`, mirrors `.env` values into `process.env` (without overriding existing values).
|
|
308
|
+
3. Warns about public-prefixed variables with secret-looking names, in dev, preview, and build.
|
|
309
|
+
4. Prints the `ß Bini.js` banner with detected `.env` files, and hides Vite's `ready in` line.
|
|
282
310
|
|
|
283
311
|
```ts
|
|
284
|
-
// what biniEnv() does internally
|
|
312
|
+
// what biniEnv() does internally (simplified)
|
|
285
313
|
config(userConfig, { command, mode }) {
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
314
|
+
if (userConfig.envDir !== false) {
|
|
315
|
+
// envDir: a string is resolved against root, otherwise root
|
|
316
|
+
if (command === 'serve') {
|
|
317
|
+
// loadEnv(mode, envDir, '') -> set every key not already in process.env
|
|
318
|
+
} else {
|
|
319
|
+
// loadEnv(mode, envDir, ['BINI_', 'VITE_']) -> secret-name warnings only
|
|
320
|
+
}
|
|
321
|
+
// on error: biniLogger.warn('Failed to load .env files: ...')
|
|
290
322
|
}
|
|
291
323
|
return { envPrefix: ['BINI_', 'VITE_'] }
|
|
292
324
|
}
|
|
293
325
|
```
|
|
294
326
|
|
|
295
|
-
The
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
ß Bini.js (dev)
|
|
299
|
-
➜ Environments: .env.local, .env
|
|
300
|
-
➜ Local: http://localhost:3000/
|
|
301
|
-
➜ Network: http://192.168.1.7:3000/
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Vite handles the rest natively: loading `.env`, `.env.local`, `.env.[mode]` files during dev, watching and restarting on change, injecting prefixed vars into `import.meta.env` at build time, and HMR when env files change.
|
|
327
|
+
### The env functions: `getEnv` / `requireEnv`
|
|
305
328
|
|
|
306
|
-
|
|
329
|
+
With a Hono context, both read through `env(c)` from `hono/adapter`, so every read is request-scoped and resolved by Hono's adapter for the current platform. A value is only returned when it is a string: other binding types, such as Cloudflare KV namespaces, come back as `undefined`. On Node, Hono's adapter reads from `process.env`, which is why the dev-mode mirror makes `.env` values resolvable there.
|
|
307
330
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`dotenv` is never used. In production, vars are set in your hosting platform's environment config.
|
|
331
|
+
`dotenv` is not used by these modules. In production, set variables in your hosting platform's environment configuration.
|
|
311
332
|
|
|
312
333
|
---
|
|
313
334
|
|
|
314
335
|
## API Reference
|
|
315
336
|
|
|
316
|
-
### `getEnv(
|
|
337
|
+
### `getEnv(source, key)`
|
|
317
338
|
|
|
318
|
-
Returns `string | undefined`.
|
|
339
|
+
Returns `string | undefined`. `source` is an `EnvInput`: a Hono context, a plain object, or `undefined` / `null`. Values that are not strings return `undefined`; an empty string is returned as-is.
|
|
319
340
|
|
|
320
341
|
```ts
|
|
321
342
|
app.get('/config', async (c) => {
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
const logLevel = getEnv(ctx, 'LOG_LEVEL') ?? 'info'
|
|
326
|
-
const debug = getEnv(ctx, 'DEBUG_MODE') === 'true'
|
|
343
|
+
const region = getEnv(c, 'AWS_REGION') ?? 'us-east-1'
|
|
344
|
+
const logLevel = getEnv(c, 'LOG_LEVEL') ?? 'info'
|
|
345
|
+
const debug = getEnv(c, 'DEBUG_MODE') === 'true'
|
|
327
346
|
|
|
328
347
|
return c.json({ region, logLevel, debug })
|
|
329
348
|
})
|
|
330
349
|
```
|
|
331
350
|
|
|
332
|
-
### `requireEnv(
|
|
351
|
+
### `requireEnv(source, key)`
|
|
333
352
|
|
|
334
|
-
Returns `string`. Throws
|
|
353
|
+
Returns `string`. Throws `Error('[bini-env] Missing required environment variable: "KEY"')` when the variable is missing, empty, or not a string, and logs a descriptive error to the terminal first.
|
|
335
354
|
|
|
336
355
|
```ts
|
|
337
356
|
app.post('/send-email', async (c) => {
|
|
338
357
|
try {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const smtpPass = requireEnv(ctx, 'SMTP_PASS')
|
|
343
|
-
const smtpPort = parseInt(getEnv(ctx, 'SMTP_PORT') ?? '587')
|
|
358
|
+
const smtpHost = requireEnv(c, 'SMTP_HOST')
|
|
359
|
+
const smtpPass = requireEnv(c, 'SMTP_PASS')
|
|
360
|
+
const smtpPort = parseInt(getEnv(c, 'SMTP_PORT') ?? '587')
|
|
344
361
|
|
|
345
362
|
// ... send email
|
|
346
363
|
|
|
347
364
|
return c.json({ sent: true })
|
|
348
|
-
|
|
349
365
|
} catch (error: any) {
|
|
350
366
|
if (error.message?.includes('[bini-env] Missing required')) {
|
|
351
367
|
return c.json({ error: error.message }, 500)
|
|
@@ -355,48 +371,89 @@ app.post('/send-email', async (c) => {
|
|
|
355
371
|
})
|
|
356
372
|
```
|
|
357
373
|
|
|
358
|
-
On failure, the terminal
|
|
374
|
+
On failure, the terminal shows:
|
|
359
375
|
|
|
376
|
+
```text
|
|
377
|
+
10:45:55 [bini-env] error Missing required environment variable: "SMTP_HOST"
|
|
378
|
+
-> Set it in your hosting dashboard (or in .env for dev, preview and bini-server).
|
|
360
379
|
```
|
|
361
|
-
|
|
362
|
-
|
|
380
|
+
|
|
381
|
+
### `checkEnv(source, keys)`
|
|
382
|
+
|
|
383
|
+
Checks several variables at once. Returns `{ ok: boolean, missing: string[] }` (`missing` holds names only, never values). When any are missing, it logs one error listing all the missing names. It does not throw.
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
const { ok, missing } = checkEnv(c, ['DATABASE_URL', 'REDIS_URL', 'SESSION_SECRET'])
|
|
387
|
+
if (!ok) return c.json({ error: `Missing: ${missing.join(', ')}` }, 500)
|
|
363
388
|
```
|
|
364
389
|
|
|
390
|
+
### `requireEnvs(source, keys)`
|
|
391
|
+
|
|
392
|
+
Requires several variables at once. Returns a record of key to value. If any are missing or empty, it logs them (through `checkEnv`) and throws one error: `[bini-env] Missing environment variables: A, B`.
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
const { DATABASE_URL, REDIS_URL } = requireEnvs(c, ['DATABASE_URL', 'REDIS_URL'])
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### `loadEnvFiles(root?, mode?)`
|
|
399
|
+
|
|
400
|
+
Imported from `bini-env/node`. Loads `.env` files into `process.env` without Vite; `bini-server` calls it at startup. Defaults: `root` is `process.cwd()`, and `mode` is `process.env.NODE_ENV`, or `'production'` when that is not set. Returns `{ files: string[], injected: number }`.
|
|
401
|
+
|
|
402
|
+
- Files are read in this order of priority: `.env.[mode].local`, `.env.[mode]`, `.env.local`, `.env`. Only files that exist are read.
|
|
403
|
+
- A value is only written when the variable is not already in `process.env`, so existing environment values win, and a variable set by a higher-priority file is not overridden by a lower one.
|
|
404
|
+
- A leading byte-order mark is removed, files are parsed with Node's `util.parseEnv`, and a file that cannot be read or parsed is skipped.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { loadEnvFiles } from 'bini-env/node'
|
|
408
|
+
|
|
409
|
+
const { files, injected } = loadEnvFiles(process.cwd(), 'production')
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`envFilesFor(root, mode)` (also from `bini-env/node`) returns the list of existing env file paths in that priority order.
|
|
413
|
+
|
|
365
414
|
### `biniEnv()`
|
|
366
415
|
|
|
367
|
-
Vite plugin
|
|
416
|
+
Vite plugin, imported from `bini-env/vite`. Takes no options. Sets `envPrefix` to `['BINI_', 'VITE_']`, mirrors `.env` values into `process.env` during dev and preview, warns about public-prefixed variables with secret-looking names, and prints the dev banner.
|
|
368
417
|
|
|
369
418
|
```ts
|
|
370
419
|
biniEnv()
|
|
371
420
|
```
|
|
372
421
|
|
|
373
|
-
There
|
|
422
|
+
There is nothing to configure: no prefix list to extend and no flag to disable the `process.env` mirror. It reads `.env` files from your `envDir` (or `root`, or the working directory), never overrides a value already set in `process.env`, never mirrors during `vite build`, and skips loading entirely when `envDir` is `false`.
|
|
374
423
|
|
|
375
424
|
### `biniLogger`
|
|
376
425
|
|
|
377
|
-
Vite-style
|
|
426
|
+
Terminal logger with Vite-style timestamps. Use it in your own Bini.js plugins or server-side code.
|
|
378
427
|
|
|
379
428
|
```ts
|
|
380
429
|
import { biniLogger } from 'bini-env'
|
|
381
430
|
|
|
382
431
|
biniLogger.info('Server ready')
|
|
383
432
|
biniLogger.warn('Missing optional var')
|
|
384
|
-
biniLogger.error('Something broke', error)
|
|
433
|
+
biniLogger.error('Something broke', error) // an Error's message is printed beneath
|
|
385
434
|
```
|
|
386
435
|
|
|
387
|
-
###
|
|
388
|
-
|
|
389
|
-
Exported type (`Context` from Hono). Use it to type helper functions that group env reads, so you only cast `c as any` once per entry point.
|
|
436
|
+
### Types
|
|
390
437
|
|
|
391
438
|
```ts
|
|
392
|
-
import type { HonoContext } from 'bini-env'
|
|
439
|
+
import type { HonoContext, EnvSource, EnvInput, EnvCheck } from 'bini-env'
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
| Type | Definition |
|
|
443
|
+
| --- | --- |
|
|
444
|
+
| `HonoContext` | Hono's `Context` |
|
|
445
|
+
| `EnvSource` | `Record<string, unknown>` |
|
|
446
|
+
| `EnvInput` | `HonoContext \| EnvSource \| undefined \| null` |
|
|
447
|
+
| `EnvCheck` | `{ ok: boolean; missing: string[] }` |
|
|
393
448
|
|
|
449
|
+
Use `HonoContext` to type helper functions that group env reads:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
394
452
|
function readDbConfig(c: HonoContext) {
|
|
395
|
-
const ctx = c as any
|
|
396
453
|
return {
|
|
397
|
-
url: requireEnv(
|
|
398
|
-
poolSize: parseInt(getEnv(
|
|
399
|
-
ssl: getEnv(
|
|
454
|
+
url: requireEnv(c, 'DATABASE_URL'),
|
|
455
|
+
poolSize: parseInt(getEnv(c, 'DB_POOL_SIZE') ?? '10'),
|
|
456
|
+
ssl: getEnv(c, 'DB_SSL') !== 'false',
|
|
400
457
|
}
|
|
401
458
|
}
|
|
402
459
|
```
|
|
@@ -406,36 +463,47 @@ function readDbConfig(c: HonoContext) {
|
|
|
406
463
|
## Security Best Practices
|
|
407
464
|
|
|
408
465
|
### Rule 1: Never Prefix Secrets
|
|
466
|
+
|
|
409
467
|
```env
|
|
410
|
-
# ❌ BAD
|
|
468
|
+
# ❌ BAD: this will be exposed to the browser!
|
|
411
469
|
BINI_DATABASE_URL=postgres://...
|
|
412
470
|
|
|
413
|
-
# ✅ GOOD
|
|
471
|
+
# ✅ GOOD: not exposed to the browser, mirrored into process.env for server-side use
|
|
414
472
|
DATABASE_URL=postgres://...
|
|
415
473
|
```
|
|
416
474
|
|
|
475
|
+
`biniEnv()` helps catch this. When a variable name contains `SECRET`, `PASSWORD`, `PASSWD`, `PRIVATE`, or `TOKEN` (case-insensitive) and starts with `BINI_` or `VITE_`, it prints a warning once per variable, in dev, preview, and build:
|
|
476
|
+
|
|
477
|
+
```text
|
|
478
|
+
(!) [bini-env] "BINI_API_TOKEN" starts with a public prefix (BINI_ / VITE_) so it is bundled into client code. Rename it without the prefix if it is a secret.
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The check is by name only; it cannot tell whether a value is actually sensitive.
|
|
482
|
+
|
|
417
483
|
### Rule 2: Use No Prefix for Anything Secret
|
|
484
|
+
|
|
418
485
|
```env
|
|
419
|
-
# ✅ Safe
|
|
486
|
+
# ✅ Safe: not exposed, available server-side via getEnv/requireEnv
|
|
420
487
|
DATABASE_URL=postgres://...
|
|
421
488
|
STRIPE_SECRET=sk_live_...
|
|
422
489
|
AWS_SECRET_KEY=...
|
|
423
490
|
```
|
|
424
491
|
|
|
425
492
|
### Rule 3: Use `BINI_` or `VITE_` for Public Data
|
|
493
|
+
|
|
426
494
|
```env
|
|
427
|
-
# ✅ Safe
|
|
495
|
+
# ✅ Safe: public data
|
|
428
496
|
BINI_API_URL=https://api.example.com
|
|
429
497
|
VITE_GA_ID=UA-XXXXX
|
|
430
498
|
```
|
|
431
499
|
|
|
432
500
|
### Rule 4: Don't Leave Secret Placeholders Empty
|
|
433
501
|
|
|
434
|
-
An empty value (`API_KEY=`) is
|
|
502
|
+
An empty value (`API_KEY=`) is mirrored as an empty string, and `requireEnv` treats it as missing and throws, so a blank placeholder never passes as configured. `getEnv` returns the empty string as-is, so check for it explicitly if you rely on `getEnv`. Fill in real values, or remove the line entirely if it is unused.
|
|
435
503
|
|
|
436
504
|
### Rule 5: Let OS/CI Env Vars Win in Dev
|
|
437
505
|
|
|
438
|
-
The `process.env` mirror never overrides a variable that
|
|
506
|
+
The `process.env` mirror never overrides a variable that is already set. If you need to override a `.env` value locally (for example to point at a different database), export it in your shell before starting the dev server rather than editing `.env`:
|
|
439
507
|
|
|
440
508
|
```bash
|
|
441
509
|
DATABASE_URL=postgres://staging... pnpm dev
|
|
@@ -443,15 +511,11 @@ DATABASE_URL=postgres://staging... pnpm dev
|
|
|
443
511
|
|
|
444
512
|
---
|
|
445
513
|
|
|
446
|
-
##
|
|
447
|
-
|
|
448
|
-
| Metric | Dev | Prod |
|
|
449
|
-
|--------|-----|------|
|
|
450
|
-
| File reads | 1 (`loadEnv`, cached by Vite) | 0 |
|
|
451
|
-
| Runtime cost | ~0ms (mirror runs once at server start) | 0 |
|
|
452
|
-
| Bundle impact | Minimal | Tree-shaken |
|
|
514
|
+
## Notes on Cost
|
|
453
515
|
|
|
454
|
-
|
|
516
|
+
- `getEnv` and `requireEnv` read no files. With a Hono context, each call goes through Hono's adapter.
|
|
517
|
+
- The `process.env` mirror runs once, in the plugin's `config` hook, when the dev or preview server starts.
|
|
518
|
+
- `loadEnvFiles` reads each existing env file once per call.
|
|
455
519
|
|
|
456
520
|
---
|
|
457
521
|
|
|
@@ -459,41 +523,40 @@ No dotenv. No per-request disk reads. No caching layer beyond Vite's own. `getEn
|
|
|
459
523
|
|
|
460
524
|
### "My BINI_ var is exposed to the browser!"
|
|
461
525
|
|
|
462
|
-
Yes
|
|
526
|
+
Yes, by design. `BINI_` is exposed the same way as `VITE_`. Use no prefix for secrets.
|
|
463
527
|
|
|
464
528
|
### "Env var is undefined in production"
|
|
465
529
|
|
|
466
|
-
|
|
530
|
+
The `process.env` mirror only runs for `vite dev` and `vite preview`. In production, set your variables in your hosting platform's environment configuration (Vercel, Netlify, Cloudflare, etc.). `bini-server` additionally loads `.env` files from its working directory at startup, and variables already in the environment win.
|
|
467
531
|
|
|
468
532
|
### "Works in dev, undefined in prod"
|
|
469
533
|
|
|
470
|
-
Same as above. Local dev works because `biniEnv()` loads `.env` files and mirrors
|
|
534
|
+
Same as above. Local dev works because `biniEnv()` loads `.env` files and mirrors them into `process.env`. Production requires platform-level configuration.
|
|
471
535
|
|
|
472
536
|
### "My .env value isn't taking effect in dev"
|
|
473
537
|
|
|
474
|
-
|
|
475
|
-
- Check your shell and CI environment first — the `process.env` mirror never overrides a variable that's already set there. Unset it in your shell, or override it directly in your shell instead of `.env`.
|
|
476
|
-
- Check the value isn't empty (`KEY=`) — empty values are skipped on purpose.
|
|
538
|
+
Check your shell and CI environment first: the `process.env` mirror never overrides a variable that is already set there. Unset it in your shell, or override it directly in your shell instead of in `.env`.
|
|
477
539
|
|
|
478
540
|
### "requireEnv still throws even though my key is in .env"
|
|
479
541
|
|
|
480
|
-
If the value is `KEY=` with nothing after the `=`, it
|
|
542
|
+
- If the value is `KEY=` with nothing after the `=`, it is treated as missing by `requireEnv`. Give it a real value.
|
|
543
|
+
- The mirror only runs during `vite dev` and `vite preview`. In a production build, the variable must come from the runtime environment.
|
|
481
544
|
|
|
482
|
-
### "
|
|
545
|
+
### "getEnv returned an empty string instead of my default"
|
|
483
546
|
|
|
484
|
-
|
|
547
|
+
An empty `.env` value is returned as `''`, and `??` only falls back for `undefined`. Use `getEnv(c, 'KEY') || 'default'`, or remove the empty line.
|
|
485
548
|
|
|
486
|
-
### "
|
|
549
|
+
### "bini-env isn't reading my .env file from the right folder"
|
|
487
550
|
|
|
488
|
-
|
|
551
|
+
`biniEnv()` reads from your Vite `envDir` if you have set one (relative paths resolve against `root`), otherwise your `root`, otherwise the working directory. Double-check `envDir` and `root` in `vite.config.ts` if your `.env` lives somewhere non-standard. The dev banner lists env files found in `root`, which can differ from `envDir`.
|
|
489
552
|
|
|
490
|
-
### "
|
|
553
|
+
### "I set `envDir: false` and nothing loads"
|
|
491
554
|
|
|
492
|
-
|
|
555
|
+
That is intended: `envDir: false` disables `.env` loading, so `bini-env` skips the mirror and the secret-name check.
|
|
493
556
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
557
|
+
### "Cloudflare secret not found"
|
|
558
|
+
|
|
559
|
+
Secrets set via `wrangler secret put` are only available via `c.env` inside a handler, which is what `getEnv(c, key)` reads. Make sure you pass `c` to the function.
|
|
497
560
|
|
|
498
561
|
### "Types not found"
|
|
499
562
|
|
|
@@ -507,12 +570,13 @@ Add to your `tsconfig.json` or entry file:
|
|
|
507
570
|
|
|
508
571
|
## Compatibility
|
|
509
572
|
|
|
510
|
-
| Tool | Version |
|
|
511
|
-
|
|
512
|
-
| Vite | 8.
|
|
513
|
-
| Hono | 4.
|
|
514
|
-
|
|
|
515
|
-
|
|
573
|
+
| Tool | Version |
|
|
574
|
+
| --- | --- |
|
|
575
|
+
| Vite | `>=8.0.0` (optional peer; only for `bini-env/vite`) |
|
|
576
|
+
| Hono | `>=4.0.0` (peer) |
|
|
577
|
+
| Node.js | `>=20.12.0` |
|
|
578
|
+
|
|
579
|
+
Typings are included.
|
|
516
580
|
|
|
517
581
|
---
|
|
518
582
|
|
|
@@ -524,4 +588,4 @@ PRs are welcome. The goal of this library is to stay minimal and runtime-cost-fr
|
|
|
524
588
|
|
|
525
589
|
## License
|
|
526
590
|
|
|
527
|
-
MIT © Bini.js Team
|
|
591
|
+
MIT © Bini.js Team
|