import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; const sourceDirectory = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "src"); async function sourceFiles(): Promise { const entries = await readdir(sourceDirectory); return entries.filter((entry) => entry.endsWith(".ts")).sort(); } /** * This package is bundled into a browser. `@ekklesia/module-sdk` re-exports all of * `@ekklesia/contracts`, which pulls Zod, Drizzle, and semver behind one barrel export, so a single * value import from a backend package here would drag that whole graph into every module UI bundle * (ADR 0022). The dependency on `@ekklesia/contracts` is deliberately type-only and must stay that * way; `import type` is erased at build time, a plain `import` is not. */ describe("module UI SDK stays browser-safe", () => { it("imports nothing from another package at all, at type level or runtime", async () => { // Stronger than "type-only imports are fine". A type-only import is erased at runtime but still // drags the imported package's *type graph* into every consuming program — which is exactly how // Node globals reached browser-only module UI before ADR 0024. This package now imports nothing // external, so there is no graph to drag. for (const file of await sourceFiles()) { const source = await readFile(path.join(sourceDirectory, file), "utf8"); const externalImports = [...source.matchAll(/^(?:import|export)\s[^;]*?from\s+"([^"]+)"/gmu)] .map(([, specifier]) => specifier) .filter((specifier) => specifier !== undefined && !specifier.startsWith(".")); expect(externalImports, `${file} imports outside this package`).toEqual([]); } }); it("declares no dependencies whatsoever", async () => { const manifestPath = path.join(sourceDirectory, "..", "package.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as { dependencies?: Record; peerDependencies?: Record; }; expect(Object.keys(manifest.dependencies ?? {})).toEqual([]); expect(Object.keys(manifest.peerDependencies ?? {})).toEqual([]); }); it("does not depend on a UI framework, so the contract stays framework-neutral", async () => { const manifestPath = path.join(sourceDirectory, "..", "package.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as { dependencies?: Record; devDependencies?: Record; peerDependencies?: Record; }; const declared = [ ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {}), ]; // React is the supported way to *author* module UI. It is deliberately absent from the host // contract, so a host never has to match a module's rendering library. for (const framework of ["react", "react-dom", "vue", "svelte", "preact"]) { expect(declared).not.toContain(framework); } // Backend graphs must not reappear through a development dependency either. for (const backend of ["drizzle-orm", "zod", "semver", "@ekklesia/contracts"]) { expect(declared).not.toContain(backend); } }); });