UNPKG

env-checker-strict

Version:

Strict environment variable checker with autogenerated ENV_LIST and CLI support

265 lines (229 loc) 8.29 kB
#!/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`); } }