feat(modules): the client registry, window.__rg and the chunk's script injection
Phase 2, PR 7 of docs/website/MODULE_SYSTEM.md 2.7 — the client half's
delivery. A module's prebuilt chunk is served, injected, handed core's React
and its UI kit, and its routes are rendered by App.jsx. The registry is empty
on a bare core, so nothing an operator can see changes.
Client:
- modules/registry.js — registerRoutes/registerNav/registerFeatureProvider,
with the URL namespace written by core, never by the module
- modules/shared.js — window.__rg: React, react-dom/client, react-router-dom,
react/jsx-runtime, the registry, the seven-member UI kit and the request
primitive, frozen
- App.jsx reads routesFor for all three areas; nav consumption is PR 8
- main.jsx publishes the global, then mounts on DOMContentLoaded
Server:
- the loader validates client.entry and publishes clientChunks() and
clientEntryUrls(); an entry in the module root is rejected, because the
directory it sits in is what gets served
- app.js mounts each chunk at /modules/<id>/ behind the module's state guard
with no-cache; anything else under /modules is a 404, not the SPA shell
- htmlShell injects the tag before </body>, so core's bundle runs first
wherever a bundler puts it
Found by loading a real chunk in a browser, and fixed here: core mounted before
any module chunk had evaluated, because document.readyState during a deferred
script is 'interactive', not 'loading'. Every test passed against that build.
The smoke is written down in MODULE_API.md 7.7.
933 server tests (+23), 123 client tests (+14). routes.manifest.json unchanged
at 230 routes; the OpenAPI spec regenerates byte-identical.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,47 @@ modules.load({
|
||||
|
||||
app.use('/api', (req, res) => res.status(404).json({ message: 'Not found' }))
|
||||
|
||||
// Installed modules' prebuilt client chunks, at /modules/<id>/ — same-origin, so
|
||||
// `script-src 'self'` admits them with no nonce and no inline script
|
||||
// (docs/website/MODULE_API.md §3.1). Three properties, each load-bearing:
|
||||
//
|
||||
// • The static root is the directory the ENTRY sits in, never the module root.
|
||||
// One express.static over a module root would publish its server source, its
|
||||
// module.json and its schema fragment; the loader rejects an entry that would
|
||||
// make those the same directory.
|
||||
// • Behind the module's own state guard, so a failed module's chunk is 503 and
|
||||
// a disabled one's is 404 — the same answers its API gives, for the same
|
||||
// reason: the browser should not be running the client half of something the
|
||||
// server half has stopped serving.
|
||||
// • `fallthrough: false`, so a missing file is a 404 here rather than falling
|
||||
// through to the SPA catch-all and answering a `<script src>` with the index
|
||||
// shell, which the browser then rejects on its MIME type instead.
|
||||
//
|
||||
// Vite's library build emits an unhashed `entry.js`, so `no-cache` (revalidate,
|
||||
// not "do not store") is what stops an upgraded module serving yesterday's chunk
|
||||
// out of the disk cache.
|
||||
for (const chunk of modules.clientChunks()) {
|
||||
app.use(
|
||||
chunk.url,
|
||||
chunk.guard,
|
||||
express.static(chunk.dir, {
|
||||
fallthrough: false,
|
||||
setHeaders: (res) => {
|
||||
res.set('Cache-Control', 'no-cache')
|
||||
res.set('X-Content-Type-Options', 'nosniff')
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Everything else under /modules is a 404, not the SPA shell. The namespace
|
||||
// belongs to installed modules' chunks — an unknown module id or a file a module
|
||||
// does not ship is a missing file, and answering a `<script src>` with an HTML
|
||||
// page turns that into a MIME-type refusal in the console with a 200 in the
|
||||
// network tab. It also keeps the namespace's boundary a fact of the app rather
|
||||
// than of whichever catch-all happens to be mounted after it.
|
||||
app.use('/modules', (req, res) => res.status(404).json({ message: 'Not found' }))
|
||||
|
||||
// ── /.well-known ──────────────────────────────────────────────────────
|
||||
// Android App Links verification file at the web root (M9 follow-up). Mounted
|
||||
// before the SPA catch-all so it returns JSON, not the index shell. 404s unless
|
||||
|
||||
Reference in New Issue
Block a user