@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/DnlTest.d.ts +2 -0
  4. package/dist/DnlTest.d.ts.map +1 -0
  5. package/dist/DnlTest.js +12 -0
  6. package/dist/cli/commands/assert.d.ts +5 -0
  7. package/dist/cli/commands/assert.d.ts.map +1 -0
  8. package/dist/cli/commands/assert.js +27 -0
  9. package/dist/cli/commands/explain.d.ts +8 -0
  10. package/dist/cli/commands/explain.d.ts.map +1 -0
  11. package/dist/cli/commands/explain.js +55 -0
  12. package/dist/cli/commands/generate.d.ts +7 -0
  13. package/dist/cli/commands/generate.d.ts.map +1 -0
  14. package/dist/cli/commands/generate.js +28 -0
  15. package/dist/cli/commands/reverseEnv.d.ts +7 -0
  16. package/dist/cli/commands/reverseEnv.d.ts.map +1 -0
  17. package/dist/cli/commands/reverseEnv.js +33 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +127 -0
  21. package/dist/cli/utils/infer-schema.d.ts +3 -0
  22. package/dist/cli/utils/infer-schema.d.ts.map +1 -0
  23. package/dist/cli/utils/infer-schema.js +24 -0
  24. package/dist/cli/utils/load-schema.d.ts +3 -0
  25. package/dist/cli/utils/load-schema.d.ts.map +1 -0
  26. package/dist/cli/utils/load-schema.js +22 -0
  27. package/dist/cli/utils/printer.d.ts +16 -0
  28. package/dist/cli/utils/printer.d.ts.map +1 -0
  29. package/dist/cli/utils/printer.js +137 -0
  30. package/dist/cli/utils/resolve-schema.d.ts +2 -0
  31. package/dist/cli/utils/resolve-schema.d.ts.map +1 -0
  32. package/dist/cli/utils/resolve-schema.js +27 -0
  33. package/dist/core.d.ts +107 -0
  34. package/dist/core.d.ts.map +1 -0
  35. package/dist/core.js +53 -0
  36. package/dist/errors.d.ts +4 -0
  37. package/dist/errors.d.ts.map +1 -0
  38. package/dist/errors.js +6 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +8 -0
  42. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=DnlTest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DnlTest.d.ts","sourceRoot":"","sources":["../src/DnlTest.ts"],"names":[],"mappings":""}
@@ -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,5 @@
1
+ export declare const assertCommand: (opts: {
2
+ schema: string;
3
+ source: string;
4
+ }) => Promise<void>;
5
+ //# sourceMappingURL=assert.d.ts.map
@@ -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,7 @@
1
+ export declare const generateCommand: (opts: {
2
+ schema?: string;
3
+ out?: string;
4
+ includeSecret?: boolean;
5
+ force?: boolean;
6
+ }) => Promise<void>;
7
+ //# sourceMappingURL=generate.d.ts.map
@@ -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,7 @@
1
+ export declare const reverseEnvCommand: (opts: {
2
+ source?: string;
3
+ out?: string;
4
+ force?: boolean;
5
+ guessSecret?: boolean;
6
+ }) => Promise<void>;
7
+ //# sourceMappingURL=reverseEnv.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ import { EnvDefinitionHelper } from "../../core.js";
2
+ export declare const loadDef: (schemaPath: string) => Promise<EnvDefinitionHelper<any>>;
3
+ //# sourceMappingURL=load-schema.d.ts.map
@@ -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,2 @@
1
+ export declare const resolveSchemaPath: (cliPath?: string) => string;
2
+ //# sourceMappingURL=resolve-schema.d.ts.map
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ export declare class EnvFileNotFoundError extends Error {
2
+ constructor(path: string);
3
+ }
4
+ //# sourceMappingURL=errors.d.ts.map
@@ -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
@@ -0,0 +1,6 @@
1
+ export class EnvFileNotFoundError extends Error {
2
+ constructor(path) {
3
+ super(`Env file not found: ${path}`);
4
+ this.name = "EnvFileNotFoundError";
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ import * as dnl from "./core.js";
2
+ export * from "./core.js";
3
+ export { dnl };
4
+ export default dnl;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }