@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.js CHANGED
@@ -143,7 +143,9 @@ var inferenceRules = [
143
143
  {
144
144
  id: "P8_numeric_literal",
145
145
  priority: 8,
146
- match: (_key, value) => /^\d+(\.\d+)?$/.test(value),
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
- match: (_key, value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value),
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+(.+?)\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 !== null) {
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("// Generated by env-typegen \u2014 do not edit manually");
294
- lines.push(`// Source: ${fileName}`);
295
- lines.push(`// Generated at: ${timestamp}`);
296
- lines.push("");
297
- lines.push("declare namespace NodeJS {");
298
- lines.push(" interface ProcessEnv {");
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
- lines.push(`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`);
328
- lines.push(`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`);
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("// Generated by env-typegen \u2014 do not edit manually");
336
- lines.push("");
337
- lines.push("export function validateEnv(): void {");
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(` const required = [${keyList}];`);
343
- lines.push(" for (const key of required) {");
344
- lines.push(" if (!process.env[key]) {");
345
- lines.push(" throw new Error(`Missing required environment variable: ${key}`);");
346
- lines.push(" }");
347
- lines.push(" }");
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("// Generated by env-typegen \u2014 do not edit manually");
366
- lines.push('import { z } from "zod";');
367
- lines.push("");
368
- lines.push("export const serverEnvSchema = z.object({");
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
- lines.push("");
384
- lines.push("export const envSchema = serverEnvSchema.merge(clientEnvSchema);");
385
- lines.push("export type Env = z.infer<typeof envSchema>;");
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
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
392
- lines.push(`// Source: ${fileName}`);
393
- lines.push("");
394
- lines.push("declare namespace NodeJS {");
395
- lines.push(" interface ProcessEnv {");
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
- lines.push("// Generated by env-typegen \u2014 do not edit manually");
428
- lines.push('import { createEnv } from "@t3-oss/env-nextjs";');
429
- lines.push('import { z } from "zod";');
430
- lines.push("");
431
- lines.push("export const env = createEnv({");
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(" server: {");
434
- for (const variable of serverVars) {
435
- const effectiveType = variable.annotatedType ?? variable.inferredType;
436
- let zodExpr = toT3ZodType(effectiveType);
437
- if (variable.description !== void 0) {
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(" client: {");
449
- for (const variable of clientVars) {
450
- const effectiveType = variable.annotatedType ?? variable.inferredType;
451
- let zodExpr = toT3ZodType(effectiveType);
452
- if (variable.description !== void 0) {
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
- lines.push("});");
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;
@@ -610,7 +641,7 @@ async function runGenerate(options) {
610
641
  const parsed = parseEnvFileContent(
611
642
  content,
612
643
  inputPath,
613
- inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
644
+ inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
614
645
  );
615
646
  for (const generator of generators) {
616
647
  let generated = buildOutput(generator, parsed);
@@ -938,8 +969,13 @@ function readEntryValue(entry, keys) {
938
969
  }
939
970
  return void 0;
940
971
  }
972
+ function getVercelEntries(value) {
973
+ if (Array.isArray(value)) return value;
974
+ if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
975
+ return [];
976
+ }
941
977
  function parseVercelPayload(value) {
942
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.envs) ? value.envs : [];
978
+ const entries = getVercelEntries(value);
943
979
  const result = {};
944
980
  for (const entry of entries) {
945
981
  if (!isRecord(entry)) continue;
@@ -950,8 +986,13 @@ function parseVercelPayload(value) {
950
986
  }
951
987
  return result;
952
988
  }
989
+ function getCloudflareEntries(value) {
990
+ if (Array.isArray(value)) return value;
991
+ if (isRecord(value) && Array.isArray(value.result)) return value.result;
992
+ return [];
993
+ }
953
994
  function parseCloudflarePayload(value) {
954
- const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.result) ? value.result : [];
995
+ const entries = getCloudflareEntries(value);
955
996
  const result = {};
956
997
  for (const entry of entries) {
957
998
  if (!isRecord(entry)) continue;
@@ -969,7 +1010,7 @@ function parseAwsPayload(value) {
969
1010
  if (!isRecord(entry)) continue;
970
1011
  const name = readEntryValue(entry, ["Name", "name"]);
971
1012
  if (name === void 0) continue;
972
- const key = name.split("/").filter((part) => part.length > 0).pop() ?? name;
1013
+ const key = name.split("/").findLast((part) => part.length > 0) ?? name;
973
1014
  const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
974
1015
  result[key] = envValue;
975
1016
  }
@@ -1109,7 +1150,7 @@ function findDefaultContractPath(cwd) {
1109
1150
  async function loadValidationContract(options) {
1110
1151
  const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
1111
1152
  const discoveredContractPath = findDefaultContractPath(cwd);
1112
- const resolvedContractPath = contractPath !== void 0 ? path6.resolve(cwd, contractPath) : discoveredContractPath;
1153
+ const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6.resolve(cwd, contractPath);
1113
1154
  if (resolvedContractPath !== void 0 && existsSync(resolvedContractPath)) {
1114
1155
  const moduleUrl = pathToFileURL(resolvedContractPath).href;
1115
1156
  const moduleValue = await import(moduleUrl);
@@ -1148,7 +1189,14 @@ async function loadPluginFromPath(pluginPath, cwd) {
1148
1189
  const moduleValue = await import(pathToFileURL(resolvedPath).href);
1149
1190
  const candidate = moduleValue.default ?? moduleValue.plugin;
1150
1191
  if (isPlugin(candidate)) return candidate;
1151
- throw new Error(`Invalid plugin at ${resolvedPath}. Expected a plugin object export.`);
1192
+ throw new Error(
1193
+ `Invalid plugin at ${resolvedPath}.
1194
+ Expected a default export matching:
1195
+ { name: string,
1196
+ transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
1197
+ transformReport?(report: ValidationReport): ValidationReport,
1198
+ transformContract?(contract: EnvContract): EnvContract }`
1199
+ );
1152
1200
  }
1153
1201
  async function loadPlugins(options) {
1154
1202
  const cwd = options.cwd ?? process.cwd();
@@ -1193,8 +1241,8 @@ function applyReportPlugins(report, plugins) {
1193
1241
  }
1194
1242
 
1195
1243
  // src/validation/engine.ts
1196
- var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
1197
- var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
1244
+ var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
1245
+ var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
1198
1246
  function detectReceivedType(value) {
1199
1247
  const normalized = value.trim();
1200
1248
  if (normalized.length === 0) return "unknown";
@@ -1214,65 +1262,72 @@ function detectReceivedType(value) {
1214
1262
  }
1215
1263
  return "string";
1216
1264
  }
1217
- function validateValueAgainstExpected(expected, rawValue) {
1218
- const normalized = rawValue.trim();
1219
- const receivedType = detectReceivedType(normalized);
1220
- if (expected.type === "unknown") return { isValid: true, receivedType };
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 };
1265
+ function validateNumber(expected, normalized, receivedType) {
1266
+ const parsed = Number(normalized);
1267
+ if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
1268
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1234
1269
  }
1235
- if (expected.type === "boolean") {
1236
- if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1237
- return { isValid: false, receivedType, issueType: "invalid_type" };
1238
- }
1239
- return { isValid: true, receivedType };
1270
+ if (expected.min !== void 0 && parsed < expected.min) {
1271
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1240
1272
  }
1241
- if (expected.type === "enum") {
1242
- if (!expected.values.includes(normalized)) {
1243
- return { isValid: false, receivedType, issueType: "invalid_value" };
1244
- }
1245
- return { isValid: true, receivedType };
1273
+ if (expected.max !== void 0 && parsed > expected.max) {
1274
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1246
1275
  }
1247
- if (expected.type === "url") {
1248
- try {
1249
- const value = new URL(normalized);
1250
- if (value.protocol.length === 0)
1251
- return { isValid: false, receivedType, issueType: "invalid_type" };
1252
- return { isValid: true, receivedType };
1253
- } catch {
1254
- return { isValid: false, receivedType, issueType: "invalid_type" };
1255
- }
1276
+ return { isValid: true, receivedType };
1277
+ }
1278
+ function validateBoolean(normalized, receivedType) {
1279
+ if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
1280
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1281
+ }
1282
+ return { isValid: true, receivedType };
1283
+ }
1284
+ function validateEnum(expected, normalized, receivedType) {
1285
+ if (!expected.values.includes(normalized)) {
1286
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1256
1287
  }
1257
- if (expected.type === "email") {
1258
- if (!EMAIL_RE.test(normalized))
1288
+ return { isValid: true, receivedType };
1289
+ }
1290
+ function validateUrl(normalized, receivedType) {
1291
+ try {
1292
+ const value = new URL(normalized);
1293
+ if (value.protocol.length === 0)
1259
1294
  return { isValid: false, receivedType, issueType: "invalid_type" };
1260
1295
  return { isValid: true, receivedType };
1296
+ } catch {
1297
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1261
1298
  }
1262
- if (expected.type === "json") {
1263
- try {
1264
- const parsed = JSON.parse(normalized);
1265
- if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1266
- return { isValid: false, receivedType, issueType: "invalid_type" };
1267
- } catch {
1268
- return { isValid: false, receivedType, issueType: "invalid_type" };
1269
- }
1299
+ }
1300
+ function validateEmail(normalized, receivedType) {
1301
+ if (!EMAIL_RE.test(normalized))
1302
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1303
+ return { isValid: true, receivedType };
1304
+ }
1305
+ function validateJson(normalized, receivedType) {
1306
+ try {
1307
+ const parsed = JSON.parse(normalized);
1308
+ if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
1309
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1310
+ } catch {
1311
+ return { isValid: false, receivedType, issueType: "invalid_type" };
1270
1312
  }
1271
- if (expected.type === "semver") {
1272
- if (!SEMVER_RE2.test(normalized))
1273
- return { isValid: false, receivedType, issueType: "invalid_value" };
1313
+ }
1314
+ function validateSemver(normalized, receivedType) {
1315
+ if (!SEMVER_RE2.test(normalized))
1316
+ return { isValid: false, receivedType, issueType: "invalid_value" };
1317
+ return { isValid: true, receivedType };
1318
+ }
1319
+ function validateValueAgainstExpected(expected, rawValue) {
1320
+ const normalized = rawValue.trim();
1321
+ const receivedType = detectReceivedType(normalized);
1322
+ if (expected.type === "unknown" || expected.type === "string")
1274
1323
  return { isValid: true, receivedType };
1275
- }
1324
+ if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
1325
+ if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
1326
+ if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
1327
+ if (expected.type === "url") return validateUrl(normalized, receivedType);
1328
+ if (expected.type === "email") return validateEmail(normalized, receivedType);
1329
+ if (expected.type === "json") return validateJson(normalized, receivedType);
1330
+ if (expected.type === "semver") return validateSemver(normalized, receivedType);
1276
1331
  return { isValid: true, receivedType };
1277
1332
  }
1278
1333
  function toIssueCode(issueType) {
@@ -1344,57 +1399,62 @@ function buildReport(env, issues, recommendations) {
1344
1399
  function isClientSecret(variable, key) {
1345
1400
  return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
1346
1401
  }
1402
+ function checkContractVariable(key, variable, context) {
1403
+ const { options, issues } = context;
1404
+ const value = options.values[key];
1405
+ const hasValue = value !== void 0 && value.trim().length > 0;
1406
+ if (variable.required && !hasValue) {
1407
+ issues.push(
1408
+ createIssue({
1409
+ type: "missing",
1410
+ severity: "error",
1411
+ key,
1412
+ environment: options.environment,
1413
+ message: `Required variable ${key} is missing.`,
1414
+ debugValues: options.debugValues,
1415
+ expected: variable.expected
1416
+ })
1417
+ );
1418
+ return;
1419
+ }
1420
+ if (!hasValue) return;
1421
+ const validation = validateValueAgainstExpected(variable.expected, value);
1422
+ if (!validation.isValid) {
1423
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
1424
+ issues.push(
1425
+ createIssue({
1426
+ type: validation.issueType,
1427
+ severity: "error",
1428
+ key,
1429
+ environment: options.environment,
1430
+ message,
1431
+ value,
1432
+ debugValues: options.debugValues,
1433
+ expected: variable.expected,
1434
+ receivedType: validation.receivedType
1435
+ })
1436
+ );
1437
+ }
1438
+ if (isClientSecret(variable, key)) {
1439
+ issues.push(
1440
+ createIssue({
1441
+ type: "secret_exposed",
1442
+ severity: "error",
1443
+ key,
1444
+ environment: options.environment,
1445
+ message: `Secret variable ${key} is marked as client-side.`,
1446
+ value,
1447
+ debugValues: options.debugValues,
1448
+ expected: variable.expected
1449
+ })
1450
+ );
1451
+ }
1452
+ }
1347
1453
  function validateAgainstContract(options) {
1348
1454
  const issues = [];
1349
1455
  const contractKeys = new Set(Object.keys(options.contract.variables));
1350
1456
  for (const [key, variable] of Object.entries(options.contract.variables)) {
1351
- const value = options.values[key];
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
- }
1457
+ checkContractVariable(key, variable, { options, issues });
1398
1458
  }
1399
1459
  for (const [key, value] of Object.entries(options.values)) {
1400
1460
  if (contractKeys.has(key)) continue;
@@ -1422,6 +1482,97 @@ function collectUnionKeys(contract, sources) {
1422
1482
  }
1423
1483
  return union;
1424
1484
  }
1485
+ function diffMissingEntries(key, missing, context) {
1486
+ const { variable, options, issues } = context;
1487
+ for (const entry of missing) {
1488
+ issues.push(
1489
+ createIssue({
1490
+ type: "missing",
1491
+ severity: "error",
1492
+ key,
1493
+ environment: entry.sourceName,
1494
+ message: `Variable ${key} is missing in ${entry.sourceName}.`,
1495
+ debugValues: options.debugValues,
1496
+ ...variable !== void 0 && { expected: variable.expected }
1497
+ })
1498
+ );
1499
+ }
1500
+ }
1501
+ function diffTypeConflicts(key, present, context) {
1502
+ const { variable, options, issues } = context;
1503
+ const typeBySource = /* @__PURE__ */ new Map();
1504
+ for (const entry of present) {
1505
+ typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1506
+ }
1507
+ if (new Set(typeBySource.values()).size <= 1) return;
1508
+ for (const [sourceName, detectedType] of typeBySource.entries()) {
1509
+ issues.push(
1510
+ createIssue({
1511
+ type: "conflict",
1512
+ severity: "error",
1513
+ key,
1514
+ environment: sourceName,
1515
+ message: `Variable ${key} has conflicting inferred type across environments.`,
1516
+ debugValues: options.debugValues,
1517
+ receivedType: detectedType,
1518
+ ...options.sources[sourceName]?.[key] !== void 0 && {
1519
+ value: options.sources[sourceName]?.[key]
1520
+ },
1521
+ ...variable !== void 0 && { expected: variable.expected }
1522
+ })
1523
+ );
1524
+ }
1525
+ }
1526
+ function diffPresentEntry(key, entry, context) {
1527
+ const { variable, options, issues } = context;
1528
+ if (entry.value === void 0) return;
1529
+ if (variable === void 0) {
1530
+ const severity = options.strict ? "error" : "warning";
1531
+ issues.push(
1532
+ createIssue({
1533
+ type: "extra",
1534
+ severity,
1535
+ key,
1536
+ environment: entry.sourceName,
1537
+ message: `Variable ${key} is not defined in the contract.`,
1538
+ value: entry.value,
1539
+ debugValues: options.debugValues
1540
+ })
1541
+ );
1542
+ return;
1543
+ }
1544
+ const validation = validateValueAgainstExpected(variable.expected, entry.value);
1545
+ if (!validation.isValid) {
1546
+ const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
1547
+ issues.push(
1548
+ createIssue({
1549
+ type: validation.issueType,
1550
+ severity: "error",
1551
+ key,
1552
+ environment: entry.sourceName,
1553
+ message,
1554
+ value: entry.value,
1555
+ debugValues: options.debugValues,
1556
+ expected: variable.expected,
1557
+ receivedType: validation.receivedType
1558
+ })
1559
+ );
1560
+ }
1561
+ if (isClientSecret(variable, key)) {
1562
+ issues.push(
1563
+ createIssue({
1564
+ type: "secret_exposed",
1565
+ severity: "error",
1566
+ key,
1567
+ environment: entry.sourceName,
1568
+ message: `Secret variable ${key} is marked as client-side.`,
1569
+ value: entry.value,
1570
+ debugValues: options.debugValues,
1571
+ expected: variable.expected
1572
+ })
1573
+ );
1574
+ }
1575
+ }
1425
1576
  function diffEnvironmentSources(options) {
1426
1577
  const issues = [];
1427
1578
  const sourceNames = Object.keys(options.sources);
@@ -1454,92 +1605,13 @@ function diffEnvironmentSources(options) {
1454
1605
  }
1455
1606
  continue;
1456
1607
  }
1608
+ const ctx = { variable, options, issues };
1457
1609
  if (present.length > 0) {
1458
- for (const entry of missing) {
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
- }
1610
+ diffMissingEntries(key, missing, ctx);
1471
1611
  }
1472
- const typeBySource = /* @__PURE__ */ new Map();
1612
+ diffTypeConflicts(key, present, ctx);
1473
1613
  for (const entry of present) {
1474
- const detected = detectReceivedType(entry.value ?? "");
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
- }
1614
+ diffPresentEntry(key, entry, ctx);
1543
1615
  }
1544
1616
  }
1545
1617
  return buildReport("diff", issues);
@@ -1588,7 +1660,7 @@ function parseEnvSourceContent(content) {
1588
1660
  for (const line of lines) {
1589
1661
  const trimmed = line.trim();
1590
1662
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
1591
- const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
1663
+ const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
1592
1664
  if (match === null) continue;
1593
1665
  const key = match[1] ?? "";
1594
1666
  const rawValue = match[2] ?? "";
@@ -1601,11 +1673,11 @@ async function loadEnvSource(options) {
1601
1673
  try {
1602
1674
  const content = await readFile(resolvedPath, "utf8");
1603
1675
  return parseEnvSourceContent(content);
1604
- } catch (errorValue) {
1605
- if (options.allowMissing === true && errorValue instanceof Error && "code" in errorValue && errorValue.code === "ENOENT") {
1676
+ } catch (error_) {
1677
+ if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1606
1678
  return {};
1607
1679
  }
1608
- throw errorValue;
1680
+ throw error_;
1609
1681
  }
1610
1682
  }
1611
1683
  function toJsonString(report, mode) {
@@ -1615,8 +1687,8 @@ function toJsonString(report, mode) {
1615
1687
  `;
1616
1688
  }
1617
1689
  function formatIssue(issue) {
1618
- const expected = issue.expected !== void 0 ? ` expected=${issue.expected.type}` : "";
1619
- const received = issue.receivedType !== void 0 ? ` received=${issue.receivedType}` : "";
1690
+ const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
1691
+ const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
1620
1692
  return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
1621
1693
  }
1622
1694
  function formatHumanReport(report) {
@@ -1625,15 +1697,13 @@ function formatHumanReport(report) {
1625
1697
  `Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
1626
1698
  );
1627
1699
  if (report.issues.length > 0) {
1628
- lines.push("");
1629
- lines.push("Issues:");
1700
+ lines.push("", "Issues:");
1630
1701
  for (const issue of report.issues) {
1631
1702
  lines.push(`- ${formatIssue(issue)}`);
1632
1703
  }
1633
1704
  }
1634
1705
  if (report.recommendations !== void 0 && report.recommendations.length > 0) {
1635
- lines.push("");
1636
- lines.push("Recommendations:");
1706
+ lines.push("", "Recommendations:");
1637
1707
  for (const recommendation of report.recommendations) {
1638
1708
  lines.push(`- ${recommendation}`);
1639
1709
  }
@@ -1678,7 +1748,11 @@ var HELP_TEXT = {
1678
1748
  " --cloud-file <path> Cloud snapshot JSON file",
1679
1749
  " --plugin <path> Plugin module path (repeatable)",
1680
1750
  " -c, --config <path> Config file path",
1681
- " -h, --help Show this help"
1751
+ " -h, --help Show this help",
1752
+ "",
1753
+ "Exit codes:",
1754
+ " 0 All checks passed (status: ok or warn)",
1755
+ " 1 One or more checks failed (status: fail) or invalid usage"
1682
1756
  ].join("\n"),
1683
1757
  diff: [
1684
1758
  "Usage: env-typegen diff [options]",
@@ -1697,7 +1771,11 @@ var HELP_TEXT = {
1697
1771
  " --cloud-file <path> Cloud snapshot JSON file added to diff sources",
1698
1772
  " --plugin <path> Plugin module path (repeatable)",
1699
1773
  " -c, --config <path> Config file path",
1700
- " -h, --help Show this help"
1774
+ " -h, --help Show this help",
1775
+ "",
1776
+ "Exit codes:",
1777
+ " 0 All checks passed (status: ok or warn)",
1778
+ " 1 One or more checks failed (status: fail) or invalid usage"
1701
1779
  ].join("\n"),
1702
1780
  doctor: [
1703
1781
  "Usage: env-typegen doctor [options]",
@@ -1717,7 +1795,11 @@ var HELP_TEXT = {
1717
1795
  " --cloud-file <path> Cloud snapshot JSON file",
1718
1796
  " --plugin <path> Plugin module path (repeatable)",
1719
1797
  " -c, --config <path> Config file path",
1720
- " -h, --help Show this help"
1798
+ " -h, --help Show this help",
1799
+ "",
1800
+ "Exit codes:",
1801
+ " 0 All checks passed (status: ok or warn)",
1802
+ " 1 One or more checks failed (status: fail) or invalid usage"
1721
1803
  ].join("\n")
1722
1804
  };
1723
1805
  function resolveConfigRelative(value, configDir) {
@@ -1804,7 +1886,10 @@ function parseValidationArgs(argv) {
1804
1886
  }
1805
1887
  });
1806
1888
  const castValues = values;
1807
- const jsonMode = castValues.json === true ? assignedMode === "off" ? "compact" : assignedMode : "off";
1889
+ let jsonMode = "off";
1890
+ if (castValues.json === true) {
1891
+ jsonMode = assignedMode === "off" ? "compact" : assignedMode;
1892
+ }
1808
1893
  return { values: castValues, jsonMode };
1809
1894
  }
1810
1895
  function resolveStrict(values, fileConfig) {
@@ -1877,12 +1962,12 @@ async function runCheckCommand(args) {
1877
1962
  const provider = context.cloudProvider;
1878
1963
  let environment = args.values.env?.[0] ?? ".env";
1879
1964
  let sourceValues;
1880
- if (provider !== void 0) {
1965
+ if (provider === void 0) {
1966
+ sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1967
+ } else {
1881
1968
  const cloudFile = context.cloudFile ?? `${provider}.env.json`;
1882
1969
  sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
1883
1970
  environment = `cloud:${provider}`;
1884
- } else {
1885
- sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
1886
1971
  }
1887
1972
  sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
1888
1973
  const report = applyReportPlugins(