162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
|
|
const PKG_DIR = resolve(__dirname, "..");
|
|
const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8")) as {
|
|
dependencies?: Record<string, string>;
|
|
devDependencies?: Record<string, string>;
|
|
exports?: Record<string, unknown>;
|
|
};
|
|
|
|
const ALLOWED_DEPS = new Set([
|
|
"@opentelemetry/api",
|
|
"@suaveplan/api-types",
|
|
"@suaveplan/error",
|
|
"@suaveplan/logger",
|
|
"@suaveplan/rules",
|
|
"tslib",
|
|
"zod",
|
|
]);
|
|
|
|
function walk(dir: string, out: string[] = []): string[] {
|
|
for (const name of readdirSync(dir)) {
|
|
const p = join(dir, name);
|
|
const st = statSync(p);
|
|
if (st.isDirectory()) {
|
|
if (name === "node_modules" || name === "dist") continue;
|
|
walk(p, out);
|
|
} else {
|
|
out.push(p);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe("dep-graph", () => {
|
|
it("dependencies match the allowlist", { timeout: 5000 }, () => {
|
|
const deps = Object.keys(pkg.dependencies ?? {});
|
|
for (const d of deps) {
|
|
expect(ALLOWED_DEPS.has(d)).toBe(true);
|
|
}
|
|
for (const expected of [
|
|
"@opentelemetry/api",
|
|
"@suaveplan/api-types",
|
|
"@suaveplan/error",
|
|
"@suaveplan/logger",
|
|
"@suaveplan/rules",
|
|
"tslib",
|
|
"zod",
|
|
]) {
|
|
expect(deps).toContain(expected);
|
|
}
|
|
});
|
|
|
|
it("subpath exports cover every feature", { timeout: 5000 }, () => {
|
|
const exports = pkg.exports ?? {};
|
|
const expected = [
|
|
".",
|
|
"./middleware",
|
|
"./registry",
|
|
"./spec",
|
|
"./error-codes",
|
|
"./instruments",
|
|
];
|
|
for (const e of expected) {
|
|
expect(Object.keys(exports)).toContain(e);
|
|
}
|
|
});
|
|
|
|
it("every impl .ts has a sibling .md", { timeout: 5000 }, () => {
|
|
const srcDir = join(PKG_DIR, "src");
|
|
const all = walk(srcDir);
|
|
const missing: string[] = [];
|
|
for (const file of all) {
|
|
if (!file.endsWith(".ts")) continue;
|
|
if (file.endsWith(".test.ts")) continue;
|
|
if (file.endsWith(".spec.ts")) continue;
|
|
if (file.endsWith(".spec-conformance.test.ts")) continue;
|
|
if (file.endsWith(".types.ts")) continue;
|
|
const base = file.replace(/\.ts$/, "");
|
|
const fileName = base.split("/").pop();
|
|
if (fileName === "index") continue;
|
|
if (file === join(srcDir, "index.ts")) continue;
|
|
const md = `${base}.md`;
|
|
try {
|
|
statSync(md);
|
|
} catch {
|
|
missing.push(md.replace(`${PKG_DIR}/`, ""));
|
|
}
|
|
}
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it("no biome suppressions in src/", { timeout: 5000 }, () => {
|
|
const all = walk(join(PKG_DIR, "src"));
|
|
const hits: string[] = [];
|
|
for (const f of all) {
|
|
if (!f.endsWith(".ts")) continue;
|
|
if (f.endsWith(".test.ts")) continue;
|
|
const txt = readFileSync(f, "utf8");
|
|
if (txt.includes("biome-ignore")) hits.push(f);
|
|
if (txt.includes("eslint-disable")) hits.push(f);
|
|
}
|
|
expect(hits).toEqual([]);
|
|
});
|
|
|
|
it("no banned patterns in src/", { timeout: 5000 }, () => {
|
|
const banned = [
|
|
"TODO",
|
|
"FIXME",
|
|
"ts-ignore",
|
|
"ts-expect-error",
|
|
"@deprecated",
|
|
"console.log",
|
|
];
|
|
const all = walk(join(PKG_DIR, "src"));
|
|
const hits: { file: string; pattern: string }[] = [];
|
|
for (const f of all) {
|
|
if (!f.endsWith(".ts")) continue;
|
|
if (f.endsWith(".test.ts")) continue;
|
|
const txt = readFileSync(f, "utf8");
|
|
for (const p of banned) {
|
|
if (txt.includes(p)) hits.push({ file: f, pattern: p });
|
|
}
|
|
}
|
|
expect(hits).toEqual([]);
|
|
});
|
|
|
|
it("no `export *` in feature barrels", { timeout: 5000 }, () => {
|
|
const featureBarrels = readdirSync(join(PKG_DIR, "src"))
|
|
.filter((entry) => {
|
|
const p = join(PKG_DIR, "src", entry);
|
|
try {
|
|
return statSync(p).isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
})
|
|
.map((d) => join(PKG_DIR, "src", d, "index.ts"));
|
|
const hits: string[] = [];
|
|
for (const b of featureBarrels) {
|
|
try {
|
|
const txt = readFileSync(b, "utf8");
|
|
if (/^export\s+\*/m.test(txt)) hits.push(b);
|
|
} catch {
|
|
/* directory may have no index.ts */
|
|
}
|
|
}
|
|
expect(hits).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("layout invariants", () => {
|
|
it("error-codes lives at src/error-codes/error-codes.ts", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const inner = join(PKG_DIR, "src", "error-codes", "error-codes.ts");
|
|
statSync(inner);
|
|
expect(dirname(inner).endsWith("/error-codes")).toBe(true);
|
|
});
|
|
});
|