ADR 0022 defined what a module UI is and selected React and TypeScript as the authoring model, but closed by recording that it was not buildable end to end: `browser.json` preserves JSX, bundler plugins are denied, and nothing turned a module's `.tsx` into the artifact its manifest declares. This closes exactly that gap. esbuild produces the artifact. It was already the right answer before it was chosen: this workspace resolves esbuild 0.28.1 through Vite/Vitest today and `pnpm-workspace.yaml` already carries `allowBuilds: esbuild`, so the pinned dependency adds no second copy and no new trust decision. It is also the smallest tool that covers TSX, ESM, browser platform, bundling, source maps, and a define-time constant with no plugin ecosystem. Vite library mode would drag in `@vitejs/plugin-react`, which the module boundary denies on purpose; `tsc` alone cannot bundle, so its output would carry bare `react/jsx-runtime` imports no browser resolves. React is bundled, not externalized. That follows from ADR 0022 rather than from convenience: `mount()` was made the ABI precisely so React would not be part of the host contract, and externalizing it would reverse that through the build system — every host would have to supply a compatible React, and a standalone host might not use React at all. The cost is real and accepted: duplicate runtimes and a ~570 KB unminified artifact. Sharing one runtime later is a build optimization that cannot change the contract, because the contract is a mount function. Output is browser ESM at `es2022`, unminified so an operator can review what they install, with `process.env.NODE_ENV` substituted — React reads it at module scope and would otherwise touch a Node global that does not exist in a browser. The output path is read from the module's own manifest rather than a build convention, so declared and produced artifacts cannot drift, and it is validated with the same `packageRelativePathSchema` discovery enforces, so a build can never write where discovery would later reject. `@ekklesia/module-ui-build` owns every option; modules own none. It uses the existing `tooling` role that modules could already depend on, so this pass required no boundary relaxation at all. Its `bin` points at a committed launcher rather than compiled output because pnpm links bins during install, before Turborepo builds anything — a bin targeting `dist/cli.js` fails to link on a fresh clone and breaks every module's UI build in CI. A module with no `entrypoints.ui` is not an error; the CLI reports nothing to build and exits zero, so one script serves both kinds of module. Styling is React inline style objects. A stylesheet import fails the build rather than emitting a sidecar `.css` that no manifest declares and no host loads — an undeclared asset would appear to work in tests and silently do nothing in a real host. `templates/module` is the canonical example and now ships optional UI. Its tests build the artifact from source, validate it with `parseModuleUiExport`, mount it into a real DOM, assert the rendered output, and unmount it; the artifact is rebuilt per run so the proof can never pass against stale output, and `dist/` stays gitignored. Because verify runs both `pnpm test` and `pnpm build`, a UI that cannot be produced fails CI through two independent paths. One correction worth stating plainly: the UI TypeScript project declares `"types": []`, and a UI file referencing `process` does fail in isolation, but TypeScript still pulls Node globals in transitively — a file importing `@ekklesia/module-ui-sdk` reaches `@ekklesia/contracts` and through its Zod and Drizzle types `process.env` typechecks again. The docs and ADR describe this as the convention it is rather than the guard it is not. Core is untouched. Building is an authoring step; discovery still only validates the artifact a module shipped and never executes or produces one. Co-Authored-By: Claude Opus 5 <[email protected]>
69 lines
4.9 KiB
Markdown
69 lines
4.9 KiB
Markdown
# Repository boundaries
|
|
|
|
These rules map platform ownership to filesystem and package boundaries. The executable check is
|
|
`pnpm boundaries`; CI must reject violations.
|
|
|
|
| Area | Owns | May depend on |
|
|
| --- | --- | --- |
|
|
| `apps/*` | Thin deployable composition roots | Public Core APIs, generated bootstrap, or a surface as required by the app |
|
|
| `core` | Feature-agnostic platform implementations | Public contracts and external infrastructure libraries |
|
|
| `modules/*` | Domain capability, data, and the module's own UI | Module SDK, module UI SDK, public contracts, React, and external backend libraries |
|
|
| `surfaces/*` | UI, renderers, navigation, and interaction | Surface SDK, API/auth clients, public contracts, and design packages |
|
|
| `builders/*` | Declarative surface composition tooling | Builder/surface SDKs and public contracts |
|
|
| `packages/*` | Explicitly scoped SDKs, contracts, clients, design, testing, or tooling | Only dependencies appropriate to the package's declared level |
|
|
| `templates/module` | A small example backend module | The same dependencies allowed for any module |
|
|
|
|
## Mandatory rules
|
|
|
|
- Core never imports a feature module, surface, Builder implementation, or app.
|
|
- A module never imports Core implementations, another module, a surface, Builder code, or an app.
|
|
- A module may own UI ([ADR 0021](decisions/0021-modules-as-hostable-products.md)) and authors it in
|
|
React and TypeScript ([ADR 0022](decisions/0022-module-ui-authoring-and-host-contract.md)). A
|
|
module may therefore contain `.tsx` source and depend on `react`, `react-dom`, and
|
|
`@ekklesia/module-ui-sdk`. It still cannot contain `.jsx`, stylesheets, single-file components,
|
|
Astro/Marko/MDX/HTML views, or SVG presentation assets, and cannot depend on any other UI framework
|
|
(including `react-router-dom`, `react-native`, and `@vitejs/plugin-react`) or on shared UI-role
|
|
packages. A module needs no bundler plugin: `@ekklesia/module-ui-build` — a `tooling` package,
|
|
a role modules could already depend on — owns the entire module UI build
|
|
([ADR 0023](decisions/0023-module-ui-build-pipeline.md)). An unsupported framework aliased behind an allowed name is resolved and rejected by its
|
|
real target.
|
|
- Module UI source is subject to the identical layer rules as module backend source: it may not
|
|
import Core, an app, a surface, a builder, or another module. Core still owns the containing
|
|
application shell and all platform policy, and module UI reaches the host only through
|
|
`@ekklesia/module-ui-sdk`.
|
|
- A surface never imports Core or a feature module. It uses public API/client contracts.
|
|
- Separate surfaces do not import each other's internals.
|
|
- Builder code composes contracts and surface renderer identifiers; it does not import module or
|
|
surface internals.
|
|
- Contract and SDK packages cannot depend upward on implementations.
|
|
- Workspace package consumers use declared `exports`, not private subpaths.
|
|
- Every internal dependency uses a `workspace:` range and every imported workspace package is
|
|
declared by the consumer.
|
|
- Workspace aliases are forbidden because they obscure package ownership in the dependency graph.
|
|
- Workspace package names are unique and shared packages declare a recognized architectural role.
|
|
- Circular source and workspace-package dependencies are rejected.
|
|
- Cross-module data access uses services, events, capabilities, or opaque identifiers rather than
|
|
direct table imports and foreign keys. Modules may not import another module's Drizzle tables,
|
|
columns, relations, schema entrypoint, or migration internals.
|
|
|
|
The boundary test suite includes intentionally invalid fixture graphs. This proves that the guard
|
|
rejects every currently machine-checkable repository rule even before production packages fill
|
|
every directory. The UI guard rejects component, view, template, stylesheet, shared UI-role, and
|
|
common UI-framework dependencies inside modules. This deny list is defense in depth; package
|
|
review remains required when adopting a new frontend technology. ADR 0022 narrowed the guard
|
|
precisely rather than removing it: React was un-denied, every other framework stayed denied, and the
|
|
layer rules were untouched.
|
|
|
|
The Phase 3 schema validator complements source boundaries by checking table ownership, foreign-key
|
|
targets, and physical table namespaces in compiled Drizzle metadata. Offline migration validation
|
|
checks each module's packaged Drizzle inventory and policy contract, while deliberately making no
|
|
claim that opaque SQL ownership can be proven without PostgreSQL-aware analysis.
|
|
|
|
## Generated code
|
|
|
|
Generated artifacts are reproducible outputs with a named generator and validation step. A
|
|
generated module bootstrap under the root `generated/` boundary may import discovered module entry
|
|
points, and apps may import that generated bootstrap. Handwritten app and Core code still cannot
|
|
import feature modules directly. Generated files are never an excuse for a hand-maintained central
|
|
module registry.
|