Files
SnapOtter/scripts/check-license-boundary.mjs

53 lines
2.0 KiB
JavaScript

#!/usr/bin/env node
// Enforces D15: core never reaches into packages/enterprise internals,
// and packages/enterprise never imports from apps/* (it must stay standalone).
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
const ROOT = process.cwd();
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".turbo", "coverage"]);
function* walk(dir) {
for (const entry of readdirSync(dir)) {
if (SKIP_DIRS.has(entry)) continue;
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
yield* walk(full);
} else if (/\.(ts|tsx|js|mjs)$/.test(entry)) {
yield full;
}
}
}
const ENTERPRISE_DIR = join(ROOT, "packages/enterprise");
// Catches static `from "..."`, side-effect `import "..."`, and dynamic `import("...")`
// forms. The bare package entry "@snapotter/enterprise" (no trailing slash) stays
// allowed; only reaching INTO the package is a violation.
const INTERNAL_IMPORT =
/(from|import)\s*[\s(]\s*["'](@snapotter\/enterprise\/|[./]*packages\/enterprise\/)/;
const APP_IMPORT = /(from|import)\s*[\s(]\s*["'][^"']*apps\/(api|web)\//;
const violations = [];
for (const root of [join(ROOT, "apps"), join(ROOT, "packages")]) {
for (const file of walk(root)) {
const insideEnterprise = file.startsWith(ENTERPRISE_DIR);
const text = readFileSync(file, "utf8");
for (const [lineNo, line] of text.split("\n").entries()) {
if (!insideEnterprise && INTERNAL_IMPORT.test(line)) {
violations.push(`core imports enterprise internals: ${relative(ROOT, file)}:${lineNo + 1}`);
}
if (insideEnterprise && APP_IMPORT.test(line)) {
violations.push(`enterprise imports app code: ${relative(ROOT, file)}:${lineNo + 1}`);
}
}
}
}
if (violations.length > 0) {
console.error("License boundary violations (D15):");
for (const v of violations) console.error(` ${v}`);
process.exit(1);
}
console.log("License boundary check passed.");