Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-db-legacy-markup.mjs
T

172 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const DEFAULT_DB_LEGACY_DIRECTORY = "apps/web/app/utils/db";
const REACT_IMPORT_PATTERN = /(?:^|[\n;])\s*import(?:\s+type)?(?:[\s\S]*?\s+from\s*)?["']react(?:\/[^"']*)?["']/g;
const ATTRIBUTE_LIKE_MARKUP_TAG_PATTERN = /<\s*\/?\s*[A-Za-z][A-Za-z0-9.:-]*(?:\s+[A-Za-z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|\{[^}]*\}))?)+\s*\/?>/g;
const LOWERCASE_HTML_LIKE_TAG_PATTERN = /<\s*\/?\s*[a-z][A-Za-z0-9.:-]*\s*\/?>/g;
const STRING_LITERAL_PATTERN = /`(?:\\[\s\S]|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g;
const TYPE_CAST_TAG_PATTERN = /<\s*(?:const|keyof|infer|readonly|unique|abstract|new|typeof|[A-Z][A-Za-z0-9_$]*(?:\s+extends\s+[^=>]+)?)(?:\s*,[^>]*)?>\s*(?=[A-Za-z_$({[])/g;
const TYPE_RETURN_TAG_PATTERN = /(?<=\bPromise\s*)<[^>]+>|(?<=\b(?:Partial|Pick|Omit|Record|Required|Readonly|Exclude|Extract|NonNullable|Awaited|Array|Set|Map)\s*)<[^>]+>/g;
const FORBIDDEN_SOURCE_PATTERNS = [
{
reason: "forbidden dangerouslySetInnerHTML marker",
pattern: /\bdangerouslySetInnerHTML\b/g,
},
{
reason: "forbidden JSX.Element type",
pattern: /\bJSX\.Element\b/g,
},
{
reason: "forbidden ReactNode type",
pattern: /\bReactNode\b/g,
},
{
reason: "forbidden ReactElement type",
pattern: /\bReactElement\b/g,
},
{
reason: "forbidden JSXFragment type",
pattern: /\bJSXFragment\b/g,
},
];
function stripStringLiterals(source) {
return source.replace(STRING_LITERAL_PATTERN, (literal) => " ".repeat(literal.length));
}
function stripTypeOnlyTags(source) {
return source
.replace(TYPE_CAST_TAG_PATTERN, (tag) => " ".repeat(tag.length))
.replace(TYPE_RETURN_TAG_PATTERN, (tag) => " ".repeat(tag.length));
}
function hasJsxLikeMarkupTag(source) {
ATTRIBUTE_LIKE_MARKUP_TAG_PATTERN.lastIndex = 0;
if (ATTRIBUTE_LIKE_MARKUP_TAG_PATTERN.test(source)) {
return true;
}
LOWERCASE_HTML_LIKE_TAG_PATTERN.lastIndex = 0;
let match;
while ((match = LOWERCASE_HTML_LIKE_TAG_PATTERN.exec(source))) {
const afterTag = source.slice(LOWERCASE_HTML_LIKE_TAG_PATTERN.lastIndex).trimStart();
if (match[0].includes("/") || afterTag.startsWith("</")) {
return true;
}
}
return false;
}
function normalizeRelativePath(relativePath) {
return relativePath.split(path.sep).join("/");
}
function hasReadableDirectory(rootDir, relativeDirectory) {
return fs.existsSync(path.join(rootDir, relativeDirectory));
}
export function listDbLegacyFiles(rootDir, relativeDirectory = DEFAULT_DB_LEGACY_DIRECTORY) {
const absoluteDirectory = path.join(rootDir, relativeDirectory);
if (!fs.existsSync(absoluteDirectory)) {
return [];
}
const entries = fs.readdirSync(absoluteDirectory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const relativePath = path.join(relativeDirectory, entry.name);
if (entry.isDirectory()) {
files.push(...listDbLegacyFiles(rootDir, relativePath));
continue;
}
if (entry.isFile() && [".ts", ".tsx"].includes(path.extname(entry.name))) {
files.push(normalizeRelativePath(relativePath));
}
}
return files.sort();
}
export function findDbLegacyMarkupFailures(rootDir = process.cwd(), relativeDirectory = DEFAULT_DB_LEGACY_DIRECTORY) {
const failures = [];
if (!hasReadableDirectory(rootDir, relativeDirectory)) {
failures.push(`${normalizeRelativePath(relativeDirectory)}: missing DB legacy directory`);
return failures;
}
for (const relativePath of listDbLegacyFiles(rootDir, relativeDirectory)) {
if (relativePath.endsWith(".tsx")) {
failures.push(`${relativePath}: forbidden .tsx file in server-only DB legacy boundary`);
}
if (!relativePath.endsWith(".ts")) {
continue;
}
const source = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
const sourceWithoutStrings = stripTypeOnlyTags(stripStringLiterals(source));
REACT_IMPORT_PATTERN.lastIndex = 0;
if (REACT_IMPORT_PATTERN.test(source)) {
failures.push(`${relativePath}: forbidden React import in server-only DB legacy boundary`);
}
for (const { reason, pattern } of FORBIDDEN_SOURCE_PATTERNS) {
pattern.lastIndex = 0;
if (pattern.test(source)) {
failures.push(`${relativePath}: ${reason}`);
}
}
if (hasJsxLikeMarkupTag(sourceWithoutStrings)) {
failures.push(`${relativePath}: forbidden JSX-like markup tag`);
}
}
return failures.sort();
}
export function verifyDbLegacyMarkup(rootDir = process.cwd(), relativeDirectory = DEFAULT_DB_LEGACY_DIRECTORY) {
const files = listDbLegacyFiles(rootDir, relativeDirectory);
const failures = findDbLegacyMarkupFailures(rootDir, relativeDirectory);
return { filesScanned: files.length, failures };
}
export function formatDbLegacyMarkupResult(result) {
if (result.failures.length > 0) {
return [
"FAIL DB legacy markup verification",
...result.failures.map((failure) => `- ${failure}`),
`Summary: ${result.filesScanned} files scanned, ${result.failures.length} failed`,
"",
].join("\n");
}
return [
"PASS DB legacy markup verification",
`Summary: ${result.filesScanned} files scanned, 0 failed`,
"",
].join("\n");
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = verifyDbLegacyMarkup(process.cwd());
const output = formatDbLegacyMarkupResult(result);
if (result.failures.length > 0) {
process.stderr.write(output);
process.exitCode = 1;
} else {
process.stdout.write(output);
}
}