Files
Ekklesia/packages/ui/tests/tokens.test.ts
T
ekklesiaandClaude Opus 5.5 89d5d0f2da
CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053)
Makes the public module-development package set usable from a module
repository outside the monorepo. Nothing is published.

- Curated @ekklesia/module-sdk: an explicit author API, no contracts
  barrel, no identity/authorization internals, no Drizzle and no legacy
  ModuleDatabase type. defineModuleRuntime returns the SDK's own named
  interfaces so a module's emitted declarations never reach into
  @ekklesia/contracts. defineModuleSchema is gone; createModuleTable and
  defineModuleSchemaMetadata live in @ekklesia/module-database.
- Drizzle: no longer a dependency of module-sdk or contracts. Core keeps
  the legacy Drizzle schema validator (moved to core/src/modules) and
  its legacy runtime support; drizzle-orm is an optional peer only.
- React is a peer (plus devDependency) of @ekklesia/ui; the boundary
  checker allows exactly that consumer-selected peer.
- @ekklesia/typescript-config is publishable; module-ui-host is private.
  Every public package carries "ekklesia.distribution":
  "module-development" and no monorepo-root prepack.
- module-pack reads the Node.js range from the module's own engines.node
  and recognizes Ekklesia packages by name plus that marker, so
  registry-installed packages are incorporated, SDK-compatibility
  checked, and never credited as third-party.
- scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs
  and inspects the nine public tarballs; scripts/test-standalone-module.sh
  (pnpm test:standalone-module, new CI job) installs them into a fixture
  module outside the repository and proves it typechecks, builds,
  packages, and verifies.
- Public-package boundary tests and module-development docs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 07:28:54 -05:00

94 lines
3.9 KiB
TypeScript

import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ekklesiaDesignTokens } from "@ekklesia/design-tokens";
import { describe, expect, it } from "vitest";
import {
EKKLESIA_UI_STYLE_ATTRIBUTE,
EKKLESIA_UI_STYLESHEET,
installEkklesiaUiStyles,
} from "../src/index.js";
const packageDirectory = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
async function sourceFiles(): Promise<readonly string[]> {
const directory = path.join(packageDirectory, "src");
return (await readdir(directory))
.filter((entry) => entry.endsWith(".ts") || entry.endsWith(".tsx"))
.map((entry) => path.join(directory, entry));
}
/** Any literal that would be a theme decision rather than a layout one. */
const THEME_LITERAL_PATTERN =
/#[0-9a-f]{3,8}\b|\brgba?\s*\(|\bhsla?\s*\(|\boklch\s*\(|\bsystem-ui\b|\bsans-serif\b/giu;
describe("the shared elements consume design tokens, never hardcoded theme values", () => {
it("expresses every theme value in the stylesheet as a token reference", () => {
expect(EKKLESIA_UI_STYLESHEET.match(THEME_LITERAL_PATTERN)).toBeNull();
expect(EKKLESIA_UI_STYLESHEET).toContain("var(--ekklesia-");
});
it("references only registered tokens", () => {
const referenced = [...EKKLESIA_UI_STYLESHEET.matchAll(/var\((--ekklesia-[a-z0-9-]+)\)/gu)].map(
([, variable]) => String(variable),
);
const registered = new Set(ekklesiaDesignTokens.list().map((token) => token.cssVariable));
expect(referenced.length).toBeGreaterThan(0);
expect([...new Set(referenced)].filter((variable) => !registered.has(variable))).toEqual([]);
});
it("never defines a global Ekklesia theme variable of its own", () => {
// Referencing `--ekklesia-*` is the contract; *declaring* one would invent a global theme
// variable outside the registry, which is precisely what the design-token contract forbids.
expect(EKKLESIA_UI_STYLESHEET).not.toMatch(/--ekklesia-[a-z0-9-]+\s*:/u);
});
it("hardcodes no theme value in component source either", async () => {
for (const file of await sourceFiles()) {
if (path.basename(file) === "styles.ts") {
continue;
}
expect(await readFile(file, "utf8")).not.toMatch(THEME_LITERAL_PATTERN);
}
});
it("never names a Node built-in or reads the environment", async () => {
for (const file of await sourceFiles()) {
const source = await readFile(file, "utf8");
expect(source).not.toContain('"node:');
expect(source).not.toContain("process.env");
}
});
it("depends only on the design tokens and React", async () => {
const manifest = JSON.parse(
await readFile(path.join(packageDirectory, "package.json"), "utf8"),
) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
};
// React is a peer only (ADR 0053): the consumer's React is the one in every bundle these
// elements are built into. The development dependency resolves the package's own imports for its
// builds and tests; nothing loads it from this workspace in a production Node.js process
// (`runtime-imports-are-production-dependencies` allows exactly this peer).
expect(Object.keys(manifest.dependencies ?? {})).toEqual(["@ekklesia/design-tokens"]);
expect(manifest.devDependencies?.react).toBeDefined();
expect(Object.keys(manifest.peerDependencies ?? {})).toEqual(["react"]);
});
it("installs its stylesheet into a document root exactly once", () => {
const remove = installEkklesiaUiStyles(document);
installEkklesiaUiStyles(document);
expect(document.querySelectorAll(`style[${EKKLESIA_UI_STYLE_ATTRIBUTE}]`).length).toBe(1);
remove();
remove();
expect(document.querySelectorAll(`style[${EKKLESIA_UI_STYLE_ATTRIBUTE}]`).length).toBe(0);
});
});