@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/cli.js
CHANGED
|
@@ -99,7 +99,7 @@ var require_picocolors = __commonJS({
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
// src/cli.ts
|
|
102
|
-
import { realpathSync } from "fs";
|
|
102
|
+
import { existsSync as existsSync6, realpathSync } from "fs";
|
|
103
103
|
import { createRequire } from "module";
|
|
104
104
|
import path13 from "path";
|
|
105
105
|
import { fileURLToPath, pathToFileURL as pathToFileURL5 } from "url";
|
|
@@ -110,14 +110,27 @@ import { existsSync } from "fs";
|
|
|
110
110
|
import path from "path";
|
|
111
111
|
import { pathToFileURL } from "url";
|
|
112
112
|
var CONFIG_FILE_NAMES = [
|
|
113
|
-
"env-typegen.config.ts",
|
|
114
113
|
"env-typegen.config.mjs",
|
|
115
|
-
"env-typegen.config.js"
|
|
114
|
+
"env-typegen.config.js",
|
|
115
|
+
"env-typegen.config.ts"
|
|
116
116
|
];
|
|
117
117
|
async function loadConfig(cwd = process.cwd()) {
|
|
118
118
|
for (const name of CONFIG_FILE_NAMES) {
|
|
119
119
|
const filePath = path.resolve(cwd, name);
|
|
120
120
|
if (existsSync(filePath)) {
|
|
121
|
+
if (filePath.endsWith(".ts")) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
|
|
124
|
+
Rename it to "env-typegen.config.mjs" and use ESM export syntax:
|
|
125
|
+
|
|
126
|
+
// env-typegen.config.mjs
|
|
127
|
+
import { defineConfig } from "@xlameiro/env-typegen";
|
|
128
|
+
export default defineConfig({ input: ".env.example" });
|
|
129
|
+
|
|
130
|
+
Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
|
|
131
|
+
env-typegen.config.mjs for runtime loading.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
121
134
|
const fileUrl = pathToFileURL(filePath).href;
|
|
122
135
|
const mod = await import(fileUrl);
|
|
123
136
|
return mod.default;
|
|
@@ -133,12 +146,13 @@ import path5 from "path";
|
|
|
133
146
|
import path2 from "path";
|
|
134
147
|
function generateDeclaration(parsed) {
|
|
135
148
|
const fileName = path2.basename(parsed.filePath);
|
|
136
|
-
const lines = [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
const lines = [
|
|
150
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
151
|
+
`// Source: ${fileName}`,
|
|
152
|
+
"",
|
|
153
|
+
"declare namespace NodeJS {",
|
|
154
|
+
" interface ProcessEnv {"
|
|
155
|
+
];
|
|
142
156
|
for (const variable of parsed.vars) {
|
|
143
157
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
144
158
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -153,12 +167,14 @@ function generateDeclaration(parsed) {
|
|
|
153
167
|
}
|
|
154
168
|
lines.push(propLine);
|
|
155
169
|
}
|
|
156
|
-
lines.push(" }");
|
|
157
|
-
lines.push("}");
|
|
170
|
+
lines.push(" }", "}");
|
|
158
171
|
return lines.join("\n") + "\n";
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
// src/generators/t3-generator.ts
|
|
175
|
+
function escapeJsStringLiteral(value) {
|
|
176
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
|
|
177
|
+
}
|
|
162
178
|
function toT3ZodType(envVarType) {
|
|
163
179
|
if (envVarType === "number") return "z.coerce.number()";
|
|
164
180
|
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
@@ -166,51 +182,47 @@ function toT3ZodType(envVarType) {
|
|
|
166
182
|
if (envVarType === "email") return "z.string().email()";
|
|
167
183
|
return "z.string()";
|
|
168
184
|
}
|
|
185
|
+
function buildZodExpr(variable) {
|
|
186
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
187
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
188
|
+
if (variable.description !== void 0) {
|
|
189
|
+
zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
|
|
190
|
+
}
|
|
191
|
+
if (variable.isOptional) {
|
|
192
|
+
zodExpr += ".optional()";
|
|
193
|
+
}
|
|
194
|
+
return zodExpr;
|
|
195
|
+
}
|
|
169
196
|
function generateT3Env(parsed) {
|
|
170
197
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
171
198
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
172
|
-
const lines = [
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
const lines = [
|
|
200
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
201
|
+
'import { createEnv } from "@t3-oss/env-nextjs";',
|
|
202
|
+
'import { z } from "zod";',
|
|
203
|
+
"",
|
|
204
|
+
"export const env = createEnv({"
|
|
205
|
+
];
|
|
178
206
|
if (serverVars.length > 0) {
|
|
179
|
-
lines.push(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
185
|
-
}
|
|
186
|
-
if (variable.isOptional) {
|
|
187
|
-
zodExpr += ".optional()";
|
|
188
|
-
}
|
|
189
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
190
|
-
}
|
|
191
|
-
lines.push(" },");
|
|
207
|
+
lines.push(
|
|
208
|
+
" server: {",
|
|
209
|
+
...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
210
|
+
" },"
|
|
211
|
+
);
|
|
192
212
|
}
|
|
193
213
|
if (clientVars.length > 0) {
|
|
194
|
-
lines.push(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
200
|
-
}
|
|
201
|
-
if (variable.isOptional) {
|
|
202
|
-
zodExpr += ".optional()";
|
|
203
|
-
}
|
|
204
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
205
|
-
}
|
|
206
|
-
lines.push(" },");
|
|
207
|
-
}
|
|
208
|
-
lines.push(" runtimeEnv: {");
|
|
209
|
-
for (const variable of parsed.vars) {
|
|
210
|
-
lines.push(` ${variable.key}: process.env.${variable.key},`);
|
|
214
|
+
lines.push(
|
|
215
|
+
" client: {",
|
|
216
|
+
...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
217
|
+
" },"
|
|
218
|
+
);
|
|
211
219
|
}
|
|
212
|
-
lines.push(
|
|
213
|
-
|
|
220
|
+
lines.push(
|
|
221
|
+
" runtimeEnv: {",
|
|
222
|
+
...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
|
|
223
|
+
" },",
|
|
224
|
+
"});"
|
|
225
|
+
);
|
|
214
226
|
return lines.join("\n") + "\n";
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -227,12 +239,14 @@ function generateTypeScriptTypes(parsed) {
|
|
|
227
239
|
const fileName = path3.basename(parsed.filePath);
|
|
228
240
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
229
241
|
const lines = [];
|
|
230
|
-
lines.push(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
lines.push(
|
|
243
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
244
|
+
`// Source: ${fileName}`,
|
|
245
|
+
`// Generated at: ${timestamp}`,
|
|
246
|
+
"",
|
|
247
|
+
"declare namespace NodeJS {",
|
|
248
|
+
" interface ProcessEnv {"
|
|
249
|
+
);
|
|
236
250
|
for (const variable of parsed.vars) {
|
|
237
251
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
238
252
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -247,10 +261,7 @@ function generateTypeScriptTypes(parsed) {
|
|
|
247
261
|
}
|
|
248
262
|
lines.push(propLine);
|
|
249
263
|
}
|
|
250
|
-
lines.push(" }");
|
|
251
|
-
lines.push("}");
|
|
252
|
-
lines.push("");
|
|
253
|
-
lines.push("export type EnvVars = {");
|
|
264
|
+
lines.push(" }", "}", "", "export type EnvVars = {");
|
|
254
265
|
for (const variable of parsed.vars) {
|
|
255
266
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
256
267
|
const tsType = toTsType(effectiveType);
|
|
@@ -260,9 +271,11 @@ function generateTypeScriptTypes(parsed) {
|
|
|
260
271
|
lines.push("};");
|
|
261
272
|
if (hasClientVars) {
|
|
262
273
|
const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
|
|
263
|
-
lines.push(
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
lines.push(
|
|
275
|
+
"",
|
|
276
|
+
`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
|
|
277
|
+
`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
|
|
278
|
+
);
|
|
266
279
|
}
|
|
267
280
|
return lines.join("\n") + "\n";
|
|
268
281
|
}
|
|
@@ -279,27 +292,29 @@ function generateZodSchema(parsed) {
|
|
|
279
292
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
280
293
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
281
294
|
const lines = [];
|
|
282
|
-
lines.push(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
lines.push(
|
|
296
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
297
|
+
'import { z } from "zod";',
|
|
298
|
+
"",
|
|
299
|
+
"export const serverEnvSchema = z.object({"
|
|
300
|
+
);
|
|
286
301
|
for (const variable of serverVars) {
|
|
287
302
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
288
303
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
289
304
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
290
305
|
}
|
|
291
|
-
lines.push("});");
|
|
292
|
-
lines.push("");
|
|
293
|
-
lines.push("export const clientEnvSchema = z.object({");
|
|
306
|
+
lines.push("});", "", "export const clientEnvSchema = z.object({");
|
|
294
307
|
for (const variable of clientVars) {
|
|
295
308
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
296
309
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
297
310
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
298
311
|
}
|
|
299
|
-
lines.push(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
lines.push(
|
|
313
|
+
"});",
|
|
314
|
+
"",
|
|
315
|
+
"export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
|
|
316
|
+
"export type Env = z.infer<typeof envSchema>;"
|
|
317
|
+
);
|
|
303
318
|
return lines.join("\n") + "\n";
|
|
304
319
|
}
|
|
305
320
|
|
|
@@ -359,7 +374,9 @@ var inferenceRules = [
|
|
|
359
374
|
{
|
|
360
375
|
id: "P8_numeric_literal",
|
|
361
376
|
priority: 8,
|
|
362
|
-
|
|
377
|
+
// Non-capturing group with \d keeps the dot/digit boundary unambiguous,
|
|
378
|
+
// eliminating super-linear backtracking (ReDoS-safe).
|
|
379
|
+
match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
|
|
363
380
|
type: "number"
|
|
364
381
|
},
|
|
365
382
|
{
|
|
@@ -377,7 +394,9 @@ var inferenceRules = [
|
|
|
377
394
|
{
|
|
378
395
|
id: "P11_email_literal",
|
|
379
396
|
priority: 11,
|
|
380
|
-
|
|
397
|
+
// Dots are excluded from each domain-segment character class so that the
|
|
398
|
+
// literal \. separators are unambiguous, preventing super-linear backtracking.
|
|
399
|
+
match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
|
|
381
400
|
type: "email"
|
|
382
401
|
},
|
|
383
402
|
{
|
|
@@ -494,7 +513,50 @@ function parseCommentBlock(lines) {
|
|
|
494
513
|
|
|
495
514
|
// src/parser/env-parser.ts
|
|
496
515
|
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
497
|
-
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(
|
|
516
|
+
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
|
|
517
|
+
function buildParsedVar(params, commentBlock, options) {
|
|
518
|
+
const annotations = parseCommentBlock(commentBlock);
|
|
519
|
+
const extraRules = options?.inferenceRules;
|
|
520
|
+
const inferredType = inferType(
|
|
521
|
+
params.key,
|
|
522
|
+
params.rawValue,
|
|
523
|
+
...extraRules === void 0 ? [] : [{ extraRules }]
|
|
524
|
+
);
|
|
525
|
+
const isRequired = params.rawValue.length > 0 || annotations.isRequired;
|
|
526
|
+
const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
|
|
527
|
+
const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
|
|
528
|
+
const parsedVar = {
|
|
529
|
+
key: params.key,
|
|
530
|
+
rawValue: params.rawValue,
|
|
531
|
+
inferredType,
|
|
532
|
+
isRequired,
|
|
533
|
+
isOptional,
|
|
534
|
+
isClientSide,
|
|
535
|
+
lineNumber: params.lineNumber
|
|
536
|
+
};
|
|
537
|
+
if (annotations.annotatedType !== void 0) {
|
|
538
|
+
parsedVar.annotatedType = annotations.annotatedType;
|
|
539
|
+
}
|
|
540
|
+
if (annotations.description !== void 0) {
|
|
541
|
+
parsedVar.description = annotations.description;
|
|
542
|
+
}
|
|
543
|
+
if (params.currentGroup !== void 0) {
|
|
544
|
+
parsedVar.group = params.currentGroup;
|
|
545
|
+
}
|
|
546
|
+
if (annotations.enumValues !== void 0) {
|
|
547
|
+
parsedVar.enumValues = annotations.enumValues;
|
|
548
|
+
}
|
|
549
|
+
if (annotations.constraints !== void 0) {
|
|
550
|
+
parsedVar.constraints = annotations.constraints;
|
|
551
|
+
}
|
|
552
|
+
if (annotations.runtime !== void 0) {
|
|
553
|
+
parsedVar.runtime = annotations.runtime;
|
|
554
|
+
}
|
|
555
|
+
if (annotations.isSecret !== void 0) {
|
|
556
|
+
parsedVar.isSecret = annotations.isSecret;
|
|
557
|
+
}
|
|
558
|
+
return parsedVar;
|
|
559
|
+
}
|
|
498
560
|
function parseEnvFileContent(content, filePath, options) {
|
|
499
561
|
const lines = content.split("\n");
|
|
500
562
|
const vars = [];
|
|
@@ -524,53 +586,18 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
524
586
|
continue;
|
|
525
587
|
}
|
|
526
588
|
const envMatch = ENV_VAR_RE.exec(trimmed);
|
|
527
|
-
if (envMatch
|
|
528
|
-
const key = envMatch[1] ?? "";
|
|
529
|
-
const rawValue = envMatch[2] ?? "";
|
|
530
|
-
const annotations = parseCommentBlock(commentBlock);
|
|
531
|
-
const inferredType = inferType(
|
|
532
|
-
key,
|
|
533
|
-
rawValue,
|
|
534
|
-
...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
|
|
535
|
-
);
|
|
536
|
-
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
537
|
-
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
538
|
-
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
539
|
-
const parsedVar = {
|
|
540
|
-
key,
|
|
541
|
-
rawValue,
|
|
542
|
-
inferredType,
|
|
543
|
-
isRequired,
|
|
544
|
-
isOptional,
|
|
545
|
-
isClientSide,
|
|
546
|
-
lineNumber
|
|
547
|
-
};
|
|
548
|
-
if (annotations.annotatedType !== void 0) {
|
|
549
|
-
parsedVar.annotatedType = annotations.annotatedType;
|
|
550
|
-
}
|
|
551
|
-
if (annotations.description !== void 0) {
|
|
552
|
-
parsedVar.description = annotations.description;
|
|
553
|
-
}
|
|
554
|
-
if (currentGroup !== void 0) {
|
|
555
|
-
parsedVar.group = currentGroup;
|
|
556
|
-
}
|
|
557
|
-
if (annotations.enumValues !== void 0) {
|
|
558
|
-
parsedVar.enumValues = annotations.enumValues;
|
|
559
|
-
}
|
|
560
|
-
if (annotations.constraints !== void 0) {
|
|
561
|
-
parsedVar.constraints = annotations.constraints;
|
|
562
|
-
}
|
|
563
|
-
if (annotations.runtime !== void 0) {
|
|
564
|
-
parsedVar.runtime = annotations.runtime;
|
|
565
|
-
}
|
|
566
|
-
if (annotations.isSecret !== void 0) {
|
|
567
|
-
parsedVar.isSecret = annotations.isSecret;
|
|
568
|
-
}
|
|
569
|
-
vars.push(parsedVar);
|
|
570
|
-
commentBlock = [];
|
|
571
|
-
} else {
|
|
589
|
+
if (envMatch === null) {
|
|
572
590
|
commentBlock = [];
|
|
591
|
+
continue;
|
|
573
592
|
}
|
|
593
|
+
vars.push(
|
|
594
|
+
buildParsedVar(
|
|
595
|
+
{ key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
|
|
596
|
+
commentBlock,
|
|
597
|
+
options
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
commentBlock = [];
|
|
574
601
|
}
|
|
575
602
|
return { filePath, vars, groups };
|
|
576
603
|
}
|
|
@@ -583,7 +610,15 @@ function parseEnvFile(filePath) {
|
|
|
583
610
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
584
611
|
import path4 from "path";
|
|
585
612
|
async function readEnvFile(filePath) {
|
|
586
|
-
|
|
613
|
+
const resolved = path4.resolve(filePath);
|
|
614
|
+
try {
|
|
615
|
+
return await readFile(resolved, "utf8");
|
|
616
|
+
} catch (err) {
|
|
617
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
618
|
+
throw new Error(`File not found: ${filePath}`);
|
|
619
|
+
}
|
|
620
|
+
throw err;
|
|
621
|
+
}
|
|
587
622
|
}
|
|
588
623
|
async function writeOutput(filePath, content) {
|
|
589
624
|
const resolved = path4.resolve(filePath);
|
|
@@ -607,14 +642,15 @@ async function formatOutput(content, parser = "typescript") {
|
|
|
607
642
|
|
|
608
643
|
// src/utils/logger.ts
|
|
609
644
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
645
|
+
var { green, red, yellow } = import_picocolors.default;
|
|
610
646
|
function log(message) {
|
|
611
647
|
console.log(message);
|
|
612
648
|
}
|
|
613
649
|
function error(message) {
|
|
614
|
-
console.error(
|
|
650
|
+
console.error(red(`\u2716 ${message}`));
|
|
615
651
|
}
|
|
616
652
|
function success(message) {
|
|
617
|
-
console.log(
|
|
653
|
+
console.log(green(`\u2714 ${message}`));
|
|
618
654
|
}
|
|
619
655
|
|
|
620
656
|
// src/pipeline.ts
|
|
@@ -657,10 +693,6 @@ async function persistOutput(params) {
|
|
|
657
693
|
}
|
|
658
694
|
if (dryRun) {
|
|
659
695
|
if (!silent) {
|
|
660
|
-
if (!isSingle) {
|
|
661
|
-
console.log(`// --- ${generator}: ${outputPath} ---`);
|
|
662
|
-
}
|
|
663
|
-
console.log(generated);
|
|
664
696
|
success(`Dry run: would write ${outputPath}`);
|
|
665
697
|
}
|
|
666
698
|
return;
|
|
@@ -690,7 +722,7 @@ async function runGenerate(options) {
|
|
|
690
722
|
const parsed = parseEnvFileContent(
|
|
691
723
|
content,
|
|
692
724
|
inputPath,
|
|
693
|
-
inferenceRules2
|
|
725
|
+
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
694
726
|
);
|
|
695
727
|
for (const generator of generators) {
|
|
696
728
|
let generated = buildOutput(generator, parsed);
|
|
@@ -712,6 +744,7 @@ async function runGenerate(options) {
|
|
|
712
744
|
}
|
|
713
745
|
|
|
714
746
|
// src/validation-command.ts
|
|
747
|
+
import { existsSync as existsSync4 } from "fs";
|
|
715
748
|
import path11 from "path";
|
|
716
749
|
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
717
750
|
import { parseArgs } from "util";
|
|
@@ -729,8 +762,13 @@ function readEntryValue(entry, keys) {
|
|
|
729
762
|
}
|
|
730
763
|
return void 0;
|
|
731
764
|
}
|
|
765
|
+
function getVercelEntries(value) {
|
|
766
|
+
if (Array.isArray(value)) return value;
|
|
767
|
+
if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
732
770
|
function parseVercelPayload(value) {
|
|
733
|
-
const entries =
|
|
771
|
+
const entries = getVercelEntries(value);
|
|
734
772
|
const result = {};
|
|
735
773
|
for (const entry of entries) {
|
|
736
774
|
if (!isRecord(entry)) continue;
|
|
@@ -741,8 +779,13 @@ function parseVercelPayload(value) {
|
|
|
741
779
|
}
|
|
742
780
|
return result;
|
|
743
781
|
}
|
|
782
|
+
function getCloudflareEntries(value) {
|
|
783
|
+
if (Array.isArray(value)) return value;
|
|
784
|
+
if (isRecord(value) && Array.isArray(value.result)) return value.result;
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
744
787
|
function parseCloudflarePayload(value) {
|
|
745
|
-
const entries =
|
|
788
|
+
const entries = getCloudflareEntries(value);
|
|
746
789
|
const result = {};
|
|
747
790
|
for (const entry of entries) {
|
|
748
791
|
if (!isRecord(entry)) continue;
|
|
@@ -760,7 +803,7 @@ function parseAwsPayload(value) {
|
|
|
760
803
|
if (!isRecord(entry)) continue;
|
|
761
804
|
const name = readEntryValue(entry, ["Name", "name"]);
|
|
762
805
|
if (name === void 0) continue;
|
|
763
|
-
const key = name.split("/").
|
|
806
|
+
const key = name.split("/").findLast((part) => part.length > 0) ?? name;
|
|
764
807
|
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
765
808
|
result[key] = envValue;
|
|
766
809
|
}
|
|
@@ -773,7 +816,15 @@ function parseProviderPayload(provider, value) {
|
|
|
773
816
|
}
|
|
774
817
|
async function loadCloudSource(options) {
|
|
775
818
|
const resolvedPath = path6.resolve(options.filePath);
|
|
776
|
-
|
|
819
|
+
let raw;
|
|
820
|
+
try {
|
|
821
|
+
raw = await readFile2(resolvedPath, "utf8");
|
|
822
|
+
} catch (err) {
|
|
823
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
824
|
+
throw new Error(`File not found: ${options.filePath}`);
|
|
825
|
+
}
|
|
826
|
+
throw err;
|
|
827
|
+
}
|
|
777
828
|
const parsed = JSON.parse(raw);
|
|
778
829
|
return parseProviderPayload(options.provider, parsed);
|
|
779
830
|
}
|
|
@@ -905,7 +956,7 @@ function findDefaultContractPath(cwd) {
|
|
|
905
956
|
async function loadValidationContract(options) {
|
|
906
957
|
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
907
958
|
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
908
|
-
const resolvedContractPath = contractPath
|
|
959
|
+
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path7.resolve(cwd, contractPath);
|
|
909
960
|
if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
|
|
910
961
|
const moduleUrl = pathToFileURL2(resolvedContractPath).href;
|
|
911
962
|
const moduleValue = await import(moduleUrl);
|
|
@@ -924,6 +975,7 @@ async function loadValidationContract(options) {
|
|
|
924
975
|
}
|
|
925
976
|
|
|
926
977
|
// src/plugins.ts
|
|
978
|
+
import { existsSync as existsSync3 } from "fs";
|
|
927
979
|
import path8 from "path";
|
|
928
980
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
929
981
|
function isRecord3(value) {
|
|
@@ -945,10 +997,20 @@ function isPlugin(value) {
|
|
|
945
997
|
}
|
|
946
998
|
async function loadPluginFromPath(pluginPath, cwd) {
|
|
947
999
|
const resolvedPath = path8.resolve(cwd, pluginPath);
|
|
1000
|
+
if (!existsSync3(resolvedPath)) {
|
|
1001
|
+
throw new Error(`Plugin not found: ${pluginPath}`);
|
|
1002
|
+
}
|
|
948
1003
|
const moduleValue = await import(pathToFileURL3(resolvedPath).href);
|
|
949
1004
|
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
950
1005
|
if (isPlugin(candidate)) return candidate;
|
|
951
|
-
throw new Error(
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`Invalid plugin at ${resolvedPath}.
|
|
1008
|
+
Expected a default export matching:
|
|
1009
|
+
{ name: string,
|
|
1010
|
+
transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
|
|
1011
|
+
transformReport?(report: ValidationReport): ValidationReport,
|
|
1012
|
+
transformContract?(contract: EnvContract): EnvContract }`
|
|
1013
|
+
);
|
|
952
1014
|
}
|
|
953
1015
|
async function loadPlugins(options) {
|
|
954
1016
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -993,8 +1055,8 @@ function applyReportPlugins(report, plugins) {
|
|
|
993
1055
|
}
|
|
994
1056
|
|
|
995
1057
|
// src/validation/engine.ts
|
|
996
|
-
var EMAIL_RE = /^[^@\s]+@[^@\s]
|
|
997
|
-
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[
|
|
1058
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
|
|
1059
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
998
1060
|
function detectReceivedType(value) {
|
|
999
1061
|
const normalized = value.trim();
|
|
1000
1062
|
if (normalized.length === 0) return "unknown";
|
|
@@ -1014,65 +1076,72 @@ function detectReceivedType(value) {
|
|
|
1014
1076
|
}
|
|
1015
1077
|
return "string";
|
|
1016
1078
|
}
|
|
1017
|
-
function
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (expected.type === "string") return { isValid: true, receivedType };
|
|
1022
|
-
if (expected.type === "number") {
|
|
1023
|
-
const parsed = Number(normalized);
|
|
1024
|
-
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1025
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1026
|
-
}
|
|
1027
|
-
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1028
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1029
|
-
}
|
|
1030
|
-
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1031
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1032
|
-
}
|
|
1033
|
-
return { isValid: true, receivedType };
|
|
1079
|
+
function validateNumber(expected, normalized, receivedType) {
|
|
1080
|
+
const parsed = Number(normalized);
|
|
1081
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1082
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1034
1083
|
}
|
|
1035
|
-
if (expected.
|
|
1036
|
-
|
|
1037
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1038
|
-
}
|
|
1039
|
-
return { isValid: true, receivedType };
|
|
1084
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1085
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1040
1086
|
}
|
|
1041
|
-
if (expected.
|
|
1042
|
-
|
|
1043
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1044
|
-
}
|
|
1045
|
-
return { isValid: true, receivedType };
|
|
1087
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1088
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1046
1089
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1090
|
+
return { isValid: true, receivedType };
|
|
1091
|
+
}
|
|
1092
|
+
function validateBoolean(normalized, receivedType) {
|
|
1093
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1094
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1095
|
+
}
|
|
1096
|
+
return { isValid: true, receivedType };
|
|
1097
|
+
}
|
|
1098
|
+
function validateEnum(expected, normalized, receivedType) {
|
|
1099
|
+
if (!expected.values.includes(normalized)) {
|
|
1100
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1056
1101
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1102
|
+
return { isValid: true, receivedType };
|
|
1103
|
+
}
|
|
1104
|
+
function validateUrl(normalized, receivedType) {
|
|
1105
|
+
try {
|
|
1106
|
+
const value = new URL(normalized);
|
|
1107
|
+
if (value.protocol.length === 0)
|
|
1059
1108
|
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1060
1109
|
return { isValid: true, receivedType };
|
|
1110
|
+
} catch {
|
|
1111
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1061
1112
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1113
|
+
}
|
|
1114
|
+
function validateEmail(normalized, receivedType) {
|
|
1115
|
+
if (!EMAIL_RE.test(normalized))
|
|
1116
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1117
|
+
return { isValid: true, receivedType };
|
|
1118
|
+
}
|
|
1119
|
+
function validateJson(normalized, receivedType) {
|
|
1120
|
+
try {
|
|
1121
|
+
const parsed = JSON.parse(normalized);
|
|
1122
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1123
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1124
|
+
} catch {
|
|
1125
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1070
1126
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1127
|
+
}
|
|
1128
|
+
function validateSemver(normalized, receivedType) {
|
|
1129
|
+
if (!SEMVER_RE.test(normalized))
|
|
1130
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1131
|
+
return { isValid: true, receivedType };
|
|
1132
|
+
}
|
|
1133
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1134
|
+
const normalized = rawValue.trim();
|
|
1135
|
+
const receivedType = detectReceivedType(normalized);
|
|
1136
|
+
if (expected.type === "unknown" || expected.type === "string")
|
|
1074
1137
|
return { isValid: true, receivedType };
|
|
1075
|
-
|
|
1138
|
+
if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
|
|
1139
|
+
if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
|
|
1140
|
+
if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
|
|
1141
|
+
if (expected.type === "url") return validateUrl(normalized, receivedType);
|
|
1142
|
+
if (expected.type === "email") return validateEmail(normalized, receivedType);
|
|
1143
|
+
if (expected.type === "json") return validateJson(normalized, receivedType);
|
|
1144
|
+
if (expected.type === "semver") return validateSemver(normalized, receivedType);
|
|
1076
1145
|
return { isValid: true, receivedType };
|
|
1077
1146
|
}
|
|
1078
1147
|
function toIssueCode(issueType) {
|
|
@@ -1144,57 +1213,62 @@ function buildReport(env, issues, recommendations) {
|
|
|
1144
1213
|
function isClientSecret(variable, key) {
|
|
1145
1214
|
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1146
1215
|
}
|
|
1216
|
+
function checkContractVariable(key, variable, context) {
|
|
1217
|
+
const { options, issues } = context;
|
|
1218
|
+
const value = options.values[key];
|
|
1219
|
+
const hasValue = value !== void 0;
|
|
1220
|
+
if (variable.required && !hasValue) {
|
|
1221
|
+
issues.push(
|
|
1222
|
+
createIssue({
|
|
1223
|
+
type: "missing",
|
|
1224
|
+
severity: "error",
|
|
1225
|
+
key,
|
|
1226
|
+
environment: options.environment,
|
|
1227
|
+
message: `Required variable ${key} is missing.`,
|
|
1228
|
+
debugValues: options.debugValues,
|
|
1229
|
+
expected: variable.expected
|
|
1230
|
+
})
|
|
1231
|
+
);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (!hasValue) return;
|
|
1235
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1236
|
+
if (!validation.isValid) {
|
|
1237
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
|
|
1238
|
+
issues.push(
|
|
1239
|
+
createIssue({
|
|
1240
|
+
type: validation.issueType,
|
|
1241
|
+
severity: "error",
|
|
1242
|
+
key,
|
|
1243
|
+
environment: options.environment,
|
|
1244
|
+
message,
|
|
1245
|
+
value,
|
|
1246
|
+
debugValues: options.debugValues,
|
|
1247
|
+
expected: variable.expected,
|
|
1248
|
+
receivedType: validation.receivedType
|
|
1249
|
+
})
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
if (isClientSecret(variable, key)) {
|
|
1253
|
+
issues.push(
|
|
1254
|
+
createIssue({
|
|
1255
|
+
type: "secret_exposed",
|
|
1256
|
+
severity: "error",
|
|
1257
|
+
key,
|
|
1258
|
+
environment: options.environment,
|
|
1259
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1260
|
+
value,
|
|
1261
|
+
debugValues: options.debugValues,
|
|
1262
|
+
expected: variable.expected
|
|
1263
|
+
})
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1147
1267
|
function validateAgainstContract(options) {
|
|
1148
1268
|
const issues = [];
|
|
1149
1269
|
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1150
1270
|
for (const [key, variable] of Object.entries(options.contract.variables)) {
|
|
1151
|
-
|
|
1152
|
-
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1153
|
-
if (variable.required && !hasValue) {
|
|
1154
|
-
issues.push(
|
|
1155
|
-
createIssue({
|
|
1156
|
-
type: "missing",
|
|
1157
|
-
severity: "error",
|
|
1158
|
-
key,
|
|
1159
|
-
environment: options.environment,
|
|
1160
|
-
message: `Required variable ${key} is missing.`,
|
|
1161
|
-
debugValues: options.debugValues,
|
|
1162
|
-
expected: variable.expected
|
|
1163
|
-
})
|
|
1164
|
-
);
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
if (!hasValue) continue;
|
|
1168
|
-
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1169
|
-
if (!validation.isValid) {
|
|
1170
|
-
issues.push(
|
|
1171
|
-
createIssue({
|
|
1172
|
-
type: validation.issueType,
|
|
1173
|
-
severity: "error",
|
|
1174
|
-
key,
|
|
1175
|
-
environment: options.environment,
|
|
1176
|
-
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`,
|
|
1177
|
-
value,
|
|
1178
|
-
debugValues: options.debugValues,
|
|
1179
|
-
expected: variable.expected,
|
|
1180
|
-
receivedType: validation.receivedType
|
|
1181
|
-
})
|
|
1182
|
-
);
|
|
1183
|
-
}
|
|
1184
|
-
if (isClientSecret(variable, key)) {
|
|
1185
|
-
issues.push(
|
|
1186
|
-
createIssue({
|
|
1187
|
-
type: "secret_exposed",
|
|
1188
|
-
severity: "error",
|
|
1189
|
-
key,
|
|
1190
|
-
environment: options.environment,
|
|
1191
|
-
message: `Secret variable ${key} is marked as client-side.`,
|
|
1192
|
-
value,
|
|
1193
|
-
debugValues: options.debugValues,
|
|
1194
|
-
expected: variable.expected
|
|
1195
|
-
})
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1271
|
+
checkContractVariable(key, variable, { options, issues });
|
|
1198
1272
|
}
|
|
1199
1273
|
for (const [key, value] of Object.entries(options.values)) {
|
|
1200
1274
|
if (contractKeys.has(key)) continue;
|
|
@@ -1222,6 +1296,97 @@ function collectUnionKeys(contract, sources) {
|
|
|
1222
1296
|
}
|
|
1223
1297
|
return union;
|
|
1224
1298
|
}
|
|
1299
|
+
function diffMissingEntries(key, missing, context) {
|
|
1300
|
+
const { variable, options, issues } = context;
|
|
1301
|
+
for (const entry of missing) {
|
|
1302
|
+
issues.push(
|
|
1303
|
+
createIssue({
|
|
1304
|
+
type: "missing",
|
|
1305
|
+
severity: "error",
|
|
1306
|
+
key,
|
|
1307
|
+
environment: entry.sourceName,
|
|
1308
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1309
|
+
debugValues: options.debugValues,
|
|
1310
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1311
|
+
})
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function diffTypeConflicts(key, present, context) {
|
|
1316
|
+
const { variable, options, issues } = context;
|
|
1317
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1318
|
+
for (const entry of present) {
|
|
1319
|
+
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1320
|
+
}
|
|
1321
|
+
if (new Set(typeBySource.values()).size <= 1) return;
|
|
1322
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1323
|
+
issues.push(
|
|
1324
|
+
createIssue({
|
|
1325
|
+
type: "conflict",
|
|
1326
|
+
severity: "error",
|
|
1327
|
+
key,
|
|
1328
|
+
environment: sourceName,
|
|
1329
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1330
|
+
debugValues: options.debugValues,
|
|
1331
|
+
receivedType: detectedType,
|
|
1332
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1333
|
+
value: options.sources[sourceName]?.[key]
|
|
1334
|
+
},
|
|
1335
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1336
|
+
})
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function diffPresentEntry(key, entry, context) {
|
|
1341
|
+
const { variable, options, issues } = context;
|
|
1342
|
+
if (entry.value === void 0) return;
|
|
1343
|
+
if (variable === void 0) {
|
|
1344
|
+
const severity = options.strict ? "error" : "warning";
|
|
1345
|
+
issues.push(
|
|
1346
|
+
createIssue({
|
|
1347
|
+
type: "extra",
|
|
1348
|
+
severity,
|
|
1349
|
+
key,
|
|
1350
|
+
environment: entry.sourceName,
|
|
1351
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1352
|
+
value: entry.value,
|
|
1353
|
+
debugValues: options.debugValues
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1359
|
+
if (!validation.isValid) {
|
|
1360
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
|
|
1361
|
+
issues.push(
|
|
1362
|
+
createIssue({
|
|
1363
|
+
type: validation.issueType,
|
|
1364
|
+
severity: "error",
|
|
1365
|
+
key,
|
|
1366
|
+
environment: entry.sourceName,
|
|
1367
|
+
message,
|
|
1368
|
+
value: entry.value,
|
|
1369
|
+
debugValues: options.debugValues,
|
|
1370
|
+
expected: variable.expected,
|
|
1371
|
+
receivedType: validation.receivedType
|
|
1372
|
+
})
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (isClientSecret(variable, key)) {
|
|
1376
|
+
issues.push(
|
|
1377
|
+
createIssue({
|
|
1378
|
+
type: "secret_exposed",
|
|
1379
|
+
severity: "error",
|
|
1380
|
+
key,
|
|
1381
|
+
environment: entry.sourceName,
|
|
1382
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1383
|
+
value: entry.value,
|
|
1384
|
+
debugValues: options.debugValues,
|
|
1385
|
+
expected: variable.expected
|
|
1386
|
+
})
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1225
1390
|
function diffEnvironmentSources(options) {
|
|
1226
1391
|
const issues = [];
|
|
1227
1392
|
const sourceNames = Object.keys(options.sources);
|
|
@@ -1232,12 +1397,8 @@ function diffEnvironmentSources(options) {
|
|
|
1232
1397
|
sourceName,
|
|
1233
1398
|
value: options.sources[sourceName]?.[key]
|
|
1234
1399
|
}));
|
|
1235
|
-
const present = valuesBySource.filter(
|
|
1236
|
-
|
|
1237
|
-
);
|
|
1238
|
-
const missing = valuesBySource.filter(
|
|
1239
|
-
(entry) => entry.value === void 0 || entry.value === ""
|
|
1240
|
-
);
|
|
1400
|
+
const present = valuesBySource.filter((entry) => entry.value !== void 0);
|
|
1401
|
+
const missing = valuesBySource.filter((entry) => entry.value === void 0);
|
|
1241
1402
|
if (present.length === 0 && variable?.required === true) {
|
|
1242
1403
|
for (const entry of missing) {
|
|
1243
1404
|
issues.push(
|
|
@@ -1254,92 +1415,13 @@ function diffEnvironmentSources(options) {
|
|
|
1254
1415
|
}
|
|
1255
1416
|
continue;
|
|
1256
1417
|
}
|
|
1418
|
+
const ctx = { variable, options, issues };
|
|
1257
1419
|
if (present.length > 0) {
|
|
1258
|
-
|
|
1259
|
-
issues.push(
|
|
1260
|
-
createIssue({
|
|
1261
|
-
type: "missing",
|
|
1262
|
-
severity: "error",
|
|
1263
|
-
key,
|
|
1264
|
-
environment: entry.sourceName,
|
|
1265
|
-
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1266
|
-
debugValues: options.debugValues,
|
|
1267
|
-
...variable !== void 0 && { expected: variable.expected }
|
|
1268
|
-
})
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1420
|
+
diffMissingEntries(key, missing, ctx);
|
|
1271
1421
|
}
|
|
1272
|
-
|
|
1422
|
+
diffTypeConflicts(key, present, ctx);
|
|
1273
1423
|
for (const entry of present) {
|
|
1274
|
-
|
|
1275
|
-
typeBySource.set(entry.sourceName, detected);
|
|
1276
|
-
}
|
|
1277
|
-
if (new Set(typeBySource.values()).size > 1) {
|
|
1278
|
-
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1279
|
-
issues.push(
|
|
1280
|
-
createIssue({
|
|
1281
|
-
type: "conflict",
|
|
1282
|
-
severity: "error",
|
|
1283
|
-
key,
|
|
1284
|
-
environment: sourceName,
|
|
1285
|
-
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1286
|
-
debugValues: options.debugValues,
|
|
1287
|
-
receivedType: detectedType,
|
|
1288
|
-
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1289
|
-
value: options.sources[sourceName]?.[key]
|
|
1290
|
-
},
|
|
1291
|
-
...variable !== void 0 && { expected: variable.expected }
|
|
1292
|
-
})
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
for (const entry of present) {
|
|
1297
|
-
if (entry.value === void 0) continue;
|
|
1298
|
-
if (variable === void 0) {
|
|
1299
|
-
const severity = options.strict ? "error" : "warning";
|
|
1300
|
-
issues.push(
|
|
1301
|
-
createIssue({
|
|
1302
|
-
type: "extra",
|
|
1303
|
-
severity,
|
|
1304
|
-
key,
|
|
1305
|
-
environment: entry.sourceName,
|
|
1306
|
-
message: `Variable ${key} is not defined in the contract.`,
|
|
1307
|
-
value: entry.value,
|
|
1308
|
-
debugValues: options.debugValues
|
|
1309
|
-
})
|
|
1310
|
-
);
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1314
|
-
if (!validation.isValid) {
|
|
1315
|
-
issues.push(
|
|
1316
|
-
createIssue({
|
|
1317
|
-
type: validation.issueType,
|
|
1318
|
-
severity: "error",
|
|
1319
|
-
key,
|
|
1320
|
-
environment: entry.sourceName,
|
|
1321
|
-
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`,
|
|
1322
|
-
value: entry.value,
|
|
1323
|
-
debugValues: options.debugValues,
|
|
1324
|
-
expected: variable.expected,
|
|
1325
|
-
receivedType: validation.receivedType
|
|
1326
|
-
})
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
if (isClientSecret(variable, key)) {
|
|
1330
|
-
issues.push(
|
|
1331
|
-
createIssue({
|
|
1332
|
-
type: "secret_exposed",
|
|
1333
|
-
severity: "error",
|
|
1334
|
-
key,
|
|
1335
|
-
environment: entry.sourceName,
|
|
1336
|
-
message: `Secret variable ${key} is marked as client-side.`,
|
|
1337
|
-
value: entry.value,
|
|
1338
|
-
debugValues: options.debugValues,
|
|
1339
|
-
expected: variable.expected
|
|
1340
|
-
})
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1424
|
+
diffPresentEntry(key, entry, ctx);
|
|
1343
1425
|
}
|
|
1344
1426
|
}
|
|
1345
1427
|
return buildReport("diff", issues);
|
|
@@ -1392,7 +1474,7 @@ function parseEnvSourceContent(content) {
|
|
|
1392
1474
|
for (const line of lines) {
|
|
1393
1475
|
const trimmed = line.trim();
|
|
1394
1476
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1395
|
-
const match = /^(?:export\s+)?([A-Za-z_]
|
|
1477
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
|
|
1396
1478
|
if (match === null) continue;
|
|
1397
1479
|
const key = match[1] ?? "";
|
|
1398
1480
|
const rawValue = match[2] ?? "";
|
|
@@ -1405,11 +1487,11 @@ async function loadEnvSource(options) {
|
|
|
1405
1487
|
try {
|
|
1406
1488
|
const content = await readFile3(resolvedPath, "utf8");
|
|
1407
1489
|
return parseEnvSourceContent(content);
|
|
1408
|
-
} catch (
|
|
1409
|
-
if (options.allowMissing === true &&
|
|
1490
|
+
} catch (error_) {
|
|
1491
|
+
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1410
1492
|
return {};
|
|
1411
1493
|
}
|
|
1412
|
-
throw
|
|
1494
|
+
throw error_;
|
|
1413
1495
|
}
|
|
1414
1496
|
}
|
|
1415
1497
|
|
|
@@ -1423,8 +1505,8 @@ function toJsonString(report, mode) {
|
|
|
1423
1505
|
`;
|
|
1424
1506
|
}
|
|
1425
1507
|
function formatIssue(issue) {
|
|
1426
|
-
const expected = issue.expected
|
|
1427
|
-
const received = issue.receivedType
|
|
1508
|
+
const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
|
|
1509
|
+
const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
|
|
1428
1510
|
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1429
1511
|
}
|
|
1430
1512
|
function formatHumanReport(report) {
|
|
@@ -1433,15 +1515,13 @@ function formatHumanReport(report) {
|
|
|
1433
1515
|
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1434
1516
|
);
|
|
1435
1517
|
if (report.issues.length > 0) {
|
|
1436
|
-
lines.push("");
|
|
1437
|
-
lines.push("Issues:");
|
|
1518
|
+
lines.push("", "Issues:");
|
|
1438
1519
|
for (const issue of report.issues) {
|
|
1439
1520
|
lines.push(`- ${formatIssue(issue)}`);
|
|
1440
1521
|
}
|
|
1441
1522
|
}
|
|
1442
1523
|
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1443
|
-
lines.push("");
|
|
1444
|
-
lines.push("Recommendations:");
|
|
1524
|
+
lines.push("", "Recommendations:");
|
|
1445
1525
|
for (const recommendation of report.recommendations) {
|
|
1446
1526
|
lines.push(`- ${recommendation}`);
|
|
1447
1527
|
}
|
|
@@ -1486,7 +1566,11 @@ var HELP_TEXT = {
|
|
|
1486
1566
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1487
1567
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1488
1568
|
" -c, --config <path> Config file path",
|
|
1489
|
-
" -h, --help Show this help"
|
|
1569
|
+
" -h, --help Show this help",
|
|
1570
|
+
"",
|
|
1571
|
+
"Exit codes:",
|
|
1572
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1573
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1490
1574
|
].join("\n"),
|
|
1491
1575
|
diff: [
|
|
1492
1576
|
"Usage: env-typegen diff [options]",
|
|
@@ -1505,7 +1589,11 @@ var HELP_TEXT = {
|
|
|
1505
1589
|
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1506
1590
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1507
1591
|
" -c, --config <path> Config file path",
|
|
1508
|
-
" -h, --help Show this help"
|
|
1592
|
+
" -h, --help Show this help",
|
|
1593
|
+
"",
|
|
1594
|
+
"Exit codes:",
|
|
1595
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1596
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1509
1597
|
].join("\n"),
|
|
1510
1598
|
doctor: [
|
|
1511
1599
|
"Usage: env-typegen doctor [options]",
|
|
@@ -1525,7 +1613,11 @@ var HELP_TEXT = {
|
|
|
1525
1613
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1526
1614
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1527
1615
|
" -c, --config <path> Config file path",
|
|
1528
|
-
" -h, --help Show this help"
|
|
1616
|
+
" -h, --help Show this help",
|
|
1617
|
+
"",
|
|
1618
|
+
"Exit codes:",
|
|
1619
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1620
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1529
1621
|
].join("\n")
|
|
1530
1622
|
};
|
|
1531
1623
|
function resolveConfigRelative(value, configDir) {
|
|
@@ -1567,6 +1659,9 @@ async function loadCommandConfig(configPath) {
|
|
|
1567
1659
|
return loadConfig(process.cwd());
|
|
1568
1660
|
}
|
|
1569
1661
|
const resolvedPath = path11.resolve(configPath);
|
|
1662
|
+
if (!existsSync4(resolvedPath)) {
|
|
1663
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
1664
|
+
}
|
|
1570
1665
|
const configDir = path11.dirname(resolvedPath);
|
|
1571
1666
|
const moduleValue = await import(pathToFileURL4(resolvedPath).href);
|
|
1572
1667
|
if (moduleValue.default === void 0) return void 0;
|
|
@@ -1612,7 +1707,10 @@ function parseValidationArgs(argv) {
|
|
|
1612
1707
|
}
|
|
1613
1708
|
});
|
|
1614
1709
|
const castValues = values;
|
|
1615
|
-
|
|
1710
|
+
let jsonMode = "off";
|
|
1711
|
+
if (castValues.json === true) {
|
|
1712
|
+
jsonMode = assignedMode === "off" ? "compact" : assignedMode;
|
|
1713
|
+
}
|
|
1616
1714
|
return { values: castValues, jsonMode };
|
|
1617
1715
|
}
|
|
1618
1716
|
function resolveStrict(values, fileConfig) {
|
|
@@ -1685,12 +1783,12 @@ async function runCheckCommand(args) {
|
|
|
1685
1783
|
const provider = context.cloudProvider;
|
|
1686
1784
|
let environment = args.values.env?.[0] ?? ".env";
|
|
1687
1785
|
let sourceValues;
|
|
1688
|
-
if (provider
|
|
1786
|
+
if (provider === void 0) {
|
|
1787
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1788
|
+
} else {
|
|
1689
1789
|
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1690
1790
|
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1691
1791
|
environment = `cloud:${provider}`;
|
|
1692
|
-
} else {
|
|
1693
|
-
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1694
1792
|
}
|
|
1695
1793
|
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1696
1794
|
const report = applyReportPlugins(
|
|
@@ -1822,8 +1920,9 @@ async function runValidationCommand(params) {
|
|
|
1822
1920
|
}
|
|
1823
1921
|
|
|
1824
1922
|
// src/watch.ts
|
|
1825
|
-
import path12 from "path";
|
|
1826
1923
|
import { watch } from "chokidar";
|
|
1924
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1925
|
+
import path12 from "path";
|
|
1827
1926
|
function debounce(fn, delay) {
|
|
1828
1927
|
let timer;
|
|
1829
1928
|
return (...args) => {
|
|
@@ -1835,6 +1934,13 @@ function debounce(fn, delay) {
|
|
|
1835
1934
|
};
|
|
1836
1935
|
}
|
|
1837
1936
|
function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
1937
|
+
const inputPaths = Array.isArray(inputPath) ? inputPath : [inputPath];
|
|
1938
|
+
for (const singlePath of inputPaths) {
|
|
1939
|
+
if (!existsSync5(singlePath)) {
|
|
1940
|
+
error(`File not found: ${singlePath}`);
|
|
1941
|
+
return process.exit(1);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1838
1944
|
const inputLabel = Array.isArray(inputPath) ? inputPath.join(", ") : inputPath;
|
|
1839
1945
|
log(`Watching ${inputLabel} for changes...`);
|
|
1840
1946
|
void runGenerate(runOptions).catch((err) => {
|
|
@@ -1858,6 +1964,9 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
1858
1964
|
if (reloaded.inferenceRules !== void 0) {
|
|
1859
1965
|
runOptions.inferenceRules = reloaded.inferenceRules;
|
|
1860
1966
|
}
|
|
1967
|
+
if (reloaded.output !== void 0) {
|
|
1968
|
+
runOptions.output = reloaded.output;
|
|
1969
|
+
}
|
|
1861
1970
|
}
|
|
1862
1971
|
void runGenerate(runOptions).catch((err) => {
|
|
1863
1972
|
const message = err instanceof Error ? err.message : JSON.stringify(err);
|
|
@@ -1875,7 +1984,7 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
1875
1984
|
const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
|
|
1876
1985
|
const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
|
|
1877
1986
|
for (const event of ["add", "change"]) {
|
|
1878
|
-
configWatcher.on(event, (eventPath) =>
|
|
1987
|
+
configWatcher.on(event, (eventPath) => handleConfigChange(eventPath));
|
|
1879
1988
|
}
|
|
1880
1989
|
process.on("SIGINT", () => {
|
|
1881
1990
|
void Promise.all([inputWatcher.close(), configWatcher.close()]).then(() => {
|
|
@@ -1899,7 +2008,9 @@ var HELP_TEXT2 = [
|
|
|
1899
2008
|
"",
|
|
1900
2009
|
"Options:",
|
|
1901
2010
|
" -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
|
|
1902
|
-
" -o, --output <path> Output
|
|
2011
|
+
" -o, --output <path> Output base path (default: env.generated.ts).",
|
|
2012
|
+
" With multiple generators, suffixes are appended:",
|
|
2013
|
+
" env.generated.typescript.ts, .zod.ts, .t3.ts, .declaration.d.ts",
|
|
1903
2014
|
" -f, --format <name> Generator format: ts|zod|t3|declaration",
|
|
1904
2015
|
" May be specified multiple times.",
|
|
1905
2016
|
" -g, --generator <name> Backward-compatible alias for --format",
|
|
@@ -1910,7 +2021,16 @@ var HELP_TEXT2 = [
|
|
|
1910
2021
|
" -w, --watch Watch for changes and regenerate",
|
|
1911
2022
|
" -c, --config <path> Path to config file",
|
|
1912
2023
|
" -v, --version Print version",
|
|
1913
|
-
" -h, --help Show this help"
|
|
2024
|
+
" -h, --help Show this help",
|
|
2025
|
+
"",
|
|
2026
|
+
"Config file:",
|
|
2027
|
+
" Auto-discovered in order: env-typegen.config.mjs \u2192 .js \u2192 .ts (in cwd)",
|
|
2028
|
+
" CLI flags always override config file values.",
|
|
2029
|
+
" Use defineConfig() from @xlameiro/env-typegen for IDE autocompletion.",
|
|
2030
|
+
"",
|
|
2031
|
+
"Exit codes:",
|
|
2032
|
+
" 0 Success \u2014 files generated without errors",
|
|
2033
|
+
" 1 Error \u2014 invalid flags or generation failed"
|
|
1914
2034
|
].join("\n");
|
|
1915
2035
|
var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
|
|
1916
2036
|
var FORMAT_TO_GENERATOR = {
|
|
@@ -1966,6 +2086,15 @@ async function runValidationSubcommand(subcommand, argv) {
|
|
|
1966
2086
|
process.exitCode = exitCode;
|
|
1967
2087
|
}
|
|
1968
2088
|
}
|
|
2089
|
+
async function loadExplicitConfig(configPath, userPath) {
|
|
2090
|
+
if (!existsSync6(configPath)) {
|
|
2091
|
+
throw new Error(`Config file not found: ${userPath}`);
|
|
2092
|
+
}
|
|
2093
|
+
const configDir = path13.dirname(configPath);
|
|
2094
|
+
const mod = await import(pathToFileURL5(configPath).href);
|
|
2095
|
+
const rawConfig = mod.default;
|
|
2096
|
+
return rawConfig ? applyConfigPaths2(rawConfig, configDir) : void 0;
|
|
2097
|
+
}
|
|
1969
2098
|
async function runCli(argv = process.argv.slice(2)) {
|
|
1970
2099
|
const maybeSubcommand = argv[0];
|
|
1971
2100
|
if (maybeSubcommand !== void 0 && VALIDATION_SUBCOMMANDS.has(maybeSubcommand)) {
|
|
@@ -2001,13 +2130,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
2001
2130
|
if (values.config === void 0) {
|
|
2002
2131
|
fileConfig = await loadConfig(process.cwd());
|
|
2003
2132
|
} else {
|
|
2004
|
-
|
|
2005
|
-
const configDir = path13.dirname(configPath);
|
|
2006
|
-
const mod = await import(pathToFileURL5(configPath).href);
|
|
2007
|
-
const rawConfig = mod.default;
|
|
2008
|
-
if (rawConfig) {
|
|
2009
|
-
fileConfig = applyConfigPaths2(rawConfig, configDir);
|
|
2010
|
-
}
|
|
2133
|
+
fileConfig = await loadExplicitConfig(path13.resolve(values.config), values.config);
|
|
2011
2134
|
}
|
|
2012
2135
|
const cliInput = values.input?.length ? values.input : void 0;
|
|
2013
2136
|
const input = cliInput ?? fileConfig?.input;
|