Closes Phase 2. Modules live on a mount, never in the image — that is what lets an operator add one to a pull-only deployment without building anything. `./modules` is a bind mount rather than a named volume: placing a module directory by hand is a supported install (MODULE_SYSTEM.md §2.5), and that has to be doable from the host rather than through `docker cp`. Read-write, because the admin panel's install/uninstall unpacks and removes directories there. The directory is tracked via its README so it exists in the checkout with the operator's own ownership — Docker recreates a missing bind-mount source as root:root, which the container user could not then write. `.dockerignore` excludes it so a module in the builder's working tree can never ship inside an image. Also corrects the route-manifest generator's list of filesystem-conditional mounts, which never picked up `/modules` when PR 7 added it. Comment only; the generator filters on an allowlist, so its behaviour was already right. Verified against a real container, not just a parsed compose file: image carries an empty node-owned /app/modules despite a module in the build context; a module on the bind mount loads, mounts, replays and reaches `started`; `/api/v1/public/modules` lists it; the chunk serves from the entry's directory only (server source and module.json 404) with `no-cache`; the injected tag follows core's bundle; and in Chrome the page renders on first paint inside core's PublicLayout with its nav row interleaved into core's public nav, under enforced `script-src 'self'` with zero CSP reports and no console errors. Removing the directory by hand reconciles the row to `startup_failed`/`require` and leaves core healthy with no injection. 933 server + 160 client tests pass, manifest unchanged at 230 routes, swagger regenerates byte-identical. Co-Authored-By: Claude <noreply@anthropic.com>
76 lines
3.0 KiB
Markdown
76 lines
3.0 KiB
Markdown
# Installed modules
|
|
|
|
This directory is bind-mounted into the container at `/app/modules` (see
|
|
`docker-compose.yml`). It is where **installed modules** live — the game-specific
|
|
routes, tables, screens and nav that are not part of core. Design of record:
|
|
[`docs/website/MODULE_SYSTEM.md`](../../docs/website/MODULE_SYSTEM.md); the
|
|
normative contract module authors build against is
|
|
[`docs/website/MODULE_API.md`](../../docs/website/MODULE_API.md).
|
|
|
|
**Core ships no module.** This directory is empty in a fresh checkout, and the
|
|
site runs cleanly that way — an empty `modules/` is the normal state for bare
|
|
core, not a misconfiguration. Everything below is ignored by git except this
|
|
README, which exists so the directory itself is tracked: `docker-compose.yml`
|
|
bind-mounts it, and Docker recreates a *missing* bind-mount source as a
|
|
root-owned directory the container user cannot write.
|
|
|
|
## Layout
|
|
|
|
One directory per module, named for its id, each holding a prebuilt bundle:
|
|
|
|
```
|
|
modules/
|
|
uo/
|
|
module.json # the manifest the loader reads
|
|
server/index.js # registers routes, streams, hooks
|
|
server/db/schema.sql # tables, replayed every boot
|
|
server/db/purge.sql # only ever run by an explicit purge
|
|
client/dist/entry.js # prebuilt ESM chunk, served at /modules/uo/
|
|
```
|
|
|
|
**Nothing here is compiled by the operator.** A module arrives already built —
|
|
that is the whole point of the design. There is no install step that runs a
|
|
bundler, and none that needs one.
|
|
|
|
## Installing a module
|
|
|
|
Two supported paths, both writing the same `installed_modules` row:
|
|
|
|
- **The admin panel** downloads the bundle from the module's release, verifies it
|
|
against its `sha256`, and unpacks it here.
|
|
- **By hand**, for a compose-managed host: unpack the bundle into a directory
|
|
named for the module id, e.g. `tar -xf uo-1.0.0.tgz -C ./modules`.
|
|
|
|
Either way, **adding or removing a module takes a restart.** The loader scans
|
|
this directory synchronously at startup (`MODULE_API.md` §4.1); nothing placed
|
|
here is picked up by a running server.
|
|
|
|
```
|
|
docker compose restart app
|
|
```
|
|
|
|
On boot each module is validated, mounted, its schema fragment replayed and its
|
|
`onBoot` hook run — reaching `started`, or `startup_failed` with the stage and
|
|
reason recorded. A module that fails to start does not stop the site: core, and
|
|
every other module, carry on without it.
|
|
|
|
## Uninstalling
|
|
|
|
Removing a directory and restarting is enough to stop a module serving. Note that
|
|
this is *not* the same as an uninstall through the admin panel, which also marks
|
|
the row `disabled` — a directory that simply vanishes leaves a row claiming to be
|
|
enabled, which the loader records as `startup_failed`.
|
|
|
|
A module's **tables and data are retained** in both cases. Dropping them is a
|
|
separate, explicit, destructive purge; it is never bundled into an uninstall.
|
|
|
|
## Ownership
|
|
|
|
The container runs as uid 1000 (`node`) and the admin panel writes here, so the
|
|
app must be able to write this directory. It is created by your checkout, with
|
|
your ownership. If they differ:
|
|
|
|
```
|
|
chown -R 1000:1000 modules
|
|
```
|