wtclaude 47809854ef
All checks were successful
PR Checks / server-tests (pull_request) Successful in 12s
PR Checks / client-build (pull_request) Successful in 15s
fix(module): ask Node whether a specifier is a builtin
The first CI run failed on `node:test`, in every test file, reported as the
module boundary being broken. It was not: `builtinModules` omits `test` on
Node 20 (CI) and includes it on Node 24 (local), so a list rebuilt from it
disagrees with itself across versions.

Use `isBuiltin`, which is Node's own answer, and treat the `node:` prefix as
sufficient on its own -- a prefixed specifier can never resolve to a package,
whatever the running version enumerates. Test covers both forms.

Also corrects this file's header: the client half's guard is no longer
`external` (it never worked), it is the Vite build's resolution-time check plus
checkExternals.js on the built chunk.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 01:36:57 -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 713 KiB
v0.3.0 Latest
2026-08-12 04:11:54 +00:00
Languages
JavaScript 100%