docs(website): record Phase 2 PR 7 — the client chunk's delivery contract
MODULE_API.md
- 3.1 gains the four normative constraints PR 7 settled: the static root is
the entry's directory (an entry in the module root is rejected), the mount
sits behind the module's state guard with no-cache, the script tag is
injected before </body> so core's bundle runs first, and core renders on
DOMContentLoaded with a readyState === 'complete' check
- 2.1: client.entry must be in a subdirectory; present-but-empty is rejected
- 3.4: AdminPage struck from the UI kit — core has no such component
- 7.7 (new): the browser smoke, and the timing bug no test in this repo can
see. Every test passed against a build that did not work in a browser
MODULE_SYSTEM.md
- 2.6 step 3 amended to the </body> injection point
- 2.7 records PR 7's four decisions and what verified them; 1-7 of 9 done
BACKEND_DESIGN.md
- /modules is the fourth filesystem-conditional static mount outside the
route manifest, with its root, guard and cache policy stated
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# The Module System — design of record
|
||||
|
||||
**Status:** approved design, **in implementation** — Phase 2's core scaffolding is landing on the
|
||||
website `edge` branch, PRs 1–6 of 9 done (§2.7 tracks what each settled). Every decision in Part 3
|
||||
website `edge` branch, PRs 1–7 of 9 done (§2.7 tracks what each settled). Every decision in Part 3
|
||||
has been settled with the org lead; Part 1 records what was verified against the working trees on
|
||||
2026-08-10, including the places the original draft was wrong.
|
||||
|
||||
@@ -267,8 +267,9 @@ It also rules out import maps as the shared-dependency mechanism: `config/csp.js
|
||||
|
||||
**Resolved** — see §2.6. The path that survives all three constraints is: the module's CI ships a
|
||||
**prebuilt ESM chunk**, core hands it React through a **global** rather than an import map, and
|
||||
`htmlShell.js:111` injects a **same-origin** `<script type="module" src>`, which `'self'` already
|
||||
allows.
|
||||
`htmlShell.js` injects a **same-origin** `<script type="module" src>`, which `'self'` already
|
||||
allows. Verified in a browser against the enforced policy in Phase 2 PR 7, not only reasoned about
|
||||
([`MODULE_API.md`](MODULE_API.md) §7.7).
|
||||
|
||||
---
|
||||
|
||||
@@ -442,10 +443,14 @@ builds nothing, production pulls a prebuilt image, and `script-src 'self'` forbi
|
||||
2. **Core exposes the shared dependencies on a global** before mount — `window.__rg = { react,
|
||||
reactDom, router, registry }` — and the module's externals resolve to it. A global, not an import
|
||||
map, precisely because an import map must be inline and CSP forbids that.
|
||||
3. **`htmlShell.js` injects the module's entry script.** It already rewrites `</head>`
|
||||
(`utils/htmlShell.js:111`), so this is an extension of a working mechanism, not a new one. The tag
|
||||
is `<script type="module" src="/modules/uo/entry.js">` — same-origin, so `'self'` passes with no
|
||||
nonce and no inline.
|
||||
3. **`htmlShell.js` injects the module's entry script.** It already rewrites the shell it serves, so
|
||||
this is an extension of a working mechanism, not a new one. The tag is
|
||||
`<script type="module" src="/modules/uo/entry.js">` — same-origin, so `'self'` passes with no
|
||||
nonce and no inline. *(Amended in Phase 2 PR 7: the tag is injected before `</body>`, not at the
|
||||
`</head>` rewrite this step assumed. Module scripts execute in document order and core's bundle
|
||||
has to run first, so the injection must be after core's own script tag wherever a bundler chooses
|
||||
to put it — [`MODULE_API.md`](MODULE_API.md) §3.1, which also states the static mount's root, its
|
||||
state guard and its cache policy.)*
|
||||
4. **The SPA reads `/api/v1/public/modules`** to feature-detect against what this backend is
|
||||
serving. Registration happens when the injected chunk executes and calls `window.__rg.registry` —
|
||||
it is not gated on this call. *(Amended by [`MODULE_API.md`](MODULE_API.md) §6.7: this step
|
||||
@@ -489,8 +494,8 @@ too (API §7.2).
|
||||
**serving**, shaped like the existing branding/site-settings endpoints (anonymous, database-free,
|
||||
not site-mode gated). The SPA and the Android plan both feature-detect against it; it is not what
|
||||
loads a client chunk ([`MODULE_API.md`](MODULE_API.md) §2.9 and §6.7).
|
||||
7. Client `src/modules/registry.js`, the `window.__rg` shared-dependency global, and the
|
||||
`htmlShell` script injection — empty registry, no visible change.
|
||||
7. Client `src/modules/registry.js`, the `window.__rg` shared-dependency global, the chunk's static
|
||||
mount and the `htmlShell` script injection — empty registry, no visible change.
|
||||
8. `MOD_PATHS` → `roles`-derived (§1.4); the generic feature-provider seam (§1.5).
|
||||
9. `docker-compose.yml` gains the `modules` volume.
|
||||
|
||||
@@ -499,7 +504,7 @@ it is wrong. PR 6 is the single deliberate exception in the phase and it *adds*:
|
||||
gains exactly one line, `GET /api/v1/public/modules`, and nothing else in the file moves. Every other
|
||||
PR in Phase 2 produces a zero-line diff.
|
||||
|
||||
**Progress: PRs 1-6 done.**
|
||||
**Progress: PRs 1-7 done.**
|
||||
|
||||
- **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules
|
||||
settled in §2.4 above.
|
||||
@@ -593,6 +598,35 @@ fixed enum because the leg set is whatever has been registered.
|
||||
schemas. The published mirror [`api-route-inventory.json`](./api-route-inventory.json) is refreshed
|
||||
to match.
|
||||
|
||||
- **PR 7** — the client half's delivery: `client/src/modules/registry.js`, `window.__rg`
|
||||
(`modules/shared.js`), the chunk's static mount and the `htmlShell` injection, with `App.jsx`
|
||||
reading `routesFor` for all three areas. The registry is empty on a bare core, so nothing an
|
||||
operator can see changes. Four decisions, all recorded in [`MODULE_API.md`](MODULE_API.md) §3.1 and
|
||||
§3.4: **routes now, nav in PR 8** — PR 7 is "a module chunk loads and renders its page", PR 8 is
|
||||
"it appears in the nav", which keeps the nav interleave and its override merge in one reviewable
|
||||
change; **the script tag is injected before `</body>`**, not into `</head>`, so the ordering that
|
||||
the whole client contract rests on comes from document structure rather than from Vite's choice to
|
||||
hoist core's entry into `<head>`; **the static mount is rooted at the entry's directory, behind the
|
||||
module's state guard, with `no-cache`** — a mount rooted at the module root would publish server
|
||||
source, `module.json` and the schema fragment, so an entry in the module root is rejected outright;
|
||||
and **the UI kit ships its seven real members**, with `AdminPage` struck from the contract rather
|
||||
than invented in core to satisfy a table.
|
||||
|
||||
The verification that mattered was **not a test**. Everything passed against a build that did not
|
||||
work in a browser: core mounted before any module chunk had evaluated, because `document.readyState`
|
||||
during a deferred script is `'interactive'`, not `'loading'`. A module's routes were missing from
|
||||
the first render and its URL redirected home — indistinguishable from a module that failed to load,
|
||||
and with nothing logged anywhere. It was found by loading a hand-written chunk in Chrome, and that
|
||||
smoke is now written down as part of the contract ([`MODULE_API.md`](MODULE_API.md) §7.7) because no
|
||||
test in this repo can see it. The same run confirmed the property §3.6 called the highest-risk
|
||||
detail in the plan: the chunk executes under the **enforced** `script-src 'self'`, resolving core's
|
||||
React and UI kit off the global, with zero CSP reports.
|
||||
|
||||
**933 server tests** (+23) and **123 client tests** (+14) pass; `routes.manifest.json` is unchanged
|
||||
at 230 routes and the OpenAPI spec regenerates byte-identical — `/modules/<id>/` is a
|
||||
filesystem-conditional static mount, not API surface, for the same reason `/uploads` and `/brand`
|
||||
are not in the manifest.
|
||||
|
||||
**Phase 3 — Extract `module-uo`.** Moves out of `website/`: the 8 model directories and their 25
|
||||
tables; the nine UO `utils/` files plus `newsGump.js`; the 13 router/controller files;
|
||||
`scripts/importSpawnAtlas.js` and `db/spawnAtlas.art.json`; `usersShard.controller.js` **minus
|
||||
|
||||
Reference in New Issue
Block a user