/** * Generate a static, importable barrel of a lexicon's post-synth checks. * * The runtime loader (`discoverPostSynthChecks`) readdir-globs the directory and * `require()`s each file via tsx — fs- and tsx-coupled, so it can't run in a * bundle or on an edge runtime (Cloudflare Workers). This generator produces a * committed `lint/post-synth/index.ts` with explicit static imports + an exported * `postSynthChecks` array, which bundlers can follow with no fs/tsx/require. See #409. * * Detection reuses the runtime's own `isPostSynthCheck`, so the barrel contains * exactly the checks discovery would — including files whose export name differs * from the filename (docker/helm/temporal) and excluding helper modules * (cf-refs, arm-refs, …) that export no check. */ import { createRequire } from "module"; import { listRuleFiles } from "../lint/discover"; import { isPostSynthCheck } from "../lint/post-synth"; export interface BarrelEntry { /** Module specifier relative to the barrel, e.g. "./gha021" or "./apt-no-recommends". */ module: string; /** Exported binding holding the check, e.g. "gha021" or "dkrd010". */ exportName: string; /** The check's id (e.g. "GHA021"), used for stable ordering. */ id: string; } /** * Scan a post-synth directory and return one entry per exported check. * Runs at codegen time on Node (tsx-powered require), never in the shipped path. */ export function scanPostSynthChecks(dir: string, importMetaUrl: string): BarrelEntry[] { const require = createRequire(importMetaUrl); const entries: BarrelEntry[] = []; for (const file of listRuleFiles(dir)) { const base = file.replace(/\.ts$/, ""); let mod: Record; try { mod = require(`${dir}/${base}`) as Record; } catch { continue; // a file that fails to load contributes nothing } for (const [exportName, value] of Object.entries(mod)) { if (isPostSynthCheck(value)) { entries.push({ module: `./${base}`, exportName, id: value.id }); } } } return entries; } /** Render the barrel file content from scanned entries (pure — easy to unit test). */ export function generatePostSynthBarrel(entries: BarrelEntry[]): string { const sorted = [...entries].sort((a, b) => a.id.localeCompare(b.id)); const lines: string[] = []; lines.push("// Code generated by chant generate. DO NOT EDIT."); lines.push('import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";'); for (const e of sorted) { lines.push(`import { ${e.exportName} } from ${JSON.stringify(e.module)};`); } lines.push(""); lines.push("export const postSynthChecks: PostSynthCheck[] = ["); for (const e of sorted) { lines.push(` ${e.exportName},`); } lines.push("];"); lines.push(""); return lines.join("\n"); } /** Scan a directory and render its barrel in one step. */ export function renderPostSynthBarrelForDir(dir: string, importMetaUrl: string): string { return generatePostSynthBarrel(scanPostSynthChecks(dir, importMetaUrl)); }