Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-core-build-without-plugins-smoke.mjs
T

282 lines
9.2 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import { spawn } from "node:child_process";
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const repoRoot = path.resolve(__dirname, "..");
export const buildCommand = { command: "pnpm", args: ["run", "build"] };
export const runtimeScanRoots = ["package.json", "apps", "packages", "scripts", "Dockerfile"];
export const excludedRuntimePrefixes = ["packages/plugins/"];
export const ignoredDirectoryNames = new Set([
".git",
".cache",
".next",
".react-router",
".tc",
".turbo",
".vite",
"build",
"cache",
"coverage",
"dist",
"generated",
"node_modules",
"tmp",
"temp",
"vendor",
]);
export const ignoredFileNames = new Set([
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
]);
export const sourceFileExtensions = new Set([
".json",
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
]);
const forbiddenPatterns = [
{ id: "toolctrl-plugins-package", pattern: /(?:^|["'`\s])@toolctrl\/plugins(?:["'`\s/]|$)/ },
{ id: "packages-plugins-path", pattern: /(?:^|["'`\s./])packages\/plugins(?:["'`\s/]|$)/ },
];
function isAllowedPluginMetadataReference(relativeFilePath, line) {
const normalized = normalizeRelativePath(relativeFilePath);
if (normalized === "Dockerfile" && line.trim() === "COPY packages/plugins/package.json packages/plugins/package.json") {
return true;
}
return normalized === "scripts/coverage-targets.json";
}
function toRelativePath(root, absolutePath) {
return path.relative(root, absolutePath).split(path.sep).join("/");
}
function normalizeRelativePath(relativePath) {
return relativePath.split(path.sep).join("/").replace(/^\.\//, "");
}
export function isIgnoredDirectory(name, relativePath = name) {
const normalized = normalizeRelativePath(relativePath);
return ignoredDirectoryNames.has(name) || ignoredDirectoryNames.has(normalized) || name.endsWith(".cache") || normalized.includes("/__generated__/");
}
export function isGeneratedOrIgnoredFile(filePath) {
const name = path.basename(filePath);
return (
ignoredFileNames.has(name) ||
name.endsWith(".generated.ts") ||
name.endsWith(".generated.tsx") ||
name.endsWith(".gen.ts") ||
name.endsWith(".gen.tsx") ||
name.endsWith(".min.js") ||
name.endsWith(".map") ||
name.endsWith(".d.ts")
);
}
export function isSourceFile(filePath) {
return path.basename(filePath) === "Dockerfile" || sourceFileExtensions.has(path.extname(filePath));
}
export function isTestFile(filePath) {
const normalized = normalizeRelativePath(filePath);
const name = path.basename(normalized);
return (
/(?:^|[./_-])(?:test|spec|stories|story)\.[cm]?[jt]sx?$/.test(name) ||
/\.(?:test|spec|stories|story)\.[cm]?[jt]sx?$/.test(name) ||
normalized.includes("/__tests__/") ||
normalized.includes("/test/") ||
normalized.includes("/tests/") ||
normalized.includes("/fixtures/") ||
normalized.includes("/mocks/") ||
normalized.includes("/__mocks__/")
);
}
export function isExcludedRuntimePath(relativePath) {
const normalized = normalizeRelativePath(relativePath);
return excludedRuntimePrefixes.some((prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix));
}
async function pathExists(root, relativePath) {
try {
await stat(path.join(root, relativePath));
return true;
} catch (error) {
if (error?.code === "ENOENT") return false;
throw error;
}
}
export async function collectSourceFiles(root = repoRoot, scanRoots = runtimeScanRoots) {
const files = [];
const roots = [];
const missingRoots = [];
async function walk(directory) {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const absolutePath = path.join(directory, entry.name);
const relativePath = toRelativePath(root, absolutePath);
if (entry.isDirectory()) {
if (isIgnoredDirectory(entry.name, relativePath) || isExcludedRuntimePath(relativePath)) continue;
await walk(absolutePath);
continue;
}
if (!entry.isFile()) continue;
if (relativePath === "scripts/verify-core-build-without-plugins-smoke.mjs") continue;
if (relativePath === "scripts/scan-removed-modules.ts") continue;
if (!isSourceFile(entry.name) || isGeneratedOrIgnoredFile(entry.name)) continue;
if (isExcludedRuntimePath(relativePath) || isTestFile(relativePath)) continue;
files.push(absolutePath);
}
}
for (const scanRoot of scanRoots) {
const normalizedRoot = normalizeRelativePath(scanRoot);
if (!(await pathExists(root, normalizedRoot))) {
missingRoots.push(normalizedRoot);
continue;
}
roots.push(normalizedRoot);
const absoluteRoot = path.join(root, normalizedRoot);
const rootStat = await stat(absoluteRoot);
if (rootStat.isFile()) {
if (isSourceFile(absoluteRoot) && !isGeneratedOrIgnoredFile(absoluteRoot) && !isTestFile(normalizedRoot) && !isExcludedRuntimePath(normalizedRoot)) {
files.push(absoluteRoot);
}
continue;
}
await walk(absoluteRoot);
}
files.sort((a, b) => toRelativePath(root, a).localeCompare(toRelativePath(root, b)));
return { roots, missingRoots, files };
}
export function findForbiddenPluginImports(sourceText, relativeFilePath) {
const diagnostics = [];
const lines = sourceText.split(/\r?\n/);
lines.forEach((line, index) => {
for (const { id, pattern } of forbiddenPatterns) {
pattern.lastIndex = 0;
if (!pattern.test(line)) continue;
if (isAllowedPluginMetadataReference(relativeFilePath, line)) continue;
diagnostics.push({ file: normalizeRelativePath(relativeFilePath), line: index + 1, id, text: line.trim() });
}
});
return diagnostics;
}
export async function scanForbiddenPluginImports(root = repoRoot, scanRoots = runtimeScanRoots) {
const collection = await collectSourceFiles(root, scanRoots);
const diagnostics = [];
for (const file of collection.files) {
const relativeFilePath = toRelativePath(root, file);
const sourceText = await readFile(file, "utf8");
diagnostics.push(...findForbiddenPluginImports(sourceText, relativeFilePath));
}
return { ...collection, diagnostics };
}
function excerptOutput(output, maxLength = 2400) {
const normalized = output.trim();
if (!normalized) return "(no output)";
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, maxLength)}\n... output truncated ...`;
}
export function runBuild(root = repoRoot) {
return new Promise((resolve) => {
const startedAt = Date.now();
const child = spawn(buildCommand.command, buildCommand.args, { cwd: root, env: { ...process.env, CI: "1" }, shell: false });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", (error) => {
stderr += `\n${error.message}`;
});
child.on("close", (code) => {
resolve({
command: [buildCommand.command, ...buildCommand.args].join(" "),
exitCode: code ?? 1,
durationMs: Date.now() - startedAt,
stdout,
stderr,
outputExcerpt: excerptOutput(`${stdout}\n${stderr}`),
});
});
});
}
export function renderSmokeReport({ buildResult, scanResult }) {
const diagnostics = scanResult.diagnostics ?? [];
const lines = [
"Feature Manifest Core Build Without Plugins Smoke",
`Build Command: ${buildResult.command}`,
`Build Exit Code: ${buildResult.exitCode}`,
`Build Duration Ms: ${buildResult.durationMs}`,
`Scanned Roots: ${scanResult.roots.join(", ") || "(none)"}`,
`Missing Roots Skipped: ${scanResult.missingRoots.join(", ") || "(none)"}`,
`Scanned File Count: ${scanResult.files.length}`,
`Forbidden Import Count: ${diagnostics.length}`,
];
if (buildResult.exitCode !== 0) {
lines.push("Build Output Excerpt:", buildResult.outputExcerpt);
}
lines.push("Forbidden Import Diagnostics:");
if (diagnostics.length === 0) {
lines.push("(none)");
} else {
for (const diagnostic of diagnostics.slice(0, 20)) {
lines.push(`${diagnostic.file}:${diagnostic.line}: [${diagnostic.id}] ${diagnostic.text}`);
}
if (diagnostics.length > 20) lines.push(`... ${diagnostics.length - 20} additional forbidden imports omitted`);
}
return `${lines.join("\n")}\n`;
}
export async function runSmoke(root = repoRoot) {
const buildResult = await runBuild(root);
const scanResult = await scanForbiddenPluginImports(root, runtimeScanRoots);
return { buildResult, scanResult, report: renderSmokeReport({ buildResult, scanResult }) };
}
async function main() {
const result = await runSmoke(repoRoot);
process.stdout.write(result.report);
if (result.buildResult.exitCode !== 0) {
process.exitCode = result.buildResult.exitCode;
return;
}
process.exitCode = result.scanResult.diagnostics.length > 0 ? 1 : 0;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error?.stack ?? error);
process.exitCode = 1;
});
}