@xlameiro/env-typegen 0.1.3 → 0.1.5
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 +67 -105
- package/dist/cli.js +474 -351
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +451 -350
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +447 -347
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import path6 from 'path';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import { format } from 'prettier';
|
|
6
|
-
import
|
|
6
|
+
import pc from 'picocolors';
|
|
7
7
|
import { parseArgs } from 'util';
|
|
8
8
|
|
|
9
9
|
// src/parser/comment-parser.ts
|
|
@@ -143,7 +143,9 @@ var inferenceRules = [
|
|
|
143
143
|
{
|
|
144
144
|
id: "P8_numeric_literal",
|
|
145
145
|
priority: 8,
|
|
146
|
-
|
|
146
|
+
// Non-capturing group with \d keeps the dot/digit boundary unambiguous,
|
|
147
|
+
// eliminating super-linear backtracking (ReDoS-safe).
|
|
148
|
+
match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
|
|
147
149
|
type: "number"
|
|
148
150
|
},
|
|
149
151
|
{
|
|
@@ -161,7 +163,9 @@ var inferenceRules = [
|
|
|
161
163
|
{
|
|
162
164
|
id: "P11_email_literal",
|
|
163
165
|
priority: 11,
|
|
164
|
-
|
|
166
|
+
// Dots are excluded from each domain-segment character class so that the
|
|
167
|
+
// literal \. separators are unambiguous, preventing super-linear backtracking.
|
|
168
|
+
match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
|
|
165
169
|
type: "email"
|
|
166
170
|
},
|
|
167
171
|
{
|
|
@@ -195,7 +199,50 @@ function inferTypesFromParsedVars(parsed, options) {
|
|
|
195
199
|
return parsed.vars.map((item) => inferType(item.key, item.rawValue, options));
|
|
196
200
|
}
|
|
197
201
|
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
198
|
-
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(
|
|
202
|
+
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
|
|
203
|
+
function buildParsedVar(params, commentBlock, options) {
|
|
204
|
+
const annotations = parseCommentBlock(commentBlock);
|
|
205
|
+
const extraRules = options?.inferenceRules;
|
|
206
|
+
const inferredType = inferType(
|
|
207
|
+
params.key,
|
|
208
|
+
params.rawValue,
|
|
209
|
+
...extraRules === void 0 ? [] : [{ extraRules }]
|
|
210
|
+
);
|
|
211
|
+
const isRequired = params.rawValue.length > 0 || annotations.isRequired;
|
|
212
|
+
const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
|
|
213
|
+
const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
|
|
214
|
+
const parsedVar = {
|
|
215
|
+
key: params.key,
|
|
216
|
+
rawValue: params.rawValue,
|
|
217
|
+
inferredType,
|
|
218
|
+
isRequired,
|
|
219
|
+
isOptional,
|
|
220
|
+
isClientSide,
|
|
221
|
+
lineNumber: params.lineNumber
|
|
222
|
+
};
|
|
223
|
+
if (annotations.annotatedType !== void 0) {
|
|
224
|
+
parsedVar.annotatedType = annotations.annotatedType;
|
|
225
|
+
}
|
|
226
|
+
if (annotations.description !== void 0) {
|
|
227
|
+
parsedVar.description = annotations.description;
|
|
228
|
+
}
|
|
229
|
+
if (params.currentGroup !== void 0) {
|
|
230
|
+
parsedVar.group = params.currentGroup;
|
|
231
|
+
}
|
|
232
|
+
if (annotations.enumValues !== void 0) {
|
|
233
|
+
parsedVar.enumValues = annotations.enumValues;
|
|
234
|
+
}
|
|
235
|
+
if (annotations.constraints !== void 0) {
|
|
236
|
+
parsedVar.constraints = annotations.constraints;
|
|
237
|
+
}
|
|
238
|
+
if (annotations.runtime !== void 0) {
|
|
239
|
+
parsedVar.runtime = annotations.runtime;
|
|
240
|
+
}
|
|
241
|
+
if (annotations.isSecret !== void 0) {
|
|
242
|
+
parsedVar.isSecret = annotations.isSecret;
|
|
243
|
+
}
|
|
244
|
+
return parsedVar;
|
|
245
|
+
}
|
|
199
246
|
function parseEnvFileContent(content, filePath, options) {
|
|
200
247
|
const lines = content.split("\n");
|
|
201
248
|
const vars = [];
|
|
@@ -225,53 +272,18 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
225
272
|
continue;
|
|
226
273
|
}
|
|
227
274
|
const envMatch = ENV_VAR_RE.exec(trimmed);
|
|
228
|
-
if (envMatch
|
|
229
|
-
const key = envMatch[1] ?? "";
|
|
230
|
-
const rawValue = envMatch[2] ?? "";
|
|
231
|
-
const annotations = parseCommentBlock(commentBlock);
|
|
232
|
-
const inferredType = inferType(
|
|
233
|
-
key,
|
|
234
|
-
rawValue,
|
|
235
|
-
...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
|
|
236
|
-
);
|
|
237
|
-
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
238
|
-
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
239
|
-
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
240
|
-
const parsedVar = {
|
|
241
|
-
key,
|
|
242
|
-
rawValue,
|
|
243
|
-
inferredType,
|
|
244
|
-
isRequired,
|
|
245
|
-
isOptional,
|
|
246
|
-
isClientSide,
|
|
247
|
-
lineNumber
|
|
248
|
-
};
|
|
249
|
-
if (annotations.annotatedType !== void 0) {
|
|
250
|
-
parsedVar.annotatedType = annotations.annotatedType;
|
|
251
|
-
}
|
|
252
|
-
if (annotations.description !== void 0) {
|
|
253
|
-
parsedVar.description = annotations.description;
|
|
254
|
-
}
|
|
255
|
-
if (currentGroup !== void 0) {
|
|
256
|
-
parsedVar.group = currentGroup;
|
|
257
|
-
}
|
|
258
|
-
if (annotations.enumValues !== void 0) {
|
|
259
|
-
parsedVar.enumValues = annotations.enumValues;
|
|
260
|
-
}
|
|
261
|
-
if (annotations.constraints !== void 0) {
|
|
262
|
-
parsedVar.constraints = annotations.constraints;
|
|
263
|
-
}
|
|
264
|
-
if (annotations.runtime !== void 0) {
|
|
265
|
-
parsedVar.runtime = annotations.runtime;
|
|
266
|
-
}
|
|
267
|
-
if (annotations.isSecret !== void 0) {
|
|
268
|
-
parsedVar.isSecret = annotations.isSecret;
|
|
269
|
-
}
|
|
270
|
-
vars.push(parsedVar);
|
|
271
|
-
commentBlock = [];
|
|
272
|
-
} else {
|
|
275
|
+
if (envMatch === null) {
|
|
273
276
|
commentBlock = [];
|
|
277
|
+
continue;
|
|
274
278
|
}
|
|
279
|
+
vars.push(
|
|
280
|
+
buildParsedVar(
|
|
281
|
+
{ key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
|
|
282
|
+
commentBlock,
|
|
283
|
+
options
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
commentBlock = [];
|
|
275
287
|
}
|
|
276
288
|
return { filePath, vars, groups };
|
|
277
289
|
}
|
|
@@ -290,12 +302,14 @@ function generateTypeScriptTypes(parsed) {
|
|
|
290
302
|
const fileName = path6.basename(parsed.filePath);
|
|
291
303
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
292
304
|
const lines = [];
|
|
293
|
-
lines.push(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
305
|
+
lines.push(
|
|
306
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
307
|
+
`// Source: ${fileName}`,
|
|
308
|
+
`// Generated at: ${timestamp}`,
|
|
309
|
+
"",
|
|
310
|
+
"declare namespace NodeJS {",
|
|
311
|
+
" interface ProcessEnv {"
|
|
312
|
+
);
|
|
299
313
|
for (const variable of parsed.vars) {
|
|
300
314
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
301
315
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -310,10 +324,7 @@ function generateTypeScriptTypes(parsed) {
|
|
|
310
324
|
}
|
|
311
325
|
lines.push(propLine);
|
|
312
326
|
}
|
|
313
|
-
lines.push(" }");
|
|
314
|
-
lines.push("}");
|
|
315
|
-
lines.push("");
|
|
316
|
-
lines.push("export type EnvVars = {");
|
|
327
|
+
lines.push(" }", "}", "", "export type EnvVars = {");
|
|
317
328
|
for (const variable of parsed.vars) {
|
|
318
329
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
319
330
|
const tsType = toTsType(effectiveType);
|
|
@@ -323,28 +334,34 @@ function generateTypeScriptTypes(parsed) {
|
|
|
323
334
|
lines.push("};");
|
|
324
335
|
if (hasClientVars) {
|
|
325
336
|
const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
|
|
326
|
-
lines.push(
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
lines.push(
|
|
338
|
+
"",
|
|
339
|
+
`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
|
|
340
|
+
`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
|
|
341
|
+
);
|
|
329
342
|
}
|
|
330
343
|
return lines.join("\n") + "\n";
|
|
331
344
|
}
|
|
332
345
|
function generateEnvValidation(parsed) {
|
|
333
346
|
const required = parsed.vars.filter((v) => v.isRequired).map((v) => v.key);
|
|
334
347
|
const lines = [];
|
|
335
|
-
lines.push(
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
lines.push(
|
|
349
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
350
|
+
"",
|
|
351
|
+
"export function validateEnv(): void {"
|
|
352
|
+
);
|
|
338
353
|
if (required.length === 0) {
|
|
339
354
|
lines.push(" // No required environment variables defined");
|
|
340
355
|
} else {
|
|
341
356
|
const keyList = required.map((k) => `"${k}"`).join(", ");
|
|
342
|
-
lines.push(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
357
|
+
lines.push(
|
|
358
|
+
` const required = [${keyList}];`,
|
|
359
|
+
" for (const key of required) {",
|
|
360
|
+
" if (!process.env[key]) {",
|
|
361
|
+
" throw new Error(`Missing required environment variable: ${key}`);",
|
|
362
|
+
" }",
|
|
363
|
+
" }"
|
|
364
|
+
);
|
|
348
365
|
}
|
|
349
366
|
lines.push("}");
|
|
350
367
|
return lines.join("\n") + "\n";
|
|
@@ -362,37 +379,40 @@ function generateZodSchema(parsed) {
|
|
|
362
379
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
363
380
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
364
381
|
const lines = [];
|
|
365
|
-
lines.push(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
382
|
+
lines.push(
|
|
383
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
384
|
+
'import { z } from "zod";',
|
|
385
|
+
"",
|
|
386
|
+
"export const serverEnvSchema = z.object({"
|
|
387
|
+
);
|
|
369
388
|
for (const variable of serverVars) {
|
|
370
389
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
371
390
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
372
391
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
373
392
|
}
|
|
374
|
-
lines.push("});");
|
|
375
|
-
lines.push("");
|
|
376
|
-
lines.push("export const clientEnvSchema = z.object({");
|
|
393
|
+
lines.push("});", "", "export const clientEnvSchema = z.object({");
|
|
377
394
|
for (const variable of clientVars) {
|
|
378
395
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
379
396
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
380
397
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
381
398
|
}
|
|
382
|
-
lines.push(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
399
|
+
lines.push(
|
|
400
|
+
"});",
|
|
401
|
+
"",
|
|
402
|
+
"export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
|
|
403
|
+
"export type Env = z.infer<typeof envSchema>;"
|
|
404
|
+
);
|
|
386
405
|
return lines.join("\n") + "\n";
|
|
387
406
|
}
|
|
388
407
|
function generateDeclaration(parsed) {
|
|
389
408
|
const fileName = path6.basename(parsed.filePath);
|
|
390
|
-
const lines = [
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
409
|
+
const lines = [
|
|
410
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
411
|
+
`// Source: ${fileName}`,
|
|
412
|
+
"",
|
|
413
|
+
"declare namespace NodeJS {",
|
|
414
|
+
" interface ProcessEnv {"
|
|
415
|
+
];
|
|
396
416
|
for (const variable of parsed.vars) {
|
|
397
417
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
398
418
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -407,12 +427,14 @@ function generateDeclaration(parsed) {
|
|
|
407
427
|
}
|
|
408
428
|
lines.push(propLine);
|
|
409
429
|
}
|
|
410
|
-
lines.push(" }");
|
|
411
|
-
lines.push("}");
|
|
430
|
+
lines.push(" }", "}");
|
|
412
431
|
return lines.join("\n") + "\n";
|
|
413
432
|
}
|
|
414
433
|
|
|
415
434
|
// src/generators/t3-generator.ts
|
|
435
|
+
function escapeJsStringLiteral(value) {
|
|
436
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
|
|
437
|
+
}
|
|
416
438
|
function toT3ZodType(envVarType) {
|
|
417
439
|
if (envVarType === "number") return "z.coerce.number()";
|
|
418
440
|
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
@@ -420,51 +442,47 @@ function toT3ZodType(envVarType) {
|
|
|
420
442
|
if (envVarType === "email") return "z.string().email()";
|
|
421
443
|
return "z.string()";
|
|
422
444
|
}
|
|
445
|
+
function buildZodExpr(variable) {
|
|
446
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
447
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
448
|
+
if (variable.description !== void 0) {
|
|
449
|
+
zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
|
|
450
|
+
}
|
|
451
|
+
if (variable.isOptional) {
|
|
452
|
+
zodExpr += ".optional()";
|
|
453
|
+
}
|
|
454
|
+
return zodExpr;
|
|
455
|
+
}
|
|
423
456
|
function generateT3Env(parsed) {
|
|
424
457
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
425
458
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
426
|
-
const lines = [
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
459
|
+
const lines = [
|
|
460
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
461
|
+
'import { createEnv } from "@t3-oss/env-nextjs";',
|
|
462
|
+
'import { z } from "zod";',
|
|
463
|
+
"",
|
|
464
|
+
"export const env = createEnv({"
|
|
465
|
+
];
|
|
432
466
|
if (serverVars.length > 0) {
|
|
433
|
-
lines.push(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
439
|
-
}
|
|
440
|
-
if (variable.isOptional) {
|
|
441
|
-
zodExpr += ".optional()";
|
|
442
|
-
}
|
|
443
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
444
|
-
}
|
|
445
|
-
lines.push(" },");
|
|
467
|
+
lines.push(
|
|
468
|
+
" server: {",
|
|
469
|
+
...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
470
|
+
" },"
|
|
471
|
+
);
|
|
446
472
|
}
|
|
447
473
|
if (clientVars.length > 0) {
|
|
448
|
-
lines.push(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
454
|
-
}
|
|
455
|
-
if (variable.isOptional) {
|
|
456
|
-
zodExpr += ".optional()";
|
|
457
|
-
}
|
|
458
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
459
|
-
}
|
|
460
|
-
lines.push(" },");
|
|
461
|
-
}
|
|
462
|
-
lines.push(" runtimeEnv: {");
|
|
463
|
-
for (const variable of parsed.vars) {
|
|
464
|
-
lines.push(` ${variable.key}: process.env.${variable.key},`);
|
|
474
|
+
lines.push(
|
|
475
|
+
" client: {",
|
|
476
|
+
...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
477
|
+
" },"
|
|
478
|
+
);
|
|
465
479
|
}
|
|
466
|
-
lines.push(
|
|
467
|
-
|
|
480
|
+
lines.push(
|
|
481
|
+
" runtimeEnv: {",
|
|
482
|
+
...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
|
|
483
|
+
" },",
|
|
484
|
+
"});"
|
|
485
|
+
);
|
|
468
486
|
return lines.join("\n") + "\n";
|
|
469
487
|
}
|
|
470
488
|
var CONTRACT_FILE_NAMES = [
|
|
@@ -487,9 +505,9 @@ async function loadContract(cwd = process.cwd()) {
|
|
|
487
505
|
return void 0;
|
|
488
506
|
}
|
|
489
507
|
var CONFIG_FILE_NAMES = [
|
|
490
|
-
"env-typegen.config.ts",
|
|
491
508
|
"env-typegen.config.mjs",
|
|
492
|
-
"env-typegen.config.js"
|
|
509
|
+
"env-typegen.config.js",
|
|
510
|
+
"env-typegen.config.ts"
|
|
493
511
|
];
|
|
494
512
|
function defineConfig(config) {
|
|
495
513
|
return config;
|
|
@@ -498,6 +516,19 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
498
516
|
for (const name of CONFIG_FILE_NAMES) {
|
|
499
517
|
const filePath = path6.resolve(cwd, name);
|
|
500
518
|
if (existsSync(filePath)) {
|
|
519
|
+
if (filePath.endsWith(".ts")) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
|
|
522
|
+
Rename it to "env-typegen.config.mjs" and use ESM export syntax:
|
|
523
|
+
|
|
524
|
+
// env-typegen.config.mjs
|
|
525
|
+
import { defineConfig } from "@xlameiro/env-typegen";
|
|
526
|
+
export default defineConfig({ input: ".env.example" });
|
|
527
|
+
|
|
528
|
+
Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
|
|
529
|
+
env-typegen.config.mjs for runtime loading.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
501
532
|
const fileUrl = pathToFileURL(filePath).href;
|
|
502
533
|
const mod = await import(fileUrl);
|
|
503
534
|
return mod.default;
|
|
@@ -506,7 +537,15 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
506
537
|
return void 0;
|
|
507
538
|
}
|
|
508
539
|
async function readEnvFile(filePath) {
|
|
509
|
-
|
|
540
|
+
const resolved = path6.resolve(filePath);
|
|
541
|
+
try {
|
|
542
|
+
return await readFile(resolved, "utf8");
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
545
|
+
throw new Error(`File not found: ${filePath}`);
|
|
546
|
+
}
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
510
549
|
}
|
|
511
550
|
async function writeOutput(filePath, content) {
|
|
512
551
|
const resolved = path6.resolve(filePath);
|
|
@@ -524,6 +563,7 @@ async function formatOutput(content, parser = "typescript") {
|
|
|
524
563
|
return content;
|
|
525
564
|
}
|
|
526
565
|
}
|
|
566
|
+
var { green, red, yellow } = pc;
|
|
527
567
|
function log(message) {
|
|
528
568
|
console.log(message);
|
|
529
569
|
}
|
|
@@ -577,10 +617,6 @@ async function persistOutput(params) {
|
|
|
577
617
|
}
|
|
578
618
|
if (dryRun) {
|
|
579
619
|
if (!silent) {
|
|
580
|
-
if (!isSingle) {
|
|
581
|
-
console.log(`// --- ${generator}: ${outputPath} ---`);
|
|
582
|
-
}
|
|
583
|
-
console.log(generated);
|
|
584
620
|
success(`Dry run: would write ${outputPath}`);
|
|
585
621
|
}
|
|
586
622
|
return;
|
|
@@ -610,7 +646,7 @@ async function runGenerate(options) {
|
|
|
610
646
|
const parsed = parseEnvFileContent(
|
|
611
647
|
content,
|
|
612
648
|
inputPath,
|
|
613
|
-
inferenceRules2
|
|
649
|
+
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
614
650
|
);
|
|
615
651
|
for (const generator of generators) {
|
|
616
652
|
let generated = buildOutput(generator, parsed);
|
|
@@ -938,8 +974,13 @@ function readEntryValue(entry, keys) {
|
|
|
938
974
|
}
|
|
939
975
|
return void 0;
|
|
940
976
|
}
|
|
977
|
+
function getVercelEntries(value) {
|
|
978
|
+
if (Array.isArray(value)) return value;
|
|
979
|
+
if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
941
982
|
function parseVercelPayload(value) {
|
|
942
|
-
const entries =
|
|
983
|
+
const entries = getVercelEntries(value);
|
|
943
984
|
const result = {};
|
|
944
985
|
for (const entry of entries) {
|
|
945
986
|
if (!isRecord(entry)) continue;
|
|
@@ -950,8 +991,13 @@ function parseVercelPayload(value) {
|
|
|
950
991
|
}
|
|
951
992
|
return result;
|
|
952
993
|
}
|
|
994
|
+
function getCloudflareEntries(value) {
|
|
995
|
+
if (Array.isArray(value)) return value;
|
|
996
|
+
if (isRecord(value) && Array.isArray(value.result)) return value.result;
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
953
999
|
function parseCloudflarePayload(value) {
|
|
954
|
-
const entries =
|
|
1000
|
+
const entries = getCloudflareEntries(value);
|
|
955
1001
|
const result = {};
|
|
956
1002
|
for (const entry of entries) {
|
|
957
1003
|
if (!isRecord(entry)) continue;
|
|
@@ -969,7 +1015,7 @@ function parseAwsPayload(value) {
|
|
|
969
1015
|
if (!isRecord(entry)) continue;
|
|
970
1016
|
const name = readEntryValue(entry, ["Name", "name"]);
|
|
971
1017
|
if (name === void 0) continue;
|
|
972
|
-
const key = name.split("/").
|
|
1018
|
+
const key = name.split("/").findLast((part) => part.length > 0) ?? name;
|
|
973
1019
|
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
974
1020
|
result[key] = envValue;
|
|
975
1021
|
}
|
|
@@ -982,7 +1028,15 @@ function parseProviderPayload(provider, value) {
|
|
|
982
1028
|
}
|
|
983
1029
|
async function loadCloudSource(options) {
|
|
984
1030
|
const resolvedPath = path6.resolve(options.filePath);
|
|
985
|
-
|
|
1031
|
+
let raw;
|
|
1032
|
+
try {
|
|
1033
|
+
raw = await readFile(resolvedPath, "utf8");
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
1036
|
+
throw new Error(`File not found: ${options.filePath}`);
|
|
1037
|
+
}
|
|
1038
|
+
throw err;
|
|
1039
|
+
}
|
|
986
1040
|
const parsed = JSON.parse(raw);
|
|
987
1041
|
return parseProviderPayload(options.provider, parsed);
|
|
988
1042
|
}
|
|
@@ -1109,7 +1163,7 @@ function findDefaultContractPath(cwd) {
|
|
|
1109
1163
|
async function loadValidationContract(options) {
|
|
1110
1164
|
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
1111
1165
|
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
1112
|
-
const resolvedContractPath = contractPath
|
|
1166
|
+
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6.resolve(cwd, contractPath);
|
|
1113
1167
|
if (resolvedContractPath !== void 0 && existsSync(resolvedContractPath)) {
|
|
1114
1168
|
const moduleUrl = pathToFileURL(resolvedContractPath).href;
|
|
1115
1169
|
const moduleValue = await import(moduleUrl);
|
|
@@ -1145,10 +1199,20 @@ function isPlugin(value) {
|
|
|
1145
1199
|
}
|
|
1146
1200
|
async function loadPluginFromPath(pluginPath, cwd) {
|
|
1147
1201
|
const resolvedPath = path6.resolve(cwd, pluginPath);
|
|
1202
|
+
if (!existsSync(resolvedPath)) {
|
|
1203
|
+
throw new Error(`Plugin not found: ${pluginPath}`);
|
|
1204
|
+
}
|
|
1148
1205
|
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|
|
1149
1206
|
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
1150
1207
|
if (isPlugin(candidate)) return candidate;
|
|
1151
|
-
throw new Error(
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`Invalid plugin at ${resolvedPath}.
|
|
1210
|
+
Expected a default export matching:
|
|
1211
|
+
{ name: string,
|
|
1212
|
+
transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
|
|
1213
|
+
transformReport?(report: ValidationReport): ValidationReport,
|
|
1214
|
+
transformContract?(contract: EnvContract): EnvContract }`
|
|
1215
|
+
);
|
|
1152
1216
|
}
|
|
1153
1217
|
async function loadPlugins(options) {
|
|
1154
1218
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -1193,8 +1257,8 @@ function applyReportPlugins(report, plugins) {
|
|
|
1193
1257
|
}
|
|
1194
1258
|
|
|
1195
1259
|
// src/validation/engine.ts
|
|
1196
|
-
var EMAIL_RE = /^[^@\s]+@[^@\s]
|
|
1197
|
-
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[
|
|
1260
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
|
|
1261
|
+
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
1198
1262
|
function detectReceivedType(value) {
|
|
1199
1263
|
const normalized = value.trim();
|
|
1200
1264
|
if (normalized.length === 0) return "unknown";
|
|
@@ -1214,65 +1278,72 @@ function detectReceivedType(value) {
|
|
|
1214
1278
|
}
|
|
1215
1279
|
return "string";
|
|
1216
1280
|
}
|
|
1217
|
-
function
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
if (expected.type === "string") return { isValid: true, receivedType };
|
|
1222
|
-
if (expected.type === "number") {
|
|
1223
|
-
const parsed = Number(normalized);
|
|
1224
|
-
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1225
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1226
|
-
}
|
|
1227
|
-
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1228
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1229
|
-
}
|
|
1230
|
-
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1231
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1232
|
-
}
|
|
1233
|
-
return { isValid: true, receivedType };
|
|
1281
|
+
function validateNumber(expected, normalized, receivedType) {
|
|
1282
|
+
const parsed = Number(normalized);
|
|
1283
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1284
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1234
1285
|
}
|
|
1235
|
-
if (expected.
|
|
1236
|
-
|
|
1237
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1238
|
-
}
|
|
1239
|
-
return { isValid: true, receivedType };
|
|
1286
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1287
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1240
1288
|
}
|
|
1241
|
-
if (expected.
|
|
1242
|
-
|
|
1243
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1244
|
-
}
|
|
1245
|
-
return { isValid: true, receivedType };
|
|
1289
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1290
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1246
1291
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
return { isValid: true, receivedType };
|
|
1253
|
-
} catch {
|
|
1254
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1255
|
-
}
|
|
1292
|
+
return { isValid: true, receivedType };
|
|
1293
|
+
}
|
|
1294
|
+
function validateBoolean(normalized, receivedType) {
|
|
1295
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1296
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1256
1297
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1298
|
+
return { isValid: true, receivedType };
|
|
1299
|
+
}
|
|
1300
|
+
function validateEnum(expected, normalized, receivedType) {
|
|
1301
|
+
if (!expected.values.includes(normalized)) {
|
|
1302
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1303
|
+
}
|
|
1304
|
+
return { isValid: true, receivedType };
|
|
1305
|
+
}
|
|
1306
|
+
function validateUrl(normalized, receivedType) {
|
|
1307
|
+
try {
|
|
1308
|
+
const value = new URL(normalized);
|
|
1309
|
+
if (value.protocol.length === 0)
|
|
1259
1310
|
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1260
1311
|
return { isValid: true, receivedType };
|
|
1312
|
+
} catch {
|
|
1313
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1261
1314
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1315
|
+
}
|
|
1316
|
+
function validateEmail(normalized, receivedType) {
|
|
1317
|
+
if (!EMAIL_RE.test(normalized))
|
|
1318
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1319
|
+
return { isValid: true, receivedType };
|
|
1320
|
+
}
|
|
1321
|
+
function validateJson(normalized, receivedType) {
|
|
1322
|
+
try {
|
|
1323
|
+
const parsed = JSON.parse(normalized);
|
|
1324
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1325
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1326
|
+
} catch {
|
|
1327
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1270
1328
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1329
|
+
}
|
|
1330
|
+
function validateSemver(normalized, receivedType) {
|
|
1331
|
+
if (!SEMVER_RE2.test(normalized))
|
|
1332
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1333
|
+
return { isValid: true, receivedType };
|
|
1334
|
+
}
|
|
1335
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1336
|
+
const normalized = rawValue.trim();
|
|
1337
|
+
const receivedType = detectReceivedType(normalized);
|
|
1338
|
+
if (expected.type === "unknown" || expected.type === "string")
|
|
1274
1339
|
return { isValid: true, receivedType };
|
|
1275
|
-
|
|
1340
|
+
if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
|
|
1341
|
+
if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
|
|
1342
|
+
if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
|
|
1343
|
+
if (expected.type === "url") return validateUrl(normalized, receivedType);
|
|
1344
|
+
if (expected.type === "email") return validateEmail(normalized, receivedType);
|
|
1345
|
+
if (expected.type === "json") return validateJson(normalized, receivedType);
|
|
1346
|
+
if (expected.type === "semver") return validateSemver(normalized, receivedType);
|
|
1276
1347
|
return { isValid: true, receivedType };
|
|
1277
1348
|
}
|
|
1278
1349
|
function toIssueCode(issueType) {
|
|
@@ -1344,57 +1415,62 @@ function buildReport(env, issues, recommendations) {
|
|
|
1344
1415
|
function isClientSecret(variable, key) {
|
|
1345
1416
|
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1346
1417
|
}
|
|
1418
|
+
function checkContractVariable(key, variable, context) {
|
|
1419
|
+
const { options, issues } = context;
|
|
1420
|
+
const value = options.values[key];
|
|
1421
|
+
const hasValue = value !== void 0;
|
|
1422
|
+
if (variable.required && !hasValue) {
|
|
1423
|
+
issues.push(
|
|
1424
|
+
createIssue({
|
|
1425
|
+
type: "missing",
|
|
1426
|
+
severity: "error",
|
|
1427
|
+
key,
|
|
1428
|
+
environment: options.environment,
|
|
1429
|
+
message: `Required variable ${key} is missing.`,
|
|
1430
|
+
debugValues: options.debugValues,
|
|
1431
|
+
expected: variable.expected
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (!hasValue) return;
|
|
1437
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1438
|
+
if (!validation.isValid) {
|
|
1439
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
|
|
1440
|
+
issues.push(
|
|
1441
|
+
createIssue({
|
|
1442
|
+
type: validation.issueType,
|
|
1443
|
+
severity: "error",
|
|
1444
|
+
key,
|
|
1445
|
+
environment: options.environment,
|
|
1446
|
+
message,
|
|
1447
|
+
value,
|
|
1448
|
+
debugValues: options.debugValues,
|
|
1449
|
+
expected: variable.expected,
|
|
1450
|
+
receivedType: validation.receivedType
|
|
1451
|
+
})
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
if (isClientSecret(variable, key)) {
|
|
1455
|
+
issues.push(
|
|
1456
|
+
createIssue({
|
|
1457
|
+
type: "secret_exposed",
|
|
1458
|
+
severity: "error",
|
|
1459
|
+
key,
|
|
1460
|
+
environment: options.environment,
|
|
1461
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1462
|
+
value,
|
|
1463
|
+
debugValues: options.debugValues,
|
|
1464
|
+
expected: variable.expected
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1347
1469
|
function validateAgainstContract(options) {
|
|
1348
1470
|
const issues = [];
|
|
1349
1471
|
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1350
1472
|
for (const [key, variable] of Object.entries(options.contract.variables)) {
|
|
1351
|
-
|
|
1352
|
-
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1353
|
-
if (variable.required && !hasValue) {
|
|
1354
|
-
issues.push(
|
|
1355
|
-
createIssue({
|
|
1356
|
-
type: "missing",
|
|
1357
|
-
severity: "error",
|
|
1358
|
-
key,
|
|
1359
|
-
environment: options.environment,
|
|
1360
|
-
message: `Required variable ${key} is missing.`,
|
|
1361
|
-
debugValues: options.debugValues,
|
|
1362
|
-
expected: variable.expected
|
|
1363
|
-
})
|
|
1364
|
-
);
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
if (!hasValue) continue;
|
|
1368
|
-
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1369
|
-
if (!validation.isValid) {
|
|
1370
|
-
issues.push(
|
|
1371
|
-
createIssue({
|
|
1372
|
-
type: validation.issueType,
|
|
1373
|
-
severity: "error",
|
|
1374
|
-
key,
|
|
1375
|
-
environment: options.environment,
|
|
1376
|
-
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`,
|
|
1377
|
-
value,
|
|
1378
|
-
debugValues: options.debugValues,
|
|
1379
|
-
expected: variable.expected,
|
|
1380
|
-
receivedType: validation.receivedType
|
|
1381
|
-
})
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
if (isClientSecret(variable, key)) {
|
|
1385
|
-
issues.push(
|
|
1386
|
-
createIssue({
|
|
1387
|
-
type: "secret_exposed",
|
|
1388
|
-
severity: "error",
|
|
1389
|
-
key,
|
|
1390
|
-
environment: options.environment,
|
|
1391
|
-
message: `Secret variable ${key} is marked as client-side.`,
|
|
1392
|
-
value,
|
|
1393
|
-
debugValues: options.debugValues,
|
|
1394
|
-
expected: variable.expected
|
|
1395
|
-
})
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1473
|
+
checkContractVariable(key, variable, { options, issues });
|
|
1398
1474
|
}
|
|
1399
1475
|
for (const [key, value] of Object.entries(options.values)) {
|
|
1400
1476
|
if (contractKeys.has(key)) continue;
|
|
@@ -1422,6 +1498,97 @@ function collectUnionKeys(contract, sources) {
|
|
|
1422
1498
|
}
|
|
1423
1499
|
return union;
|
|
1424
1500
|
}
|
|
1501
|
+
function diffMissingEntries(key, missing, context) {
|
|
1502
|
+
const { variable, options, issues } = context;
|
|
1503
|
+
for (const entry of missing) {
|
|
1504
|
+
issues.push(
|
|
1505
|
+
createIssue({
|
|
1506
|
+
type: "missing",
|
|
1507
|
+
severity: "error",
|
|
1508
|
+
key,
|
|
1509
|
+
environment: entry.sourceName,
|
|
1510
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1511
|
+
debugValues: options.debugValues,
|
|
1512
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1513
|
+
})
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function diffTypeConflicts(key, present, context) {
|
|
1518
|
+
const { variable, options, issues } = context;
|
|
1519
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1520
|
+
for (const entry of present) {
|
|
1521
|
+
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1522
|
+
}
|
|
1523
|
+
if (new Set(typeBySource.values()).size <= 1) return;
|
|
1524
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1525
|
+
issues.push(
|
|
1526
|
+
createIssue({
|
|
1527
|
+
type: "conflict",
|
|
1528
|
+
severity: "error",
|
|
1529
|
+
key,
|
|
1530
|
+
environment: sourceName,
|
|
1531
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1532
|
+
debugValues: options.debugValues,
|
|
1533
|
+
receivedType: detectedType,
|
|
1534
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1535
|
+
value: options.sources[sourceName]?.[key]
|
|
1536
|
+
},
|
|
1537
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1538
|
+
})
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function diffPresentEntry(key, entry, context) {
|
|
1543
|
+
const { variable, options, issues } = context;
|
|
1544
|
+
if (entry.value === void 0) return;
|
|
1545
|
+
if (variable === void 0) {
|
|
1546
|
+
const severity = options.strict ? "error" : "warning";
|
|
1547
|
+
issues.push(
|
|
1548
|
+
createIssue({
|
|
1549
|
+
type: "extra",
|
|
1550
|
+
severity,
|
|
1551
|
+
key,
|
|
1552
|
+
environment: entry.sourceName,
|
|
1553
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1554
|
+
value: entry.value,
|
|
1555
|
+
debugValues: options.debugValues
|
|
1556
|
+
})
|
|
1557
|
+
);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1561
|
+
if (!validation.isValid) {
|
|
1562
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
|
|
1563
|
+
issues.push(
|
|
1564
|
+
createIssue({
|
|
1565
|
+
type: validation.issueType,
|
|
1566
|
+
severity: "error",
|
|
1567
|
+
key,
|
|
1568
|
+
environment: entry.sourceName,
|
|
1569
|
+
message,
|
|
1570
|
+
value: entry.value,
|
|
1571
|
+
debugValues: options.debugValues,
|
|
1572
|
+
expected: variable.expected,
|
|
1573
|
+
receivedType: validation.receivedType
|
|
1574
|
+
})
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
if (isClientSecret(variable, key)) {
|
|
1578
|
+
issues.push(
|
|
1579
|
+
createIssue({
|
|
1580
|
+
type: "secret_exposed",
|
|
1581
|
+
severity: "error",
|
|
1582
|
+
key,
|
|
1583
|
+
environment: entry.sourceName,
|
|
1584
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1585
|
+
value: entry.value,
|
|
1586
|
+
debugValues: options.debugValues,
|
|
1587
|
+
expected: variable.expected
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1425
1592
|
function diffEnvironmentSources(options) {
|
|
1426
1593
|
const issues = [];
|
|
1427
1594
|
const sourceNames = Object.keys(options.sources);
|
|
@@ -1432,12 +1599,8 @@ function diffEnvironmentSources(options) {
|
|
|
1432
1599
|
sourceName,
|
|
1433
1600
|
value: options.sources[sourceName]?.[key]
|
|
1434
1601
|
}));
|
|
1435
|
-
const present = valuesBySource.filter(
|
|
1436
|
-
|
|
1437
|
-
);
|
|
1438
|
-
const missing = valuesBySource.filter(
|
|
1439
|
-
(entry) => entry.value === void 0 || entry.value === ""
|
|
1440
|
-
);
|
|
1602
|
+
const present = valuesBySource.filter((entry) => entry.value !== void 0);
|
|
1603
|
+
const missing = valuesBySource.filter((entry) => entry.value === void 0);
|
|
1441
1604
|
if (present.length === 0 && variable?.required === true) {
|
|
1442
1605
|
for (const entry of missing) {
|
|
1443
1606
|
issues.push(
|
|
@@ -1454,92 +1617,13 @@ function diffEnvironmentSources(options) {
|
|
|
1454
1617
|
}
|
|
1455
1618
|
continue;
|
|
1456
1619
|
}
|
|
1620
|
+
const ctx = { variable, options, issues };
|
|
1457
1621
|
if (present.length > 0) {
|
|
1458
|
-
|
|
1459
|
-
issues.push(
|
|
1460
|
-
createIssue({
|
|
1461
|
-
type: "missing",
|
|
1462
|
-
severity: "error",
|
|
1463
|
-
key,
|
|
1464
|
-
environment: entry.sourceName,
|
|
1465
|
-
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1466
|
-
debugValues: options.debugValues,
|
|
1467
|
-
...variable !== void 0 && { expected: variable.expected }
|
|
1468
|
-
})
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1622
|
+
diffMissingEntries(key, missing, ctx);
|
|
1471
1623
|
}
|
|
1472
|
-
|
|
1624
|
+
diffTypeConflicts(key, present, ctx);
|
|
1473
1625
|
for (const entry of present) {
|
|
1474
|
-
|
|
1475
|
-
typeBySource.set(entry.sourceName, detected);
|
|
1476
|
-
}
|
|
1477
|
-
if (new Set(typeBySource.values()).size > 1) {
|
|
1478
|
-
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1479
|
-
issues.push(
|
|
1480
|
-
createIssue({
|
|
1481
|
-
type: "conflict",
|
|
1482
|
-
severity: "error",
|
|
1483
|
-
key,
|
|
1484
|
-
environment: sourceName,
|
|
1485
|
-
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1486
|
-
debugValues: options.debugValues,
|
|
1487
|
-
receivedType: detectedType,
|
|
1488
|
-
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1489
|
-
value: options.sources[sourceName]?.[key]
|
|
1490
|
-
},
|
|
1491
|
-
...variable !== void 0 && { expected: variable.expected }
|
|
1492
|
-
})
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
for (const entry of present) {
|
|
1497
|
-
if (entry.value === void 0) continue;
|
|
1498
|
-
if (variable === void 0) {
|
|
1499
|
-
const severity = options.strict ? "error" : "warning";
|
|
1500
|
-
issues.push(
|
|
1501
|
-
createIssue({
|
|
1502
|
-
type: "extra",
|
|
1503
|
-
severity,
|
|
1504
|
-
key,
|
|
1505
|
-
environment: entry.sourceName,
|
|
1506
|
-
message: `Variable ${key} is not defined in the contract.`,
|
|
1507
|
-
value: entry.value,
|
|
1508
|
-
debugValues: options.debugValues
|
|
1509
|
-
})
|
|
1510
|
-
);
|
|
1511
|
-
continue;
|
|
1512
|
-
}
|
|
1513
|
-
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1514
|
-
if (!validation.isValid) {
|
|
1515
|
-
issues.push(
|
|
1516
|
-
createIssue({
|
|
1517
|
-
type: validation.issueType,
|
|
1518
|
-
severity: "error",
|
|
1519
|
-
key,
|
|
1520
|
-
environment: entry.sourceName,
|
|
1521
|
-
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`,
|
|
1522
|
-
value: entry.value,
|
|
1523
|
-
debugValues: options.debugValues,
|
|
1524
|
-
expected: variable.expected,
|
|
1525
|
-
receivedType: validation.receivedType
|
|
1526
|
-
})
|
|
1527
|
-
);
|
|
1528
|
-
}
|
|
1529
|
-
if (isClientSecret(variable, key)) {
|
|
1530
|
-
issues.push(
|
|
1531
|
-
createIssue({
|
|
1532
|
-
type: "secret_exposed",
|
|
1533
|
-
severity: "error",
|
|
1534
|
-
key,
|
|
1535
|
-
environment: entry.sourceName,
|
|
1536
|
-
message: `Secret variable ${key} is marked as client-side.`,
|
|
1537
|
-
value: entry.value,
|
|
1538
|
-
debugValues: options.debugValues,
|
|
1539
|
-
expected: variable.expected
|
|
1540
|
-
})
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1626
|
+
diffPresentEntry(key, entry, ctx);
|
|
1543
1627
|
}
|
|
1544
1628
|
}
|
|
1545
1629
|
return buildReport("diff", issues);
|
|
@@ -1588,7 +1672,7 @@ function parseEnvSourceContent(content) {
|
|
|
1588
1672
|
for (const line of lines) {
|
|
1589
1673
|
const trimmed = line.trim();
|
|
1590
1674
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1591
|
-
const match = /^(?:export\s+)?([A-Za-z_]
|
|
1675
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
|
|
1592
1676
|
if (match === null) continue;
|
|
1593
1677
|
const key = match[1] ?? "";
|
|
1594
1678
|
const rawValue = match[2] ?? "";
|
|
@@ -1601,11 +1685,11 @@ async function loadEnvSource(options) {
|
|
|
1601
1685
|
try {
|
|
1602
1686
|
const content = await readFile(resolvedPath, "utf8");
|
|
1603
1687
|
return parseEnvSourceContent(content);
|
|
1604
|
-
} catch (
|
|
1605
|
-
if (options.allowMissing === true &&
|
|
1688
|
+
} catch (error_) {
|
|
1689
|
+
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1606
1690
|
return {};
|
|
1607
1691
|
}
|
|
1608
|
-
throw
|
|
1692
|
+
throw error_;
|
|
1609
1693
|
}
|
|
1610
1694
|
}
|
|
1611
1695
|
function toJsonString(report, mode) {
|
|
@@ -1615,8 +1699,8 @@ function toJsonString(report, mode) {
|
|
|
1615
1699
|
`;
|
|
1616
1700
|
}
|
|
1617
1701
|
function formatIssue(issue) {
|
|
1618
|
-
const expected = issue.expected
|
|
1619
|
-
const received = issue.receivedType
|
|
1702
|
+
const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
|
|
1703
|
+
const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
|
|
1620
1704
|
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1621
1705
|
}
|
|
1622
1706
|
function formatHumanReport(report) {
|
|
@@ -1625,15 +1709,13 @@ function formatHumanReport(report) {
|
|
|
1625
1709
|
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1626
1710
|
);
|
|
1627
1711
|
if (report.issues.length > 0) {
|
|
1628
|
-
lines.push("");
|
|
1629
|
-
lines.push("Issues:");
|
|
1712
|
+
lines.push("", "Issues:");
|
|
1630
1713
|
for (const issue of report.issues) {
|
|
1631
1714
|
lines.push(`- ${formatIssue(issue)}`);
|
|
1632
1715
|
}
|
|
1633
1716
|
}
|
|
1634
1717
|
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1635
|
-
lines.push("");
|
|
1636
|
-
lines.push("Recommendations:");
|
|
1718
|
+
lines.push("", "Recommendations:");
|
|
1637
1719
|
for (const recommendation of report.recommendations) {
|
|
1638
1720
|
lines.push(`- ${recommendation}`);
|
|
1639
1721
|
}
|
|
@@ -1678,7 +1760,11 @@ var HELP_TEXT = {
|
|
|
1678
1760
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1679
1761
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1680
1762
|
" -c, --config <path> Config file path",
|
|
1681
|
-
" -h, --help Show this help"
|
|
1763
|
+
" -h, --help Show this help",
|
|
1764
|
+
"",
|
|
1765
|
+
"Exit codes:",
|
|
1766
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1767
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1682
1768
|
].join("\n"),
|
|
1683
1769
|
diff: [
|
|
1684
1770
|
"Usage: env-typegen diff [options]",
|
|
@@ -1697,7 +1783,11 @@ var HELP_TEXT = {
|
|
|
1697
1783
|
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1698
1784
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1699
1785
|
" -c, --config <path> Config file path",
|
|
1700
|
-
" -h, --help Show this help"
|
|
1786
|
+
" -h, --help Show this help",
|
|
1787
|
+
"",
|
|
1788
|
+
"Exit codes:",
|
|
1789
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1790
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1701
1791
|
].join("\n"),
|
|
1702
1792
|
doctor: [
|
|
1703
1793
|
"Usage: env-typegen doctor [options]",
|
|
@@ -1717,7 +1807,11 @@ var HELP_TEXT = {
|
|
|
1717
1807
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1718
1808
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1719
1809
|
" -c, --config <path> Config file path",
|
|
1720
|
-
" -h, --help Show this help"
|
|
1810
|
+
" -h, --help Show this help",
|
|
1811
|
+
"",
|
|
1812
|
+
"Exit codes:",
|
|
1813
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1814
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1721
1815
|
].join("\n")
|
|
1722
1816
|
};
|
|
1723
1817
|
function resolveConfigRelative(value, configDir) {
|
|
@@ -1759,6 +1853,9 @@ async function loadCommandConfig(configPath) {
|
|
|
1759
1853
|
return loadConfig(process.cwd());
|
|
1760
1854
|
}
|
|
1761
1855
|
const resolvedPath = path6.resolve(configPath);
|
|
1856
|
+
if (!existsSync(resolvedPath)) {
|
|
1857
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
1858
|
+
}
|
|
1762
1859
|
const configDir = path6.dirname(resolvedPath);
|
|
1763
1860
|
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|
|
1764
1861
|
if (moduleValue.default === void 0) return void 0;
|
|
@@ -1804,7 +1901,10 @@ function parseValidationArgs(argv) {
|
|
|
1804
1901
|
}
|
|
1805
1902
|
});
|
|
1806
1903
|
const castValues = values;
|
|
1807
|
-
|
|
1904
|
+
let jsonMode = "off";
|
|
1905
|
+
if (castValues.json === true) {
|
|
1906
|
+
jsonMode = assignedMode === "off" ? "compact" : assignedMode;
|
|
1907
|
+
}
|
|
1808
1908
|
return { values: castValues, jsonMode };
|
|
1809
1909
|
}
|
|
1810
1910
|
function resolveStrict(values, fileConfig) {
|
|
@@ -1877,12 +1977,12 @@ async function runCheckCommand(args) {
|
|
|
1877
1977
|
const provider = context.cloudProvider;
|
|
1878
1978
|
let environment = args.values.env?.[0] ?? ".env";
|
|
1879
1979
|
let sourceValues;
|
|
1880
|
-
if (provider
|
|
1980
|
+
if (provider === void 0) {
|
|
1981
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1982
|
+
} else {
|
|
1881
1983
|
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1882
1984
|
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1883
1985
|
environment = `cloud:${provider}`;
|
|
1884
|
-
} else {
|
|
1885
|
-
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1886
1986
|
}
|
|
1887
1987
|
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1888
1988
|
const report = applyReportPlugins(
|