You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Pages and layouts never hydrate, so the framework can drop their modules from the browser entirely: a page/layout whose closure does no client work is INERT (#179), and one whose only client work is shipping components is IMPORT-ONLY (#605, the boot emits the components directly in its place). When that works, page.ts / layout.ts never appear in the network tab and the app ships only its interactive leaves.
But it is easy for an AI agent (or a human) to silently DEFEAT this without realising it, because nothing surfaces WHY a page started shipping whole. Any of these, introduced anywhere in a page/layout's transitive closure, pins the whole module to the browser:
A NON-component utility that touches a client global (customElements, window, document, localStorage, ...) at module scope. This is exactly what bit the blog: lib/utils/cn.ts referenced customElements in dead helper code, so the home page shipped whole (see dogfood: ship a class-name utility (cn) in @webjsdev/core #619).
A bare side-effect import of a non-core package (import 'some-pkg') reached from the page.
The page/layout doing its OWN client work (a module-scope call, a self-registering bare import).
The cost is invisible in npm test (it is an elision verdict, not a behaviour change), and there is no feedback telling the author "this route ships because of module X." So apps accrete network weight no one asked for, and the "pages are just carriers" model erodes. The blog is the proof: it took manual network-tab inspection to even notice.
Design / approach
Two parts, guidance first, optional diagnostic second.
Encode the pattern for every agent surface webjs ships (the AGENTS.md guardrails-for-all-agents contract): teach "keep pages and layouts as pure carriers." Concretely, the rule of thumb to document:
A page/layout should reach the browser ONLY through the components it renders. Do not import a client-global-touching or side-effecting NON-component utility into a page/layout (or into a component chain a page reaches) when it can be avoided; put client-only behaviour inside a component.
Server-only work belongs in .server.{js,ts} (it never reaches the client closure at all).
A utility that mixes a pure helper with client-global code (the cn.ts shape) should split the client-global part out, so the pure helper does not drag the client reference into every importer's closure.
How to self-check: look at the network tab (or the boot script) and confirm page.ts / layout.ts are absent; if present, something in the closure does client work and is not a component.
Optional diagnostic so the feedback is not manual (stretch, scope to taste): a webjs check advisory or a webjs doctor / MCP "explain why this route ships" mode that runs the existing analyzeElision and, for a route that ships whole, names the FIRST client-effecting non-component in its closure (the blocker), e.g. "app/page.ts ships whole: lib/utils/cn.ts references a client global and is not a component." This turns an invisible regression into an actionable line. The analyser already computes everything needed (inertRouteModules / importOnlyRouteModules and the clientGlobalOrBareFiles / reactiveFiles sets in packages/server/src/component-elision.js); the diagnostic is a reporting layer over it.
The scaffold per-agent configs under packages/cli/templates/: .cursorrules, .agents/rules/workflow.md, .github/copilot-instructions.md, the scaffolded AGENTS.md, and CONVENTIONS.md. Keep all in lockstep (the doc-sync expectation).
Diagnostic (if included):
packages/server/src/check.js for a webjs check advisory, OR the webjs doctor path, OR the @webjsdev/mcp introspection tools (packages/mcp/src/*). Reuse analyzeElision rather than re-deriving the closure.
It must be ADVISORY, not a correctness error: a page legitimately may need to ship (the analyser biases toward shipping, server AGENTS invariant 7), so this is a "you may not have intended this" hint, not a failure. Do not make it a hard webjs check rule.
Landmines:
Do NOT turn this into a hard gate. Elision is a conservative optimisation; a forced "must be import-only" rule would break legitimate pages and fight the analyser's safe-by-default posture.
Respect AGENTS.md invariant 11 (no banned prose glyphs) in every doc surface.
Invariants: elision stays output-identical (server AGENTS invariant 7); guardrails-for-all-agents parity (root AGENTS.md); doc-sync across every surface (run the webjs-doc-sync skill).
Acceptance criteria
The carrier-hygiene pattern (keep pages/layouts free of client-effecting non-component imports) is documented in root AGENTS.md, agent-docs, and CONVENTIONS.md
The same rule is propagated to every per-agent config the scaffold ships (.cursorrules, .agents/rules/workflow.md, .github/copilot-instructions.md, scaffolded AGENTS.md + CONVENTIONS.md)
If the diagnostic is included: a route that ships whole gets an advisory naming the first client-effecting non-component blocker, built over analyzeElision, advisory-only (no hard failure), with a test
webjs-doc-sync run so no doc surface repeats the wrong guidance
Problem
Pages and layouts never hydrate, so the framework can drop their modules from the browser entirely: a page/layout whose closure does no client work is INERT (#179), and one whose only client work is shipping components is IMPORT-ONLY (#605, the boot emits the components directly in its place). When that works,
page.ts/layout.tsnever appear in the network tab and the app ships only its interactive leaves.But it is easy for an AI agent (or a human) to silently DEFEAT this without realising it, because nothing surfaces WHY a page started shipping whole. Any of these, introduced anywhere in a page/layout's transitive closure, pins the whole module to the browser:
customElements,window,document,localStorage, ...) at module scope. This is exactly what bit the blog:lib/utils/cn.tsreferencedcustomElementsin dead helper code, so the home page shipped whole (see dogfood: ship a class-name utility (cn) in @webjsdev/core #619).import 'some-pkg') reached from the page.@webjsdev/core/client-routerimport in a layout (the layout then always ships; see dogfood: auto-enable client router via webjs-core-browser.js, drop layout imports #620 for making routing automatic).The cost is invisible in
npm test(it is an elision verdict, not a behaviour change), and there is no feedback telling the author "this route ships because of module X." So apps accrete network weight no one asked for, and the "pages are just carriers" model erodes. The blog is the proof: it took manual network-tab inspection to even notice.Design / approach
Two parts, guidance first, optional diagnostic second.
Encode the pattern for every agent surface webjs ships (the AGENTS.md guardrails-for-all-agents contract): teach "keep pages and layouts as pure carriers." Concretely, the rule of thumb to document:
.server.{js,ts}(it never reaches the client closure at all).cn.tsshape) should split the client-global part out, so the pure helper does not drag the client reference into every importer's closure.page.ts/layout.tsare absent; if present, something in the closure does client work and is not a component.Optional diagnostic so the feedback is not manual (stretch, scope to taste): a
webjs checkadvisory or awebjs doctor/ MCP "explain why this route ships" mode that runs the existinganalyzeElisionand, for a route that ships whole, names the FIRST client-effecting non-component in its closure (the blocker), e.g. "app/page.tsships whole:lib/utils/cn.tsreferences a client global and is not a component." This turns an invisible regression into an actionable line. The analyser already computes everything needed (inertRouteModules/importOnlyRouteModulesand theclientGlobalOrBareFiles/reactiveFilessets inpackages/server/src/component-elision.js); the diagnostic is a reporting layer over it.Implementation notes (for the implementing agent)
AGENTS.md(the execution-model section already explains pages/layouts do not hydrate; add the carrier-hygiene rule near the Elide page/layout modules that ship only to import interactive components #605/Stabilize: elision analyzer masks comments/strings (no false signals from prose) #179 description).agent-docs/components.md(the elision deep-dive lives here) andagent-docs/advanced.md(performance/bundling) as the long-form reference.CONVENTIONS.md(project conventions prose, customizable).packages/cli/templates/:.cursorrules,.agents/rules/workflow.md,.github/copilot-instructions.md, the scaffoldedAGENTS.md, andCONVENTIONS.md. Keep all in lockstep (the doc-sync expectation).packages/server/src/check.jsfor awebjs checkadvisory, OR thewebjs doctorpath, OR the@webjsdev/mcpintrospection tools (packages/mcp/src/*). ReuseanalyzeElisionrather than re-deriving the closure.webjs checkrule.Acceptance criteria
.cursorrules,.agents/rules/workflow.md,.github/copilot-instructions.md, scaffolded AGENTS.md + CONVENTIONS.md)analyzeElision, advisory-only (no hard failure), with a test