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 +303 -162
- package/dist/index.d.ts +19 -25
- package/dist/index.js +47 -72
- 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 -34
- package/dist/index.mjs +0 -101
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -6,24 +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
|
|
22
|
-
- **Vite-native
|
|
23
|
-
-
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
75
|
-
const appName = getEnv(
|
|
76
|
-
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')
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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.**
|
|
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? |
|
|
138
|
-
|
|
139
|
-
| `BINI_` | **Yes** |
|
|
140
|
-
| `VITE_` | **Yes**
|
|
141
|
-
|
|
|
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()
|
|
234
|
+
// biniEnv() always returns this from its config hook:
|
|
148
235
|
envPrefix: ['BINI_', 'VITE_']
|
|
149
236
|
|
|
150
|
-
// This tells Vite: "Expose
|
|
237
|
+
// This tells Vite: "Expose variables starting with BINI_ or VITE_ to import.meta.env"
|
|
151
238
|
```
|
|
152
239
|
|
|
153
240
|
**This means:**
|
|
154
|
-
|
|
155
|
-
- ✅ `
|
|
156
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
197
|
-
| `BINI_` | ✅ Yes (
|
|
198
|
-
| `VITE_` | ✅ Yes (
|
|
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
|
|
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:**
|
|
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
|
|
302
|
+
### The plugin: `biniEnv()`
|
|
223
303
|
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
337
|
+
### `getEnv(source, key)`
|
|
255
338
|
|
|
256
|
-
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.
|
|
257
340
|
|
|
258
341
|
```ts
|
|
259
342
|
app.get('/config', async (c) => {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
const
|
|
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(
|
|
351
|
+
### `requireEnv(source, key)`
|
|
271
352
|
|
|
272
|
-
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.
|
|
273
354
|
|
|
274
355
|
```ts
|
|
275
356
|
app.post('/send-email', async (c) => {
|
|
276
357
|
try {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
const
|
|
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
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
### `
|
|
398
|
+
### `loadEnvFiles(root?, mode?)`
|
|
304
399
|
|
|
305
|
-
Vite
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
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(
|
|
340
|
-
poolSize: parseInt(getEnv(
|
|
341
|
-
ssl: getEnv(
|
|
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
|
|
468
|
+
# ❌ BAD: this will be exposed to the browser!
|
|
353
469
|
BINI_DATABASE_URL=postgres://...
|
|
354
470
|
|
|
355
|
-
# ✅ GOOD
|
|
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 -
|
|
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
|
|
492
|
+
### Rule 3: Use `BINI_` or `VITE_` for Public Data
|
|
493
|
+
|
|
368
494
|
```env
|
|
369
|
-
# ✅ Safe
|
|
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:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
##
|
|
514
|
+
## Notes on Cost
|
|
384
515
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
### "
|
|
536
|
+
### "My .env value isn't taking effect in dev"
|
|
410
537
|
|
|
411
|
-
|
|
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
|
-
### "
|
|
540
|
+
### "requireEnv still throws even though my key is in .env"
|
|
414
541
|
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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 |
|
|
434
|
-
|
|
435
|
-
| Vite | 8.
|
|
436
|
-
| Hono | 4.
|
|
437
|
-
|
|
|
438
|
-
|
|
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
|
|