env-audit.mjs3.5 KBView on GitHub import { readdir, readFile, stat } from 'fs/promises';
import { join, relative } from 'path';
const ROOT = process.cwd();
const SKIP_DIRS = new Set([
'.git',
'node_modules',
'.turbo',
'dist',
'build',
'coverage',
'.next',
'.idea',
]);
const FILE_EXTENSIONS = new Set([
'.ts',
'.tsx',
'.js',
'.jsx',
'.mjs',
'.cjs',
'.json',
'.jsonc',
'.yml',
'.yaml',
]);
const SKIP_FILES = new Set([
'pnpm-lock.yaml',
'i18n.lock',
'bun.lock',
]);
const ENV_PATTERNS = [
{ label: 'env', regex: /\benv\.([A-Z][A-Z0-9_]+)\b/g },
{ label: 'process.env', regex: /\bprocess\.env\.([A-Z][A-Z0-9_]+)\b/g },
{ label: 'import.meta.env', regex: /\bimport\.meta\.env\.([A-Z][A-Z0-9_]+)\b/g },
{ label: 'c.env', regex: /\bc\.env\.([A-Z][A-Z0-9_]+)\b/g },
{ label: 'ctx.c.env', regex: /\bctx\.c\.env\.([A-Z][A-Z0-9_]+)\b/g },
];
function isEnvKey(value) {
return /^[A-Z][A-Z0-9_]*$/.test(value);
}
async function walk(dir, results) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('cdk.out')) {
await walk(fullPath, results);
}
continue;
}
const ext = entry.name.slice(entry.name.lastIndexOf('.'));
if (!SKIP_FILES.has(entry.name) && FILE_EXTENSIONS.has(ext)) {
results.push(fullPath);
}
}
}
async function readRootEnvKeys() {
const envPath = join(ROOT, '.env');
try {
const info = await stat(envPath);
if (!info.isFile()) return [];
const content = await readFile(envPath, 'utf8');
return content
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#') && line.includes('='))
.map((line) => line.split('=')[0].replace(/^export\s+/, '').trim())
.filter(Boolean);
} catch {
return [];
}
}
async function main() {
const files = [];
await walk(ROOT, files);
const envUsage = new Map();
for (const file of files) {
const content = await readFile(file, 'utf8').catch(() => '');
if (!content) continue;
const labelsByKey = new Map();
for (const pattern of ENV_PATTERNS) {
for (const match of content.matchAll(pattern.regex)) {
const key=[redacted];
if (!isEnvKey(key)) {
continue;
}
if (!labelsByKey.has(key)) {
labelsByKey.set(key, new Set());
}
labelsByKey.get(key).add(pattern.label);
}
}
for (const [key, labels] of labelsByKey) {
if (!envUsage.has(key)) {
envUsage.set(key, []);
}
envUsage.get(key).push({
file: relative(ROOT, file),
labels: [...labels].sort(),
});
}
}
const rootEnvKeys = await readRootEnvKeys();
const usedKeys = new Set(envUsage.keys());
const unusedRootEnvKeys = rootEnvKeys.filter((key) => !usedKeys.has(key));
const output = {
summary: {
referencedKeyCount: usedKeys.size,
rootEnvPresent: rootEnvKeys.length > 0,
rootEnvKeyCount: rootEnvKeys.length,
unusedRootEnvKeyCount: unusedRootEnvKeys.length,
},
referencedKeys: [...envUsage.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, sources]) => ({
key,
sources: sources.sort((a, b) => a.file.localeCompare(b.file)),
})),
unusedRootEnvKeys,
};
console.log(JSON.stringify(output, null, 2));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});