chore(ci): add workspace and db guardrails

This commit is contained in:
2026-03-31 22:36:12 +02:00
parent cb8669c489
commit 0b192efdb1
10 changed files with 563 additions and 38 deletions
+150
View File
@@ -0,0 +1,150 @@
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { resolveRealWorkspaceRoot } from "./load-env.mjs";
const rootDir = resolveRealWorkspaceRoot();
const workspaceDirs = ["apps", "packages", "tooling"];
const sourceFileExtensions = new Set([
".ts",
".tsx",
".mts",
".cts",
".js",
".jsx",
".mjs",
".cjs",
]);
const resolutionExtensions = [
"",
".ts",
".tsx",
".mts",
".cts",
".js",
".jsx",
".mjs",
".cjs",
];
const violations = [];
const importPattern =
/(?:import|export)\s+(?:[^"'`]*?\s+from\s+)?["'`](\.{1,2}\/[^"'`]+)["'`]|import\s*\(\s*["'`](\.{1,2}\/[^"'`]+)["'`]\s*\)/g;
async function pathExists(targetPath) {
try {
await stat(targetPath);
return true;
} catch {
return false;
}
}
async function collectSourceFiles(directoryPath, result) {
const entries = await readdir(directoryPath, { withFileTypes: true });
for (const entry of entries) {
const absolutePath = path.join(directoryPath, entry.name);
if (entry.isDirectory()) {
if (
entry.name === "node_modules"
|| entry.name.startsWith(".next")
|| entry.name === ".turbo"
|| entry.name === "dist"
|| entry.name === "coverage"
|| entry.name === "playwright-report"
|| entry.name === "test-results"
) {
continue;
}
await collectSourceFiles(absolutePath, result);
continue;
}
if (!entry.isFile()) {
continue;
}
const extension = path.extname(entry.name);
if (sourceFileExtensions.has(extension)) {
result.push(absolutePath);
}
}
}
function getExtensionlessCandidates(basePath) {
return resolutionExtensions.flatMap((extension) => [
`${basePath}${extension}`,
path.join(basePath, `index${extension}`),
]);
}
function getExplicitExtensionCandidates(basePath, importExtension) {
const baseWithoutExtension = basePath.slice(0, -importExtension.length);
const siblingExtensions =
importExtension === ".js" || importExtension === ".mjs" || importExtension === ".cjs"
? [importExtension, ".ts", ".tsx", ".mts", ".cts"]
: [importExtension];
return siblingExtensions.flatMap((extension) => [
`${baseWithoutExtension}${extension}`,
path.join(baseWithoutExtension, `index${extension}`),
]);
}
async function resolvesImport(fromFilePath, importPath) {
const resolvedBasePath = path.resolve(path.dirname(fromFilePath), importPath);
const rawExtension = path.extname(importPath);
const importExtension = resolutionExtensions.includes(rawExtension) ? rawExtension : "";
const candidates = importExtension
? getExplicitExtensionCandidates(resolvedBasePath, importExtension)
: getExtensionlessCandidates(resolvedBasePath);
for (const candidate of candidates) {
if (await pathExists(candidate)) {
return true;
}
}
return false;
}
const sourceFiles = [];
for (const workspaceDir of workspaceDirs) {
const absoluteWorkspaceDir = path.join(rootDir, workspaceDir);
if (!(await pathExists(absoluteWorkspaceDir))) {
continue;
}
await collectSourceFiles(absoluteWorkspaceDir, sourceFiles);
}
for (const sourceFile of sourceFiles) {
const content = await readFile(sourceFile, "utf8");
const relativeSourceFile = path.relative(rootDir, sourceFile);
const imports = new Set();
for (const match of content.matchAll(importPattern)) {
const importPath = match[1] ?? match[2];
if (importPath) {
imports.add(importPath.trim());
}
}
for (const importPath of imports) {
if (!(await resolvesImport(sourceFile, importPath))) {
violations.push(`${relativeSourceFile}: unresolved relative import ${importPath}`);
}
}
}
if (violations.length > 0) {
console.error("Workspace import check failed:");
for (const violation of violations) {
console.error(`- ${violation}`);
}
process.exit(1);
}
console.log("Workspace imports passed.");