@super-repo/envx 0.2.3-b.2 → 0.2.3-b.3
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 +479 -104
- package/dist/chunks/{commands-B8vc6UKO.js → commands-DNf_gJRx.js} +192 -8
- package/dist/chunks/commands-DNf_gJRx.js.map +1 -0
- package/dist/chunks/{src-CDuEfaCY.js → src-CwrtyfZE.js} +0 -0
- package/dist/chunks/src-CwrtyfZE.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/info.d.ts +10 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/rotate.d.ts +13 -0
- package/dist/commands/rotate.d.ts.map +1 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -26
- package/dist/index.js.map +1 -1
- package/docs/auto-detection.md +217 -0
- package/docs/configuration.md +224 -0
- package/docs/recipes.md +234 -0
- package/package.json +6 -4
- package/dist/chunks/commands-B8vc6UKO.js.map +0 -1
- package/dist/chunks/src-CDuEfaCY.js.map +0 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Auto-detection
|
|
2
|
+
|
|
3
|
+
When `--env` is left at its default (`[".env"]`) and no `envFiles` is set in config, envx auto-detects the deployment environment from well-known platform signals and rewrites the env-file suffix accordingly. This page walks through every input shape with the resulting file load.
|
|
4
|
+
|
|
5
|
+
The implementation is `detectEnvironment()` — bundled into this package's dist via Vite.
|
|
6
|
+
|
|
7
|
+
## Precedence
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. VERCEL → detection from VERCEL_ENV
|
|
11
|
+
2. NETLIFY → detection from CONTEXT
|
|
12
|
+
3. NODE_ENV → built-in map, then lowercased passthrough
|
|
13
|
+
4. (none) → .env (no rewrite)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The first signal that fires wins. So a Vercel build with `NODE_ENV=development` set to a non-default still reads `VERCEL_ENV` first.
|
|
17
|
+
|
|
18
|
+
## Worked examples
|
|
19
|
+
|
|
20
|
+
### No platform signals (typical local dev)
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
unset VERCEL VERCEL_ENV NETLIFY CONTEXT NODE_ENV
|
|
24
|
+
envx -- node app.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| signal | value |
|
|
28
|
+
|------------|-----------|
|
|
29
|
+
| (none) | — |
|
|
30
|
+
|
|
31
|
+
→ Detected: **`root`** → loads **`.env`**.
|
|
32
|
+
|
|
33
|
+
### Local dev with NODE_ENV
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
NODE_ENV=development envx -- node app.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| signal | value |
|
|
40
|
+
|-------------|---------------|
|
|
41
|
+
| `NODE_ENV` | `development` |
|
|
42
|
+
|
|
43
|
+
→ Built-in map matches `development → "dev"` → loads **`.env.dev`**.
|
|
44
|
+
|
|
45
|
+
### Staging environment via NODE_ENV (no config required)
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
NODE_ENV=staging envx -- pnpm test
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| signal | value |
|
|
52
|
+
|-------------|-----------|
|
|
53
|
+
| `NODE_ENV` | `staging` |
|
|
54
|
+
|
|
55
|
+
`staging` isn't in the built-in map, so envx **passes it through lowercased**. → Loads **`.env.staging`**.
|
|
56
|
+
|
|
57
|
+
The same rule applies to any value: `NODE_ENV=qa` → `.env.qa`, `NODE_ENV=preview` → `.env.preview`, `NODE_ENV=PROD` → `.env.prod` (lowercased — built-in map normalizes the canonical names anyway).
|
|
58
|
+
|
|
59
|
+
### Vercel preview build
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# Set by Vercel automatically:
|
|
63
|
+
VERCEL=1
|
|
64
|
+
VERCEL_ENV=preview
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| signal | value |
|
|
68
|
+
|--------------|-----------|
|
|
69
|
+
| `VERCEL` | `1` |
|
|
70
|
+
| `VERCEL_ENV` | `preview` |
|
|
71
|
+
|
|
72
|
+
→ Detected: **`dev`** → loads **`.env.dev`**.
|
|
73
|
+
|
|
74
|
+
`VERCEL_ENV=development` and `VERCEL_ENV=preview` both map to `dev`. Only `VERCEL_ENV=production` maps to `prod`.
|
|
75
|
+
|
|
76
|
+
### Vercel production build
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
VERCEL=1
|
|
80
|
+
VERCEL_ENV=production
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
→ Detected: **`prod`** → loads **`.env.prod`**.
|
|
84
|
+
|
|
85
|
+
### Netlify production build
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
NETLIFY=true
|
|
89
|
+
CONTEXT=production
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
→ Detected: **`prod`** → loads **`.env.prod`**.
|
|
93
|
+
|
|
94
|
+
### Netlify preview / branch deploy
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
NETLIFY=true
|
|
98
|
+
CONTEXT=deploy-preview
|
|
99
|
+
# or: CONTEXT=branch-deploy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
→ Detected: **`dev`** → loads **`.env.dev`**.
|
|
103
|
+
|
|
104
|
+
Any other Netlify `CONTEXT` value also resolves to `dev` (Netlify uses `production` only for the canonical site).
|
|
105
|
+
|
|
106
|
+
### NODE_ENV with custom mapping
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// envx.config.ts
|
|
110
|
+
export default {
|
|
111
|
+
nodeEnvMap: {
|
|
112
|
+
production: "prod",
|
|
113
|
+
development: "dev",
|
|
114
|
+
local: "local",
|
|
115
|
+
// your additions:
|
|
116
|
+
qa: "qa-prod", // NODE_ENV=qa loads .env.qa-prod, not .env.qa
|
|
117
|
+
preview: "", // NODE_ENV=preview loads .env (no suffix)
|
|
118
|
+
"2025-prod": "prod", // alias an arbitrary value
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
NODE_ENV=qa envx -- node app.js
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
→ Map override hits `qa → "qa-prod"` → loads **`.env.qa-prod`**.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
NODE_ENV=preview envx -- node app.js
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
→ Map override hits `preview → ""` (empty string = no suffix) → loads **`.env`**.
|
|
134
|
+
|
|
135
|
+
### Disabling auto-detection entirely
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// envx.config.ts
|
|
139
|
+
export default { autoDetect: false }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
NODE_ENV=production envx -- node app.js
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
→ Auto-detection is off → loads **`.env`** regardless of platform signals. Pass `--env` explicitly when you want a different file.
|
|
147
|
+
|
|
148
|
+
## Verifying what envx will do
|
|
149
|
+
|
|
150
|
+
`envx info` prints the resolved settings, the platform signals it sees, the detected environment, and the active `NODE_ENV → suffix` map. Use it as a sanity check before deploying.
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
envx info
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Sample output:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
envx info
|
|
160
|
+
────────────────────────────────────────────────────────────
|
|
161
|
+
Workspace
|
|
162
|
+
cwd /repo/apps/web
|
|
163
|
+
workspace root /repo
|
|
164
|
+
|
|
165
|
+
Config
|
|
166
|
+
source /repo/envx.config.ts
|
|
167
|
+
origin auto
|
|
168
|
+
|
|
169
|
+
Resolved settings
|
|
170
|
+
envFiles [".env"]
|
|
171
|
+
envPath (none)
|
|
172
|
+
cascade (off)
|
|
173
|
+
envKeysFile /repo/.env.keys (default: cwd-first, ws-root fallback)
|
|
174
|
+
override false
|
|
175
|
+
quiet true
|
|
176
|
+
autoDetect true
|
|
177
|
+
|
|
178
|
+
Platform signals (process.env)
|
|
179
|
+
VERCEL (unset)
|
|
180
|
+
VERCEL_ENV (unset)
|
|
181
|
+
NETLIFY (unset)
|
|
182
|
+
CONTEXT (unset)
|
|
183
|
+
NODE_ENV staging
|
|
184
|
+
|
|
185
|
+
Auto-detection
|
|
186
|
+
source NODE_ENV
|
|
187
|
+
detected staging
|
|
188
|
+
→ env file .env.staging
|
|
189
|
+
|
|
190
|
+
NODE_ENV → suffix mapping
|
|
191
|
+
development → .env.dev (default)
|
|
192
|
+
local → .env.local (default)
|
|
193
|
+
production → .env.prod (default)
|
|
194
|
+
qa → .env.qa-prod (config)
|
|
195
|
+
preview → (no suffix → .env) (config)
|
|
196
|
+
|
|
197
|
+
Resolved env file paths (would be loaded in this order)
|
|
198
|
+
.env.staging → /repo/.env.staging
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Summary table
|
|
202
|
+
|
|
203
|
+
| input | detected | file |
|
|
204
|
+
|------------------------------------------------|------------|------------|
|
|
205
|
+
| nothing | `root` | `.env` |
|
|
206
|
+
| `NODE_ENV=production` | `prod` | `.env.prod`|
|
|
207
|
+
| `NODE_ENV=development` | `dev` | `.env.dev` |
|
|
208
|
+
| `NODE_ENV=local` | `local` | `.env.local`|
|
|
209
|
+
| `NODE_ENV=staging` | `staging` | `.env.staging`|
|
|
210
|
+
| `NODE_ENV=QA` (uppercased) | `qa` | `.env.qa` |
|
|
211
|
+
| `VERCEL=1, VERCEL_ENV=production` | `prod` | `.env.prod`|
|
|
212
|
+
| `VERCEL=1, VERCEL_ENV=preview` | `dev` | `.env.dev` |
|
|
213
|
+
| `VERCEL=1` (no `VERCEL_ENV`) | `dev` | `.env.dev` |
|
|
214
|
+
| `NETLIFY=true, CONTEXT=production` | `prod` | `.env.prod`|
|
|
215
|
+
| `NETLIFY=true, CONTEXT=deploy-preview` | `dev` | `.env.dev` |
|
|
216
|
+
| `NETLIFY=true, CONTEXT=branch-deploy` | `dev` | `.env.dev` |
|
|
217
|
+
| `autoDetect: false` (any signals) | `root` | `.env` |
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
envx settings come from four layers, resolved per-field with CLI flags winning. This page documents the schema, the discovery walk, and the merge rules.
|
|
4
|
+
|
|
5
|
+
The full schema is the `DotenvxConfig` interface — bundled into this package's dist and importable from the CLI's bundled types.
|
|
6
|
+
|
|
7
|
+
## The schema
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// envx.config.ts
|
|
11
|
+
export default {
|
|
12
|
+
// ── env-file selection ─────────────────────────────────
|
|
13
|
+
envFiles: [".env", ".env.local"], // bypass auto-detect; explicit list
|
|
14
|
+
envPath: "vault", // workspace-relative subdir (cwd-first, ws-root fallback)
|
|
15
|
+
cascade: true, // boolean: cascade using the auto-detected env name
|
|
16
|
+
// (CLI `--cascade <name>` overrides with an explicit string)
|
|
17
|
+
variables: ["DEBUG=1"], // KEY=VALUE overrides applied after load
|
|
18
|
+
|
|
19
|
+
// ── encryption keys ────────────────────────────────────
|
|
20
|
+
envKeysFile: ".env.keys", // overrides the cwd-first / workspace fallback default
|
|
21
|
+
|
|
22
|
+
// ── load behavior ──────────────────────────────────────
|
|
23
|
+
override: false, // false: existing process.env wins
|
|
24
|
+
// true: env files win (incompatible with cascade)
|
|
25
|
+
quiet: true, // suppress dotenv's load-line output
|
|
26
|
+
|
|
27
|
+
// ── auto-detection ─────────────────────────────────────
|
|
28
|
+
autoDetect: true, // false disables Vercel/Netlify/NODE_ENV detection
|
|
29
|
+
nodeEnvMap: { // override the NODE_ENV → suffix mapping
|
|
30
|
+
production: "prod", // (built-ins shown — yours layer on top)
|
|
31
|
+
development: "dev",
|
|
32
|
+
local: "local",
|
|
33
|
+
qa: "qa-prod", // NODE_ENV=qa → .env.qa-prod
|
|
34
|
+
preview: "", // NODE_ENV=preview → .env (no suffix)
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// ── post-load behavior ─────────────────────────────────
|
|
38
|
+
required: ["DATABASE_URL", "API_KEY"], // fail-fast: exit 1 if any are unset after load
|
|
39
|
+
expand: true, // auto-resolve ${VAR} references against process.env
|
|
40
|
+
defaults: { // applied AFTER files + variables, only for unset keys
|
|
41
|
+
LOG_LEVEL: "info",
|
|
42
|
+
PORT: "3000",
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// ── advanced ───────────────────────────────────────────
|
|
46
|
+
workspaceRoot: "/abs/path/to/repo", // escape hatch — skip findWorkspaceRoot() walk-up
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
All fields are optional. Anything you omit falls through to the built-in default for that field — see [auto-detection](./auto-detection.md) for the per-field defaults.
|
|
51
|
+
|
|
52
|
+
### Field reference
|
|
53
|
+
|
|
54
|
+
| field | type | default | meaning |
|
|
55
|
+
|----------------|-------------------------------|--------------------------------|---------|
|
|
56
|
+
| `envFiles` | `string[]` | `[".env"]` (with auto-detect) | Files to load. Bare names get the `.env.` prefix. Bypasses auto-detection. |
|
|
57
|
+
| `envPath` | `string` | _(none)_ | Subdirectory to look in. Cwd-first; falls back to `<workspaceRoot>/<envPath>`. |
|
|
58
|
+
| `cascade` | `boolean` | `false` | When `true`, expand each base file into `.env.<env>.local`, `.env.local`, `.env.<env>`, `.env` — where `<env>` is the auto-detected name. CLI `--cascade <name>` overrides with an explicit string. |
|
|
59
|
+
| `variables` | `string[]` | `[]` | `KEY=VALUE` overrides applied after env files load. |
|
|
60
|
+
| `envKeysFile` | `string` | `.env.keys` (cwd-first walk-up)| Path to the keys file. Cwd-first with workspace-root fallback. |
|
|
61
|
+
| `override` | `boolean` | `false` | When `true`, env file values overwrite existing `process.env`. Conflicts with `cascade`. |
|
|
62
|
+
| `quiet` | `boolean` | `true` | Suppress dotenv's `Loading environment from:` lines. |
|
|
63
|
+
| `autoDetect` | `boolean` | `true` | Toggle Vercel/Netlify/NODE_ENV detection. |
|
|
64
|
+
| `nodeEnvMap` | `Record<string, string>` | see below | Override the `NODE_ENV → suffix` map. |
|
|
65
|
+
| `required` | `string[]` | `[]` | Keys that MUST be set in `process.env` after envx finishes. Missing values cause `process.exit(1)`. |
|
|
66
|
+
| `expand` | `boolean` | `false` | Auto-resolve `${VAR}` / `$VAR` references against `process.env` after files + variables load. |
|
|
67
|
+
| `defaults` | `Record<string, string>` | `{}` | Fallback values applied only to keys still unset after files + variables. Different from `variables`, which always overrides. |
|
|
68
|
+
| `workspaceRoot`| `string` | _(auto-detect)_ | Explicit workspace root, skipping `findWorkspaceRoot()` walk-up. Useful for non-standard layouts. |
|
|
69
|
+
|
|
70
|
+
### Built-in `nodeEnvMap`
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
{
|
|
74
|
+
production: "prod",
|
|
75
|
+
development: "dev",
|
|
76
|
+
local: "local",
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Anything **not** in the map (after merging your overrides) passes through as the lowercased `NODE_ENV` value — `staging` → `.env.staging`, `qa` → `.env.qa`. Use empty string `""` to force "no suffix" for a specific value (`preview: ""` → `NODE_ENV=preview` loads `.env`).
|
|
81
|
+
|
|
82
|
+
## Discovery walk
|
|
83
|
+
|
|
84
|
+
envx finds **one** config file per invocation (no merging across files) in this exact order:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
1. --config <path> (CLI only — wins if present)
|
|
88
|
+
2. package.json#envx.config: "<path>" (walks UP from cwd to /)
|
|
89
|
+
3. envx.config.{ts,mts,js,mjs,cjs,json} (cwd ONLY — does not walk up)
|
|
90
|
+
4. (none) (built-in defaults)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The first match wins; we don't merge multiple files together. This means:
|
|
94
|
+
|
|
95
|
+
- A `package.json#envx.config` reference anywhere from cwd up to `/` will be picked up. Use this to point sub-packages at a workspace-root config: `{ "envx": { "config": "../../envx.config.ts" } }`.
|
|
96
|
+
- A bare `envx.config.ts` at the workspace root is **only** found if `cwd` happens to be the workspace root itself. Sub-packages don't auto-discover it — they need a `package.json#envx.config` reference.
|
|
97
|
+
|
|
98
|
+
`package.json#envx.config` only accepts a string path (not an inline object). This keeps the manifest free of secret-related fields and forces config to be a single file you can lint and review.
|
|
99
|
+
|
|
100
|
+
### Programmatic API uses the same discovery
|
|
101
|
+
|
|
102
|
+
`envx()` and `envx({...})` from `@super-repo/envx` run the same `loadDotenvxConfig()` discovery as the CLI. The only difference is step 1 (`--config`) is unavailable; programmatic callers pass options directly to `envx({...})`. Programmatic args play the same role as CLI flags — see [Per-field merge](#per-field-merge) below.
|
|
103
|
+
|
|
104
|
+
### Example: per-package overrides
|
|
105
|
+
|
|
106
|
+
Because `envx.config.*` is **cwd-only**, sub-packages need a `package.json#envx.config` reference to find a workspace-root config. The minimal monorepo layout looks like:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/repo
|
|
110
|
+
├── envx.config.ts ← workspace defaults
|
|
111
|
+
├── packages/
|
|
112
|
+
│ ├── api/
|
|
113
|
+
│ │ └── package.json ← { "envx": { "config": "../../envx.config.ts" } }
|
|
114
|
+
│ └── docker-builder/
|
|
115
|
+
│ ├── envx.config.ts ← package-specific overrides
|
|
116
|
+
│ └── package.json ← (no envx.config field needed — local file is in cwd)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Running `envx -- node app.js` from `/repo/packages/api`:
|
|
120
|
+
- Discovery step 2 walks up `packages/api/package.json` → `packages/package.json` → `/repo/package.json`. The first one is read first; its `envx.config` field points at `../../envx.config.ts`, which resolves to `/repo/envx.config.ts`. **Match — that file is loaded.**
|
|
121
|
+
|
|
122
|
+
Running the same command from `/repo/packages/docker-builder`:
|
|
123
|
+
- Discovery step 2 walks up the package.jsons. None has an `envx.config` field. No match.
|
|
124
|
+
- Discovery step 3 looks for `envx.config.*` in cwd (`/repo/packages/docker-builder/`). **Match — that file is loaded.**
|
|
125
|
+
|
|
126
|
+
Configs don't merge across the walk — the first one found is the **only** one read. If you want true workspace-root + package-level layering, that's a feature we don't currently have.
|
|
127
|
+
|
|
128
|
+
## Per-field merge
|
|
129
|
+
|
|
130
|
+
Within a single invocation, each setting is resolved **independently** through these layers:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
For each setting field (envFiles, cascade, envPath, envKeysFile,
|
|
134
|
+
override, quiet, autoDetect, nodeEnvMap, …):
|
|
135
|
+
|
|
136
|
+
1. Did the CLI flag — or programmatic arg — set this field? → use that, stop
|
|
137
|
+
2. Is it set in the loaded config? → use that, stop
|
|
138
|
+
3. Otherwise → use the built-in default
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
So you can mix layers freely:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
# Config provides envFiles + nodeEnvMap; CLI overrides only `cascade`.
|
|
145
|
+
envx --cascade prod -- node app.js
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// envx.config.ts
|
|
150
|
+
export default {
|
|
151
|
+
envFiles: [".env", "vault/.env.prod"],
|
|
152
|
+
nodeEnvMap: { qa: "qa-prod" },
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Effective settings:
|
|
157
|
+
|
|
158
|
+
| field | value | source |
|
|
159
|
+
|-------------|------------------------------------|------------------|
|
|
160
|
+
| `envFiles` | `[".env", "vault/.env.prod"]` | config |
|
|
161
|
+
| `cascade` | `"prod"` (string overrides config's boolean) | CLI flag |
|
|
162
|
+
| `envPath` | _(none)_ | default |
|
|
163
|
+
| `nodeEnvMap`| `{ qa: "qa-prod", + built-ins }` | config (merged) |
|
|
164
|
+
| `override` | `false` | default |
|
|
165
|
+
| `quiet` | `true` | default |
|
|
166
|
+
| `autoDetect`| `true` | default |
|
|
167
|
+
|
|
168
|
+
## Load pipeline
|
|
169
|
+
|
|
170
|
+
When `envx run` (or any subcommand that calls `loadEnv`) executes, the steps run in this exact order:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
1. resolve workspaceRoot (config field, else findWorkspaceRoot())
|
|
174
|
+
2. resolve env file paths (envFiles → .env. prefix, cascade expansion)
|
|
175
|
+
3. load each env file (encrypted values decrypted with .env.keys)
|
|
176
|
+
→ mutates process.env (subject to `override`)
|
|
177
|
+
4. apply `variables` (-v KEY=VALUE on CLI / variables in config)
|
|
178
|
+
→ unconditional overwrite of process.env
|
|
179
|
+
5. apply `defaults` (only for keys still undefined)
|
|
180
|
+
→ fills holes; never overwrites
|
|
181
|
+
6. expand `${VAR}` references (when `expand: true`)
|
|
182
|
+
→ in-place substitution against process.env
|
|
183
|
+
7. enforce `required` (any unset → log + process.exit(1))
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Implications:
|
|
187
|
+
- `variables` beats `defaults` — variables always overwrite, defaults only fill in.
|
|
188
|
+
- `expand` runs AFTER `defaults`, so a default like `URL: "${HOST}/api"` can resolve.
|
|
189
|
+
- `required` runs LAST, so it sees the final shape of `process.env`. A key that was unset in the env file but provided by `defaults` will pass the `required` check.
|
|
190
|
+
|
|
191
|
+
## Variable-value precedence (after files load)
|
|
192
|
+
|
|
193
|
+
This is separate from setting precedence. Once envx has resolved settings and loaded the env files, the actual values that end up in `process.env` follow this order:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
Final value of process.env.SOME_KEY is the FIRST hit, top-down:
|
|
197
|
+
|
|
198
|
+
1. `-v SOME_KEY=…` passed on the CLI (always wins)
|
|
199
|
+
2. Existing process.env.SOME_KEY when override=false (the default)
|
|
200
|
+
OR, when override=true:
|
|
201
|
+
The most-specific env file's value (cascade order)
|
|
202
|
+
3. The next-most-specific env file's value
|
|
203
|
+
4. … all the way down the cascade
|
|
204
|
+
5. unset
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`-v` is **not** a config layer — it's a post-load mutation of `process.env`. Use it for one-off injections; use `envFiles` / `cascade` to swap whole environments.
|
|
208
|
+
|
|
209
|
+
## Validation
|
|
210
|
+
|
|
211
|
+
The config is parsed by `normalize()` in `config.ts`. Unknown fields are silently dropped (so adding a typo won't error — it'll just have no effect). Wrong types for known fields are dropped too:
|
|
212
|
+
|
|
213
|
+
| input | result |
|
|
214
|
+
|----------------------------------------|----------------------------------------------|
|
|
215
|
+
| `envFiles: ".env"` (string, not array) | dropped — must be `string[]` |
|
|
216
|
+
| `nodeEnvMap: { qa: 42 }` | the entry is dropped (value must be string) |
|
|
217
|
+
| `unknown: "field"` | silently dropped |
|
|
218
|
+
|
|
219
|
+
If you want strict validation in CI, run `envx info` and grep for the field — the displayed value tells you whether envx accepted it. (A future addition could be a `--strict` flag that errors on unknowns; not currently implemented.)
|
|
220
|
+
|
|
221
|
+
## See also
|
|
222
|
+
|
|
223
|
+
- [Auto-detection](./auto-detection.md) — every signal shape walked through with examples.
|
|
224
|
+
- [Recipes](./recipes.md) — common monorepo layouts with the exact config files.
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Monorepo recipes
|
|
2
|
+
|
|
3
|
+
Concrete layouts for the most common envx-in-a-monorepo setups. Each recipe lists the directory tree, the config files, and the commands you'd run.
|
|
4
|
+
|
|
5
|
+
## Recipe 1: Single workspace `.env.keys`, vault subdirectory
|
|
6
|
+
|
|
7
|
+
The most common shape — encrypted env files live in `vault/` at the workspace root, with one `.env.keys` (gitignored) covering everything.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/repo
|
|
11
|
+
├── .env.keys ← gitignored, holds private keys for every vault file
|
|
12
|
+
├── pnpm-workspace.yaml
|
|
13
|
+
├── envx.config.ts ← workspace-level defaults
|
|
14
|
+
├── vault/
|
|
15
|
+
│ ├── .env ← committed, encrypted
|
|
16
|
+
│ ├── .env.dev ← committed, encrypted
|
|
17
|
+
│ ├── .env.prod ← committed, encrypted
|
|
18
|
+
│ └── .env.staging ← committed, encrypted
|
|
19
|
+
└── packages/
|
|
20
|
+
├── api/
|
|
21
|
+
│ └── package.json
|
|
22
|
+
└── web/
|
|
23
|
+
└── package.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// /repo/envx.config.ts
|
|
28
|
+
export default {
|
|
29
|
+
envPath: "vault", // every command pulls from vault/
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
# Commands run from any package directory — envx walks up to find the config.
|
|
35
|
+
cd /repo/packages/api
|
|
36
|
+
|
|
37
|
+
envx encrypt -e .env.prod # writes vault/.env.prod + updates /repo/.env.keys
|
|
38
|
+
envx -- pnpm start # auto-detects environment, loads vault/.env.<detected>
|
|
39
|
+
envx --cascade prod -- node app.js # layers vault/.env, vault/.env.prod, etc.
|
|
40
|
+
envx rotate # rotates the keypair for vault/.env
|
|
41
|
+
envx info # shows what envx resolved
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`.gitignore` should contain `.env.keys` (and any `.env.*.local` if you use cascades).
|
|
45
|
+
|
|
46
|
+
## Recipe 2: Per-package overrides on top of workspace defaults
|
|
47
|
+
|
|
48
|
+
Some packages have additional environment files that don't belong at the workspace root (e.g. a Docker image-builder that pulls from a separate vault).
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
/repo
|
|
52
|
+
├── envx.config.ts ← workspace defaults
|
|
53
|
+
├── vault/
|
|
54
|
+
│ └── .env ← shared by most packages
|
|
55
|
+
└── packages/
|
|
56
|
+
├── api/ ← inherits /repo/envx.config.ts
|
|
57
|
+
│ └── package.json
|
|
58
|
+
└── docker-builder/
|
|
59
|
+
├── envx.config.ts ← package-local override
|
|
60
|
+
├── vault/
|
|
61
|
+
│ └── .env.image ← only relevant to this package
|
|
62
|
+
└── package.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// /repo/envx.config.ts
|
|
67
|
+
export default {
|
|
68
|
+
envPath: "vault",
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// /repo/packages/docker-builder/envx.config.ts
|
|
74
|
+
export default {
|
|
75
|
+
envFiles: [".env.image"],
|
|
76
|
+
envPath: "vault",
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
When run from `packages/docker-builder/`, envx finds the package-local config first and uses it. From `packages/api/`, envx walks up and uses the workspace config. Configs **don't merge** across the walk — the closest one wins entirely.
|
|
81
|
+
|
|
82
|
+
## Recipe 3: NODE_ENV-driven environments without `--env` everywhere
|
|
83
|
+
|
|
84
|
+
You want `pnpm start` to load `.env.dev`, `pnpm start:prod` to load `.env.prod`, and `NODE_ENV=staging pnpm test` to load `.env.staging` — all without typing `--env` each time.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// /repo/envx.config.ts
|
|
88
|
+
export default {
|
|
89
|
+
// No envFiles → auto-detect kicks in.
|
|
90
|
+
// No nodeEnvMap override needed — staging passes through automatically.
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```jsonc
|
|
95
|
+
// /repo/package.json
|
|
96
|
+
{
|
|
97
|
+
"scripts": {
|
|
98
|
+
"start": "envx -- node app.js", // → .env (no NODE_ENV)
|
|
99
|
+
"start:dev": "NODE_ENV=development envx -- node app.js", // → .env.dev
|
|
100
|
+
"start:prod": "NODE_ENV=production envx -- node app.js", // → .env.prod
|
|
101
|
+
"test:staging": "NODE_ENV=staging envx -- vitest run" // → .env.staging
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If your team uses non-canonical names (`qa`, `preview`, …), envx loads `.env.<value>` automatically — no config change needed.
|
|
107
|
+
|
|
108
|
+
## Recipe 4: Custom `NODE_ENV` mapping
|
|
109
|
+
|
|
110
|
+
You want `NODE_ENV=qa` to load `.env.qa-prod` (because QA actually shares prod's external services), and `NODE_ENV=preview` to load just `.env` (no per-environment overrides).
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// /repo/envx.config.ts
|
|
114
|
+
export default {
|
|
115
|
+
envPath: "vault",
|
|
116
|
+
nodeEnvMap: {
|
|
117
|
+
qa: "qa-prod",
|
|
118
|
+
preview: "", // empty = no suffix → .env
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
NODE_ENV=qa envx -- node app.js
|
|
125
|
+
# Loads vault/.env.qa-prod (or walks up if missing)
|
|
126
|
+
|
|
127
|
+
NODE_ENV=preview envx -- node app.js
|
|
128
|
+
# Loads vault/.env (no preview-specific file)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Verify with `envx info`:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
NODE_ENV → suffix mapping
|
|
135
|
+
development → .env.dev (default)
|
|
136
|
+
local → .env.local (default)
|
|
137
|
+
production → .env.prod (default)
|
|
138
|
+
qa → .env.qa-prod (config)
|
|
139
|
+
preview → (no suffix → .env) (config)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Recipe 5: CI without committing `.env.keys`
|
|
143
|
+
|
|
144
|
+
In CI (GitHub Actions, Vercel, Netlify, …), the `.env.keys` file isn't checked in. Inject the private keys via the platform's secret store and write them out at job start.
|
|
145
|
+
|
|
146
|
+
### GitHub Actions
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
# .github/workflows/ci.yml
|
|
150
|
+
jobs:
|
|
151
|
+
test:
|
|
152
|
+
runs-on: ubuntu-latest
|
|
153
|
+
steps:
|
|
154
|
+
- uses: actions/checkout@v4
|
|
155
|
+
- uses: pnpm/action-setup@v4
|
|
156
|
+
- uses: actions/setup-node@v4
|
|
157
|
+
with:
|
|
158
|
+
node-version: 22
|
|
159
|
+
|
|
160
|
+
- run: pnpm install --frozen-lockfile
|
|
161
|
+
|
|
162
|
+
# Materialize .env.keys from secrets — one entry per encrypted file.
|
|
163
|
+
- name: Write .env.keys
|
|
164
|
+
run: |
|
|
165
|
+
cat > .env.keys <<EOF
|
|
166
|
+
ENVX_PRIVATE_KEY="${{ secrets.ENVX_PRIVATE_KEY }}"
|
|
167
|
+
ENVX_PRIVATE_KEY_PROD="${{ secrets.ENVX_PRIVATE_KEY_PROD }}"
|
|
168
|
+
ENVX_PRIVATE_KEY_STAGING="${{ secrets.ENVX_PRIVATE_KEY_STAGING }}"
|
|
169
|
+
EOF
|
|
170
|
+
|
|
171
|
+
- run: NODE_ENV=staging pnpm test
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `.env.keys` lands at the repo root; envx finds it via the cwd-first walk-up regardless of which package the test runs in.
|
|
175
|
+
|
|
176
|
+
### Vercel / Netlify
|
|
177
|
+
|
|
178
|
+
These platforms set `VERCEL_ENV` / `CONTEXT` automatically, so envx auto-detects the right `.env.<env>` without `NODE_ENV`. Inject `.env.keys` via their secret-file or env-var mechanism.
|
|
179
|
+
|
|
180
|
+
For per-environment private keys, set them as project env vars and write `.env.keys` in your build step:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
# Vercel build command
|
|
184
|
+
echo "ENVX_PRIVATE_KEY_PROD=$ENVX_PRIVATE_KEY_PROD" > .env.keys && pnpm build
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Recipe 6: Programmatic embed with custom config
|
|
188
|
+
|
|
189
|
+
You're building a CLI of your own and want to embed envx's loading + auto-detect without the user having to install envx separately.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import envx from "@super-repo/envx";
|
|
193
|
+
|
|
194
|
+
// Same shape as envx.config.ts:
|
|
195
|
+
envx({
|
|
196
|
+
envFiles: ["vault/.env.prod"],
|
|
197
|
+
envPath: "vault",
|
|
198
|
+
override: true,
|
|
199
|
+
autoDetect: false, // we're picking files explicitly
|
|
200
|
+
variables: ["MY_TOOL_VERSION=1.2.3"],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// process.env is now populated; carry on.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For full control over which yargs commands you re-export, pull `createCli` from the `/commands` subpath:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { createCli } from "@super-repo/envx/commands";
|
|
210
|
+
|
|
211
|
+
createCli(process.argv.slice(2)).parseSync();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Recipe 7: Migrating an existing dotenvx project
|
|
215
|
+
|
|
216
|
+
You have a project using `@dotenvx/dotenvx` directly and want to switch.
|
|
217
|
+
|
|
218
|
+
1. **Install:**
|
|
219
|
+
```sh
|
|
220
|
+
pnpm add @super-repo/envx
|
|
221
|
+
pnpm remove @dotenvx/dotenvx
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
2. **Rename references:** `dotenvx encrypt` → `envx encrypt`, `dotenvx run` → `envx run`. The `dotenvx-proxy` bin in this package is a direct passthrough to the upstream dotenvx CLI if you need to keep specific commands working during migration.
|
|
225
|
+
|
|
226
|
+
3. **Keys file:** existing `.env.keys` files **work as-is**. envx reads `DOTENV_PRIVATE_KEY*` and `DOTENV_PUBLIC_KEY*` as fallbacks — no rename needed. New encrypts will use the canonical `ENVX_PRIVATE_KEY*` / `ENVX_PUBLIC_KEY*` names; you can migrate the existing entries opportunistically (or never).
|
|
227
|
+
|
|
228
|
+
4. **Verify:**
|
|
229
|
+
```sh
|
|
230
|
+
envx info
|
|
231
|
+
envx -- node app.js # same behavior as before
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If anything decrypts under dotenvx but not envx (or vice versa), they're not actually wire-compatible and we should know — open an issue.
|