feat(module): the bundle skeleton (phase 3, slice 0) #2
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/module-bundle-skeleton"
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?
The first real module. It registers nothing, deliberately — what slice 0 proves is the delivery path itself, end to end, before a single UO file moves into it. Plan:
MODULE_SYSTEM.md§2.7.1 (docs#135); contract:MODULE_API.md.What landed
Server half —
module.json; an entry point taking(ctx, api)that registers nothing; a test suite built on a fakectx(the contract holding is what makes the module testable without core at all); andscripts/checkImports.js, the §5.1 boundary check.Client half — the Vite library build; four shims re-exporting
react,react-dom/client,react-router-domandreact/jsx-runtimefromwindow.__rg; an entry that verifies each is identity-equal to core's copy; andscripts/checkExternals.js.29 server tests, 9 client tests, both new. CI's conditional "planning phase" gates are now armed and the two
check:*scripts run in it.Verified against a real core
Copied in as
website/modules/uoonedge, core booted against the dev database:started, and/api/v1/public/modulespublishes itapplication/javascriptwithCache-Control: no-cache; the module'sserver/index.js,module.jsonandserver/package.jsonall 404, as does/modules/nope/entry.jsand a..traversalhtmlShellinjects<script type="module" src="/modules/uo/entry.js">script-src 'self':[module-uo] loaded against core API 1.0.0; shared dependencies OK— zero CSP reports, no console errors, site renders normallyCore's working tree is untouched by this PR, so its manifest and OpenAPI spec are unchanged by construction.
Three findings, each of which had produced a green build that was wrong
1.
externaland the aliases do not compose. §3.6 shows both. Rollup asksexternalbefore Vite's alias resolver runs, so a specifier in both is marked external and never aliased — the chunk then ships bareimport "react", which no browser can resolve without an import map, and CSP forbids the inline script an import map has to be. It built cleanly and emitted exactly that;checkExternalscaught it. Now: alias only,externalempty, andvite.config.jsgrows a resolution-time guard that fails the build if a shared dependency resolves intonode_modules. That is a better net than the fingerprint-grep alternative, which has to guess at strings that survive minification.2. That guard was wrong twice before it worked. Written against Rollup's
loadhook it never ran —loadis first-wins and an earlier plugin had already claimed the module — so a deliberately-broken alias produced a 24 kB chunk with react-router welded into it and a green build. Then its forbidden-package list was derived from the alias list "so the two cannot disagree", which meant deleting an alias also deleted the guard against what that alias prevented. It states the contract independently now, and a test asserts the aliases stay inside it. Both failure modes were found by breaking an alias on purpose and checking the build actually goes red.3.
checkImportsfailed on its own documentation. The comment namingrequire("../../etc/passwd")as an example of what to catch, andindex.jsexplaining why the module must neverrequire("express"). A boundary check that cannot survive being described is one people stop writing comments around. It now strips comments and template literals with a character walk rather than a regexp — a URL in a string contains a comment opener, and a comment contains quotes — and it has its own 13-test suite, because a check never shown to fail is a check nobody knows the state of.Two smaller notes
readdirSync(…, {withFileTypes:true}).filter(e => e.isDirectory())reports a Windows junction as a symlink, so the first smoke run found no module and said nothing. Not a defect for a real install —modules/is a bind mount of real directories — but it cost twenty minutes here, so it is written down. No core change proposed.server/ships zero dependencies: everything arrives onctx.expressandexpress-validatorare devDependencies only, becausetest/_fakes.jsbuilds a real express router — a fake Router would test the fake.checkImportsallows devDependencies intest/andscripts/and forbids any bare specifier in shipped code.The first real module. It registers nothing, deliberately: what slice 0 proves is the delivery path itself, end to end, before a single UO file moves into it. Server half: module.json, an entry point that takes (ctx, api) and registers nothing, a test suite built on a fake ctx, and scripts/checkImports.js -- the MODULE_API.md §5.1 boundary check. Client half: the Vite library build, four shims re-exporting react / react-dom/client / react-router-dom / jsx-runtime from window.__rg, an entry that verifies each is identity-equal to core's copy, and scripts/checkExternals.js. 29 server tests, 9 client tests, both new. Verified against a real core: the module loads, mounts its zero routes, runs to `started`, and is published by /api/v1/public/modules. Its chunk serves from the entry's directory with `Cache-Control: no-cache` while the module's server source, module.json and package.json all 404. In Chrome, under the enforced `script-src 'self'`, the chunk evaluates and reports all four shared dependencies OK, with zero CSP reports and no console errors. Three findings, each of which had produced a green build that was wrong. MODULE_API.md §3.6 shows `external` alongside the aliases and they do not compose. Rollup asks `external` BEFORE Vite's alias resolver runs, so a specifier in both is marked external and never aliased -- the chunk then ships bare `import "react"`, which no browser can resolve without an import map, and CSP forbids one. Built cleanly and emitted exactly that; checkExternals caught it. So: alias only, `external` empty, and vite.config.js grows a resolution-time guard that fails the build if a shared dependency resolves into node_modules. That guard was wrong twice before it worked. Written against Rollup's `load` hook it never ran -- `load` is first-wins and an earlier plugin had already claimed the module -- so a deliberately-broken alias produced a 24 kB chunk with react-router welded in, and a green build. And its forbidden-package list was derived from the alias list "so the two cannot disagree", which meant deleting an alias also deleted the guard against what that alias prevented. It states the contract now, and a test asserts the aliases stay inside it. checkImports failed on its own documentation the first time it ran: the comment naming require("../../etc/passwd") as an example of what to catch, and index.js explaining why the module must never require("express"). A boundary check that cannot survive being described is one people stop writing comments around. It strips comments and template literals with a character walk rather than a regexp, because a URL in a string contains a comment opener and a comment contains quotes -- and it has its own test suite, since a check never shown to fail is a check nobody knows the state of. Co-Authored-By: Claude <noreply@anthropic.com>