@xlameiro/env-typegen 0.1.2 → 0.1.4
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/CHANGELOG.md +18 -0
- package/README.md +97 -54
- package/dist/cli.js +1448 -169
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1716 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +537 -1
- package/dist/index.d.ts +537 -1
- package/dist/index.js +1704 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -99,25 +99,38 @@ var require_picocolors = __commonJS({
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
// src/cli.ts
|
|
102
|
-
import { createRequire } from "module";
|
|
103
102
|
import { realpathSync } from "fs";
|
|
104
|
-
import
|
|
105
|
-
import
|
|
106
|
-
import {
|
|
103
|
+
import { createRequire } from "module";
|
|
104
|
+
import path13 from "path";
|
|
105
|
+
import { fileURLToPath, pathToFileURL as pathToFileURL5 } from "url";
|
|
106
|
+
import { inspect, parseArgs as parseArgs2 } from "util";
|
|
107
107
|
|
|
108
108
|
// src/config.ts
|
|
109
109
|
import { existsSync } from "fs";
|
|
110
110
|
import path from "path";
|
|
111
111
|
import { pathToFileURL } from "url";
|
|
112
112
|
var CONFIG_FILE_NAMES = [
|
|
113
|
-
"env-typegen.config.ts",
|
|
114
113
|
"env-typegen.config.mjs",
|
|
115
|
-
"env-typegen.config.js"
|
|
114
|
+
"env-typegen.config.js",
|
|
115
|
+
"env-typegen.config.ts"
|
|
116
116
|
];
|
|
117
117
|
async function loadConfig(cwd = process.cwd()) {
|
|
118
118
|
for (const name of CONFIG_FILE_NAMES) {
|
|
119
119
|
const filePath = path.resolve(cwd, name);
|
|
120
120
|
if (existsSync(filePath)) {
|
|
121
|
+
if (filePath.endsWith(".ts")) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
|
|
124
|
+
Rename it to "env-typegen.config.mjs" and use ESM export syntax:
|
|
125
|
+
|
|
126
|
+
// env-typegen.config.mjs
|
|
127
|
+
import { defineConfig } from "@xlameiro/env-typegen";
|
|
128
|
+
export default defineConfig({ input: ".env.example" });
|
|
129
|
+
|
|
130
|
+
Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
|
|
131
|
+
env-typegen.config.mjs for runtime loading.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
121
134
|
const fileUrl = pathToFileURL(filePath).href;
|
|
122
135
|
const mod = await import(fileUrl);
|
|
123
136
|
return mod.default;
|
|
@@ -133,12 +146,13 @@ import path5 from "path";
|
|
|
133
146
|
import path2 from "path";
|
|
134
147
|
function generateDeclaration(parsed) {
|
|
135
148
|
const fileName = path2.basename(parsed.filePath);
|
|
136
|
-
const lines = [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
const lines = [
|
|
150
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
151
|
+
`// Source: ${fileName}`,
|
|
152
|
+
"",
|
|
153
|
+
"declare namespace NodeJS {",
|
|
154
|
+
" interface ProcessEnv {"
|
|
155
|
+
];
|
|
142
156
|
for (const variable of parsed.vars) {
|
|
143
157
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
144
158
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -153,12 +167,14 @@ function generateDeclaration(parsed) {
|
|
|
153
167
|
}
|
|
154
168
|
lines.push(propLine);
|
|
155
169
|
}
|
|
156
|
-
lines.push(" }");
|
|
157
|
-
lines.push("}");
|
|
170
|
+
lines.push(" }", "}");
|
|
158
171
|
return lines.join("\n") + "\n";
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
// src/generators/t3-generator.ts
|
|
175
|
+
function escapeJsStringLiteral(value) {
|
|
176
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
|
|
177
|
+
}
|
|
162
178
|
function toT3ZodType(envVarType) {
|
|
163
179
|
if (envVarType === "number") return "z.coerce.number()";
|
|
164
180
|
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
@@ -166,51 +182,47 @@ function toT3ZodType(envVarType) {
|
|
|
166
182
|
if (envVarType === "email") return "z.string().email()";
|
|
167
183
|
return "z.string()";
|
|
168
184
|
}
|
|
185
|
+
function buildZodExpr(variable) {
|
|
186
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
187
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
188
|
+
if (variable.description !== void 0) {
|
|
189
|
+
zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
|
|
190
|
+
}
|
|
191
|
+
if (variable.isOptional) {
|
|
192
|
+
zodExpr += ".optional()";
|
|
193
|
+
}
|
|
194
|
+
return zodExpr;
|
|
195
|
+
}
|
|
169
196
|
function generateT3Env(parsed) {
|
|
170
197
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
171
198
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
172
|
-
const lines = [
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
const lines = [
|
|
200
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
201
|
+
'import { createEnv } from "@t3-oss/env-nextjs";',
|
|
202
|
+
'import { z } from "zod";',
|
|
203
|
+
"",
|
|
204
|
+
"export const env = createEnv({"
|
|
205
|
+
];
|
|
178
206
|
if (serverVars.length > 0) {
|
|
179
|
-
lines.push(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
185
|
-
}
|
|
186
|
-
if (variable.isOptional) {
|
|
187
|
-
zodExpr += ".optional()";
|
|
188
|
-
}
|
|
189
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
190
|
-
}
|
|
191
|
-
lines.push(" },");
|
|
207
|
+
lines.push(
|
|
208
|
+
" server: {",
|
|
209
|
+
...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
210
|
+
" },"
|
|
211
|
+
);
|
|
192
212
|
}
|
|
193
213
|
if (clientVars.length > 0) {
|
|
194
|
-
lines.push(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
200
|
-
}
|
|
201
|
-
if (variable.isOptional) {
|
|
202
|
-
zodExpr += ".optional()";
|
|
203
|
-
}
|
|
204
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
205
|
-
}
|
|
206
|
-
lines.push(" },");
|
|
207
|
-
}
|
|
208
|
-
lines.push(" runtimeEnv: {");
|
|
209
|
-
for (const variable of parsed.vars) {
|
|
210
|
-
lines.push(` ${variable.key}: process.env.${variable.key},`);
|
|
214
|
+
lines.push(
|
|
215
|
+
" client: {",
|
|
216
|
+
...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
217
|
+
" },"
|
|
218
|
+
);
|
|
211
219
|
}
|
|
212
|
-
lines.push(
|
|
213
|
-
|
|
220
|
+
lines.push(
|
|
221
|
+
" runtimeEnv: {",
|
|
222
|
+
...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
|
|
223
|
+
" },",
|
|
224
|
+
"});"
|
|
225
|
+
);
|
|
214
226
|
return lines.join("\n") + "\n";
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -227,12 +239,14 @@ function generateTypeScriptTypes(parsed) {
|
|
|
227
239
|
const fileName = path3.basename(parsed.filePath);
|
|
228
240
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
229
241
|
const lines = [];
|
|
230
|
-
lines.push(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
lines.push(
|
|
243
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
244
|
+
`// Source: ${fileName}`,
|
|
245
|
+
`// Generated at: ${timestamp}`,
|
|
246
|
+
"",
|
|
247
|
+
"declare namespace NodeJS {",
|
|
248
|
+
" interface ProcessEnv {"
|
|
249
|
+
);
|
|
236
250
|
for (const variable of parsed.vars) {
|
|
237
251
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
238
252
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -247,10 +261,7 @@ function generateTypeScriptTypes(parsed) {
|
|
|
247
261
|
}
|
|
248
262
|
lines.push(propLine);
|
|
249
263
|
}
|
|
250
|
-
lines.push(" }");
|
|
251
|
-
lines.push("}");
|
|
252
|
-
lines.push("");
|
|
253
|
-
lines.push("export type EnvVars = {");
|
|
264
|
+
lines.push(" }", "}", "", "export type EnvVars = {");
|
|
254
265
|
for (const variable of parsed.vars) {
|
|
255
266
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
256
267
|
const tsType = toTsType(effectiveType);
|
|
@@ -260,9 +271,11 @@ function generateTypeScriptTypes(parsed) {
|
|
|
260
271
|
lines.push("};");
|
|
261
272
|
if (hasClientVars) {
|
|
262
273
|
const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
|
|
263
|
-
lines.push(
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
lines.push(
|
|
275
|
+
"",
|
|
276
|
+
`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
|
|
277
|
+
`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
|
|
278
|
+
);
|
|
266
279
|
}
|
|
267
280
|
return lines.join("\n") + "\n";
|
|
268
281
|
}
|
|
@@ -279,27 +292,29 @@ function generateZodSchema(parsed) {
|
|
|
279
292
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
280
293
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
281
294
|
const lines = [];
|
|
282
|
-
lines.push(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
lines.push(
|
|
296
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
297
|
+
'import { z } from "zod";',
|
|
298
|
+
"",
|
|
299
|
+
"export const serverEnvSchema = z.object({"
|
|
300
|
+
);
|
|
286
301
|
for (const variable of serverVars) {
|
|
287
302
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
288
303
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
289
304
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
290
305
|
}
|
|
291
|
-
lines.push("});");
|
|
292
|
-
lines.push("");
|
|
293
|
-
lines.push("export const clientEnvSchema = z.object({");
|
|
306
|
+
lines.push("});", "", "export const clientEnvSchema = z.object({");
|
|
294
307
|
for (const variable of clientVars) {
|
|
295
308
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
296
309
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
297
310
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
298
311
|
}
|
|
299
|
-
lines.push(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
lines.push(
|
|
313
|
+
"});",
|
|
314
|
+
"",
|
|
315
|
+
"export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
|
|
316
|
+
"export type Env = z.infer<typeof envSchema>;"
|
|
317
|
+
);
|
|
303
318
|
return lines.join("\n") + "\n";
|
|
304
319
|
}
|
|
305
320
|
|
|
@@ -359,7 +374,9 @@ var inferenceRules = [
|
|
|
359
374
|
{
|
|
360
375
|
id: "P8_numeric_literal",
|
|
361
376
|
priority: 8,
|
|
362
|
-
|
|
377
|
+
// Non-capturing group with \d keeps the dot/digit boundary unambiguous,
|
|
378
|
+
// eliminating super-linear backtracking (ReDoS-safe).
|
|
379
|
+
match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
|
|
363
380
|
type: "number"
|
|
364
381
|
},
|
|
365
382
|
{
|
|
@@ -377,7 +394,9 @@ var inferenceRules = [
|
|
|
377
394
|
{
|
|
378
395
|
id: "P11_email_literal",
|
|
379
396
|
priority: 11,
|
|
380
|
-
|
|
397
|
+
// Dots are excluded from each domain-segment character class so that the
|
|
398
|
+
// literal \. separators are unambiguous, preventing super-linear backtracking.
|
|
399
|
+
match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
|
|
381
400
|
type: "email"
|
|
382
401
|
},
|
|
383
402
|
{
|
|
@@ -422,35 +441,122 @@ var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
422
441
|
function isEnvVarType(value) {
|
|
423
442
|
return VALID_ENV_VAR_TYPES.has(value);
|
|
424
443
|
}
|
|
444
|
+
function applyTypeAnnotation(state, content) {
|
|
445
|
+
const typeStr = content.slice("@type ".length).trim();
|
|
446
|
+
if (isEnvVarType(typeStr)) state.annotatedType = typeStr;
|
|
447
|
+
}
|
|
448
|
+
function applyEnumAnnotation(state, content) {
|
|
449
|
+
const values = content.slice("@enum ".length).trim().split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
450
|
+
if (values.length > 0) state.enumValues = values;
|
|
451
|
+
}
|
|
452
|
+
function applyMinAnnotation(state, content) {
|
|
453
|
+
const num = Number(content.slice("@min ".length).trim());
|
|
454
|
+
if (Number.isFinite(num)) state.minConstraint = num;
|
|
455
|
+
}
|
|
456
|
+
function applyMaxAnnotation(state, content) {
|
|
457
|
+
const num = Number(content.slice("@max ".length).trim());
|
|
458
|
+
if (Number.isFinite(num)) state.maxConstraint = num;
|
|
459
|
+
}
|
|
460
|
+
function applyRuntimeAnnotation(state, content) {
|
|
461
|
+
const scope = content.slice("@runtime ".length).trim();
|
|
462
|
+
if (scope === "server" || scope === "client" || scope === "edge") {
|
|
463
|
+
state.runtime = scope;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function processAnnotationContent(state, content) {
|
|
467
|
+
const trimmed = content.trim();
|
|
468
|
+
if (trimmed === "@required") {
|
|
469
|
+
state.isRequired = true;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (trimmed === "@secret") {
|
|
473
|
+
state.isSecret = true;
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (trimmed === "@optional") return;
|
|
477
|
+
if (content.startsWith("@description ")) {
|
|
478
|
+
state.description = content.slice("@description ".length).trim();
|
|
479
|
+
} else if (content.startsWith("@type ")) {
|
|
480
|
+
applyTypeAnnotation(state, content);
|
|
481
|
+
} else if (content.startsWith("@enum ")) {
|
|
482
|
+
applyEnumAnnotation(state, content);
|
|
483
|
+
} else if (content.startsWith("@min ")) {
|
|
484
|
+
applyMinAnnotation(state, content);
|
|
485
|
+
} else if (content.startsWith("@max ")) {
|
|
486
|
+
applyMaxAnnotation(state, content);
|
|
487
|
+
} else if (content.startsWith("@runtime ")) {
|
|
488
|
+
applyRuntimeAnnotation(state, content);
|
|
489
|
+
} else if (state.description === void 0 && trimmed.length > 0) {
|
|
490
|
+
state.description = trimmed;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
425
493
|
function parseCommentBlock(lines) {
|
|
426
|
-
|
|
427
|
-
let description;
|
|
428
|
-
let isRequired = false;
|
|
494
|
+
const state = { isRequired: false };
|
|
429
495
|
for (const line of lines) {
|
|
430
|
-
|
|
431
|
-
if (content.startsWith("@description ")) {
|
|
432
|
-
description = content.slice("@description ".length).trim();
|
|
433
|
-
} else if (content.startsWith("@type ")) {
|
|
434
|
-
const typeStr = content.slice("@type ".length).trim();
|
|
435
|
-
if (isEnvVarType(typeStr)) {
|
|
436
|
-
annotatedType = typeStr;
|
|
437
|
-
}
|
|
438
|
-
} else if (content.trim() === "@required") {
|
|
439
|
-
isRequired = true;
|
|
440
|
-
} else if (content.trim() === "@optional") {
|
|
441
|
-
} else if (description === void 0 && content.trim().length > 0) {
|
|
442
|
-
description = content.trim();
|
|
443
|
-
}
|
|
496
|
+
processAnnotationContent(state, line.replace(/^#\s*/, "").trimEnd());
|
|
444
497
|
}
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
498
|
+
let constraints;
|
|
499
|
+
if (state.minConstraint !== void 0 || state.maxConstraint !== void 0) {
|
|
500
|
+
constraints = {};
|
|
501
|
+
if (state.minConstraint !== void 0) constraints.min = state.minConstraint;
|
|
502
|
+
if (state.maxConstraint !== void 0) constraints.max = state.maxConstraint;
|
|
503
|
+
}
|
|
504
|
+
const result = { isRequired: state.isRequired };
|
|
505
|
+
if (state.annotatedType !== void 0) result.annotatedType = state.annotatedType;
|
|
506
|
+
if (state.description !== void 0) result.description = state.description;
|
|
507
|
+
if (state.enumValues !== void 0) result.enumValues = state.enumValues;
|
|
508
|
+
if (constraints !== void 0) result.constraints = constraints;
|
|
509
|
+
if (state.runtime !== void 0) result.runtime = state.runtime;
|
|
510
|
+
if (state.isSecret !== void 0) result.isSecret = state.isSecret;
|
|
448
511
|
return result;
|
|
449
512
|
}
|
|
450
513
|
|
|
451
514
|
// src/parser/env-parser.ts
|
|
452
515
|
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
453
|
-
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(
|
|
516
|
+
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
|
|
517
|
+
function buildParsedVar(params, commentBlock, options) {
|
|
518
|
+
const annotations = parseCommentBlock(commentBlock);
|
|
519
|
+
const extraRules = options?.inferenceRules;
|
|
520
|
+
const inferredType = inferType(
|
|
521
|
+
params.key,
|
|
522
|
+
params.rawValue,
|
|
523
|
+
...extraRules === void 0 ? [] : [{ extraRules }]
|
|
524
|
+
);
|
|
525
|
+
const isRequired = params.rawValue.length > 0 || annotations.isRequired;
|
|
526
|
+
const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
|
|
527
|
+
const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
|
|
528
|
+
const parsedVar = {
|
|
529
|
+
key: params.key,
|
|
530
|
+
rawValue: params.rawValue,
|
|
531
|
+
inferredType,
|
|
532
|
+
isRequired,
|
|
533
|
+
isOptional,
|
|
534
|
+
isClientSide,
|
|
535
|
+
lineNumber: params.lineNumber
|
|
536
|
+
};
|
|
537
|
+
if (annotations.annotatedType !== void 0) {
|
|
538
|
+
parsedVar.annotatedType = annotations.annotatedType;
|
|
539
|
+
}
|
|
540
|
+
if (annotations.description !== void 0) {
|
|
541
|
+
parsedVar.description = annotations.description;
|
|
542
|
+
}
|
|
543
|
+
if (params.currentGroup !== void 0) {
|
|
544
|
+
parsedVar.group = params.currentGroup;
|
|
545
|
+
}
|
|
546
|
+
if (annotations.enumValues !== void 0) {
|
|
547
|
+
parsedVar.enumValues = annotations.enumValues;
|
|
548
|
+
}
|
|
549
|
+
if (annotations.constraints !== void 0) {
|
|
550
|
+
parsedVar.constraints = annotations.constraints;
|
|
551
|
+
}
|
|
552
|
+
if (annotations.runtime !== void 0) {
|
|
553
|
+
parsedVar.runtime = annotations.runtime;
|
|
554
|
+
}
|
|
555
|
+
if (annotations.isSecret !== void 0) {
|
|
556
|
+
parsedVar.isSecret = annotations.isSecret;
|
|
557
|
+
}
|
|
558
|
+
return parsedVar;
|
|
559
|
+
}
|
|
454
560
|
function parseEnvFileContent(content, filePath, options) {
|
|
455
561
|
const lines = content.split("\n");
|
|
456
562
|
const vars = [];
|
|
@@ -480,44 +586,25 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
480
586
|
continue;
|
|
481
587
|
}
|
|
482
588
|
const envMatch = ENV_VAR_RE.exec(trimmed);
|
|
483
|
-
if (envMatch
|
|
484
|
-
const key = envMatch[1] ?? "";
|
|
485
|
-
const rawValue = envMatch[2] ?? "";
|
|
486
|
-
const annotations = parseCommentBlock(commentBlock);
|
|
487
|
-
const inferredType = inferType(
|
|
488
|
-
key,
|
|
489
|
-
rawValue,
|
|
490
|
-
...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
|
|
491
|
-
);
|
|
492
|
-
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
493
|
-
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
494
|
-
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
495
|
-
const parsedVar = {
|
|
496
|
-
key,
|
|
497
|
-
rawValue,
|
|
498
|
-
inferredType,
|
|
499
|
-
isRequired,
|
|
500
|
-
isOptional,
|
|
501
|
-
isClientSide,
|
|
502
|
-
lineNumber
|
|
503
|
-
};
|
|
504
|
-
if (annotations.annotatedType !== void 0) {
|
|
505
|
-
parsedVar.annotatedType = annotations.annotatedType;
|
|
506
|
-
}
|
|
507
|
-
if (annotations.description !== void 0) {
|
|
508
|
-
parsedVar.description = annotations.description;
|
|
509
|
-
}
|
|
510
|
-
if (currentGroup !== void 0) {
|
|
511
|
-
parsedVar.group = currentGroup;
|
|
512
|
-
}
|
|
513
|
-
vars.push(parsedVar);
|
|
514
|
-
commentBlock = [];
|
|
515
|
-
} else {
|
|
589
|
+
if (envMatch === null) {
|
|
516
590
|
commentBlock = [];
|
|
591
|
+
continue;
|
|
517
592
|
}
|
|
593
|
+
vars.push(
|
|
594
|
+
buildParsedVar(
|
|
595
|
+
{ key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
|
|
596
|
+
commentBlock,
|
|
597
|
+
options
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
commentBlock = [];
|
|
518
601
|
}
|
|
519
602
|
return { filePath, vars, groups };
|
|
520
603
|
}
|
|
604
|
+
function parseEnvFile(filePath) {
|
|
605
|
+
const content = readFileSync(filePath, "utf8");
|
|
606
|
+
return parseEnvFileContent(content, filePath);
|
|
607
|
+
}
|
|
521
608
|
|
|
522
609
|
// src/utils/file.ts
|
|
523
610
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -630,7 +717,7 @@ async function runGenerate(options) {
|
|
|
630
717
|
const parsed = parseEnvFileContent(
|
|
631
718
|
content,
|
|
632
719
|
inputPath,
|
|
633
|
-
inferenceRules2
|
|
720
|
+
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
634
721
|
);
|
|
635
722
|
for (const generator of generators) {
|
|
636
723
|
let generated = buildOutput(generator, parsed);
|
|
@@ -651,9 +738,1173 @@ async function runGenerate(options) {
|
|
|
651
738
|
}
|
|
652
739
|
}
|
|
653
740
|
|
|
654
|
-
// src/
|
|
741
|
+
// src/validation-command.ts
|
|
742
|
+
import path11 from "path";
|
|
743
|
+
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
744
|
+
import { parseArgs } from "util";
|
|
745
|
+
|
|
746
|
+
// src/cloud/connectors.ts
|
|
747
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
655
748
|
import path6 from "path";
|
|
749
|
+
function isRecord(value) {
|
|
750
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
751
|
+
}
|
|
752
|
+
function readEntryValue(entry, keys) {
|
|
753
|
+
for (const key of keys) {
|
|
754
|
+
const value = entry[key];
|
|
755
|
+
if (typeof value === "string") return value;
|
|
756
|
+
}
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
function getVercelEntries(value) {
|
|
760
|
+
if (Array.isArray(value)) return value;
|
|
761
|
+
if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
function parseVercelPayload(value) {
|
|
765
|
+
const entries = getVercelEntries(value);
|
|
766
|
+
const result = {};
|
|
767
|
+
for (const entry of entries) {
|
|
768
|
+
if (!isRecord(entry)) continue;
|
|
769
|
+
const key = readEntryValue(entry, ["key", "name"]);
|
|
770
|
+
if (key === void 0) continue;
|
|
771
|
+
const envValue = readEntryValue(entry, ["value", "targetValue", "content"]) ?? "";
|
|
772
|
+
result[key] = envValue;
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
function getCloudflareEntries(value) {
|
|
777
|
+
if (Array.isArray(value)) return value;
|
|
778
|
+
if (isRecord(value) && Array.isArray(value.result)) return value.result;
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
function parseCloudflarePayload(value) {
|
|
782
|
+
const entries = getCloudflareEntries(value);
|
|
783
|
+
const result = {};
|
|
784
|
+
for (const entry of entries) {
|
|
785
|
+
if (!isRecord(entry)) continue;
|
|
786
|
+
const key = readEntryValue(entry, ["name", "key"]);
|
|
787
|
+
if (key === void 0) continue;
|
|
788
|
+
const envValue = readEntryValue(entry, ["text", "value", "secret"]) ?? "";
|
|
789
|
+
result[key] = envValue;
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
function parseAwsPayload(value) {
|
|
794
|
+
const entries = isRecord(value) && Array.isArray(value.Parameters) ? value.Parameters : [];
|
|
795
|
+
const result = {};
|
|
796
|
+
for (const entry of entries) {
|
|
797
|
+
if (!isRecord(entry)) continue;
|
|
798
|
+
const name = readEntryValue(entry, ["Name", "name"]);
|
|
799
|
+
if (name === void 0) continue;
|
|
800
|
+
const key = name.split("/").findLast((part) => part.length > 0) ?? name;
|
|
801
|
+
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
802
|
+
result[key] = envValue;
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
function parseProviderPayload(provider, value) {
|
|
807
|
+
if (provider === "vercel") return parseVercelPayload(value);
|
|
808
|
+
if (provider === "cloudflare") return parseCloudflarePayload(value);
|
|
809
|
+
return parseAwsPayload(value);
|
|
810
|
+
}
|
|
811
|
+
async function loadCloudSource(options) {
|
|
812
|
+
const resolvedPath = path6.resolve(options.filePath);
|
|
813
|
+
const raw = await readFile2(resolvedPath, "utf8");
|
|
814
|
+
const parsed = JSON.parse(raw);
|
|
815
|
+
return parseProviderPayload(options.provider, parsed);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/contract.ts
|
|
819
|
+
import { existsSync as existsSync2 } from "fs";
|
|
820
|
+
import path7 from "path";
|
|
821
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
822
|
+
var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
|
|
823
|
+
var CONTRACT_FILE_NAMES = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
|
|
824
|
+
function isRecord2(value) {
|
|
825
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
826
|
+
}
|
|
827
|
+
function isExpected(value) {
|
|
828
|
+
if (!isRecord2(value)) return false;
|
|
829
|
+
const typeValue = value.type;
|
|
830
|
+
if (typeof typeValue !== "string") return false;
|
|
831
|
+
const validTypes = /* @__PURE__ */ new Set([
|
|
832
|
+
"string",
|
|
833
|
+
"number",
|
|
834
|
+
"boolean",
|
|
835
|
+
"enum",
|
|
836
|
+
"url",
|
|
837
|
+
"email",
|
|
838
|
+
"json",
|
|
839
|
+
"semver",
|
|
840
|
+
"unknown"
|
|
841
|
+
]);
|
|
842
|
+
if (!validTypes.has(typeValue)) return false;
|
|
843
|
+
if (typeValue === "enum") {
|
|
844
|
+
return Array.isArray(value.values) && value.values.every((item) => typeof item === "string");
|
|
845
|
+
}
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
function isEnvContractVariable(value) {
|
|
849
|
+
if (!isRecord2(value)) return false;
|
|
850
|
+
if (!isExpected(value.expected)) return false;
|
|
851
|
+
if (typeof value.required !== "boolean") return false;
|
|
852
|
+
if (typeof value.clientSide !== "boolean") return false;
|
|
853
|
+
if (value.description !== void 0 && typeof value.description !== "string") return false;
|
|
854
|
+
if (value.secret !== void 0 && typeof value.secret !== "boolean") return false;
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
function isEnvContract(value) {
|
|
858
|
+
if (!isRecord2(value)) return false;
|
|
859
|
+
if (value.schemaVersion !== 1) return false;
|
|
860
|
+
if (!isRecord2(value.variables)) return false;
|
|
861
|
+
return Object.values(value.variables).every((item) => isEnvContractVariable(item));
|
|
862
|
+
}
|
|
863
|
+
function isLegacyContract(value) {
|
|
864
|
+
if (!isRecord2(value)) return false;
|
|
865
|
+
if (!Array.isArray(value.vars)) return false;
|
|
866
|
+
return value.vars.every((entry) => isRecord2(entry) && typeof entry.name === "string");
|
|
867
|
+
}
|
|
868
|
+
function mapEnvVarTypeToExpected(type) {
|
|
869
|
+
if (type === "number") return { type: "number" };
|
|
870
|
+
if (type === "boolean") return { type: "boolean" };
|
|
871
|
+
if (type === "url") return { type: "url" };
|
|
872
|
+
if (type === "email") return { type: "email" };
|
|
873
|
+
if (type === "json") return { type: "json" };
|
|
874
|
+
if (type === "semver") return { type: "semver" };
|
|
875
|
+
if (type === "unknown") return { type: "unknown" };
|
|
876
|
+
return { type: "string" };
|
|
877
|
+
}
|
|
878
|
+
function shouldTreatAsSecret(key) {
|
|
879
|
+
return SECRET_KEY_RE.test(key);
|
|
880
|
+
}
|
|
881
|
+
function toExpectedFromLegacyEntry(entry) {
|
|
882
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
883
|
+
return { type: "enum", values: entry.enumValues };
|
|
884
|
+
}
|
|
885
|
+
if (entry.expectedType === "number") {
|
|
886
|
+
return {
|
|
887
|
+
type: "number",
|
|
888
|
+
...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
|
|
889
|
+
...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (entry.expectedType === "boolean") return { type: "boolean" };
|
|
893
|
+
if (entry.expectedType === "url") return { type: "url" };
|
|
894
|
+
if (entry.expectedType === "email") return { type: "email" };
|
|
895
|
+
if (entry.expectedType === "json") return { type: "json" };
|
|
896
|
+
if (entry.expectedType === "semver") return { type: "semver" };
|
|
897
|
+
if (entry.expectedType === "unknown") return { type: "unknown" };
|
|
898
|
+
return { type: "string" };
|
|
899
|
+
}
|
|
900
|
+
function convertLegacyContract(contract) {
|
|
901
|
+
const variables = {};
|
|
902
|
+
for (const entry of contract.vars) {
|
|
903
|
+
const clientSide = entry.runtime === "client" || entry.name.startsWith("NEXT_PUBLIC_");
|
|
904
|
+
variables[entry.name] = {
|
|
905
|
+
expected: toExpectedFromLegacyEntry(entry),
|
|
906
|
+
required: entry.required,
|
|
907
|
+
clientSide,
|
|
908
|
+
...entry.description !== void 0 && { description: entry.description },
|
|
909
|
+
...(entry.isSecret ?? shouldTreatAsSecret(entry.name)) && { secret: true }
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
schemaVersion: 1,
|
|
914
|
+
variables
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function buildContractFromExample(examplePath) {
|
|
918
|
+
const parsed = parseEnvFile(examplePath);
|
|
919
|
+
const variables = {};
|
|
920
|
+
for (const variable of parsed.vars) {
|
|
921
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
922
|
+
variables[variable.key] = {
|
|
923
|
+
expected: mapEnvVarTypeToExpected(effectiveType),
|
|
924
|
+
required: variable.isRequired,
|
|
925
|
+
clientSide: variable.isClientSide,
|
|
926
|
+
...variable.description !== void 0 && { description: variable.description },
|
|
927
|
+
...shouldTreatAsSecret(variable.key) && { secret: true }
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
schemaVersion: 1,
|
|
932
|
+
variables
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function findDefaultContractPath(cwd) {
|
|
936
|
+
for (const fileName of CONTRACT_FILE_NAMES) {
|
|
937
|
+
const candidatePath = path7.resolve(cwd, fileName);
|
|
938
|
+
if (existsSync2(candidatePath)) return candidatePath;
|
|
939
|
+
}
|
|
940
|
+
return void 0;
|
|
941
|
+
}
|
|
942
|
+
async function loadValidationContract(options) {
|
|
943
|
+
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
944
|
+
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
945
|
+
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path7.resolve(cwd, contractPath);
|
|
946
|
+
if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
|
|
947
|
+
const moduleUrl = pathToFileURL2(resolvedContractPath).href;
|
|
948
|
+
const moduleValue = await import(moduleUrl);
|
|
949
|
+
const candidate = moduleValue.default ?? moduleValue.contract;
|
|
950
|
+
if (isEnvContract(candidate)) {
|
|
951
|
+
return candidate;
|
|
952
|
+
}
|
|
953
|
+
if (isLegacyContract(candidate)) {
|
|
954
|
+
return convertLegacyContract(candidate);
|
|
955
|
+
}
|
|
956
|
+
throw new Error(
|
|
957
|
+
`Invalid contract at ${resolvedContractPath}. Export default must match EnvContract.`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
return buildContractFromExample(path7.resolve(cwd, fallbackExamplePath));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/plugins.ts
|
|
964
|
+
import path8 from "path";
|
|
965
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
966
|
+
function isRecord3(value) {
|
|
967
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
968
|
+
}
|
|
969
|
+
function isPlugin(value) {
|
|
970
|
+
if (!isRecord3(value)) return false;
|
|
971
|
+
if (typeof value.name !== "string") return false;
|
|
972
|
+
if (value.transformContract !== void 0 && typeof value.transformContract !== "function") {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
if (value.transformSource !== void 0 && typeof value.transformSource !== "function") {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
if (value.transformReport !== void 0 && typeof value.transformReport !== "function") {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
async function loadPluginFromPath(pluginPath, cwd) {
|
|
984
|
+
const resolvedPath = path8.resolve(cwd, pluginPath);
|
|
985
|
+
const moduleValue = await import(pathToFileURL3(resolvedPath).href);
|
|
986
|
+
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
987
|
+
if (isPlugin(candidate)) return candidate;
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Invalid plugin at ${resolvedPath}.
|
|
990
|
+
Expected a default export matching:
|
|
991
|
+
{ name: string,
|
|
992
|
+
transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
|
|
993
|
+
transformReport?(report: ValidationReport): ValidationReport,
|
|
994
|
+
transformContract?(contract: EnvContract): EnvContract }`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
async function loadPlugins(options) {
|
|
998
|
+
const cwd = options.cwd ?? process.cwd();
|
|
999
|
+
const plugins = [];
|
|
1000
|
+
const references = [...options.configPlugins ?? [], ...options.pluginPaths];
|
|
1001
|
+
for (const reference of references) {
|
|
1002
|
+
if (typeof reference === "string") {
|
|
1003
|
+
plugins.push(await loadPluginFromPath(reference, cwd));
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (isPlugin(reference)) {
|
|
1007
|
+
plugins.push(reference);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
throw new Error("Invalid plugin reference in configuration.");
|
|
1011
|
+
}
|
|
1012
|
+
return plugins;
|
|
1013
|
+
}
|
|
1014
|
+
function applyContractPlugins(contract, plugins) {
|
|
1015
|
+
let next = contract;
|
|
1016
|
+
for (const plugin of plugins) {
|
|
1017
|
+
if (plugin.transformContract === void 0) continue;
|
|
1018
|
+
next = plugin.transformContract(next);
|
|
1019
|
+
}
|
|
1020
|
+
return next;
|
|
1021
|
+
}
|
|
1022
|
+
function applySourcePlugins(params, plugins) {
|
|
1023
|
+
let next = params.values;
|
|
1024
|
+
for (const plugin of plugins) {
|
|
1025
|
+
if (plugin.transformSource === void 0) continue;
|
|
1026
|
+
next = plugin.transformSource({ environment: params.environment, values: next });
|
|
1027
|
+
}
|
|
1028
|
+
return next;
|
|
1029
|
+
}
|
|
1030
|
+
function applyReportPlugins(report, plugins) {
|
|
1031
|
+
let next = report;
|
|
1032
|
+
for (const plugin of plugins) {
|
|
1033
|
+
if (plugin.transformReport === void 0) continue;
|
|
1034
|
+
next = plugin.transformReport(next);
|
|
1035
|
+
}
|
|
1036
|
+
return next;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/validation/engine.ts
|
|
1040
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
|
|
1041
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
1042
|
+
function detectReceivedType(value) {
|
|
1043
|
+
const normalized = value.trim();
|
|
1044
|
+
if (normalized.length === 0) return "unknown";
|
|
1045
|
+
if (["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) return "boolean";
|
|
1046
|
+
if (!Number.isNaN(Number(normalized)) && Number.isFinite(Number(normalized))) return "number";
|
|
1047
|
+
if (SEMVER_RE.test(normalized)) return "semver";
|
|
1048
|
+
try {
|
|
1049
|
+
const url = new URL(normalized);
|
|
1050
|
+
if (url.protocol.length > 0) return "url";
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
if (EMAIL_RE.test(normalized)) return "email";
|
|
1054
|
+
try {
|
|
1055
|
+
const parsed = JSON.parse(normalized);
|
|
1056
|
+
if (typeof parsed === "object" && parsed !== null) return "json";
|
|
1057
|
+
} catch {
|
|
1058
|
+
}
|
|
1059
|
+
return "string";
|
|
1060
|
+
}
|
|
1061
|
+
function validateNumber(expected, normalized, receivedType) {
|
|
1062
|
+
const parsed = Number(normalized);
|
|
1063
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1064
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1065
|
+
}
|
|
1066
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1067
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1068
|
+
}
|
|
1069
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1070
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1071
|
+
}
|
|
1072
|
+
return { isValid: true, receivedType };
|
|
1073
|
+
}
|
|
1074
|
+
function validateBoolean(normalized, receivedType) {
|
|
1075
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1076
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1077
|
+
}
|
|
1078
|
+
return { isValid: true, receivedType };
|
|
1079
|
+
}
|
|
1080
|
+
function validateEnum(expected, normalized, receivedType) {
|
|
1081
|
+
if (!expected.values.includes(normalized)) {
|
|
1082
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1083
|
+
}
|
|
1084
|
+
return { isValid: true, receivedType };
|
|
1085
|
+
}
|
|
1086
|
+
function validateUrl(normalized, receivedType) {
|
|
1087
|
+
try {
|
|
1088
|
+
const value = new URL(normalized);
|
|
1089
|
+
if (value.protocol.length === 0)
|
|
1090
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1091
|
+
return { isValid: true, receivedType };
|
|
1092
|
+
} catch {
|
|
1093
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function validateEmail(normalized, receivedType) {
|
|
1097
|
+
if (!EMAIL_RE.test(normalized))
|
|
1098
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1099
|
+
return { isValid: true, receivedType };
|
|
1100
|
+
}
|
|
1101
|
+
function validateJson(normalized, receivedType) {
|
|
1102
|
+
try {
|
|
1103
|
+
const parsed = JSON.parse(normalized);
|
|
1104
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1105
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1106
|
+
} catch {
|
|
1107
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function validateSemver(normalized, receivedType) {
|
|
1111
|
+
if (!SEMVER_RE.test(normalized))
|
|
1112
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1113
|
+
return { isValid: true, receivedType };
|
|
1114
|
+
}
|
|
1115
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1116
|
+
const normalized = rawValue.trim();
|
|
1117
|
+
const receivedType = detectReceivedType(normalized);
|
|
1118
|
+
if (expected.type === "unknown" || expected.type === "string")
|
|
1119
|
+
return { isValid: true, receivedType };
|
|
1120
|
+
if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
|
|
1121
|
+
if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
|
|
1122
|
+
if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
|
|
1123
|
+
if (expected.type === "url") return validateUrl(normalized, receivedType);
|
|
1124
|
+
if (expected.type === "email") return validateEmail(normalized, receivedType);
|
|
1125
|
+
if (expected.type === "json") return validateJson(normalized, receivedType);
|
|
1126
|
+
if (expected.type === "semver") return validateSemver(normalized, receivedType);
|
|
1127
|
+
return { isValid: true, receivedType };
|
|
1128
|
+
}
|
|
1129
|
+
function toIssueCode(issueType) {
|
|
1130
|
+
if (issueType === "missing") return "ENV_MISSING";
|
|
1131
|
+
if (issueType === "extra") return "ENV_EXTRA";
|
|
1132
|
+
if (issueType === "invalid_type") return "ENV_INVALID_TYPE";
|
|
1133
|
+
if (issueType === "invalid_value") return "ENV_INVALID_VALUE";
|
|
1134
|
+
if (issueType === "conflict") return "ENV_CONFLICT";
|
|
1135
|
+
return "ENV_SECRET_EXPOSED";
|
|
1136
|
+
}
|
|
1137
|
+
function toIssueValue(value, debugValues) {
|
|
1138
|
+
if (!debugValues) return null;
|
|
1139
|
+
if (value === void 0) return null;
|
|
1140
|
+
return value;
|
|
1141
|
+
}
|
|
1142
|
+
function createIssue(params) {
|
|
1143
|
+
return {
|
|
1144
|
+
code: toIssueCode(params.type),
|
|
1145
|
+
type: params.type,
|
|
1146
|
+
severity: params.severity,
|
|
1147
|
+
key: params.key,
|
|
1148
|
+
environment: params.environment,
|
|
1149
|
+
message: params.message,
|
|
1150
|
+
value: toIssueValue(params.value, params.debugValues),
|
|
1151
|
+
...params.expected !== void 0 && { expected: params.expected },
|
|
1152
|
+
...params.receivedType !== void 0 && { receivedType: params.receivedType }
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function dedupeIssues(issues) {
|
|
1156
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1157
|
+
const unique = [];
|
|
1158
|
+
for (const issue of issues) {
|
|
1159
|
+
const token = [
|
|
1160
|
+
issue.code,
|
|
1161
|
+
issue.type,
|
|
1162
|
+
issue.severity,
|
|
1163
|
+
issue.environment,
|
|
1164
|
+
issue.key,
|
|
1165
|
+
issue.message,
|
|
1166
|
+
issue.receivedType ?? ""
|
|
1167
|
+
].join("|");
|
|
1168
|
+
if (seen.has(token)) continue;
|
|
1169
|
+
seen.add(token);
|
|
1170
|
+
unique.push(issue);
|
|
1171
|
+
}
|
|
1172
|
+
return unique;
|
|
1173
|
+
}
|
|
1174
|
+
function buildReport(env, issues, recommendations) {
|
|
1175
|
+
const dedupedIssues = dedupeIssues(issues);
|
|
1176
|
+
const errors = dedupedIssues.filter((item) => item.severity === "error").length;
|
|
1177
|
+
const warnings = dedupedIssues.filter((item) => item.severity === "warning").length;
|
|
1178
|
+
const status = errors > 0 ? "fail" : "ok";
|
|
1179
|
+
return {
|
|
1180
|
+
schemaVersion: 1,
|
|
1181
|
+
status,
|
|
1182
|
+
summary: {
|
|
1183
|
+
errors,
|
|
1184
|
+
warnings,
|
|
1185
|
+
total: dedupedIssues.length
|
|
1186
|
+
},
|
|
1187
|
+
issues: dedupedIssues,
|
|
1188
|
+
meta: {
|
|
1189
|
+
env,
|
|
1190
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1191
|
+
},
|
|
1192
|
+
...recommendations !== void 0 && recommendations.length > 0 && { recommendations }
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function isClientSecret(variable, key) {
|
|
1196
|
+
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1197
|
+
}
|
|
1198
|
+
function checkContractVariable(key, variable, context) {
|
|
1199
|
+
const { options, issues } = context;
|
|
1200
|
+
const value = options.values[key];
|
|
1201
|
+
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1202
|
+
if (variable.required && !hasValue) {
|
|
1203
|
+
issues.push(
|
|
1204
|
+
createIssue({
|
|
1205
|
+
type: "missing",
|
|
1206
|
+
severity: "error",
|
|
1207
|
+
key,
|
|
1208
|
+
environment: options.environment,
|
|
1209
|
+
message: `Required variable ${key} is missing.`,
|
|
1210
|
+
debugValues: options.debugValues,
|
|
1211
|
+
expected: variable.expected
|
|
1212
|
+
})
|
|
1213
|
+
);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (!hasValue) return;
|
|
1217
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1218
|
+
if (!validation.isValid) {
|
|
1219
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
|
|
1220
|
+
issues.push(
|
|
1221
|
+
createIssue({
|
|
1222
|
+
type: validation.issueType,
|
|
1223
|
+
severity: "error",
|
|
1224
|
+
key,
|
|
1225
|
+
environment: options.environment,
|
|
1226
|
+
message,
|
|
1227
|
+
value,
|
|
1228
|
+
debugValues: options.debugValues,
|
|
1229
|
+
expected: variable.expected,
|
|
1230
|
+
receivedType: validation.receivedType
|
|
1231
|
+
})
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (isClientSecret(variable, key)) {
|
|
1235
|
+
issues.push(
|
|
1236
|
+
createIssue({
|
|
1237
|
+
type: "secret_exposed",
|
|
1238
|
+
severity: "error",
|
|
1239
|
+
key,
|
|
1240
|
+
environment: options.environment,
|
|
1241
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1242
|
+
value,
|
|
1243
|
+
debugValues: options.debugValues,
|
|
1244
|
+
expected: variable.expected
|
|
1245
|
+
})
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function validateAgainstContract(options) {
|
|
1250
|
+
const issues = [];
|
|
1251
|
+
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1252
|
+
for (const [key, variable] of Object.entries(options.contract.variables)) {
|
|
1253
|
+
checkContractVariable(key, variable, { options, issues });
|
|
1254
|
+
}
|
|
1255
|
+
for (const [key, value] of Object.entries(options.values)) {
|
|
1256
|
+
if (contractKeys.has(key)) continue;
|
|
1257
|
+
const severity = options.strict ? "error" : "warning";
|
|
1258
|
+
issues.push(
|
|
1259
|
+
createIssue({
|
|
1260
|
+
type: "extra",
|
|
1261
|
+
severity,
|
|
1262
|
+
key,
|
|
1263
|
+
environment: options.environment,
|
|
1264
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1265
|
+
value,
|
|
1266
|
+
debugValues: options.debugValues
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
return buildReport(options.environment, issues);
|
|
1271
|
+
}
|
|
1272
|
+
function collectUnionKeys(contract, sources) {
|
|
1273
|
+
const union = new Set(Object.keys(contract.variables));
|
|
1274
|
+
for (const source of Object.values(sources)) {
|
|
1275
|
+
for (const key of Object.keys(source)) {
|
|
1276
|
+
union.add(key);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return union;
|
|
1280
|
+
}
|
|
1281
|
+
function diffMissingEntries(key, missing, context) {
|
|
1282
|
+
const { variable, options, issues } = context;
|
|
1283
|
+
for (const entry of missing) {
|
|
1284
|
+
issues.push(
|
|
1285
|
+
createIssue({
|
|
1286
|
+
type: "missing",
|
|
1287
|
+
severity: "error",
|
|
1288
|
+
key,
|
|
1289
|
+
environment: entry.sourceName,
|
|
1290
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1291
|
+
debugValues: options.debugValues,
|
|
1292
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1293
|
+
})
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function diffTypeConflicts(key, present, context) {
|
|
1298
|
+
const { variable, options, issues } = context;
|
|
1299
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1300
|
+
for (const entry of present) {
|
|
1301
|
+
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1302
|
+
}
|
|
1303
|
+
if (new Set(typeBySource.values()).size <= 1) return;
|
|
1304
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1305
|
+
issues.push(
|
|
1306
|
+
createIssue({
|
|
1307
|
+
type: "conflict",
|
|
1308
|
+
severity: "error",
|
|
1309
|
+
key,
|
|
1310
|
+
environment: sourceName,
|
|
1311
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1312
|
+
debugValues: options.debugValues,
|
|
1313
|
+
receivedType: detectedType,
|
|
1314
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1315
|
+
value: options.sources[sourceName]?.[key]
|
|
1316
|
+
},
|
|
1317
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function diffPresentEntry(key, entry, context) {
|
|
1323
|
+
const { variable, options, issues } = context;
|
|
1324
|
+
if (entry.value === void 0) return;
|
|
1325
|
+
if (variable === void 0) {
|
|
1326
|
+
const severity = options.strict ? "error" : "warning";
|
|
1327
|
+
issues.push(
|
|
1328
|
+
createIssue({
|
|
1329
|
+
type: "extra",
|
|
1330
|
+
severity,
|
|
1331
|
+
key,
|
|
1332
|
+
environment: entry.sourceName,
|
|
1333
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1334
|
+
value: entry.value,
|
|
1335
|
+
debugValues: options.debugValues
|
|
1336
|
+
})
|
|
1337
|
+
);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1341
|
+
if (!validation.isValid) {
|
|
1342
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
|
|
1343
|
+
issues.push(
|
|
1344
|
+
createIssue({
|
|
1345
|
+
type: validation.issueType,
|
|
1346
|
+
severity: "error",
|
|
1347
|
+
key,
|
|
1348
|
+
environment: entry.sourceName,
|
|
1349
|
+
message,
|
|
1350
|
+
value: entry.value,
|
|
1351
|
+
debugValues: options.debugValues,
|
|
1352
|
+
expected: variable.expected,
|
|
1353
|
+
receivedType: validation.receivedType
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (isClientSecret(variable, key)) {
|
|
1358
|
+
issues.push(
|
|
1359
|
+
createIssue({
|
|
1360
|
+
type: "secret_exposed",
|
|
1361
|
+
severity: "error",
|
|
1362
|
+
key,
|
|
1363
|
+
environment: entry.sourceName,
|
|
1364
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1365
|
+
value: entry.value,
|
|
1366
|
+
debugValues: options.debugValues,
|
|
1367
|
+
expected: variable.expected
|
|
1368
|
+
})
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function diffEnvironmentSources(options) {
|
|
1373
|
+
const issues = [];
|
|
1374
|
+
const sourceNames = Object.keys(options.sources);
|
|
1375
|
+
const unionKeys = collectUnionKeys(options.contract, options.sources);
|
|
1376
|
+
for (const key of unionKeys) {
|
|
1377
|
+
const variable = options.contract.variables[key];
|
|
1378
|
+
const valuesBySource = sourceNames.map((sourceName) => ({
|
|
1379
|
+
sourceName,
|
|
1380
|
+
value: options.sources[sourceName]?.[key]
|
|
1381
|
+
}));
|
|
1382
|
+
const present = valuesBySource.filter(
|
|
1383
|
+
(entry) => entry.value !== void 0 && entry.value !== ""
|
|
1384
|
+
);
|
|
1385
|
+
const missing = valuesBySource.filter(
|
|
1386
|
+
(entry) => entry.value === void 0 || entry.value === ""
|
|
1387
|
+
);
|
|
1388
|
+
if (present.length === 0 && variable?.required === true) {
|
|
1389
|
+
for (const entry of missing) {
|
|
1390
|
+
issues.push(
|
|
1391
|
+
createIssue({
|
|
1392
|
+
type: "missing",
|
|
1393
|
+
severity: "error",
|
|
1394
|
+
key,
|
|
1395
|
+
environment: entry.sourceName,
|
|
1396
|
+
message: `Required variable ${key} is missing in ${entry.sourceName}.`,
|
|
1397
|
+
debugValues: options.debugValues,
|
|
1398
|
+
expected: variable.expected
|
|
1399
|
+
})
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
const ctx = { variable, options, issues };
|
|
1405
|
+
if (present.length > 0) {
|
|
1406
|
+
diffMissingEntries(key, missing, ctx);
|
|
1407
|
+
}
|
|
1408
|
+
diffTypeConflicts(key, present, ctx);
|
|
1409
|
+
for (const entry of present) {
|
|
1410
|
+
diffPresentEntry(key, entry, ctx);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return buildReport("diff", issues);
|
|
1414
|
+
}
|
|
1415
|
+
function buildRecommendations(issues) {
|
|
1416
|
+
const codes = new Set(issues.map((item) => item.code));
|
|
1417
|
+
const recommendations = [];
|
|
1418
|
+
if (codes.has("ENV_MISSING")) {
|
|
1419
|
+
recommendations.push("Add missing required variables to each target environment.");
|
|
1420
|
+
}
|
|
1421
|
+
if (codes.has("ENV_EXTRA")) {
|
|
1422
|
+
recommendations.push(
|
|
1423
|
+
"Remove undeclared variables or add them to env.contract.ts intentionally."
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
if (codes.has("ENV_INVALID_TYPE") || codes.has("ENV_INVALID_VALUE")) {
|
|
1427
|
+
recommendations.push(
|
|
1428
|
+
"Normalize variable values so they match the expected contract types and constraints."
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
if (codes.has("ENV_CONFLICT")) {
|
|
1432
|
+
recommendations.push("Align variable semantics across environments to avoid drift.");
|
|
1433
|
+
}
|
|
1434
|
+
if (codes.has("ENV_SECRET_EXPOSED")) {
|
|
1435
|
+
recommendations.push(
|
|
1436
|
+
"Move secret variables to server-only scope and avoid NEXT_PUBLIC_ exposure for secrets."
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
return recommendations;
|
|
1440
|
+
}
|
|
1441
|
+
function buildDoctorReport(options) {
|
|
1442
|
+
const merged = [...options.checkReport.issues, ...options.diffReport.issues];
|
|
1443
|
+
const recommendations = buildRecommendations(merged);
|
|
1444
|
+
return buildReport("doctor", merged, recommendations);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/validation/env-source.ts
|
|
1448
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1449
|
+
import path9 from "path";
|
|
1450
|
+
function stripWrappingQuotes(value) {
|
|
1451
|
+
if (value.length < 2) return value;
|
|
1452
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1453
|
+
return value.slice(1, -1);
|
|
1454
|
+
}
|
|
1455
|
+
return value;
|
|
1456
|
+
}
|
|
1457
|
+
function parseEnvSourceContent(content) {
|
|
1458
|
+
const result = {};
|
|
1459
|
+
const lines = content.split("\n");
|
|
1460
|
+
for (const line of lines) {
|
|
1461
|
+
const trimmed = line.trim();
|
|
1462
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1463
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
|
|
1464
|
+
if (match === null) continue;
|
|
1465
|
+
const key = match[1] ?? "";
|
|
1466
|
+
const rawValue = match[2] ?? "";
|
|
1467
|
+
result[key] = stripWrappingQuotes(rawValue.trim());
|
|
1468
|
+
}
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
async function loadEnvSource(options) {
|
|
1472
|
+
const resolvedPath = path9.resolve(options.filePath);
|
|
1473
|
+
try {
|
|
1474
|
+
const content = await readFile3(resolvedPath, "utf8");
|
|
1475
|
+
return parseEnvSourceContent(content);
|
|
1476
|
+
} catch (error_) {
|
|
1477
|
+
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1478
|
+
return {};
|
|
1479
|
+
}
|
|
1480
|
+
throw error_;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/validation/output.ts
|
|
1485
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1486
|
+
import path10 from "path";
|
|
1487
|
+
function toJsonString(report, mode) {
|
|
1488
|
+
if (mode === "pretty") return `${JSON.stringify(report, null, 2)}
|
|
1489
|
+
`;
|
|
1490
|
+
return `${JSON.stringify(report)}
|
|
1491
|
+
`;
|
|
1492
|
+
}
|
|
1493
|
+
function formatIssue(issue) {
|
|
1494
|
+
const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
|
|
1495
|
+
const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
|
|
1496
|
+
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1497
|
+
}
|
|
1498
|
+
function formatHumanReport(report) {
|
|
1499
|
+
const lines = [];
|
|
1500
|
+
lines.push(
|
|
1501
|
+
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1502
|
+
);
|
|
1503
|
+
if (report.issues.length > 0) {
|
|
1504
|
+
lines.push("", "Issues:");
|
|
1505
|
+
for (const issue of report.issues) {
|
|
1506
|
+
lines.push(`- ${formatIssue(issue)}`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1510
|
+
lines.push("", "Recommendations:");
|
|
1511
|
+
for (const recommendation of report.recommendations) {
|
|
1512
|
+
lines.push(`- ${recommendation}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return `${lines.join("\n")}
|
|
1516
|
+
`;
|
|
1517
|
+
}
|
|
1518
|
+
async function persistJsonOutput(outputFile, report) {
|
|
1519
|
+
const resolvedPath = path10.resolve(outputFile);
|
|
1520
|
+
await mkdir2(path10.dirname(resolvedPath), { recursive: true });
|
|
1521
|
+
await writeFile2(resolvedPath, `${JSON.stringify(report, null, 2)}
|
|
1522
|
+
`, "utf8");
|
|
1523
|
+
}
|
|
1524
|
+
async function emitValidationReport(options) {
|
|
1525
|
+
const { report, outputFile, jsonMode } = options;
|
|
1526
|
+
if (outputFile !== void 0) {
|
|
1527
|
+
await persistJsonOutput(outputFile, report);
|
|
1528
|
+
}
|
|
1529
|
+
if (jsonMode === "off") {
|
|
1530
|
+
process.stdout.write(formatHumanReport(report));
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
process.stdout.write(toJsonString(report, jsonMode));
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/validation-command.ts
|
|
1537
|
+
var HELP_TEXT = {
|
|
1538
|
+
check: [
|
|
1539
|
+
"Usage: env-typegen check [options]",
|
|
1540
|
+
"",
|
|
1541
|
+
"Options:",
|
|
1542
|
+
" --env <path> Environment file to validate (default: .env)",
|
|
1543
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1544
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1545
|
+
" --strict Validate extras as errors (default: true)",
|
|
1546
|
+
" --no-strict Downgrade extras to warnings",
|
|
1547
|
+
" --json Emit machine-readable JSON report",
|
|
1548
|
+
" --json=pretty Emit pretty JSON report",
|
|
1549
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1550
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1551
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1552
|
+
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1553
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1554
|
+
" -c, --config <path> Config file path",
|
|
1555
|
+
" -h, --help Show this help",
|
|
1556
|
+
"",
|
|
1557
|
+
"Exit codes:",
|
|
1558
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1559
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1560
|
+
].join("\n"),
|
|
1561
|
+
diff: [
|
|
1562
|
+
"Usage: env-typegen diff [options]",
|
|
1563
|
+
"",
|
|
1564
|
+
"Options:",
|
|
1565
|
+
" --targets <list> Comma-separated targets (default: .env,.env.example,.env.production)",
|
|
1566
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1567
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1568
|
+
" --strict Validate extras as errors (default: true)",
|
|
1569
|
+
" --no-strict Downgrade extras to warnings",
|
|
1570
|
+
" --json Emit machine-readable JSON report",
|
|
1571
|
+
" --json=pretty Emit pretty JSON report",
|
|
1572
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1573
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1574
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1575
|
+
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1576
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1577
|
+
" -c, --config <path> Config file path",
|
|
1578
|
+
" -h, --help Show this help",
|
|
1579
|
+
"",
|
|
1580
|
+
"Exit codes:",
|
|
1581
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1582
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1583
|
+
].join("\n"),
|
|
1584
|
+
doctor: [
|
|
1585
|
+
"Usage: env-typegen doctor [options]",
|
|
1586
|
+
"",
|
|
1587
|
+
"Options:",
|
|
1588
|
+
" --env <path> Environment file to validate (default: .env)",
|
|
1589
|
+
" --targets <list> Comma-separated targets for drift analysis",
|
|
1590
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1591
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1592
|
+
" --strict Validate extras as errors (default: true)",
|
|
1593
|
+
" --no-strict Downgrade extras to warnings",
|
|
1594
|
+
" --json Emit machine-readable JSON report",
|
|
1595
|
+
" --json=pretty Emit pretty JSON report",
|
|
1596
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1597
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1598
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1599
|
+
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1600
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1601
|
+
" -c, --config <path> Config file path",
|
|
1602
|
+
" -h, --help Show this help",
|
|
1603
|
+
"",
|
|
1604
|
+
"Exit codes:",
|
|
1605
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1606
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1607
|
+
].join("\n")
|
|
1608
|
+
};
|
|
1609
|
+
function resolveConfigRelative(value, configDir) {
|
|
1610
|
+
if (value === void 0 || path11.isAbsolute(value)) return value;
|
|
1611
|
+
return path11.resolve(configDir, value);
|
|
1612
|
+
}
|
|
1613
|
+
function resolvePluginReference(reference, configDir) {
|
|
1614
|
+
if (typeof reference === "string" && !path11.isAbsolute(reference)) {
|
|
1615
|
+
return path11.resolve(configDir, reference);
|
|
1616
|
+
}
|
|
1617
|
+
return reference;
|
|
1618
|
+
}
|
|
1619
|
+
function applyConfigPaths(config, configDir) {
|
|
1620
|
+
let input;
|
|
1621
|
+
if (Array.isArray(config.input)) {
|
|
1622
|
+
input = config.input.map((item) => resolveConfigRelative(item, configDir) ?? item);
|
|
1623
|
+
} else {
|
|
1624
|
+
input = resolveConfigRelative(config.input, configDir);
|
|
1625
|
+
}
|
|
1626
|
+
const output = resolveConfigRelative(config.output, configDir);
|
|
1627
|
+
const schemaFile = resolveConfigRelative(config.schemaFile, configDir);
|
|
1628
|
+
return {
|
|
1629
|
+
...config,
|
|
1630
|
+
...input !== void 0 && { input },
|
|
1631
|
+
...output !== void 0 && { output },
|
|
1632
|
+
...schemaFile !== void 0 && { schemaFile },
|
|
1633
|
+
...config.diffTargets !== void 0 && {
|
|
1634
|
+
diffTargets: config.diffTargets.map(
|
|
1635
|
+
(target) => resolveConfigRelative(target, configDir) ?? target
|
|
1636
|
+
)
|
|
1637
|
+
},
|
|
1638
|
+
...config.plugins !== void 0 && {
|
|
1639
|
+
plugins: config.plugins.map((reference) => resolvePluginReference(reference, configDir))
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
async function loadCommandConfig(configPath) {
|
|
1644
|
+
if (configPath === void 0) {
|
|
1645
|
+
return loadConfig(process.cwd());
|
|
1646
|
+
}
|
|
1647
|
+
const resolvedPath = path11.resolve(configPath);
|
|
1648
|
+
const configDir = path11.dirname(resolvedPath);
|
|
1649
|
+
const moduleValue = await import(pathToFileURL4(resolvedPath).href);
|
|
1650
|
+
if (moduleValue.default === void 0) return void 0;
|
|
1651
|
+
return applyConfigPaths(moduleValue.default, configDir);
|
|
1652
|
+
}
|
|
1653
|
+
function preprocessJsonArguments(argv) {
|
|
1654
|
+
const normalizedArgs = [];
|
|
1655
|
+
let assignedMode = "off";
|
|
1656
|
+
for (const item of argv) {
|
|
1657
|
+
if (item === "--json=pretty") {
|
|
1658
|
+
normalizedArgs.push("--json");
|
|
1659
|
+
assignedMode = "pretty";
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
if (item === "--json=compact") {
|
|
1663
|
+
normalizedArgs.push("--json");
|
|
1664
|
+
assignedMode = "compact";
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
normalizedArgs.push(item);
|
|
1668
|
+
}
|
|
1669
|
+
return { normalizedArgs, assignedMode };
|
|
1670
|
+
}
|
|
1671
|
+
function parseValidationArgs(argv) {
|
|
1672
|
+
const { normalizedArgs, assignedMode } = preprocessJsonArguments(argv);
|
|
1673
|
+
const { values } = parseArgs({
|
|
1674
|
+
args: normalizedArgs,
|
|
1675
|
+
options: {
|
|
1676
|
+
env: { type: "string", multiple: true },
|
|
1677
|
+
targets: { type: "string" },
|
|
1678
|
+
contract: { type: "string" },
|
|
1679
|
+
example: { type: "string" },
|
|
1680
|
+
strict: { type: "boolean" },
|
|
1681
|
+
"no-strict": { type: "boolean" },
|
|
1682
|
+
json: { type: "boolean" },
|
|
1683
|
+
"output-file": { type: "string" },
|
|
1684
|
+
"debug-values": { type: "boolean" },
|
|
1685
|
+
"cloud-provider": { type: "string" },
|
|
1686
|
+
"cloud-file": { type: "string" },
|
|
1687
|
+
plugin: { type: "string", multiple: true },
|
|
1688
|
+
config: { type: "string", short: "c" },
|
|
1689
|
+
help: { type: "boolean", short: "h" }
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
const castValues = values;
|
|
1693
|
+
let jsonMode = "off";
|
|
1694
|
+
if (castValues.json === true) {
|
|
1695
|
+
jsonMode = assignedMode === "off" ? "compact" : assignedMode;
|
|
1696
|
+
}
|
|
1697
|
+
return { values: castValues, jsonMode };
|
|
1698
|
+
}
|
|
1699
|
+
function resolveStrict(values, fileConfig) {
|
|
1700
|
+
if (values["no-strict"] === true) return false;
|
|
1701
|
+
if (values.strict !== void 0) return values.strict;
|
|
1702
|
+
if (fileConfig?.strict !== void 0) return fileConfig.strict;
|
|
1703
|
+
return true;
|
|
1704
|
+
}
|
|
1705
|
+
function parseCloudProvider(value) {
|
|
1706
|
+
if (value === void 0) return void 0;
|
|
1707
|
+
if (value === "vercel" || value === "cloudflare" || value === "aws") return value;
|
|
1708
|
+
throw new Error(`Unknown cloud provider: ${value}. Valid: vercel, cloudflare, aws`);
|
|
1709
|
+
}
|
|
1710
|
+
function parseTargets(values, fileConfig) {
|
|
1711
|
+
const fromCli = values.targets;
|
|
1712
|
+
if (fromCli !== void 0) {
|
|
1713
|
+
return fromCli.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1714
|
+
}
|
|
1715
|
+
if (fileConfig?.diffTargets !== void 0 && fileConfig.diffTargets.length > 0) {
|
|
1716
|
+
return fileConfig.diffTargets;
|
|
1717
|
+
}
|
|
1718
|
+
return [".env", ".env.example", ".env.production"];
|
|
1719
|
+
}
|
|
1720
|
+
async function prepareCommonContext(values) {
|
|
1721
|
+
const fileConfig = await loadCommandConfig(values.config);
|
|
1722
|
+
const strict = resolveStrict(values, fileConfig);
|
|
1723
|
+
const debugValues = values["debug-values"] ?? false;
|
|
1724
|
+
const contractPath = values.contract ?? fileConfig?.schemaFile;
|
|
1725
|
+
const fallbackExamplePath = values.example ?? ".env.example";
|
|
1726
|
+
const cloudProvider = parseCloudProvider(values["cloud-provider"]);
|
|
1727
|
+
const cloudFile = values["cloud-file"];
|
|
1728
|
+
const pluginLoadOptions = {
|
|
1729
|
+
pluginPaths: values.plugin ?? [],
|
|
1730
|
+
cwd: process.cwd(),
|
|
1731
|
+
...fileConfig?.plugins !== void 0 && { configPlugins: fileConfig.plugins }
|
|
1732
|
+
};
|
|
1733
|
+
const plugins = await loadPlugins(pluginLoadOptions);
|
|
1734
|
+
return {
|
|
1735
|
+
fileConfig,
|
|
1736
|
+
strict,
|
|
1737
|
+
debugValues,
|
|
1738
|
+
outputFile: values["output-file"],
|
|
1739
|
+
contractPath,
|
|
1740
|
+
fallbackExamplePath,
|
|
1741
|
+
cloudProvider,
|
|
1742
|
+
cloudFile,
|
|
1743
|
+
plugins
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
async function emitAndReturnExitCode(report, params) {
|
|
1747
|
+
const emitOptions = {
|
|
1748
|
+
report,
|
|
1749
|
+
jsonMode: params.jsonMode,
|
|
1750
|
+
...params.outputFile !== void 0 && { outputFile: params.outputFile }
|
|
1751
|
+
};
|
|
1752
|
+
await emitValidationReport(emitOptions);
|
|
1753
|
+
return report.status === "fail" ? 1 : 0;
|
|
1754
|
+
}
|
|
1755
|
+
async function runCheckCommand(args) {
|
|
1756
|
+
const context = await prepareCommonContext(args.values);
|
|
1757
|
+
const loadContractOptions = {
|
|
1758
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1759
|
+
cwd: process.cwd(),
|
|
1760
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1761
|
+
};
|
|
1762
|
+
const contract = applyContractPlugins(
|
|
1763
|
+
await loadValidationContract(loadContractOptions),
|
|
1764
|
+
context.plugins
|
|
1765
|
+
);
|
|
1766
|
+
const provider = context.cloudProvider;
|
|
1767
|
+
let environment = args.values.env?.[0] ?? ".env";
|
|
1768
|
+
let sourceValues;
|
|
1769
|
+
if (provider === void 0) {
|
|
1770
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1771
|
+
} else {
|
|
1772
|
+
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1773
|
+
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1774
|
+
environment = `cloud:${provider}`;
|
|
1775
|
+
}
|
|
1776
|
+
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1777
|
+
const report = applyReportPlugins(
|
|
1778
|
+
validateAgainstContract({
|
|
1779
|
+
contract,
|
|
1780
|
+
values: sourceValues,
|
|
1781
|
+
environment,
|
|
1782
|
+
strict: context.strict,
|
|
1783
|
+
debugValues: context.debugValues
|
|
1784
|
+
}),
|
|
1785
|
+
context.plugins
|
|
1786
|
+
);
|
|
1787
|
+
return emitAndReturnExitCode(report, {
|
|
1788
|
+
jsonMode: args.jsonMode,
|
|
1789
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
async function runDiffCommand(args) {
|
|
1793
|
+
const context = await prepareCommonContext(args.values);
|
|
1794
|
+
const loadContractOptions = {
|
|
1795
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1796
|
+
cwd: process.cwd(),
|
|
1797
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1798
|
+
};
|
|
1799
|
+
const contract = applyContractPlugins(
|
|
1800
|
+
await loadValidationContract(loadContractOptions),
|
|
1801
|
+
context.plugins
|
|
1802
|
+
);
|
|
1803
|
+
const sources = {};
|
|
1804
|
+
for (const target of parseTargets(args.values, context.fileConfig)) {
|
|
1805
|
+
const values = await loadEnvSource({ filePath: target, allowMissing: true });
|
|
1806
|
+
sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
|
|
1807
|
+
}
|
|
1808
|
+
if (context.cloudProvider !== void 0) {
|
|
1809
|
+
const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
|
|
1810
|
+
const cloudEnvironment = `cloud:${context.cloudProvider}`;
|
|
1811
|
+
const cloudValues = await loadCloudSource({
|
|
1812
|
+
provider: context.cloudProvider,
|
|
1813
|
+
filePath: cloudFile
|
|
1814
|
+
});
|
|
1815
|
+
sources[cloudEnvironment] = applySourcePlugins(
|
|
1816
|
+
{ environment: cloudEnvironment, values: cloudValues },
|
|
1817
|
+
context.plugins
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
const report = applyReportPlugins(
|
|
1821
|
+
diffEnvironmentSources({
|
|
1822
|
+
contract,
|
|
1823
|
+
sources,
|
|
1824
|
+
strict: context.strict,
|
|
1825
|
+
debugValues: context.debugValues
|
|
1826
|
+
}),
|
|
1827
|
+
context.plugins
|
|
1828
|
+
);
|
|
1829
|
+
return emitAndReturnExitCode(report, {
|
|
1830
|
+
jsonMode: args.jsonMode,
|
|
1831
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
async function runDoctorCommand(args) {
|
|
1835
|
+
const context = await prepareCommonContext(args.values);
|
|
1836
|
+
const loadContractOptions = {
|
|
1837
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1838
|
+
cwd: process.cwd(),
|
|
1839
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1840
|
+
};
|
|
1841
|
+
const contract = applyContractPlugins(
|
|
1842
|
+
await loadValidationContract(loadContractOptions),
|
|
1843
|
+
context.plugins
|
|
1844
|
+
);
|
|
1845
|
+
const checkEnvironment = args.values.env?.[0] ?? ".env";
|
|
1846
|
+
let checkValues = await loadEnvSource({ filePath: checkEnvironment, allowMissing: true });
|
|
1847
|
+
checkValues = applySourcePlugins(
|
|
1848
|
+
{ environment: checkEnvironment, values: checkValues },
|
|
1849
|
+
context.plugins
|
|
1850
|
+
);
|
|
1851
|
+
const checkReport = validateAgainstContract({
|
|
1852
|
+
contract,
|
|
1853
|
+
values: checkValues,
|
|
1854
|
+
environment: checkEnvironment,
|
|
1855
|
+
strict: context.strict,
|
|
1856
|
+
debugValues: context.debugValues
|
|
1857
|
+
});
|
|
1858
|
+
const sources = {};
|
|
1859
|
+
for (const target of parseTargets(args.values, context.fileConfig)) {
|
|
1860
|
+
const values = await loadEnvSource({ filePath: target, allowMissing: true });
|
|
1861
|
+
sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
|
|
1862
|
+
}
|
|
1863
|
+
if (context.cloudProvider !== void 0) {
|
|
1864
|
+
const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
|
|
1865
|
+
const cloudEnvironment = `cloud:${context.cloudProvider}`;
|
|
1866
|
+
const cloudValues = await loadCloudSource({
|
|
1867
|
+
provider: context.cloudProvider,
|
|
1868
|
+
filePath: cloudFile
|
|
1869
|
+
});
|
|
1870
|
+
sources[cloudEnvironment] = applySourcePlugins(
|
|
1871
|
+
{ environment: cloudEnvironment, values: cloudValues },
|
|
1872
|
+
context.plugins
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
const diffReport = diffEnvironmentSources({
|
|
1876
|
+
contract,
|
|
1877
|
+
sources,
|
|
1878
|
+
strict: context.strict,
|
|
1879
|
+
debugValues: context.debugValues
|
|
1880
|
+
});
|
|
1881
|
+
const report = applyReportPlugins(
|
|
1882
|
+
buildDoctorReport({ checkReport, diffReport }),
|
|
1883
|
+
context.plugins
|
|
1884
|
+
);
|
|
1885
|
+
return emitAndReturnExitCode(report, {
|
|
1886
|
+
jsonMode: args.jsonMode,
|
|
1887
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
async function runValidationCommand(params) {
|
|
1891
|
+
const parsed = parseValidationArgs(params.argv);
|
|
1892
|
+
if (parsed.values.help === true) {
|
|
1893
|
+
console.log(HELP_TEXT[params.command]);
|
|
1894
|
+
return 0;
|
|
1895
|
+
}
|
|
1896
|
+
if (params.command === "check") {
|
|
1897
|
+
return runCheckCommand(parsed);
|
|
1898
|
+
}
|
|
1899
|
+
if (params.command === "diff") {
|
|
1900
|
+
return runDiffCommand(parsed);
|
|
1901
|
+
}
|
|
1902
|
+
return runDoctorCommand(parsed);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/watch.ts
|
|
656
1906
|
import { watch } from "chokidar";
|
|
1907
|
+
import path12 from "path";
|
|
657
1908
|
function debounce(fn, delay) {
|
|
658
1909
|
let timer;
|
|
659
1910
|
return (...args) => {
|
|
@@ -688,6 +1939,9 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
688
1939
|
if (reloaded.inferenceRules !== void 0) {
|
|
689
1940
|
runOptions.inferenceRules = reloaded.inferenceRules;
|
|
690
1941
|
}
|
|
1942
|
+
if (reloaded.output !== void 0) {
|
|
1943
|
+
runOptions.output = reloaded.output;
|
|
1944
|
+
}
|
|
691
1945
|
}
|
|
692
1946
|
void runGenerate(runOptions).catch((err) => {
|
|
693
1947
|
const message = err instanceof Error ? err.message : JSON.stringify(err);
|
|
@@ -702,10 +1956,10 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
702
1956
|
for (const event of ["add", "change", "unlink"]) {
|
|
703
1957
|
inputWatcher.on(event, handleChange);
|
|
704
1958
|
}
|
|
705
|
-
const configPaths = CONFIG_FILE_NAMES.map((name) =>
|
|
1959
|
+
const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
|
|
706
1960
|
const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
|
|
707
1961
|
for (const event of ["add", "change"]) {
|
|
708
|
-
configWatcher.on(event, (eventPath) =>
|
|
1962
|
+
configWatcher.on(event, (eventPath) => handleConfigChange(eventPath));
|
|
709
1963
|
}
|
|
710
1964
|
process.on("SIGINT", () => {
|
|
711
1965
|
void Promise.all([inputWatcher.close(), configWatcher.close()]).then(() => {
|
|
@@ -718,15 +1972,20 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
718
1972
|
// src/cli.ts
|
|
719
1973
|
var _require = createRequire(import.meta.url);
|
|
720
1974
|
var VERSION = _require("../package.json").version;
|
|
721
|
-
var
|
|
1975
|
+
var HELP_TEXT2 = [
|
|
722
1976
|
"env-typegen \u2014 Generate TypeScript types from .env.example",
|
|
723
1977
|
"",
|
|
724
1978
|
"Usage:",
|
|
725
|
-
" env-typegen -i <path> [options]",
|
|
1979
|
+
" env-typegen [generate] -i <path> [options]",
|
|
1980
|
+
" env-typegen check [options]",
|
|
1981
|
+
" env-typegen diff [options]",
|
|
1982
|
+
" env-typegen doctor [options]",
|
|
726
1983
|
"",
|
|
727
1984
|
"Options:",
|
|
728
1985
|
" -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
|
|
729
|
-
" -o, --output <path> Output
|
|
1986
|
+
" -o, --output <path> Output base path (default: env.generated.ts).",
|
|
1987
|
+
" With multiple generators, suffixes are appended:",
|
|
1988
|
+
" env.generated.typescript.ts, .zod.ts, .t3.ts, .declaration.d.ts",
|
|
730
1989
|
" -f, --format <name> Generator format: ts|zod|t3|declaration",
|
|
731
1990
|
" May be specified multiple times.",
|
|
732
1991
|
" -g, --generator <name> Backward-compatible alias for --format",
|
|
@@ -737,8 +1996,18 @@ var HELP_TEXT = [
|
|
|
737
1996
|
" -w, --watch Watch for changes and regenerate",
|
|
738
1997
|
" -c, --config <path> Path to config file",
|
|
739
1998
|
" -v, --version Print version",
|
|
740
|
-
" -h, --help Show this help"
|
|
1999
|
+
" -h, --help Show this help",
|
|
2000
|
+
"",
|
|
2001
|
+
"Config file:",
|
|
2002
|
+
" Auto-discovered in order: env-typegen.config.mjs \u2192 .js \u2192 .ts (in cwd)",
|
|
2003
|
+
" CLI flags always override config file values.",
|
|
2004
|
+
" Use defineConfig() from @xlameiro/env-typegen for IDE autocompletion.",
|
|
2005
|
+
"",
|
|
2006
|
+
"Exit codes:",
|
|
2007
|
+
" 0 Success \u2014 files generated without errors",
|
|
2008
|
+
" 1 Error \u2014 invalid flags or generation failed"
|
|
741
2009
|
].join("\n");
|
|
2010
|
+
var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
|
|
742
2011
|
var FORMAT_TO_GENERATOR = {
|
|
743
2012
|
ts: "typescript",
|
|
744
2013
|
typescript: "typescript",
|
|
@@ -749,32 +2018,56 @@ var FORMAT_TO_GENERATOR = {
|
|
|
749
2018
|
function normalizeGenerator(input) {
|
|
750
2019
|
return FORMAT_TO_GENERATOR[input] ?? FORMAT_TO_GENERATOR[input.toLowerCase()];
|
|
751
2020
|
}
|
|
2021
|
+
function resolveGenerators(rawFormats, rawGenerators, fallback) {
|
|
2022
|
+
const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
|
|
2023
|
+
if (requested.length === 0) {
|
|
2024
|
+
return fallback ?? ["typescript", "zod", "t3", "declaration"];
|
|
2025
|
+
}
|
|
2026
|
+
const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
|
|
2027
|
+
const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
|
|
2028
|
+
if (invalid.length > 0) {
|
|
2029
|
+
error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
|
|
2030
|
+
process.exit(1);
|
|
2031
|
+
}
|
|
2032
|
+
return [...new Set(normalizedGenerators)];
|
|
2033
|
+
}
|
|
752
2034
|
function getErrorMessage(errorValue) {
|
|
753
2035
|
if (errorValue instanceof Error) {
|
|
754
2036
|
return errorValue.message;
|
|
755
2037
|
}
|
|
756
2038
|
return inspect(errorValue, { depth: 2 });
|
|
757
2039
|
}
|
|
758
|
-
function
|
|
759
|
-
if (value === void 0 ||
|
|
760
|
-
return
|
|
2040
|
+
function resolveConfigRelative2(value, configDir) {
|
|
2041
|
+
if (value === void 0 || path13.isAbsolute(value)) return value;
|
|
2042
|
+
return path13.resolve(configDir, value);
|
|
761
2043
|
}
|
|
762
|
-
function
|
|
2044
|
+
function applyConfigPaths2(config, configDir) {
|
|
763
2045
|
let input;
|
|
764
2046
|
if (Array.isArray(config.input)) {
|
|
765
|
-
input = config.input.map((v) =>
|
|
2047
|
+
input = config.input.map((v) => resolveConfigRelative2(v, configDir) ?? v);
|
|
766
2048
|
} else {
|
|
767
|
-
input =
|
|
2049
|
+
input = resolveConfigRelative2(config.input, configDir);
|
|
768
2050
|
}
|
|
769
|
-
const output =
|
|
2051
|
+
const output = resolveConfigRelative2(config.output, configDir);
|
|
770
2052
|
return {
|
|
771
2053
|
...config,
|
|
772
2054
|
...input !== void 0 && { input },
|
|
773
2055
|
...output !== void 0 && { output }
|
|
774
2056
|
};
|
|
775
2057
|
}
|
|
2058
|
+
async function runValidationSubcommand(subcommand, argv) {
|
|
2059
|
+
const exitCode = await runValidationCommand({ command: subcommand, argv });
|
|
2060
|
+
if (exitCode !== 0) {
|
|
2061
|
+
process.exitCode = exitCode;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
776
2064
|
async function runCli(argv = process.argv.slice(2)) {
|
|
777
|
-
const
|
|
2065
|
+
const maybeSubcommand = argv[0];
|
|
2066
|
+
if (maybeSubcommand !== void 0 && VALIDATION_SUBCOMMANDS.has(maybeSubcommand)) {
|
|
2067
|
+
await runValidationSubcommand(maybeSubcommand, argv.slice(1));
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
const { values } = parseArgs2({
|
|
778
2071
|
args: argv,
|
|
779
2072
|
options: {
|
|
780
2073
|
input: { type: "string", short: "i", multiple: true },
|
|
@@ -796,19 +2089,19 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
796
2089
|
return;
|
|
797
2090
|
}
|
|
798
2091
|
if (values.help === true) {
|
|
799
|
-
console.log(
|
|
2092
|
+
console.log(HELP_TEXT2);
|
|
800
2093
|
return;
|
|
801
2094
|
}
|
|
802
2095
|
let fileConfig;
|
|
803
2096
|
if (values.config === void 0) {
|
|
804
2097
|
fileConfig = await loadConfig(process.cwd());
|
|
805
2098
|
} else {
|
|
806
|
-
const configPath =
|
|
807
|
-
const configDir =
|
|
808
|
-
const mod = await import(
|
|
2099
|
+
const configPath = path13.resolve(values.config);
|
|
2100
|
+
const configDir = path13.dirname(configPath);
|
|
2101
|
+
const mod = await import(pathToFileURL5(configPath).href);
|
|
809
2102
|
const rawConfig = mod.default;
|
|
810
2103
|
if (rawConfig) {
|
|
811
|
-
fileConfig =
|
|
2104
|
+
fileConfig = applyConfigPaths2(rawConfig, configDir);
|
|
812
2105
|
}
|
|
813
2106
|
}
|
|
814
2107
|
const cliInput = values.input?.length ? values.input : void 0;
|
|
@@ -818,21 +2111,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
818
2111
|
process.exit(1);
|
|
819
2112
|
}
|
|
820
2113
|
const output = values.output ?? fileConfig?.output ?? "env.generated.ts";
|
|
821
|
-
const
|
|
822
|
-
const rawGenerators = values.generator;
|
|
823
|
-
const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
|
|
824
|
-
let generators;
|
|
825
|
-
if (requested.length > 0) {
|
|
826
|
-
const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
|
|
827
|
-
const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
|
|
828
|
-
if (invalid.length > 0) {
|
|
829
|
-
error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
|
|
830
|
-
process.exit(1);
|
|
831
|
-
}
|
|
832
|
-
generators = [...new Set(normalizedGenerators)];
|
|
833
|
-
} else {
|
|
834
|
-
generators = fileConfig?.generators ?? ["typescript", "zod", "t3", "declaration"];
|
|
835
|
-
}
|
|
2114
|
+
const generators = resolveGenerators(values.format, values.generator, fileConfig?.generators);
|
|
836
2115
|
const shouldFormat = values["no-format"] === true ? false : fileConfig?.format ?? true;
|
|
837
2116
|
const useStdout = values.stdout ?? false;
|
|
838
2117
|
const isDryRun = values["dry-run"] ?? false;
|
|
@@ -856,7 +2135,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
856
2135
|
}
|
|
857
2136
|
if (process.argv[1] !== void 0 && (() => {
|
|
858
2137
|
try {
|
|
859
|
-
return realpathSync(
|
|
2138
|
+
return realpathSync(path13.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
|
|
860
2139
|
} catch {
|
|
861
2140
|
return false;
|
|
862
2141
|
}
|