feat: give module UI real stylesheets, and every artifact its own root
ADR 0023 refused stylesheet imports for a good reason: esbuild would emit a
`.css` sidecar beside the artifact, nothing would declare it, no host would
fetch it, and it would work in a local test and silently do nothing in a real
host. Inline style objects were the whole story. They cannot express
`:hover`, `:focus-visible`, `:disabled`, `@media`, or `@keyframes` — so an
accessible focus treatment was not expressible, and the shared elements added
in the previous commit are built from real CSS for exactly that reason.
Compile the stylesheet into the artifact instead of beside it. A module
imports plain `.css`; the canonical builder resolves local `@import`s through
a nested bundle and turns each file into its compiled text, which the author
declares as `defineModuleUi({ styles })`. One declared file still contains
everything: nothing extra to publish, nothing to fetch, and no way for CSS to
become an asset nobody serves. The build additionally fails if a CSS output
ever appears beside an artifact, so a future loader change cannot bring the
sidecar back. Two rules are enforced with their own codes: a module stylesheet
may reference `--ekklesia-*` and never declare one, and every `@import` must
resolve locally or the artifact would fetch styles at run time. Plain `.css`
only; other dialects are a toolchain decision needing their own ADR.
Then make the isolation real. The generic loader mounts every browser artifact
— primary, display, and later a contributed renderer — inside an open Shadow
DOM root it creates, installs the artifact's stylesheet there, and hands the
artifact an element inside it. Shadow DOM is the one mechanism a browser
actually enforces; a class prefix or a "scope your selectors" instruction is a
convention one mistake defeats, and the failure mode is a module silently
restyling the Dashboard. It applies unconditionally, including to artifacts
with no stylesheet, because a conditional boundary is one a module can fall
out of by editing a source file. Both wrappers are `display: contents` through
base rules installed before the artifact's own, so layout is unchanged and a
full-viewport display can still override `:host`. Unmount removes the whole
root in a `finally`, so a cleanup that throws cannot leave styles behind, and
remounting cannot accumulate a second copy.
This is a correctness boundary for styles and is not described as a sandbox:
the root is `open` precisely because module code is trusted, same-realm code
that could reach `document` anyway.
An artifact's DOM is no longer a direct child of the host's element, so the
mount ABI is versioned explicitly. `defineModuleUi` stamps
`MODULE_UI_ABI_VERSION` so it can never drift from the SDK the artifact
compiled against, and a version-1 artifact fails with its own code — "rebuild
your module", not a typo hunt. No dual acceptance, matching every manifest
cutover here. Migration is one `pnpm build`.
`pnpm boundaries` allows `.css` only under `src/ui/` and `src/display/`: a
stylesheet anywhere else in a module is the undeclared sidecar again, just
relocated.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e03d38fcab
commit
07835b7b56
@@ -100,6 +100,15 @@ This is deliberately the smallest viable styling story. A real stylesheet contra
|
||||
field, a host loading mechanism, and a style-isolation decision, none of which exist — and inventing
|
||||
them here would expand the manifest and runtime contract before a host can validate them.
|
||||
|
||||
> **Correction — this rule is superseded.**
|
||||
> [ADR 0040](0040-module-stylesheets-and-artifact-isolation.md) supplies the three missing pieces and
|
||||
> reverses this one decision: a module may import plain `.css` from a browser-artifact source root,
|
||||
> the canonical builder compiles it **into** the declared artifact as text (so no sidecar is ever
|
||||
> emitted, which is what this section was protecting), and the host installs it into a Shadow DOM
|
||||
> root it creates for every artifact. No manifest field was needed after all. The rest of this ADR —
|
||||
> esbuild, bundled React, fixed source paths, no author bundler configuration, unminified output — is
|
||||
> unchanged.
|
||||
|
||||
### Browser TypeScript stays scoped to UI source
|
||||
|
||||
`@ekklesia/typescript-config/browser-react.json` extends the existing `browser.json` and sets
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 0040: Module stylesheets, and every browser artifact gets an isolated root
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-27
|
||||
- Supersedes: none
|
||||
- Related: [ADR 0021](0021-modules-as-hostable-products.md),
|
||||
[ADR 0022](0022-module-ui-authoring-and-host-contract.md),
|
||||
[ADR 0023](0023-module-ui-build-pipeline.md),
|
||||
[ADR 0024](0024-browser-safe-ui-sdk-and-host-loader.md),
|
||||
[ADR 0033](0033-display-kiosk-host-contract.md),
|
||||
[ADR 0039](0039-shared-design-tokens-and-elements.md)
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0023 rejected stylesheet imports in module UI, and gave an honest reason: esbuild would emit a
|
||||
`.css` sidecar beside the artifact, nothing would declare it, no host would fetch it, and it would
|
||||
*work in a local test and silently do nothing in a real host*. Inline style objects were the whole
|
||||
supported story, and a stylesheet contract was deferred to "a manifest field, a host loading
|
||||
mechanism, and a style-isolation decision, none of which exist yet."
|
||||
|
||||
Two of those three now exist or are unnecessary, and the remaining gap is blocking real work:
|
||||
|
||||
- Inline style objects cannot express `:hover`, `:focus-visible`, `:disabled`, `@media`, or
|
||||
`@keyframes`. A production management screen needs all of them, and an accessible focus treatment
|
||||
needs `:focus-visible` specifically. This is not a preference; it is a capability inline styles do
|
||||
not have.
|
||||
- ADR 0039's shared elements are built from real CSS for exactly that reason, so a module using them
|
||||
already needs somewhere for a stylesheet to live.
|
||||
- A contributed renderer (a later ADR) makes the isolation question urgent rather than theoretical:
|
||||
one module's renderer mounted inside another module's screen is precisely where a leaked selector
|
||||
does damage nobody can trace.
|
||||
|
||||
The manifest field turns out not to be needed, and the loading mechanism and the isolation decision
|
||||
turn out to be the same decision.
|
||||
|
||||
## Decision
|
||||
|
||||
### A stylesheet is compiled **into** the declared artifact, as text
|
||||
|
||||
A module imports its local `.css` files. `@ekklesia/module-ui-build` compiles each one — resolving
|
||||
local `@import`s through a nested bundle — into a JavaScript module whose default export is that
|
||||
stylesheet's text. The author passes the text to `defineModuleUi({ styles })`.
|
||||
|
||||
That is why no manifest field is required. There is still exactly **one** declared file per browser
|
||||
role, containing everything: nothing new to publish, nothing for a host to fetch, no second URL, and
|
||||
no way for a stylesheet to become an undeclared sidecar — the failure ADR 0023 named. The builder
|
||||
additionally fails if any CSS output file is ever produced beside an artifact, so a future option or
|
||||
loader change cannot quietly reintroduce one.
|
||||
|
||||
Two rules are enforced on the compiled text, each with its own sanitized error code:
|
||||
|
||||
- **A module stylesheet may reference Ekklesia's global theme variables and must never declare one.**
|
||||
Declaring `--ekklesia-*` would invent a global outside the registry a future theme editor
|
||||
enumerates (ADR 0039). A private alias under the module's own prefix is the supported way to name
|
||||
a local value, and is explicitly allowed.
|
||||
- **An `@import` that survives compilation is rejected.** A local import is inlined and leaves
|
||||
nothing behind, so anything remaining is a remote or unresolvable reference, and an artifact that
|
||||
fetches a stylesheet at run time is not self-contained.
|
||||
|
||||
Plain `.css` only. `.scss`, `.less`, `.styl`, and friends stay rejected: adding a preprocessor is a
|
||||
language and toolchain decision needing its own ADR, not a detail of a build pass.
|
||||
|
||||
`pnpm boundaries` allows `.css` inside a module **only** under the browser-artifact source roots the
|
||||
canonical builder actually compiles (`src/ui/`, `src/display/`, and the contribution sources a later
|
||||
ADR adds). Anywhere else in a module a stylesheet is a file no build reads and no host loads — the
|
||||
undeclared sidecar again, just relocated. Every other stylesheet dialect stays rejected everywhere.
|
||||
|
||||
### Every browser artifact mounts inside an isolated root
|
||||
|
||||
The generic loader creates a Shadow DOM root inside the host's container, installs the artifact's
|
||||
stylesheet there, and hands the artifact an element inside that root:
|
||||
|
||||
```text
|
||||
host container ← the host's element; the loader never mutates it beyond this one child
|
||||
└─ <div data-ekklesia-module-ui> ← shadow host, created and removed by the loader
|
||||
#shadow-root (open)
|
||||
├─ <style data-ekklesia-module-ui-styles> base rules, then the artifact's declared CSS
|
||||
└─ <div data-ekklesia-module-ui-root> ← the Element handed to mount()
|
||||
```
|
||||
|
||||
Shadow DOM is chosen because it is the one mechanism in a browser that **enforces** the rule rather
|
||||
than describing it: rules inside a shadow root do not apply outside it, and document rules do not
|
||||
apply inside it. A class-name convention, a build-time prefix, or a "please scope your selectors"
|
||||
instruction are conventions that one mistake defeats, and the failure mode is a module silently
|
||||
restyling the Dashboard or another module's renderer.
|
||||
|
||||
This applies to **every** role — primary UI, display UI, and contributed renderers — unconditionally,
|
||||
including artifacts that declare no stylesheet. One mounting path, not a styled one and an unstyled
|
||||
one, because a conditional isolation boundary is one a module can accidentally fall out of.
|
||||
|
||||
Both wrapper elements are `display: contents` through a base stylesheet the loader installs *before*
|
||||
the artifact's own, so the loader's two elements contribute no boxes and an artifact lays out exactly
|
||||
as it did before isolation existed — and an artifact that genuinely needs a box (a full-viewport
|
||||
display) overrides `:host` by ordinary cascade order rather than by anything the host special-cases.
|
||||
|
||||
`mode: "open"` is deliberate. **This is not a security boundary, and must not be described as one.**
|
||||
An installed module is trusted first-party code running in the host's own realm (ADR 0022, ADR 0024);
|
||||
it could reach `document` directly if it wanted to. `closed` would buy no protection while making the
|
||||
artifact's DOM unreachable to the host's own tests and diagnostics. This is a correctness boundary
|
||||
for *styles*.
|
||||
|
||||
Design tokens still reach the artifact: CSS custom properties inherit through a shadow boundary, so a
|
||||
host that installed the theme at its root (ADR 0039) has already supplied every artifact on the page.
|
||||
|
||||
### Mount and unmount are deterministic, in both directions
|
||||
|
||||
Each mount builds a fresh root with exactly one `<style>`. Unmount runs the module's cleanup and then
|
||||
removes the whole root — content and styles together — in a `finally`, so a module whose cleanup
|
||||
throws still has its stylesheet taken out of the page. Remounting cannot accumulate a second copy
|
||||
because there is nothing left to accumulate onto. A mount that throws disposes the root it built, so
|
||||
a failed artifact leaves neither partial DOM nor installed styles behind.
|
||||
|
||||
### The mount ABI is versioned, and the cutover is hard
|
||||
|
||||
An artifact's DOM is no longer a direct child of the host's element, and the definition gained a
|
||||
field. That is observable, so `MODULE_UI_ABI_VERSION` is now `2` and `defineModuleUi` stamps it —
|
||||
the author never writes it, so it can never drift from the SDK the artifact actually compiled
|
||||
against.
|
||||
|
||||
There is no dual acceptance, exactly as there is none for a manifest version (ADR 0006, ADR 0014,
|
||||
ADR 0021, ADR 0033). A version-1 artifact fails with `MODULE_UI_ABI_UNSUPPORTED` — checked first and
|
||||
on its own, so the message is "rebuild your module" rather than a developer hunting for a typo.
|
||||
Migration is one `pnpm build`: the canonical builder owns every option, and a module that declares no
|
||||
stylesheet needs no source change at all.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Module UI can express hover, focus-visible, disabled, media queries, and keyframes, so ADR 0039's
|
||||
accessible focus treatment actually works inside a module artifact.
|
||||
- One module's stylesheet provably cannot reach the Dashboard, another module, or another renderer,
|
||||
and the Dashboard's cannot reach in. Two artifacts of the *same* module are isolated from each
|
||||
other too: they share styles only if they share a source file.
|
||||
- A host's own tests reach an artifact's DOM through its open root rather than through
|
||||
`container.querySelector`. Every mounting test in this repository was updated accordingly; that
|
||||
churn is the visible price of the boundary being real.
|
||||
- Every module must be rebuilt once. Nothing else changes for an author who uses no stylesheet.
|
||||
- The artifact is larger by the size of its CSS, exactly as it is larger by the size of React
|
||||
(ADR 0023). It remains one file.
|
||||
- A module still cannot reference a bundled image or font from CSS: no asset loader is configured, so
|
||||
such a reference fails the build. Data URLs work; object storage remains the answer for real media.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Keep inline styles only.** Rejected: it cannot express the pseudo-classes an accessible control
|
||||
needs, and the workaround — JavaScript hover and focus tracking — is worse in every way.
|
||||
- **Publish a `.css` sidecar and have the host `<link>` it.** Rejected: it needs a manifest field, a
|
||||
second publication route, a second cache policy, and a second failure mode, and it reintroduces the
|
||||
exact "a file nothing declares" hazard ADR 0023 refused. Compiling into the one declared artifact
|
||||
costs none of that.
|
||||
- **Scope styles by build-time class prefixing instead of Shadow DOM.** Rejected: a convention a
|
||||
single unprefixed selector defeats, with a silent failure mode and no way for the host to prove
|
||||
containment. It also cannot stop the *host's* rules from reaching into an artifact.
|
||||
- **Isolate only artifacts that declare a stylesheet.** Rejected: an artifact would fall in and out
|
||||
of isolation as a source file changed, and a module rendering the shared elements' `<style>` from
|
||||
inside React would leak into the host document without ever importing a `.css` file.
|
||||
- **`mode: "closed"`.** Rejected as security theatre: the artifact is same-realm trusted code, and
|
||||
closing the root only blinds the host's own diagnostics.
|
||||
- **Accept both ABI versions.** Rejected for consistency with every manifest cutover this repository
|
||||
has made: dual acceptance means one artifact can be read two ways, and the migration here is a
|
||||
rebuild rather than an edit.
|
||||
|
||||
## Deferred
|
||||
|
||||
CSS preprocessors, CSS Modules, bundled image and font assets, `adoptedStyleSheets` for shared
|
||||
constructable stylesheets across artifacts, a host-supplied reset inside artifacts beyond the two
|
||||
layout rules, and any claim that Shadow DOM confines module *code*.
|
||||
@@ -48,3 +48,4 @@ ADRs capture durable choices that affect more than one package or phase.
|
||||
- [0037: Outbound email is a host-provided transport, and SMTP is its first provider](0037-host-provided-outbound-email.md) — Accepted
|
||||
- [0038: Email verification and password recovery, with a trusted-bootstrap exception](0038-email-verification-and-password-recovery.md) — Accepted
|
||||
- [0039: Shared design tokens and foundational elements](0039-shared-design-tokens-and-elements.md) — Accepted
|
||||
- [0040: Module stylesheets, and every browser artifact gets an isolated root](0040-module-stylesheets-and-artifact-isolation.md) — Accepted
|
||||
|
||||
@@ -20,7 +20,11 @@ These rules map platform ownership to filesystem and package boundaries. The exe
|
||||
- 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,
|
||||
`@ekklesia/module-ui-sdk`. Since
|
||||
[ADR 0040](decisions/0040-module-stylesheets-and-artifact-isolation.md) it may also contain plain
|
||||
`.css`, but **only** inside a browser-artifact source root the canonical builder compiles
|
||||
(`src/ui/`, `src/display/`); a stylesheet anywhere else in a module is a file no build reads and no
|
||||
host loads. It still cannot contain `.jsx`, other stylesheet dialects, 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 any shared role
|
||||
beyond the two approved design packages named below. A module needs no bundler plugin: `@ekklesia/module-ui-build` — a `tooling` package,
|
||||
@@ -51,6 +55,14 @@ These rules map platform ownership to filesystem and package boundaries. The exe
|
||||
Node's global declarations — into browser code. Core still owns the containing
|
||||
application shell and all platform policy, and module UI reaches the host only through
|
||||
`@ekklesia/module-ui-sdk`.
|
||||
- A module stylesheet is compiled **into** the one declared JavaScript artifact as text and installed
|
||||
by the host at mount; there is no sidecar file, no second publication route, and no manifest field
|
||||
for one. Every browser artifact is mounted inside its own Shadow DOM root the generic loader
|
||||
creates, so one module's styles cannot reach the Dashboard, another module, or another renderer,
|
||||
and the host's cannot reach in. That root is a correctness boundary for styles, never a sandbox:
|
||||
module code remains trusted, same-realm code. The browser mount ABI is versioned
|
||||
(`MODULE_UI_ABI_VERSION`) and a mismatched artifact fails closed rather than mounting under guessed
|
||||
semantics.
|
||||
- Manifest v6 permits independently optional primary (`entrypoints.ui`) and full-viewport display
|
||||
(`entrypoints.display`) artifacts. Modules own both feature experiences; surfaces own their
|
||||
containers, routes, navigation/chrome, credentials, error states, and publication wiring. The
|
||||
|
||||
@@ -471,20 +471,105 @@ Every other shared package stays rejected inside a module: `@ekklesia/api-client
|
||||
`@ekklesia/auth-client`, `@ekklesia/surface-sdk`, and `@ekklesia/module-ui-host` are not yours to
|
||||
import, and `pnpm boundaries` says so.
|
||||
|
||||
### Styling: inline style objects only, for now
|
||||
### Styling: a stylesheet compiled into your artifact
|
||||
|
||||
Style with React's `style` prop:
|
||||
Author plain `.css` beside your UI source, import it, and declare it
|
||||
([ADR 0040](../architecture/decisions/0040-module-stylesheets-and-artifact-isolation.md)):
|
||||
|
||||
```tsx
|
||||
<section style={{ display: "grid", gap: "0.5rem" }}>…</section>
|
||||
```text
|
||||
src/ui/index.tsx
|
||||
src/ui/styles.css
|
||||
```
|
||||
|
||||
**Stylesheet imports are not supported, and the build fails on one.** That is deliberate rather than
|
||||
an oversight: esbuild would emit a `.css` sidecar beside a primary or display artifact, but no
|
||||
manifest field declares it and no host loads it — so it would work in your tests and silently do
|
||||
nothing in a real host. A stylesheet contract needs a manifest field, a host loading mechanism, and
|
||||
a style-isolation decision, none of which exist yet. Inline styles are the whole supported story for
|
||||
this phase.
|
||||
```tsx
|
||||
import { defineModuleUi } from "@ekklesia/module-ui-sdk";
|
||||
import { EKKLESIA_UI_STYLESHEET } from "@ekklesia/ui";
|
||||
import styles from "./styles.css";
|
||||
|
||||
export default defineModuleUi({
|
||||
id: "announcements",
|
||||
version: "0.1.0",
|
||||
styles: `${EKKLESIA_UI_STYLESHEET}\n${styles}`,
|
||||
mount(container, context) { /* … */ },
|
||||
});
|
||||
```
|
||||
|
||||
TypeScript needs to know what a `.css` import is. Copy the template's `src/module-styles.d.ts` into
|
||||
your module and include it in your UI TypeScript project:
|
||||
|
||||
```ts
|
||||
declare module "*.css" {
|
||||
const stylesheet: string;
|
||||
export default stylesheet;
|
||||
}
|
||||
```
|
||||
|
||||
**There is no sidecar file.** `@ekklesia/module-ui-build` compiles each stylesheet — resolving local
|
||||
`@import`s — into your declared JavaScript artifact as text. One declared file still contains
|
||||
everything, so there is nothing extra to publish, nothing for a host to fetch, and no way for your
|
||||
CSS to become an asset nobody serves. That was the exact hazard that kept stylesheets out until now.
|
||||
|
||||
Two rules are enforced by the build, each with its own error:
|
||||
|
||||
- **Reference Ekklesia's global theme variables; never declare one.** `--ekklesia-*` belongs to the
|
||||
registry a future theme editor enumerates (ADR 0039). Derive private aliases under your own prefix
|
||||
freely: `--announcements-slide-gap: var(--ekklesia-spacing-sm)`.
|
||||
- **Every `@import` must resolve to a local file.** A remote import would make your artifact fetch a
|
||||
stylesheet at run time, and a declared artifact has to be self-contained.
|
||||
|
||||
Plain `.css` only. `.scss`, `.less`, and other dialects stay rejected — adding a preprocessor is a
|
||||
toolchain decision that needs its own ADR. A CSS `url()` pointing at a local image or font also fails
|
||||
the build: no asset loader is configured. Use a data URL, or serve real media from your module's
|
||||
object storage.
|
||||
|
||||
`pnpm boundaries` allows `.css` inside `src/ui/` and `src/display/` only. A stylesheet anywhere else
|
||||
in your module is a file no build reads and no host loads.
|
||||
|
||||
### Your artifact renders in its own isolated root
|
||||
|
||||
Every browser artifact — primary UI, display UI, and any renderer you contribute — is mounted inside
|
||||
a Shadow DOM root the host creates, with your stylesheet installed in it:
|
||||
|
||||
```text
|
||||
host container
|
||||
└─ <div data-ekklesia-module-ui>
|
||||
#shadow-root (open)
|
||||
├─ <style> host base rules, then your declared CSS
|
||||
└─ <div> ← the container your mount() receives
|
||||
```
|
||||
|
||||
What that buys you, and what it costs:
|
||||
|
||||
- **Your selectors cannot leak out**, and the Dashboard's cannot leak in. Write `.slide` without a
|
||||
prefix and without worrying about another module's `.slide`.
|
||||
- **Design tokens still reach you.** CSS custom properties inherit through the boundary, so
|
||||
`var(--ekklesia-…)` resolves from the values the host installed at its root. Nothing to import at
|
||||
run time, nothing for the host to pass.
|
||||
- **Two artifacts of your own module are isolated from each other too.** Your management UI and your
|
||||
display share styles only if they share a source file.
|
||||
- **Your DOM is not in the host document's tree.** `document.querySelector` from outside will not
|
||||
find your elements — including in your own tests, which should mount the artifact and query inside
|
||||
the root the loader hands `mount`.
|
||||
- **You get no box by default.** The host's two wrapper elements are `display: contents`, so you lay
|
||||
out exactly where the host put you. A full-viewport display that needs a real box overrides it:
|
||||
`:host { display: block }`, which works because the host installs its base rules *before* yours.
|
||||
- **Mount and unmount are deterministic.** The host installs your stylesheet once per mount and
|
||||
removes the whole root on unmount, so repeated mounts never accumulate styles and a cleanup that
|
||||
throws still cannot leave your CSS behind. You never install or remove your own `<style>`.
|
||||
|
||||
**This is not a sandbox.** Your artifact is trusted first-party code in the host's own JavaScript
|
||||
realm and can reach `document` if it wants to; the root is `open` for exactly that reason. It is a
|
||||
correctness boundary for styles, and nothing more. See
|
||||
[Security model and deployment trust boundaries](../development/security.md).
|
||||
|
||||
### The mount ABI is versioned
|
||||
|
||||
`defineModuleUi` stamps the browser mount ABI version onto your artifact — you never write it. A host
|
||||
verifies it before mounting, and an artifact built against an older SDK fails with
|
||||
`MODULE_UI_ABI_UNSUPPORTED` rather than being mounted under guessed semantics.
|
||||
|
||||
Migrating from ABI 1 is one `pnpm build`. If you declare no stylesheet, no source change is needed at
|
||||
all; if your own tests assert on module DOM, they need to look inside the isolated root.
|
||||
|
||||
### What you may and may not use today
|
||||
|
||||
@@ -493,7 +578,7 @@ the build pipeline** — adding the pipeline required no boundary relaxation at
|
||||
|
||||
| Allowed in a module | Still rejected |
|
||||
| --- | --- |
|
||||
| `.tsx` source | `.jsx` (strict TypeScript only), `.css`, `.vue`, `.svelte`, `.astro`, `.mdx`, `.svg`, `.html` |
|
||||
| `.tsx` source, and plain `.css` inside `src/ui/` or `src/display/` | `.jsx` (strict TypeScript only), `.css` outside a browser source root, `.scss`/`.less`/`.styl`, `.vue`, `.svelte`, `.astro`, `.mdx`, `.svg`, `.html` |
|
||||
| `react`, `react-dom` | Every other framework, plus `react-router-dom` (routing is the host's), `react-native`, `@vitejs/plugin-react` |
|
||||
| `@ekklesia/module-ui-sdk`, `@ekklesia/module-ui-build`, `@ekklesia/design-tokens`, `@ekklesia/ui` | Every other shared role: `api-client`, `auth-client`, `surface-sdk`, `module-ui-host` |
|
||||
|
||||
@@ -581,8 +666,9 @@ Four things about it matter to you as a module author:
|
||||
- **Only the artifact that role declares is served.** The module id and version in either URL are
|
||||
lookup keys, never path components — there is no way to request a second file from your package.
|
||||
Your source map, any sibling chunk, and every package-root file stay unreachable. If your UI needs
|
||||
an asset, it must be inside the bundle; there is no sidecar contract yet, which is also why a
|
||||
stylesheet import still fails the build.
|
||||
an asset, it must be inside the bundle: that is exactly why your stylesheet is compiled into the
|
||||
artifact as text rather than published beside it (ADR 0040), and why a bundled image or font is
|
||||
still not supported.
|
||||
- **The version must match exactly.** A request for any other version is refused rather than served
|
||||
your installed artifact, so a stale host cannot mount something it did not ask for.
|
||||
- **Your artifact bytes are public.** These two routes require no authentication — fetching code
|
||||
|
||||
Reference in New Issue
Block a user