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
The app-module modulepreload walk in deduplicatedPreloads (packages/server/src/ssr.js) emits a <link rel="modulepreload"> for a module reached ONLY through a page/layout dropped from the boot (an inert page, or an import-only page whose own module is substituted by its components). The canonical case is a dropped page's SSR-only RELATIVE HELPER. The page imports ./fmt.js for server render only, the page module is dropped from moduleUrls (inert or import-only), yet fmt.js still gets a preload hint because the walk roots are the RAW route entries, not the shipped set.
This is the exact app-module analog of the vendor over-fetch fixed in #754 (PR #776), and was flagged by the PR #776 round-2 independent review as a pre-existing issue to tighten in a follow-up.
It is a wasted-roundtrip and network-tab-pollution bug (over-fetch), not a correctness or security bug. The hinted module is never imported by anything that ships, so the browser preloads a module it then never executes. It does NOT cause under-fetch (a shipped module is still hinted), so first paint is unaffected. The cost is a speculative fetch of a dead module plus a misleading network tab.
Design / approach
reachedVendorSpecifiers (the #754 vendor walk) was fixed to walk from shippedRoots = moduleUrls.map(absolutePath) (the boot's actually-shipped page/layout module set, which already drops inert modules and expands an import-only page into its components) plus the rendered components. deduplicatedPreloads should walk from the SAME shipped-roots set instead of the raw [route.file, ...route.layouts].
Concretely, the call site (ssr.js around L183 to L191) passes [route.file, ...route.layouts] as the entryFiles arg to deduplicatedPreloads. Change it to pass the shippedRoots array already computed around L200 to L201 for the vendor walk (hoist that computation above both call sites so both share it). Inside deduplicatedPreloads, the graph walk around L1512 to L1524 combines entryFiles plus componentUrls as allEntries. Once entryFiles is the shipped set, a dropped page is no longer a walk root, so its SSR-only subtree (direct or via a relative helper) is never reached and never preloaded.
Note deduplicatedPreloads already seeds seen = new Set(moduleUrls), so the dropped module's OWN url is excluded from the result today. The gap is purely the TRANSITIVE deps reachable only through the dropped root. The shipped-roots change closes exactly that gap, matching the vendor walk's semantics so the two walks stay consistent (a later divergence between "which app modules ship" and "which vendors ship" would be a bug).
Implementation notes (for the implementing agent)
Where to edit:
packages/server/src/ssr.js, the deduplicatedPreloads call (around L183 to L191). Pass shippedRoots (the absolute-path array built around L200 to L201) instead of [route.file, ...route.layouts]. Hoist the shippedRoots computation above the deduplicatedPreloads call so both it and the reachedVendorSpecifiers call consume the one array.
deduplicatedPreloads (around L1488). The entryFiles param now carries shipped absolute paths already. Confirm the resolve(appDir, url...) round-trip for componentUrls is unchanged, and that seen = new Set(moduleUrls) still excludes the shipped page/layout urls themselves (it should, moduleUrls is unchanged).
Update the function JSDoc to state the roots are the shipped set (mirror the reachedVendorSpecifiers doc around L1537 to L1563, which already documents this for vendors).
Landmines and gotchas:
route.file and route.layouts are the RAW entries (an inert page is still in this list). moduleUrls is the post-drop, post-expand SHIPPED set. The whole bug is the gap between these two. Do not "simplify" by feeding raw entries back in.
An import-only page substitutes its components into moduleUrls, so the shipped roots already REACH the page's shipping components (their vendors plus app deps are still hinted). Verify the import-only under-fetch case stays covered (a shipping component reachable only via an import-only page is still preloaded).
A component shared between a shipped route and a dropped page is reached via the shipped route's root, so it is still (correctly) preloaded. Removing the dropped page as a root must NOT drop a shared shipped component.
transitiveDeps stops at a .server.* or action-index boundary but returns the boundary file itself. The existing byName and byIndex server-file filters must stay.
Unit: packages/server/test/ssr/* (or the preload-focused test file). A multi-route shape where an inert / imports an SSR-only relative helper ./fmt.js, a sibling /live ships, and /'s render emits NO modulepreload for fmt.js. Add the import-only under-fetch counterpart (a shipping component reached only via an import-only page IS preloaded). Include a counterfactual (revert the shipped-roots change, the over-fetch test reds).
deduplicatedPreloads walks from the shipped-module roots (moduleUrls-derived absolute paths) plus rendered components, not the raw route entries
A dropped page's SSR-only relative helper (and SSR-only direct app import) receives NO modulepreload hint
A shipped module (including a component reachable only via an import-only page, and a component shared with a shipped route) STILL receives its hint (no under-fetch)
A counterfactual proves the over-fetch test fires (revert shipped-roots, test reds)
Tests cover the new behaviour at unit and (where applicable) e2e network-probe layers
The deduplicatedPreloads JSDoc documents the shipped-roots semantics, consistent with reachedVendorSpecifiers
Docs and AGENTS.md updated if the documented preload surface changed
Problem
The app-module
modulepreloadwalk indeduplicatedPreloads(packages/server/src/ssr.js) emits a<link rel="modulepreload">for a module reached ONLY through a page/layout dropped from the boot (an inert page, or an import-only page whose own module is substituted by its components). The canonical case is a dropped page's SSR-only RELATIVE HELPER. The page imports./fmt.jsfor server render only, the page module is dropped frommoduleUrls(inert or import-only), yetfmt.jsstill gets a preload hint because the walk roots are the RAW route entries, not the shipped set.This is the exact app-module analog of the vendor over-fetch fixed in #754 (PR #776), and was flagged by the PR #776 round-2 independent review as a pre-existing issue to tighten in a follow-up.
It is a wasted-roundtrip and network-tab-pollution bug (over-fetch), not a correctness or security bug. The hinted module is never imported by anything that ships, so the browser preloads a module it then never executes. It does NOT cause under-fetch (a shipped module is still hinted), so first paint is unaffected. The cost is a speculative fetch of a dead module plus a misleading network tab.
Design / approach
reachedVendorSpecifiers(the #754 vendor walk) was fixed to walk fromshippedRoots = moduleUrls.map(absolutePath)(the boot's actually-shipped page/layout module set, which already drops inert modules and expands an import-only page into its components) plus the rendered components.deduplicatedPreloadsshould walk from the SAME shipped-roots set instead of the raw[route.file, ...route.layouts].Concretely, the call site (ssr.js around L183 to L191) passes
[route.file, ...route.layouts]as theentryFilesarg todeduplicatedPreloads. Change it to pass theshippedRootsarray already computed around L200 to L201 for the vendor walk (hoist that computation above both call sites so both share it). InsidededuplicatedPreloads, the graph walk around L1512 to L1524 combinesentryFilespluscomponentUrlsasallEntries. OnceentryFilesis the shipped set, a dropped page is no longer a walk root, so its SSR-only subtree (direct or via a relative helper) is never reached and never preloaded.Note
deduplicatedPreloadsalready seedsseen = new Set(moduleUrls), so the dropped module's OWN url is excluded from the result today. The gap is purely the TRANSITIVE deps reachable only through the dropped root. The shipped-roots change closes exactly that gap, matching the vendor walk's semantics so the two walks stay consistent (a later divergence between "which app modules ship" and "which vendors ship" would be a bug).Implementation notes (for the implementing agent)
packages/server/src/ssr.js, thededuplicatedPreloadscall (around L183 to L191). PassshippedRoots(the absolute-path array built around L200 to L201) instead of[route.file, ...route.layouts]. Hoist theshippedRootscomputation above thededuplicatedPreloadscall so both it and thereachedVendorSpecifierscall consume the one array.deduplicatedPreloads(around L1488). TheentryFilesparam now carries shipped absolute paths already. Confirm theresolve(appDir, url...)round-trip forcomponentUrlsis unchanged, and thatseen = new Set(moduleUrls)still excludes the shipped page/layout urls themselves (it should,moduleUrlsis unchanged).reachedVendorSpecifiersdoc around L1537 to L1563, which already documents this for vendors).route.fileandroute.layoutsare the RAW entries (an inert page is still in this list).moduleUrlsis the post-drop, post-expand SHIPPED set. The whole bug is the gap between these two. Do not "simplify" by feeding raw entries back in.moduleUrls, so the shipped roots already REACH the page's shipping components (their vendors plus app deps are still hinted). Verify the import-only under-fetch case stays covered (a shipping component reachable only via an import-only page is still preloaded).transitiveDepsstops at a.server.*or action-index boundary but returns the boundary file itself. The existingbyNameandbyIndexserver-file filters must stay.npm testfirst paint. The proof is a network-tab or preload-list assertion, like IMPORTANT: vendor deps waterfall over the CDN (no modulepreload for reached vendor entries) #754.packages/server/test/ssr/*(or the preload-focused test file). A multi-route shape where an inert/imports an SSR-only relative helper./fmt.js, a sibling/liveships, and/'s render emits NOmodulepreloadforfmt.js. Add the import-only under-fetch counterpart (a shipping component reached only via an import-only page IS preloaded). Include a counterfactual (revert the shipped-roots change, the over-fetch test reds).test/e2e/*,WEBJS_E2E=1): a network-probe that the dropped helper is not fetched, mirroring the IMPORTANT: vendor deps waterfall over the CDN (no modulepreload for reached vendor entries) #754 vendor e2e if one exists.agent-docs/advanced.mddocuments the app-module preload set, sync it. The IMPORTANT: vendor deps waterfall over the CDN (no modulepreload for reached vendor entries) #754 vendor-preload prose already states the shipped-roots rule, so cross-reference.Acceptance criteria
deduplicatedPreloadswalks from the shipped-module roots (moduleUrls-derived absolute paths) plus rendered components, not the raw route entriesmodulepreloadhintdeduplicatedPreloadsJSDoc documents the shipped-roots semantics, consistent withreachedVendorSpecifiers