Skip to content

dogfood: elision false-positives pin page.ts/layout.ts (# alias, inline-script globals, new Set) #623

Description

@vivek7405

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.ts to the network tab. Confirmed by instrumenting analyzeElision (packages/server/src/component-elision.js) against the in-repo apps:

website/app/layout.ts  selfFlags=clientGlobalOrBare  closure=[theme-toggle[comp]]
website/app/page.ts    selfFlags=none                closure=[_lib/tier.ts[NON-comp]]
blog/app/layout.ts     selfFlags=clientRouter,clientGlobalOrBare  closure=[theme-toggle[comp]]
blog/app/page.ts       selfFlags=clientGlobalOrBare   closure=[counter,muted-text,chat-box (comp), utils/cn.ts[NON-comp]]

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):
    • Re-instrument or add a unit test asserting each of blog app/page.ts, blog app/layout.ts, ui-website app/page.ts, ui-website app/layout.ts becomes 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).
    • 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.
  • Dependencies: this is the KEYSTONE for getting page.ts / layout.ts out of the network. It compounds with dogfood: ship a class-name utility (cn) in @webjsdev/core #619 (blog cn.ts dead customElements code) and dogfood: auto-enable client router via webjs-core-browser.js, drop layout imports #620 (auto client router, removes the blog/website/docs layout clientRouter self-flag). Land all three to fully drop the blog's modules; A+B+C alone fully drop ui-website's.
  • 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
  • A page/layout whose only browser job is registering #-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)
  • A layout pinned only by inline-<script> client globals becomes IMPORT-ONLY/INERT (all four apps)
  • A module-scope new Set/Map(...) data constant no longer disqualifies an importing page (ui-website home -> import-only/inert)
  • scanBareImports does not send #-alias specifiers to the vendor resolver
  • Differential elision stays output-identical; e2e network-probe proves page.ts/layout.ts are no longer fetched, with counterfactuals
  • agent-docs + server AGENTS.md updated; webjs-doc-sync run

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions