chore(ci): add workspace and db guardrails
This commit is contained in:
@@ -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.");
|
||||
Reference in New Issue
Block a user