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);
});