fix(packages): declare runtime imports as production dependencies
CI / verify (pull_request) Successful in 47s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m45s

@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]>
This commit is contained in:
ekklesia
2026-10-01 19:29:35 -05:00
co-authored by Claude Opus 5.5
parent d5d4928b9f
commit fc422b6d8d
23 changed files with 322 additions and 13 deletions
+55
View File
@@ -56,6 +56,10 @@ describe("repository dependency boundaries", () => {
["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);
@@ -106,6 +110,57 @@ describe("repository dependency boundaries", () => {
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.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