The 35 files behind twelve public pages, seven admin views, two player views and three core-page extensions, ported onto `window.__rg`. Every one of them imports exactly the seven kit members plus `lib/format.js`, which is the finding §2.7.1 predicted and this confirms. `client/src/core.js` is the port mechanism, and unlike the server's it is a plain read: `window.__rg` is published before any module chunk evaluates, so there is no gap to defer around and a ported component keeps its ordinary import shape. `client/src/api.js` rebuilds the UO namespaces over the request primitive — same URLs, because §1.2 freezes the API surface. SPA paths changed and API paths did not. `/site/shard` is `/uo/shard`, and the admin paths lost their now-redundant `shard-` prefixes (`/admin/uo/ops`), a clean break being the only moment that is free. `shim/rg.js` becomes the single reader of the global, so the "core did not publish its dependencies" message is reachable from whichever module the bundler happens to touch first rather than from whichever one is imported first — a guarantee that used to last until someone sorted the imports. Co-Authored-By: Claude <noreply@anthropic.com>
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
and the normative contract is
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
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 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 | Core — the site, admin panel and API that loads this module. |
| RunicGateway/link | The uo-link sidecar — the network-facing half of the game bridge this module talks to. |
| RunicGateway/servuo-plugins | The C# ServUO plugin that feeds the sidecar. |
| RunicGateway/installer | Deploys the shard side. Not the module delivery path. |
| RunicGateway/docs | All project documentation, including the module system design and this module's docs under modules/uo/. |
Contributing
See CONTRIBUTING.md. Contributions are welcome, AI assistance must be disclosed, and security problems go to SECURITY.md rather than a public issue.
License
GNU General Public License v3.0 or later — see LICENSE.md.