Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-ci-baseline.mjs
T

274 lines
11 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const REQUIRED_PACKAGE_MANAGER = "pnpm@10.33.0";
const REQUIRED_SCRIPTS = ["typecheck", "lint", "test", "test:packages", "test:mobile", "build", "build:ci"];
const REQUIRED_FILES = ["package.json", "pnpm-lock.yaml", ".eslintrc"];
const REQUIRED_SCRIPT_BODIES = {
build: "pnpm --filter @toolctrl/db prisma:generate && pnpm --filter @toolctrl/db build && pnpm --filter @toolctrl/web build",
"build:ci": "pnpm run build",
"verify:i18n": "tsx scripts/i18n-check.ts && node scripts/verify-hardcoded-strings.mjs && eslint --max-warnings=0 --ext .tsx,.ts packages/ui-app/src packages/ui-subframe/src packages/ui-mock/src",
"verify:ui-hot-swap": "tsx scripts/verify-ui-hot-swap.ts",
"test:hot-swap": "pnpm exec vitest run scripts/verify-ui-hot-swap.test.ts && pnpm run verify:ui-hot-swap",
"verify:test-coverage": "node scripts/verify-test-coverage.mjs",
"verify:sidebar-routes": "pnpm exec vitest run apps/web/app/application/sidebar/AdminSidebar.test.tsx apps/web/app/application/sidebar/AppSidebar.test.tsx apps/web/app/components/layouts/sidebars/shadcn/app-sidebar.test.tsx apps/web/app/routes/admin/admin-route-targets.test.ts apps/web/app/routes.test.ts",
"verify:a11y-coverage": "node scripts/verify-a11y-coverage.mjs",
"test:a11y": "pnpm exec vitest run apps/web/app/components/ui/button.a11y.test.tsx apps/web/app/components/ui/errors/ErrorPage.a11y.test.tsx apps/web/app/components/ui/*.axe.test.tsx apps/web/app/components/core/SkipToContent.test.tsx apps/web/app/components/aceternity/HoverEffect.a11y.test.tsx apps/web/app/components/ui/lists/ViewToggle.a11y.test.tsx apps/web/app/components/ui/lists/ViewToggleWithUrl.a11y.test.tsx apps/web/app/components/ui/tabs/TabsWithIcons.a11y.test.tsx scripts/verify-color-contrast.test.mjs",
"test:mobile": "pnpm --filter @toolctrl/mobile test",
};
const PHASE_C_UI_GLOBS = ["packages/ui-app/src", "packages/ui-subframe/src", "packages/ui-mock/src"];
const PHASE_C_I18N_SELECTOR_SNIPPETS = [
"Property[key.name=/^(title|label|placeholder|ariaLabel|alt|description|message|heading|subject)$/] > Literal",
"Property[key.value=/^(title|label|placeholder|aria-label|ariaLabel|alt|description|message|heading|subject)$/] > Literal",
"Property[key.name=/^(title|label|placeholder|ariaLabel|alt|description|message|heading|subject)$/] > TemplateLiteral",
"Property[key.value=/^(title|label|placeholder|aria-label|ariaLabel|alt|description|message|heading|subject)$/] > TemplateLiteral",
"JSXText",
"JSXAttribute[name.name=/^(aria-label|title|placeholder)$/] > Literal",
];
function readJsonFile(rootDir, relativePath) {
return JSON.parse(fs.readFileSync(path.join(rootDir, relativePath), "utf8"));
}
function checkFileExists(rootDir, relativePath) {
return fs.existsSync(path.join(rootDir, relativePath));
}
function hasScript(manifest, scriptName) {
return typeof manifest.scripts?.[scriptName] === "string" && manifest.scripts[scriptName].trim().length > 0;
}
function isWarnRule(ruleValue) {
if (ruleValue === "warn" || ruleValue === 1) {
return true;
}
return Array.isArray(ruleValue) && (ruleValue[0] === "warn" || ruleValue[0] === 1);
}
export function checkRequiredFiles(rootDir) {
const passes = [];
const failures = [];
for (const relativePath of REQUIRED_FILES) {
if (checkFileExists(rootDir, relativePath)) {
passes.push(`${relativePath}: exists`);
} else {
failures.push(`${relativePath}: missing required CI baseline file`);
}
}
return { passes, failures, observations: [] };
}
export function checkPackageManagerAndScripts(rootDir) {
const relativePath = "package.json";
const passes = [];
const failures = [];
if (!checkFileExists(rootDir, relativePath)) {
return { passes, failures: [`${relativePath}: missing required CI baseline file`], observations: [] };
}
let manifest;
try {
manifest = readJsonFile(rootDir, relativePath);
} catch (error) {
return { passes, failures: [`${relativePath}: failed to read manifest JSON: ${error.message}`], observations: [] };
}
if (manifest.packageManager === REQUIRED_PACKAGE_MANAGER) {
passes.push(`${relativePath}: packageManager is ${REQUIRED_PACKAGE_MANAGER}`);
} else {
failures.push(`${relativePath}: packageManager must be exactly ${REQUIRED_PACKAGE_MANAGER}`);
}
for (const scriptName of REQUIRED_SCRIPTS) {
if (hasScript(manifest, scriptName)) {
passes.push(`${relativePath}: script "${scriptName}" is defined`);
} else {
failures.push(`${relativePath}: missing required script "${scriptName}"`);
}
}
for (const [scriptName, expectedCommand] of Object.entries(REQUIRED_SCRIPT_BODIES)) {
const actualCommand = manifest.scripts?.[scriptName];
if (actualCommand === expectedCommand) {
passes.push(`${relativePath}: script "${scriptName}" matches the required CI command`);
} else if (typeof actualCommand === "string" && actualCommand.trim().length > 0) {
failures.push(`${relativePath}: script "${scriptName}" must be exactly "${expectedCommand}"`);
} else {
failures.push(`${relativePath}: missing required script "${scriptName}"`);
}
}
if (hasScript(manifest, "lint")) {
const lintCommand = manifest.scripts.lint;
if (/\beslint\b/.test(lintCommand)) {
passes.push(`${relativePath}: script "lint" invokes ESLint`);
} else {
failures.push(`${relativePath}: script "lint" must invoke ESLint`);
}
if (/\s--max-warnings=0(?:\s|$)/.test(` ${lintCommand} `)) {
passes.push(`${relativePath}: script "lint" fails on ESLint warnings`);
} else {
failures.push(`${relativePath}: script "lint" must include --max-warnings=0`);
}
}
if (hasScript(manifest, "verify:i18n")) {
const i18nCommand = manifest.scripts["verify:i18n"];
if (/\beslint\b/.test(i18nCommand) && PHASE_C_UI_GLOBS.every((uiGlob) => i18nCommand.includes(uiGlob))) {
passes.push(`${relativePath}: script "verify:i18n" runs ESLint over Phase C UI packages`);
} else {
failures.push(`${relativePath}: script "verify:i18n" must run ESLint over Phase C UI packages`);
}
if (i18nCommand.includes("--no-error-on-unmatched-pattern")) {
failures.push(`${relativePath}: script "verify:i18n" must not include --no-error-on-unmatched-pattern`);
} else {
passes.push(`${relativePath}: script "verify:i18n" fails when configured UI lint roots disappear`);
}
if (/\s--max-warnings=0(?:\s|$)/.test(` ${i18nCommand} `)) {
passes.push(`${relativePath}: script "verify:i18n" fails on ESLint warnings`);
} else {
failures.push(`${relativePath}: script "verify:i18n" must include --max-warnings=0`);
}
} else {
failures.push(`${relativePath}: missing required script "verify:i18n"`);
}
return { passes, failures, observations: [] };
}
export function checkPhaseCUiLintRoots(rootDir) {
const passes = [];
const failures = [];
for (const uiRoot of PHASE_C_UI_GLOBS) {
const absoluteRoot = path.join(rootDir, uiRoot);
if (!fs.existsSync(absoluteRoot) || !fs.statSync(absoluteRoot).isDirectory()) {
failures.push(`${uiRoot}: missing Phase C UI lint root`);
continue;
}
const lintableFiles = listLintableFiles(absoluteRoot);
if (lintableFiles.length === 0) {
failures.push(`${uiRoot}: Phase C UI lint root contains no TypeScript files`);
} else {
passes.push(`${uiRoot}: Phase C UI lint root contains ${lintableFiles.length} TypeScript file(s)`);
}
}
return { passes, failures, observations: [] };
}
function listLintableFiles(absoluteRoot) {
const files = [];
for (const entry of fs.readdirSync(absoluteRoot, { withFileTypes: true })) {
const absolutePath = path.join(absoluteRoot, entry.name);
if (entry.isDirectory()) {
files.push(...listLintableFiles(absolutePath));
} else if (/\.(?:ts|tsx)$/.test(entry.name)) {
files.push(absolutePath);
}
}
return files;
}
export function checkEslintBaseline(rootDir) {
const relativePath = ".eslintrc";
const passes = [];
const failures = [];
if (!checkFileExists(rootDir, relativePath)) {
return { passes, failures: [`${relativePath}: missing required CI baseline file`], observations: [] };
}
let eslintConfig;
try {
eslintConfig = readJsonFile(rootDir, relativePath);
} catch (error) {
return { passes, failures: [`${relativePath}: failed to read ESLint config JSON: ${error.message}`], observations: [] };
}
const rules = eslintConfig.rules ?? {};
const requiredWarnRules = ["no-console", "@typescript-eslint/no-unused-vars"];
for (const ruleName of requiredWarnRules) {
if (isWarnRule(rules[ruleName])) {
passes.push(`${relativePath}: rule "${ruleName}" is warn`);
} else {
failures.push(`${relativePath}: rule "${ruleName}" must be warn for the current CI baseline`);
}
}
const restrictedSyntaxSelectors = getPhaseCRestrictedSyntaxSelectors(eslintConfig);
for (const selectorSnippet of PHASE_C_I18N_SELECTOR_SNIPPETS) {
if (restrictedSyntaxSelectors.some((selector) => selector.includes(selectorSnippet))) {
passes.push(`${relativePath}: Phase C i18n ESLint selector "${selectorSnippet}" is enforced`);
} else {
failures.push(`${relativePath}: missing Phase C i18n ESLint selector "${selectorSnippet}"`);
}
}
return { passes, failures, observations: [] };
}
function getPhaseCRestrictedSyntaxSelectors(eslintConfig) {
const selectors = [];
for (const override of eslintConfig.overrides ?? []) {
const files = Array.isArray(override.files) ? override.files : [override.files].filter(Boolean);
const targetsPhaseCUi = PHASE_C_UI_GLOBS.every((uiGlob) => files.some((filePattern) => String(filePattern).includes(uiGlob)));
const rule = override.rules?.["no-restricted-syntax"];
if (!targetsPhaseCUi || !Array.isArray(rule) || rule[0] !== "error") {
continue;
}
for (const entry of rule.slice(1)) {
if (entry && typeof entry.selector === "string") {
selectors.push(entry.selector);
}
}
}
return selectors;
}
export function verifyCiBaseline(rootDir = process.cwd()) {
const checks = [checkRequiredFiles(rootDir), checkPackageManagerAndScripts(rootDir), checkEslintBaseline(rootDir), checkPhaseCUiLintRoots(rootDir)];
return checks.reduce(
(result, check) => ({
passes: [...result.passes, ...check.passes],
failures: [...result.failures, ...check.failures],
observations: [...result.observations, ...check.observations],
}),
{ passes: [], failures: [], observations: [] },
);
}
export function formatCiBaselineResult(result) {
const status = result.failures.length > 0 ? "FAIL" : "PASS";
return [
`${status} CI baseline verification`,
...result.passes.map((pass) => `PASS ${pass}`),
...result.observations.map((observation) => `OBSERVE ${observation}`),
...result.failures.map((failure) => `FAIL ${failure}`),
`Summary: ${result.passes.length} passed, ${result.observations.length} observed, ${result.failures.length} failed`,
"",
].join("\n");
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = verifyCiBaseline(process.cwd());
const output = formatCiBaselineResult(result);
if (result.failures.length > 0) {
process.stderr.write(output);
process.exitCode = 1;
} else {
process.stdout.write(output);
}
}