fix(release): prepare aligned 0.1.45 composition and guarded candidate sequencing
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/** Reject erased type-only imports used as runtime JSX component tags. */
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolve, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const workspaceRoot = resolve(import.meta.dirname, "../../..");
|
||||
const require = createRequire(resolve(workspaceRoot, "govoplan-core/webui/package.json"));
|
||||
const ts = require("typescript");
|
||||
|
||||
function isTypeOnlyImport(declaration) {
|
||||
if (ts.isImportSpecifier(declaration)) return declaration.isTypeOnly || declaration.parent.parent.isTypeOnly;
|
||||
if (ts.isNamespaceImport(declaration)) return declaration.parent.isTypeOnly;
|
||||
return (ts.isImportClause(declaration) || ts.isImportEqualsDeclaration(declaration)) && declaration.isTypeOnly;
|
||||
}
|
||||
|
||||
/** Resolve lexical bindings, including shadowing; do not typecheck unrelated
|
||||
* optional dependencies or report ordinary application diagnostics.
|
||||
*/
|
||||
export function findTypeOnlyJsxImports(sources) {
|
||||
const files = new Map(sources.map(({ path, source }) => [resolve(path),
|
||||
ts.createSourceFile(resolve(path), source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)]));
|
||||
const options = { noEmit: true, noResolve: true, noLib: true, types: [], jsx: ts.JsxEmit.Preserve };
|
||||
const host = ts.createCompilerHost(options);
|
||||
host.getSourceFile = (path) => files.get(resolve(path));
|
||||
const program = ts.createProgram([...files.keys()], options, host);
|
||||
const checker = program.getTypeChecker();
|
||||
const findings = [];
|
||||
for (const [path, source] of files) {
|
||||
function visit(node) {
|
||||
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
||||
let root = node.tagName;
|
||||
// Lower-case direct tags are intrinsic HTML, not runtime bindings.
|
||||
if (!(ts.isIdentifier(root) && /^[a-z]/.test(root.text))) {
|
||||
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
||||
const declarations = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
||||
if (declarations.some(isTypeOnlyImport)) {
|
||||
const position = source.getLineAndCharacterOfPosition(node.tagName.getStart(source));
|
||||
findings.push({ path, line: position.line + 1, column: position.character + 1, component: node.tagName.getText(source) });
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(source);
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function sourceFiles(directory) {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = resolve(directory, entry.name);
|
||||
return entry.isDirectory() ? sourceFiles(path) : entry.name.endsWith(".tsx") ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
export function checkWorkspace(root = workspaceRoot) {
|
||||
const modules = readdirSync(root, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith("govoplan"))
|
||||
.map((entry) => resolve(root, entry.name, "webui/src")).filter(existsSync);
|
||||
const paths = modules.flatMap(sourceFiles);
|
||||
const findings = findTypeOnlyJsxImports(paths.map((path) => ({ path, source: readFileSync(path, "utf8") })));
|
||||
for (const finding of findings) {
|
||||
console.error(`${relative(root, finding.path)}:${finding.line}:${finding.column}: JSX component ${finding.component} is imported type-only and will be erased at runtime.`);
|
||||
}
|
||||
if (!findings.length) console.log(`JSX runtime-import contract passed: ${paths.length} TSX files across ${modules.length} WebUI modules.`);
|
||||
return findings.length ? 1 : 0;
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
process.exitCode = checkWorkspace();
|
||||
}
|
||||
Reference in New Issue
Block a user