feat(modules): the filesystem module loader
All checks were successful
PR Checks / bot-install (pull_request) Successful in 16s
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 10m4s

Phase 2 PR 2 of docs/website/MODULE_SYSTEM.md 2.7. Adds
server/src/modules/{loader,semver,version}.js: the synchronous scan of
MODULES_DIR, manifest validation, prefix and table-name collision
rejection, per-module try/catch and the mount into the three tier
routers behind the MODULE_API.md 4.5 dispatch guard.

Two decisions the contract left open, both now written up there:

- The load trigger is one explicit modules.load(tierRouters) call in
  app.js, not a lazy scan (API 7.6). Accessors throw until it has run,
  because "no modules installed" is a real answer a caller must not be
  handed by accident.
- Whether core owns a prefix is asked of the live tier routers via
  express's own layer.match(), skipping root-mounted layers, rather than
  a hardcoded table -- the spike's was already stale when written
  (API 4.3).

Mounting is a second pass after every module is validated. Doing it
inside the scan loop makes the first module's layers indistinguishable
from core's, so the second module claiming a taken prefix is told it
collided with core and the module-versus-module check is unreachable.

registerExtension/NotificationStreams/AnnounceLeg and onBoot/onShutdown
throw "not available until phase 2 PR 4/5" rather than no-op; an
accepting stub would let a module believe it had registered something.
No schema replay, no boot dispatch, no installed_modules reconcile --
those are PRs 3 and 5, and until PR 5 a record's state is in memory only.

No module ships on the volume, so nothing an operator or client can see
changes: 842 tests pass, routes.manifest.json is unchanged at 229 routes
and swagger-output.json regenerates byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-10 14:35:27 -05:00
parent dcf6ef1886
commit ec1ca7e794
5 changed files with 1001 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ require('dotenv').config()
const swaggerUi = require('swagger-ui-express')
const apiRouter = require('./router/api.router')
const modules = require('./modules/loader')
const wellKnown = require('./router/wellKnown.controller')
const cspReport = require('./router/cspReport.controller')
const brand = require('./config/brand')
@@ -154,6 +155,28 @@ app.get(
app.post(csp.REPORT_PATH, cspReportLimiter, ...cspReport.parsers, cspReport.receive)
app.use('/api', apiRouter)
// ── Installed modules ─────────────────────────────────────────────────
// Discover, validate and mount whatever is on the modules volume
// (docs/website/MODULE_API.md Part 4). One explicit call, here and nowhere else:
// the loader has no lazy self-scan, so there is exactly one place that decides
// when modules are discovered, and reading the module list before this line is
// an error rather than a silent empty answer (§7.6).
//
// Position is load-bearing, in both directions. It is AFTER `/api` is mounted,
// so every core prefix is already on the tier routers when the collision check
// asks them what core owns — and so first-match-wins means a module physically
// cannot shadow a core route. It is BEFORE the `/api` 404 below, so a module
// route reaches its handler instead of the catch-all.
//
// The three requires resolve from cache to the very routers v1.router.js
// mounted; this is a reference to them, not a second copy.
modules.load({
public: require('./router/v1/public'),
admin: require('./router/v1/admin'),
player: require('./router/v1/player'),
})
app.use('/api', (req, res) => res.status(404).json({ message: 'Not found' }))
// ── /.well-known ──────────────────────────────────────────────────────