@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.cjs
CHANGED
|
@@ -5,12 +5,13 @@ var path6 = require('path');
|
|
|
5
5
|
var url = require('url');
|
|
6
6
|
var promises = require('fs/promises');
|
|
7
7
|
var prettier = require('prettier');
|
|
8
|
-
var
|
|
8
|
+
var pc = require('picocolors');
|
|
9
9
|
var util = require('util');
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
|
|
13
13
|
var path6__default = /*#__PURE__*/_interopDefault(path6);
|
|
14
|
+
var pc__default = /*#__PURE__*/_interopDefault(pc);
|
|
14
15
|
|
|
15
16
|
// src/parser/comment-parser.ts
|
|
16
17
|
var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -149,7 +150,9 @@ var inferenceRules = [
|
|
|
149
150
|
{
|
|
150
151
|
id: "P8_numeric_literal",
|
|
151
152
|
priority: 8,
|
|
152
|
-
|
|
153
|
+
// Non-capturing group with \d keeps the dot/digit boundary unambiguous,
|
|
154
|
+
// eliminating super-linear backtracking (ReDoS-safe).
|
|
155
|
+
match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
|
|
153
156
|
type: "number"
|
|
154
157
|
},
|
|
155
158
|
{
|
|
@@ -167,7 +170,9 @@ var inferenceRules = [
|
|
|
167
170
|
{
|
|
168
171
|
id: "P11_email_literal",
|
|
169
172
|
priority: 11,
|
|
170
|
-
|
|
173
|
+
// Dots are excluded from each domain-segment character class so that the
|
|
174
|
+
// literal \. separators are unambiguous, preventing super-linear backtracking.
|
|
175
|
+
match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
|
|
171
176
|
type: "email"
|
|
172
177
|
},
|
|
173
178
|
{
|
|
@@ -201,7 +206,50 @@ function inferTypesFromParsedVars(parsed, options) {
|
|
|
201
206
|
return parsed.vars.map((item) => inferType(item.key, item.rawValue, options));
|
|
202
207
|
}
|
|
203
208
|
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
204
|
-
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(
|
|
209
|
+
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
|
|
210
|
+
function buildParsedVar(params, commentBlock, options) {
|
|
211
|
+
const annotations = parseCommentBlock(commentBlock);
|
|
212
|
+
const extraRules = options?.inferenceRules;
|
|
213
|
+
const inferredType = inferType(
|
|
214
|
+
params.key,
|
|
215
|
+
params.rawValue,
|
|
216
|
+
...extraRules === void 0 ? [] : [{ extraRules }]
|
|
217
|
+
);
|
|
218
|
+
const isRequired = params.rawValue.length > 0 || annotations.isRequired;
|
|
219
|
+
const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
|
|
220
|
+
const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
|
|
221
|
+
const parsedVar = {
|
|
222
|
+
key: params.key,
|
|
223
|
+
rawValue: params.rawValue,
|
|
224
|
+
inferredType,
|
|
225
|
+
isRequired,
|
|
226
|
+
isOptional,
|
|
227
|
+
isClientSide,
|
|
228
|
+
lineNumber: params.lineNumber
|
|
229
|
+
};
|
|
230
|
+
if (annotations.annotatedType !== void 0) {
|
|
231
|
+
parsedVar.annotatedType = annotations.annotatedType;
|
|
232
|
+
}
|
|
233
|
+
if (annotations.description !== void 0) {
|
|
234
|
+
parsedVar.description = annotations.description;
|
|
235
|
+
}
|
|
236
|
+
if (params.currentGroup !== void 0) {
|
|
237
|
+
parsedVar.group = params.currentGroup;
|
|
238
|
+
}
|
|
239
|
+
if (annotations.enumValues !== void 0) {
|
|
240
|
+
parsedVar.enumValues = annotations.enumValues;
|
|
241
|
+
}
|
|
242
|
+
if (annotations.constraints !== void 0) {
|
|
243
|
+
parsedVar.constraints = annotations.constraints;
|
|
244
|
+
}
|
|
245
|
+
if (annotations.runtime !== void 0) {
|
|
246
|
+
parsedVar.runtime = annotations.runtime;
|
|
247
|
+
}
|
|
248
|
+
if (annotations.isSecret !== void 0) {
|
|
249
|
+
parsedVar.isSecret = annotations.isSecret;
|
|
250
|
+
}
|
|
251
|
+
return parsedVar;
|
|
252
|
+
}
|
|
205
253
|
function parseEnvFileContent(content, filePath, options) {
|
|
206
254
|
const lines = content.split("\n");
|
|
207
255
|
const vars = [];
|
|
@@ -231,53 +279,18 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
231
279
|
continue;
|
|
232
280
|
}
|
|
233
281
|
const envMatch = ENV_VAR_RE.exec(trimmed);
|
|
234
|
-
if (envMatch
|
|
235
|
-
const key = envMatch[1] ?? "";
|
|
236
|
-
const rawValue = envMatch[2] ?? "";
|
|
237
|
-
const annotations = parseCommentBlock(commentBlock);
|
|
238
|
-
const inferredType = inferType(
|
|
239
|
-
key,
|
|
240
|
-
rawValue,
|
|
241
|
-
...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
|
|
242
|
-
);
|
|
243
|
-
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
244
|
-
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
245
|
-
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
246
|
-
const parsedVar = {
|
|
247
|
-
key,
|
|
248
|
-
rawValue,
|
|
249
|
-
inferredType,
|
|
250
|
-
isRequired,
|
|
251
|
-
isOptional,
|
|
252
|
-
isClientSide,
|
|
253
|
-
lineNumber
|
|
254
|
-
};
|
|
255
|
-
if (annotations.annotatedType !== void 0) {
|
|
256
|
-
parsedVar.annotatedType = annotations.annotatedType;
|
|
257
|
-
}
|
|
258
|
-
if (annotations.description !== void 0) {
|
|
259
|
-
parsedVar.description = annotations.description;
|
|
260
|
-
}
|
|
261
|
-
if (currentGroup !== void 0) {
|
|
262
|
-
parsedVar.group = currentGroup;
|
|
263
|
-
}
|
|
264
|
-
if (annotations.enumValues !== void 0) {
|
|
265
|
-
parsedVar.enumValues = annotations.enumValues;
|
|
266
|
-
}
|
|
267
|
-
if (annotations.constraints !== void 0) {
|
|
268
|
-
parsedVar.constraints = annotations.constraints;
|
|
269
|
-
}
|
|
270
|
-
if (annotations.runtime !== void 0) {
|
|
271
|
-
parsedVar.runtime = annotations.runtime;
|
|
272
|
-
}
|
|
273
|
-
if (annotations.isSecret !== void 0) {
|
|
274
|
-
parsedVar.isSecret = annotations.isSecret;
|
|
275
|
-
}
|
|
276
|
-
vars.push(parsedVar);
|
|
277
|
-
commentBlock = [];
|
|
278
|
-
} else {
|
|
282
|
+
if (envMatch === null) {
|
|
279
283
|
commentBlock = [];
|
|
284
|
+
continue;
|
|
280
285
|
}
|
|
286
|
+
vars.push(
|
|
287
|
+
buildParsedVar(
|
|
288
|
+
{ key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
|
|
289
|
+
commentBlock,
|
|
290
|
+
options
|
|
291
|
+
)
|
|
292
|
+
);
|
|
293
|
+
commentBlock = [];
|
|
281
294
|
}
|
|
282
295
|
return { filePath, vars, groups };
|
|
283
296
|
}
|
|
@@ -296,12 +309,14 @@ function generateTypeScriptTypes(parsed) {
|
|
|
296
309
|
const fileName = path6__default.default.basename(parsed.filePath);
|
|
297
310
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
298
311
|
const lines = [];
|
|
299
|
-
lines.push(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
312
|
+
lines.push(
|
|
313
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
314
|
+
`// Source: ${fileName}`,
|
|
315
|
+
`// Generated at: ${timestamp}`,
|
|
316
|
+
"",
|
|
317
|
+
"declare namespace NodeJS {",
|
|
318
|
+
" interface ProcessEnv {"
|
|
319
|
+
);
|
|
305
320
|
for (const variable of parsed.vars) {
|
|
306
321
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
307
322
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -316,10 +331,7 @@ function generateTypeScriptTypes(parsed) {
|
|
|
316
331
|
}
|
|
317
332
|
lines.push(propLine);
|
|
318
333
|
}
|
|
319
|
-
lines.push(" }");
|
|
320
|
-
lines.push("}");
|
|
321
|
-
lines.push("");
|
|
322
|
-
lines.push("export type EnvVars = {");
|
|
334
|
+
lines.push(" }", "}", "", "export type EnvVars = {");
|
|
323
335
|
for (const variable of parsed.vars) {
|
|
324
336
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
325
337
|
const tsType = toTsType(effectiveType);
|
|
@@ -329,28 +341,34 @@ function generateTypeScriptTypes(parsed) {
|
|
|
329
341
|
lines.push("};");
|
|
330
342
|
if (hasClientVars) {
|
|
331
343
|
const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
|
|
332
|
-
lines.push(
|
|
333
|
-
|
|
334
|
-
|
|
344
|
+
lines.push(
|
|
345
|
+
"",
|
|
346
|
+
`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
|
|
347
|
+
`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
|
|
348
|
+
);
|
|
335
349
|
}
|
|
336
350
|
return lines.join("\n") + "\n";
|
|
337
351
|
}
|
|
338
352
|
function generateEnvValidation(parsed) {
|
|
339
353
|
const required = parsed.vars.filter((v) => v.isRequired).map((v) => v.key);
|
|
340
354
|
const lines = [];
|
|
341
|
-
lines.push(
|
|
342
|
-
|
|
343
|
-
|
|
355
|
+
lines.push(
|
|
356
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
357
|
+
"",
|
|
358
|
+
"export function validateEnv(): void {"
|
|
359
|
+
);
|
|
344
360
|
if (required.length === 0) {
|
|
345
361
|
lines.push(" // No required environment variables defined");
|
|
346
362
|
} else {
|
|
347
363
|
const keyList = required.map((k) => `"${k}"`).join(", ");
|
|
348
|
-
lines.push(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
lines.push(
|
|
365
|
+
` const required = [${keyList}];`,
|
|
366
|
+
" for (const key of required) {",
|
|
367
|
+
" if (!process.env[key]) {",
|
|
368
|
+
" throw new Error(`Missing required environment variable: ${key}`);",
|
|
369
|
+
" }",
|
|
370
|
+
" }"
|
|
371
|
+
);
|
|
354
372
|
}
|
|
355
373
|
lines.push("}");
|
|
356
374
|
return lines.join("\n") + "\n";
|
|
@@ -368,37 +386,40 @@ function generateZodSchema(parsed) {
|
|
|
368
386
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
369
387
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
370
388
|
const lines = [];
|
|
371
|
-
lines.push(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
389
|
+
lines.push(
|
|
390
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
391
|
+
'import { z } from "zod";',
|
|
392
|
+
"",
|
|
393
|
+
"export const serverEnvSchema = z.object({"
|
|
394
|
+
);
|
|
375
395
|
for (const variable of serverVars) {
|
|
376
396
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
377
397
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
378
398
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
379
399
|
}
|
|
380
|
-
lines.push("});");
|
|
381
|
-
lines.push("");
|
|
382
|
-
lines.push("export const clientEnvSchema = z.object({");
|
|
400
|
+
lines.push("});", "", "export const clientEnvSchema = z.object({");
|
|
383
401
|
for (const variable of clientVars) {
|
|
384
402
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
385
403
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
386
404
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
387
405
|
}
|
|
388
|
-
lines.push(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
406
|
+
lines.push(
|
|
407
|
+
"});",
|
|
408
|
+
"",
|
|
409
|
+
"export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
|
|
410
|
+
"export type Env = z.infer<typeof envSchema>;"
|
|
411
|
+
);
|
|
392
412
|
return lines.join("\n") + "\n";
|
|
393
413
|
}
|
|
394
414
|
function generateDeclaration(parsed) {
|
|
395
415
|
const fileName = path6__default.default.basename(parsed.filePath);
|
|
396
|
-
const lines = [
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
416
|
+
const lines = [
|
|
417
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
418
|
+
`// Source: ${fileName}`,
|
|
419
|
+
"",
|
|
420
|
+
"declare namespace NodeJS {",
|
|
421
|
+
" interface ProcessEnv {"
|
|
422
|
+
];
|
|
402
423
|
for (const variable of parsed.vars) {
|
|
403
424
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
404
425
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -413,12 +434,14 @@ function generateDeclaration(parsed) {
|
|
|
413
434
|
}
|
|
414
435
|
lines.push(propLine);
|
|
415
436
|
}
|
|
416
|
-
lines.push(" }");
|
|
417
|
-
lines.push("}");
|
|
437
|
+
lines.push(" }", "}");
|
|
418
438
|
return lines.join("\n") + "\n";
|
|
419
439
|
}
|
|
420
440
|
|
|
421
441
|
// src/generators/t3-generator.ts
|
|
442
|
+
function escapeJsStringLiteral(value) {
|
|
443
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
|
|
444
|
+
}
|
|
422
445
|
function toT3ZodType(envVarType) {
|
|
423
446
|
if (envVarType === "number") return "z.coerce.number()";
|
|
424
447
|
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
@@ -426,51 +449,47 @@ function toT3ZodType(envVarType) {
|
|
|
426
449
|
if (envVarType === "email") return "z.string().email()";
|
|
427
450
|
return "z.string()";
|
|
428
451
|
}
|
|
452
|
+
function buildZodExpr(variable) {
|
|
453
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
454
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
455
|
+
if (variable.description !== void 0) {
|
|
456
|
+
zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
|
|
457
|
+
}
|
|
458
|
+
if (variable.isOptional) {
|
|
459
|
+
zodExpr += ".optional()";
|
|
460
|
+
}
|
|
461
|
+
return zodExpr;
|
|
462
|
+
}
|
|
429
463
|
function generateT3Env(parsed) {
|
|
430
464
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
431
465
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
432
|
-
const lines = [
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
466
|
+
const lines = [
|
|
467
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
468
|
+
'import { createEnv } from "@t3-oss/env-nextjs";',
|
|
469
|
+
'import { z } from "zod";',
|
|
470
|
+
"",
|
|
471
|
+
"export const env = createEnv({"
|
|
472
|
+
];
|
|
438
473
|
if (serverVars.length > 0) {
|
|
439
|
-
lines.push(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
445
|
-
}
|
|
446
|
-
if (variable.isOptional) {
|
|
447
|
-
zodExpr += ".optional()";
|
|
448
|
-
}
|
|
449
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
450
|
-
}
|
|
451
|
-
lines.push(" },");
|
|
474
|
+
lines.push(
|
|
475
|
+
" server: {",
|
|
476
|
+
...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
477
|
+
" },"
|
|
478
|
+
);
|
|
452
479
|
}
|
|
453
480
|
if (clientVars.length > 0) {
|
|
454
|
-
lines.push(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
460
|
-
}
|
|
461
|
-
if (variable.isOptional) {
|
|
462
|
-
zodExpr += ".optional()";
|
|
463
|
-
}
|
|
464
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
465
|
-
}
|
|
466
|
-
lines.push(" },");
|
|
467
|
-
}
|
|
468
|
-
lines.push(" runtimeEnv: {");
|
|
469
|
-
for (const variable of parsed.vars) {
|
|
470
|
-
lines.push(` ${variable.key}: process.env.${variable.key},`);
|
|
481
|
+
lines.push(
|
|
482
|
+
" client: {",
|
|
483
|
+
...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
484
|
+
" },"
|
|
485
|
+
);
|
|
471
486
|
}
|
|
472
|
-
lines.push(
|
|
473
|
-
|
|
487
|
+
lines.push(
|
|
488
|
+
" runtimeEnv: {",
|
|
489
|
+
...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
|
|
490
|
+
" },",
|
|
491
|
+
"});"
|
|
492
|
+
);
|
|
474
493
|
return lines.join("\n") + "\n";
|
|
475
494
|
}
|
|
476
495
|
var CONTRACT_FILE_NAMES = [
|
|
@@ -493,9 +512,9 @@ async function loadContract(cwd = process.cwd()) {
|
|
|
493
512
|
return void 0;
|
|
494
513
|
}
|
|
495
514
|
var CONFIG_FILE_NAMES = [
|
|
496
|
-
"env-typegen.config.ts",
|
|
497
515
|
"env-typegen.config.mjs",
|
|
498
|
-
"env-typegen.config.js"
|
|
516
|
+
"env-typegen.config.js",
|
|
517
|
+
"env-typegen.config.ts"
|
|
499
518
|
];
|
|
500
519
|
function defineConfig(config) {
|
|
501
520
|
return config;
|
|
@@ -504,6 +523,19 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
504
523
|
for (const name of CONFIG_FILE_NAMES) {
|
|
505
524
|
const filePath = path6__default.default.resolve(cwd, name);
|
|
506
525
|
if (fs.existsSync(filePath)) {
|
|
526
|
+
if (filePath.endsWith(".ts")) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
|
|
529
|
+
Rename it to "env-typegen.config.mjs" and use ESM export syntax:
|
|
530
|
+
|
|
531
|
+
// env-typegen.config.mjs
|
|
532
|
+
import { defineConfig } from "@xlameiro/env-typegen";
|
|
533
|
+
export default defineConfig({ input: ".env.example" });
|
|
534
|
+
|
|
535
|
+
Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
|
|
536
|
+
env-typegen.config.mjs for runtime loading.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
507
539
|
const fileUrl = url.pathToFileURL(filePath).href;
|
|
508
540
|
const mod = await import(fileUrl);
|
|
509
541
|
return mod.default;
|
|
@@ -512,7 +544,15 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
512
544
|
return void 0;
|
|
513
545
|
}
|
|
514
546
|
async function readEnvFile(filePath) {
|
|
515
|
-
|
|
547
|
+
const resolved = path6__default.default.resolve(filePath);
|
|
548
|
+
try {
|
|
549
|
+
return await promises.readFile(resolved, "utf8");
|
|
550
|
+
} catch (err) {
|
|
551
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
552
|
+
throw new Error(`File not found: ${filePath}`);
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
516
556
|
}
|
|
517
557
|
async function writeOutput(filePath, content) {
|
|
518
558
|
const resolved = path6__default.default.resolve(filePath);
|
|
@@ -530,17 +570,18 @@ async function formatOutput(content, parser = "typescript") {
|
|
|
530
570
|
return content;
|
|
531
571
|
}
|
|
532
572
|
}
|
|
573
|
+
var { green, red, yellow } = pc__default.default;
|
|
533
574
|
function log(message) {
|
|
534
575
|
console.log(message);
|
|
535
576
|
}
|
|
536
577
|
function warn(message) {
|
|
537
|
-
console.warn(
|
|
578
|
+
console.warn(yellow(`\u26A0 ${message}`));
|
|
538
579
|
}
|
|
539
580
|
function error(message) {
|
|
540
|
-
console.error(
|
|
581
|
+
console.error(red(`\u2716 ${message}`));
|
|
541
582
|
}
|
|
542
583
|
function success(message) {
|
|
543
|
-
console.log(
|
|
584
|
+
console.log(green(`\u2714 ${message}`));
|
|
544
585
|
}
|
|
545
586
|
|
|
546
587
|
// src/pipeline.ts
|
|
@@ -583,10 +624,6 @@ async function persistOutput(params) {
|
|
|
583
624
|
}
|
|
584
625
|
if (dryRun) {
|
|
585
626
|
if (!silent) {
|
|
586
|
-
if (!isSingle) {
|
|
587
|
-
console.log(`// --- ${generator}: ${outputPath} ---`);
|
|
588
|
-
}
|
|
589
|
-
console.log(generated);
|
|
590
627
|
success(`Dry run: would write ${outputPath}`);
|
|
591
628
|
}
|
|
592
629
|
return;
|
|
@@ -616,7 +653,7 @@ async function runGenerate(options) {
|
|
|
616
653
|
const parsed = parseEnvFileContent(
|
|
617
654
|
content,
|
|
618
655
|
inputPath,
|
|
619
|
-
inferenceRules2
|
|
656
|
+
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
620
657
|
);
|
|
621
658
|
for (const generator of generators) {
|
|
622
659
|
let generated = buildOutput(generator, parsed);
|
|
@@ -944,8 +981,13 @@ function readEntryValue(entry, keys) {
|
|
|
944
981
|
}
|
|
945
982
|
return void 0;
|
|
946
983
|
}
|
|
984
|
+
function getVercelEntries(value) {
|
|
985
|
+
if (Array.isArray(value)) return value;
|
|
986
|
+
if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
|
|
987
|
+
return [];
|
|
988
|
+
}
|
|
947
989
|
function parseVercelPayload(value) {
|
|
948
|
-
const entries =
|
|
990
|
+
const entries = getVercelEntries(value);
|
|
949
991
|
const result = {};
|
|
950
992
|
for (const entry of entries) {
|
|
951
993
|
if (!isRecord(entry)) continue;
|
|
@@ -956,8 +998,13 @@ function parseVercelPayload(value) {
|
|
|
956
998
|
}
|
|
957
999
|
return result;
|
|
958
1000
|
}
|
|
1001
|
+
function getCloudflareEntries(value) {
|
|
1002
|
+
if (Array.isArray(value)) return value;
|
|
1003
|
+
if (isRecord(value) && Array.isArray(value.result)) return value.result;
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
959
1006
|
function parseCloudflarePayload(value) {
|
|
960
|
-
const entries =
|
|
1007
|
+
const entries = getCloudflareEntries(value);
|
|
961
1008
|
const result = {};
|
|
962
1009
|
for (const entry of entries) {
|
|
963
1010
|
if (!isRecord(entry)) continue;
|
|
@@ -975,7 +1022,7 @@ function parseAwsPayload(value) {
|
|
|
975
1022
|
if (!isRecord(entry)) continue;
|
|
976
1023
|
const name = readEntryValue(entry, ["Name", "name"]);
|
|
977
1024
|
if (name === void 0) continue;
|
|
978
|
-
const key = name.split("/").
|
|
1025
|
+
const key = name.split("/").findLast((part) => part.length > 0) ?? name;
|
|
979
1026
|
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
980
1027
|
result[key] = envValue;
|
|
981
1028
|
}
|
|
@@ -988,7 +1035,15 @@ function parseProviderPayload(provider, value) {
|
|
|
988
1035
|
}
|
|
989
1036
|
async function loadCloudSource(options) {
|
|
990
1037
|
const resolvedPath = path6__default.default.resolve(options.filePath);
|
|
991
|
-
|
|
1038
|
+
let raw;
|
|
1039
|
+
try {
|
|
1040
|
+
raw = await promises.readFile(resolvedPath, "utf8");
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
1043
|
+
throw new Error(`File not found: ${options.filePath}`);
|
|
1044
|
+
}
|
|
1045
|
+
throw err;
|
|
1046
|
+
}
|
|
992
1047
|
const parsed = JSON.parse(raw);
|
|
993
1048
|
return parseProviderPayload(options.provider, parsed);
|
|
994
1049
|
}
|
|
@@ -1115,7 +1170,7 @@ function findDefaultContractPath(cwd) {
|
|
|
1115
1170
|
async function loadValidationContract(options) {
|
|
1116
1171
|
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
1117
1172
|
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
1118
|
-
const resolvedContractPath = contractPath
|
|
1173
|
+
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6__default.default.resolve(cwd, contractPath);
|
|
1119
1174
|
if (resolvedContractPath !== void 0 && fs.existsSync(resolvedContractPath)) {
|
|
1120
1175
|
const moduleUrl = url.pathToFileURL(resolvedContractPath).href;
|
|
1121
1176
|
const moduleValue = await import(moduleUrl);
|
|
@@ -1151,10 +1206,20 @@ function isPlugin(value) {
|
|
|
1151
1206
|
}
|
|
1152
1207
|
async function loadPluginFromPath(pluginPath, cwd) {
|
|
1153
1208
|
const resolvedPath = path6__default.default.resolve(cwd, pluginPath);
|
|
1209
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1210
|
+
throw new Error(`Plugin not found: ${pluginPath}`);
|
|
1211
|
+
}
|
|
1154
1212
|
const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
|
|
1155
1213
|
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
1156
1214
|
if (isPlugin(candidate)) return candidate;
|
|
1157
|
-
throw new Error(
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
`Invalid plugin at ${resolvedPath}.
|
|
1217
|
+
Expected a default export matching:
|
|
1218
|
+
{ name: string,
|
|
1219
|
+
transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
|
|
1220
|
+
transformReport?(report: ValidationReport): ValidationReport,
|
|
1221
|
+
transformContract?(contract: EnvContract): EnvContract }`
|
|
1222
|
+
);
|
|
1158
1223
|
}
|
|
1159
1224
|
async function loadPlugins(options) {
|
|
1160
1225
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -1199,8 +1264,8 @@ function applyReportPlugins(report, plugins) {
|
|
|
1199
1264
|
}
|
|
1200
1265
|
|
|
1201
1266
|
// src/validation/engine.ts
|
|
1202
|
-
var EMAIL_RE = /^[^@\s]+@[^@\s]
|
|
1203
|
-
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[
|
|
1267
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
|
|
1268
|
+
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
1204
1269
|
function detectReceivedType(value) {
|
|
1205
1270
|
const normalized = value.trim();
|
|
1206
1271
|
if (normalized.length === 0) return "unknown";
|
|
@@ -1220,65 +1285,72 @@ function detectReceivedType(value) {
|
|
|
1220
1285
|
}
|
|
1221
1286
|
return "string";
|
|
1222
1287
|
}
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (expected.type === "string") return { isValid: true, receivedType };
|
|
1228
|
-
if (expected.type === "number") {
|
|
1229
|
-
const parsed = Number(normalized);
|
|
1230
|
-
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1231
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1232
|
-
}
|
|
1233
|
-
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1234
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1235
|
-
}
|
|
1236
|
-
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1237
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1238
|
-
}
|
|
1239
|
-
return { isValid: true, receivedType };
|
|
1288
|
+
function validateNumber(expected, normalized, receivedType) {
|
|
1289
|
+
const parsed = Number(normalized);
|
|
1290
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1291
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1240
1292
|
}
|
|
1241
|
-
if (expected.
|
|
1242
|
-
|
|
1243
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1244
|
-
}
|
|
1245
|
-
return { isValid: true, receivedType };
|
|
1293
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1294
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1246
1295
|
}
|
|
1247
|
-
if (expected.
|
|
1248
|
-
|
|
1249
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1250
|
-
}
|
|
1251
|
-
return { isValid: true, receivedType };
|
|
1296
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1297
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1252
1298
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
return { isValid: true, receivedType };
|
|
1259
|
-
} catch {
|
|
1260
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1261
|
-
}
|
|
1299
|
+
return { isValid: true, receivedType };
|
|
1300
|
+
}
|
|
1301
|
+
function validateBoolean(normalized, receivedType) {
|
|
1302
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1303
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1262
1304
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1305
|
+
return { isValid: true, receivedType };
|
|
1306
|
+
}
|
|
1307
|
+
function validateEnum(expected, normalized, receivedType) {
|
|
1308
|
+
if (!expected.values.includes(normalized)) {
|
|
1309
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1310
|
+
}
|
|
1311
|
+
return { isValid: true, receivedType };
|
|
1312
|
+
}
|
|
1313
|
+
function validateUrl(normalized, receivedType) {
|
|
1314
|
+
try {
|
|
1315
|
+
const value = new URL(normalized);
|
|
1316
|
+
if (value.protocol.length === 0)
|
|
1265
1317
|
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1266
1318
|
return { isValid: true, receivedType };
|
|
1319
|
+
} catch {
|
|
1320
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1267
1321
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1322
|
+
}
|
|
1323
|
+
function validateEmail(normalized, receivedType) {
|
|
1324
|
+
if (!EMAIL_RE.test(normalized))
|
|
1325
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1326
|
+
return { isValid: true, receivedType };
|
|
1327
|
+
}
|
|
1328
|
+
function validateJson(normalized, receivedType) {
|
|
1329
|
+
try {
|
|
1330
|
+
const parsed = JSON.parse(normalized);
|
|
1331
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1332
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1333
|
+
} catch {
|
|
1334
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1276
1335
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1336
|
+
}
|
|
1337
|
+
function validateSemver(normalized, receivedType) {
|
|
1338
|
+
if (!SEMVER_RE2.test(normalized))
|
|
1339
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1340
|
+
return { isValid: true, receivedType };
|
|
1341
|
+
}
|
|
1342
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1343
|
+
const normalized = rawValue.trim();
|
|
1344
|
+
const receivedType = detectReceivedType(normalized);
|
|
1345
|
+
if (expected.type === "unknown" || expected.type === "string")
|
|
1280
1346
|
return { isValid: true, receivedType };
|
|
1281
|
-
|
|
1347
|
+
if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
|
|
1348
|
+
if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
|
|
1349
|
+
if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
|
|
1350
|
+
if (expected.type === "url") return validateUrl(normalized, receivedType);
|
|
1351
|
+
if (expected.type === "email") return validateEmail(normalized, receivedType);
|
|
1352
|
+
if (expected.type === "json") return validateJson(normalized, receivedType);
|
|
1353
|
+
if (expected.type === "semver") return validateSemver(normalized, receivedType);
|
|
1282
1354
|
return { isValid: true, receivedType };
|
|
1283
1355
|
}
|
|
1284
1356
|
function toIssueCode(issueType) {
|
|
@@ -1350,57 +1422,62 @@ function buildReport(env, issues, recommendations) {
|
|
|
1350
1422
|
function isClientSecret(variable, key) {
|
|
1351
1423
|
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1352
1424
|
}
|
|
1425
|
+
function checkContractVariable(key, variable, context) {
|
|
1426
|
+
const { options, issues } = context;
|
|
1427
|
+
const value = options.values[key];
|
|
1428
|
+
const hasValue = value !== void 0;
|
|
1429
|
+
if (variable.required && !hasValue) {
|
|
1430
|
+
issues.push(
|
|
1431
|
+
createIssue({
|
|
1432
|
+
type: "missing",
|
|
1433
|
+
severity: "error",
|
|
1434
|
+
key,
|
|
1435
|
+
environment: options.environment,
|
|
1436
|
+
message: `Required variable ${key} is missing.`,
|
|
1437
|
+
debugValues: options.debugValues,
|
|
1438
|
+
expected: variable.expected
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!hasValue) return;
|
|
1444
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1445
|
+
if (!validation.isValid) {
|
|
1446
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
|
|
1447
|
+
issues.push(
|
|
1448
|
+
createIssue({
|
|
1449
|
+
type: validation.issueType,
|
|
1450
|
+
severity: "error",
|
|
1451
|
+
key,
|
|
1452
|
+
environment: options.environment,
|
|
1453
|
+
message,
|
|
1454
|
+
value,
|
|
1455
|
+
debugValues: options.debugValues,
|
|
1456
|
+
expected: variable.expected,
|
|
1457
|
+
receivedType: validation.receivedType
|
|
1458
|
+
})
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
if (isClientSecret(variable, key)) {
|
|
1462
|
+
issues.push(
|
|
1463
|
+
createIssue({
|
|
1464
|
+
type: "secret_exposed",
|
|
1465
|
+
severity: "error",
|
|
1466
|
+
key,
|
|
1467
|
+
environment: options.environment,
|
|
1468
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1469
|
+
value,
|
|
1470
|
+
debugValues: options.debugValues,
|
|
1471
|
+
expected: variable.expected
|
|
1472
|
+
})
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1353
1476
|
function validateAgainstContract(options) {
|
|
1354
1477
|
const issues = [];
|
|
1355
1478
|
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1356
1479
|
for (const [key, variable] of Object.entries(options.contract.variables)) {
|
|
1357
|
-
|
|
1358
|
-
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1359
|
-
if (variable.required && !hasValue) {
|
|
1360
|
-
issues.push(
|
|
1361
|
-
createIssue({
|
|
1362
|
-
type: "missing",
|
|
1363
|
-
severity: "error",
|
|
1364
|
-
key,
|
|
1365
|
-
environment: options.environment,
|
|
1366
|
-
message: `Required variable ${key} is missing.`,
|
|
1367
|
-
debugValues: options.debugValues,
|
|
1368
|
-
expected: variable.expected
|
|
1369
|
-
})
|
|
1370
|
-
);
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
if (!hasValue) continue;
|
|
1374
|
-
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1375
|
-
if (!validation.isValid) {
|
|
1376
|
-
issues.push(
|
|
1377
|
-
createIssue({
|
|
1378
|
-
type: validation.issueType,
|
|
1379
|
-
severity: "error",
|
|
1380
|
-
key,
|
|
1381
|
-
environment: options.environment,
|
|
1382
|
-
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`,
|
|
1383
|
-
value,
|
|
1384
|
-
debugValues: options.debugValues,
|
|
1385
|
-
expected: variable.expected,
|
|
1386
|
-
receivedType: validation.receivedType
|
|
1387
|
-
})
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
if (isClientSecret(variable, key)) {
|
|
1391
|
-
issues.push(
|
|
1392
|
-
createIssue({
|
|
1393
|
-
type: "secret_exposed",
|
|
1394
|
-
severity: "error",
|
|
1395
|
-
key,
|
|
1396
|
-
environment: options.environment,
|
|
1397
|
-
message: `Secret variable ${key} is marked as client-side.`,
|
|
1398
|
-
value,
|
|
1399
|
-
debugValues: options.debugValues,
|
|
1400
|
-
expected: variable.expected
|
|
1401
|
-
})
|
|
1402
|
-
);
|
|
1403
|
-
}
|
|
1480
|
+
checkContractVariable(key, variable, { options, issues });
|
|
1404
1481
|
}
|
|
1405
1482
|
for (const [key, value] of Object.entries(options.values)) {
|
|
1406
1483
|
if (contractKeys.has(key)) continue;
|
|
@@ -1428,6 +1505,97 @@ function collectUnionKeys(contract, sources) {
|
|
|
1428
1505
|
}
|
|
1429
1506
|
return union;
|
|
1430
1507
|
}
|
|
1508
|
+
function diffMissingEntries(key, missing, context) {
|
|
1509
|
+
const { variable, options, issues } = context;
|
|
1510
|
+
for (const entry of missing) {
|
|
1511
|
+
issues.push(
|
|
1512
|
+
createIssue({
|
|
1513
|
+
type: "missing",
|
|
1514
|
+
severity: "error",
|
|
1515
|
+
key,
|
|
1516
|
+
environment: entry.sourceName,
|
|
1517
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1518
|
+
debugValues: options.debugValues,
|
|
1519
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1520
|
+
})
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function diffTypeConflicts(key, present, context) {
|
|
1525
|
+
const { variable, options, issues } = context;
|
|
1526
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1527
|
+
for (const entry of present) {
|
|
1528
|
+
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1529
|
+
}
|
|
1530
|
+
if (new Set(typeBySource.values()).size <= 1) return;
|
|
1531
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1532
|
+
issues.push(
|
|
1533
|
+
createIssue({
|
|
1534
|
+
type: "conflict",
|
|
1535
|
+
severity: "error",
|
|
1536
|
+
key,
|
|
1537
|
+
environment: sourceName,
|
|
1538
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1539
|
+
debugValues: options.debugValues,
|
|
1540
|
+
receivedType: detectedType,
|
|
1541
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1542
|
+
value: options.sources[sourceName]?.[key]
|
|
1543
|
+
},
|
|
1544
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1545
|
+
})
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function diffPresentEntry(key, entry, context) {
|
|
1550
|
+
const { variable, options, issues } = context;
|
|
1551
|
+
if (entry.value === void 0) return;
|
|
1552
|
+
if (variable === void 0) {
|
|
1553
|
+
const severity = options.strict ? "error" : "warning";
|
|
1554
|
+
issues.push(
|
|
1555
|
+
createIssue({
|
|
1556
|
+
type: "extra",
|
|
1557
|
+
severity,
|
|
1558
|
+
key,
|
|
1559
|
+
environment: entry.sourceName,
|
|
1560
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1561
|
+
value: entry.value,
|
|
1562
|
+
debugValues: options.debugValues
|
|
1563
|
+
})
|
|
1564
|
+
);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1568
|
+
if (!validation.isValid) {
|
|
1569
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
|
|
1570
|
+
issues.push(
|
|
1571
|
+
createIssue({
|
|
1572
|
+
type: validation.issueType,
|
|
1573
|
+
severity: "error",
|
|
1574
|
+
key,
|
|
1575
|
+
environment: entry.sourceName,
|
|
1576
|
+
message,
|
|
1577
|
+
value: entry.value,
|
|
1578
|
+
debugValues: options.debugValues,
|
|
1579
|
+
expected: variable.expected,
|
|
1580
|
+
receivedType: validation.receivedType
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
if (isClientSecret(variable, key)) {
|
|
1585
|
+
issues.push(
|
|
1586
|
+
createIssue({
|
|
1587
|
+
type: "secret_exposed",
|
|
1588
|
+
severity: "error",
|
|
1589
|
+
key,
|
|
1590
|
+
environment: entry.sourceName,
|
|
1591
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1592
|
+
value: entry.value,
|
|
1593
|
+
debugValues: options.debugValues,
|
|
1594
|
+
expected: variable.expected
|
|
1595
|
+
})
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1431
1599
|
function diffEnvironmentSources(options) {
|
|
1432
1600
|
const issues = [];
|
|
1433
1601
|
const sourceNames = Object.keys(options.sources);
|
|
@@ -1438,12 +1606,8 @@ function diffEnvironmentSources(options) {
|
|
|
1438
1606
|
sourceName,
|
|
1439
1607
|
value: options.sources[sourceName]?.[key]
|
|
1440
1608
|
}));
|
|
1441
|
-
const present = valuesBySource.filter(
|
|
1442
|
-
|
|
1443
|
-
);
|
|
1444
|
-
const missing = valuesBySource.filter(
|
|
1445
|
-
(entry) => entry.value === void 0 || entry.value === ""
|
|
1446
|
-
);
|
|
1609
|
+
const present = valuesBySource.filter((entry) => entry.value !== void 0);
|
|
1610
|
+
const missing = valuesBySource.filter((entry) => entry.value === void 0);
|
|
1447
1611
|
if (present.length === 0 && variable?.required === true) {
|
|
1448
1612
|
for (const entry of missing) {
|
|
1449
1613
|
issues.push(
|
|
@@ -1460,92 +1624,13 @@ function diffEnvironmentSources(options) {
|
|
|
1460
1624
|
}
|
|
1461
1625
|
continue;
|
|
1462
1626
|
}
|
|
1627
|
+
const ctx = { variable, options, issues };
|
|
1463
1628
|
if (present.length > 0) {
|
|
1464
|
-
|
|
1465
|
-
issues.push(
|
|
1466
|
-
createIssue({
|
|
1467
|
-
type: "missing",
|
|
1468
|
-
severity: "error",
|
|
1469
|
-
key,
|
|
1470
|
-
environment: entry.sourceName,
|
|
1471
|
-
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1472
|
-
debugValues: options.debugValues,
|
|
1473
|
-
...variable !== void 0 && { expected: variable.expected }
|
|
1474
|
-
})
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1629
|
+
diffMissingEntries(key, missing, ctx);
|
|
1477
1630
|
}
|
|
1478
|
-
|
|
1631
|
+
diffTypeConflicts(key, present, ctx);
|
|
1479
1632
|
for (const entry of present) {
|
|
1480
|
-
|
|
1481
|
-
typeBySource.set(entry.sourceName, detected);
|
|
1482
|
-
}
|
|
1483
|
-
if (new Set(typeBySource.values()).size > 1) {
|
|
1484
|
-
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1485
|
-
issues.push(
|
|
1486
|
-
createIssue({
|
|
1487
|
-
type: "conflict",
|
|
1488
|
-
severity: "error",
|
|
1489
|
-
key,
|
|
1490
|
-
environment: sourceName,
|
|
1491
|
-
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1492
|
-
debugValues: options.debugValues,
|
|
1493
|
-
receivedType: detectedType,
|
|
1494
|
-
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1495
|
-
value: options.sources[sourceName]?.[key]
|
|
1496
|
-
},
|
|
1497
|
-
...variable !== void 0 && { expected: variable.expected }
|
|
1498
|
-
})
|
|
1499
|
-
);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
for (const entry of present) {
|
|
1503
|
-
if (entry.value === void 0) continue;
|
|
1504
|
-
if (variable === void 0) {
|
|
1505
|
-
const severity = options.strict ? "error" : "warning";
|
|
1506
|
-
issues.push(
|
|
1507
|
-
createIssue({
|
|
1508
|
-
type: "extra",
|
|
1509
|
-
severity,
|
|
1510
|
-
key,
|
|
1511
|
-
environment: entry.sourceName,
|
|
1512
|
-
message: `Variable ${key} is not defined in the contract.`,
|
|
1513
|
-
value: entry.value,
|
|
1514
|
-
debugValues: options.debugValues
|
|
1515
|
-
})
|
|
1516
|
-
);
|
|
1517
|
-
continue;
|
|
1518
|
-
}
|
|
1519
|
-
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1520
|
-
if (!validation.isValid) {
|
|
1521
|
-
issues.push(
|
|
1522
|
-
createIssue({
|
|
1523
|
-
type: validation.issueType,
|
|
1524
|
-
severity: "error",
|
|
1525
|
-
key,
|
|
1526
|
-
environment: entry.sourceName,
|
|
1527
|
-
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`,
|
|
1528
|
-
value: entry.value,
|
|
1529
|
-
debugValues: options.debugValues,
|
|
1530
|
-
expected: variable.expected,
|
|
1531
|
-
receivedType: validation.receivedType
|
|
1532
|
-
})
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
if (isClientSecret(variable, key)) {
|
|
1536
|
-
issues.push(
|
|
1537
|
-
createIssue({
|
|
1538
|
-
type: "secret_exposed",
|
|
1539
|
-
severity: "error",
|
|
1540
|
-
key,
|
|
1541
|
-
environment: entry.sourceName,
|
|
1542
|
-
message: `Secret variable ${key} is marked as client-side.`,
|
|
1543
|
-
value: entry.value,
|
|
1544
|
-
debugValues: options.debugValues,
|
|
1545
|
-
expected: variable.expected
|
|
1546
|
-
})
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1633
|
+
diffPresentEntry(key, entry, ctx);
|
|
1549
1634
|
}
|
|
1550
1635
|
}
|
|
1551
1636
|
return buildReport("diff", issues);
|
|
@@ -1594,7 +1679,7 @@ function parseEnvSourceContent(content) {
|
|
|
1594
1679
|
for (const line of lines) {
|
|
1595
1680
|
const trimmed = line.trim();
|
|
1596
1681
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1597
|
-
const match = /^(?:export\s+)?([A-Za-z_]
|
|
1682
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
|
|
1598
1683
|
if (match === null) continue;
|
|
1599
1684
|
const key = match[1] ?? "";
|
|
1600
1685
|
const rawValue = match[2] ?? "";
|
|
@@ -1607,11 +1692,11 @@ async function loadEnvSource(options) {
|
|
|
1607
1692
|
try {
|
|
1608
1693
|
const content = await promises.readFile(resolvedPath, "utf8");
|
|
1609
1694
|
return parseEnvSourceContent(content);
|
|
1610
|
-
} catch (
|
|
1611
|
-
if (options.allowMissing === true &&
|
|
1695
|
+
} catch (error_) {
|
|
1696
|
+
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1612
1697
|
return {};
|
|
1613
1698
|
}
|
|
1614
|
-
throw
|
|
1699
|
+
throw error_;
|
|
1615
1700
|
}
|
|
1616
1701
|
}
|
|
1617
1702
|
function toJsonString(report, mode) {
|
|
@@ -1621,8 +1706,8 @@ function toJsonString(report, mode) {
|
|
|
1621
1706
|
`;
|
|
1622
1707
|
}
|
|
1623
1708
|
function formatIssue(issue) {
|
|
1624
|
-
const expected = issue.expected
|
|
1625
|
-
const received = issue.receivedType
|
|
1709
|
+
const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
|
|
1710
|
+
const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
|
|
1626
1711
|
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1627
1712
|
}
|
|
1628
1713
|
function formatHumanReport(report) {
|
|
@@ -1631,15 +1716,13 @@ function formatHumanReport(report) {
|
|
|
1631
1716
|
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1632
1717
|
);
|
|
1633
1718
|
if (report.issues.length > 0) {
|
|
1634
|
-
lines.push("");
|
|
1635
|
-
lines.push("Issues:");
|
|
1719
|
+
lines.push("", "Issues:");
|
|
1636
1720
|
for (const issue of report.issues) {
|
|
1637
1721
|
lines.push(`- ${formatIssue(issue)}`);
|
|
1638
1722
|
}
|
|
1639
1723
|
}
|
|
1640
1724
|
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1641
|
-
lines.push("");
|
|
1642
|
-
lines.push("Recommendations:");
|
|
1725
|
+
lines.push("", "Recommendations:");
|
|
1643
1726
|
for (const recommendation of report.recommendations) {
|
|
1644
1727
|
lines.push(`- ${recommendation}`);
|
|
1645
1728
|
}
|
|
@@ -1684,7 +1767,11 @@ var HELP_TEXT = {
|
|
|
1684
1767
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1685
1768
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1686
1769
|
" -c, --config <path> Config file path",
|
|
1687
|
-
" -h, --help Show this help"
|
|
1770
|
+
" -h, --help Show this help",
|
|
1771
|
+
"",
|
|
1772
|
+
"Exit codes:",
|
|
1773
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1774
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1688
1775
|
].join("\n"),
|
|
1689
1776
|
diff: [
|
|
1690
1777
|
"Usage: env-typegen diff [options]",
|
|
@@ -1703,7 +1790,11 @@ var HELP_TEXT = {
|
|
|
1703
1790
|
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1704
1791
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1705
1792
|
" -c, --config <path> Config file path",
|
|
1706
|
-
" -h, --help Show this help"
|
|
1793
|
+
" -h, --help Show this help",
|
|
1794
|
+
"",
|
|
1795
|
+
"Exit codes:",
|
|
1796
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1797
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1707
1798
|
].join("\n"),
|
|
1708
1799
|
doctor: [
|
|
1709
1800
|
"Usage: env-typegen doctor [options]",
|
|
@@ -1723,7 +1814,11 @@ var HELP_TEXT = {
|
|
|
1723
1814
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1724
1815
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1725
1816
|
" -c, --config <path> Config file path",
|
|
1726
|
-
" -h, --help Show this help"
|
|
1817
|
+
" -h, --help Show this help",
|
|
1818
|
+
"",
|
|
1819
|
+
"Exit codes:",
|
|
1820
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1821
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1727
1822
|
].join("\n")
|
|
1728
1823
|
};
|
|
1729
1824
|
function resolveConfigRelative(value, configDir) {
|
|
@@ -1765,6 +1860,9 @@ async function loadCommandConfig(configPath) {
|
|
|
1765
1860
|
return loadConfig(process.cwd());
|
|
1766
1861
|
}
|
|
1767
1862
|
const resolvedPath = path6__default.default.resolve(configPath);
|
|
1863
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1864
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
1865
|
+
}
|
|
1768
1866
|
const configDir = path6__default.default.dirname(resolvedPath);
|
|
1769
1867
|
const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
|
|
1770
1868
|
if (moduleValue.default === void 0) return void 0;
|
|
@@ -1810,7 +1908,10 @@ function parseValidationArgs(argv) {
|
|
|
1810
1908
|
}
|
|
1811
1909
|
});
|
|
1812
1910
|
const castValues = values;
|
|
1813
|
-
|
|
1911
|
+
let jsonMode = "off";
|
|
1912
|
+
if (castValues.json === true) {
|
|
1913
|
+
jsonMode = assignedMode === "off" ? "compact" : assignedMode;
|
|
1914
|
+
}
|
|
1814
1915
|
return { values: castValues, jsonMode };
|
|
1815
1916
|
}
|
|
1816
1917
|
function resolveStrict(values, fileConfig) {
|
|
@@ -1883,12 +1984,12 @@ async function runCheckCommand(args) {
|
|
|
1883
1984
|
const provider = context.cloudProvider;
|
|
1884
1985
|
let environment = args.values.env?.[0] ?? ".env";
|
|
1885
1986
|
let sourceValues;
|
|
1886
|
-
if (provider
|
|
1987
|
+
if (provider === void 0) {
|
|
1988
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1989
|
+
} else {
|
|
1887
1990
|
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1888
1991
|
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1889
1992
|
environment = `cloud:${provider}`;
|
|
1890
|
-
} else {
|
|
1891
|
-
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1892
1993
|
}
|
|
1893
1994
|
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1894
1995
|
const report = applyReportPlugins(
|