fix(packages): declare runtime imports as production dependencies
@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:
co-authored by
Claude Opus 5.5
parent
d5d4928b9f
commit
fc422b6d8d
@@ -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
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@ekklesia/sdk",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "0.45.2",
|
||||
"react": "19.2.8",
|
||||
"polyfill": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"drizzle-orm": "0.45.2",
|
||||
"react": "19.2.8"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.28.1",
|
||||
"type-only": "1.0.0",
|
||||
"vitest": "4.1.10"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./schema": "./src/index.ts"
|
||||
},
|
||||
"ekklesia": {
|
||||
"layer": "module-sdk"
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { build } from "esbuild";
|
||||
|
||||
await build({ entryPoints: ["src/index.ts"] });
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
describe("sdk", () => {
|
||||
it("runs with development dependencies installed", () => undefined);
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import path from "node:path";
|
||||
import "polyfill";
|
||||
import { pgTableCreator } from "drizzle-orm/pg-core";
|
||||
import type { Config } from "type-only";
|
||||
import { type Options, type Settings } from "type-only/settings";
|
||||
export type { Shape } from "type-only/shape";
|
||||
export { createSchema } from "@ekklesia/sdk/schema";
|
||||
|
||||
export async function load(config: Config, options: Options, settings: Settings) {
|
||||
const watcher = await import("fsevents");
|
||||
return { config, options, path, readFile, settings, table: pgTableCreator, watcher };
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export function View() {
|
||||
return <div />;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { build } from "esbuild";
|
||||
|
||||
export const builder = build;
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ekklesia/module-ui-host": "workspace:*"
|
||||
"@ekklesia/module-ui-host": "workspace:*",
|
||||
"react": "19.2.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
|
||||
+3
@@ -2,6 +2,9 @@
|
||||
"name": "@ekklesia/module-announcements",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"react": "19.2.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@ekklesia/sdk",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"devDependencies": { "zod": "4.0.0" },
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"ekklesia": {
|
||||
"layer": "module-sdk"
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const identifier = z.string();
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@ekklesia/ui",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"peerDependencies": { "react": "19.2.8" },
|
||||
"devDependencies": { "react": "19.2.8" },
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"ekklesia": {
|
||||
"layer": "ui"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function Badge(props: { readonly label: string }) {
|
||||
return <span>{props.label}</span>;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@ekklesia/sdk",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"peerDependencies": { "drizzle-orm": "0.45.2" },
|
||||
"devDependencies": { "drizzle-orm": "0.45.2" },
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"ekklesia": {
|
||||
"layer": "module-sdk"
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { pgTableCreator } from "drizzle-orm/pg-core";
|
||||
|
||||
export const createTable = pgTableCreator((name) => name);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@ekklesia/module-calendar",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"ekklesia": {
|
||||
"layer": "module"
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export async function loadSharp() {
|
||||
return await import("sharp");
|
||||
}
|
||||
Reference in New Issue
Block a user