bini-env 1.1.2 → 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 CHANGED
@@ -6,24 +6,26 @@
6
6
  ![vite](https://img.shields.io/badge/vite-8.x-646CFF?style=flat-square&logo=vite)
7
7
  ![hono](https://img.shields.io/badge/hono-4.x-E36002?style=flat-square&logo=hono)
8
8
  ![typescript](https://img.shields.io/badge/TypeScript-5.x-3178C6?style=flat-square&logo=typescript)
9
- ![node](https://img.shields.io/badge/node-%3E%3D20.19-339933?style=flat-square&logo=node.js)
9
+ ![node](https://img.shields.io/badge/node-%3E%3D20.12.0-339933?style=flat-square&logo=node.js)
10
10
 
11
- **Environment variable system + Vite plugin for Bini.js**
11
+ Environment variable system + Vite plugin for Bini.js
12
12
 
13
- Powered by [Hono](https://hono.dev) — reads env vars from the Hono request context so variables are always resolved from the correct runtime binding, whether you're on Node.js, Bun, Deno, Vercel Edge, Netlify Edge, or Cloudflare Workers.
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** — `getEnv(c, key)` / `requireEnv(c, key)` read directly from the Hono request context
20
- - **Universal runtime** — CF Workers, Node, Bun, Deno, Vercel Edge, Netlify Edge — Hono's adapter handles all of it
21
- - **Zero dotenv** — no `.env` parsing at runtime; vars come from the host platform
22
- - **Vite-native dev** — `.env` loading, file watching, and server restarts are handled by Vite itself
23
- - **Dev banner** — prints `ß Bini.js (dev/preview)` and lists detected `.env` files on server start
24
- - **Prefix control** — `BINI_` and `VITE_` included out of the box, extend with your own
25
- - **Tree-shakeable** — nothing lands in the client bundle that shouldn't
26
- - **Typed** — full TypeScript support with an exported `HonoContext` type
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.
27
29
 
28
30
  ---
29
31
 
@@ -37,25 +39,35 @@ npm install bini-env hono
37
39
  yarn add bini-env hono
38
40
  ```
39
41
 
40
- > `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 |
41
51
 
42
52
  ---
43
53
 
44
54
  ## Quick Start
45
55
 
46
- **1. Register the Vite plugin**
56
+ ### 1. Register the Vite plugin
47
57
 
48
58
  ```ts
49
59
  // vite.config.ts
50
60
  import { defineConfig } from 'vite'
51
- import { biniEnv } from 'bini-env'
61
+ import { biniEnv } from 'bini-env/vite'
52
62
 
53
63
  export default defineConfig({
54
64
  plugins: [biniEnv()]
55
65
  })
56
66
  ```
57
67
 
58
- **2. Read env vars in your Hono handlers**
68
+ `biniEnv()` takes no options; there is nothing to configure.
69
+
70
+ ### 2. Read env vars in your Hono handlers
59
71
 
60
72
  ```ts
61
73
  // src/app/api/hello.ts
@@ -66,17 +78,14 @@ const app = new Hono()
66
78
 
67
79
  app.post('/hello', async (c) => {
68
80
  try {
69
- const ctx = c as any
70
-
71
- // requireEnv throws if the var is missing — fail fast on required config
72
- 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')
73
83
 
74
- // getEnv returns undefined if missing — use ?? to provide a default
75
- const appName = getEnv(ctx, 'APP_NAME') ?? 'World'
76
- const timeout = parseInt(getEnv(ctx, 'TIMEOUT_MS') ?? '5000')
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')
77
87
 
78
88
  return c.json({ message: `Hello, ${appName}!` })
79
-
80
89
  } catch (error: any) {
81
90
  if (error.message?.includes('[bini-env] Missing required')) {
82
91
  return c.json({ error: error.message }, 500)
@@ -88,29 +97,28 @@ app.post('/hello', async (c) => {
88
97
  export default app
89
98
  ```
90
99
 
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.
101
+
91
102
  ---
92
103
 
93
104
  ## Usage Pattern
94
105
 
95
- **Always pass `c` explicitly.** Cast it once at the top of the handler, then use `ctx` throughout.
106
+ Pass the Hono context `c` to every call.
96
107
 
97
108
  ```ts
98
109
  app.post('/example', async (c) => {
99
110
  try {
100
- const ctx = c as any
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')
101
114
 
102
- // Required vars — handler throws immediately if missing
103
- const dbUrl = requireEnv(ctx, 'DATABASE_URL')
104
- const apiKey = requireEnv(ctx, 'STRIPE_SECRET_KEY')
105
-
106
- // Optional vars — fall back to sensible defaults
107
- const model = getEnv(ctx, 'AI_MODEL') ?? 'gpt-4o'
108
- const region = getEnv(ctx, 'AWS_REGION') ?? 'us-east-1'
109
- const maxRetries = parseInt(getEnv(ctx, 'MAX_RETRIES') ?? '3')
110
- const debug = getEnv(ctx, 'DEBUG_MODE') === 'true'
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'
111
120
 
112
121
  // ... rest of handler
113
-
114
122
  } catch (error: any) {
115
123
  if (error.message?.includes('[bini-env] Missing required')) {
116
124
  return c.json({ error: error.message }, 500)
@@ -120,11 +128,91 @@ app.post('/example', async (c) => {
120
128
  })
121
129
  ```
122
130
 
123
- **The pattern in three steps:**
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)).
134
+
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.
136
+
137
+ ### Reading without a Hono context
138
+
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
+ ```
146
+
147
+ When no source is given and `Deno.env` cannot be read (for example without `--allow-env`), the lookup returns `undefined`.
148
+
149
+ ---
150
+
151
+ ## How It Works in Dev
152
+
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.
154
+
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.
156
+
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>
161
+ ```
162
+
163
+ This happens automatically; there is no flag to turn it off.
164
+
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.
166
+
167
+ **What gets mirrored:**
168
+
169
+ | Variable | Mirrored into `process.env`? |
170
+ | --- | --- |
171
+ | No prefix (`DATABASE_URL`) | ✅ Yes |
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 |
175
+
176
+ **Scope:**
177
+
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.
181
+
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:
183
+
184
+ ```ts
185
+ // vite.config.ts — no longer needed, remove this
186
+ const env = loadEnv(mode, process.cwd(), '')
187
+ for (const [key, value] of Object.entries(env)) {
188
+ if (!key.startsWith('BINI_') && !key.startsWith('VITE_')) {
189
+ process.env[key] = value
190
+ }
191
+ }
192
+ ```
193
+
194
+ ```ts
195
+ // vite.config.ts — this is all you need now
196
+ export default defineConfig({
197
+ plugins: [biniEnv()]
198
+ })
199
+ ```
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
+ ```
124
213
 
125
- 1. `const ctx = c as any` — once, at the top of the handler
126
- 2. `requireEnv(ctx, 'KEY')` — for vars the handler cannot run without
127
- 3. `getEnv(ctx, 'KEY') ?? 'default'` — for optional vars with sensible defaults
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.
128
216
 
129
217
  ---
130
218
 
@@ -132,158 +220,148 @@ app.post('/example', async (c) => {
132
220
 
133
221
  ### The Truth About Prefixes
134
222
 
135
- **Both `BINI_` and `VITE_` work exactly the same way.** Neither is special.
223
+ **Both `BINI_` and `VITE_` work exactly the same way.** `biniEnv()` has no options, so the prefix list is fixed.
136
224
 
137
- | Prefix | Exposed to Browser? | Default Behavior |
138
- |--------|--------------------|------------------|
139
- | `BINI_` | **Yes** | Included in `envPrefix` by default |
140
- | `VITE_` | **Yes** | Vite's default prefix (always included) |
141
- | Any prefix you add | **Yes** | If you add it to `envPrefix` |
142
- | No prefix | **No** | Not exposed to browser |
225
+ | Prefix | Exposed to Browser? |
226
+ | --- | --- |
227
+ | `BINI_` | **Yes** |
228
+ | `VITE_` | **Yes** (Vite's own default prefix) |
229
+ | No prefix | **No** |
143
230
 
144
231
  ### What `biniEnv()` Actually Does
145
232
 
146
233
  ```ts
147
- // biniEnv() sets envPrefix to:
234
+ // biniEnv() always returns this from its config hook:
148
235
  envPrefix: ['BINI_', 'VITE_']
149
236
 
150
- // This tells Vite: "Expose ALL variables starting with BINI_ or VITE_ to import.meta.env"
237
+ // This tells Vite: "Expose variables starting with BINI_ or VITE_ to import.meta.env"
151
238
  ```
152
239
 
153
240
  **This means:**
154
- - ✅ `BINI_API_KEY` → available in browser via `import.meta.env.BINI_API_KEY`
155
- - ✅ `VITE_API_URL` → available in browser via `import.meta.env.VITE_API_URL`
156
- - ❌ `DATABASE_URL` → NOT available in browser (no prefix)
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`
157
245
 
158
246
  ### How to Keep Secrets Secret
159
247
 
160
248
  **Use no prefix for secrets:**
161
249
 
162
250
  ```env
163
- # ✅ NOT exposed to browser
251
+ # ✅ NOT exposed to the browser; mirrored into process.env for server-side use
164
252
  DATABASE_URL=postgres://...
165
253
  STRIPE_SECRET_KEY=sk_live_...
166
254
  AWS_SECRET_KEY=...
167
255
  ```
168
256
 
169
- **If you want `BINI_` to be private, remove it from `envPrefix`:**
170
-
171
- ```ts
172
- // vite.config.ts
173
- biniEnv({ envPrefix: ['VITE_'] }) // Only VITE_ exposed
174
- // BINI_ is no longer exposed to the browser
175
- ```
257
+ Anything without `BINI_` or `VITE_` in front of it is not exposed to the browser.
176
258
 
177
259
  ### Choosing the Right Prefix
178
260
 
179
- Since `BINI_` and `VITE_` are both exposed by default:
180
-
181
261
  ```env
182
262
  # ✅ Use no prefix for secrets
183
263
  DATABASE_URL=postgres://...
184
264
  STRIPE_SECRET_KEY=sk_live_...
185
265
 
186
- # ✅ Use any prefix for public config
266
+ # ✅ Use BINI_ or VITE_ for public config
187
267
  BINI_API_URL=https://api.example.com
188
268
  VITE_ANALYTICS_ID=UA-XXXX
189
- PUBLIC_APP_NAME=MyApp # If you add PUBLIC_ to envPrefix
190
269
  ```
191
270
 
192
271
  ### Prefix Summary
193
272
 
194
- | Prefix | Exposed to Browser? | Use For |
195
- |--------|--------------------|---------|
196
- | No prefix | ❌ Never | **SECRETS** — API keys, database URLs, tokens |
197
- | `BINI_` | ✅ Yes (by default) | Public config (same as VITE_) |
198
- | `VITE_` | ✅ Yes (always) | Public config (Vite's default) |
199
- | Custom prefix | ✅ If in `envPrefix` | Public config you want to control |
273
+ | Prefix | Exposed to Browser? | Mirrored to `process.env` (dev/preview)? | Use For |
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) |
200
278
 
201
279
  ---
202
280
 
203
281
  ## Platform Support
204
282
 
205
- `getEnv` and `requireEnv` delegate to Hono's `env(c)` adapter, which reads from the correct source on every supported platform automatically. Your code never changes regardless of where it deploys.
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.
206
284
 
207
285
  | Platform | Runtime | Env source | How Hono reads it |
208
- |----------|---------|------------|-------------------|
209
- | Node.js | Node | System env / platform dashboard | `process.env` ✅ |
210
- | Bun | Bun | System env / platform dashboard | `process.env` ✅ |
286
+ | --- | --- | --- | --- |
287
+ | Node.js | Node | System env / platform dashboard / `bini-env` dev mirror / `.env` files loaded by `bini-server` | `process.env` ✅ |
288
+ | Bun | Bun | System env / platform dashboard / `bini-env` dev mirror | `process.env` ✅ |
211
289
  | Vercel Edge | V8 isolate | Project settings → Env Vars | `process.env` ✅ |
212
290
  | Netlify Edge | Deno | Site settings → Env Vars | `Deno.env.get()` ✅ |
213
291
  | Cloudflare Workers | V8 isolate | `wrangler.toml` / dashboard | CF bindings via `c.env` ✅ |
214
292
  | Deno Deploy | Deno | Project settings → Env Vars | `Deno.env.get()` ✅ |
215
293
 
216
- > **Cloudflare note:** Secrets set via `wrangler secret put` are only available inside the fetch handler via `c.env` — this is a Workers architecture constraint. `getEnv(ctx, key)` reads them correctly as long as you pass `c`.
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).
217
297
 
218
298
  ---
219
299
 
220
300
  ## How It Works
221
301
 
222
- ### The plugin — `biniEnv()`
302
+ ### The plugin: `biniEnv()`
223
303
 
224
- Does two things: tells Vite which env prefixes to expose to `import.meta.env`, and prints the `ß Bini.js` banner with detected `.env` files when the dev or preview server starts.
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.
225
310
 
226
311
  ```ts
227
- // what biniEnv() does internally
228
- config() {
229
- return { envPrefix: ['BINI_', 'VITE_', ...yourExtras] }
312
+ // what biniEnv() does internally (simplified)
313
+ config(userConfig, { command, mode }) {
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: ...')
322
+ }
323
+ return { envPrefix: ['BINI_', 'VITE_'] }
230
324
  }
231
325
  ```
232
326
 
233
- On server start you will see:
327
+ ### The env functions: `getEnv` / `requireEnv`
234
328
 
235
- ```
236
- ß Bini.js (dev)
237
- ➜ Environments: .env.local, .env
238
- ➜ Local: http://localhost:3000/
239
- ➜ Network: http://192.168.1.7:3000/
240
- ```
241
-
242
- Vite handles everything else 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. bini-env does not reimplement any of that.
243
-
244
- ### The env functions — `getEnv` / `requireEnv`
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.
245
330
 
246
- Both functions read exclusively from `env(c)` via `hono/adapter` — the Hono request context. There are no `process.env` fallbacks. Every read is request-scoped and resolved by Hono's adapter for the current platform.
247
-
248
- `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.
249
332
 
250
333
  ---
251
334
 
252
335
  ## API Reference
253
336
 
254
- ### `getEnv(c, key)`
337
+ ### `getEnv(source, key)`
255
338
 
256
- Returns `string | undefined`. Reads from the Hono request context.
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.
257
340
 
258
341
  ```ts
259
342
  app.get('/config', async (c) => {
260
- const ctx = c as any
261
-
262
- const region = getEnv(ctx, 'AWS_REGION') ?? 'us-east-1'
263
- const logLevel = getEnv(ctx, 'LOG_LEVEL') ?? 'info'
264
- 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'
265
346
 
266
347
  return c.json({ region, logLevel, debug })
267
348
  })
268
349
  ```
269
350
 
270
- ### `requireEnv(c, key)`
351
+ ### `requireEnv(source, key)`
271
352
 
272
- Returns `string`. Throws immediately if the variable is missing or empty. Logs a descriptive error to the terminal on failure.
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.
273
354
 
274
355
  ```ts
275
356
  app.post('/send-email', async (c) => {
276
357
  try {
277
- const ctx = c as any
278
-
279
- const smtpHost = requireEnv(ctx, 'SMTP_HOST')
280
- const smtpPass = requireEnv(ctx, 'SMTP_PASS')
281
- 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')
282
361
 
283
362
  // ... send email
284
363
 
285
364
  return c.json({ sent: true })
286
-
287
365
  } catch (error: any) {
288
366
  if (error.message?.includes('[bini-env] Missing required')) {
289
367
  return c.json({ error: error.message }, 500)
@@ -293,52 +371,89 @@ app.post('/send-email', async (c) => {
293
371
  })
294
372
  ```
295
373
 
296
- On failure, the terminal will show:
374
+ On failure, the terminal shows:
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).
379
+ ```
380
+
381
+ ### `checkEnv(source, keys)`
297
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)
298
388
  ```
299
- [bini-env] error Missing required environment variable: "SMTP_HOST"
300
- -> Set it in your platform's env config or hosting dashboard.
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'])
301
396
  ```
302
397
 
303
- ### `biniEnv(options?)`
398
+ ### `loadEnvFiles(root?, mode?)`
304
399
 
305
- Vite plugin. Registers `BINI_` and `VITE_` as env prefixes and optionally adds more.
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
+
414
+ ### `biniEnv()`
415
+
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.
306
417
 
307
418
  ```ts
308
419
  biniEnv()
309
- // or with extra prefixes
310
- biniEnv({ envPrefix: ['MY_PUBLIC_'] })
311
420
  ```
312
421
 
313
- | Option | Type | Default | Description |
314
- |--------|------|---------|-------------|
315
- | `envPrefix` | `string \| string[]` | `[]` | Extra prefixes to expose to `import.meta.env`, in addition to `BINI_` and `VITE_` |
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`.
316
423
 
317
424
  ### `biniLogger`
318
425
 
319
- Vite-style terminal logger. Use it in your own Bini.js plugins or server-side code.
426
+ Terminal logger with Vite-style timestamps. Use it in your own Bini.js plugins or server-side code.
320
427
 
321
428
  ```ts
322
429
  import { biniLogger } from 'bini-env'
323
430
 
324
431
  biniLogger.info('Server ready')
325
432
  biniLogger.warn('Missing optional var')
326
- biniLogger.error('Something broke', error)
433
+ biniLogger.error('Something broke', error) // an Error's message is printed beneath
327
434
  ```
328
435
 
329
- ### `HonoContext`
330
-
331
- 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
332
437
 
333
438
  ```ts
334
- 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[] }` |
335
448
 
449
+ Use `HonoContext` to type helper functions that group env reads:
450
+
451
+ ```ts
336
452
  function readDbConfig(c: HonoContext) {
337
- const ctx = c as any
338
453
  return {
339
- url: requireEnv(ctx, 'DATABASE_URL'),
340
- poolSize: parseInt(getEnv(ctx, 'DB_POOL_SIZE') ?? '10'),
341
- ssl: getEnv(ctx, 'DB_SSL') !== 'false',
454
+ url: requireEnv(c, 'DATABASE_URL'),
455
+ poolSize: parseInt(getEnv(c, 'DB_POOL_SIZE') ?? '10'),
456
+ ssl: getEnv(c, 'DB_SSL') !== 'false',
342
457
  }
343
458
  }
344
459
  ```
@@ -348,47 +463,59 @@ function readDbConfig(c: HonoContext) {
348
463
  ## Security Best Practices
349
464
 
350
465
  ### Rule 1: Never Prefix Secrets
466
+
351
467
  ```env
352
- # ❌ BAD - This will be exposed to the browser!
468
+ # ❌ BAD: this will be exposed to the browser!
353
469
  BINI_DATABASE_URL=postgres://...
354
470
 
355
- # ✅ GOOD - Not exposed to browser
471
+ # ✅ GOOD: not exposed to the browser, mirrored into process.env for server-side use
356
472
  DATABASE_URL=postgres://...
357
473
  ```
358
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
+
359
483
  ### Rule 2: Use No Prefix for Anything Secret
484
+
360
485
  ```env
361
- # ✅ Safe - Not exposed
486
+ # ✅ Safe: not exposed, available server-side via getEnv/requireEnv
362
487
  DATABASE_URL=postgres://...
363
488
  STRIPE_SECRET=sk_live_...
364
489
  AWS_SECRET_KEY=...
365
490
  ```
366
491
 
367
- ### Rule 3: Use Any Prefix for Public Data
492
+ ### Rule 3: Use `BINI_` or `VITE_` for Public Data
493
+
368
494
  ```env
369
- # ✅ Safe - Public data
495
+ # ✅ Safe: public data
370
496
  BINI_API_URL=https://api.example.com
371
497
  VITE_GA_ID=UA-XXXXX
372
- PUBLIC_APP_NAME=MyApp
373
498
  ```
374
499
 
375
- ### Rule 4: Remove `BINI_` from envPrefix if You Want It Private
376
- ```ts
377
- // To make BINI_ private, remove it from envPrefix
378
- biniEnv({ envPrefix: ['VITE_'] })
500
+ ### Rule 4: Don't Leave Secret Placeholders Empty
501
+
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.
503
+
504
+ ### Rule 5: Let OS/CI Env Vars Win in Dev
505
+
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`:
507
+
508
+ ```bash
509
+ DATABASE_URL=postgres://staging... pnpm dev
379
510
  ```
380
511
 
381
512
  ---
382
513
 
383
- ## Performance
514
+ ## Notes on Cost
384
515
 
385
- | Metric | Dev | Prod |
386
- |--------|-----|------|
387
- | File reads | 0 | 0 |
388
- | Runtime cost | ~0ms | 0 |
389
- | Bundle impact | Minimal | Tree-shaken |
390
-
391
- No dotenv. No disk reads. No caching layer. `getEnv` is a direct call to Hono's adapter on every invocation — request-scoped and correct.
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.
392
519
 
393
520
  ---
394
521
 
@@ -396,27 +523,40 @@ No dotenv. No disk reads. No caching layer. `getEnv` is a direct call to Hono's
396
523
 
397
524
  ### "My BINI_ var is exposed to the browser!"
398
525
 
399
- Yes — by design. `BINI_` is included in `envPrefix` by default. Use no prefix for secrets.
526
+ Yes, by design. `BINI_` is exposed the same way as `VITE_`. Use no prefix for secrets.
400
527
 
401
528
  ### "Env var is undefined in production"
402
529
 
403
- Vars set in `.env` files are only loaded by Vite during local development. In production, set your variables in your hosting platform's environment dashboard (Vercel, Netlify, Cloudflare, etc.).
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.
404
531
 
405
532
  ### "Works in dev, undefined in prod"
406
533
 
407
- Same as above. Local dev works because Vite loads `.env` files automatically. Production requires platform-level configuration.
534
+ Same as above. Local dev works because `biniEnv()` loads `.env` files and mirrors them into `process.env`. Production requires platform-level configuration.
408
535
 
409
- ### "Cloudflare secret not found"
536
+ ### "My .env value isn't taking effect in dev"
410
537
 
411
- Secrets set via `wrangler secret put` are only available via `c.env` inside a handler — which is exactly what `getEnv(ctx, key)` reads. Ensure you are passing `c` to the function.
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`.
412
539
 
413
- ### "TypeScript error: Context not assignable to HonoContext"
540
+ ### "requireEnv still throws even though my key is in .env"
414
541
 
415
- Hono 4.12+ added a symbol to `HonoRequest` that can break structural assignability in strict TypeScript projects. Cast once per handler:
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.
416
544
 
417
- ```ts
418
- const ctx = c as any
419
- ```
545
+ ### "getEnv returned an empty string instead of my default"
546
+
547
+ An empty `.env` value is returned as `''`, and `??` only falls back for `undefined`. Use `getEnv(c, 'KEY') || 'default'`, or remove the empty line.
548
+
549
+ ### "bini-env isn't reading my .env file from the right folder"
550
+
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`.
552
+
553
+ ### "I set `envDir: false` and nothing loads"
554
+
555
+ That is intended: `envDir: false` disables `.env` loading, so `bini-env` skips the mirror and the secret-name check.
556
+
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.
420
560
 
421
561
  ### "Types not found"
422
562
 
@@ -430,12 +570,13 @@ Add to your `tsconfig.json` or entry file:
430
570
 
431
571
  ## Compatibility
432
572
 
433
- | Tool | Version | Notes |
434
- |------|---------|-------|
435
- | Vite | 8.x | `envPrefix` hook is fully compatible; empty prefix array is never passed |
436
- | Hono | 4.x | `hono/adapter` `env()` is stable across all 4.x releases |
437
- | TypeScript | 5.x | Full type support via exported `HonoContext` |
438
- | Node.js | ≥ 20.19 | Minimum required version |
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.
439
580
 
440
581
  ---
441
582