@romaintaillandier1978/dotenv-never-lies 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/DnlTest.d.ts +2 -0
- package/dist/DnlTest.d.ts.map +1 -0
- package/dist/DnlTest.js +12 -0
- package/dist/cli/commands/assert.d.ts +5 -0
- package/dist/cli/commands/assert.d.ts.map +1 -0
- package/dist/cli/commands/assert.js +27 -0
- package/dist/cli/commands/explain.d.ts +8 -0
- package/dist/cli/commands/explain.d.ts.map +1 -0
- package/dist/cli/commands/explain.js +55 -0
- package/dist/cli/commands/generate.d.ts +7 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +28 -0
- package/dist/cli/commands/reverseEnv.d.ts +7 -0
- package/dist/cli/commands/reverseEnv.d.ts.map +1 -0
- package/dist/cli/commands/reverseEnv.js +33 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/utils/infer-schema.d.ts +3 -0
- package/dist/cli/utils/infer-schema.d.ts.map +1 -0
- package/dist/cli/utils/infer-schema.js +24 -0
- package/dist/cli/utils/load-schema.d.ts +3 -0
- package/dist/cli/utils/load-schema.d.ts.map +1 -0
- package/dist/cli/utils/load-schema.js +22 -0
- package/dist/cli/utils/printer.d.ts +16 -0
- package/dist/cli/utils/printer.d.ts.map +1 -0
- package/dist/cli/utils/printer.js +137 -0
- package/dist/cli/utils/resolve-schema.d.ts +2 -0
- package/dist/cli/utils/resolve-schema.d.ts.map +1 -0
- package/dist/cli/utils/resolve-schema.js +27 -0
- package/dist/core.d.ts +107 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +53 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Romain TAILLANDIER
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# dotenv-never-lies
|
|
2
|
+
|
|
3
|
+
> Parce que les variables d’environnement **mentent tout le temps**.
|
|
4
|
+
|
|
5
|
+
**dotenv-never-lies** valide, type et documente tes variables d’environnement à partir d’un schéma TypeScript / Zod.
|
|
6
|
+
Il échoue **vite**, **fort**, et **avant la prod**.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Pourquoi ?
|
|
11
|
+
|
|
12
|
+
Parce que tout ça arrive **tout le temps** :
|
|
13
|
+
|
|
14
|
+
- ❌ une variable d’env manquante → **crash au runtime**
|
|
15
|
+
- ❌ une URL mal formée → **bug subtil en prod**
|
|
16
|
+
- ❌ la CI n’a pas été mise à jour après une nouvelle variable → **déploiement rouge incompréhensible**
|
|
17
|
+
- ❌ un `process.env.FOO!` optimiste → **mensonge à toi-même**
|
|
18
|
+
|
|
19
|
+
Et parce que `.env` est :
|
|
20
|
+
|
|
21
|
+
- non typé
|
|
22
|
+
- non documenté
|
|
23
|
+
- partagé à la main
|
|
24
|
+
- rarement à jour
|
|
25
|
+
|
|
26
|
+
👉 **dotenv-never-lies** transforme cette configuration fragile en **contrat explicite**.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Ce que fait la lib
|
|
31
|
+
|
|
32
|
+
- ✅ valide les variables d’environnement au démarrage
|
|
33
|
+
- ✅ fournit un typage TypeScript fiable
|
|
34
|
+
- ✅ documente chaque variable
|
|
35
|
+
- ✅ expose un CLI pour la CI et les humains
|
|
36
|
+
- ✅ permet des transformations complexes (arrays, parsing, coercion…)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Ce que dotenv-never-lies n’est pas
|
|
41
|
+
|
|
42
|
+
Ce package a un périmètre volontairement **limité**.
|
|
43
|
+
|
|
44
|
+
- ❌ **Ce n’est pas un outil frontend**
|
|
45
|
+
Il n’est pas destiné à être utilisé dans un navigateur.
|
|
46
|
+
Pas de bundler, pas de `import.meta.env`, pas de variables exposées au client.
|
|
47
|
+
|
|
48
|
+
- ❌ **Ce n’est pas un gestionnaire de secrets**
|
|
49
|
+
Il ne chiffre rien, ne stocke rien, ne remplace ni Vault, ni AWS Secrets Manager,
|
|
50
|
+
ni les variables sécurisées de ton provider CI/CD.
|
|
51
|
+
|
|
52
|
+
- ❌ **Ce n’est pas une solution cross-runtime**
|
|
53
|
+
Support ciblé : **Node.js**.
|
|
54
|
+
Deno, Bun, Cloudflare Workers, edge runtimes : hors scope (pour l’instant).
|
|
55
|
+
|
|
56
|
+
- ❌ **Ce n’est pas un framework de configuration global**
|
|
57
|
+
Il ne gère ni les fichiers YAML/JSON, ni les profiles dynamiques,
|
|
58
|
+
ni les overrides magiques par environnement.
|
|
59
|
+
|
|
60
|
+
- ❌ **Ce n’est pas permissif**
|
|
61
|
+
S’il manque une variable ou qu’une valeur est invalide, ça plante.
|
|
62
|
+
C’est le but.
|
|
63
|
+
|
|
64
|
+
En résumé :
|
|
65
|
+
**dotenv-never-lies** est fait pour des **APIs Node.js** et des **services backend**
|
|
66
|
+
qui préfèrent **échouer proprement au démarrage** plutôt que **bugger silencieusement en prod**.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Dependency warnings
|
|
71
|
+
|
|
72
|
+
⚠️ Important
|
|
73
|
+
dotenv-never-lies expose des schémas Zod dans son API publique.
|
|
74
|
+
Zod v4 est requis.
|
|
75
|
+
Mélanger les versions cassera l’inférence de types (et oui, ça fait mal).
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
yarn add dotenv-never-lies
|
|
81
|
+
# ou
|
|
82
|
+
npm install dotenv-never-lies
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Expansion des variables (`dotenv-expand`)
|
|
86
|
+
|
|
87
|
+
**dotenv-never-lies** gère automatiquement l’expansion des variables d’environnement,
|
|
88
|
+
via [`dotenv-expand`](https://www.npmjs.com/package/dotenv-expand).
|
|
89
|
+
|
|
90
|
+
Cela permet de définir des variables composées à partir d’autres variables,
|
|
91
|
+
sans duplication ni copier-coller fragile.
|
|
92
|
+
|
|
93
|
+
### Exemple
|
|
94
|
+
|
|
95
|
+
```env
|
|
96
|
+
FRONT_A=https://a.site.com
|
|
97
|
+
FRONT_B=https://b.site.com
|
|
98
|
+
FRONT_C=https://c.site.com
|
|
99
|
+
|
|
100
|
+
NODE_CORS_ORIGIN="${FRONT_A};${FRONT_B};${FRONT_C}"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Définir un schéma
|
|
104
|
+
|
|
105
|
+
env.dnl.ts
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { z } from "zod";
|
|
109
|
+
import { define } from "dotenv-never-lies";
|
|
110
|
+
|
|
111
|
+
export default define({
|
|
112
|
+
NODE_ENV: {
|
|
113
|
+
description: "Environnement d’exécution",
|
|
114
|
+
schema: z.enum(["test", "development", "staging", "production"]),
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
NODE_PORT: {
|
|
118
|
+
description: "Port de l’API",
|
|
119
|
+
schema: z.coerce.number(),
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
FRONT_A: {
|
|
123
|
+
description: "Mon site A",
|
|
124
|
+
schema: z.url(),
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
FRONT_B: {
|
|
128
|
+
description: "Mon site B",
|
|
129
|
+
schema: z.url(),
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
FRONT_C: {
|
|
133
|
+
description: "Mon site C",
|
|
134
|
+
schema: z.url(),
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
NODE_CORS_ORIGIN: {
|
|
138
|
+
description: "URLs frontend autorisées à appeler cette API",
|
|
139
|
+
schema: z.string().transform((v) =>
|
|
140
|
+
v
|
|
141
|
+
.split(";")
|
|
142
|
+
.map((s) => s.trim())
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.map((url) => z.url().parse(url))
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
JWT_SECRET: {
|
|
149
|
+
description: "JWT Secret",
|
|
150
|
+
schema: z.string(),
|
|
151
|
+
secret: true,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Utilisation runtime
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import envDef from "./env.dnl";
|
|
160
|
+
|
|
161
|
+
export const ENV = envDef.load();
|
|
162
|
+
|
|
163
|
+
if(ENV.NODE_ENV === "test"){...}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Résultat :
|
|
169
|
+
|
|
170
|
+
- ENV.NODE_ENV est un enum
|
|
171
|
+
- ENV.NODE_PORT est un number
|
|
172
|
+
- ENV.FRONT_A ENV.FRONT_B ENV.FRONT_C sont des URLs valides
|
|
173
|
+
- ENV.NODE_CORS_ORIGIN est un string[] contenant des URLs valides
|
|
174
|
+
- ENV.JWT_SECRET est une string
|
|
175
|
+
|
|
176
|
+
Si une variable est absente ou invalide → le process s’arrête immédiatement. \
|
|
177
|
+
C’est volontaire.
|
|
178
|
+
|
|
179
|
+
## Éviter `process.env` dans le code applicatif
|
|
180
|
+
|
|
181
|
+
Une fois le schéma chargé, l’accès aux variables d’environnement
|
|
182
|
+
doit se faire exclusivement via l’objet `ENV`.
|
|
183
|
+
|
|
184
|
+
Cela garantit :
|
|
185
|
+
|
|
186
|
+
- un typage strict
|
|
187
|
+
- des valeurs validées
|
|
188
|
+
- un point d’entrée unique pour la configuration
|
|
189
|
+
|
|
190
|
+
Pour identifier les usages résiduels de `process.env` dans votre codebase, un simple outil de recherche suffit :
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
grep -R "process\.env" src
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Le choix de corriger (ou non) ces usages dépend du contexte et reste volontairement laissé au développeur.
|
|
197
|
+
|
|
198
|
+
## CLI
|
|
199
|
+
|
|
200
|
+
Le CLI permet de valider, charger, générer et documenter les variables d’environnement à partir d’un schéma `dotenv-never-lies`.
|
|
201
|
+
|
|
202
|
+
Il est conçu pour être utilisé :
|
|
203
|
+
|
|
204
|
+
- en local (par des humains)
|
|
205
|
+
- en CI (sans surprise)
|
|
206
|
+
- avant que l’application ne démarre (et pas après)
|
|
207
|
+
|
|
208
|
+
### Valider un fichier `.env` (CI-friendly)
|
|
209
|
+
|
|
210
|
+
Valide les variables sans les injecter dans `process.env`.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
dnl check --schema env.dnl.ts
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
→ échoue si :
|
|
217
|
+
|
|
218
|
+
- une variable est manquante
|
|
219
|
+
- une valeur est invalide
|
|
220
|
+
- le schéma n’est pas respecté
|
|
221
|
+
|
|
222
|
+
### Charger les variables dans le process
|
|
223
|
+
|
|
224
|
+
Charge et valide les variables dans `process.env`.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
dnl load --schema env.dnl.ts
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Usage typique : scripts de démarrage, tooling local.
|
|
231
|
+
|
|
232
|
+
### Générer un fichier .env à partir du schéma
|
|
233
|
+
|
|
234
|
+
Génère un .env documenté à partir du schéma.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
dnl generate --schema env.dnl.ts --out .env
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Utile pour :
|
|
241
|
+
|
|
242
|
+
- initialiser un projet
|
|
243
|
+
- partager un template
|
|
244
|
+
- éviter les .env.example obsolètes
|
|
245
|
+
|
|
246
|
+
### Générer un schéma depuis un .env existant
|
|
247
|
+
|
|
248
|
+
Crée un fichier env.dnl.ts à partir d’un .env.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
dnl reverse-env --source .env
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Utile pour :
|
|
255
|
+
|
|
256
|
+
- migrer un projet existant
|
|
257
|
+
- documenter a posteriori une configuration legacy
|
|
258
|
+
|
|
259
|
+
### Afficher la documentation des variables
|
|
260
|
+
|
|
261
|
+
Affiche la liste des variables connues et leur description.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
dnl print
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Exemple de sortie :
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
FRONT_A: Mon site A
|
|
271
|
+
FRONT_B: Mon site B
|
|
272
|
+
FRONT_C: Mon site C
|
|
273
|
+
NODE_CORS_ORIGIN: URLs frontend autorisées à appeler cette API
|
|
274
|
+
JWT_SECRET: JWT Secret
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
TODO : check CI in real life.
|
|
279
|
+
|
|
280
|
+
## Usages dans la vraie vie
|
|
281
|
+
|
|
282
|
+
### Git
|
|
283
|
+
|
|
284
|
+
#### Git hooks recommandés
|
|
285
|
+
|
|
286
|
+
Il est fortement conseillé d’utiliser **dotenv-never-lies** via des hooks Git :
|
|
287
|
+
|
|
288
|
+
- **pre-commit** : empêche de committer si la configuration locale n’est pas conforme au schéma
|
|
289
|
+
- **post-merge** : détecte immédiatement les changements de schéma impactant l’environnement local
|
|
290
|
+
|
|
291
|
+
L’objectif est simple :
|
|
292
|
+
**si la configuration locale n’est pas conforme au schéma, le code ne doit pas être committé.**
|
|
293
|
+
|
|
294
|
+
Le schéma est la source de vérité, pas les fichiers `.env`.
|
|
295
|
+
|
|
296
|
+
Ces hooks permettent d’éviter les erreurs classiques :
|
|
297
|
+
|
|
298
|
+
- variable manquante après un pull
|
|
299
|
+
- format invalide détecté trop tard
|
|
300
|
+
- “ça marche chez moi” dû à un `.env` obsolète
|
|
301
|
+
|
|
302
|
+
#### Installation des hooks
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
git config core.hooksPath .githooks
|
|
306
|
+
mkdir -p .githooks
|
|
307
|
+
|
|
308
|
+
cat > .githooks/pre-commit <<'EOF'
|
|
309
|
+
#!/bin/sh
|
|
310
|
+
yarn dnl assert --source .env
|
|
311
|
+
EOF
|
|
312
|
+
|
|
313
|
+
cat > .githooks/post-merge <<'EOF'
|
|
314
|
+
#!/bin/sh
|
|
315
|
+
yarn dnl assert --source .env || true
|
|
316
|
+
EOF
|
|
317
|
+
|
|
318
|
+
chmod +x .githooks/pre-commit .githooks/post-merge
|
|
319
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DnlTest.d.ts","sourceRoot":"","sources":["../src/DnlTest.ts"],"names":[],"mappings":""}
|
package/dist/DnlTest.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
//envDefinition.explain({ key: "NODE_CORS_ORIGIN" });
|
|
3
|
+
// const source: EnvSource = dnl.readEnvFile(".env");
|
|
4
|
+
// let ok = envDefinition.check();
|
|
5
|
+
// ok = envDefinition.check({ source: process.env });
|
|
6
|
+
// const ENV = envDefinition.load({ source });
|
|
7
|
+
// ok = envDefinition.check();
|
|
8
|
+
// ok = envDefinition.check({ source: process.env });
|
|
9
|
+
// // intellisens OK
|
|
10
|
+
// ENV.NODE_ENV; //(property) NODE_ENV: "test" | "development" | "staging" | "production"
|
|
11
|
+
// ENV.NODE_PORT; //(property) NODE_PORT: number
|
|
12
|
+
// envDefinition.explain();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/assert.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,aAAa,GAAU,MAAM;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,kBAwB3E,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import dnl from "../../index.js";
|
|
3
|
+
import { loadDef as loadDef } from "../utils/load-schema.js";
|
|
4
|
+
import { resolveSchemaPath } from "../utils/resolve-schema.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const assertCommand = async (opts) => {
|
|
7
|
+
const schemaPath = resolveSchemaPath(opts.schema);
|
|
8
|
+
const envDef = (await loadDef(schemaPath));
|
|
9
|
+
try {
|
|
10
|
+
envDef.assert({
|
|
11
|
+
source: opts.source ? dnl.readEnvFile(path.resolve(process.cwd(), opts.source)) : process.env,
|
|
12
|
+
});
|
|
13
|
+
console.log("✅ Environment is valid");
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error instanceof z.ZodError) {
|
|
17
|
+
console.error("❌ Invalid environment variables:\n");
|
|
18
|
+
for (const issue of error.issues) {
|
|
19
|
+
const key = issue.path.join(".");
|
|
20
|
+
console.error(`- ${key}`);
|
|
21
|
+
console.error(` → ${issue.message}`);
|
|
22
|
+
}
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type ExplainCliOptions = {
|
|
2
|
+
schema?: string | undefined;
|
|
3
|
+
keys?: string[] | undefined;
|
|
4
|
+
format?: "human" | "json" | undefined;
|
|
5
|
+
};
|
|
6
|
+
export declare const explainCommand: (options?: ExplainCliOptions) => Promise<number>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=explain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/explain.ts"],"names":[],"mappings":"AAKA,KAAK,iBAAiB,GAAG;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CACzC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,UAAU,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAmChF,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { loadDef } from "../utils/load-schema.js";
|
|
2
|
+
import { resolveSchemaPath } from "../utils/resolve-schema.js";
|
|
3
|
+
import { toExplanation } from "../utils/printer.js";
|
|
4
|
+
export const explainCommand = async (options) => {
|
|
5
|
+
const schemaPath = resolveSchemaPath(options?.schema);
|
|
6
|
+
const envDef = (await loadDef(schemaPath));
|
|
7
|
+
const format = options?.format ?? "human";
|
|
8
|
+
const keysToSerialize = new Array();
|
|
9
|
+
if (options?.keys) {
|
|
10
|
+
keysToSerialize.push(...options.keys);
|
|
11
|
+
}
|
|
12
|
+
const result = new Array();
|
|
13
|
+
for (const [key, value] of Object.entries(envDef.def)) {
|
|
14
|
+
if (keysToSerialize.length > 0 && !keysToSerialize.includes(key)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
result.push(toExplanation(key, value));
|
|
18
|
+
}
|
|
19
|
+
if (result.length === 0) {
|
|
20
|
+
console.error("No variables found");
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
switch (format) {
|
|
24
|
+
case "json":
|
|
25
|
+
console.log(JSON.stringify(result, null, 2));
|
|
26
|
+
return 0;
|
|
27
|
+
case "human":
|
|
28
|
+
printHuman(result);
|
|
29
|
+
return 0;
|
|
30
|
+
default:
|
|
31
|
+
console.error(`Invalid format: ${format}`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const printHuman = (result) => {
|
|
36
|
+
if (result.length > 1) {
|
|
37
|
+
for (const item of result) {
|
|
38
|
+
console.log(`${item.key}: ${item.description} --- dnl explain ${item.key} --format human for more details`);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(`${result[0].key}:`);
|
|
43
|
+
console.log(` Description: ${result[0].description}`);
|
|
44
|
+
console.log(` Type: ${result[0].type}`);
|
|
45
|
+
if (result[0].required !== undefined) {
|
|
46
|
+
console.log(` Required: ${result[0].required ? "Yes" : "No"}`);
|
|
47
|
+
}
|
|
48
|
+
if (result[0].default !== undefined) {
|
|
49
|
+
console.log(` Default: ${result[0].default ?? "No"}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(` Secret: ${result[0].secret === true ? "Yes" : "No"}`);
|
|
52
|
+
if (result[0].examples) {
|
|
53
|
+
console.log(` Examples: ${result[0].examples.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/generate.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,eAAe,GAAU,MAAM;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,kBA4BtH,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadDef } from "../utils/load-schema.js";
|
|
4
|
+
import { resolveSchemaPath } from "../utils/resolve-schema.js";
|
|
5
|
+
import { getDefaultEnvValue } from "../utils/printer.js";
|
|
6
|
+
export const generateCommand = async (opts) => {
|
|
7
|
+
const outFile = opts.out ?? ".env";
|
|
8
|
+
const target = path.resolve(process.cwd(), outFile);
|
|
9
|
+
if (fs.existsSync(target) && !opts.force) {
|
|
10
|
+
console.error(`❌ ${outFile} already exists. Use --force to overwrite.`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const schemaPath = resolveSchemaPath(opts.schema);
|
|
14
|
+
const envDef = (await loadDef(schemaPath));
|
|
15
|
+
const lines = [];
|
|
16
|
+
for (const [key, def] of Object.entries(envDef.def)) {
|
|
17
|
+
const typedDef = def;
|
|
18
|
+
if (typedDef.description) {
|
|
19
|
+
lines.push(`# ${typedDef.description}`);
|
|
20
|
+
}
|
|
21
|
+
const defaultValue = getDefaultEnvValue(typedDef.schema.def);
|
|
22
|
+
lines.push(`${key}=${defaultValue ?? ""}`);
|
|
23
|
+
lines.push(""); // blank line
|
|
24
|
+
}
|
|
25
|
+
const output = lines.join("\n");
|
|
26
|
+
fs.writeFileSync(target, output);
|
|
27
|
+
console.log(`✅ ${outFile} generated`);
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverseEnv.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/reverseEnv.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB,GAAU,MAAM;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,kBAiCtH,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import dnl from "../../index.js";
|
|
3
|
+
import { guessSecret, inferSchema } from "../utils/infer-schema.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
export const reverseEnvCommand = async (opts) => {
|
|
6
|
+
const source = path.resolve(process.cwd(), opts.source ?? ".env");
|
|
7
|
+
const out = opts.out ?? "env.dnl.ts";
|
|
8
|
+
const target = path.resolve(process.cwd(), out);
|
|
9
|
+
if (fs.existsSync(target) && !opts.force) {
|
|
10
|
+
console.error(`❌ ${out} already exists. Use --force to overwrite.`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const env = dnl.readEnvFile(source);
|
|
14
|
+
const lines = [];
|
|
15
|
+
lines.push(`// ⚠️ This file was generated by dotenv-never-lies`);
|
|
16
|
+
lines.push(`// Review and adjust schemas, descriptions and secrets before using`);
|
|
17
|
+
lines.push("");
|
|
18
|
+
lines.push(`import { z } from "zod";`);
|
|
19
|
+
lines.push(`import { define } from "dotenv-never-lies";`);
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push(`export default define({`);
|
|
22
|
+
for (const [key, value] of Object.entries(env)) {
|
|
23
|
+
lines.push(` ${key}: {`);
|
|
24
|
+
lines.push(` description: "TODO",`);
|
|
25
|
+
lines.push(` schema: ${inferSchema(value)},`);
|
|
26
|
+
if (opts.guessSecret && guessSecret(key)) {
|
|
27
|
+
lines.push(` secret: true,`);
|
|
28
|
+
}
|
|
29
|
+
lines.push(` },`);
|
|
30
|
+
}
|
|
31
|
+
lines.push(`});`);
|
|
32
|
+
fs.writeFileSync(target, lines.join("\n"));
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { assertCommand } from "./commands/assert.js";
|
|
4
|
+
import { generateCommand } from "./commands/generate.js";
|
|
5
|
+
import { reverseEnvCommand } from "./commands/reverseEnv.js";
|
|
6
|
+
import { explainCommand } from "./commands/explain.js";
|
|
7
|
+
program
|
|
8
|
+
.name("dnl")
|
|
9
|
+
.version("0.1.0")
|
|
10
|
+
.addHelpText("before", `
|
|
11
|
+
CLI pour dotenv-never-lies.
|
|
12
|
+
Valide, charge et génère des variables d’environnement typées à partir d’un schéma TypeScript/Zod.
|
|
13
|
+
`)
|
|
14
|
+
.addHelpText("after", `\nExemples :
|
|
15
|
+
|
|
16
|
+
# Vérifier l’environnement à l’exécution et arrêter le process si le schéma n’est pas respecté
|
|
17
|
+
dnl assert
|
|
18
|
+
dnl assert --schema env.dnl.ts
|
|
19
|
+
|
|
20
|
+
# Générer un fichier .env documenté à partir du schéma
|
|
21
|
+
dnl generate
|
|
22
|
+
dnl generate --schema env.dnl.ts --out .env
|
|
23
|
+
|
|
24
|
+
# Créer un schéma env.dnl.ts depuis un .env existant
|
|
25
|
+
dnl reverse-env --source .env
|
|
26
|
+
|
|
27
|
+
# Afficher les variables connues et leur description
|
|
28
|
+
dnl explain
|
|
29
|
+
`);
|
|
30
|
+
program
|
|
31
|
+
.command("assert")
|
|
32
|
+
.description("Vérifie l’environnement runtime et termine le process si le schéma n’est pas respecté.")
|
|
33
|
+
.option("--schema <file>", "Fichier de schéma dnl (ex: my-dnl.ts)", "env.dnl.ts")
|
|
34
|
+
.option("-s, --source <source>", "Source des variables (défaut : process.env)")
|
|
35
|
+
.action(assertCommand)
|
|
36
|
+
.addHelpText("after", `\nExemples :
|
|
37
|
+
|
|
38
|
+
# Valider les variables d’environnement de process.env
|
|
39
|
+
# Recommandé en CI pour empêcher un démarrage avec une configuration invalide
|
|
40
|
+
dnl assert
|
|
41
|
+
dnl assert --schema my-dnl.ts
|
|
42
|
+
|
|
43
|
+
# Valider les variables d’environnement depuis un fichier .env
|
|
44
|
+
# Recommandé en local (préparation du schéma, onboarding)
|
|
45
|
+
dnl assert --source .env
|
|
46
|
+
dnl assert --schema my-dnl.ts --source .env
|
|
47
|
+
|
|
48
|
+
# valider les variables d'environnement du fichier fourni par la CI
|
|
49
|
+
dnl assert --source $ENV_FILE
|
|
50
|
+
dnl assert --schema my-dnl.ts --source $ENV_FILE
|
|
51
|
+
`);
|
|
52
|
+
program
|
|
53
|
+
.command("generate")
|
|
54
|
+
.description("Génère un fichier .env à partir d’un schéma dnl.\n" +
|
|
55
|
+
"Utile pour initialiser un projet ou faciliter l’onboarding d’un nouveau développeur.\n" +
|
|
56
|
+
"Seules les valeurs définies par défaut dans le schéma sont écrites.")
|
|
57
|
+
.option("--schema <file>", "Fichier de schéma env (ex: env.dnl.ts)")
|
|
58
|
+
.option("-o, --out <file>", "Fichier de sortie (défaut : .env)")
|
|
59
|
+
.option("-f, --force", "Écraser le fichier existant")
|
|
60
|
+
.action(generateCommand)
|
|
61
|
+
.addHelpText("after", `\nExemples :
|
|
62
|
+
|
|
63
|
+
# Générer un fichier .env à partir du schéma par défaut (env.dnl.ts)
|
|
64
|
+
dnl generate
|
|
65
|
+
|
|
66
|
+
# Générer un fichier .env à partir d'un schéma spécifié
|
|
67
|
+
dnl generate --schema my-dnl.ts
|
|
68
|
+
|
|
69
|
+
# Générer un fichier .env.local à partir du schéma
|
|
70
|
+
dnl generate --out .env.local
|
|
71
|
+
|
|
72
|
+
# Générer un fichier .env à partir d'un schéma et écraser le fichier existant
|
|
73
|
+
dnl generate --out .env --force
|
|
74
|
+
`);
|
|
75
|
+
program
|
|
76
|
+
.command("reverse-env")
|
|
77
|
+
.description("Génère un schéma dotenv-never-lies à partir d’un fichier .env.\n" +
|
|
78
|
+
"Utile pour migrer un projet existant vers dotenv-never-lies.\n" +
|
|
79
|
+
"Le schéma généré est un point de départ et doit être affiné manuellement.")
|
|
80
|
+
.option("-s, --source <source>", "Fichier .env source", ".env")
|
|
81
|
+
.option("-o, --out <file>", "Fichier dnl de sortie", "env.dnl.ts")
|
|
82
|
+
.option("-f, --force", "Écraser le fichier existant")
|
|
83
|
+
.option("--guess-secret", "Tenter de deviner les variables sensibles (heuristique)")
|
|
84
|
+
.action(reverseEnvCommand)
|
|
85
|
+
.addHelpText("after", `\nExemples :
|
|
86
|
+
|
|
87
|
+
# Générer un schéma env.dnl.ts à partir d'un fichier .env
|
|
88
|
+
dnl reverse-env
|
|
89
|
+
|
|
90
|
+
# Générer un schéma env.dnl.ts à partir d'un fichier .env.local
|
|
91
|
+
dnl reverse-env --source .env.local
|
|
92
|
+
|
|
93
|
+
# Générer un schéma my-dnl.ts à partir d'un fichier .env
|
|
94
|
+
dnl reverse-env --out my-dnl.ts
|
|
95
|
+
|
|
96
|
+
# Générer un schéma env.dnl.ts à partir d'un fichier .env et écraser le fichier existant
|
|
97
|
+
dnl reverse-env --force
|
|
98
|
+
`);
|
|
99
|
+
program
|
|
100
|
+
.command("explain")
|
|
101
|
+
.description("Affiche la liste des variables d’environnement connues et leur description.")
|
|
102
|
+
.argument("[keys...]", "Clés à expliquer (0..N). Sans argument, toutes les clés.")
|
|
103
|
+
.option("--schema <file>", "Fichier de schéma env (ex: env.dnl.ts)")
|
|
104
|
+
.option("-f, --format <format>", 'Format d\'affichage ("human" | "json")', "human")
|
|
105
|
+
.action(async (keys, opts) => {
|
|
106
|
+
const result = await explainCommand({ keys: keys ?? [], schema: opts.schema, format: opts.format });
|
|
107
|
+
process.exit(result);
|
|
108
|
+
})
|
|
109
|
+
.addHelpText("after", `\nExemples :
|
|
110
|
+
|
|
111
|
+
# expliquer toutes les variables connues et leur description
|
|
112
|
+
dnl explain
|
|
113
|
+
|
|
114
|
+
# expliquer une variable en détail
|
|
115
|
+
dnl explain NODE_ENV
|
|
116
|
+
|
|
117
|
+
# sortie machine
|
|
118
|
+
dnl explain --format json
|
|
119
|
+
|
|
120
|
+
# expliquer toutes les variables connues et leur description à partir d'un schéma
|
|
121
|
+
dnl explain --schema another-env.ts
|
|
122
|
+
|
|
123
|
+
# expliquer une partie des variables connues et leur description
|
|
124
|
+
dnl explain NODE_ENV NODE_PORT
|
|
125
|
+
|
|
126
|
+
`);
|
|
127
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const inferSchema: (value: string | undefined) => "z.string().optional()" | "z.coerce.boolean()" | "z.coerce.number()" | "z.string().url()" | "z.string().email()" | "z.string()";
|
|
2
|
+
export declare const guessSecret: (value: string) => boolean;
|
|
3
|
+
//# sourceMappingURL=infer-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infer-schema.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/infer-schema.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,GAAG,SAAS,oIAuBpD,CAAC;AAIF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,YAExC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const inferSchema = (value) => {
|
|
2
|
+
if (!value) {
|
|
3
|
+
return "z.string().optional()";
|
|
4
|
+
}
|
|
5
|
+
if (/^(true|false)$/i.test(value)) {
|
|
6
|
+
return "z.coerce.boolean()";
|
|
7
|
+
}
|
|
8
|
+
if (!isNaN(Number(value))) {
|
|
9
|
+
return "z.coerce.number()";
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
new URL(value);
|
|
13
|
+
return "z.string().url()";
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
if (/^[^@]+@[^@]+\.[^@]+$/.test(value)) {
|
|
17
|
+
return "z.string().email()";
|
|
18
|
+
}
|
|
19
|
+
return "z.string()";
|
|
20
|
+
};
|
|
21
|
+
const secretSuffixes = ["_SECRET", "_KEY", "_TOKEN", "_PASSWORD", "_AUTH"];
|
|
22
|
+
export const guessSecret = (value) => {
|
|
23
|
+
return secretSuffixes.some((suffix) => value.endsWith(suffix));
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-schema.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/load-schema.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,eAAO,MAAM,OAAO,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAsBlF,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
export const loadDef = async (schemaPath) => {
|
|
6
|
+
const outDir = path.join(process.cwd(), ".dnl");
|
|
7
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
8
|
+
const outFile = path.join(outDir, "env.dnl.mjs");
|
|
9
|
+
await build({
|
|
10
|
+
entryPoints: [schemaPath],
|
|
11
|
+
outfile: outFile,
|
|
12
|
+
format: "esm",
|
|
13
|
+
platform: "node",
|
|
14
|
+
target: "node18",
|
|
15
|
+
bundle: false,
|
|
16
|
+
});
|
|
17
|
+
const mod = await import(pathToFileURL(outFile).href);
|
|
18
|
+
if (!mod.default) {
|
|
19
|
+
throw new Error("Le fichier env.dnl.ts doit exporter un schéma par défaut (export default).");
|
|
20
|
+
}
|
|
21
|
+
return mod.default;
|
|
22
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EnvVarDefinition } from "../../core.js";
|
|
3
|
+
export type Explanation = {
|
|
4
|
+
key: string;
|
|
5
|
+
description: string;
|
|
6
|
+
type: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
default?: string;
|
|
9
|
+
secret: boolean;
|
|
10
|
+
examples?: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare const toExplanation: (key: string, value: EnvVarDefinition<any>) => Explanation;
|
|
13
|
+
export declare function printZodType(def: z.core.$ZodTypeDef): string;
|
|
14
|
+
export declare function getDefaultEnvValue(def: z.core.$ZodTypeDef): string | undefined;
|
|
15
|
+
export declare function isRequired(def: z.core.$ZodTypeDef): boolean;
|
|
16
|
+
//# sourceMappingURL=printer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"printer.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/printer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AACF,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,EAAE,OAAO,gBAAgB,CAAC,GAAG,CAAC,KAAG,WAWzE,CAAC;AAEF,wBAAgB,YAAY,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,MAAM,CAoE5D;AAuBD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,MAAM,GAAG,SAAS,CAwB9E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,OAAO,CAyB3D"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export const toExplanation = (key, value) => {
|
|
2
|
+
const def = value.schema.def;
|
|
3
|
+
return {
|
|
4
|
+
key,
|
|
5
|
+
description: value.description,
|
|
6
|
+
type: printZodType(def),
|
|
7
|
+
required: isRequired(def),
|
|
8
|
+
default: getDefaultEnvValue(def),
|
|
9
|
+
secret: value.secret === true,
|
|
10
|
+
examples: value.examples,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export function printZodType(def) {
|
|
14
|
+
switch (def.type) {
|
|
15
|
+
case "string":
|
|
16
|
+
case "number":
|
|
17
|
+
case "boolean":
|
|
18
|
+
case "transform":
|
|
19
|
+
return def.type;
|
|
20
|
+
case "enum":
|
|
21
|
+
if ("entries" in def) {
|
|
22
|
+
return Object.keys(def.entries)
|
|
23
|
+
.map((k) => k)
|
|
24
|
+
.join(" | ");
|
|
25
|
+
}
|
|
26
|
+
return "unknown";
|
|
27
|
+
case "literal":
|
|
28
|
+
if ("values" in def) {
|
|
29
|
+
return def.values[0].toString() + " (literal)";
|
|
30
|
+
}
|
|
31
|
+
return "unknown";
|
|
32
|
+
case "array":
|
|
33
|
+
if ("element" in def) {
|
|
34
|
+
return printZodType(def.element) + "[]";
|
|
35
|
+
}
|
|
36
|
+
return "[]";
|
|
37
|
+
case "optional":
|
|
38
|
+
if ("innerType" in def) {
|
|
39
|
+
return printZodType(def.innerType) + " | undefined";
|
|
40
|
+
}
|
|
41
|
+
return "unknown | undefined";
|
|
42
|
+
case "nullable":
|
|
43
|
+
if ("innerType" in def) {
|
|
44
|
+
return printZodType(def.innerType) + " | null";
|
|
45
|
+
}
|
|
46
|
+
return "unknown | null";
|
|
47
|
+
case "default":
|
|
48
|
+
if ("innerType" in def) {
|
|
49
|
+
const result = printZodType(def.innerType);
|
|
50
|
+
const defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : (def.defaultValue ?? undefined);
|
|
51
|
+
// const defaultValue = (def as any).defaultValue;
|
|
52
|
+
if (defaultValue) {
|
|
53
|
+
return result + " (default: " + defaultValue.toString() + ")";
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
return "unknown | null";
|
|
58
|
+
// z.union
|
|
59
|
+
case "union":
|
|
60
|
+
if ("options" in def) {
|
|
61
|
+
return def.options.map(printZodType).join(" | ");
|
|
62
|
+
}
|
|
63
|
+
return "unknown";
|
|
64
|
+
// z.transform
|
|
65
|
+
case "pipe": {
|
|
66
|
+
if (!("in" in def)) {
|
|
67
|
+
return "unknown (pipe)";
|
|
68
|
+
}
|
|
69
|
+
const result = printZodType(def.in);
|
|
70
|
+
return result + " (transform)";
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function stringifyEnvValue(value) {
|
|
77
|
+
if (value === undefined || value === null) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
84
|
+
return String(value);
|
|
85
|
+
}
|
|
86
|
+
// arrays / objects → JSON
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(value);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function getDefaultEnvValue(def) {
|
|
95
|
+
switch (def.type) {
|
|
96
|
+
case "default": {
|
|
97
|
+
const raw = typeof def.defaultValue === "function" ? def.defaultValue() : (def.defaultValue ?? undefined);
|
|
98
|
+
return stringifyEnvValue(raw);
|
|
99
|
+
}
|
|
100
|
+
// wrappers transparents
|
|
101
|
+
case "optional":
|
|
102
|
+
case "nullable":
|
|
103
|
+
if ("innerType" in def) {
|
|
104
|
+
return getDefaultEnvValue(def.innerType);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
case "pipe":
|
|
108
|
+
if ("in" in def) {
|
|
109
|
+
return getDefaultEnvValue(def.in);
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
default:
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function isRequired(def) {
|
|
117
|
+
switch (def.type) {
|
|
118
|
+
case "optional":
|
|
119
|
+
return false;
|
|
120
|
+
case "default":
|
|
121
|
+
return false;
|
|
122
|
+
// nullable n’enlève PAS le required
|
|
123
|
+
case "nullable":
|
|
124
|
+
if ("innerType" in def) {
|
|
125
|
+
return isRequired(def.innerType);
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
// pipe / transform → transparent côté required
|
|
129
|
+
case "pipe":
|
|
130
|
+
if ("in" in def) {
|
|
131
|
+
return isRequired(def.in);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
default:
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-schema.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/resolve-schema.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,KAAG,MA0BpD,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CANDIDATES = ["env.dnl.ts", "env.dnl.js", "dnl.config.ts", "dnl.config.js"];
|
|
4
|
+
export const resolveSchemaPath = (cliPath) => {
|
|
5
|
+
// 1. --schema
|
|
6
|
+
if (cliPath) {
|
|
7
|
+
return path.resolve(process.cwd(), cliPath);
|
|
8
|
+
}
|
|
9
|
+
// 2. package.json
|
|
10
|
+
const pkgPath = path.resolve(process.cwd(), "package.json");
|
|
11
|
+
if (fs.existsSync(pkgPath)) {
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
13
|
+
const schema = pkg?.["dotenv-never-lies"]?.schema;
|
|
14
|
+
if (schema) {
|
|
15
|
+
return path.resolve(process.cwd(), schema);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// 3. convention
|
|
19
|
+
for (const file of CANDIDATES) {
|
|
20
|
+
const full = path.resolve(process.cwd(), file);
|
|
21
|
+
console.log("full", full);
|
|
22
|
+
if (fs.existsSync(full)) {
|
|
23
|
+
return full;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error("No env schema found. Use --schema or define one in package.json.");
|
|
27
|
+
};
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Un objet contenant les variables d'environnement sous forme de string, issue d'un fichier .env ou de process.env.
|
|
4
|
+
*/
|
|
5
|
+
export type EnvSource = Record<string, string | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Une variable d'environnement.
|
|
8
|
+
*/
|
|
9
|
+
export interface EnvVarDefinition<T extends z.ZodType = z.ZodType> {
|
|
10
|
+
/**
|
|
11
|
+
* Le schéma Zod de la variable d'environnement.
|
|
12
|
+
*/
|
|
13
|
+
schema: T;
|
|
14
|
+
/**
|
|
15
|
+
* La description de la variable d'environnement.
|
|
16
|
+
*/
|
|
17
|
+
description: string;
|
|
18
|
+
/**
|
|
19
|
+
* Indique si la variable d'environnement est secrète (pour les token, les mots de passe), RFU.
|
|
20
|
+
*/
|
|
21
|
+
secret?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Indique des exemple pour cette variable
|
|
24
|
+
*/
|
|
25
|
+
examples?: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Un objet contenant les variables d'environnement définies.
|
|
29
|
+
*/
|
|
30
|
+
export type EnvDefinition = Record<string, EnvVarDefinition<any>>;
|
|
31
|
+
/**
|
|
32
|
+
* Le shape Zod du schéma d'environnement.
|
|
33
|
+
*/
|
|
34
|
+
export type ZodShapeFromEnv<T extends EnvDefinition> = {
|
|
35
|
+
[K in keyof T & string]: T[K]["schema"];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Le type inféré du schéma d'environnement.
|
|
39
|
+
*/
|
|
40
|
+
export type InferEnv<T extends EnvDefinition> = {
|
|
41
|
+
[K in keyof T & string]: z.infer<T[K]["schema"]>;
|
|
42
|
+
};
|
|
43
|
+
type CheckFn = (options?: {
|
|
44
|
+
source?: EnvSource | undefined;
|
|
45
|
+
}) => boolean;
|
|
46
|
+
type AssertFn<T extends EnvDefinition> = (options?: {
|
|
47
|
+
source?: EnvSource | undefined;
|
|
48
|
+
}) => InferEnv<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Un objet contenant les fonctions pour vérifier, charger et afficher les variables d'environnement.
|
|
51
|
+
* @template T - Le schéma d'environnement à définir.
|
|
52
|
+
*/
|
|
53
|
+
export type EnvDefinitionHelper<T extends EnvDefinition> = {
|
|
54
|
+
/**
|
|
55
|
+
* Le schéma d'environnement défini.
|
|
56
|
+
*/
|
|
57
|
+
def: T;
|
|
58
|
+
/**
|
|
59
|
+
* Le shape Zod du schéma d'environnement.
|
|
60
|
+
*/
|
|
61
|
+
zodShape: ZodShapeFromEnv<T>;
|
|
62
|
+
/**
|
|
63
|
+
* Le schéma Zod du schéma d'environnement.
|
|
64
|
+
*/
|
|
65
|
+
zodSchema: z.ZodObject<ZodShapeFromEnv<T>>;
|
|
66
|
+
/**
|
|
67
|
+
* Vérifie si les variables d'environnement sont valides sans lever d'exception.
|
|
68
|
+
*
|
|
69
|
+
* Si `options.source` est fourni, il est utilisé avec le mode strict (zod.strict())
|
|
70
|
+
* Si `options.source` est fourni, on utilise `process.env`, en mode non strict
|
|
71
|
+
*
|
|
72
|
+
* @param options - Les options pour vérifier les variables d'environnement.
|
|
73
|
+
* @returns true si les variables d'environnement sont valides, false sinon.
|
|
74
|
+
*/
|
|
75
|
+
check: CheckFn;
|
|
76
|
+
/**
|
|
77
|
+
* Vérifie les variables d'environnement et arrête le process si elle ne sont pas conformes au schéma.
|
|
78
|
+
*
|
|
79
|
+
* Si `options.source` est fourni, il est utilisé avec le mode strict (zod.strict())
|
|
80
|
+
* Si `options.source` est fourni, on utilise `process.env`, en mode non strict
|
|
81
|
+
*
|
|
82
|
+
* @param options - Les options pour charger les variables d'environnement.
|
|
83
|
+
* @returns Les variables d'environnement chargées.
|
|
84
|
+
* @throws Si les variables d'environnement sont invalides.
|
|
85
|
+
*/
|
|
86
|
+
assert: AssertFn<T>;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Définit un schéma d'environnement.
|
|
90
|
+
* @param def - Le schéma d'environnement à définir.
|
|
91
|
+
* @returns Un objet contenant les fonctions pour vérifier, charger et afficher les variables d'environnement.
|
|
92
|
+
*/
|
|
93
|
+
export declare const define: <T extends EnvDefinition>(def: T) => EnvDefinitionHelper<T>;
|
|
94
|
+
/**
|
|
95
|
+
* Lit un fichier .env et retourne les variables d'environnement sous forme d'objet.
|
|
96
|
+
* Utilise dotenv et dotenv-expand.
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const ENV = envDefinition.load({ source: readEnvFile(".env") });
|
|
100
|
+
* ```
|
|
101
|
+
* @param path - Le chemin vers le fichier .env.
|
|
102
|
+
* @returns Les variables d'environnement sous forme d'objet.
|
|
103
|
+
* @throws Si le fichier .env n'existe pas, ou n'est pas conforme
|
|
104
|
+
*/
|
|
105
|
+
export declare const readEnvFile: (path: string) => EnvSource;
|
|
106
|
+
export {};
|
|
107
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO;IAC7D;;OAEG;IACH,MAAM,EAAE,CAAC,CAAC;IACV;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAID;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,aAAa,IAAI;KAClD,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;CAC1C,CAAC;AACF;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,aAAa,IAAI;KAC3C,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CACnD,CAAC;AAEF,KAAK,OAAO,GAAG,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,KAAK,OAAO,CAAC;AACzE,KAAK,QAAQ,CAAC,CAAC,SAAS,aAAa,IAAI,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEvG;;;GAGG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,aAAa,IAAI;IACvD;;OAEG;IACH,GAAG,EAAE,CAAC,CAAC;IACP;;OAEG;IACH,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IAC7B;;OAEG;IACH,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C;;;;;;;;OAQG;IACH,KAAK,EAAE,OAAO,CAAC;IACf;;;;;;;;;OASG;IACH,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,aAAa,EAAE,KAAK,CAAC,KAAG,mBAAmB,CAAC,CAAC,CAuB7E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,SAa1C,CAAC"}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import dotenvExpand from "dotenv-expand";
|
|
4
|
+
import { EnvFileNotFoundError } from "./errors.js";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
/**
|
|
7
|
+
* Définit un schéma d'environnement.
|
|
8
|
+
* @param def - Le schéma d'environnement à définir.
|
|
9
|
+
* @returns Un objet contenant les fonctions pour vérifier, charger et afficher les variables d'environnement.
|
|
10
|
+
*/
|
|
11
|
+
export const define = (def) => {
|
|
12
|
+
const zodShape = Object.fromEntries(Object.entries(def).map(([key, value]) => [key, value.schema]));
|
|
13
|
+
const zodSchema = z.object(zodShape);
|
|
14
|
+
const strictSchema = zodSchema.strict();
|
|
15
|
+
const extractParams = (options) => {
|
|
16
|
+
const source = options?.source ?? process.env;
|
|
17
|
+
const strict = source !== process.env;
|
|
18
|
+
return { source, strict };
|
|
19
|
+
};
|
|
20
|
+
const getSchema = (strict) => (strict ? strictSchema : zodSchema);
|
|
21
|
+
const check = (options) => {
|
|
22
|
+
const { source, strict } = extractParams(options);
|
|
23
|
+
return getSchema(strict).safeParse(source).success;
|
|
24
|
+
};
|
|
25
|
+
const assert = (options) => {
|
|
26
|
+
const { source, strict } = extractParams(options);
|
|
27
|
+
return getSchema(strict).parse(source);
|
|
28
|
+
};
|
|
29
|
+
return { def, zodShape, zodSchema, check, assert };
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Lit un fichier .env et retourne les variables d'environnement sous forme d'objet.
|
|
33
|
+
* Utilise dotenv et dotenv-expand.
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const ENV = envDefinition.load({ source: readEnvFile(".env") });
|
|
37
|
+
* ```
|
|
38
|
+
* @param path - Le chemin vers le fichier .env.
|
|
39
|
+
* @returns Les variables d'environnement sous forme d'objet.
|
|
40
|
+
* @throws Si le fichier .env n'existe pas, ou n'est pas conforme
|
|
41
|
+
*/
|
|
42
|
+
export const readEnvFile = (path) => {
|
|
43
|
+
if (!fs.existsSync(path)) {
|
|
44
|
+
throw new EnvFileNotFoundError(path);
|
|
45
|
+
}
|
|
46
|
+
const content = fs.readFileSync(path);
|
|
47
|
+
const parsed = dotenv.parse(content);
|
|
48
|
+
dotenvExpand.expand({
|
|
49
|
+
processEnv: {}, // important, else, process.env is mutated
|
|
50
|
+
parsed,
|
|
51
|
+
});
|
|
52
|
+
return parsed;
|
|
53
|
+
};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,oBAAqB,SAAQ,KAAK;gBAC/B,IAAI,EAAE,MAAM;CAI3B"}
|
package/dist/errors.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,WAAW,CAAC;AASjC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,CAAC;AACf,eAAe,GAAG,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as dnl from "./core.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
if (!("ZodFirstPartyTypeKind" in z)) {
|
|
4
|
+
throw new Error("dotenv-never-lies requires Zod v4+. Detected an incompatible Zod version. " + "This library exposes Zod schemas as part of its public API.");
|
|
5
|
+
}
|
|
6
|
+
export * from "./core.js";
|
|
7
|
+
export { dnl };
|
|
8
|
+
export default dnl;
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@romaintaillandier1978/dotenv-never-lies",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Typed, validated, and explicit environment variables — powered by Zod.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Romain TAILLANDIER",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/romaintaillandier1978/dotenv-never-lies"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"env",
|
|
14
|
+
"environment",
|
|
15
|
+
"dotenv",
|
|
16
|
+
"configuration",
|
|
17
|
+
"zod",
|
|
18
|
+
"validation",
|
|
19
|
+
"typesafe",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./cli": {
|
|
38
|
+
"types": "./dist/cli/index.d.ts",
|
|
39
|
+
"import": "./dist/cli/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"dnl": "./dist/cli/index.js"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"prepublishOnly": "npm test && npm run build",
|
|
48
|
+
"test": "yarn build && node dist/DnlTest.js",
|
|
49
|
+
"dev": "tsx src/cli/index.ts",
|
|
50
|
+
"devold": "yarn build && node dist/cli/index.js",
|
|
51
|
+
"gen": "rm -rf dist; rm dotenv-never-lies-v*.tgz; yarn build && yarn pack"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"zod": ">=4.2.1"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"commander": "^14.0.2",
|
|
58
|
+
"dotenv": "^17.2.3",
|
|
59
|
+
"dotenv-expand": "^12.0.3",
|
|
60
|
+
"esbuild": "^0.27.2",
|
|
61
|
+
"tsx": "^4.21.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^25.0.3",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
66
|
+
"@typescript-eslint/parser": "^8.50.0",
|
|
67
|
+
"eslint": "^9.39.2",
|
|
68
|
+
"prettier": "^3.7.4",
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"zod": "^4.2.1"
|
|
71
|
+
}
|
|
72
|
+
}
|