Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-prisma-ownership.mjs
T

228 lines
6.5 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const EXPECTED_SCHEMA_OUTPUT = "../../packages/db/generated/prisma/client";
export const PACKAGE_GENERATED_ENTRYPOINT_PATTERN = /(?:from\s+["']|export\s+\*\s+from\s+["'])\.\.\/generated\/prisma\/client\/(?:client|browser)\.(?:ts|js)["'];?/;
const DEFAULT_IGNORED_DIRECTORIES = new Set([
".git",
".tc",
".turbo",
".next",
".react-router",
".vite",
"build",
"coverage",
"dist",
"generated",
"node_modules",
"vendor",
]);
const DEFAULT_TEXT_EXTENSIONS = new Set([
".cjs",
".css",
".js",
".json",
".jsx",
".md",
".mdx",
".mjs",
".prisma",
".ts",
".tsx",
".txt",
".yaml",
".yml",
]);
const FORBIDDEN_TEXT_PATTERNS = [
{
label: "root/app Prisma generated-client path",
pattern: /app\/generated\/prisma/g,
},
{
label: "root-local generated-client import",
pattern: /(?:from|import)\s*\(?\s*["'][^"']*(?:^|\/)app\/generated\/prisma[^"']*["']/g,
},
{
label: "app-local generated Prisma client import",
pattern: /(?:from|import)\s*\(?\s*["'](?:~\/generated\/prisma\/client\/(?:browser|client)|(?:~\/)?generated\/prisma\/client\/(?:browser|client))["']/g,
},
];
function normalizeRelativePath(relativePath) {
return relativePath.split(path.sep).join("/");
}
function isIgnoredDirectory(name) {
return DEFAULT_IGNORED_DIRECTORIES.has(name) || name.endsWith(".cache") || name === "cache";
}
function isScannableTextFile(filePath) {
return DEFAULT_TEXT_EXTENSIONS.has(path.extname(filePath));
}
function readTextFile(rootDir, relativePath) {
return fs.readFileSync(path.join(rootDir, relativePath), "utf8");
}
export function listRepositoryTextFiles(rootDir, directory = ".") {
const absoluteDirectory = path.join(rootDir, directory);
const entries = fs.readdirSync(absoluteDirectory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const relativePath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (!isIgnoredDirectory(entry.name)) {
files.push(...listRepositoryTextFiles(rootDir, relativePath));
}
continue;
}
if (entry.isFile() && isScannableTextFile(entry.name)) {
files.push(normalizeRelativePath(relativePath === "." ? entry.name : relativePath));
}
}
return files.sort();
}
export function checkSchemaOutput(rootDir) {
const schemaPath = "prisma/schema/schema.prisma";
const failures = [];
const passes = [];
if (!fs.existsSync(path.join(rootDir, schemaPath))) {
return { passes, failures: [`missing Prisma schema: ${schemaPath}`] };
}
const schema = readTextFile(rootDir, schemaPath);
const outputMatch = schema.match(/^\s*output\s*=\s*"([^"]+)"\s*$/m);
if (!outputMatch) {
failures.push(`${schemaPath}: generator client must declare output = "${EXPECTED_SCHEMA_OUTPUT}"`);
} else if (outputMatch[1] !== EXPECTED_SCHEMA_OUTPUT) {
failures.push(`${schemaPath}: expected generator output "${EXPECTED_SCHEMA_OUTPUT}", got "${outputMatch[1]}"`);
} else {
passes.push(`${schemaPath}: generator output is package-owned`);
}
if (/\.\.\/\.\.\/app\/generated\/prisma|app\/generated\/prisma/.test(schema)) {
failures.push(`${schemaPath}: must not reference old root/app generated Prisma ownership`);
}
return { passes, failures };
}
export function checkPackageBoundaryImports(rootDir) {
const boundaryFiles = [
"packages/db/src/client.server.ts",
"packages/db/src/prisma-browser.ts",
];
const passes = [];
const failures = [];
for (const relativePath of boundaryFiles) {
const absolutePath = path.join(rootDir, relativePath);
if (!fs.existsSync(absolutePath)) {
failures.push(`missing package boundary file: ${relativePath}`);
continue;
}
const source = fs.readFileSync(absolutePath, "utf8");
if (PACKAGE_GENERATED_ENTRYPOINT_PATTERN.test(source)) {
passes.push(`${relativePath}: uses package-local Prisma 7 TypeScript generated entrypoint`);
} else {
failures.push(`${relativePath}: must import/export ../generated/prisma/client/client.ts or ../generated/prisma/client/browser.ts`);
}
if (/app\/generated\/prisma|\.\.\/\.\.\/.*generated\/prisma/.test(source)) {
failures.push(`${relativePath}: must not cross out to root/app generated Prisma ownership`);
}
}
return { passes, failures };
}
export function scanForbiddenPrismaOwnershipText(rootDir) {
const passes = [];
const failures = [];
const files = listRepositoryTextFiles(rootDir);
for (const relativePath of files) {
if (
relativePath === "scripts/verify-prisma-ownership.mjs" ||
relativePath === "scripts/verify-prisma-ownership.test.mjs" ||
relativePath === "scripts/verify-db-boundary-eslint.test.mjs"
) {
continue;
}
const source = readTextFile(rootDir, relativePath);
for (const { label, pattern } of FORBIDDEN_TEXT_PATTERNS) {
pattern.lastIndex = 0;
if (pattern.test(source)) {
failures.push(`${relativePath}: forbidden ${label}`);
}
}
}
if (failures.length === 0) {
passes.push("repository text scan: no forbidden root/app Prisma generated-client ownership references found");
}
return { passes, failures };
}
export function verifyPrismaOwnership(rootDir = process.cwd()) {
const checks = [
checkSchemaOutput(rootDir),
checkPackageBoundaryImports(rootDir),
scanForbiddenPrismaOwnershipText(rootDir),
];
return checks.reduce(
(result, check) => ({
passes: [...result.passes, ...check.passes],
failures: [...result.failures, ...check.failures],
}),
{ passes: [], failures: [] },
);
}
export function formatVerificationResult(result) {
if (result.failures.length > 0) {
return [
"FAIL Prisma ownership verification",
...result.failures.map((failure) => `- ${failure}`),
`Summary: ${result.passes.length} passed, ${result.failures.length} failed`,
"",
].join("\n");
}
return [
"PASS Prisma ownership verification",
...result.passes.map((pass) => `- ${pass}`),
`Summary: ${result.passes.length} passed, 0 failed`,
"",
].join("\n");
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = verifyPrismaOwnership(process.cwd());
const output = formatVerificationResult(result);
if (result.failures.length > 0) {
process.stderr.write(output);
process.exitCode = 1;
} else {
process.stdout.write(output);
}
}