@ekklesia/module-sdk imports drizzle-orm at run time but declared it only as a peer and a development dependency. A workspace package is linked into its consumers as a directory, so its imports resolve from its own node_modules, where a consumer never satisfies a peer; a production install omits development dependencies. In the production image this made every module with a schema fail: the API exited at startup and Doctor reported NOT READY. - Move drizzle-orm to module-sdk's dependencies, keeping the peer. - Move react to @ekklesia/ui's dependencies, keeping the peer. The new guard flags it the same way; it is bundled at build time today, so it was not a live failure. Both lockfile entries keep the identical resolution, so nothing new is installed and the shell bundles still contain one React. - Add the boundary rule runtime-imports-are-production-dependencies: code a package ships (src/, bin/, excluding tests and .d.ts) must declare every runtime import in dependencies or optionalDependencies. Type-only imports are exempt; .tsx/.jsx implies react (automatic JSX runtime). Fixtures cover peer-only, dev-only, JSX, undeclared, and the accepted cases. Co-Authored-By: Claude Opus 5.5 <[email protected]>
89 lines
3.7 KiB
TypeScript
89 lines
3.7 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>; peerDependencies?: Record<string, string> };
|
|
|
|
// React is both a production dependency and a peer: the peer states that the consumer's React
|
|
// must be the same one, and the production declaration is what makes the package's own imports
|
|
// resolve, since a consumer never satisfies a linked workspace package's peer
|
|
// (`runtime-imports-are-production-dependencies`).
|
|
expect(Object.keys(manifest.dependencies ?? {})).toEqual(["@ekklesia/design-tokens", "react"]);
|
|
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);
|
|
});
|
|
});
|