# The template module A Runic Gateway module that builds, loads, and does almost nothing. Copy it, rename it, and you have a running module before you have read a chapter. Installed into a core, it adds: - **one public page** at `/examplegame/status`, and a nav row pointing at it; - **one API route**, `GET /api/v1/public/world/status`, described in an OpenAPI fragment core merges into its own `/api/docs`; - **one table**, `examplegame_world_status`, created by an idempotent schema fragment and dropped by a purge file; - **both lifecycle hooks**, so there is something to see at boot and at shutdown. That is deliberately less than your module will do. What it is *complete* about is the shape: every seam a real module uses is here once, with the reasoning next to it, and CI proves the whole thing still builds against a pinned core. ## The tree ``` module.json what core reads first — id, version, coreApi, mounts server/ index.js register(ctx, api) — the entire server-side handshake core.js the lazy accessors over ctx; read this second boot.js onBoot / onShutdown db/schema.sql idempotent, replayed every boot db/purge.sql destructive, run only by an explicit admin purge model/worldStatus/ the .db.js / .model.js pair router/public/ one router, one controller, the #swagger annotations swagger/doc.js tags and schemas the annotations refer to scripts/checkImports.js the module boundary, enforced scripts/swaggerFragment.js generates swagger-fragment.json from your own routes test/ the suites — start with entry.test.js client/ vite.config.js the library build: anchored aliases, external: [] src/entry.jsx registers routes and nav at evaluation time src/core.js what core hands you: the seven-member UI kit src/shim/ the four shared dependencies, re-exported from core src/routes/public/ the page scripts/checkExternals.js asks the BUILT chunk whether a bare import survived test/ build.test.js and registration.test.js .gitea/workflows/release.yml packaging CI — Gitea .github/workflows/release.yml the same, for GitHub. Keep one, delete the other. swagger-fragment.json generated; commit it ``` Neither workflow runs while it sits inside the kit — a workflow is only read from a repository root. They arm themselves when your copy is a repository of its own. ## Build it ```bash npm ci --prefix server npm test --prefix server npm run check:imports --prefix server npm ci --prefix client npm run build --prefix client # → client/dist/entry.js, the chunk that ships npm run check:externals --prefix client npm test --prefix client # build FIRST: two of these tests read the chunk ``` `npm test` in `client/` passes with no build, by skipping the tests that need one. That is on purpose — the suite has to be runnable before the build — and it means **a CI job that tests without building is a job asking nothing.** Build first. Regenerate the OpenAPI fragment whenever a route or an annotation changes: ```bash npm run swagger --prefix server # writes swagger-fragment.json npm run check:swagger --prefix server # fails if it is stale ``` ## Install it Three supported ways, and none of them builds anything on the operator's machine: 1. **Admin → Modules**, pasting the URL of an install manifest — the JSON the release workflow attaches beside the tarball. This is how an operator installs your module. 2. **The `MODULES` environment variable**, `@=`, for a deployment that declares its module set rather than clicking it. 3. **A directory on the volume.** Copy this whole tree to `/modules//` and restart. The fastest loop while you are developing. For (3): **copy, do not symlink.** The loader lists directory entries and a symlink is not a directory, so a linked module is skipped in silence. ## Rename it Change `id` in `module.json` first, then work down the list. Nothing here is subtle, and the suites catch most of a half-finished job: `schema.test.js` fails the moment a table name stops matching the id, and `registration.test.js` fails when a nav row stops matching its route. Your id must match `^[a-z][a-z0-9-]{1,31}$`, must equal the directory name core loads you from, and becomes your table prefix — so **no hyphen unless you enjoy backticking table names**. | File | What to change | | --- | --- | | `module.json` | `id`, `name`, `version`, the `mounts` prefix, `capabilities` | | `server/package.json` | package `name` and `description` | | `server/core.js` | the message every accessor throws | | `server/boot.js` | the placeholder world name | | `server/db/schema.sql` | every table name — the prefix must be your id | | `server/db/purge.sql` | the same table names | | `server/model/worldStatus/worldStatus.db.js` | the `TABLE` constant | | `server/router/public/world.router.js` | the `#swagger.tags` name | | `server/swagger/doc.js` | the tag, and the `Examplegame…` schema prefix | | `server/scripts/swaggerFragment.js` | the generated fragment's `info.title` | | `server/test/_fakes.js` | `ctx.moduleId` | | `server/test/worldStatus.test.js` | the fixture's world name | | `server/package-lock.json` | **regenerated** — `npm install --prefix server` | | `client/package.json` | package `name` and `description` | | `client/vite.config.js` | the guard plugin's `name` | | `client/src/core.js` | the console tag on the identity check | | `client/src/shim/rg.js` | the console tag on the missing-global error | | `client/src/entry.jsx` | `ID`, and every route path and nav `to` | | `client/test/registration.test.js` | the example path in the comment | | `client/package-lock.json` | **regenerated** — `npm install --prefix client` | | `swagger-fragment.json` | **regenerated** — `npm run swagger --prefix server` | That table is checked. `scripts/checkRenameSites.js` at the root of this kit compares it against the tree on every pull request: a file that still mentions the placeholder and is not listed fails the build, and so does a listed file with nothing left to rename. A checklist nobody verifies is a checklist that is wrong by the second edit. Two things you do **not** rename: the mount prefix `/world` need not be your id (the server's prefix namespace is shared with core's, and `/status`, `/settings`, `/version` and `/contact` are already taken), and the `world` / `worldStatus` naming throughout is ordinary vocabulary you should replace with your own domain's when you replace the feature. ## Licence GPL-3.0-or-later, like everything else in this project — see [LICENSE.md](LICENSE.md). This directory is meant to be copied and made yours; it carries that licence, and so does anything derived from it.