Skip to content

Modulepreload hints emitted for server-only files the gate 404s #158

@vivek7405

Description

@vivek7405

Problem

The SSR modulepreload emitter and the source-serving authorization gate disagree about the .server.* boundary, so the framework preloads URLs it then refuses to serve.

Observed live on example-blog.webjs.dev: the homepage HTML emits

<link rel="modulepreload" href="/modules/posts/utils/slugify.ts">
<link rel="modulepreload" href="/modules/posts/types.ts">
<link rel="modulepreload" href="/modules/auth/types.ts">

all of which return 404. Those three files are server-only: slugify.ts is imported only by .server.ts query/action files, and the two types.ts are imported only via import type from .server.ts files. None is reachable from a browser entry.

Design / approach

Root cause is an inconsistency between two graph walks in packages/server/src/module-graph.js:

  • reachableFromEntries (the authorization gate) stops traversal at .server.{js,ts,mjs,mts} boundaries (SERVER_FILE_RE), so slugify.ts / types.ts are correctly NOT in the servable set, hence 404.
  • transitiveDeps (which feeds the preload hints at ssr.js:1163) has no such guard and walks straight through the server file into its server-only deps, emitting preload hints for them. The .server.ts files themselves are filtered from preloads by the server-file-index (byIndex) check, but the plain .ts files imported BY them slip past.

Fix: add the same server-boundary stop to transitiveDeps so the two walks structurally agree (the .server.* file may stay in the result, but its imports are not followed). Both elision callers in component-elision.js already pass serverFiles in their skip set, so they are unaffected; the preload emitter is the only caller walking through. This is a framework consistency bug, not a behavior change to elision.

Acceptance criteria

  • transitiveDeps does not follow edges out of .server.{js,ts,mjs,mts} files
  • No <link rel="modulepreload"> is emitted for a file outside the browser-bound servable set (preload set is a subset of the gate set, by construction)
  • The three live 404s on example-blog are gone
  • Unit test: transitiveDeps stops at the server boundary (counterfactual: fails if the guard is removed)
  • E2E/SSR test: a page importing a server action through a plain server-only util emits zero preloads for the util
  • Docs / AGENTS.md updated if the preload/gate contract wording changed

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions