Skip to content

Support building route-aware server reference manifests #1337

Description

@hi-ogawa

Trying to capture background context and high level motivation for

Following is an AI-assited write-up. I steered the high level picture and get the vibe but haven't reviewed much:


Motivation

plugin-RSC currently generates a server-reference registry containing dynamic imports for all discovered server references in an RSC server build. When every route is handled by that same build and registry, each reference is globally addressable and can be loaded lazily on first use. A route-aware manifest is not necessary for that architecture.

A framework may instead choose to create route-scoped server outputs, independently deployed workers, or route-specific action policies. In those architectures, different route entrypoints intentionally have access to different server references. The framework then needs to determine which entrypoints consume or can load each reference.

A route-aware server-reference manifest would let a framework:

  • determine which route entrypoints can load each server reference;
  • route a request to a compatible entrypoint when the current one cannot load it;
  • validate that an action is reachable from the relevant application graph;
  • support future deployment models in which route entrypoints become separate server bundles or workers.

Vinext is exploring such route-scoped availability for future multi-entrypoint or multi-worker deployments. It can derive the required relationships by separately parsing application modules and reconstructing their import and re-export graph, but that duplicates work already performed by the RSC build pipeline, requires careful coordination with RSC transforms, and risks observing source at the wrong stage.

The requested capability is therefore not a change to plugin-RSC's default global lazy-loading model. It is build-time information that lets framework integrations deliberately construct narrower route boundaries.

Example Scenario

Consider a framework that builds /dashboard and /settings as route-scoped server outputs:

  1. /dashboard consumes a server function that is included in the /dashboard output but intentionally absent from /settings.
  2. An action request reaches the /settings output, for example after navigation or through an external router.
  3. /settings cannot resolve that reference from its route-scoped registry.
  4. The framework consults a build-generated manifest and finds that /dashboard can load it.
  5. The framework forwards the request to /dashboard or applies its own rejection policy.

The manifest could conceptually contain:

{
  "server-reference-id": {
    "entrypoints": ["/dashboard"]
  }
}

The exact output boundaries, manifest schema, request transport, and routing policy remain framework responsibilities. The RSC plugin only needs to expose enough stable build-time information for a framework to derive this relationship without performing a second full source crawl.

Requested Capability

Provide framework integrations with sufficient build-time information to derive which server references are consumed by or reachable from framework-defined entrypoints.

The supported capability should describe the available information and its lifecycle guarantees rather than require a particular implementation such as lexer callbacks or scan-build observers.

Potentially relevant information includes:

  • server-reference IDs and their defining modules;
  • whether a reference is a module export or a generated inline export;
  • module import and re-export relationships as observed by the RSC build;
  • the environment in which a module is scanned;
  • build boundaries or reset events needed to discard stale observations;
  • final bundler module information for correlating framework entrypoints with reachable modules.

Implementation Strategies to Evaluate

There may be a substantially simpler alternative to exposing source and lexer observations. The appropriate design depends on whether frameworks need module-level availability or exact export-level consumption.

Module Reachability

plugin-RSC could tag server-reference modules through Rollup ModuleInfo.meta, or otherwise expose their existing server-reference metadata. A framework could then traverse getModuleInfo(entry).importedIds and dynamicallyImportedIds from each route entrypoint and associate every server reference exported by a reachable module with that route.

Conceptually:

route entrypoint
  -> traverse resolved ModuleInfo edges
  -> find reachable server-reference modules
  -> associate their server references with the route

This resembles plugin-RSC's existing client-reference build flow, which records "use client" modules and later associates them with emitted RSC chunks during generateBundle.

It also matches the verified Next.js Webpack strategy for server actions. Next.js traverses each page entrypoint's reachable module graph and, when it encounters a module carrying server-action metadata, associates every action ID recorded for that module with the page. Although the same traversal tracks imported identifiers when building client-component entries, its server-action collection does not filter action IDs by the specific imported binding.

Advantages:

  • uses Vite/Rollup's resolved module graph;
  • requires little or no additional source parsing;
  • naturally handles aliases and ordinary static or dynamic module reachability;
  • may be sufficient when the manifest is used only to find a route capable of loading an action.

Tradeoff:

  • module reachability produces a coarse candidate set. If a module is reachable through a helper export or side-effect import and also exports ten actions, all ten may be associated with the route. This is conservative and may be the correct definition when the route output or its server-reference registry can load the whole module.

Specifier-Sensitive Scan Graph

The approach explored by Vinext and PR #1278 combines resolved ModuleInfo with original source and es-module-lexer import/export metadata. Vinext reconstructs named, default, and namespace imports, aliases, barrels, star re-exports, cycles, dynamic imports, and generated inline action exports.

This can narrow the candidate set for common ESM patterns. For example, when a route graph imports only actionA from a module exporting actionA and actionB, it can associate only actionA with the route.

It is not exact semantic consumption analysis. A server reference is an ordinary runtime value after it enters a module and can be stored in an object, aliased through local variables, returned from a function, passed through props, or re-serialized. Import/export syntax identifies where a reference identity enters the module graph, but it does not trace arbitrary value flow inside that graph.

For example, a syntactic re-export tracker may not understand relationships such as:

import { action } from './actions'

