@ayronforge/envil 0.6.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/generate-env-ts.d.ts +1 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/types.d.ts +4 -18
- package/dist/cli.js +327 -347
- package/dist/cli.js.map +10 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +585 -80
- package/dist/index.js.map +10 -6
- package/dist/introspect.d.ts +25 -0
- package/dist/schemas.d.ts +21 -16
- package/package.json +1 -1
- package/dist/cli/generate-example.d.ts +0 -2
- package/dist/cli/manifest-codec.d.ts +0 -3
package/dist/index.js
CHANGED
|
@@ -197,6 +197,11 @@ function buildEnv(opts) {
|
|
|
197
197
|
return env;
|
|
198
198
|
}
|
|
199
199
|
function createEnv(opts) {
|
|
200
|
+
if (process.env.ENVIL_INTROSPECT_ONLY === "1") {
|
|
201
|
+
return new Proxy({}, { get: () => {
|
|
202
|
+
return;
|
|
203
|
+
} });
|
|
204
|
+
}
|
|
200
205
|
if (opts.resolvers?.length) {
|
|
201
206
|
const shouldAutoRedact = opts.autoRedactResolver !== false;
|
|
202
207
|
return Effect.all(opts.resolvers, { concurrency: "unbounded" }).pipe(Effect.map((results) => {
|
|
@@ -225,6 +230,577 @@ function createEnv(opts) {
|
|
|
225
230
|
}
|
|
226
231
|
return buildEnv(opts);
|
|
227
232
|
}
|
|
233
|
+
// src/introspect.ts
|
|
234
|
+
import { Option, Redacted as Redacted2, Schema as Schema4, SchemaAST } from "effect";
|
|
235
|
+
|
|
236
|
+
// src/cli/dotenv-codec.ts
|
|
237
|
+
import { Schema as Schema2 } from "effect";
|
|
238
|
+
|
|
239
|
+
// src/cli/literals.ts
|
|
240
|
+
function toCodeLiteral(value) {
|
|
241
|
+
if (typeof value === "string")
|
|
242
|
+
return JSON.stringify(value);
|
|
243
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
244
|
+
return String(value);
|
|
245
|
+
if (value === null)
|
|
246
|
+
return "null";
|
|
247
|
+
if (value === undefined)
|
|
248
|
+
return "undefined";
|
|
249
|
+
return JSON.stringify(value);
|
|
250
|
+
}
|
|
251
|
+
function toEnvValueLiteral(value) {
|
|
252
|
+
if (typeof value === "string")
|
|
253
|
+
return value;
|
|
254
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
255
|
+
return String(value);
|
|
256
|
+
if (value === null || value === undefined)
|
|
257
|
+
return "";
|
|
258
|
+
return JSON.stringify(value);
|
|
259
|
+
}
|
|
260
|
+
function parseBooleanDirective(value, defaultValue) {
|
|
261
|
+
if (value === undefined || value.length === 0) {
|
|
262
|
+
return defaultValue;
|
|
263
|
+
}
|
|
264
|
+
const normalized = value.trim().toLowerCase();
|
|
265
|
+
if (normalized === "true")
|
|
266
|
+
return true;
|
|
267
|
+
if (normalized === "false")
|
|
268
|
+
return false;
|
|
269
|
+
throw new Error(`Invalid boolean directive value "${value}"`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/cli/types.ts
|
|
273
|
+
var BUCKETS = ["server", "client", "shared"];
|
|
274
|
+
var SCHEMA_KINDS = [
|
|
275
|
+
"requiredString",
|
|
276
|
+
"boolean",
|
|
277
|
+
"integer",
|
|
278
|
+
"number",
|
|
279
|
+
"port",
|
|
280
|
+
"url",
|
|
281
|
+
"postgresUrl",
|
|
282
|
+
"redisUrl",
|
|
283
|
+
"mongoUrl",
|
|
284
|
+
"mysqlUrl",
|
|
285
|
+
"commaSeparated",
|
|
286
|
+
"commaSeparatedNumbers",
|
|
287
|
+
"commaSeparatedUrls",
|
|
288
|
+
"json",
|
|
289
|
+
"stringEnum"
|
|
290
|
+
];
|
|
291
|
+
var FRAMEWORKS = ["nextjs", "vite", "expo", "nuxt", "sveltekit", "astro"];
|
|
292
|
+
|
|
293
|
+
// src/cli/dotenv-codec.ts
|
|
294
|
+
var SENTINEL_DIRECTIVE_PREFIX = "@";
|
|
295
|
+
var KEY_ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
296
|
+
var DotenvCodec = Schema2.transform(Schema2.String, Schema2.Unknown, {
|
|
297
|
+
decode: (text) => parseDotenvText(String(text)),
|
|
298
|
+
encode: (value) => stringifyDotenvDocument(value)
|
|
299
|
+
});
|
|
300
|
+
function decodeDotenvText(text) {
|
|
301
|
+
return Schema2.decodeUnknownSync(DotenvCodec)(text);
|
|
302
|
+
}
|
|
303
|
+
function encodeDotenvText(document) {
|
|
304
|
+
return Schema2.encodeSync(DotenvCodec)(document);
|
|
305
|
+
}
|
|
306
|
+
function parseDotenvText(text) {
|
|
307
|
+
const lines = text.split(/\r?\n/);
|
|
308
|
+
const entries = [];
|
|
309
|
+
const prefix = {};
|
|
310
|
+
let activeBucket;
|
|
311
|
+
let pendingDirectives = {};
|
|
312
|
+
for (const [index, line] of lines.entries()) {
|
|
313
|
+
const lineNumber = index + 1;
|
|
314
|
+
const trimmed = line.trim();
|
|
315
|
+
if (trimmed.length === 0) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (trimmed.startsWith("#")) {
|
|
319
|
+
const comment = trimmed.slice(1).trim();
|
|
320
|
+
if (comment.startsWith(SENTINEL_DIRECTIVE_PREFIX)) {
|
|
321
|
+
const directiveResult = parseDirectiveGroup(comment, lineNumber, pendingDirectives);
|
|
322
|
+
if (directiveResult.sectionBucket) {
|
|
323
|
+
activeBucket = directiveResult.sectionBucket;
|
|
324
|
+
if (directiveResult.sectionPrefix) {
|
|
325
|
+
prefix[directiveResult.sectionBucket] = directiveResult.sectionPrefix;
|
|
326
|
+
}
|
|
327
|
+
pendingDirectives = {};
|
|
328
|
+
} else {
|
|
329
|
+
pendingDirectives = directiveResult.directives;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const assignmentMatch = KEY_ASSIGNMENT_PATTERN.exec(trimmed);
|
|
335
|
+
if (!assignmentMatch) {
|
|
336
|
+
throw new Error(`Malformed assignment at line ${lineNumber}: "${line}"`);
|
|
337
|
+
}
|
|
338
|
+
const key = assignmentMatch[1];
|
|
339
|
+
const rightHandSide = assignmentMatch[2];
|
|
340
|
+
const split = splitValueAndInlineComment(rightHandSide);
|
|
341
|
+
const value = normalizeAssignedValue(split.value.trim());
|
|
342
|
+
let directives = { ...pendingDirectives };
|
|
343
|
+
if (split.comment) {
|
|
344
|
+
const inlineComment = split.comment.trim();
|
|
345
|
+
if (inlineComment.startsWith(SENTINEL_DIRECTIVE_PREFIX)) {
|
|
346
|
+
const parsedInline = parseDirectiveGroup(inlineComment, lineNumber, directives);
|
|
347
|
+
if (parsedInline.sectionBucket) {
|
|
348
|
+
throw new Error(`Section directives are not allowed inline at line ${lineNumber}`);
|
|
349
|
+
}
|
|
350
|
+
directives = parsedInline.directives;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
entries.push({
|
|
354
|
+
key,
|
|
355
|
+
value,
|
|
356
|
+
line: lineNumber,
|
|
357
|
+
sectionBucket: activeBucket,
|
|
358
|
+
directives
|
|
359
|
+
});
|
|
360
|
+
pendingDirectives = {};
|
|
361
|
+
}
|
|
362
|
+
const hasPrefix = Object.values(prefix).some((v) => v && v.length > 0);
|
|
363
|
+
return { entries, ...hasPrefix ? { prefix } : {} };
|
|
364
|
+
}
|
|
365
|
+
function stringifyDotenvDocument(document) {
|
|
366
|
+
if (!document || !Array.isArray(document.entries)) {
|
|
367
|
+
throw new Error("Invalid dotenv document: expected an entries array");
|
|
368
|
+
}
|
|
369
|
+
const grouped = {
|
|
370
|
+
server: [],
|
|
371
|
+
client: [],
|
|
372
|
+
shared: []
|
|
373
|
+
};
|
|
374
|
+
const sortedEntries = [...document.entries].sort((left, right) => {
|
|
375
|
+
if (left.line !== right.line)
|
|
376
|
+
return left.line - right.line;
|
|
377
|
+
return left.key.localeCompare(right.key);
|
|
378
|
+
});
|
|
379
|
+
for (const entry of sortedEntries) {
|
|
380
|
+
const bucket = entry.directives.bucket ?? entry.sectionBucket ?? "server";
|
|
381
|
+
grouped[bucket].push(entry);
|
|
382
|
+
}
|
|
383
|
+
const lines = [];
|
|
384
|
+
for (const bucket of BUCKETS) {
|
|
385
|
+
const pfx = document.prefix?.[bucket];
|
|
386
|
+
lines.push(pfx && pfx.length > 0 ? `# @${bucket} ${pfx}` : `# @${bucket}`);
|
|
387
|
+
lines.push("");
|
|
388
|
+
for (const entry of grouped[bucket]) {
|
|
389
|
+
const optional = entry.directives.optional ?? false;
|
|
390
|
+
const redacted = entry.directives.redacted ?? false;
|
|
391
|
+
const noDefault = entry.directives.hasDefault === false;
|
|
392
|
+
if (entry.directives.type === "stringEnum" && entry.directives.stringEnumValues) {
|
|
393
|
+
lines.push(`# @type enum ${entry.directives.stringEnumValues.join(",")}`);
|
|
394
|
+
}
|
|
395
|
+
if (optional)
|
|
396
|
+
lines.push("# @optional");
|
|
397
|
+
if (noDefault)
|
|
398
|
+
lines.push("# @no-default");
|
|
399
|
+
if (redacted)
|
|
400
|
+
lines.push("# @redacted");
|
|
401
|
+
lines.push(`${entry.key}=${serializeEnvValue(entry.value)}`);
|
|
402
|
+
lines.push("");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
406
|
+
lines.pop();
|
|
407
|
+
}
|
|
408
|
+
return `${lines.join(`
|
|
409
|
+
`)}
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
function splitValueAndInlineComment(value) {
|
|
413
|
+
let inSingleQuote = false;
|
|
414
|
+
let inDoubleQuote = false;
|
|
415
|
+
let escaped = false;
|
|
416
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
417
|
+
const char = value[index];
|
|
418
|
+
if (escaped) {
|
|
419
|
+
escaped = false;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (char === "\\") {
|
|
423
|
+
escaped = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (char === "'" && !inDoubleQuote) {
|
|
427
|
+
inSingleQuote = !inSingleQuote;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (char === '"' && !inSingleQuote) {
|
|
431
|
+
inDoubleQuote = !inDoubleQuote;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (!inSingleQuote && !inDoubleQuote && char === "#") {
|
|
435
|
+
return {
|
|
436
|
+
value: value.slice(0, index).trimEnd(),
|
|
437
|
+
comment: value.slice(index + 1)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { value };
|
|
442
|
+
}
|
|
443
|
+
function parseDirectiveGroup(directiveText, lineNumber, base) {
|
|
444
|
+
const directives = { ...base };
|
|
445
|
+
const tokens = directiveText.split(/\s+(?=@)/g).map((token) => token.trim()).filter(Boolean);
|
|
446
|
+
let sectionBucket;
|
|
447
|
+
let sectionPrefix;
|
|
448
|
+
for (const token of tokens) {
|
|
449
|
+
const parsed = parseDirectiveToken(token, lineNumber);
|
|
450
|
+
if ("type" in parsed)
|
|
451
|
+
directives.type = parsed.type;
|
|
452
|
+
if ("optional" in parsed)
|
|
453
|
+
directives.optional = parsed.optional;
|
|
454
|
+
if ("hasDefault" in parsed)
|
|
455
|
+
directives.hasDefault = parsed.hasDefault;
|
|
456
|
+
if ("redacted" in parsed)
|
|
457
|
+
directives.redacted = parsed.redacted;
|
|
458
|
+
if ("bucket" in parsed)
|
|
459
|
+
directives.bucket = parsed.bucket;
|
|
460
|
+
if ("stringEnumValues" in parsed)
|
|
461
|
+
directives.stringEnumValues = parsed.stringEnumValues;
|
|
462
|
+
if ("sectionBucket" in parsed)
|
|
463
|
+
sectionBucket = parsed.sectionBucket;
|
|
464
|
+
if ("sectionPrefix" in parsed)
|
|
465
|
+
sectionPrefix = parsed.sectionPrefix;
|
|
466
|
+
}
|
|
467
|
+
return { directives, sectionBucket, sectionPrefix };
|
|
468
|
+
}
|
|
469
|
+
function parseDirectiveToken(token, lineNumber) {
|
|
470
|
+
if (!token.startsWith("@")) {
|
|
471
|
+
throw new Error(`Malformed directive at line ${lineNumber}: "${token}"`);
|
|
472
|
+
}
|
|
473
|
+
const spaceIndex = token.indexOf(" ");
|
|
474
|
+
const name = (spaceIndex === -1 ? token.slice(1) : token.slice(1, spaceIndex)).trim();
|
|
475
|
+
const value = (spaceIndex === -1 ? "" : token.slice(spaceIndex + 1)).trim();
|
|
476
|
+
if (name === "server" || name === "client" || name === "shared") {
|
|
477
|
+
return value.length > 0 ? { sectionBucket: name, sectionPrefix: value } : { sectionBucket: name };
|
|
478
|
+
}
|
|
479
|
+
if (name === "type") {
|
|
480
|
+
if (value.length === 0) {
|
|
481
|
+
throw new Error(`Directive "@type" requires a value at line ${lineNumber}`);
|
|
482
|
+
}
|
|
483
|
+
if (value === "enum" || value.startsWith("enum ")) {
|
|
484
|
+
const raw = value.slice(4).trim();
|
|
485
|
+
if (raw.length === 0) {
|
|
486
|
+
throw new Error(`Directive "@type enum" requires comma-separated values at line ${lineNumber}`);
|
|
487
|
+
}
|
|
488
|
+
const stringEnumValues = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
489
|
+
if (stringEnumValues.length === 0) {
|
|
490
|
+
throw new Error(`Directive "@type enum" requires at least one value at line ${lineNumber}`);
|
|
491
|
+
}
|
|
492
|
+
return { type: "stringEnum", stringEnumValues };
|
|
493
|
+
}
|
|
494
|
+
return { type: parseSchemaKind(value, lineNumber) };
|
|
495
|
+
}
|
|
496
|
+
if (name === "optional") {
|
|
497
|
+
return { optional: parseBooleanDirective(value || undefined, true) };
|
|
498
|
+
}
|
|
499
|
+
if (name === "no-default") {
|
|
500
|
+
return { hasDefault: false };
|
|
501
|
+
}
|
|
502
|
+
if (name === "redacted") {
|
|
503
|
+
return { redacted: parseBooleanDirective(value || undefined, true) };
|
|
504
|
+
}
|
|
505
|
+
if (name === "bucket") {
|
|
506
|
+
if (value.length === 0) {
|
|
507
|
+
throw new Error(`Directive "@bucket" requires a value at line ${lineNumber}`);
|
|
508
|
+
}
|
|
509
|
+
if (!isBucket(value)) {
|
|
510
|
+
throw new Error(`Invalid bucket "${value}" at line ${lineNumber}`);
|
|
511
|
+
}
|
|
512
|
+
return { bucket: value };
|
|
513
|
+
}
|
|
514
|
+
throw new Error(`Unknown directive "@${name}" at line ${lineNumber}`);
|
|
515
|
+
}
|
|
516
|
+
function parseSchemaKind(rawValue, lineNumber) {
|
|
517
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
518
|
+
const aliasMap = {
|
|
519
|
+
string: "requiredString",
|
|
520
|
+
requiredstring: "requiredString",
|
|
521
|
+
bool: "boolean",
|
|
522
|
+
boolean: "boolean",
|
|
523
|
+
int: "integer",
|
|
524
|
+
integer: "integer",
|
|
525
|
+
number: "number",
|
|
526
|
+
port: "port",
|
|
527
|
+
url: "url",
|
|
528
|
+
postgresurl: "postgresUrl",
|
|
529
|
+
redisurl: "redisUrl",
|
|
530
|
+
mongourl: "mongoUrl",
|
|
531
|
+
mysqlurl: "mysqlUrl",
|
|
532
|
+
commaseparated: "commaSeparated",
|
|
533
|
+
commaseparatednumbers: "commaSeparatedNumbers",
|
|
534
|
+
commaseparatedurls: "commaSeparatedUrls",
|
|
535
|
+
json: "json",
|
|
536
|
+
"json(schema.unknown)": "json"
|
|
537
|
+
};
|
|
538
|
+
const resolved = aliasMap[normalized];
|
|
539
|
+
if (!resolved || !SCHEMA_KINDS.includes(resolved)) {
|
|
540
|
+
throw new Error(`Invalid @type value "${rawValue}" at line ${lineNumber}`);
|
|
541
|
+
}
|
|
542
|
+
return resolved;
|
|
543
|
+
}
|
|
544
|
+
function normalizeAssignedValue(value) {
|
|
545
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
546
|
+
return value.slice(1, -1);
|
|
547
|
+
}
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
function serializeEnvValue(value) {
|
|
551
|
+
const literal = toEnvValueLiteral(value);
|
|
552
|
+
if (literal.length === 0)
|
|
553
|
+
return "";
|
|
554
|
+
if (/[\s#]/.test(literal)) {
|
|
555
|
+
return JSON.stringify(literal);
|
|
556
|
+
}
|
|
557
|
+
return literal;
|
|
558
|
+
}
|
|
559
|
+
function isBucket(value) {
|
|
560
|
+
return BUCKETS.includes(value);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/schemas.ts
|
|
564
|
+
import { Function, Schema as Schema3 } from "effect";
|
|
565
|
+
var DEFAULT_VALUE_ANNOTATION = Symbol.for("@ayronforge/envil/default-value");
|
|
566
|
+
var SCHEMA_KIND_ANNOTATION = Symbol.for("@ayronforge/envil/schema-kind");
|
|
567
|
+
var PLACEHOLDER_ANNOTATION = Symbol.for("@ayronforge/envil/placeholder");
|
|
568
|
+
var OPTIONAL_ANNOTATION = Symbol.for("@ayronforge/envil/optional");
|
|
569
|
+
var REDACTED_ANNOTATION = Symbol.for("@ayronforge/envil/redacted");
|
|
570
|
+
var STRING_ENUM_VALUES_ANNOTATION = Symbol.for("@ayronforge/envil/string-enum-values");
|
|
571
|
+
var withDefault = Function.dual(2, (schema, defaultValue) => {
|
|
572
|
+
const withDefaultSchema = Schema3.transform(Schema3.UndefinedOr(schema), Schema3.typeSchema(schema), {
|
|
573
|
+
decode: (value) => value ?? defaultValue,
|
|
574
|
+
encode: (value) => value
|
|
575
|
+
});
|
|
576
|
+
return withDefaultSchema.annotations({
|
|
577
|
+
[DEFAULT_VALUE_ANNOTATION]: defaultValue
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
var optional = (schema) => Schema3.UndefinedOr(schema).annotations({
|
|
581
|
+
[OPTIONAL_ANNOTATION]: true
|
|
582
|
+
});
|
|
583
|
+
var redacted = (schema) => Schema3.Redacted(schema).annotations({
|
|
584
|
+
[REDACTED_ANNOTATION]: true
|
|
585
|
+
});
|
|
586
|
+
var requiredString = Schema3.String.pipe(Schema3.minLength(1)).annotations({
|
|
587
|
+
identifier: "RequiredString",
|
|
588
|
+
[SCHEMA_KIND_ANNOTATION]: "requiredString",
|
|
589
|
+
[PLACEHOLDER_ANNOTATION]: "CHANGE_ME"
|
|
590
|
+
});
|
|
591
|
+
var boolean = Schema3.transform(Schema3.String.pipe(Schema3.filter((s) => ["true", "false", "1", "0"].includes(s.toLowerCase()), {
|
|
592
|
+
identifier: "BooleanString",
|
|
593
|
+
message: () => "Expected 'true', 'false', '1', or '0'"
|
|
594
|
+
})), Schema3.Boolean, {
|
|
595
|
+
decode: (s) => s.toLowerCase() === "true" || s === "1",
|
|
596
|
+
encode: (b) => b ? "true" : "false"
|
|
597
|
+
}).annotations({
|
|
598
|
+
[SCHEMA_KIND_ANNOTATION]: "boolean",
|
|
599
|
+
[PLACEHOLDER_ANNOTATION]: "true"
|
|
600
|
+
});
|
|
601
|
+
var integer = Schema3.NumberFromString.pipe(Schema3.int()).annotations({
|
|
602
|
+
identifier: "Integer",
|
|
603
|
+
[SCHEMA_KIND_ANNOTATION]: "integer",
|
|
604
|
+
[PLACEHOLDER_ANNOTATION]: "123"
|
|
605
|
+
});
|
|
606
|
+
var number = Schema3.NumberFromString.annotations({
|
|
607
|
+
identifier: "Number",
|
|
608
|
+
[SCHEMA_KIND_ANNOTATION]: "number",
|
|
609
|
+
[PLACEHOLDER_ANNOTATION]: "3.14"
|
|
610
|
+
});
|
|
611
|
+
var positiveNumber = Schema3.NumberFromString.pipe(Schema3.positive()).annotations({
|
|
612
|
+
identifier: "PositiveNumber"
|
|
613
|
+
});
|
|
614
|
+
var nonNegativeNumber = Schema3.NumberFromString.pipe(Schema3.nonNegative()).annotations({
|
|
615
|
+
identifier: "NonNegativeNumber"
|
|
616
|
+
});
|
|
617
|
+
var port = Schema3.NumberFromString.pipe(Schema3.int(), Schema3.between(1, 65535)).annotations({
|
|
618
|
+
identifier: "Port",
|
|
619
|
+
[SCHEMA_KIND_ANNOTATION]: "port",
|
|
620
|
+
[PLACEHOLDER_ANNOTATION]: "3000"
|
|
621
|
+
});
|
|
622
|
+
var url = Schema3.String.pipe(Schema3.filter((s) => {
|
|
623
|
+
try {
|
|
624
|
+
new URL(s);
|
|
625
|
+
return s.startsWith("http://") || s.startsWith("https://");
|
|
626
|
+
} catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}, { identifier: "Url", message: () => "Expected a valid HTTP or HTTPS URL" })).annotations({
|
|
630
|
+
[SCHEMA_KIND_ANNOTATION]: "url",
|
|
631
|
+
[PLACEHOLDER_ANNOTATION]: "https://example.com"
|
|
632
|
+
});
|
|
633
|
+
var postgresUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("postgres://") || s.startsWith("postgresql://"), {
|
|
634
|
+
identifier: "PostgresUrl",
|
|
635
|
+
message: () => "Expected a valid PostgreSQL connection URL"
|
|
636
|
+
}), Schema3.pattern(/^(postgres|postgresql):\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/)).annotations({
|
|
637
|
+
[SCHEMA_KIND_ANNOTATION]: "postgresUrl",
|
|
638
|
+
[PLACEHOLDER_ANNOTATION]: "postgres://user:pass@localhost:5432/app"
|
|
639
|
+
});
|
|
640
|
+
var redisUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("redis://") || s.startsWith("rediss://"), {
|
|
641
|
+
identifier: "RedisUrl",
|
|
642
|
+
message: () => "Expected a valid Redis connection URL"
|
|
643
|
+
}), Schema3.pattern(/^rediss?:\/\/(?:[^:]+:[^@]+@)?[^:]+(?::\d+)?(?:\/\d+)?$/)).annotations({
|
|
644
|
+
[SCHEMA_KIND_ANNOTATION]: "redisUrl",
|
|
645
|
+
[PLACEHOLDER_ANNOTATION]: "redis://localhost:6379"
|
|
646
|
+
});
|
|
647
|
+
var mongoUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("mongodb://") || s.startsWith("mongodb+srv://"), {
|
|
648
|
+
identifier: "MongoUrl",
|
|
649
|
+
message: () => "Expected a valid MongoDB connection URL"
|
|
650
|
+
}), Schema3.pattern(/^mongodb(\+srv)?:\/\/(?:[^:]+:[^@]+@)?[^/]+(?:\/[^?]*)?(?:\?.*)?$/)).annotations({
|
|
651
|
+
[SCHEMA_KIND_ANNOTATION]: "mongoUrl",
|
|
652
|
+
[PLACEHOLDER_ANNOTATION]: "mongodb://localhost:27017/app"
|
|
653
|
+
});
|
|
654
|
+
var mysqlUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("mysql://") || s.startsWith("mysqls://"), {
|
|
655
|
+
identifier: "MysqlUrl",
|
|
656
|
+
message: () => "Expected a valid MySQL connection URL"
|
|
657
|
+
}), Schema3.pattern(/^mysqls?:\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/)).annotations({
|
|
658
|
+
[SCHEMA_KIND_ANNOTATION]: "mysqlUrl",
|
|
659
|
+
[PLACEHOLDER_ANNOTATION]: "mysql://user:pass@localhost:3306/app"
|
|
660
|
+
});
|
|
661
|
+
var commaSeparated = Schema3.transform(Schema3.String, Schema3.mutable(Schema3.Array(Schema3.String)), {
|
|
662
|
+
decode: (s) => s.split(",").map((x) => x.trim()),
|
|
663
|
+
encode: (a) => a.join(",")
|
|
664
|
+
}).annotations({
|
|
665
|
+
[SCHEMA_KIND_ANNOTATION]: "commaSeparated",
|
|
666
|
+
[PLACEHOLDER_ANNOTATION]: "alpha,beta,gamma"
|
|
667
|
+
});
|
|
668
|
+
var commaSeparatedNumbers = Schema3.transform(Schema3.String, Schema3.mutable(Schema3.Array(Schema3.Number)), {
|
|
669
|
+
decode: (s) => s.split(",").map((x) => {
|
|
670
|
+
const n = Number(x.trim());
|
|
671
|
+
if (Number.isNaN(n))
|
|
672
|
+
throw new Error(`"${x.trim()}" is not a valid number`);
|
|
673
|
+
return n;
|
|
674
|
+
}),
|
|
675
|
+
encode: (a) => a.join(",")
|
|
676
|
+
}).annotations({
|
|
677
|
+
[SCHEMA_KIND_ANNOTATION]: "commaSeparatedNumbers",
|
|
678
|
+
[PLACEHOLDER_ANNOTATION]: "1,2,3"
|
|
679
|
+
});
|
|
680
|
+
var commaSeparatedUrls = Schema3.transform(Schema3.String, Schema3.mutable(Schema3.Array(url)), {
|
|
681
|
+
decode: (s) => s.split(",").map((x) => Schema3.decodeUnknownSync(url)(x.trim())),
|
|
682
|
+
encode: (a) => a.join(",")
|
|
683
|
+
}).annotations({
|
|
684
|
+
[SCHEMA_KIND_ANNOTATION]: "commaSeparatedUrls",
|
|
685
|
+
[PLACEHOLDER_ANNOTATION]: "https://one.example.com,https://two.example.com"
|
|
686
|
+
});
|
|
687
|
+
var stringEnum = (values) => Schema3.Literal(...values).annotations({
|
|
688
|
+
[SCHEMA_KIND_ANNOTATION]: "stringEnum",
|
|
689
|
+
[STRING_ENUM_VALUES_ANNOTATION]: values,
|
|
690
|
+
[PLACEHOLDER_ANNOTATION]: values[0]
|
|
691
|
+
});
|
|
692
|
+
var json = (schema) => Schema3.parseJson(schema).annotations({
|
|
693
|
+
[SCHEMA_KIND_ANNOTATION]: "json",
|
|
694
|
+
[PLACEHOLDER_ANNOTATION]: '{"key":"value"}'
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// src/introspect.ts
|
|
698
|
+
var BUCKETS2 = ["server", "client", "shared"];
|
|
699
|
+
function getAnn(ast, key) {
|
|
700
|
+
return Option.getOrUndefined(SchemaAST.getAnnotation(ast, key));
|
|
701
|
+
}
|
|
702
|
+
function examineSchema(schema) {
|
|
703
|
+
let ast = schema.ast;
|
|
704
|
+
let isRedacted = false;
|
|
705
|
+
let isOptional = false;
|
|
706
|
+
let hasDefault = false;
|
|
707
|
+
let defaultValue = undefined;
|
|
708
|
+
if (SchemaAST.isTransformation(ast) && getAnn(ast, REDACTED_ANNOTATION)) {
|
|
709
|
+
isRedacted = true;
|
|
710
|
+
ast = ast.from;
|
|
711
|
+
}
|
|
712
|
+
if (SchemaAST.isTransformation(ast) && getAnn(ast, DEFAULT_VALUE_ANNOTATION) !== undefined) {
|
|
713
|
+
hasDefault = true;
|
|
714
|
+
defaultValue = getAnn(ast, DEFAULT_VALUE_ANNOTATION);
|
|
715
|
+
const fromAst = ast.from;
|
|
716
|
+
if (SchemaAST.isUnion(fromAst)) {
|
|
717
|
+
const nonUndefined = fromAst.types.find((t) => !SchemaAST.isUndefinedKeyword(t));
|
|
718
|
+
if (nonUndefined)
|
|
719
|
+
ast = nonUndefined;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (SchemaAST.isUnion(ast) && getAnn(ast, OPTIONAL_ANNOTATION)) {
|
|
723
|
+
isOptional = true;
|
|
724
|
+
const nonUndefined = ast.types.find((t) => !SchemaAST.isUndefinedKeyword(t));
|
|
725
|
+
if (nonUndefined)
|
|
726
|
+
ast = nonUndefined;
|
|
727
|
+
}
|
|
728
|
+
const kind = getAnn(ast, SCHEMA_KIND_ANNOTATION);
|
|
729
|
+
const placeholder = getAnn(ast, PLACEHOLDER_ANNOTATION);
|
|
730
|
+
const stringEnumValues = kind === "stringEnum" ? getAnn(ast, STRING_ENUM_VALUES_ANNOTATION) : undefined;
|
|
731
|
+
return {
|
|
732
|
+
kind,
|
|
733
|
+
placeholder,
|
|
734
|
+
optional: isOptional,
|
|
735
|
+
hasDefault,
|
|
736
|
+
defaultValue,
|
|
737
|
+
redacted: isRedacted,
|
|
738
|
+
stringEnumValues
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function buildEnvExample(definition) {
|
|
742
|
+
const prefix = {
|
|
743
|
+
server: definition.prefix?.server ?? "",
|
|
744
|
+
client: definition.prefix?.client ?? "",
|
|
745
|
+
shared: definition.prefix?.shared ?? ""
|
|
746
|
+
};
|
|
747
|
+
const entries = [];
|
|
748
|
+
let line = 1;
|
|
749
|
+
for (const bucket of BUCKETS2) {
|
|
750
|
+
const schemas = definition[bucket] ?? {};
|
|
751
|
+
const keys = Object.keys(schemas).sort();
|
|
752
|
+
for (const key of keys) {
|
|
753
|
+
const schema = schemas[key];
|
|
754
|
+
const examined = examineSchema(schema);
|
|
755
|
+
const runtimeKey = `${prefix[bucket]}${key}`;
|
|
756
|
+
let value;
|
|
757
|
+
if (examined.hasDefault) {
|
|
758
|
+
value = encodeDefault(schema, examined);
|
|
759
|
+
} else {
|
|
760
|
+
value = examined.placeholder ?? "CHANGE_ME";
|
|
761
|
+
}
|
|
762
|
+
entries.push({
|
|
763
|
+
key: runtimeKey,
|
|
764
|
+
value,
|
|
765
|
+
line,
|
|
766
|
+
sectionBucket: bucket,
|
|
767
|
+
directives: {
|
|
768
|
+
type: examined.kind,
|
|
769
|
+
bucket,
|
|
770
|
+
optional: examined.optional,
|
|
771
|
+
hasDefault: examined.hasDefault,
|
|
772
|
+
redacted: examined.redacted,
|
|
773
|
+
stringEnumValues: examined.stringEnumValues
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
line += 1;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const hasPrefix = Object.values(prefix).some((v) => v.length > 0);
|
|
780
|
+
return encodeDotenvText({ entries, ...hasPrefix ? { prefix } : {} });
|
|
781
|
+
}
|
|
782
|
+
function encodeDefault(schema, examined) {
|
|
783
|
+
try {
|
|
784
|
+
let valueToEncode = examined.defaultValue;
|
|
785
|
+
if (examined.redacted) {
|
|
786
|
+
valueToEncode = Redacted2.make(valueToEncode);
|
|
787
|
+
}
|
|
788
|
+
const encoded = Schema4.encodeSync(schema)(valueToEncode);
|
|
789
|
+
return String(encoded ?? "");
|
|
790
|
+
} catch {
|
|
791
|
+
return stringifyDefault(examined);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function stringifyDefault(examined) {
|
|
795
|
+
const val = examined.defaultValue;
|
|
796
|
+
if (val === undefined || val === null)
|
|
797
|
+
return "";
|
|
798
|
+
if (Array.isArray(val))
|
|
799
|
+
return val.map(String).join(",");
|
|
800
|
+
if (typeof val === "object")
|
|
801
|
+
return JSON.stringify(val);
|
|
802
|
+
return String(val);
|
|
803
|
+
}
|
|
228
804
|
// src/resolvers/remote.ts
|
|
229
805
|
import { Effect as Effect3 } from "effect";
|
|
230
806
|
|
|
@@ -332,84 +908,6 @@ function safeCreateEnv(opts) {
|
|
|
332
908
|
return { success: false, error: normalizeEnvValidationError(error) };
|
|
333
909
|
}
|
|
334
910
|
}
|
|
335
|
-
// src/schemas.ts
|
|
336
|
-
import { Function, Schema as Schema2 } from "effect";
|
|
337
|
-
var DEFAULT_VALUE_ANNOTATION = Symbol.for("@ayronforge/envil/default-value");
|
|
338
|
-
var withDefault = Function.dual(2, (schema, defaultValue) => {
|
|
339
|
-
const withDefaultSchema = Schema2.transform(Schema2.UndefinedOr(schema), Schema2.typeSchema(schema), {
|
|
340
|
-
decode: (value) => value ?? defaultValue,
|
|
341
|
-
encode: (value) => value
|
|
342
|
-
});
|
|
343
|
-
return withDefaultSchema.annotations({
|
|
344
|
-
[DEFAULT_VALUE_ANNOTATION]: defaultValue
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
var optional = (schema) => Schema2.UndefinedOr(schema);
|
|
348
|
-
var redacted = (schema) => Schema2.Redacted(schema);
|
|
349
|
-
var requiredString = Schema2.String.pipe(Schema2.minLength(1)).annotations({
|
|
350
|
-
identifier: "RequiredString"
|
|
351
|
-
});
|
|
352
|
-
var optionalString = Schema2.UndefinedOr(Schema2.String);
|
|
353
|
-
var positiveNumber = Schema2.NumberFromString.pipe(Schema2.positive()).annotations({
|
|
354
|
-
identifier: "PositiveNumber"
|
|
355
|
-
});
|
|
356
|
-
var commaSeparated = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(Schema2.String)), {
|
|
357
|
-
decode: (s) => s.split(",").map((x) => x.trim()),
|
|
358
|
-
encode: (a) => a.join(",")
|
|
359
|
-
});
|
|
360
|
-
var commaSeparatedNumbers = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(Schema2.Number)), {
|
|
361
|
-
decode: (s) => s.split(",").map((x) => {
|
|
362
|
-
const n = Number(x.trim());
|
|
363
|
-
if (Number.isNaN(n))
|
|
364
|
-
throw new Error(`"${x.trim()}" is not a valid number`);
|
|
365
|
-
return n;
|
|
366
|
-
}),
|
|
367
|
-
encode: (a) => a.join(",")
|
|
368
|
-
});
|
|
369
|
-
var url = Schema2.String.pipe(Schema2.filter((s) => {
|
|
370
|
-
try {
|
|
371
|
-
new URL(s);
|
|
372
|
-
return s.startsWith("http://") || s.startsWith("https://");
|
|
373
|
-
} catch {
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
}, { identifier: "Url", message: () => "Expected a valid HTTP or HTTPS URL" }));
|
|
377
|
-
var postgresUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("postgres://") || s.startsWith("postgresql://"), {
|
|
378
|
-
identifier: "PostgresUrl",
|
|
379
|
-
message: () => "Expected a valid PostgreSQL connection URL"
|
|
380
|
-
}), Schema2.pattern(/^(postgres|postgresql):\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/));
|
|
381
|
-
var redisUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("redis://") || s.startsWith("rediss://"), {
|
|
382
|
-
identifier: "RedisUrl",
|
|
383
|
-
message: () => "Expected a valid Redis connection URL"
|
|
384
|
-
}), Schema2.pattern(/^rediss?:\/\/(?:[^:]+:[^@]+@)?[^:]+(?::\d+)?(?:\/\d+)?$/));
|
|
385
|
-
var commaSeparatedUrls = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(url)), {
|
|
386
|
-
decode: (s) => s.split(",").map((x) => Schema2.decodeUnknownSync(url)(x.trim())),
|
|
387
|
-
encode: (a) => a.join(",")
|
|
388
|
-
});
|
|
389
|
-
var boolean = Schema2.transform(Schema2.String.pipe(Schema2.filter((s) => ["true", "false", "1", "0"].includes(s.toLowerCase()), {
|
|
390
|
-
identifier: "BooleanString",
|
|
391
|
-
message: () => "Expected 'true', 'false', '1', or '0'"
|
|
392
|
-
})), Schema2.Boolean, {
|
|
393
|
-
decode: (s) => s.toLowerCase() === "true" || s === "1",
|
|
394
|
-
encode: (b) => b ? "true" : "false"
|
|
395
|
-
});
|
|
396
|
-
var integer = Schema2.NumberFromString.pipe(Schema2.int()).annotations({
|
|
397
|
-
identifier: "Integer"
|
|
398
|
-
});
|
|
399
|
-
var nonNegativeNumber = Schema2.NumberFromString.pipe(Schema2.nonNegative()).annotations({
|
|
400
|
-
identifier: "NonNegativeNumber"
|
|
401
|
-
});
|
|
402
|
-
var port = Schema2.NumberFromString.pipe(Schema2.int(), Schema2.between(1, 65535)).annotations({ identifier: "Port" });
|
|
403
|
-
var stringEnum = (values) => Schema2.Literal(...values);
|
|
404
|
-
var json = (schema) => Schema2.parseJson(schema);
|
|
405
|
-
var mongoUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("mongodb://") || s.startsWith("mongodb+srv://"), {
|
|
406
|
-
identifier: "MongoUrl",
|
|
407
|
-
message: () => "Expected a valid MongoDB connection URL"
|
|
408
|
-
}), Schema2.pattern(/^mongodb(\+srv)?:\/\/(?:[^:]+:[^@]+@)?[^/]+(?:\/[^?]*)?(?:\?.*)?$/));
|
|
409
|
-
var mysqlUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("mysql://") || s.startsWith("mysqls://"), {
|
|
410
|
-
identifier: "MysqlUrl",
|
|
411
|
-
message: () => "Expected a valid MySQL connection URL"
|
|
412
|
-
}), Schema2.pattern(/^mysqls?:\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/));
|
|
413
911
|
export {
|
|
414
912
|
withDefault,
|
|
415
913
|
url,
|
|
@@ -421,23 +919,30 @@ export {
|
|
|
421
919
|
postgresUrl,
|
|
422
920
|
positiveNumber,
|
|
423
921
|
port,
|
|
424
|
-
optionalString,
|
|
425
922
|
optional,
|
|
923
|
+
number,
|
|
426
924
|
nonNegativeNumber,
|
|
427
925
|
mysqlUrl,
|
|
428
926
|
mongoUrl,
|
|
429
927
|
json,
|
|
430
928
|
integer,
|
|
431
929
|
fromRemoteSecrets,
|
|
930
|
+
examineSchema,
|
|
432
931
|
createEnv,
|
|
433
932
|
commaSeparatedUrls,
|
|
434
933
|
commaSeparatedNumbers,
|
|
435
934
|
commaSeparated,
|
|
935
|
+
buildEnvExample,
|
|
436
936
|
boolean,
|
|
937
|
+
STRING_ENUM_VALUES_ANNOTATION,
|
|
938
|
+
SCHEMA_KIND_ANNOTATION,
|
|
939
|
+
REDACTED_ANNOTATION,
|
|
940
|
+
PLACEHOLDER_ANNOTATION,
|
|
941
|
+
OPTIONAL_ANNOTATION,
|
|
437
942
|
EnvValidationError,
|
|
438
943
|
DEFAULT_VALUE_ANNOTATION,
|
|
439
944
|
ClientAccessError
|
|
440
945
|
};
|
|
441
946
|
|
|
442
|
-
//# debugId=
|
|
947
|
+
//# debugId=D41C1808EDE7815C64756E2164756E21
|
|
443
948
|
//# sourceMappingURL=index.js.map
|