chore(ci): add workspace and db guardrails
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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 = ["packages", "tooling"];
|
||||
const violations = [];
|
||||
|
||||
async function pathExists(targetPath) {
|
||||
try {
|
||||
await stat(targetPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function listPackageJsonFiles(baseDir) {
|
||||
const absoluteBaseDir = path.join(rootDir, baseDir);
|
||||
if (!(await pathExists(absoluteBaseDir))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = await readdir(absoluteBaseDir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(baseDir, entry.name, "package.json"));
|
||||
}
|
||||
|
||||
function collectExportTargets(value, keyPath, targets) {
|
||||
if (typeof value === "string") {
|
||||
targets.push({ keyPath, target: value });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, nestedValue] of Object.entries(value)) {
|
||||
collectExportTargets(nestedValue, `${keyPath}.${key}`, targets);
|
||||
}
|
||||
}
|
||||
|
||||
function getWildcardBaseDir(target) {
|
||||
const wildcardIndex = target.indexOf("*");
|
||||
if (wildcardIndex === -1) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const targetPrefix = target.slice(0, wildcardIndex);
|
||||
const lastSlashIndex = targetPrefix.lastIndexOf("/");
|
||||
return lastSlashIndex === -1 ? "." : targetPrefix.slice(0, lastSlashIndex);
|
||||
}
|
||||
|
||||
const packageJsonFiles = (
|
||||
await Promise.all(workspaceDirs.map((workspaceDir) => listPackageJsonFiles(workspaceDir)))
|
||||
).flat();
|
||||
|
||||
for (const packageJsonFile of packageJsonFiles) {
|
||||
const packageJsonPath = path.join(rootDir, packageJsonFile);
|
||||
if (!(await pathExists(packageJsonPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||
const exportsField = packageJson.exports;
|
||||
|
||||
if (!exportsField || typeof exportsField !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const exportTargets = [];
|
||||
collectExportTargets(exportsField, "exports", exportTargets);
|
||||
|
||||
for (const exportTarget of exportTargets) {
|
||||
const exportPath = exportTarget.target;
|
||||
const containsWildcard = exportPath.includes("*");
|
||||
const relativeCheckPath = containsWildcard ? getWildcardBaseDir(exportPath) : exportPath;
|
||||
const absoluteCheckPath = path.resolve(packageDir, relativeCheckPath);
|
||||
const relativeFromRoot = path.relative(rootDir, absoluteCheckPath) || ".";
|
||||
|
||||
if (!relativeFromRoot || relativeFromRoot.startsWith("..")) {
|
||||
violations.push(
|
||||
`${packageJsonFile}: ${exportTarget.keyPath} points outside the repository: ${exportPath}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(await pathExists(absoluteCheckPath))) {
|
||||
violations.push(
|
||||
`${packageJsonFile}: ${exportTarget.keyPath} references a missing path: ${exportPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Workspace export check failed:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Workspace exports passed.");
|
||||
Reference in New Issue
Block a user