const wrapped = { action }
export { wrapped }

A namespace import or unconstrained dynamic access generally has to fall back to all exports. References introduced through runtime data rather than a statically analyzable module edge cannot be assigned reliably by either strategy.

Advantages:

  • can distinguish imported action bindings for common static ESM patterns;
  • avoids assigning unrelated exports from the same server-reference module;
  • can produce smaller route-specific action sets when its supported syntax is sufficient.

Costs:

  • exposes low-level scan lifecycle and lexer details from plugin-RSC;
  • requires framework code to reconstruct JavaScript import and re-export semantics;
  • duplicates complex graph interpretation across framework integrations;
  • creates more behavior to define for invalidation, transforms, virtual modules, and unusual import syntax.
  • can under-approximate references propagated through syntax or runtime value flow it does not model.

Final Output Analysis

A third option is to derive availability from final chunks and renderedExports, similar to client-reference grouping. This may work well when each route is built as an isolated output. In a multi-entry build, shared chunks, re-export barrels, and plugin-RSC's global server-reference registry can make it difficult to recover exact per-route export consumption from final output metadata alone.

The feature request should first establish the required correctness model. If the manifest must conservatively include every action a route might receive, module reachability is easier to reason about and may be preferable. Specifier-sensitive tracking is useful as an optimization only if its supported propagation rules and fallback behavior are explicit.

The module-reachability option is therefore not merely a simplified approximation invented for plugin-RSC. It is close to the model used by Next.js for its route-to-action worker manifest. Vinext's proposal attempts to be narrower than that prior art.

A syntactic consumption graph should not by itself be treated as a security boundary. If the manifest is intended as an authorization allowlist, arbitrary value propagation, dynamic loading, retained client references, and references received through serialized data need a defined conservative fallback. Even then, plugin-RSC could potentially expose a higher-level graph rather than raw lexer observations.

Expected Outcome

A complete example should demonstrate a framework integration that:

  1. deliberately builds at least two route-scoped RSC server outputs with different reachable server references;
  2. derives which server references each entrypoint can load;
  3. emits a route-aware server-reference manifest;
  4. sends an action request to a route whose entrypoint cannot load it;
  5. forwards the request to a compatible entrypoint or rejects it based on the manifest;
  6. obtains the required information without reparsing every source module in a separate framework pass.

The example can run both outputs in one process and route between them by pathname. Physical worker isolation is not required to prove that the generated availability relationship is correct.

Scope Boundary

The RSC plugin should provide the build-time facts needed to derive entrypoint-to-reference relationships. It should not initially prescribe:

  • how frameworks define routes or entrypoints;
  • whether an entrypoint is deployed as a worker, function, process, or server bundle;
  • how action requests are transported between entrypoints;
  • which compatible entrypoint a framework should prefer when several can load an action;
  • framework-specific authorization or request validation policy;
  • a universal manifest schema unless common requirements emerge from integrations.

Open Questions

  • Which build-time facts are stable enough to expose as a supported plugin API?
  • Can final bundler module graphs provide the required reachability information, or are source-level import and re-export edges also necessary?
  • Is module-level conservative assignment sufficient for request routing, and what concrete optimization requires a narrower specifier-sensitive candidate set?
  • Which server-reference propagation patterns must the manifest handle, and what is the conservative fallback when static syntax cannot represent them?
  • At which build phase can an integration reliably correlate transformed server references, original modules, and framework entrypoints?
  • How should watch-mode invalidation and repeated environment builds reset collected state?
  • Is inline-versus-module-level server-reference metadata part of this capability or an independent generally useful feature?
  • Should plugin-RSC expose lower-level observations for frameworks to interpret, or provide a higher-level entrypoint reachability abstraction?
  • What is the smallest end-to-end fixture that proves the capability without embedding one framework's routing architecture in plugin-RSC?

Prior Art and Context

Next.js generates a server-reference manifest that maps each action ID to the App Router page entrypoints capable of loading it. If the current page entrypoint cannot load an action, Next.js selects another compatible page entrypoint and internally forwards the request to its pathname.

Detailed research on Next.js server-action manifests and forwarding

Next.js needs this mapping because server actions are compiled into route-specific page entrypoint graphs. Lazy loading can load a known chunk from the current graph, but it cannot load an action that has no module mapping in that entrypoint. Next.js calls these page entrypoints workers, but this mechanism does not directly select a process, lambda, or deployment.

In the verified Webpack implementation, “a page uses an action” is determined conservatively through module reachability. Reaching a module with server-action metadata associates all action IDs from that module with the page. Next.js does not reconstruct named import and re-export semantics to narrow this action set.

This differs from plugin-RSC's current global registry, which emits a dynamic import for every discovered server reference in the server build. The Next.js navigation race is therefore prior art for route-scoped action availability, not evidence that every RSC integration needs route-aware forwarding.

vite-plugin-react PR #1278 proposes observing imports and exports during RSC scan builds and recording inline export names. This is one possible implementation or prerequisite, rather than the feature definition.

Vinext PR #2520 explores using that information to derive action availability. Vinext is also interested in future multi-entrypoint or multi-worker deployments, where a framework could use the same manifest to route requests between independently deployed entrypoints.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions