88 lines
3.5 KiB
JavaScript
88 lines
3.5 KiB
JavaScript
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.`);
|
|
}
|