import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { checkBoundaries } from "../scripts/check-boundaries.js"; const testDirectory = path.dirname(fileURLToPath(import.meta.url)); const fixtures = path.join(testDirectory, "fixtures", "boundaries"); describe("repository dependency boundaries", () => { it("accepts public contracts below Core", async () => { const result = await checkBoundaries(path.join(fixtures, "legal")); expect(result.violations).toEqual([]); }); it("accepts generated module composition only through the generated bootstrap boundary", async () => { const result = await checkBoundaries(path.join(fixtures, "legal-generated-bootstrap")); expect(result.violations).toEqual([]); }); it("honors explicit pnpm workspace exclusions", async () => { const result = await checkBoundaries(path.join(fixtures, "workspace-exclusion")); expect(result.violations).toEqual([]); }); it.each([ ["core-imports-module", "core-is-feature-agnostic"], ["app-imports-module", "apps-are-composition-roots"], ["module-imports-core", "modules-use-public-platform-contracts"], ["module-imports-surface", "modules-use-public-platform-contracts"], ["package-boundary-direction", "modules-use-public-platform-contracts"], ["cross-module-import", "modules-do-not-import-other-modules"], ["module-schema-imports-module", "modules-do-not-import-other-modules"], ["surface-imports-module", "surfaces-use-public-contracts"], ["surface-imports-contracts", "surfaces-use-public-contracts"], ["cross-surface-import", "surfaces-are-independent"], ["builder-imports-module", "builder-composes-contracts"], ["contracts-import-core", "contracts-do-not-depend-upward"], ["sdk-imports-core", "sdks-do-not-depend-on-implementations"], ["shared-role-order", "sdks-do-not-depend-on-implementations"], ["package-without-exports", "workspace-imports-use-public-exports"], ["private-package-import", "workspace-imports-use-public-exports"], ["non-workspace-version", "internal-dependencies-use-workspace-protocol"], ["workspace-alias", "workspace-dependency-aliases-are-forbidden"], ["undeclared-workspace-dependency", "workspace-dependencies-are-declared"], ["duplicate-package-name", "workspace-package-names-are-unique"], ["unknown-shared-role", "shared-packages-declare-known-roles"], ["workspace-unsupported-pattern", "workspace-patterns-use-supported-syntax"], ["package-cycle", "no-circular-package-dependencies"], ["module-has-ui", "modules-do-not-own-ui"], ["module-ui-framework", "modules-do-not-own-ui"], ["module-ui-aliased-framework", "modules-do-not-own-ui"], ["module-ui-imports-core", "modules-use-public-platform-contracts"], ["module-ui-bundler-plugin", "modules-do-not-own-ui"], ["module-imports-ui-host", "modules-use-public-platform-contracts"], ["module-imports-surface-sdk", "modules-use-public-platform-contracts"], ["design-package-depends-upward", "design-packages-depend-downward-only"], ["cross-package-relative", "workspace-imports-use-public-exports"], ["cycle", "no-circular-source-dependencies"], ["runtime-peer-only-dependency", "runtime-imports-are-production-dependencies"], ["runtime-dev-only-dependency", "runtime-imports-are-production-dependencies"], ["runtime-jsx-implies-react", "runtime-imports-are-production-dependencies"], ["runtime-undeclared-dependency", "runtime-imports-are-production-dependencies"], ])("rejects %s", async (fixture, expectedRule) => { const result = await checkBoundaries(path.join(fixtures, fixture)); expect(result.violations.map((violation) => violation.rule)).toContain(expectedRule); }); it("rejects a misplaced stylesheet and case-insensitive view files inside a module, but not TSX", async () => { const result = await checkBoundaries(path.join(fixtures, "module-has-ui")); const uiViolations = result.violations.filter( (violation) => violation.rule === "modules-do-not-own-ui", ); expect(uiViolations).toHaveLength(2); expect(uiViolations.map((violation) => violation.from)).toEqual( expect.arrayContaining([ // A stylesheet outside a compiled browser source root is a file no build reads and no host // loads — the undeclared sidecar ADR 0023 rejected, just relocated (ADR 0040). "modules/calendar/src/calendar.css", "modules/calendar/src/view.ASTRO", ]), ); // Inside `src/ui/`, a plain stylesheet is legitimate source the canonical builder compiles into // the declared artifact. expect(uiViolations.map((violation) => violation.from)).not.toContain( "modules/calendar/src/ui/styles.css", ); // React is the supported module UI authoring model (ADR 0022), so a module's own `.tsx` is // legitimate source and must no longer be reported. expect(uiViolations.map((violation) => violation.from)).not.toContain( "modules/calendar/src/calendar.tsx", ); }); it("rejects unsupported UI frameworks, their imports, and view formats inside a module", async () => { const result = await checkBoundaries(path.join(fixtures, "module-ui-framework")); const uiViolations = result.violations.filter( (violation) => violation.rule === "modules-do-not-own-ui", ); expect(uiViolations).toHaveLength(3); expect(uiViolations.map((violation) => violation.message)).toEqual( expect.arrayContaining([ "Module package depends on UI framework preact.", "Module source imports UI framework preact.", ]), ); // The fixture also aliases `compat` to react, which is now the supported framework and is // therefore no longer a violation. expect(uiViolations.map((violation) => violation.message).join(" ")).not.toContain("compat"); }); it("accepts runtime imports declared as production dependencies, and everything that is not shipped runtime code", async () => { // Production and optional dependencies, a peer kept beside its production declaration, Node // built-ins, the package's own exports, type-only imports of development dependencies, and // tests or build scripts that run with development dependencies installed. const result = await checkBoundaries(path.join(fixtures, "legal-runtime-dependencies")); expect(result.violations).toEqual([]); }); it("accepts only the consumer-selected runtime peers: the database adapter's Drizzle and the shared UI's React", async () => { const result = await checkBoundaries(path.join(fixtures, "legal-consumer-runtime-peer")); expect(result.violations).toEqual([]); }); it.each([ [ "runtime-peer-only-dependency", "packages/sdk/src/index.ts", "drizzle-orm", "only as a peer dependency", ], [ "runtime-dev-only-dependency", "packages/sdk/src/index.ts", "zod", "only as a development dependency", ], // The automatic JSX runtime imports `react/jsx-runtime` without that import appearing in source. [ "runtime-jsx-implies-react", "packages/ui/src/index.tsx", "react", "only as a peer dependency", ], [ "runtime-undeclared-dependency", "modules/calendar/src/server/index.ts", "sharp", "not at all", ], ])("reports %s once, naming the file, the package, and how it is declared", async (fixture, from, to, declaredAs) => { // A workspace package's imports resolve from its own node_modules: a peer is never satisfied // there by the consumer, and a production install omits development dependencies, so each of // these imports fails at run time in the production image even though development works. const result = await checkBoundaries(path.join(fixtures, fixture)); expect(result.violations).toEqual([ { from, message: expect.stringContaining(`declares it ${declaredAs}`), rule: "runtime-imports-are-production-dependencies", to, }, ]); }); it("still rejects module UI source that reaches into Core internals", async () => { // Allowing `.tsx` inside a module changed which *formats* a module may contain. It did not // change which layers a module may import: module UI is subject to the identical layer rules // as module backend source. const result = await checkBoundaries(path.join(fixtures, "module-ui-imports-core")); expect(result.violations).toEqual([ { from: "modules/announcements/src/ui/editor.tsx", message: "module code may not import core code directly.", rule: "modules-use-public-platform-contracts", to: "core/src/index.ts", }, ]); }); it("accepts the canonical module UI dependency set: React, the UI SDK, and the build tool", async () => { // Mirrors what `templates/module` actually declares (ADR 0023). The build tool is a `tooling` // shared package, a role modules were already permitted to depend on, so the module UI build // pipeline required no boundary relaxation at all. const result = await checkBoundaries(path.join(fixtures, "module-owns-react-ui")); expect(result.violations).toEqual([]); }); it("accepts a module consuming the approved design packages", async () => { // ADR 0039 opens exactly two shared roles to module UI — `design-tokens` and `ui` — so two // modules' screens can look like one product. Both are browser-safe by construction, so this // adds no path from module UI to a backend type graph. const result = await checkBoundaries(path.join(fixtures, "module-uses-design-packages")); expect(result.violations).toEqual([]); }); it("keeps every other shared role out of module packages", async () => { // The relaxation above is a named allowlist, not "browser-safe shared packages are fine": a // surface SDK, an API client, and an auth client all stay rejected. const result = await checkBoundaries(path.join(fixtures, "module-imports-surface-sdk")); expect(result.violations.map((violation) => violation.rule)).toEqual([ "modules-use-public-platform-contracts", "modules-use-public-platform-contracts", ]); }); it("keeps the design packages at the bottom of the presentation stack", async () => { // Neither may reach an API client, an auth client, the contracts barrel, or an implementation // layer: a design package that could would put all of it inside every module artifact that // renders a button. const result = await checkBoundaries(path.join(fixtures, "design-package-depends-upward")); expect(result.violations.map((violation) => violation.rule)).toEqual([ "design-packages-depend-downward-only", "design-packages-depend-downward-only", "design-packages-depend-downward-only", "design-packages-depend-downward-only", ]); }); it("keeps the host loader out of module packages", async () => { // `@ekklesia/module-ui-host` is host infrastructure. A module that depended on it would bundle // loader code into its own artifact and blur the author/host split (ADR 0024), so the // `module-ui-host` role is deliberately absent from the roles a module may depend on. const result = await checkBoundaries(path.join(fixtures, "module-imports-ui-host")); expect(result.violations.map((violation) => violation.rule)).toEqual([ "modules-use-public-platform-contracts", "modules-use-public-platform-contracts", ]); }); it("still rejects a module reaching for a bundler plugin ecosystem", async () => { // Module authors do not configure a bundler; `@ekklesia/module-ui-build` owns that entirely. const result = await checkBoundaries(path.join(fixtures, "module-ui-bundler-plugin")); expect(result.violations.map((violation) => violation.message)).toEqual([ "Module package depends on UI framework @vitejs/plugin-react.", ]); }); it("rejects an unsupported framework aliased behind the supported one", async () => { const result = await checkBoundaries(path.join(fixtures, "module-ui-aliased-framework")); expect(result.violations.map((violation) => violation.message)).toEqual([ "Module package depends on UI framework vue aliased as react.", ]); }); it("requires workspace: ranges in every dependency field", async () => { const result = await checkBoundaries(path.join(fixtures, "non-workspace-version")); expect( result.violations.filter( (violation) => violation.rule === "internal-dependencies-use-workspace-protocol", ), ).toHaveLength(4); }); it("resolves forbidden workspace aliases before applying layer rules", async () => { const result = await checkBoundaries(path.join(fixtures, "workspace-alias")); expect( result.violations.filter( (violation) => violation.rule === "workspace-dependency-aliases-are-forbidden", ), ).toHaveLength(2); expect(result.violations.map((violation) => violation.rule)).toEqual( expect.arrayContaining([ "workspace-dependency-aliases-are-forbidden", "modules-use-public-platform-contracts", ]), ); }); it("prevents contracts and SDKs from depending upward through shared packages", async () => { const result = await checkBoundaries(path.join(fixtures, "shared-role-order")); expect(result.violations.map((violation) => violation.rule)).toEqual( expect.arrayContaining([ "contracts-do-not-depend-upward", "sdks-do-not-depend-on-implementations", ]), ); }); });