Files
Module-uo/README.md
wtclaude 5d7668d5ea
Some checks failed
PR Checks / server-tests (pull_request) Failing after 10s
PR Checks / client-build (pull_request) Successful in 8m45s
feat(module): the bundle skeleton (phase 3, slice 0)
The first real module. It registers nothing, deliberately: what slice 0 proves
is the delivery path itself, end to end, before a single UO file moves into it.

Server half: module.json, an entry point that takes (ctx, api) and registers
nothing, a test suite built on a fake ctx, and scripts/checkImports.js -- the
MODULE_API.md §5.1 boundary check. Client half: the Vite library build, four
shims re-exporting react / react-dom/client / react-router-dom / jsx-runtime
from window.__rg, an entry that verifies each is identity-equal to core's copy,
and scripts/checkExternals.js. 29 server tests, 9 client tests, both new.

Verified against a real core: the module loads, mounts its zero routes, runs to
`started`, and is published by /api/v1/public/modules. Its chunk serves from
the entry's directory with `Cache-Control: no-cache` while the module's server
source, module.json and package.json all 404. In Chrome, under the enforced
`script-src 'self'`, the chunk evaluates and reports all four shared
dependencies OK, with zero CSP reports and no console errors.

Three findings, each of which had produced a green build that was wrong.

MODULE_API.md §3.6 shows `external` alongside the aliases and they do not
compose. Rollup asks `external` BEFORE Vite's alias resolver runs, so a
specifier in both is marked external and never aliased -- the chunk then ships
bare `import "react"`, which no browser can resolve without an import map, and
CSP forbids one. Built cleanly and emitted exactly that; checkExternals caught
it. So: alias only, `external` empty, and vite.config.js grows a resolution-time
guard that fails the build if a shared dependency resolves into node_modules.

That guard was wrong twice before it worked. Written against Rollup's `load`
hook it never ran -- `load` is first-wins and an earlier plugin had already
claimed the module -- so a deliberately-broken alias produced a 24 kB chunk with
react-router welded in, and a green build. And its forbidden-package list was
derived from the alias list "so the two cannot disagree", which meant deleting
an alias also deleted the guard against what that alias prevented. It states the
contract now, and a test asserts the aliases stay inside it.

checkImports failed on its own documentation the first time it ran: the comment
naming require("../../etc/passwd") as an example of what to catch, and index.js
explaining why the module must never require("express"). A boundary check that
cannot survive being described is one people stop writing comments around. It
strips comments and template literals with a character walk rather than a
regexp, because a URL in a string contains a comment opener and a comment
contains quotes -- and it has its own test suite, since a check never shown to
fail is a check nobody knows the state of.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 01:31:37 -05:00

134 lines
7.8 KiB
Markdown

