@xlameiro/env-typegen 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -149,7 +149,9 @@ var inferenceRules = [
149
149
  {
150
150
  id: "P8_numeric_literal",
151
151
  priority: 8,
152
- match: (_key, value) => /^\d+(\.\d+)?$/.test(value),
152
+ // Non-capturing group with \d keeps the dot/digit boundary unambiguous,
153
+ // eliminating super-linear backtracking (ReDoS-safe).
154
+ match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
153
155
  type: "number"
154
156
  },
155
157
  {
@@ -167,7 +169,9 @@ var inferenceRules = [
167
169
  {
168
170
  id: "P11_email_literal",
169
171
  priority: 11,
170
- match: (_key, value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value),
172
+ // Dots are excluded from each domain-segment character class so that the
173
+ // literal \. separators are unambiguous, preventing super-linear backtracking.
174
+ match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
171
175
  type: "email"
172
176
  },
173
177
  {
@@ -201,7 +205,50 @@ function inferTypesFromParsedVars(parsed, options) {
201
205
  return parsed.vars.map((item) => inferType(item.key, item.rawValue, options));
202
206
  }
203
207
  var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
204
- var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
208
+ var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
209
+ function buildParsedVar(params, commentBlock, options) {
210
+ const annotations = parseCommentBlock(commentBlock);
211
+ const extraRules = options?.inferenceRules;
212
+ const inferredType = inferType(
213
+ params.key,
214
+ params.rawValue,
215
+ ...extraRules === void 0 ? [] : [{ extraRules }]
216
+ );
217
+ const isRequired = params.rawValue.length > 0 || annotations.isRequired;
218
+ const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
219
+ const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
220
+ const parsedVar = {
221
+ key: params.key,
222
+ rawValue: params.rawValue,
223
+ inferredType,
224
+ isRequired,
225
+ isOptional,
226
+ isClientSide,
227
+ lineNumber: params.lineNumber
228
+ };
229
+ if (annotations.annotatedType !== void 0) {
230
+ parsedVar.annotatedType = annotations.annotatedType;
231
+ }
232
+ if (annotations.description !== void 0) {
233
+ parsedVar.description = annotations.description;
234
+ }
235
+ if (params.currentGroup !== void 0) {
236
+ parsedVar.group = params.currentGroup;
237
+ }
238
+ if (annotations.enumValues !== void 0) {
239
+ parsedVar.enumValues = annotations.enumValues;
240
+ }
241
+ if (annotations.constraints !== void 0) {
242
+ parsedVar.constraints = annotations.constraints;
243
+ }
244
+ if (annotations.runtime !== void 0) {
245
+ parsedVar.runtime = annotations.runtime;
246
+ }
247
+ if (annotations.isSecret !== void 0) {
248
+ parsedVar.isSecret = annotations.isSecret;
249
+ }
250
+ return parsedVar;
251
+ }
205
252
  function parseEnvFileContent(content, filePath, options) {
206
253
  const lines = content.split("\n");
207
254
  const vars = [];
@@ -231,53 +278,18 @@ function parseEnvFileContent(content, filePath, options) {
231
278
  continue;
232
279
  }
233
280
  const envMatch = ENV_VAR_RE.exec(trimmed);
234
- if (envMatch !== null) {
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 {
281
+ if (envMatch === null) {
279
282
  commentBlock = [];
283
+ continue;
280
284
  }
285
+ vars.push(
286
+ buildParsedVar(
287
+ { key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
288
+ commentBlock,
289
+ options
290
+ )
291
+ );
292
+ commentBlock = [];
281
293
  }
282
294
  return { filePath, vars, groups };
283
295
  }
@@ -296,12 +308,14 @@ function generateTypeScriptTypes(parsed) {
296
308
  const fileName = path6__default.default.basename(parsed.filePath);
297
309
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
298
310
  const lines = [];
299
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
300
- lines.push(`// Source: ${fileName}`);
301
- lines.push(`// Generated at: ${timestamp}`);
302
- lines.push("");
303
- lines.push("declare namespace NodeJS {");
304
- lines.push(" interface ProcessEnv {");
311
+ lines.push(
312
+ "// Generated by env-typegen \u2014 do not edit manually",
313
+ `// Source: ${fileName}`,
314
+ `// Generated at: ${timestamp}`,
315
+ "",
316
+ "declare namespace NodeJS {",
317
+ " interface ProcessEnv {"
318
+ );
305
319
  for (const variable of parsed.vars) {
306
320
  const effectiveType = variable.annotatedType ?? variable.inferredType;
307
321
  const optional = variable.isOptional ? "?" : "";
@@ -316,10 +330,7 @@ function generateTypeScriptTypes(parsed) {
316
330
  }
317
331
  lines.push(propLine);
318
332
  }
319
- lines.push(" }");
320
- lines.push("}");
321
- lines.push("");
322
- lines.push("export type EnvVars = {");
333
+ lines.push(" }", "}", "", "export type EnvVars = {");
323
334
  for (const variable of parsed.vars) {
324
335
  const effectiveType = variable.annotatedType ?? variable.inferredType;
325
336
  const tsType = toTsType(effectiveType);
@@ -329,28 +340,34 @@ function generateTypeScriptTypes(parsed) {
329
340
  lines.push("};");
330
341
  if (hasClientVars) {
331
342
  const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
332
- lines.push("");
333
- lines.push(`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`);
334
- lines.push(`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`);
343
+ lines.push(
344
+ "",
345
+ `export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
346
+ `export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
347
+ );
335
348
  }
336
349
  return lines.join("\n") + "\n";
337
350
  }
338
351
  function generateEnvValidation(parsed) {
339
352
  const required = parsed.vars.filter((v) => v.isRequired).map((v) => v.key);
340
353
  const lines = [];
341
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
342
- lines.push("");
343
- lines.push("export function validateEnv(): void {");
354
+ lines.push(
355
+ "// Generated by env-typegen \u2014 do not edit manually",
356
+ "",
357
+ "export function validateEnv(): void {"
358
+ );
344
359
  if (required.length === 0) {
345
360
  lines.push(" // No required environment variables defined");
346
361
  } else {
347
362
  const keyList = required.map((k) => `"${k}"`).join(", ");
348
- lines.push(` const required = [${keyList}];`);
349
- lines.push(" for (const key of required) {");
350
- lines.push(" if (!process.env[key]) {");
351
- lines.push(" throw new Error(`Missing required environment variable: ${key}`);");
352
- lines.push(" }");
353
- lines.push(" }");
363
+ lines.push(
364
+ ` const required = [${keyList}];`,
365
+ " for (const key of required) {",
366
+ " if (!process.env[key]) {",
367
+ " throw new Error(`Missing required environment variable: ${key}`);",
368
+ " }",
369
+ " }"
370
+ );
354
371
  }
355
372
  lines.push("}");
356
373
  return lines.join("\n") + "\n";
@@ -368,37 +385,40 @@ function generateZodSchema(parsed) {
368
385
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
369
386
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
370
387
  const lines = [];
371
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
372
- lines.push('import { z } from "zod";');
373
- lines.push("");
374
- lines.push("export const serverEnvSchema = z.object({");
388
+ lines.push(
389
+ "// Generated by env-typegen \u2014 do not edit manually",
390
+ 'import { z } from "zod";',
391
+ "",
392
+ "export const serverEnvSchema = z.object({"
393
+ );
375
394
  for (const variable of serverVars) {
376
395
  const effectiveType = variable.annotatedType ?? variable.inferredType;
377
396
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
378
397
  lines.push(` ${variable.key}: ${zodExpr},`);
379
398
  }
380
- lines.push("});");
381
- lines.push("");
382
- lines.push("export const clientEnvSchema = z.object({");
399
+ lines.push("});", "", "export const clientEnvSchema = z.object({");
383
400
  for (const variable of clientVars) {
384
401
  const effectiveType = variable.annotatedType ?? variable.inferredType;
385
402
  const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
386
403
  lines.push(` ${variable.key}: ${zodExpr},`);
387
404
  }
388
- lines.push("});");
389
- lines.push("");
390
- lines.push("export const envSchema = serverEnvSchema.merge(clientEnvSchema);");
391
- lines.push("export type Env = z.infer<typeof envSchema>;");
405
+ lines.push(
406
+ "});",
407
+ "",
408
+ "export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
409
+ "export type Env = z.infer<typeof envSchema>;"
410
+ );
392
411
  return lines.join("\n") + "\n";
393
412
  }
394
413
  function generateDeclaration(parsed) {
395
414
  const fileName = path6__default.default.basename(parsed.filePath);
396
- const lines = [];
397
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
398
- lines.push(`// Source: ${fileName}`);
399
- lines.push("");
400
- lines.push("declare namespace NodeJS {");
401
- lines.push(" interface ProcessEnv {");
415
+ const lines = [
416
+ "// Generated by env-typegen \u2014 do not edit manually",
417
+ `// Source: ${fileName}`,
418
+ "",
419
+ "declare namespace NodeJS {",
420
+ " interface ProcessEnv {"
421
+ ];
402
422
  for (const variable of parsed.vars) {
403
423
  const effectiveType = variable.annotatedType ?? variable.inferredType;
404
424
  const optional = variable.isOptional ? "?" : "";
@@ -413,12 +433,14 @@ function generateDeclaration(parsed) {
413
433
  }
414
434
  lines.push(propLine);
415
435
  }
416
- lines.push(" }");
417
- lines.push("}");
436
+ lines.push(" }", "}");
418
437
  return lines.join("\n") + "\n";
419
438
  }
420
439
 
421
440
  // src/generators/t3-generator.ts
441
+ function escapeJsStringLiteral(value) {
442
+ return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
443
+ }
422
444
  function toT3ZodType(envVarType) {
423
445
  if (envVarType === "number") return "z.coerce.number()";
424
446
  if (envVarType === "boolean") return "z.coerce.boolean()";
@@ -426,51 +448,47 @@ function toT3ZodType(envVarType) {
426
448
  if (envVarType === "email") return "z.string().email()";
427
449
  return "z.string()";
428
450
  }
451
+ function buildZodExpr(variable) {
452
+ const effectiveType = variable.annotatedType ?? variable.inferredType;
453
+ let zodExpr = toT3ZodType(effectiveType);
454
+ if (variable.description !== void 0) {
455
+ zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
456
+ }
457
+ if (variable.isOptional) {
458
+ zodExpr += ".optional()";
459
+ }
460
+ return zodExpr;
461
+ }
429
462
  function generateT3Env(parsed) {
430
463
  const serverVars = parsed.vars.filter((v) => !v.isClientSide);
431
464
  const clientVars = parsed.vars.filter((v) => v.isClientSide);
432
- const lines = [];
433
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
434
- lines.push('import { createEnv } from "@t3-oss/env-nextjs";');
435
- lines.push('import { z } from "zod";');
436
- lines.push("");
437
- lines.push("export const env = createEnv({");
465
+ const lines = [
466
+ "// Generated by env-typegen \u2014 do not edit manually",
467
+ 'import { createEnv } from "@t3-oss/env-nextjs";',
468
+ 'import { z } from "zod";',
469
+ "",
470
+ "export const env = createEnv({"
471
+ ];
438
472
  if (serverVars.length > 0) {
439
- lines.push(" server: {");
440
- for (const variable of serverVars) {
441
- const effectiveType = variable.annotatedType ?? variable.inferredType;
442
- let zodExpr = toT3ZodType(effectiveType);
443
- if (variable.description !== void 0) {
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(" },");
473
+ lines.push(
474
+ " server: {",
475
+ ...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
476
+ " },"
477
+ );
452
478
  }
453
479
  if (clientVars.length > 0) {
454
- lines.push(" client: {");
455
- for (const variable of clientVars) {
456
- const effectiveType = variable.annotatedType ?? variable.inferredType;
457
- let zodExpr = toT3ZodType(effectiveType);
458
- if (variable.description !== void 0) {
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},`);
480
+ lines.push(
481
+ " client: {",
482
+ ...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
483
+ " },"
484
+ );
471
485
  }
472
- lines.push(" },");
473
- lines.push("});");
486
+ lines.push(
487
+ " runtimeEnv: {",
488
+ ...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
489
+ " },",
490
+ "});"
491
+ );
474
492
  return lines.join("\n") + "\n";
475
493
  }
476
494
  var CONTRACT_FILE_NAMES = [
@@ -493,9 +511,9 @@ async function loadContract(cwd = process.cwd()) {
493
511
  return void 0;
494
512
  }
495
513
  var CONFIG_FILE_NAMES = [
496
- "env-typegen.config.ts",
497
514
  "env-typegen.config.mjs",
498
- "env-typegen.config.js"
515
+ "env-typegen.config.js",
516
+ "env-typegen.config.ts"
499
517
  ];
500
518
  function defineConfig(config) {
501
519
  return config;
@@ -504,6 +522,19 @@ async function loadConfig(cwd = process.cwd()) {
504
522
  for (const name of CONFIG_FILE_NAMES) {
505
523
  const filePath = path6__default.default.resolve(cwd, name);
506
524
  if (fs.existsSync(filePath)) {
525
+ if (filePath.endsWith(".ts")) {
526
+ throw new Error(
527
+ `Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
528
+ Rename it to "env-typegen.config.mjs" and use ESM export syntax:
529
+
530
+ // env-typegen.config.mjs
531
+ import { defineConfig } from "@xlameiro/env-typegen";
532
+ export default defineConfig({ input: ".env.example" });
533
+
534
+ Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
535
+ env-typegen.config.mjs for runtime loading.`
536
+ );
537
+ }
507
538
  const fileUrl = url.pathToFileURL(filePath).href;
508
539
  const mod = await import(fileUrl);
509
540
  return mod.default;
@@ -616,7 +647,7 @@ async function runGenerate(options) {
616
647
  const parsed = parseEnvFileContent(
617
648
  content,
618
649
  inputPath,
619
- inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
650
+ inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
620
651
  );
621
652
  for (const generator of generators) {
622
653
  let generated = buildOutput(generator, parsed);
@@ -944,8 +975,13 @@ function readEntryValue(entry, keys) {
944
975
  }
945
976
  return void 0;
946
977
  }
978
+ function getVercelEntries(value) {
979
+ if (Array.isArray(value)) return value;
980
+ if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
981
+ return [];
982
+ }
947
983
  function parseVercelPayload(value) {
948
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.envs) ? value.envs : [];
984
+ const entries = getVercelEntries(value);
949
985
  const result = {};
950
986
  for (const entry of entries) {
951
987
  if (!isRecord(entry)) continue;
@@ -956,8 +992,13 @@ function parseVercelPayload(value) {
956
992
  }
957
993
  return result;
958
994
  }
995
+ function getCloudflareEntries(value) {
996
+ if (Array.isArray(value)) return value;
997
+ if (isRecord(value) && Array.isArray(value.result)) return value.result;
998
+ return [];
999
+ }
959
1000
  function parseCloudflarePayload(value) {
960
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.result) ? value.result : [];
1001
+ const entries = getCloudflareEntries(value);
961
1002
  const result = {};
962
1003
  for (const entry of entries) {
963
1004
  if (!isRecord(entry)) continue;
@@ -975,7 +1016,7 @@ function parseAwsPayload(value) {
975
1016
  if (!isRecord(entry)) continue;
976
1017
  const name = readEntryValue(entry, ["Name", "name"]);
977
1018
  if (name === void 0) continue;
978
- const key = name.split("/").filter((part) => part.length > 0).pop() ?? name;
1019
+ const key = name.split("/").findLast((part) => part.length > 0) ?? name;
979
1020
  const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
980
1021
  result[key] = envValue;
981
1022
  }
@@ -1115,7 +1156,7 @@ function findDefaultContractPath(cwd) {
1115
1156
  async function loadValidationContract(options) {
1116
1157
  const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
1117
1158
  const discoveredContractPath = findDefaultContractPath(cwd);
1118
- const resolvedContractPath = contractPath !== void 0 ? path6__default.default.resolve(cwd, contractPath) : discoveredContractPath;
1159
+ const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6__default.default.resolve(cwd, contractPath);
1119
1160
  if (resolvedContractPath !== void 0 && fs.existsSync(resolvedContractPath)) {
1120
1161
  const moduleUrl = url.pathToFileURL(resolvedContractPath).href;
1121
1162
  const moduleValue = await import(moduleUrl);
@@ -1154,7 +1195,14 @@ async function loadPluginFromPath(pluginPath, cwd) {
1154
1195
  const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
1155
1196
  const candidate = moduleValue.default ?? moduleValue.plugin;
1156
1197
  if (isPlugin(candidate)) return candidate;
1157
- throw new Error(`Invalid plugin at ${resolvedPath}. Expected a plugin object export.`);
1198
+ throw new Error(
1199
+ `Invalid plugin at ${resolvedPath}.
1200
+ Expected a default export matching:
1201
+ { name: string,
1202
+ transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
1203
+ transformReport?(report: ValidationReport): ValidationReport,
1204
+ transformContract?(contract: EnvContract): EnvContract }`
1205
+ );
1158
1206
  }
1159
1207
  async function loadPlugins(options) {
1160
1208
  const cwd = options.cwd ?? process.cwd();
@@ -1199,8 +1247,8 @@ function applyReportPlugins(report, plugins) {
1199
1247
  }
1200
1248
 
1201
1249
  // src/validation/engine.ts
1202
- var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
1203
- var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
1250
+ var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
1251
+ var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
1204
1252
  function detectReceivedType(value) {
1205
1253
  const normalized = value.trim();
1206
1254
  if (normalized.length === 0) return "unknown";
@@ -1220,65 +1268,72 @@ function detectReceivedType(value) {
1220
1268
  }
1221
1269
  return "string";
1222
1270
  }
1223
- function validateValueAgainstExpected(expected, rawValue) {
1224
- const normalized = rawValue.trim();
1225
- const receivedType = detectReceivedType(normalized);
1226
- if (expected.type === "unknown") return { isValid: true, receivedType };
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 };
1271
+ function validateNumber(expected, normalized, receivedType) {
1272
+ const parsed = Number(normalized);
1273
+ if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1274
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1240
1275
  }
1241
- if (expected.type === "boolean") {
1242
- if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1243
- return { isValid: false, receivedType, issueType: "invalid_type" };
1244
- }
1245
- return { isValid: true, receivedType };
1276
+ if (expected.min !== void 0 && parsed < expected.min) {
1277
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1246
1278
  }
1247
- if (expected.type === "enum") {
1248
- if (!expected.values.includes(normalized)) {
1249
- return { isValid: false, receivedType, issueType: "invalid_value" };
1250
- }
1251
- return { isValid: true, receivedType };
1279
+ if (expected.max !== void 0 && parsed > expected.max) {
1280
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1252
1281
  }
1253
- if (expected.type === "url") {
1254
- try {
1255
- const value = new URL(normalized);
1256
- if (value.protocol.length === 0)
1257
- return { isValid: false, receivedType, issueType: "invalid_type" };
1258
- return { isValid: true, receivedType };
1259
- } catch {
1260
- return { isValid: false, receivedType, issueType: "invalid_type" };
1261
- }
1282
+ return { isValid: true, receivedType };
1283
+ }
1284
+ function validateBoolean(normalized, receivedType) {
1285
+ if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1286
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1287
+ }
1288
+ return { isValid: true, receivedType };
1289
+ }
1290
+ function validateEnum(expected, normalized, receivedType) {
1291
+ if (!expected.values.includes(normalized)) {
1292
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1262
1293
  }
1263
- if (expected.type === "email") {
1264
- if (!EMAIL_RE.test(normalized))
1294
+ return { isValid: true, receivedType };
1295
+ }
1296
+ function validateUrl(normalized, receivedType) {
1297
+ try {
1298
+ const value = new URL(normalized);
1299
+ if (value.protocol.length === 0)
1265
1300
  return { isValid: false, receivedType, issueType: "invalid_type" };
1266
1301
  return { isValid: true, receivedType };
1302
+ } catch {
1303
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1267
1304
  }
1268
- if (expected.type === "json") {
1269
- try {
1270
- const parsed = JSON.parse(normalized);
1271
- if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1272
- return { isValid: false, receivedType, issueType: "invalid_type" };
1273
- } catch {
1274
- return { isValid: false, receivedType, issueType: "invalid_type" };
1275
- }
1305
+ }
1306
+ function validateEmail(normalized, receivedType) {
1307
+ if (!EMAIL_RE.test(normalized))
1308
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1309
+ return { isValid: true, receivedType };
1310
+ }
1311
+ function validateJson(normalized, receivedType) {
1312
+ try {
1313
+ const parsed = JSON.parse(normalized);
1314
+ if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1315
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1316
+ } catch {
1317
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1276
1318
  }
1277
- if (expected.type === "semver") {
1278
- if (!SEMVER_RE2.test(normalized))
1279
- return { isValid: false, receivedType, issueType: "invalid_value" };
1319
+ }
1320
+ function validateSemver(normalized, receivedType) {
1321
+ if (!SEMVER_RE2.test(normalized))
1322
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1323
+ return { isValid: true, receivedType };
1324
+ }
1325
+ function validateValueAgainstExpected(expected, rawValue) {
1326
+ const normalized = rawValue.trim();
1327
+ const receivedType = detectReceivedType(normalized);
1328
+ if (expected.type === "unknown" || expected.type === "string")
1280
1329
  return { isValid: true, receivedType };
1281
- }
1330
+ if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
1331
+ if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
1332
+ if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
1333
+ if (expected.type === "url") return validateUrl(normalized, receivedType);
1334
+ if (expected.type === "email") return validateEmail(normalized, receivedType);
1335
+ if (expected.type === "json") return validateJson(normalized, receivedType);
1336
+ if (expected.type === "semver") return validateSemver(normalized, receivedType);
1282
1337
  return { isValid: true, receivedType };
1283
1338
  }
1284
1339
  function toIssueCode(issueType) {
@@ -1350,57 +1405,62 @@ function buildReport(env, issues, recommendations) {
1350
1405
  function isClientSecret(variable, key) {
1351
1406
  return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
1352
1407
  }
1408
+ function checkContractVariable(key, variable, context) {
1409
+ const { options, issues } = context;
1410
+ const value = options.values[key];
1411
+ const hasValue = value !== void 0 && value.trim().length > 0;
1412
+ if (variable.required && !hasValue) {
1413
+ issues.push(
1414
+ createIssue({
1415
+ type: "missing",
1416
+ severity: "error",
1417
+ key,
1418
+ environment: options.environment,
1419
+ message: `Required variable ${key} is missing.`,
1420
+ debugValues: options.debugValues,
1421
+ expected: variable.expected
1422
+ })
1423
+ );
1424
+ return;
1425
+ }
1426
+ if (!hasValue) return;
1427
+ const validation = validateValueAgainstExpected(variable.expected, value);
1428
+ if (!validation.isValid) {
1429
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
1430
+ issues.push(
1431
+ createIssue({
1432
+ type: validation.issueType,
1433
+ severity: "error",
1434
+ key,
1435
+ environment: options.environment,
1436
+ message,
1437
+ value,
1438
+ debugValues: options.debugValues,
1439
+ expected: variable.expected,
1440
+ receivedType: validation.receivedType
1441
+ })
1442
+ );
1443
+ }
1444
+ if (isClientSecret(variable, key)) {
1445
+ issues.push(
1446
+ createIssue({
1447
+ type: "secret_exposed",
1448
+ severity: "error",
1449
+ key,
1450
+ environment: options.environment,
1451
+ message: `Secret variable ${key} is marked as client-side.`,
1452
+ value,
1453
+ debugValues: options.debugValues,
1454
+ expected: variable.expected
1455
+ })
1456
+ );
1457
+ }
1458
+ }
1353
1459
  function validateAgainstContract(options) {
1354
1460
  const issues = [];
1355
1461
  const contractKeys = new Set(Object.keys(options.contract.variables));
1356
1462
  for (const [key, variable] of Object.entries(options.contract.variables)) {
1357
- const value = options.values[key];
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
- }
1463
+ checkContractVariable(key, variable, { options, issues });
1404
1464
  }
1405
1465
  for (const [key, value] of Object.entries(options.values)) {
1406
1466
  if (contractKeys.has(key)) continue;
@@ -1428,6 +1488,97 @@ function collectUnionKeys(contract, sources) {
1428
1488
  }
1429
1489
  return union;
1430
1490
  }
1491
+ function diffMissingEntries(key, missing, context) {
1492
+ const { variable, options, issues } = context;
1493
+ for (const entry of missing) {
1494
+ issues.push(
1495
+ createIssue({
1496
+ type: "missing",
1497
+ severity: "error",
1498
+ key,
1499
+ environment: entry.sourceName,
1500
+ message: `Variable ${key} is missing in ${entry.sourceName}.`,
1501
+ debugValues: options.debugValues,
1502
+ ...variable !== void 0 && { expected: variable.expected }
1503
+ })
1504
+ );
1505
+ }
1506
+ }
1507
+ function diffTypeConflicts(key, present, context) {
1508
+ const { variable, options, issues } = context;
1509
+ const typeBySource = /* @__PURE__ */ new Map();
1510
+ for (const entry of present) {
1511
+ typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1512
+ }
1513
+ if (new Set(typeBySource.values()).size <= 1) return;
1514
+ for (const [sourceName, detectedType] of typeBySource.entries()) {
1515
+ issues.push(
1516
+ createIssue({
1517
+ type: "conflict",
1518
+ severity: "error",
1519
+ key,
1520
+ environment: sourceName,
1521
+ message: `Variable ${key} has conflicting inferred type across environments.`,
1522
+ debugValues: options.debugValues,
1523
+ receivedType: detectedType,
1524
+ ...options.sources[sourceName]?.[key] !== void 0 && {
1525
+ value: options.sources[sourceName]?.[key]
1526
+ },
1527
+ ...variable !== void 0 && { expected: variable.expected }
1528
+ })
1529
+ );
1530
+ }
1531
+ }
1532
+ function diffPresentEntry(key, entry, context) {
1533
+ const { variable, options, issues } = context;
1534
+ if (entry.value === void 0) return;
1535
+ if (variable === void 0) {
1536
+ const severity = options.strict ? "error" : "warning";
1537
+ issues.push(
1538
+ createIssue({
1539
+ type: "extra",
1540
+ severity,
1541
+ key,
1542
+ environment: entry.sourceName,
1543
+ message: `Variable ${key} is not defined in the contract.`,
1544
+ value: entry.value,
1545
+ debugValues: options.debugValues
1546
+ })
1547
+ );
1548
+ return;
1549
+ }
1550
+ const validation = validateValueAgainstExpected(variable.expected, entry.value);
1551
+ if (!validation.isValid) {
1552
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
1553
+ issues.push(
1554
+ createIssue({
1555
+ type: validation.issueType,
1556
+ severity: "error",
1557
+ key,
1558
+ environment: entry.sourceName,
1559
+ message,
1560
+ value: entry.value,
1561
+ debugValues: options.debugValues,
1562
+ expected: variable.expected,
1563
+ receivedType: validation.receivedType
1564
+ })
1565
+ );
1566
+ }
1567
+ if (isClientSecret(variable, key)) {
1568
+ issues.push(
1569
+ createIssue({
1570
+ type: "secret_exposed",
1571
+ severity: "error",
1572
+ key,
1573
+ environment: entry.sourceName,
1574
+ message: `Secret variable ${key} is marked as client-side.`,
1575
+ value: entry.value,
1576
+ debugValues: options.debugValues,
1577
+ expected: variable.expected
1578
+ })
1579
+ );
1580
+ }
1581
+ }
1431
1582
  function diffEnvironmentSources(options) {
1432
1583
  const issues = [];
1433
1584
  const sourceNames = Object.keys(options.sources);
@@ -1460,92 +1611,13 @@ function diffEnvironmentSources(options) {
1460
1611
  }
1461
1612
  continue;
1462
1613
  }
1614
+ const ctx = { variable, options, issues };
1463
1615
  if (present.length > 0) {
1464
- for (const entry of missing) {
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
- }
1616
+ diffMissingEntries(key, missing, ctx);
1477
1617
  }
1478
- const typeBySource = /* @__PURE__ */ new Map();
1618
+ diffTypeConflicts(key, present, ctx);
1479
1619
  for (const entry of present) {
1480
- const detected = detectReceivedType(entry.value ?? "");
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
- }
1620
+ diffPresentEntry(key, entry, ctx);
1549
1621
  }
1550
1622
  }
1551
1623
  return buildReport("diff", issues);
@@ -1594,7 +1666,7 @@ function parseEnvSourceContent(content) {
1594
1666
  for (const line of lines) {
1595
1667
  const trimmed = line.trim();
1596
1668
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1597
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
1669
+ const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
1598
1670
  if (match === null) continue;
1599
1671
  const key = match[1] ?? "";
1600
1672
  const rawValue = match[2] ?? "";
@@ -1607,11 +1679,11 @@ async function loadEnvSource(options) {
1607
1679
  try {
1608
1680
  const content = await promises.readFile(resolvedPath, "utf8");
1609
1681
  return parseEnvSourceContent(content);
1610
- } catch (errorValue) {
1611
- if (options.allowMissing === true && errorValue instanceof Error && "code" in errorValue && errorValue.code === "ENOENT") {
1682
+ } catch (error_) {
1683
+ if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1612
1684
  return {};
1613
1685
  }
1614
- throw errorValue;
1686
+ throw error_;
1615
1687
  }
1616
1688
  }
1617
1689
  function toJsonString(report, mode) {
@@ -1621,8 +1693,8 @@ function toJsonString(report, mode) {
1621
1693
  `;
1622
1694
  }
1623
1695
  function formatIssue(issue) {
1624
- const expected = issue.expected !== void 0 ? ` expected=${issue.expected.type}` : "";
1625
- const received = issue.receivedType !== void 0 ? ` received=${issue.receivedType}` : "";
1696
+ const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
1697
+ const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
1626
1698
  return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
1627
1699
  }
1628
1700
  function formatHumanReport(report) {
@@ -1631,15 +1703,13 @@ function formatHumanReport(report) {
1631
1703
  `Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
1632
1704
  );
1633
1705
  if (report.issues.length > 0) {
1634
- lines.push("");
1635
- lines.push("Issues:");
1706
+ lines.push("", "Issues:");
1636
1707
  for (const issue of report.issues) {
1637
1708
  lines.push(`- ${formatIssue(issue)}`);
1638
1709
  }
1639
1710
  }
1640
1711
  if (report.recommendations !== void 0 && report.recommendations.length > 0) {
1641
- lines.push("");
1642
- lines.push("Recommendations:");
1712
+ lines.push("", "Recommendations:");
1643
1713
  for (const recommendation of report.recommendations) {
1644
1714
  lines.push(`- ${recommendation}`);
1645
1715
  }
@@ -1684,7 +1754,11 @@ var HELP_TEXT = {
1684
1754
  " --cloud-file <path> Cloud snapshot JSON file",
1685
1755
  " --plugin <path> Plugin module path (repeatable)",
1686
1756
  " -c, --config <path> Config file path",
1687
- " -h, --help Show this help"
1757
+ " -h, --help Show this help",
1758
+ "",
1759
+ "Exit codes:",
1760
+ " 0 All checks passed (status: ok or warn)",
1761
+ " 1 One or more checks failed (status: fail) or invalid usage"
1688
1762
  ].join("\n"),
1689
1763
  diff: [
1690
1764
  "Usage: env-typegen diff [options]",
@@ -1703,7 +1777,11 @@ var HELP_TEXT = {
1703
1777
  " --cloud-file <path> Cloud snapshot JSON file added to diff sources",
1704
1778
  " --plugin <path> Plugin module path (repeatable)",
1705
1779
  " -c, --config <path> Config file path",
1706
- " -h, --help Show this help"
1780
+ " -h, --help Show this help",
1781
+ "",
1782
+ "Exit codes:",
1783
+ " 0 All checks passed (status: ok or warn)",
1784
+ " 1 One or more checks failed (status: fail) or invalid usage"
1707
1785
  ].join("\n"),
1708
1786
  doctor: [
1709
1787
  "Usage: env-typegen doctor [options]",
@@ -1723,7 +1801,11 @@ var HELP_TEXT = {
1723
1801
  " --cloud-file <path> Cloud snapshot JSON file",
1724
1802
  " --plugin <path> Plugin module path (repeatable)",
1725
1803
  " -c, --config <path> Config file path",
1726
- " -h, --help Show this help"
1804
+ " -h, --help Show this help",
1805
+ "",
1806
+ "Exit codes:",
1807
+ " 0 All checks passed (status: ok or warn)",
1808
+ " 1 One or more checks failed (status: fail) or invalid usage"
1727
1809
  ].join("\n")
1728
1810
  };
1729
1811
  function resolveConfigRelative(value, configDir) {
@@ -1810,7 +1892,10 @@ function parseValidationArgs(argv) {
1810
1892
  }
1811
1893
  });
1812
1894
  const castValues = values;
1813
- const jsonMode = castValues.json === true ? assignedMode === "off" ? "compact" : assignedMode : "off";
1895
+ let jsonMode = "off";
1896
+ if (castValues.json === true) {
1897
+ jsonMode = assignedMode === "off" ? "compact" : assignedMode;
1898
+ }
1814
1899
  return { values: castValues, jsonMode };
1815
1900
  }
1816
1901
  function resolveStrict(values, fileConfig) {
@@ -1883,12 +1968,12 @@ async function runCheckCommand(args) {
1883
1968
  const provider = context.cloudProvider;
1884
1969
  let environment = args.values.env?.[0] ?? ".env";
1885
1970
  let sourceValues;
1886
- if (provider !== void 0) {
1971
+ if (provider === void 0) {
1972
+ sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1973
+ } else {
1887
1974
  const cloudFile = context.cloudFile ?? `${provider}.env.json`;
1888
1975
  sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
1889
1976
  environment = `cloud:${provider}`;
1890
- } else {
1891
- sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1892
1977
  }
1893
1978
  sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
1894
1979
  const report = applyReportPlugins(