155 lines
5.2 KiB
TypeScript
155 lines
5.2 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Regenerates src/<feature>/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<string>();
|
|
const types = new Set<string>();
|
|
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();
|