envsaurus
Version:
ENVSAURUS is a zero-dependency CLI + library that turns your .env.example into a fully typed config file. It validates environment variables at runtime, fails fast in CI, and exports a safe CONFIG object for Node/TypeScript apps. Catch missing or invalid
202 lines (201 loc) • 6.49 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
/* basic CLI with subcommands */
// goals
// - help first
// - zero-setup defaults
// - no heavy deps
const fs = require("fs");
const path = require("path");
const parser_1 = require("./parser");
const generator_1 = require("./generator");
// show usage
function printHelp() {
console.log(`envsaurus
Usage:
envsaurus gen [--example .env.example] [--out src/config.ts] [--js]
envsaurus check [--example .env.example] [--strict]
envsaurus schema [--example .env.example] [--format json|yaml]
`);
}
function main() {
const args = process.argv.slice(2);
const cmd = args[0];
if (!cmd || cmd === '-h' || cmd === '--help') {
printHelp();
process.exit(0);
}
const opts = parseOpts(args.slice(1));
try {
if (cmd === 'gen')
return cmdGen(opts);
if (cmd === 'schema')
return cmdSchema(opts);
if (cmd === 'check')
return cmdCheck(opts);
console.error(`Unknown command: ${cmd}`);
process.exit(1);
}
catch (err) {
console.error(String(err.message || err));
process.exit(1);
}
}
main();
// tiny flag parser: --k v or --flag
function parseOpts(rest) {
const out = {};
for (let i = 0; i < rest.length; i++) {
const t = rest[i];
if (t.startsWith('--')) {
const key = t.slice(2);
const next = rest[i + 1];
if (next && !next.startsWith('--')) {
out[key] = next;
i++;
}
else {
out[key] = true;
}
}
}
return out;
}
// choose example path
function resolveExample(opts) {
const p = opts.example || '.env.example';
return path.resolve(p);
}
// choose out path (src/config.ts if src exists)
function resolveOut(opts) {
const custom = opts.out;
if (custom)
return path.resolve(custom);
const hasSrc = fs.existsSync(path.resolve('src'));
return path.resolve(hasSrc ? 'src/config.ts' : 'config.ts');
}
// generate config file from example
function cmdGen(opts) {
const example = resolveExample(opts);
const schema = (0, parser_1.parseExampleFile)(example);
const outFile = resolveOut(opts);
fs.mkdirSync(path.dirname(outFile), { recursive: true });
if (opts.js) {
const jsOut = outFile.endsWith('.ts')
? outFile.replace(/\.ts$/, '\.js')
: outFile;
const js = (0, generator_1.generateConfigJs)(schema);
fs.writeFileSync(jsOut, js, 'utf8');
console.log(`generated ${path.relative(process.cwd(), jsOut)}`);
}
else {
const ts = (0, generator_1.generateConfigTs)(schema);
fs.writeFileSync(outFile, ts, 'utf8');
console.log(`generated ${path.relative(process.cwd(), outFile)}`);
}
}
// print machine-readable schema
function cmdSchema(opts) {
const example = resolveExample(opts);
const schema = (0, parser_1.parseExampleFile)(example);
const format = opts.format || 'json';
if (format === 'json') {
console.log(JSON.stringify(schema, null, 2));
return;
}
if (format === 'yaml') {
// simple YAML without dependency
const lines = ['entries:'];
for (const e of schema.entries) {
lines.push(' - key: ' + e.key);
lines.push(' type: ' + e.type);
if (e.enumValues && e.enumValues.length)
lines.push(' enumValues: [' + e.enumValues.join(', ') + ']');
if (e.defaultValue != null)
lines.push(' defaultValue: ' + JSON.stringify(e.defaultValue));
}
console.log(lines.join('\n'));
return;
}
throw new Error('Unsupported --format (use json|yaml)');
}
// validate current process.env against declared vars
function cmdCheck(opts) {
const example = resolveExample(opts);
const schema = (0, parser_1.parseExampleFile)(example);
const strict = Boolean(opts.strict);
const errors = [];
const declaredKeys = new Set(schema.entries.map((e) => e.key));
// presence + type checks
for (const e of schema.entries) {
const raw = process.env[e.key];
const source = raw ?? e.defaultValue;
switch (e.type) {
case 'number': {
try {
require('./validators');
}
catch { }
if (source == null || source === '')
errors.push(`${e.key} required`);
else if (Number.isNaN(Number(source)))
errors.push(`${e.key} must be number`);
break;
}
case 'boolean': {
// no required error; defaults to false if empty
break;
}
case 'enum': {
const opts = e.enumValues || [];
if (!source || !opts.includes(String(source)))
errors.push(`${e.key} must be one of ${opts.join(',')}`);
break;
}
case 'url': {
try {
new URL(String(source));
}
catch {
errors.push(`${e.key} must be a valid URL`);
}
break;
}
case 'email': {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!source || !re.test(String(source)))
errors.push(`${e.key} must be a valid email`);
break;
}
case 'json': {
try {
JSON.parse(String(source));
}
catch {
errors.push(`${e.key} must be valid JSON`);
}
break;
}
case 'string':
default: {
if ((source == null || source === '') && e.defaultValue == null)
errors.push(`${e.key} required`);
break;
}
}
}
if (strict) {
for (const k of Object.keys(process.env)) {
if (/^[A-Z][A-Z0-9_]*$/.test(k) && !declaredKeys.has(k)) {
errors.push(`undeclared key: ${k}`);
}
}
}
if (errors.length) {
for (const e of errors)
console.error(e);
process.exit(1);
}
console.log('ok');
}
;