wtclaude 28f4b9afe2 feat(client): the whole client half (phase 3, slice 3)
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>
2026-08-11 18:00:25 -05:00

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.

Repo What
thisRunicGateway/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.

Description
No description provided
Readme 724 KiB
v0.3.0 Latest
2026-08-12 04:11:54 +00:00
Languages
JavaScript 100%