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>
102 lines
4.6 KiB
JavaScript
102 lines
4.6 KiB
JavaScript
// ── window.__rg — the shared-dependency global ─────────────────────────────
|
|
//
|
|
// Phase 2, PR 7 of docs/website/MODULE_SYSTEM.md §2.7; the normative shape is
|
|
// docs/website/MODULE_API.md §3.2.
|
|
//
|
|
// A module's client half is a PREBUILT ESM chunk — the operator never builds
|
|
// anything (MODULE_SYSTEM.md §1.14) — served same-origin and loaded under
|
|
// `script-src 'self'` with no 'unsafe-inline'. That combination is what rules out
|
|
// an import map: an import map has to be an inline `<script type="importmap">`,
|
|
// and the policy forbids inline scripts outright. So the shared dependencies ride
|
|
// on a global, and the module's Rollup externals are aliased to two-line shims
|
|
// that re-export from it (§3.6).
|
|
//
|
|
// **There is exactly one React in the page and core owns it.** A module that
|
|
// bundled its own would get a second hook dispatcher and fail at its first
|
|
// useState. That is the same rule the server half enforces for `express` and
|
|
// `express-validator` on `ctx`, and for the same reason: anything shared between
|
|
// core and a module is owned by core and HANDED OVER, never resolved by the
|
|
// module.
|
|
|
|
import * as react from 'react'
|
|
import * as reactDom from 'react-dom/client'
|
|
import * as router from 'react-router-dom'
|
|
// The automatic JSX runtime, and it is not decoration. A module's bundler
|
|
// compiles every .jsx file to imports from `react/jsx-runtime` under the modern
|
|
// default, and those have to resolve to CORE's React like every other import.
|
|
// Without it here a module would have to build with `jsxRuntime: 'classic'`;
|
|
// with it, a module uses the default its tooling already assumes.
|
|
import * as jsxRuntime from 'react/jsx-runtime'
|
|
|
|
import { registry } from './registry.js'
|
|
import { MODULE_API_VERSION } from './version.js'
|
|
|
|
import PublicLayout from '../components/PublicLayout.jsx'
|
|
import PageHeader from '../components/PageHeader.jsx'
|
|
import { Loading, ErrorState, EmptyState } from '../components/PageState.jsx'
|
|
import { useAsync } from '../lib/useAsync.js'
|
|
import { useAuth } from '../contexts/AuthContext.jsx'
|
|
import { useSite } from '../contexts/SiteContext.jsx'
|
|
import { request, ApiError, BASE } from '../api/client.js'
|
|
|
|
// The UI kit is CURATED AND CLOSED (§3.4), not a re-export of components/. These
|
|
// seven are what the smallest UO page already needs beyond React and the router:
|
|
// without them a module either reaches into core's tree — violating the
|
|
// zero-import rule the whole boundary rests on — or ships its own copies, which
|
|
// means a module page that does not look like the site it is installed in, and
|
|
// that drifts further every time core's layout changes.
|
|
//
|
|
// Adding a member is a MINOR MODULE_API_VERSION bump; changing a member's props
|
|
// is a MAJOR one. That is a real constraint on core's own refactoring and it is
|
|
// the price of the boundary being worth anything.
|
|
//
|
|
// `AdminPage` appears in §3.4's table and is deliberately absent: core has no
|
|
// such component — admin views are plain markup inside AdminLayout — and
|
|
// inventing one to satisfy a table would be a core change with no consumer until
|
|
// Phase 3. The contract is amended rather than the code padded, and adding it
|
|
// later costs a minor bump, which is exactly the case the versioning is for.
|
|
const ui = {
|
|
PublicLayout,
|
|
PageHeader,
|
|
Loading,
|
|
ErrorState,
|
|
EmptyState,
|
|
useAsync,
|
|
useAuth,
|
|
useSite,
|
|
}
|
|
|
|
// The request PRIMITIVE, not the `api` object (§3.5): a module builds its own
|
|
// namespace over `request` and owns the paths it calls, which is right, because
|
|
// it owns the routes at the other end.
|
|
//
|
|
// `BASE` was in §3.5 from the start and missing from this object until slice 3,
|
|
// which is when something first needed it. `request` is fetch-only, so an
|
|
// EventSource — the shard's live feed is two of them — has to build its own URL,
|
|
// and the alternative is a module hardcoding `/api/v1`: an assertion about where
|
|
// core mounts its API that core has never promised to keep.
|
|
const api = { request, ApiError, BASE }
|
|
|
|
/**
|
|
* Publish `window.__rg`. Called by main.jsx before it renders, and before any
|
|
* module chunk evaluates.
|
|
*
|
|
* Frozen, one level down as well as at the top: the object a module reaches for
|
|
* its React is not somewhere a module gets to leave something for the next one.
|
|
* Cross-module communication is a thing the contract does not have, and an
|
|
* unfrozen global is how a codebase acquires one by accident.
|
|
*/
|
|
export function publishSharedDependencies() {
|
|
window.__rg = Object.freeze({
|
|
version: MODULE_API_VERSION,
|
|
react,
|
|
reactDom,
|
|
router,
|
|
jsxRuntime,
|
|
registry,
|
|
ui: Object.freeze(ui),
|
|
api: Object.freeze(api),
|
|
})
|
|
return window.__rg
|
|
}
|