feat: standalone module-build readiness for the public @ekklesia packages (ADR 0053)
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
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
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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9426c83109
commit
89d5d0f2da
@@ -66,8 +66,101 @@ module with no migration history omits `entrypoints.migrations` and needs no pla
|
||||
}
|
||||
```
|
||||
|
||||
The package version must match the manifest and runtime-definition versions. Internal package
|
||||
dependencies use `workspace:` ranges and supported public package entrypoints.
|
||||
The package version must match the manifest and runtime-definition versions. Inside this
|
||||
repository, Ekklesia package dependencies use `workspace:` ranges and supported public package
|
||||
entrypoints. A module in its own repository depends on the published packages instead
|
||||
([Developing a module in its own repository](#developing-a-module-in-its-own-repository)).
|
||||
|
||||
### Developing a module in its own repository
|
||||
|
||||
> **Status:** the public packages are ready for a standalone module, and a release check proves it
|
||||
> from locally packed tarballs. They are **not published yet**, and there is no registry URL to use
|
||||
> yet: registry publication is the next slice
|
||||
> ([ADR 0053](../architecture/decisions/0053-standalone-module-development-packages.md)). This
|
||||
> section describes the workflow that publication enables.
|
||||
|
||||
A module does not need an Ekklesia checkout. Do not clone Ekklesia and put your module in its
|
||||
`modules/` directory: your module is its own repository, and it depends on Ekklesia's
|
||||
module-development packages like any other npm packages.
|
||||
|
||||
| Package | Where it goes | What it is |
|
||||
| --- | --- | --- |
|
||||
| `@ekklesia/module-sdk` | `dependencies` | `defineModuleRuntime`, routes, configuration, storage, realtime, principals |
|
||||
| `@ekklesia/module-database` | `dependencies` | Module Database ABI v1 types, `sql`, schema metadata; `/drizzle` for Drizzle |
|
||||
| `@ekklesia/module-ui-sdk` | `dependencies` | `defineModuleUi` and the host context |
|
||||
| `@ekklesia/design-tokens` | `dependencies` | design-token names and helpers |
|
||||
| `@ekklesia/ui` | `dependencies` | shared elements; a pre-1.0 API that may still change |
|
||||
| `react`, `react-dom`, your ORM if any | `dependencies` | your own copies, bundled into your artifacts |
|
||||
| `@ekklesia/module-ui-build` | `devDependencies` | `ekklesia-build-module-ui` |
|
||||
| `@ekklesia/module-pack` | `devDependencies`, **exact version** | `ekklesia-pack-module` |
|
||||
| `@ekklesia/typescript-config` | `devDependencies` | `node.json`, `browser-react.json`, ... |
|
||||
|
||||
`@ekklesia/contracts` is not a direct dependency. The packages above install it; never add or
|
||||
import it yourself, because what it exports is the platform's, not a supported author API.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@acme/notes",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": { "node": ">=24.11.0 <25" },
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json && ekklesia-build-module-ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ekklesia/module-database": "^0.1.2",
|
||||
"@ekklesia/module-sdk": "^0.1.2",
|
||||
"@ekklesia/module-ui-sdk": "^0.1.2",
|
||||
"@ekklesia/ui": "^0.1.2",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ekklesia/module-pack": "0.1.2",
|
||||
"@ekklesia/module-ui-build": "^0.1.2",
|
||||
"@ekklesia/typescript-config": "^0.1.2",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **One coordinated version.** Every Ekklesia module-development package has the same version,
|
||||
equal to the Ekklesia release it belongs to, and the platform checks your manifest's
|
||||
`compatibility.moduleSdk` against it. Ranges in your `package.json` are fine; your lockfile pins
|
||||
exact artifacts, and you install with a frozen lockfile.
|
||||
- **Pin `@ekklesia/module-pack` exactly** in `package.json`, not only in the lockfile: its version
|
||||
decides the package it produces.
|
||||
- **`engines.node` is required.** The packager records your module's supported Node.js range from
|
||||
`engines.node` in your `package.json`, and refuses to package without it
|
||||
(`MODULE_PACK_RUNTIME_UNKNOWN`). It never looks for a workspace file.
|
||||
- **No Drizzle unless you use it.** The Module SDK does not install Drizzle. A module that uses the
|
||||
Drizzle adapter, `@ekklesia/module-database/drizzle`, adds `drizzle-orm` to its own
|
||||
dependencies; it is an optional peer of that adapter only.
|
||||
- **One React.** `@ekklesia/ui` takes React as a peer, so your bundle contains one React, your own.
|
||||
Your UI artifacts still bundle it
|
||||
([ADR 0023](../architecture/decisions/0023-module-ui-build-pipeline.md)).
|
||||
- **esbuild's install script.** The UI build and the packager use esbuild, which runs an install
|
||||
script. With pnpm, allow it in your repository's `pnpm-workspace.yaml`:
|
||||
|
||||
```yaml
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
```
|
||||
- **TypeScript.** Extend the shared configurations by package name, for example
|
||||
`"extends": "@ekklesia/typescript-config/node.json"` for the backend and
|
||||
`@ekklesia/typescript-config/browser-react.json` for the UI.
|
||||
|
||||
Packaging and verification work exactly as for a module in this repository
|
||||
([Packaging your module](#packaging-your-module)). The packager recognizes Ekklesia's packages by
|
||||
their identity, not by where they are installed, so they are recorded under `incorporates` and never
|
||||
credited as third-party software, wherever they came from.
|
||||
|
||||
Ekklesia proves this before any release: `pnpm packages:pack` builds, packs, and inspects the public
|
||||
packages, and `bash scripts/test-standalone-module.sh` installs them into a module outside the
|
||||
repository (`tests/fixtures/standalone-module`), with no workspace link and no Ekklesia registry,
|
||||
then typechecks, builds, packages, and verifies it.
|
||||
|
||||
### An installed module is self-contained
|
||||
|
||||
@@ -92,8 +185,9 @@ through no link, `NODE_PATH`, loader hook, or alias. The packager therefore bund
|
||||
|
||||
#### Packaging your module
|
||||
|
||||
Build your module, then run the packager from the workspace your module is built in. It is a
|
||||
development dependency like `@ekklesia/module-ui-build`:
|
||||
Build your module, then run the packager from your module's directory. It is a development
|
||||
dependency like `@ekklesia/module-ui-build`, and it takes your module's supported Node.js range from
|
||||
`engines.node` in its `package.json`:
|
||||
|
||||
```bash
|
||||
pnpm exec ekklesia-pack-module --module . --target any --out ../packages \
|
||||
@@ -163,7 +257,10 @@ The archive holds one directory named with your module id:
|
||||
licence's standard text is complete without a copyright line (Apache-2.0, which the packager then
|
||||
supplies and says so). The module itself is not added to this notice file. Bundled Ekklesia
|
||||
packages remain listed in `ekklesia.package.json` under `incorporates`; one applicable AGPL text
|
||||
is sufficient for current official modules.
|
||||
is sufficient for current official modules. An Ekklesia package is one named `@ekklesia/*` whose
|
||||
`package.json` also carries `"ekklesia": { "distribution": "module-development" }`, whether it is
|
||||
linked from this repository or installed from a registry; a package with only the name is
|
||||
third-party.
|
||||
- **Reviewed licence supplements.** Some dependencies omit material that redistribution requires.
|
||||
For one of those, the packager adds material from a reviewed supplement checked in with it, after
|
||||
the package's own material and labelled as such
|
||||
@@ -1458,27 +1555,27 @@ export const events = moduleTable("events", {
|
||||
});
|
||||
```
|
||||
|
||||
The production schema entrypoint in `src/schema/index.ts` wraps those same objects with the Module
|
||||
SDK ownership marker:
|
||||
The production schema entrypoint in `src/schema/index.ts` turns those same objects into the plain
|
||||
schema metadata an ABI-v1 module declares:
|
||||
|
||||
```ts
|
||||
import { defineModuleSchema } from "@ekklesia/module-sdk";
|
||||
import { moduleDrizzleSchemaMetadata } from "@ekklesia/module-database/drizzle";
|
||||
import { events } from "./tables.js";
|
||||
|
||||
export { events } from "./tables.js";
|
||||
|
||||
export default defineModuleSchema({
|
||||
moduleId: "calendar",
|
||||
tables: { events }
|
||||
});
|
||||
export default moduleDrizzleSchemaMetadata("calendar", { events });
|
||||
```
|
||||
|
||||
A module without an ORM declares the same metadata directly with `defineModuleSchemaMetadata` from
|
||||
`@ekklesia/module-database`. A legacy-v8 module's schema entrypoint default-exports
|
||||
`{ moduleId, tables }` with the Drizzle table objects themselves.
|
||||
|
||||
The prefix callback converts the module ID to a snake-case namespace and prefixes every local table
|
||||
name. Here the physical PostgreSQL name is `calendar_events`; `care-team` plus `members` becomes
|
||||
`care_team_members`. Local names must be lowercase snake case, and the complete physical identifier
|
||||
must not exceed 63 characters. The SDK's `createModuleTable` provides the same convenience when a
|
||||
generator-facing pure declaration file is not needed; Core validates built metadata independently
|
||||
of either helper.
|
||||
must not exceed 63 characters. `createModuleTable` from `@ekklesia/module-database/drizzle` provides
|
||||
the same convenience; Core validates built metadata independently of either helper.
|
||||
|
||||
Schema definitions use PostgreSQL tables in the platform-managed default/public schema. They must
|
||||
be side-effect-free: do not connect to a database, read configuration, or perform I/O during import.
|
||||
@@ -1511,8 +1608,8 @@ export default defineConfig({
|
||||
Keep `tables.ts` limited to Drizzle declarations so the stable generator can load it without
|
||||
executing the ESM module wrapper. For an ABI-v1 module, `src/schema/index.ts` remains the production
|
||||
compiled schema entrypoint and converts those same table objects to plain metadata with
|
||||
`moduleDrizzleSchemaMetadata`; tables are not declared twice. A legacy-v8 module continues to wrap
|
||||
them with `defineModuleSchema` while that facade exists.
|
||||
`moduleDrizzleSchemaMetadata`; tables are not declared twice. A legacy-v8 module continues to export
|
||||
them as `{ moduleId, tables }` while that facade exists.
|
||||
|
||||
Run the module's `migration:generate` script with a lowercase snake-case name, review the generated
|
||||
SQL and snapshot, and add the matching entry to `drizzle/meta/_ekklesia.json`. Every entry must
|
||||
@@ -1636,6 +1733,15 @@ not Core.
|
||||
|
||||
## Legacy database access
|
||||
|
||||
> **New modules use [Module Database ABI v1](#module-database-abi-v1).** Core still serves the
|
||||
> legacy facade described here for its deprecation window
|
||||
> ([ADR 0050](../architecture/decisions/0050-module-database-abi-v1.md)), so existing legacy modules
|
||||
> keep running. But the Module SDK no longer exposes the facade's type
|
||||
> ([ADR 0053](../architecture/decisions/0053-standalone-module-development-packages.md)): built
|
||||
> against the current SDK, a module that declares no database sees `services.database` as `never`,
|
||||
> and the example below no longer typechecks. Migrate to ABI v1 rather than reaching for the type
|
||||
> elsewhere.
|
||||
|
||||
For a version 8 manifest that omits `database`, `context.services.database` — available in `start`, `stop`, and every authorized route handler,
|
||||
never in `register` — is a `ModuleDatabase`: a typed Drizzle query surface over exactly `select`,
|
||||
`selectDistinct`, `selectDistinctOn`, `insert`, `update`, `delete`, `execute`, and `transaction`,
|
||||
|
||||
Reference in New Issue
Block a user