import { readdirSync, readFileSync, statSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; const ROOT = process.cwd(); const TEMPLATES_DIR = path.join(ROOT, "src/pdf/templates"); /** * Model fields that carry regulated data. Templates must not read them: the data may leave the model * only through the Slot components, so the contract test can see every place it is printed. */ const REGULATED = [ /\.gstin\b/, /\.showGstin\b/, /\.posLabel\b/, /\.hsnSac\b/, /\.taxRows\b/, /\.amountInWords\b/, /\.reverseCharge\b/, /\.signature\b/, /\.slots\b/, ]; function sources(dir: string): string[] { return readdirSync(dir).flatMap((name) => { const full = path.join(dir, name); if (statSync(full).isDirectory()) return sources(full); return /\.(ts|tsx)$/.test(name) && !/\.test\.|harness(\.check)?\.ts$/.test(name) ? [full] : []; }); } describe("templates read regulated data only through Slot.*", () => { it("scans the classic template", () => { expect(sources(TEMPLATES_DIR).some((f) => f.endsWith("classic/Layout.tsx"))).toBe(true); }); it("finds no direct access to the regulated model fields", () => { const hits: string[] = []; for (const file of sources(TEMPLATES_DIR)) { readFileSync(file, "utf8") .split("\n") .forEach((line, i) => { if (/^\s*(\/\/|\*|\/\*)/.test(line)) return; // The slot API itself is the sanctioned way in: Slot.TaxRows, slotHeights.amountInWords, ... const code = line.replace(/\b(Slot|slotHeights)\.\w+/g, ""); for (const re of REGULATED) if (re.test(code)) hits.push(`${path.relative(ROOT, file)}:${i + 1} ${line.trim()}`); }); } expect(hits).toEqual([]); }); });