# module-uo — the Ultima Online module for Runic Gateway
The Runic Gateway website is becoming **game-agnostic**: core keeps accounts, sessions, the wiki,
posts, branding, theming and the admin panel, and everything that knows what a *shard* is moves out
into an installable module. This repo is that module — the first one, and the reference for every
module that follows.
```
RunicGateway/website (core — game-agnostic)
│ loads modules at boot, synchronously, from the filesystem
┌───────────────────────────────────────────┐
│ module-uo (>>> HERE <<<) │
│ shard status · spawn atlas · marketplace │
│ governors · cliloc · town crier · uo-link│
└───────────────────────────────────────────┘
│ server half: routers, models, schema fragment
│ client half: prebuilt ESM chunk, SPA routes + nav
the shard bridge (RunicGateway/link → RunicGateway/servuo-plugins)
```
The module's **id** is `uo` — that is what appears in `module.json`, in the `installed_modules`
table, in the `modules/<id>/` path on disk and in the URL segment (`/uo/*`, `/admin/uo/*`,
`/player/uo/*`). `Module-uo` is the repository; `module-uo` is the module and its release artifact.
## Status: the bundle skeleton exists; the extraction has started
The design of record is
[`website/MODULE_SYSTEM.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_SYSTEM.md)
and the normative contract is
[`website/MODULE_API.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md)
in the docs repo — **read them before opening a PR here.** Where the two differ, the contract wins.
| Phase | Where it happens | State |
|---|---|---|
| 0 — CI trigger fix, cut `website` `edge`, bootstrap this repo | `website`, here | ✅ done |
| 1 — module API contract (`docs/website/MODULE_API.md`) + the atlas spike | `docs`, `website` | ✅ done |
| 2 — core scaffolding: loader, `installed_modules`, registries, client registry | `website` | ✅ done |
| 3 — extract the UO half of the site into this repo | `website`, here | 🟡 in progress |
| 4 — delivery: the admin Modules screen + the Docker path | `website` | ⬜ |
Phase 3 moves the UO half of `website/` here in ten slices (`MODULE_SYSTEM.md` §2.7.1), server-first
and then client. Each slice is one PR here that adds, and one PR in `website` that deletes — this one
merging first, so `website`'s `edge` branch serves the feature from core right up to the moment core
drops it.
**Slice 0 is the bundle skeleton, and it registers nothing on purpose.** What it proves is the
delivery path itself: core discovers the module, validates `module.json`, calls `register()`, serves
the client chunk, injects it, and reports the module `started` — and the chunk resolves React, the
renderer and the router from core's `window.__rg` rather than bundling its own. Every slice after
this one adds registrations to `server/index.js` and `client/src/entry.jsx`.
## Working on it
```bash
npm ci --prefix server && npm test --prefix server && npm run check:imports --prefix server
npm ci --prefix client && npm test --prefix client && npm run build --prefix client
npm run check:externals --prefix client # asks the BUILT chunk, so it runs after the build
```
The two `check:*` scripts are the contract's acceptance criteria rather than this module's own tests:
no import may escape the module root (`MODULE_API.md` §5.1), and no bare specifier may survive into
the built chunk (§3.6). The matching failure — a shared dependency being *bundled* — fails the build
itself, from a guard inside `vite.config.js`.
Running it against a real core means checking this repo out as `website/modules/uo`, building the
client half, and booting core. The four-step browser smoke in `MODULE_API.md` §7.7 is the only thing
that proves the client half works: its real failure modes are timing and module resolution, and
neither has a shape a DOM-less test runner can see.
## What it contains
One repo, one bundle: the server half and the client half live side by side and version together, so
a route and the screen that calls it can never be mismatched. A ✅ is in the tree today.
```
module.json ✅ id, version, coreApi range, mounts, extensions
server/index.js ✅ the entry point — register(ctx, api), synchronous, no database
server/scripts/ ✅ checkImports.js — the §5.1 boundary check
server/test/ ✅ node --test, with a fake ctx standing in for core
server/ routers, controllers, models, utils
server/db/schema.sql idempotent fragment, replayed by core's ensureSchema()
server/db/purge.sql destructive; only ever run by an explicit purge
client/src/entry.jsx ✅ the chunk's entry — registers routes, nav, feature provider
client/src/shim/ ✅ react, react-dom, react-router-dom, jsx-runtime, from window.__rg
client/vite.config.js ✅ the library build, the aliases, the not-bundled guard
client/src/ route components, nav registrations, feature provider
client/dist/ ✅ PREBUILT ESM chunk, built by CI — never by an operator
```
Release artifact: `module-uo-<version>.tar.gz`, plus a manifest carrying its `sha256`.
## How it reaches an operator
**An operator never builds anything.** Installing a module is the WordPress-plugin experience: an
admin-panel action, or a directory mounted into the Docker container — never a build step, because
production runs a prebuilt, pull-only image. That constraint is why the client half ships as a
prebuilt ESM chunk that resolves React from a `window.__rg` global rather than an import map (an
import map must be inline, and the site's CSP is `script-src 'self'`).
The [installer](https://gitea.whitlocktech.com/RunicGateway/installer) is **not** the delivery path.
It deploys the *shard* side — the plugin overlay and the uo-link sidecar — and never contacts the
website. Module delivery is website-side only.
## Compatibility
`module.json` declares a `coreApi` semver range, checked at boot against core's `MODULE_API_VERSION`.
A mismatch fails **loudly** — the module is marked `startup_failed` and the site comes up without it,
rather than mis-loading. This is a separate number from `PROTOCOL_VERSION`, which versions the shard
wire protocol and says nothing about a website module.
A module that fails to load must never take the site down.
## Related repos
| Repo | What |
|------|------|
| **this**`RunicGateway/Module-uo` | The UO module: the game-specific half of the website. |
| [RunicGateway/website](https://gitea.whitlocktech.com/RunicGateway/website) | Core — the site, admin panel and API that loads this module. |
| [RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link) | The **uo-link sidecar** — the network-facing half of the game bridge this module talks to. |
| [RunicGateway/servuo-plugins](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) | The **C# ServUO plugin** that feeds the sidecar. |
| [RunicGateway/installer](https://gitea.whitlocktech.com/RunicGateway/installer) | Deploys the shard side. Not the module delivery path. |
| [RunicGateway/docs](https://gitea.whitlocktech.com/RunicGateway/docs) | All project documentation, including the module system design and this module's docs under `modules/uo/`. |
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). Contributions are welcome, AI assistance must be disclosed,
and security problems go to [SECURITY.md](SECURITY.md) rather than a public issue.
## License
**GNU General Public License v3.0 or later** — see [LICENSE.md](LICENSE.md).