@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/CHANGELOG.md +12 -0
- package/README.md +67 -105
- package/dist/cli.js +424 -329
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +419 -334
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +419 -334
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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
|
}
|
|
@@ -690,7 +717,7 @@ async function runGenerate(options) {
|
|
|
690
717
|
const parsed = parseEnvFileContent(
|
|
691
718
|
content,
|
|
692
719
|
inputPath,
|
|
693
|
-
inferenceRules2
|
|
720
|
+
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
694
721
|
);
|
|
695
722
|
for (const generator of generators) {
|
|
696
723
|
let generated = buildOutput(generator, parsed);
|
|
@@ -729,8 +756,13 @@ function readEntryValue(entry, keys) {
|
|
|
729
756
|
}
|
|
730
757
|
return void 0;
|
|
731
758
|
}
|
|
759
|
+
function getVercelEntries(value) {
|
|
760
|
+
if (Array.isArray(value)) return value;
|
|
761
|
+
if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
732
764
|
function parseVercelPayload(value) {
|
|
733
|
-
const entries =
|
|
765
|
+
const entries = getVercelEntries(value);
|
|
734
766
|
const result = {};
|
|
735
767
|
for (const entry of entries) {
|
|
736
768
|
if (!isRecord(entry)) continue;
|
|
@@ -741,8 +773,13 @@ function parseVercelPayload(value) {
|
|
|
741
773
|
}
|
|
742
774
|
return result;
|
|
743
775
|
}
|
|
776
|
+
function getCloudflareEntries(value) {
|
|
777
|
+
if (Array.isArray(value)) return value;
|
|
778
|
+
if (isRecord(value) && Array.isArray(value.result)) return value.result;
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
744
781
|
function parseCloudflarePayload(value) {
|
|
745
|
-
const entries =
|
|
782
|
+
const entries = getCloudflareEntries(value);
|
|
746
783
|
const result = {};
|
|
747
784
|
for (const entry of entries) {
|
|
748
785
|
if (!isRecord(entry)) continue;
|
|
@@ -760,7 +797,7 @@ function parseAwsPayload(value) {
|
|
|
760
797
|
if (!isRecord(entry)) continue;
|
|
761
798
|
const name = readEntryValue(entry, ["Name", "name"]);
|
|
762
799
|
if (name === void 0) continue;
|
|
763
|
-
const key = name.split("/").
|
|
800
|
+
const key = name.split("/").findLast((part) => part.length > 0) ?? name;
|
|
764
801
|
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
765
802
|
result[key] = envValue;
|
|
766
803
|
}
|
|
@@ -905,7 +942,7 @@ function findDefaultContractPath(cwd) {
|
|
|
905
942
|
async function loadValidationContract(options) {
|
|
906
943
|
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
907
944
|
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
908
|
-
const resolvedContractPath = contractPath
|
|
945
|
+
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path7.resolve(cwd, contractPath);
|
|
909
946
|
if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
|
|
910
947
|
const moduleUrl = pathToFileURL2(resolvedContractPath).href;
|
|
911
948
|
const moduleValue = await import(moduleUrl);
|
|
@@ -948,7 +985,14 @@ async function loadPluginFromPath(pluginPath, cwd) {
|
|
|
948
985
|
const moduleValue = await import(pathToFileURL3(resolvedPath).href);
|
|
949
986
|
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
950
987
|
if (isPlugin(candidate)) return candidate;
|
|
951
|
-
throw new Error(
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Invalid plugin at ${resolvedPath}.
|
|
990
|
+
Expected a default export matching:
|
|
991
|
+
{ name: string,
|
|
992
|
+
transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
|
|
993
|
+
transformReport?(report: ValidationReport): ValidationReport,
|
|
994
|
+
transformContract?(contract: EnvContract): EnvContract }`
|
|
995
|
+
);
|
|
952
996
|
}
|
|
953
997
|
async function loadPlugins(options) {
|
|
954
998
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -993,8 +1037,8 @@ function applyReportPlugins(report, plugins) {
|
|
|
993
1037
|
}
|
|
994
1038
|
|
|
995
1039
|
// src/validation/engine.ts
|
|
996
|
-
var EMAIL_RE = /^[^@\s]+@[^@\s]
|
|
997
|
-
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[
|
|
1040
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
|
|
1041
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
998
1042
|
function detectReceivedType(value) {
|
|
999
1043
|
const normalized = value.trim();
|
|
1000
1044
|
if (normalized.length === 0) return "unknown";
|
|
@@ -1014,65 +1058,72 @@ function detectReceivedType(value) {
|
|
|
1014
1058
|
}
|
|
1015
1059
|
return "string";
|
|
1016
1060
|
}
|
|
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 };
|
|
1061
|
+
function validateNumber(expected, normalized, receivedType) {
|
|
1062
|
+
const parsed = Number(normalized);
|
|
1063
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1064
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1034
1065
|
}
|
|
1035
|
-
if (expected.
|
|
1036
|
-
|
|
1037
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1038
|
-
}
|
|
1039
|
-
return { isValid: true, receivedType };
|
|
1066
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1067
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1040
1068
|
}
|
|
1041
|
-
if (expected.
|
|
1042
|
-
|
|
1043
|
-
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1044
|
-
}
|
|
1045
|
-
return { isValid: true, receivedType };
|
|
1069
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1070
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1046
1071
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
return { isValid: true, receivedType };
|
|
1053
|
-
} catch {
|
|
1054
|
-
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1055
|
-
}
|
|
1072
|
+
return { isValid: true, receivedType };
|
|
1073
|
+
}
|
|
1074
|
+
function validateBoolean(normalized, receivedType) {
|
|
1075
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1076
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1056
1077
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1078
|
+
return { isValid: true, receivedType };
|
|
1079
|
+
}
|
|
1080
|
+
function validateEnum(expected, normalized, receivedType) {
|
|
1081
|
+
if (!expected.values.includes(normalized)) {
|
|
1082
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1083
|
+
}
|
|
1084
|
+
return { isValid: true, receivedType };
|
|
1085
|
+
}
|
|
1086
|
+
function validateUrl(normalized, receivedType) {
|
|
1087
|
+
try {
|
|
1088
|
+
const value = new URL(normalized);
|
|
1089
|
+
if (value.protocol.length === 0)
|
|
1059
1090
|
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1060
1091
|
return { isValid: true, receivedType };
|
|
1092
|
+
} catch {
|
|
1093
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1061
1094
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1095
|
+
}
|
|
1096
|
+
function validateEmail(normalized, receivedType) {
|
|
1097
|
+
if (!EMAIL_RE.test(normalized))
|
|
1098
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1099
|
+
return { isValid: true, receivedType };
|
|
1100
|
+
}
|
|
1101
|
+
function validateJson(normalized, receivedType) {
|
|
1102
|
+
try {
|
|
1103
|
+
const parsed = JSON.parse(normalized);
|
|
1104
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1105
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1106
|
+
} catch {
|
|
1107
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1070
1108
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1109
|
+
}
|
|
1110
|
+
function validateSemver(normalized, receivedType) {
|
|
1111
|
+
if (!SEMVER_RE.test(normalized))
|
|
1112
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1113
|
+
return { isValid: true, receivedType };
|
|
1114
|
+
}
|
|
1115
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1116
|
+
const normalized = rawValue.trim();
|
|
1117
|
+
const receivedType = detectReceivedType(normalized);
|
|
1118
|
+
if (expected.type === "unknown" || expected.type === "string")
|
|
1074
1119
|
return { isValid: true, receivedType };
|
|
1075
|
-
|
|
1120
|
+
if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
|
|
1121
|
+
if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
|
|
1122
|
+
if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
|
|
1123
|
+
if (expected.type === "url") return validateUrl(normalized, receivedType);
|
|
1124
|
+
if (expected.type === "email") return validateEmail(normalized, receivedType);
|
|
1125
|
+
if (expected.type === "json") return validateJson(normalized, receivedType);
|
|
1126
|
+
if (expected.type === "semver") return validateSemver(normalized, receivedType);
|
|
1076
1127
|
return { isValid: true, receivedType };
|
|
1077
1128
|
}
|
|
1078
1129
|
function toIssueCode(issueType) {
|
|
@@ -1144,57 +1195,62 @@ function buildReport(env, issues, recommendations) {
|
|
|
1144
1195
|
function isClientSecret(variable, key) {
|
|
1145
1196
|
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1146
1197
|
}
|
|
1198
|
+
function checkContractVariable(key, variable, context) {
|
|
1199
|
+
const { options, issues } = context;
|
|
1200
|
+
const value = options.values[key];
|
|
1201
|
+
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1202
|
+
if (variable.required && !hasValue) {
|
|
1203
|
+
issues.push(
|
|
1204
|
+
createIssue({
|
|
1205
|
+
type: "missing",
|
|
1206
|
+
severity: "error",
|
|
1207
|
+
key,
|
|
1208
|
+
environment: options.environment,
|
|
1209
|
+
message: `Required variable ${key} is missing.`,
|
|
1210
|
+
debugValues: options.debugValues,
|
|
1211
|
+
expected: variable.expected
|
|
1212
|
+
})
|
|
1213
|
+
);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (!hasValue) return;
|
|
1217
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1218
|
+
if (!validation.isValid) {
|
|
1219
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
|
|
1220
|
+
issues.push(
|
|
1221
|
+
createIssue({
|
|
1222
|
+
type: validation.issueType,
|
|
1223
|
+
severity: "error",
|
|
1224
|
+
key,
|
|
1225
|
+
environment: options.environment,
|
|
1226
|
+
message,
|
|
1227
|
+
value,
|
|
1228
|
+
debugValues: options.debugValues,
|
|
1229
|
+
expected: variable.expected,
|
|
1230
|
+
receivedType: validation.receivedType
|
|
1231
|
+
})
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (isClientSecret(variable, key)) {
|
|
1235
|
+
issues.push(
|
|
1236
|
+
createIssue({
|
|
1237
|
+
type: "secret_exposed",
|
|
1238
|
+
severity: "error",
|
|
1239
|
+
key,
|
|
1240
|
+
environment: options.environment,
|
|
1241
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1242
|
+
value,
|
|
1243
|
+
debugValues: options.debugValues,
|
|
1244
|
+
expected: variable.expected
|
|
1245
|
+
})
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1147
1249
|
function validateAgainstContract(options) {
|
|
1148
1250
|
const issues = [];
|
|
1149
1251
|
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1150
1252
|
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
|
-
}
|
|
1253
|
+
checkContractVariable(key, variable, { options, issues });
|
|
1198
1254
|
}
|
|
1199
1255
|
for (const [key, value] of Object.entries(options.values)) {
|
|
1200
1256
|
if (contractKeys.has(key)) continue;
|
|
@@ -1222,6 +1278,97 @@ function collectUnionKeys(contract, sources) {
|
|
|
1222
1278
|
}
|
|
1223
1279
|
return union;
|
|
1224
1280
|
}
|
|
1281
|
+
function diffMissingEntries(key, missing, context) {
|
|
1282
|
+
const { variable, options, issues } = context;
|
|
1283
|
+
for (const entry of missing) {
|
|
1284
|
+
issues.push(
|
|
1285
|
+
createIssue({
|
|
1286
|
+
type: "missing",
|
|
1287
|
+
severity: "error",
|
|
1288
|
+
key,
|
|
1289
|
+
environment: entry.sourceName,
|
|
1290
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1291
|
+
debugValues: options.debugValues,
|
|
1292
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1293
|
+
})
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function diffTypeConflicts(key, present, context) {
|
|
1298
|
+
const { variable, options, issues } = context;
|
|
1299
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1300
|
+
for (const entry of present) {
|
|
1301
|
+
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1302
|
+
}
|
|
1303
|
+
if (new Set(typeBySource.values()).size <= 1) return;
|
|
1304
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1305
|
+
issues.push(
|
|
1306
|
+
createIssue({
|
|
1307
|
+
type: "conflict",
|
|
1308
|
+
severity: "error",
|
|
1309
|
+
key,
|
|
1310
|
+
environment: sourceName,
|
|
1311
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1312
|
+
debugValues: options.debugValues,
|
|
1313
|
+
receivedType: detectedType,
|
|
1314
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1315
|
+
value: options.sources[sourceName]?.[key]
|
|
1316
|
+
},
|
|
1317
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function diffPresentEntry(key, entry, context) {
|
|
1323
|
+
const { variable, options, issues } = context;
|
|
1324
|
+
if (entry.value === void 0) return;
|
|
1325
|
+
if (variable === void 0) {
|
|
1326
|
+
const severity = options.strict ? "error" : "warning";
|
|
1327
|
+
issues.push(
|
|
1328
|
+
createIssue({
|
|
1329
|
+
type: "extra",
|
|
1330
|
+
severity,
|
|
1331
|
+
key,
|
|
1332
|
+
environment: entry.sourceName,
|
|
1333
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1334
|
+
value: entry.value,
|
|
1335
|
+
debugValues: options.debugValues
|
|
1336
|
+
})
|
|
1337
|
+
);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1341
|
+
if (!validation.isValid) {
|
|
1342
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
|
|
1343
|
+
issues.push(
|
|
1344
|
+
createIssue({
|
|
1345
|
+
type: validation.issueType,
|
|
1346
|
+
severity: "error",
|
|
1347
|
+
key,
|
|
1348
|
+
environment: entry.sourceName,
|
|
1349
|
+
message,
|
|
1350
|
+
value: entry.value,
|
|
1351
|
+
debugValues: options.debugValues,
|
|
1352
|
+
expected: variable.expected,
|
|
1353
|
+
receivedType: validation.receivedType
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (isClientSecret(variable, key)) {
|
|
1358
|
+
issues.push(
|
|
1359
|
+
createIssue({
|
|
1360
|
+
type: "secret_exposed",
|
|
1361
|
+
severity: "error",
|
|
1362
|
+
key,
|
|
1363
|
+
environment: entry.sourceName,
|
|
1364
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1365
|
+
value: entry.value,
|
|
1366
|
+
debugValues: options.debugValues,
|
|
1367
|
+
expected: variable.expected
|
|
1368
|
+
})
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1225
1372
|
function diffEnvironmentSources(options) {
|
|
1226
1373
|
const issues = [];
|
|
1227
1374
|
const sourceNames = Object.keys(options.sources);
|
|
@@ -1254,92 +1401,13 @@ function diffEnvironmentSources(options) {
|
|
|
1254
1401
|
}
|
|
1255
1402
|
continue;
|
|
1256
1403
|
}
|
|
1404
|
+
const ctx = { variable, options, issues };
|
|
1257
1405
|
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
|
-
}
|
|
1406
|
+
diffMissingEntries(key, missing, ctx);
|
|
1271
1407
|
}
|
|
1272
|
-
|
|
1408
|
+
diffTypeConflicts(key, present, ctx);
|
|
1273
1409
|
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
|
-
}
|
|
1410
|
+
diffPresentEntry(key, entry, ctx);
|
|
1343
1411
|
}
|
|
1344
1412
|
}
|
|
1345
1413
|
return buildReport("diff", issues);
|
|
@@ -1392,7 +1460,7 @@ function parseEnvSourceContent(content) {
|
|
|
1392
1460
|
for (const line of lines) {
|
|
1393
1461
|
const trimmed = line.trim();
|
|
1394
1462
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1395
|
-
const match = /^(?:export\s+)?([A-Za-z_]
|
|
1463
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
|
|
1396
1464
|
if (match === null) continue;
|
|
1397
1465
|
const key = match[1] ?? "";
|
|
1398
1466
|
const rawValue = match[2] ?? "";
|
|
@@ -1405,11 +1473,11 @@ async function loadEnvSource(options) {
|
|
|
1405
1473
|
try {
|
|
1406
1474
|
const content = await readFile3(resolvedPath, "utf8");
|
|
1407
1475
|
return parseEnvSourceContent(content);
|
|
1408
|
-
} catch (
|
|
1409
|
-
if (options.allowMissing === true &&
|
|
1476
|
+
} catch (error_) {
|
|
1477
|
+
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1410
1478
|
return {};
|
|
1411
1479
|
}
|
|
1412
|
-
throw
|
|
1480
|
+
throw error_;
|
|
1413
1481
|
}
|
|
1414
1482
|
}
|
|
1415
1483
|
|
|
@@ -1423,8 +1491,8 @@ function toJsonString(report, mode) {
|
|
|
1423
1491
|
`;
|
|
1424
1492
|
}
|
|
1425
1493
|
function formatIssue(issue) {
|
|
1426
|
-
const expected = issue.expected
|
|
1427
|
-
const received = issue.receivedType
|
|
1494
|
+
const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
|
|
1495
|
+
const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
|
|
1428
1496
|
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1429
1497
|
}
|
|
1430
1498
|
function formatHumanReport(report) {
|
|
@@ -1433,15 +1501,13 @@ function formatHumanReport(report) {
|
|
|
1433
1501
|
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1434
1502
|
);
|
|
1435
1503
|
if (report.issues.length > 0) {
|
|
1436
|
-
lines.push("");
|
|
1437
|
-
lines.push("Issues:");
|
|
1504
|
+
lines.push("", "Issues:");
|
|
1438
1505
|
for (const issue of report.issues) {
|
|
1439
1506
|
lines.push(`- ${formatIssue(issue)}`);
|
|
1440
1507
|
}
|
|
1441
1508
|
}
|
|
1442
1509
|
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1443
|
-
lines.push("");
|
|
1444
|
-
lines.push("Recommendations:");
|
|
1510
|
+
lines.push("", "Recommendations:");
|
|
1445
1511
|
for (const recommendation of report.recommendations) {
|
|
1446
1512
|
lines.push(`- ${recommendation}`);
|
|
1447
1513
|
}
|
|
@@ -1486,7 +1552,11 @@ var HELP_TEXT = {
|
|
|
1486
1552
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1487
1553
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1488
1554
|
" -c, --config <path> Config file path",
|
|
1489
|
-
" -h, --help Show this help"
|
|
1555
|
+
" -h, --help Show this help",
|
|
1556
|
+
"",
|
|
1557
|
+
"Exit codes:",
|
|
1558
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1559
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1490
1560
|
].join("\n"),
|
|
1491
1561
|
diff: [
|
|
1492
1562
|
"Usage: env-typegen diff [options]",
|
|
@@ -1505,7 +1575,11 @@ var HELP_TEXT = {
|
|
|
1505
1575
|
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1506
1576
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1507
1577
|
" -c, --config <path> Config file path",
|
|
1508
|
-
" -h, --help Show this help"
|
|
1578
|
+
" -h, --help Show this help",
|
|
1579
|
+
"",
|
|
1580
|
+
"Exit codes:",
|
|
1581
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1582
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1509
1583
|
].join("\n"),
|
|
1510
1584
|
doctor: [
|
|
1511
1585
|
"Usage: env-typegen doctor [options]",
|
|
@@ -1525,7 +1599,11 @@ var HELP_TEXT = {
|
|
|
1525
1599
|
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1526
1600
|
" --plugin <path> Plugin module path (repeatable)",
|
|
1527
1601
|
" -c, --config <path> Config file path",
|
|
1528
|
-
" -h, --help Show this help"
|
|
1602
|
+
" -h, --help Show this help",
|
|
1603
|
+
"",
|
|
1604
|
+
"Exit codes:",
|
|
1605
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1606
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1529
1607
|
].join("\n")
|
|
1530
1608
|
};
|
|
1531
1609
|
function resolveConfigRelative(value, configDir) {
|
|
@@ -1612,7 +1690,10 @@ function parseValidationArgs(argv) {
|
|
|
1612
1690
|
}
|
|
1613
1691
|
});
|
|
1614
1692
|
const castValues = values;
|
|
1615
|
-
|
|
1693
|
+
let jsonMode = "off";
|
|
1694
|
+
if (castValues.json === true) {
|
|
1695
|
+
jsonMode = assignedMode === "off" ? "compact" : assignedMode;
|
|
1696
|
+
}
|
|
1616
1697
|
return { values: castValues, jsonMode };
|
|
1617
1698
|
}
|
|
1618
1699
|
function resolveStrict(values, fileConfig) {
|
|
@@ -1685,12 +1766,12 @@ async function runCheckCommand(args) {
|
|
|
1685
1766
|
const provider = context.cloudProvider;
|
|
1686
1767
|
let environment = args.values.env?.[0] ?? ".env";
|
|
1687
1768
|
let sourceValues;
|
|
1688
|
-
if (provider
|
|
1769
|
+
if (provider === void 0) {
|
|
1770
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1771
|
+
} else {
|
|
1689
1772
|
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1690
1773
|
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1691
1774
|
environment = `cloud:${provider}`;
|
|
1692
|
-
} else {
|
|
1693
|
-
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1694
1775
|
}
|
|
1695
1776
|
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1696
1777
|
const report = applyReportPlugins(
|
|
@@ -1822,8 +1903,8 @@ async function runValidationCommand(params) {
|
|
|
1822
1903
|
}
|
|
1823
1904
|
|
|
1824
1905
|
// src/watch.ts
|
|
1825
|
-
import path12 from "path";
|
|
1826
1906
|
import { watch } from "chokidar";
|
|
1907
|
+
import path12 from "path";
|
|
1827
1908
|
function debounce(fn, delay) {
|
|
1828
1909
|
let timer;
|
|
1829
1910
|
return (...args) => {
|
|
@@ -1858,6 +1939,9 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
1858
1939
|
if (reloaded.inferenceRules !== void 0) {
|
|
1859
1940
|
runOptions.inferenceRules = reloaded.inferenceRules;
|
|
1860
1941
|
}
|
|
1942
|
+
if (reloaded.output !== void 0) {
|
|
1943
|
+
runOptions.output = reloaded.output;
|
|
1944
|
+
}
|
|
1861
1945
|
}
|
|
1862
1946
|
void runGenerate(runOptions).catch((err) => {
|
|
1863
1947
|
const message = err instanceof Error ? err.message : JSON.stringify(err);
|
|
@@ -1875,7 +1959,7 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
1875
1959
|
const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
|
|
1876
1960
|
const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
|
|
1877
1961
|
for (const event of ["add", "change"]) {
|
|
1878
|
-
configWatcher.on(event, (eventPath) =>
|
|
1962
|
+
configWatcher.on(event, (eventPath) => handleConfigChange(eventPath));
|
|
1879
1963
|
}
|
|
1880
1964
|
process.on("SIGINT", () => {
|
|
1881
1965
|
void Promise.all([inputWatcher.close(), configWatcher.close()]).then(() => {
|
|
@@ -1899,7 +1983,9 @@ var HELP_TEXT2 = [
|
|
|
1899
1983
|
"",
|
|
1900
1984
|
"Options:",
|
|
1901
1985
|
" -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
|
|
1902
|
-
" -o, --output <path> Output
|
|
1986
|
+
" -o, --output <path> Output base path (default: env.generated.ts).",
|
|
1987
|
+
" With multiple generators, suffixes are appended:",
|
|
1988
|
+
" env.generated.typescript.ts, .zod.ts, .t3.ts, .declaration.d.ts",
|
|
1903
1989
|
" -f, --format <name> Generator format: ts|zod|t3|declaration",
|
|
1904
1990
|
" May be specified multiple times.",
|
|
1905
1991
|
" -g, --generator <name> Backward-compatible alias for --format",
|
|
@@ -1910,7 +1996,16 @@ var HELP_TEXT2 = [
|
|
|
1910
1996
|
" -w, --watch Watch for changes and regenerate",
|
|
1911
1997
|
" -c, --config <path> Path to config file",
|
|
1912
1998
|
" -v, --version Print version",
|
|
1913
|
-
" -h, --help Show this help"
|
|
1999
|
+
" -h, --help Show this help",
|
|
2000
|
+
"",
|
|
2001
|
+
"Config file:",
|
|
2002
|
+
" Auto-discovered in order: env-typegen.config.mjs \u2192 .js \u2192 .ts (in cwd)",
|
|
2003
|
+
" CLI flags always override config file values.",
|
|
2004
|
+
" Use defineConfig() from @xlameiro/env-typegen for IDE autocompletion.",
|
|
2005
|
+
"",
|
|
2006
|
+
"Exit codes:",
|
|
2007
|
+
" 0 Success \u2014 files generated without errors",
|
|
2008
|
+
" 1 Error \u2014 invalid flags or generation failed"
|
|
1914
2009
|
].join("\n");
|
|
1915
2010
|
var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
|
|
1916
2011
|
var FORMAT_TO_GENERATOR = {
|