#!/usr/bin/env bun /** * Regenerates src//index.ts barrels. */ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs"; import { join } from "node:path"; const PACKAGES_DIR = "packages"; interface NamedExports { readonly values: readonly string[]; readonly types: readonly string[]; readonly hasDefault: boolean; } function parseExports(implPath: string): NamedExports { const src = readFileSync(implPath, "utf8"); const values = new Set(); const types = new Set(); let hasDefault = false; const cleaned = src .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/\/\/.*$/gm, ""); // Captures: 1 = "default" or "", 2 = kind, 3 = name. const re = /\bexport\s+(default\s+)?(?:abstract\s+)?(?:declare\s+)?(?:async\s+)?(const|let|var|function|class|enum|interface|type)\s+([A-Za-z_$][\w$]*)/g; let match: RegExpExecArray | null; while ((match = re.exec(cleaned)) !== null) { const isDefault = Boolean(match[1]); const kind = match[2] as string; const name = match[3] as string; if (isDefault) { hasDefault = true; // `export default class X {}` also creates a named binding X locally, // but the named export only happens through the default slot. continue; } if (kind === "type" || kind === "interface") types.add(name); else values.add(name); } // Detect bare `export default Identifier;`. const bareDefaultRe = /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;/g; while ((match = bareDefaultRe.exec(cleaned)) !== null) { hasDefault = true; } const listRe = /export\s+(type\s+)?\{([^}]+)\}/g; while ((match = listRe.exec(cleaned)) !== null) { const blockIsType = Boolean(match[1]); const body = match[2] as string; for (const piece of body.split(",")) { const trimmed = piece.trim(); if (!trimmed) continue; const typeM = trimmed.match(/^type\s+(.+)$/); const inner = typeM ? (typeM[1] as string) : trimmed; const asM = inner.match( /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/ ); const name = asM ? (asM[2] as string) : inner; if (!/^[A-Za-z_$][\w$]*$/.test(name)) continue; if (blockIsType || typeM) types.add(name); else values.add(name); } } values.delete("default"); return { values: [...values].sort(), types: [...types].sort(), hasDefault, }; } function buildBarrel(feature: string, exports: NamedExports): string { const lines: string[] = []; if (exports.types.length > 0) { lines.push("export type {"); for (const t of exports.types) lines.push(` ${t},`); lines.push(`} from "./${feature}.js";`); } if (exports.values.length > 0 || exports.hasDefault) { const names = [...exports.values]; if (exports.hasDefault) names.push("default"); lines.push("export {"); for (const v of names) lines.push(` ${v},`); lines.push(`} from "./${feature}.js";`); } if (lines.length === 0) lines.push(`export {} from "./${feature}.js";`); lines.push(""); return lines.join("\n"); } function listPackages(): readonly string[] { const out: string[] = []; for (const cat of readdirSync(PACKAGES_DIR)) { const catDir = join(PACKAGES_DIR, cat); if (!statSync(catDir).isDirectory()) continue; for (const plugin of readdirSync(catDir)) { const pluginDir = join(catDir, plugin); if (!statSync(pluginDir).isDirectory()) continue; for (const pkg of readdirSync(pluginDir)) { const pkgDir = join(pluginDir, pkg); if (!statSync(pkgDir).isDirectory()) continue; if (existsSync(join(pkgDir, "src"))) out.push(pkgDir); } } } return out; } function main(): void { let touched = 0; for (const pkgDir of listPackages()) { const srcDir = join(pkgDir, "src"); for (const entry of readdirSync(srcDir)) { const featureDir = join(srcDir, entry); try { if (!statSync(featureDir).isDirectory()) continue; } catch { continue; } const impl = join(featureDir, `${entry}.ts`); const indexFile = join(featureDir, "index.ts"); if (!existsSync(impl) || !existsSync(indexFile)) continue; const current = readFileSync(indexFile, "utf8"); const onlyTouchesImpl = current .split("\n") .filter((l) => l.includes("from ")) .every((l) => l.includes(`from "./${entry}.js"`)); if (!onlyTouchesImpl) continue; const exports = parseExports(impl); const barrel = buildBarrel(entry, exports); if (barrel !== current) { writeFileSync(indexFile, barrel); console.log(`REGEN ${indexFile}`); touched++; } } } console.log(`\nRegenerated ${touched} barrels.`); } main();