mirror of
https://github.com/earendil-works/pi.git
synced 2026-09-28 22:22:57 +08:00
Separate stable and development entrypoints, exclude remote harness code from distributions, and verify isolated consumer installs before release. Fixes #9132.
94 lines
3.9 KiB
JavaScript
94 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { isBuiltin } from "node:module";
|
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
import ts from "typescript";
|
|
import { getPublicWorkspacePackages } from "./release-packages.mjs";
|
|
|
|
const failures = [];
|
|
|
|
function checkSource(source, manifest) {
|
|
const file = source.fileName;
|
|
const declared = new Set([
|
|
manifest.name,
|
|
...Object.keys(manifest.dependencies ?? {}),
|
|
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
...Object.keys(manifest.peerDependencies ?? {}),
|
|
]);
|
|
|
|
function checkSpecifier(node) {
|
|
if (!node || !ts.isStringLiteralLike(node)) return;
|
|
const specifier = node.text;
|
|
if (specifier.startsWith(".") || specifier.startsWith("/") || isBuiltin(specifier)) return;
|
|
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
if (declared.has(name)) return;
|
|
const { line } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
failures.push(`${file}:${line + 1}: ${specifier} is not declared in ${manifest.name}'s runtime dependencies`);
|
|
}
|
|
|
|
function visit(node) {
|
|
if (ts.isImportDeclaration(node)) {
|
|
const clause = node.importClause;
|
|
const bindings = clause?.namedBindings;
|
|
if (
|
|
!clause ||
|
|
(!clause.isTypeOnly &&
|
|
(clause.name || !bindings || !ts.isNamedImports(bindings) ||
|
|
bindings.elements.length === 0 || bindings.elements.some((element) => !element.isTypeOnly)))
|
|
) {
|
|
checkSpecifier(node.moduleSpecifier);
|
|
}
|
|
} else if (ts.isExportDeclaration(node) && !node.isTypeOnly) {
|
|
const clause = node.exportClause;
|
|
if (!clause || !ts.isNamedExports(clause) || clause.elements.length === 0 || clause.elements.some((element) => !element.isTypeOnly)) {
|
|
checkSpecifier(node.moduleSpecifier);
|
|
}
|
|
} else if (
|
|
ts.isCallExpression(node) &&
|
|
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
(ts.isIdentifier(node.expression) && node.expression.text === "require") ||
|
|
(ts.isPropertyAccessExpression(node.expression) && node.expression.getText(source) === "require.resolve"))
|
|
) {
|
|
checkSpecifier(node.arguments[0]);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(source);
|
|
}
|
|
|
|
for (const { directory } of getPublicWorkspacePackages()) {
|
|
const sourceDirectory = resolve(directory, "src");
|
|
if (!existsSync(sourceDirectory)) continue;
|
|
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
const configPath = join(directory, "tsconfig.build.json");
|
|
const config = existsSync(configPath)
|
|
? ts.readConfigFile(configPath, ts.sys.readFile)
|
|
: { config: { include: ["src/**/*"] } };
|
|
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n"));
|
|
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, resolve(directory));
|
|
if (parsed.errors.length > 0) {
|
|
throw new Error(parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"));
|
|
}
|
|
const roots = new Set(parsed.fileNames.map((file) => resolve(file)));
|
|
const program = ts.createProgram(parsed.fileNames, parsed.options);
|
|
for (const source of program.getSourceFiles()) {
|
|
if (source.isDeclarationFile || source.fileName.endsWith(".json")) continue;
|
|
const path = relative(sourceDirectory, resolve(source.fileName));
|
|
if (path.startsWith("..") || isAbsolute(path)) continue;
|
|
// TypeScript's exclude only filters roots: imports can pull excluded files
|
|
// back into the build. Reject that too, including type-only imports.
|
|
if (!roots.has(resolve(source.fileName))) {
|
|
failures.push(`${source.fileName} is excluded from ${manifest.name}'s build but imported by it`);
|
|
}
|
|
checkSource(source, manifest);
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error("Undeclared runtime imports in public packages:");
|
|
for (const failure of failures) console.error(` ${failure}`);
|
|
process.exit(1);
|
|
}
|
|
console.log("Public package runtime imports have declared dependencies.");
|