mirror of
https://github.com/earendil-works/pi.git
synced 2026-09-28 14:13:13 +08:00
Replace the tsgo native preview with typescript@7.0.2 and port the check scripts to the TS 7 API. Target ES2024, drop useDefineForClassFields: false and unused decorator options, and enable verbatimModuleSyntax. Replace tsx with node plus the source resolver hook, passed to --import as a file URL. closes #9965
134 lines
5.0 KiB
JavaScript
134 lines
5.0 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 { SyntaxKind } from "typescript/unstable/ast";
|
|
import {
|
|
isCallExpression,
|
|
isExportDeclaration,
|
|
isIdentifier,
|
|
isImportDeclaration,
|
|
isNamedExports,
|
|
isNamedImports,
|
|
isNoSubstitutionTemplateLiteral,
|
|
isPropertyAccessExpression,
|
|
isStringLiteral,
|
|
} from "typescript/unstable/ast/is";
|
|
import { API } from "typescript/unstable/sync";
|
|
import { getPublicWorkspacePackages } from "./release-packages.mjs";
|
|
|
|
// Packages without tsconfig.build.json are checked against a synthetic config.
|
|
const fallbackConfigName = "tsconfig.runtime-deps-fallback.json";
|
|
const fallbackConfig = JSON.stringify({ include: ["src/**/*"] });
|
|
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 || !(isStringLiteral(node) || isNoSubstitutionTemplateLiteral(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 (isImportDeclaration(node)) {
|
|
const clause = node.importClause;
|
|
const bindings = clause?.namedBindings;
|
|
if (
|
|
!clause ||
|
|
(clause.phaseModifier !== SyntaxKind.TypeKeyword &&
|
|
(clause.name ||
|
|
!bindings ||
|
|
!isNamedImports(bindings) ||
|
|
bindings.elements.length === 0 ||
|
|
bindings.elements.some((element) => !element.isTypeOnly)))
|
|
) {
|
|
checkSpecifier(node.moduleSpecifier);
|
|
}
|
|
} else if (isExportDeclaration(node) && !node.isTypeOnly) {
|
|
const clause = node.exportClause;
|
|
if (
|
|
!clause ||
|
|
!isNamedExports(clause) ||
|
|
clause.elements.length === 0 ||
|
|
clause.elements.some((element) => !element.isTypeOnly)
|
|
) {
|
|
checkSpecifier(node.moduleSpecifier);
|
|
}
|
|
} else if (
|
|
isCallExpression(node) &&
|
|
(node.expression.kind === SyntaxKind.ImportKeyword ||
|
|
(isIdentifier(node.expression) && node.expression.text === "require") ||
|
|
(isPropertyAccessExpression(node.expression) && node.expression.getText(source) === "require.resolve"))
|
|
) {
|
|
checkSpecifier(node.arguments[0]);
|
|
}
|
|
node.forEachChild(visit);
|
|
}
|
|
visit(source);
|
|
}
|
|
|
|
const packages = getPublicWorkspacePackages()
|
|
.map(({ directory }) => ({ directory, sourceDirectory: resolve(directory, "src") }))
|
|
.filter(({ sourceDirectory }) => existsSync(sourceDirectory))
|
|
.map(({ directory, sourceDirectory }) => {
|
|
const configPath = resolve(directory, "tsconfig.build.json");
|
|
return {
|
|
sourceDirectory,
|
|
manifest: JSON.parse(readFileSync(join(directory, "package.json"), "utf8")),
|
|
configPath: existsSync(configPath) ? configPath : resolve(directory, fallbackConfigName),
|
|
};
|
|
});
|
|
|
|
const fallbackConfigs = new Set(
|
|
packages.map(({ configPath }) => configPath).filter((path) => path.endsWith(fallbackConfigName)),
|
|
);
|
|
const api = new API({
|
|
cwd: process.cwd(),
|
|
fs: {
|
|
fileExists: (fileName) => (fallbackConfigs.has(resolve(fileName)) ? true : undefined),
|
|
readFile: (fileName) => (fallbackConfigs.has(resolve(fileName)) ? fallbackConfig : undefined),
|
|
},
|
|
});
|
|
try {
|
|
const snapshot = api.updateSnapshot({ openProjects: packages.map(({ configPath }) => configPath) });
|
|
for (const { sourceDirectory, manifest, configPath } of packages) {
|
|
const project = snapshot.getProject(configPath);
|
|
const diagnostics = project.program.getConfigFileParsingDiagnostics();
|
|
if (diagnostics.length > 0) throw new Error(diagnostics.map((diagnostic) => diagnostic.text).join("\n"));
|
|
const roots = new Set(project.rootFiles.map((file) => resolve(file)));
|
|
for (const fileName of project.program.getSourceFileNames()) {
|
|
if (fileName.endsWith(".d.ts") || fileName.endsWith(".json")) continue;
|
|
const path = relative(sourceDirectory, resolve(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(fileName))) {
|
|
failures.push(`${fileName} is excluded from ${manifest.name}'s build but imported by it`);
|
|
}
|
|
checkSource(project.program.getSourceFile(fileName), manifest);
|
|
}
|
|
}
|
|
} finally {
|
|
api.close();
|
|
}
|
|
|
|
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.");
|