// ── The server entry point ───────────────────────────────────────────────── // // Core requires this file once, synchronously, while its own `app.js` is still // being required, and calls the exported function with `(ctx, api)`. That is the // entire server-side handshake: everything this module can reach arrives on // `ctx`, and everything it can offer is registered through `api`. // // Normative: MODULE_API.md §2.2 (the entry point) and §2.4 (what you register). // // ── Three rules, and each one has a failure behind it ────────────────────── // // 1. **No `await`, and no database.** Core requires `app.js` in two build tools // with the connection pool pointed at a dead port — the route-manifest // generator and the OpenAPI generator both do it — so a module that queried // at registration time would hang both. Anything that needs a live database // goes in `onBoot`, which runs after the schema is up. // // 2. **Never resolve what core owns.** Your module lives at // `/modules//`, which is outside core's `server/`, so Node's // resolver never reaches core's `node_modules` and `require('express')` from // here simply fails. express, express-validator, the database, the logger and // the middleware all arrive on `ctx` (§2.3) and are re-exported by `./core`. // This is not a style rule: a second express in the process would be a second // `Router` prototype, exactly as a second React would be a second renderer. // // 3. **Never reach into core's tree.** No relative path may escape this module's // root. `scripts/checkImports.js` enforces it (§5.1) and CI runs it. // // ── Why the requires are INSIDE the function ─────────────────────────────── // // Every file below reaches core through `./core`, whose members resolve `ctx` // when they are CALLED. But a router writes `const express = core.express` at its // own file scope, and that runs the moment the file is required. So // `core.init(ctx)` has to happen before the first `require` of anything under // `router/`. Hoisting these to the top of the file breaks the module with an // error about a missing `ctx`, thrown from a file that never mentions one. // // Node caches modules, so requiring here costs nothing after the first call. const core = require('./core') /** * @param {object} ctx what core hands the module (MODULE_API.md §2.3), frozen * @param {object} api what the module registers (§2.4) */ module.exports = function register(ctx, api) { core.init(ctx) /* eslint-disable global-require */ const worldRouter = require('./router/public/world.router') const boot = require('./boot') /* eslint-enable global-require */ const log = core.logger() // One prefix, on one tier. The keys here must match `module.json`'s `mounts` // exactly — the loader compares the two and rejects a mismatch in either // direction, so a route you forgot to declare and a prefix you declared and // never registered both fail loudly at boot instead of quietly at runtime. // // This mounts at `/api/v1/public/world`. The router sits INSIDE the tier // router, so it structurally cannot reach above its prefix, and the tier's own // gate is already applied: `public` is behind nothing by design, `admin` sits // behind `noindex, isLoggedIn, requireRole(...)` and `player` behind // `noindex, requireAuth`. You add per-route gates on top; you never // re-implement the tier gate. // // **Prefixes share one namespace with core's own, and `/world` was chosen to // stay out of it.** Core answers `/api/v1/public/` + contact, modules, pages, // posts, settings, status, version and wiki. The loader rejects a collision at // registration time — but four of those eight are mounted at the tier root // rather than under a prefix of their own, and the loader's probe cannot see // them. `/status` would have been the obvious name for this module's route and // is exactly the one that would have gone wrong. Check the list before you // choose (§2.4, and MODULE_SYSTEM.md §2.7's own note about the probe). api.registerRoutes({ public: { '/world': worldRouter }, }) // The lifecycle hooks (§2.5). `onBoot` runs after core's schema, after this // module's schema fragment, and BEFORE the HTTP listener binds — so a module // that must not serve traffic until it has warmed a cache gets that for free. // It has no timeout, deliberately: a slow boot delays the listener, which is // the guarantee rather than a problem to be timed out. // // `onShutdown` runs while core's database pool and push dispatcher are still // open, because flushing through them is the only thing it is for. It gets a // five-second budget and is abandoned past it. // // Both are optional. A module with neither still reaches `started`. api.onBoot(boot.onBoot) api.onShutdown(boot.onShutdown) log.info('registered', { version: require('../module.json').version, routes: 'public:/world', }) }