import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import console from "node:console"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import process from "node:process"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const require = createRequire(import.meta.url); // TypeScript 7's root package is the native CLI and no longer exposes the // legacy compiler API. Public API extraction intentionally uses the compiler // version embedded by @wrnexus/typecheck. const ts = require(join(root, "packages", "typecheck", "node_modules", "typescript")); const baselinePath = join(root, "docs", "public-api-0.8.json"); function flattenTargets(value) { if (typeof value === "string") return [value]; if (Array.isArray(value)) return value.flatMap(flattenTargets); if (value && typeof value === "object") return Object.values(value).flatMap(flattenTargets); return []; } const entries = []; for (const directory of readdirSync(join(root, "packages"), { withFileTypes: true })) { if (!directory.isDirectory()) continue; const packageRoot = join(root, "packages", directory.name); const manifestPath = join(packageRoot, "package.json"); if (!existsSync(manifestPath)) continue; const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); const exportsMap = manifest.exports && typeof manifest.exports === "object" ? manifest.exports : { ".": manifest.main ?? "./src/index.ts" }; for (const [subpath, value] of Object.entries(exportsMap)) { const target = flattenTargets(value).find((candidate) => /\.[cm]?[jt]sx?$/.test(candidate)); if (!target) continue; const absolute = resolve(packageRoot, target); if (existsSync(absolute)) entries.push({ package: manifest.name, subpath, absolute }); } } const config = ts.parseJsonConfigFileContent( ts.readConfigFile(join(root, "tsconfig.json"), ts.sys.readFile).config, ts.sys, root, ); const program = ts.createProgram( [...new Set(entries.map(({ absolute }) => absolute))], config.options, ); const checker = program.getTypeChecker(); const packages = {}; for (const entry of entries.sort( (left, right) => left.package.localeCompare(right.package) || left.subpath.localeCompare(right.subpath), )) { const source = program.getSourceFile(entry.absolute); if (!source) throw new Error(`Public API entry was not loaded: ${relative(root, entry.absolute)}`); const symbol = checker.getSymbolAtLocation(source); const names = symbol ? checker .getExportsOfModule(symbol) .map((item) => item.getName()) .filter((name) => name !== "__esModule") .sort() : []; (packages[entry.package] ??= {})[entry.subpath] = names; } const report = { schemaVersion: 1, releaseLine: "0.8", packages }; const output = `${JSON.stringify(report, null, 2)}\n`; if (process.argv.includes("--write")) { writeFileSync(baselinePath, output, "utf8"); console.log(`Wrote ${relative(root, baselinePath)}`); } else { if (!existsSync(baselinePath)) { throw new Error("Public API baseline is missing. Run `bun run generate:public-api`."); } const expected = readFileSync(baselinePath, "utf8"); if (expected !== output) { throw new Error( "Public API changed. Review compatibility, then run `bun run generate:public-api` intentionally.", ); } console.log(`Public API baseline matches ${Object.keys(packages).length} packages.`); }