env-checker-strict
Version:
Strict environment variable checker with autogenerated ENV_LIST and CLI support
265 lines (229 loc) • 8.29 kB
JavaScript
#!/usr/bin/env node
// bin/env-list-generator.js
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { resolve, extname } from 'path';
import { parse } from 'dotenv';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import { envCommentContent } from '../src/comment.js';
const argv = yargs(hideBin(process.argv))
.option('input', {
alias: 'i',
type: 'string',
default: '.env',
description: 'Path to .env file',
})
.option('output', {
alias: 'o',
type: 'string',
default: 'env_checker.js',
description: 'Output file path',
})
.option('skip', {
alias: 's',
type: 'string',
description: 'Comma-separated list of keys to skip',
})
.option('comment', {
alias: 'c',
type: 'boolean',
default: true,
description: 'Update .env with instruction comment on top about run `env-checker-strict`',
})
.option('ts', {
type: 'boolean',
default: false,
description: 'Generate TypeScript output (.ts)',
})
.option('inject', {
type: 'string',
default: 'default',
description: 'Automatically inject import and checkEnvAndThrowError() into the app entry file',
})
.option('show-logs', {
type: 'boolean',
default: true,
description: 'Show console logs of progress and process'
})
.option('show-warns', {
type: 'boolean',
default: true,
description: 'Show erros and warnings for non-succcess operations',
})
.help()
.argv;
const showLogs = argv['show-logs'];
const showWarns = argv['show-warns'];
const envPath = resolve(process.cwd(), argv.input);
const isTs = argv.ts;
const envComment = argv.comment;
let outputPath = argv.output;
const shouldInject = argv.inject;
if (isTs && outputPath.endsWith('.js')) {
outputPath = outputPath.replace(/\.js$/, '.ts');
}
const skipKeys = argv.skip ? argv.skip.split(',') : [];
if (!existsSync(envPath)) {
if (showWarns)
console.error(`❌ File not found: ${envPath}`);
process.exit(1);
}
const envVars = parse(readFileSync(envPath));
const keys = Object.keys(envVars).filter(key => !skipKeys.includes(key));
const commentBlock = `
/**
* Always throw error if sensitive environment variable
* not found, to detect issues immediately after boot.
* Make sure to run \`npx env-checker-strict\` to update
* this list if new env vars are added.
*/
`;
const generated = `// Auto-generated by env-checker-strict
// ⚠️ Do not modify manually
${commentBlock}
/**
* List of required environment variables.
* Used for strict validation and reference.
*/
const ENV_LIST = [
${keys.map(k => `'${k}'`).join(',\n ')}
];
/**
* Access all environment variables with autocomplete and type safety support.
* @typedef {Object} ENVS
* ${keys.map(k => `@property {string} ${k}`).join('\n * ')}
*/
/** @type {ENVS} */
const ENVS = {
${keys.map(k => `${k}: process.env.${k}`).join(',\n ')}
};
/**
* Throws an error if any required environment variable is missing.
*/
const checkEnvAndThrowError = () => {
for (const env of ENV_LIST) {
if (!process.env[env]) {
const message =
\`[ENV ERROR] Missing required environment variable: \${env}\\n\` +
\`➡️ Make sure '\${env}' is defined in your .env file.\\n\` +
\`📄 Location: \${__filename}\`;
console.error(message);
throw new Error(message);
}
}
};
module.exports = { checkEnvAndThrowError, ENVS };`;
const tsGenerated = `// Auto-generated by env-checker-strict
// ⚠️ Do not modify manually
${commentBlock}
/**
* List of required environment variables.
* Used for strict validation and reference.
*/
export const ENV_LIST: string[] = [
${keys.map(k => `'${k}'`).join(',\n ')}
];
/**
* Access all environment variables with autocomplete and type safety support.
*/
export interface ENVS {
${keys.map(k => `${k}: string`).join(';\n ')}
}
export const ENVS: ENVS = {
${keys.map(k => `${k}: process.env.${k} as string`).join(',\n ')}
};
/**
* Throws an error if any required environment variable is missing.
*/
export const checkEnvAndThrowError = (): void => {
for (const env of ENV_LIST) {
if (!process.env[env]) {
const message =
\`[ENV ERROR] Missing required environment variable: \${env}\\n\` +
\`➡️ Make sure '\${env}' is defined in your .env file.\\n\` +
\`📄 Location: \${import.meta.url || 'env-checker.ts'}\`;
console.error(message);
throw new Error(message);
}
}
};`;
writeFileSync(outputPath, isTs ? tsGenerated : generated);
if (envComment) {
const originalContent = readFileSync(envPath, 'utf8');
if (!originalContent.includes(envCommentContent)) {
const newContent = `${envCommentContent}\n${originalContent}`;
writeFileSync(envPath, newContent, 'utf8');
}
}
if (showLogs) {
console.log(`\n\x1b[32m✔ ENV_LIST generated and saved to: \x1b[0m ${outputPath}\n`);
console.log(`\x1b[36mℹ Step 1: Add the following line at the top of your entry file (e.g., ${isTs ? 'main.ts' : 'index.js'}) to validate required envs:\x1b[0m`);
console.log('\x1b[90m------------------------------------------------------\x1b[0m');
if (isTs) {
console.log(`\x1b[33mimport { checkEnvAndThrowError } from './${outputPath}';\x1b[0m`);
} else {
console.log(`\x1b[33mconst { checkEnvAndThrowError } = require('./${outputPath}');\x1b[0m`);
}
console.log(`\x1b[33mcheckEnvAndThrowError();\x1b[0m`);
console.log('\x1b[90m------------------------------------------------------\x1b[0m\n');
console.log(`\x1b[36mℹ Step 2: Access your env variables anywhere in your app using \`ENVS\`:\x1b[0m`);
console.log('\x1b[90m------------------------------------------------------\x1b[0m');
if (isTs) {
console.log(`\x1b[33mimport { ENVS } from './${outputPath}';\x1b[0m`);
} else {
console.log(`\x1b[33mconst { ENVS } = require('./${outputPath}');\x1b[0m`);
}
console.log(`\x1b[33mconsole.log(ENVS.JWT_SECRET); // access with autocomplete\x1b[0m`);
console.log('\x1b[90m------------------------------------------------------\x1b[0m\n');
}
function generateImportStatement({ isTs, ext, type, outputPath }) {
const importPath = `./${outputPath.replace(/\.(ts|js)$/, '')}`;
if (isTs || ext === '.ts') {
return `import { checkEnvAndThrowError } from '${importPath}';`;
}
if (type === 'module') {
return `import { checkEnvAndThrowError } from '${importPath}';`;
}
return `const { checkEnvAndThrowError } = require('${importPath}');`;
}
function generateDotenvStatement({ isTs, type }) {
if (isTs || type === 'module') {
return `import 'dotenv/config';`;
}
return `require('dotenv').config();`;
}
if (shouldInject) {
try {
const pkgJson = JSON.parse(readFileSync(resolve('package.json'), 'utf-8'));
const mainFile = shouldInject === 'default' ? pkgJson.main || 'index.js' : shouldInject;
const type = pkgJson.type;
const mainPath = resolve(mainFile);
if (existsSync(mainPath)) {
const ext = extname(mainPath);
let entryCode = readFileSync(mainPath, 'utf-8');
const lines = entryCode.split('\n');
const importStatement = generateImportStatement({ outputPath, isTs, type, ext });
const dotenvStatement = generateDotenvStatement({ isTs, type });
const alreadyHasCheck = entryCode.includes('checkEnvAndThrowError');
let dotenvLineIndex = lines.findIndex(line =>
line.includes('dotenv.config') || line.includes("dotenv/config")
);
if (!alreadyHasCheck) {
// Remove dotenv line if it exists
if (dotenvLineIndex !== -1) lines.splice(dotenvLineIndex, 1);
// Inject in correct order at top
const insertLines = [dotenvStatement, importStatement, 'checkEnvAndThrowError();', ''];
entryCode = [...insertLines, ...lines].join('\n');
writeFileSync(mainPath, entryCode, 'utf-8');
if (showLogs)
console.log(`\x1b[32m✔ Injected dotenv + checkEnvAndThrowError into:\x1b[0m ${mainPath}`);
}
} else {
if (showWarns)
console.warn(`\x1b[33m⚠ Could not find entry file to inject: ${mainPath}\x1b[0m`);
}
} catch (err) {
if (showLogs)
console.error(`\x1b[31m❌ Failed to inject into entry file: ${err.message}\x1b[0m`);
}
}