feat(modules): the client registry, window.__rg and the chunk's script injection #134
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/module-client-registry"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 UI kit, and its routes are rendered byApp.jsx. The registry is empty on a bare core, so nothing an operator can see changes. Docs half: docs#132.The part that matters: every test passed against a build that did not work
The unit tests were green. The chunk fetched, executed, resolved
window.__rg, and registered its route. The page still redirected home.document.readyStateduring a deferred script is'interactive', not'loading'— a deferred script runs after the document is parsed and beforeDOMContentLoaded. So the obviousreadyState === 'loading'guard is false, core mounted immediately, and it read the registry before a single module chunk had evaluated. A module's routes were absent from the first render and its URL fell through to core's catch-all — indistinguishable from a module that failed to load, with nothing logged anywhere.No test in this repo can see that. There is no DOM in either runner, and the ordering being asserted is the browser's, not the code's. It was found by writing a throwaway module with a hand-written ESM entry, pointing
MODULES_DIRat it and opening the page in Chrome; the smoke is now written down as a repeatable procedure inMODULE_API.md§7.7.The same run confirmed the property §3.6 calls the highest-risk mechanical detail in the whole plan, against the enforced policy rather than a reasoned one:
Zero CSP reports; the module page rendered inside core's
PublicLayoutwith core's React. Screenshot in the review notes below.Client
modules/registry.jsregisterRoutes/registerNav/registerFeatureProvider, plus the readers. The URL namespace is written by core, so a module cannot spell its way out of it — a leading/, several, or a trailing one all land under<id>/.modules/shared.jswindow.__rg: React,react-dom/client,react-router-dom,react/jsx-runtime, the registry, the UI kit and the request primitive. Frozen one level down as well as at the top — the object a module reaches for its React is not somewhere to leave something for the next module.modules/version.jsMODULE_API_VERSION. Duplicated deliberately (it must be on the global before any chunk evaluates, which is earlier than a fetch could answer); a test asserts the two halves agree.App.jsxroutesForfor all three areas. Nav consumption is PR 8.main.jsxDOMContentLoaded.api/client.jsreqasrequest— the primitive, not theapiobject.Server
client.entryand publishesclientChunks()/clientEntryUrls(). An entry sitting directly in the module root is rejected: the directory it sits in is what gets served, and oneexpress.staticover a module root would publish server source,module.jsonand the schema fragment. Containment is checked first, before the checks that a real file outside the module would pass.app.jsmounts each chunk at/modules/<id>/, behind the module's own state guard (503failed,404disabled — the same answers its API gives) withno-cacheandnosniff, andfallthrough: false. Anything else under/modulesis a 404 rather than the SPA shell: answering a<script src>with an HTML page turns a missing file into a MIME-type refusal with a200in the network tab.htmlShellinjects before</body>. Module scripts execute in document order, so core's bundle has to run first.</head>works today only because Vite hoists core's entry into<head>— a bundler's emit choice, and if it changed every module in the wild would break with nothing here edited.Verification
moduleClientChunk.test.jsboots the realapp.jsagainst a module on a temp volume, so the mount's three properties are tested where they live: nothing above the dist directory is reachable, the guard answers for the live state, and a miss is a 404.routes.manifest.jsonunchanged at 230 routes,routes.guards.jsonunchanged,npm run swaggerregenerates byte-identical./modules/<id>/is a filesystem-conditional static mount, not API surface — the same reason/uploadsand/brandare outside the manifest.test/announceLegs.test.js(PR 4's, untouched here) failed at file level with no failing subtest. It passed alone and in three subsequent full runs, and the baseline suite onedgeis clean. Not reproduced; recorded rather than swept up.Decisions
All four settled with the org lead before implementation; all four the recommended option.
MOD_PATHS, the feature seam) in PR 8</body>no-cacheAdminPagefrom the contract🤖 Generated with Claude Code
https://claude.ai/code/session_018ocYxQWk3EhZe5gWRJXFU8
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>