The three things core owes the client half before it can leave, all additive, all MODULE_API 1.2.0 → 1.3.0. `player.invite.accepted` is the third extension slot. Core's invite page owned a UO game-account step — it read a `gameAccountSignup` flag out of core's own settings and posted to a shard route — and an invite is a core concept that staff receive too, so the page stays and its optional next step becomes a slot. Named for the place, like the other two. Whether there is a step at all is the filling module's call, made from data core does not have; core keeps the shell, the skip control and the destination. `icon` on a nav item, because without it the six extracted UO rows would have been the only text-only entries in a sidebar where every other row has a glyph. Core supplies no fallback — an invented one is core making a presentation choice for content it knows nothing about. `icon` was already among the fields an override may not touch, so the concept predates a module being able to send one. `api.BASE` was in §3.5 from the first draft and never actually published. `request` is fetch-only, so an EventSource builds its own URL, and the shard's live feed is two of them; the alternative is a module hardcoding `/api/v1`, which asserts something about core that core has not promised. `AcceptInvite` is the one legitimate reader of `extensionFor` outside Slot.jsx: the answer decides a NAVIGATION, not a decoration. Decoration goes inside `<Slot wrap>`, which is why `hasExtension` stayed deleted. Co-Authored-By: Claude <noreply@anthropic.com>
99 lines
5.3 KiB
JavaScript
99 lines
5.3 KiB
JavaScript
import React from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import { BrowserRouter } from 'react-router-dom'
|
|
import App from './App.jsx'
|
|
import { publishSharedDependencies } from './modules/shared.js'
|
|
import { declareSlot } from './modules/registry.js'
|
|
import './styles/theme.css'
|
|
|
|
// Publish window.__rg BEFORE rendering and before any module chunk evaluates.
|
|
// Installed modules arrive as `<script type="module" src="/modules/<id>/…">`
|
|
// tags the server injects into the shell (server/src/utils/htmlShell.js), placed
|
|
// after this bundle's own tag; module scripts execute in document order, so they
|
|
// resolve their externals against the global this call sets up
|
|
// (docs/website/MODULE_API.md §3.2).
|
|
publishSharedDependencies()
|
|
|
|
// Core registered a feature provider here until slice 3, under owner id `core`
|
|
// and namespace `uo`, so that the seam was exercised by real content from the
|
|
// day it was built. That prediction paid out exactly as written: the extraction
|
|
// deleted the registration and the hook it named, and SiteHeader was not touched.
|
|
// There is nothing for core to register now — no core nav row carries a
|
|
// `feature` — and the filter is a correct no-op until a module supplies one.
|
|
|
|
// ── Extension slots (MODULE_API.md §3.7) ───────────────────────────────────
|
|
//
|
|
// Declared HERE, in core's own bundle, which is what makes the ordering a fact
|
|
// rather than a hope: module chunks are deferred scripts the shell injects after
|
|
// this one (§3.1), so a module can never reach registerExtension before the slot
|
|
// it names exists. "Unknown slot" therefore always means a typo or a version
|
|
// skew, never a load-order accident — which is why that case throws.
|
|
//
|
|
// Both slots are named for a PLACE, not for a meaning. `site.footer.status` is
|
|
// the spot in the footer's info row, not a declaration that core knows what a
|
|
// game server's status is; the label, the target and whether anything renders at
|
|
// all belong to whoever fills it. A slot typed by its content would put game
|
|
// semantics back into core, which is the thing Phase 3 takes out.
|
|
declareSlot('site.footer.status')
|
|
// Deliberately the same name as the server's slot (MODULE_API.md §2.4): one
|
|
// resource, one extension point, two halves. The module with routes under
|
|
// /api/v1/admin/users/:id is the module with something to show on that page.
|
|
declareSlot('admin.users.detail')
|
|
// The invite-acceptance page's optional next step. Core owns invites — staff are
|
|
// invited too — and owned the game-account step inside them until slice 3, which
|
|
// meant core reading a `gameAccountSignup` flag and posting to a shard route.
|
|
//
|
|
// Named for the place, like the other two: it is "the point after an invite has
|
|
// been accepted and before the invitee is sent on", not "create a game account".
|
|
// Whether there is a step at all is the filling module's decision, made from
|
|
// data core does not have; core renders the shell and a skip control, and hands
|
|
// over `onDone`. With the slot unfilled the invitee goes straight to the portal,
|
|
// which is what core's own code did whenever the flag was off.
|
|
declareSlot('player.invite.accepted')
|
|
|
|
// Core filled the first two itself until slice 3, with the components that were
|
|
// inline in SiteFooter.jsx and UserDetail.jsx. Both are gone: the module fills
|
|
// all three, and core's own fills had to go for it to be able to — the first
|
|
// fill wins, and core registered first (§3.7).
|
|
|
|
// Render on DOMContentLoaded rather than immediately, and that is the one line
|
|
// of core's boot the module system changes.
|
|
//
|
|
// Deferred scripts — which every `type="module"` script is — execute in document
|
|
// order and ALL of them finish before DOMContentLoaded fires. Waiting for that
|
|
// event is therefore the guarantee that every installed module has registered
|
|
// its routes before React reads the registry: no loading state, no re-render,
|
|
// and no ordering race between core's bundle and a module's. A module chunk that
|
|
// 404s or throws does not hold the event back, so a broken module costs its own
|
|
// pages and not the site.
|
|
//
|
|
// The readyState check below is `'complete'`, and it is not the obvious
|
|
// `'loading'`. A DEFERRED script — which every `type="module"` script is — runs
|
|
// after the document has been parsed, so by the time this line executes
|
|
// readyState is already `'interactive'`; DOMContentLoaded has NOT fired yet and
|
|
// still comes after every deferred script. Testing for `'loading'` therefore
|
|
// mounts immediately, before any module chunk has evaluated, and a module's
|
|
// routes are missing from the very first render — which looks exactly like a
|
|
// module that failed to load: its URL falls through to core's catch-all and
|
|
// redirects home. Found by loading a real chunk in a browser; no unit test in
|
|
// this repo can see it.
|
|
//
|
|
// `'complete'` is only reached after `load`, which is strictly later than any
|
|
// static deferred script, so this branch is the genuine "the event has already
|
|
// been and gone" case and not a wrong guess about our own timing.
|
|
function mount() {
|
|
createRoot(document.getElementById('root')).render(
|
|
<React.StrictMode>
|
|
<BrowserRouter>
|
|
<App />
|
|
</BrowserRouter>
|
|
</React.StrictMode>,
|
|
)
|
|
}
|
|
|
|
if (document.readyState === 'complete') {
|
|
mount()
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', mount, { once: true })
|
|
}
|