#!/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; }); }