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 elision analyser can drop their modules from the browser (INERT #179 / IMPORT-ONLY #605) whenever their only browser job is carrying components. In practice almost no real page/layout qualifies, because three analyser checks produce FALSE POSITIVES on route modules, pinning page.ts / layout.ts to the network tab. Confirmed by instrumenting analyzeElision (packages/server/src/component-elision.js) against the in-repo apps:
Three distinct bugs, each independently enough to pin a module:
A. #-alias side-effect imports are misread as npm packages (clear bug, hits every modern app)
importsSideEffectNonCorePackage skips . / / / @webjsdev/core / node: specifiers, but NOT # path aliases (#555). So import '#components/counter.ts'; (the recommended way to register a component from a page/layout) is treated as a bare npm side-effect import and flags the module client-effecting. Proven:
SIDE_EFFECT_BARE_IMPORT_RE on `import '#components/counter.ts';` => returns '#components/counter.ts' (treated as non-core npm)
This is a real misclassification: #components/... resolves to a LOCAL file (Node package.json "imports", the scaffold's catch-all "#*": "./*"), not an npm package. It pins page.ts in EVERY app that uses the #components/... registration convention. It may ALSO mislead the vendor bare-import scan (scanBareImports) into trying to resolve #components/... against jspm; verify and fix there too if so.
B. module-scope new Set/Map data constants read as side effects
hasModuleScopeSideEffect returns true on any top-level new (/(?<![.\w])(?:new|await)\s/). A pure data constant like export const TIER_2_NAMES = new Set([...]) (ui-website app/_lib/tier.ts) therefore flags the util client-effecting, and since the page reaches it, the page ships whole. A new Set([literals]) / new Map([...]) in an export const initializer is pure data, not client work.
C. inline-<script> client globals in a page/layout TEMPLATE read as module client work
CLIENT_GLOBAL_RE (window|document|localStorage|...) runs on the comment-masked source with STRINGS/TEMPLATES KEPT. A layout's inline <script> (theme no-FOUC bootstrap, menu handler) contains document / localStorage / addEventListener, so the layout MODULE is flagged. But a page/layout never hydrates: that inline script runs from the SSR'd HTML, and loading layout.ts as a module never executes the template's script text. So this is a provable false positive on route modules. It pins layout.ts in all four apps.
Design / approach
The analyser is a denylist biased to ship (a false "ship" only misses an optimisation, a false "elide" breaks a component). For ROUTE MODULES the safety direction is even more forgiving: a page/layout never hydrates, so dropping it can never break interactivity (inline scripts stay in the SSR'd HTML, components re-emit directly). The only real client work a route module can do is genuine MODULE-SCOPE code (a real top-level document.x, a real bare npm side-effect import, a client-router import). So:
A (general). Teach the side-effect / bare-import detectors about # aliases. Expand a #-prefixed specifier through the app's package.json "imports" map (the module graph already does this via appImportsMap / expandImportAlias in packages/server/src/module-graph.js) and treat a local-resolving alias like a relative import (skip it). Reuse that helper rather than re-deriving. Also audit scanBareImports (vendor) for the same gap.
C (route-module-scoped). For ROUTE modules (pages/layouts), run the client-global / event / module-scope-side-effect scans on the TEMPLATE-REDACTED source (redactStringsAndTemplates), so template content (inline scripts, @event in a page template that SSR drops anyway, client globals in returned HTML) does not flag the module, while a genuine module-scope document.x OUTSIDE any template still does. Do NOT change component detection (a component's template events ARE its interactivity signal). Scope this relaxation to route modules only.
B (careful, general). Relax hasModuleScopeSideEffect so a new <PureBuiltin>(...) (Set / Map / Date / RegExp / WeakSet / WeakMap / Array) in an export const / const initializer is not a disqualifying side effect. Keep flagging a bare top-level new SomethingElse() / await / call. This is the riskiest change; gate it tightly and lean on the differential test.
All three must preserve the invariant that elision never changes observable output.
Implementation notes (for the implementing agent)
Where: packages/server/src/component-elision.js (importsSideEffectNonCorePackage, CLIENT_GLOBAL_RE usage in the per-file scan loop around L800-820, hasModuleScopeSideEffect); reuse appImportsMap / expandImportAlias from packages/server/src/module-graph.js; audit scanBareImports (vendor path) for the #-alias gap. The route-module-vs-component distinction is available: the analyser already knows routeModules vs componentFiles.
How to verify (the win is an elision VERDICT change, invisible to npm test):
e2e/network-probe: page.ts / layout.ts ABSENT from the blog home boot, interactive leaves still present, with a counterfactual.
The differential-elision test (test/elision/differential-elision.test.js) is the safety net: SSR HTML + post-hydration DOM/behaviour must stay identical on vs off. Each of A/B/C must keep it green.
Landmines:
Conservative-safety invariant (server AGENTS invariant 7): bias to ship. B especially must not over-relax (a new EventSource() / new WebSocket() at module scope IS client work; only inert data builtins are safe).
C must stay route-module-scoped. A component that references a client global only inside its template is rare but real; do not change component verdicts.
The #-alias expansion must match how the graph resolves it, or the side-effect verdict and the import-graph will disagree.
Docs: update agent-docs/components.md (elision deep-dive) + the relevant packages/server/AGENTS.md invariant 7 prose to describe route-module template handling and #-alias handling. Run webjs-doc-sync.
Acceptance criteria
A #-alias side-effect import (import '#components/x.ts') no longer flags a page/layout client-effecting; verified with a unit test + the offending-spec proof as counterfactual
Problem
Pages and layouts never hydrate, so the elision analyser can drop their modules from the browser (INERT #179 / IMPORT-ONLY #605) whenever their only browser job is carrying components. In practice almost no real page/layout qualifies, because three analyser checks produce FALSE POSITIVES on route modules, pinning
page.ts/layout.tsto the network tab. Confirmed by instrumentinganalyzeElision(packages/server/src/component-elision.js) against the in-repo apps:Three distinct bugs, each independently enough to pin a module:
A.
#-alias side-effect imports are misread as npm packages (clear bug, hits every modern app)importsSideEffectNonCorePackageskips.///@webjsdev/core/node:specifiers, but NOT#path aliases (#555). Soimport '#components/counter.ts';(the recommended way to register a component from a page/layout) is treated as a bare npm side-effect import and flags the module client-effecting. Proven:This is a real misclassification:
#components/...resolves to a LOCAL file (Nodepackage.json"imports", the scaffold's catch-all"#*": "./*"), not an npm package. It pinspage.tsin EVERY app that uses the#components/...registration convention. It may ALSO mislead the vendor bare-import scan (scanBareImports) into trying to resolve#components/...against jspm; verify and fix there too if so.B. module-scope
new Set/Mapdata constants read as side effectshasModuleScopeSideEffectreturns true on any top-levelnew(/(?<![.\w])(?:new|await)\s/). A pure data constant likeexport const TIER_2_NAMES = new Set([...])(ui-websiteapp/_lib/tier.ts) therefore flags the util client-effecting, and since the page reaches it, the page ships whole. Anew Set([literals])/new Map([...])in anexport constinitializer is pure data, not client work.C. inline-
<script>client globals in a page/layout TEMPLATE read as module client workCLIENT_GLOBAL_RE(window|document|localStorage|...) runs on the comment-masked source with STRINGS/TEMPLATES KEPT. A layout's inline<script>(theme no-FOUC bootstrap, menu handler) containsdocument/localStorage/addEventListener, so the layout MODULE is flagged. But a page/layout never hydrates: that inline script runs from the SSR'd HTML, and loadinglayout.tsas a module never executes the template's script text. So this is a provable false positive on route modules. It pinslayout.tsin all four apps.Design / approach
The analyser is a denylist biased to ship (a false "ship" only misses an optimisation, a false "elide" breaks a component). For ROUTE MODULES the safety direction is even more forgiving: a page/layout never hydrates, so dropping it can never break interactivity (inline scripts stay in the SSR'd HTML, components re-emit directly). The only real client work a route module can do is genuine MODULE-SCOPE code (a real top-level
document.x, a real bare npm side-effect import, aclient-routerimport). So:#aliases. Expand a#-prefixed specifier through the app'spackage.json"imports" map (the module graph already does this viaappImportsMap/expandImportAliasinpackages/server/src/module-graph.js) and treat a local-resolving alias like a relative import (skip it). Reuse that helper rather than re-deriving. Also auditscanBareImports(vendor) for the same gap.redactStringsAndTemplates), so template content (inline scripts,@eventin a page template that SSR drops anyway, client globals in returned HTML) does not flag the module, while a genuine module-scopedocument.xOUTSIDE any template still does. Do NOT change component detection (a component's template events ARE its interactivity signal). Scope this relaxation to route modules only.hasModuleScopeSideEffectso anew <PureBuiltin>(...)(Set/Map/Date/RegExp/WeakSet/WeakMap/Array) in anexport const/constinitializer is not a disqualifying side effect. Keep flagging a bare top-levelnew SomethingElse()/await/ call. This is the riskiest change; gate it tightly and lean on the differential test.All three must preserve the invariant that elision never changes observable output.
Implementation notes (for the implementing agent)
packages/server/src/component-elision.js(importsSideEffectNonCorePackage,CLIENT_GLOBAL_REusage in the per-file scan loop around L800-820,hasModuleScopeSideEffect); reuseappImportsMap/expandImportAliasfrompackages/server/src/module-graph.js; auditscanBareImports(vendor path) for the#-alias gap. The route-module-vs-component distinction is available: the analyser already knowsrouteModulesvscomponentFiles.npm test):app/page.ts, blogapp/layout.ts, ui-websiteapp/page.ts, ui-websiteapp/layout.tsbecomes IMPORT-ONLY or INERT after the fix (with the cn fix dogfood: ship a class-name utility (cn) in @webjsdev/core #619 + router-import removal dogfood: auto-enable client router via webjs-core-browser.js, drop layout imports #620 also applied for the blog cases, see Dependencies).page.ts/layout.tsABSENT from the blog home boot, interactive leaves still present, with a counterfactual.test/elision/differential-elision.test.js) is the safety net: SSR HTML + post-hydration DOM/behaviour must stay identical on vs off. Each of A/B/C must keep it green.new EventSource()/new WebSocket()at module scope IS client work; only inert data builtins are safe).#-alias expansion must match how the graph resolves it, or the side-effect verdict and the import-graph will disagree.page.ts/layout.tsout of the network. It compounds with dogfood: ship a class-name utility (cn) in @webjsdev/core #619 (blog cn.ts deadcustomElementscode) and dogfood: auto-enable client router via webjs-core-browser.js, drop layout imports #620 (auto client router, removes the blog/website/docs layoutclientRouterself-flag). Land all three to fully drop the blog's modules; A+B+C alone fully drop ui-website's.agent-docs/components.md(elision deep-dive) + the relevantpackages/server/AGENTS.mdinvariant 7 prose to describe route-module template handling and#-alias handling. Run webjs-doc-sync.Acceptance criteria
#-alias side-effect import (import '#components/x.ts') no longer flags a page/layout client-effecting; verified with a unit test + the offending-spec proof as counterfactual#-imported components becomes IMPORT-ONLY (blog home, with dogfood: ship a class-name utility (cn) in @webjsdev/core #619 + dogfood: auto-enable client router via webjs-core-browser.js, drop layout imports #620 applied)<script>client globals becomes IMPORT-ONLY/INERT (all four apps)new Set/Map(...)data constant no longer disqualifies an importing page (ui-website home -> import-only/inert)scanBareImportsdoes not send#-alias specifiers to the vendor resolver