API Reference

This document provides a comprehensive reference for the Vite React Server Plugin's API.

Plugin Options

The plugin accepts a configuration object that satisfies the StreamPluginOptions type:

import type { StreamPluginOptions } from "vite-plugin-react-server/types";

export const config = {
  moduleBase: 'src',
  // ... options
} satisfies StreamPluginOptions;

Core Options

Option Type Description Example
moduleBase string Root directory for project modules "src"
moduleBasePath string Second argument to renderToPipeableStream "/my-repo/"
moduleBaseURL string Requests from this base "/my-repo/"
publicOrigin string Origin for building absolute urls (absoluteURL/pageURL, canonical hrefs in snapshots). Browser module loading is deliberately same-origin — the bootstrap entry and flight-loaded chunks must share one origin/module graph or hydration fails with two Reacts — so this never affects where chunks load from; to CDN-serve modules, pass an absolute moduleBaseURL explicitly. "https://username.github.io"
Page (url: string) => string Maps URLs to page component files -
props (url: string) => string Maps URLs to props files -
routes RoutesOption File-based router (v3+): scans a route tree and derives Page, props, routePatterns and the prerender worklist. Takes { dir?, staticPaths? } or a fileRouter() result. See Routing. { dir: "routes" }
routePatterns string[] Route patterns for param matching (derived by routes; state explicitly only without it) ["/", "/greet/$name"]
stripHtmlSuffix boolean Strip a trailing .html before route matching. Default true (SSG-correct: /profile/42.html → { id: "42" }); set false when .html is content, not transport (a catch-all serving .html-named documents). See Routing. false
Html React.ComponentType<HtmlProps> Wrapper component for production pages -
pageExportName string Name of the page export "Page"
propsExportName string Name of the props export "props"
clientEntry string Optional explicit client entry. Not needed when index.html has a <script type="module" src> for the entry — Vite discovers it itself. See Configuration. -
htmlWorkerPath string Path to custom HTML worker -
rscWorkerPath string Path to custom RSC worker -
CssCollector React.ComponentType<CssCollectorProps> Component for collecting CSS (handles both inline and non-inline modes) -
build BuildOptions Build configuration -
css CssOptions CSS handling configuration -
verbose boolean Enable verbose logging true
rscTimeout number Timeout in milliseconds for RSC operations 5000
htmlTimeout number Timeout in milliseconds for HTML generation operations 15000
htmlWorkerStartupTimeout number Timeout in milliseconds for HTML worker startup 5000
rscWorkerStartupTimeout number Timeout in milliseconds for RSC worker startup 5000
runner "main" | "isolated" | "edge" Required. The execution paradigm: where react-server executes (see Configuration → Runner); validated against the process condition at config-resolve time. "edge" requires transport: "webpack" and makes the baked pair the serving artifact; its dev server runs the isolated worker shape (the pair serves prod) -
transport "esm" | "webpack" The deploy's flight flavor; "webpack" renders the static snapshots through the baked pair and the dev server serves webpack flight, so every surface agrees (see Configuration → Transport) "esm"
onMetrics OnMetrics Callback for build metrics (render, worker-startup, module-resolution, edge-bake, inline-flight and ssg-render metrics) -
onEvent (event: PluginEvent) => void Callback for plugin events -
normalizer InputNormalizer Custom input normalizer -
moduleID (id: string) => string Custom module ID transformer -
pipeableStreamOptions ReactServerDomEsmOptions Options for React's renderToPipeableStream -

Build Options

Option Type Description Default
pages string[] Routes to generate []
dir string Base directory "dist"
client string Client assets directory "client"
server string Server assets directory "server"
static string Static output directory "static"
hash string Hash for client files "hash"
preserveModulesRoot boolean When true, preserves the moduleBase directory (e.g. src/) in output paths. When false, strips it from output paths. false
renderMode "parallel" | "sequential" How SSG renders pages: concurrent batches, or one at a time (less memory, deterministic order) "parallel"
batchSize number Pages rendered concurrently when renderMode is "parallel" 8
inlineFlight boolean | "blob" | "stream" How the flight payload rides inside the HTML. "blob" (alias true): buffer and inline it into each prerendered route's index.html as one non-executable <script id="vprs-flight"> at the post-SSG point, in both build modes — static/CDN targets, simplest client. "stream": interleave it into the HTML as it streams on per-request document renders — dynamic/edge targets, TTFB is the shell flush; prerendered pages have no streamed form — under transport: "webpack" the SSG freeze emits them with the blob form instead, under the esm pass they fall back to fetching index.rsc. false: no inlining, the client always fetches .rsc. See Build Output. false
edge boolean | EdgeBuildConfig Single-isolate edge bundle (see build.edge below) true
assetsDir string Where every generated browser file goes (JS, CSS, images, fonts), mirroring the source tree: assets/page/hero-<hash>.png. "" drops the prefix. See Build Output. "assets"
api string API output directory "api"
outDir string Output directory "dist"
rscOutputPath string RSC output filename "index.rsc"
htmlOutputPath string HTML output filename "index.html"
entryFile (chunk: PreRenderedChunk, ssr: boolean) => string Custom entry file naming -
chunkFile (chunk: PreRenderedChunk, ssr: boolean) => string Custom chunk file naming -
assetFile (asset: PreRenderedAsset, ssr: boolean) => string Custom asset file naming -
extensionMap Record<string, string> Custom file extensions -
moduleExtension string Module file extension ".js"
jsExtension string JavaScript file extension ".js"
cssExtension string CSS file extension ".css"
htmlExtension string HTML file extension ".html"
jsonExtension string JSON file extension ".json"
rscExtension string RSC file extension ".rsc"
cssModuleExtension string CSS module file extension ".css.js"
nodeExtension string Node.js file extension ".node"

CSS Options

Option Type Description Default
inlineCss boolean | undefined undefined (default) = auto: inline a file when it is at or under inlineThreshold or matches inlinePatterns; true = always inline; false = always link undefined
inlineThreshold number Size threshold for inlining (bytes) 4096
inlinePatterns RegExp[] Patterns for files to always inline []
linkPatterns RegExp[] Patterns for files to always link []

Loader Options

Option Type Description Default
importServerPath string Path for server imports "vite-plugin-react-server/loader"
importClientPath string Path for client imports "vite-plugin-react-server/loader"
registerClientReferenceName string Name for client reference registration "registerClientReference"
registerServerReferenceName string Name for server reference registration "registerServerReference"
serverDirective RegExp Pattern to match server directives /^"use server"$/
clientDirective RegExp Pattern to match client directives /^"use client"$/
directivePattern RegExp General pattern for directives /^"use (server|client)"$/
allowedDirectives string[] List of allowed directive names ["use server", "use client"]
mode "development" | "production" | "test" Loader mode "development"
isServerFunctionCode (code: string, moduleId?: string) => boolean Custom server function detection -
isClientComponentCode (code: string, moduleId?: string) => boolean Custom client-module detection (source + filename) detectClientModule (top-of-file "use client" directive; the .client.* filename convention carries no meaning to detection)
isClientComponentByCode (code: string) => boolean Custom client-module detection (source only) detectClientModule
isClientComponentByName (moduleId: string) => boolean Opt-in escape hatch for name-based client detection. The default never classifies by name — only a "use client" directive makes a client module. Supply your own predicate if you really want the filename to decide. always false
getDirectiveType (directive: string, moduleId?: string) => "client" | "server" | undefined Custom directive type detection -

Auto-Discovery Options

Option Type Description Default
cssPattern RegExp | string Pattern to match CSS files /.css$/
cssModulePattern RegExp | string Pattern to match CSS module files /.css.js$/
clientPattern RegExp | string Filenames that follow the client-module naming convention (*.client.tsx). Only the "use client" directive makes a module a client module; this pattern is a lint — a matching first-party file without the directive gets a build warning to add one. /(^|[/.])client.[cm]?[jt]sx?$/
serverPattern RegExp | string Pattern to match server function files /.server.(js|ts|jsx|tsx)$/
htmlPattern RegExp | string Pattern to match HTML files /.html$/
jsonPattern RegExp | string Pattern to match JSON files /.json$/
modulePattern RegExp | string Pattern to match module files /.(js|ts|jsx|tsx)$/
rscPattern RegExp | string Pattern to match RSC files /.rsc$/
pagePattern RegExp | string Pattern to match page files /[Pp]age.(js|ts|jsx|tsx)$/
propsPattern RegExp | string Pattern to match props files /[Pp]rops.(js|ts|jsx|tsx)$/
dotPattern RegExp | string Pattern to match dot files /^.[^/]+$/
nodePattern RegExp | string Pattern to match Node.js native modules /.node$/
vendorPattern RegExp | string Vendor files the RSC transform skips. A vendor module that carries a "use client" or "use server" directive, and any module of a package in clientPackages, is still transformed. The pattern is tested against three strings, and a match on any counts: the module id as the bundler gives it, the root-relative path with a leading slash (/node_modules/react/index.js), and that path from its first /node_modules/ segment; the derived forms use forward slashes and drop the query. /^/node_modules//
virtualPattern RegExp | string Pattern to match virtual files /^virtual:/

Component Props

HtmlProps

Props passed to the Html wrapper component during static generation:

type HtmlProps = {
  pageProps?: any;
  Page: PageComponentType;
  route: string;
  url: string;
  projectRoot: string;
  moduleBase: string;
  moduleBaseURL: string;
  moduleBasePath: string;
  moduleRootPath: string;
  cssFiles: Map<string, CssContent>;
  manifest: Manifest;
  Root: RootComponentType | typeof React.Fragment;
  globalCss: Map<string, CssContent>;
  as?: keyof JSX.IntrinsicElements;
};

RootProps

Props for the Root component that wraps page content:

type RootProps = {
  as: keyof JSX.IntrinsicElements;
  cssFiles?: Map<string, CssContent>;
  pageProps?: any;
  Page: PageComponentType;
  id?: string;
};

CssContent

A value in cssFiles / globalCss maps — props for one <style> or <link> tag, exactly as the Css component renders it (see CSS Handling):

type CssContent =
  // inline: rendered as <style type="text/css">{children}</style>
  | { as: "style"; type: "text/css"; children?: React.ReactNode }
  // linked: rendered as <link rel="stylesheet" href … /> (react-dom hoistable)
  | { as: "link"; rel: string; href: string; precedence?: string };

Build Configuration

BuildConfig

Configuration for build processes:

interface BuildConfig {
  pages: string[];
  client?: string; // Default: "client"
  server?: string; // Default: "server"
  static?: string; // Default: "static"
  outDir?: string; // Default: "dist"
  assetsDir?: string; // Default: "assets"
  rscOutputPath?: string; // Default: "index.rsc"
  htmlOutputPath?: string; // Default: "index.html"
  preserveModulesRoot?: boolean;
  hash?: string;
  edge?: EdgeBuildConfig;
}

build.edge

Single-isolate edge bundle (additive; ON by default). See Edge / Single-Isolate.

// build.edge?: boolean | EdgeBuildConfig   (default: true)
//   true / omitted  → emit with defaults
//   false           → opt out
//   { … }           → emit, with overrides
interface EdgeBuildConfig {
  outDir?: string;    // Default: "server-edge" (under build.outDir)
  minify?: boolean;   // Default: true — edge runtimes cap bundle size
  // Which RSC transport the baked bundle renders through. "esm" resolves
  // client references via import(moduleBaseURL + id) at request time;
  // "webpack" resolves them through the baked client manifest (closed module
  // map — the fully self-contained deployment model). Defaults from the
  // top-level `transport` option when set. Don't mix flavors across one
  // deploy's routes — see [Edge / Single-Isolate](./edge.md).
  transport?: "esm" | "webpack"; // Default: "esm"
}

Drive the baked render.js with createEdgeHandler from vite-plugin-react-server/stream: renderRouteToDocument for a flash-free inline-flight document, handleRouteAction for the baked server-action gate, or the low-level renderRouteToFlight producer.

CssConfig

Configuration for CSS handling:

interface CssConfig {
  inlineCss?: boolean; // Default: undefined = auto (inline files <= inlineThreshold); false disables inlining
  inlineThreshold?: number; // Default: 4096 (4KB)
  inlinePatterns?: RegExp[];
  linkPatterns?: RegExp[];
}

preserveModulesRoot Behavior

The build.preserveModulesRoot option controls how the moduleBase directory appears in build output paths:

When preserveModulesRoot: true (preserve paths)

  • Input: src/page/home.tsx
  • Output: dist/client/src/page/home.js
  • Behavior: The src/ directory is preserved in the output path

When preserveModulesRoot: false (strip paths - default)

  • Input: src/page/home.tsx
  • Output: dist/client/page/home.js
  • Behavior: The src/ directory is removed from the output path

This option is useful when you want to maintain your source directory structure in the build output, especially for debugging or when integrating with tools that expect specific path structures.

Event System

PluginEvent

onEvent receives every plugin event; switch on event.type. Each event carries a data payload (full shapes in plugin/types.ts):

type When Notable data fields
"file.write" a prerendered file starts writing path, fileType ("html"/"rsc"), route, stream
"file.write.done" that write completed route, fileType, path, content, chunks
"route.error" a route render failed route, error, isPanic, panicThreshold
"route.shellError" the HTML shell failed route, error
"route.shellReady" / "route.allReady" streaming milestones per route route
"route.postpone" React postponed a route route, reason
"props.load" a route's loader resolved route
"css.process" CSS was processed for a route route/css details
"build.start" a build began —
"build.writeBundle.server" / ".client" / ".static" an environment's bundle was written bundle details
"build.ssg.start" / "build.ssg.end" the SSG pass began / finished —

Metrics

onMetrics receives a discriminated union of render, worker-startup, module-resolution, ssg-render, inline-flight and edge-bake metrics — see the Metric types table under Metric Watcher for the full list of metrics.type values and their fields.

Type Definitions

Component Types

// Page component — receives page props, returns React element
type PageComponentType = (props: any) => React.ReactNode;

// Root component — wraps page with CSS and layout
type RootComponentType = (props: RootProps) => React.ReactNode;

// Html component — outer HTML shell for static generation
type HtmlComponentType = (props: HtmlProps) => React.ReactNode;

// CSS component — renders inline <style> or <link> tags
type CssComponentType = (props: CssProps) => React.ReactNode;

Note: The full type signatures use generics constrained by a ViteReactServerComponentsPlugin interface for advanced type customization. The simplified versions above cover most use cases. See plugin/types.ts for the full generic signatures.

Environment Detection

import { getCondition } from "vite-plugin-react-server/config";

// "react-server" when the process runs under that condition, else null.
function getCondition(): string | null;

Directive Patterns

Server Directives

const SERVER_DIRECTIVE = /^["']use server["'];?\s*$/gm;

Client Directives

const CLIENT_DIRECTIVE = /^["']use client["'];?\s*$/gm;

Validation Rules

const DIRECTIVE_CONFIGS = {
  client: {
    functionLevel: false,
    target: 'client',
    validate: (params) => params.index === 0, // Must be at file start
    warning: "'use client' directive is only allowed at the top of a file"
  },
  server: {
    functionLevel: true,
    target: 'server',
    validate: (params) => {
      const before = params.code.slice(0, params.index).trim();
      return before === '' || before.endsWith('\n');
    },
    warning: "File-level directives must be at the top of the file, before any other code"
  }
};

File Patterns

Auto-Discovery Patterns

const AUTO_DISCOVER = {
  modulePattern: /\.(m|c)?(j|t)sx?$/,
  serverPattern: /(?:\.\/)?server(?:\.(m|c)?(j|t)sx?)?$/,
  clientPattern: /(^|[\/.])client\.[cm]?[jt]sx?$/,
  pagePattern: /(?:\.\/)?page(?:\.(m|c)?(j|t)sx?)?$/,
  propsPattern: /(?:\.\/)?props(?:\.(m|c)?(j|t)sx?)?$/,
  cssPattern: /\.css$/,
  jsonPattern: /\.json$/,
  htmlPattern: /\.html$/,
  rscPattern: /\.rsc$/,
};

A file is a client module when its source starts with a top-of-file "use client" directive. That is the only mechanism.

clientPattern is a lint, not a classifier: a first-party file whose name matches but has no directive gets a build warning to add one. The warning exists because filename conventions don't travel — every React toolchain honors the directive, none honor a name — so a file relying on its name alone would render as a server module anywhere else.

Extension Mapping

const EXTENSION_MAP = {
  ".js": ".js",
  ".ts": ".js",
  ".jsx": ".js",
  ".tsx": ".js",
  ".css": ".css",
  ".json": ".json",
  ".html": ".html",
  ".rsc": ".rsc",
  ".client": ".client.js",
  ".server": ".server.js",
};

Testing Utilities

doBuild Function

function doBuild(options: {
  projectRoot: string;
  build: BuildConfig;
  verbose?: boolean;
}): Promise<PluginEvent[]>;

Test Configuration

const testConfig: StreamPluginOptions = {
  moduleBase: "src",
  Page: (url) => `src/page.tsx`,
  build: { pages: ["/"] },
  verbose: true,
  onEvent: (event) => console.log(event),
  onMetrics: (metrics) => console.log(metrics),
};

Import Paths

Main Plugin

import { vitePluginReactServer } from "vite-plugin-react-server";

Client Plugin

import { vitePluginReactClient } from "vite-plugin-react-server/client";

Server Plugin

import { vitePluginReactServer } from "vite-plugin-react-server/server";

Router (config side)

// Condition-neutral: fileRouter builds the router table (used in vite.config),
// withParams wraps a loader, matchRoutes/fillPattern are the pattern helpers.
import { fileRouter, withParams, matchRoutes, fillPattern } from "vite-plugin-react-server/router";

Router (client runtime)

// "use client" side: startClient wires router + hydration + HMR in one call.
import { startClient, Link, useParams, useLocation, useRouter, RouterProvider, createRouter } from "vite-plugin-react-server/router/client";

See Routing for the full guide.

Stream Helpers

import { createRscStream, createHtmlStream, handleRscStream } from "vite-plugin-react-server/stream";

Utils (Conditional Export)

// Default condition (client) — includes createReactFetcher, setupRscHmr, useRscHmr
import { createReactFetcher, setupRscHmr, useRscHmr, callServer } from "vite-plugin-react-server/utils";

// react-server condition — excludes browser-only modules
import { callServer, env, routeToURL } from "vite-plugin-react-server/utils";

For consumers who want to import only the pure helpers (urls, env, routeToURL) without dragging in the optional react-server-dom-esm peer that the RSC-client helpers require, the RSC-client helpers are also available behind their own subpath:

// Opt-in subpath for RSC-client helpers — explicitly requires the
// `react-server-dom-esm` peer to be resolvable (the vprs Vite plugin
// sets this up automatically for RSC apps).
import { createReactFetcher, setupRscHmr, useRscHmr, callServer } from "vite-plugin-react-server/utils/rsc-client";

Cancelling a superseded RSC fetch

createReactFetcher accepts an AbortSignal. Without one, a flight fetch that gets superseded — a fast double-navigation, a refetch racing an earlier fetch — aborts mid-body and the decoder's TypeError: Error in input stream lands in the nearest error boundary, briefly flashing an error card for a stream nobody is waiting on anymore.

Have each navigation/refetch own an AbortController, and abort the previous one before starting the next. A stream cancelled through its signal never rejects — the stale thenable stays pending (React keeps the current UI) until the replacing fetch resolves:

let controller: AbortController | undefined;

function navigate(url: string) {
  controller?.abort();          // cancel the in-flight stream, silently
  controller = new AbortController();
  setContent(createReactFetcher({ url, signal: controller.signal }));
}

Genuine flight failures (network errors, decode failures on a stream that was NOT aborted) still reject and reach the error boundary as before.

Storybook preset

A Storybook preset that makes a vprs app build and render in Storybook. Referenced as an addon, not imported directly. See Storybook.

// .storybook/main.ts
export default { addons: ["vite-plugin-react-server/storybook"] };

react-server-dom-esm transport

The react-server-dom-esm transport ships in the react-server-loader dependency, which exposes it under public subpaths so non-plugin consumers (e.g. the Storybook preset) can resolve it. Not typically imported by app code.

// ESM client.browser build (dev/prod conditioned)
import "react-server-loader/client.browser";

In vprs 1.x this was re-hosted under vite-plugin-react-server/react-server-dom-esm/*. That self-export was removed in 2.0 — import from react-server-loader instead.

Type Imports

import type { 
  StreamPluginOptions,
  HtmlProps,
  RootProps,
  BuildConfig,
  PluginEvent,
  OnMetrics
} from "vite-plugin-react-server/types";

Component Imports

import { Css } from "vite-plugin-react-server/components";

Configuration Utilities

import { getCondition } from "vite-plugin-react-server/config";

Metric Watcher

metricWatcher renders the plugin's metrics stream as readable build output. It is opt-in: without an onMetrics callback the build prints only its raw summary lines. Wire the watcher in and those summaries are replaced by richer, per-route output:

import { metricWatcher } from "vite-plugin-react-server/metrics";

export const config = {
  moduleBase: "src",
  onMetrics: metricWatcher(),
};

A healthy static build then reads like this (a 300-page consumer build):

HTML-worker started in 154ms (initial route: /)
cold module load 211ms (mainThread, first route: /10mmc/credits)
— warm-up: 1 route in 317ms (cold module load paid here) —
dist/static/10mmc/credits/index.{rsc,html} 20.4 kB+21.8 kB 317ms (modules 211ms + route 106ms)
— batch 1: 8 routes in 150ms wall (sum 1.11s, 7.4× parallel) —
…
rendered 300 pages in 5.51s (54.4 pages/s)
inlined flight into 300 page(s) in 350ms
baked edge producer in 796ms → dist/server-edge

Reading it: the first route renders solo as a warm-up and pays the one-time cold module load; every later batch renders full-width against the warm graph, and its overview line shows wall time vs summed per-route time (the parallel factor). Each route gets one consolidated line — the flight (.rsc) and the document (.html) share a clock and complete together (the html render consumes the flight as it streams); a (+Nms html) suffix appears only when the document genuinely trailed the flight. A (modules X + route Y) split separates a route's own module-load cost from its render time.

Warnings are reserved for anomalies: renders slower than maxTime after cold-start attribution, stream backpressure past maxBackpressure, and module resolutions that were all cache-hit waiting.

Metric Watcher Options

Option Type Default Description
maxTime number 200 Slow-render / slow-resolution warning threshold (ms)
maxBackpressure number 1 Backpressure occurrences tolerated before warning
warnOnly boolean false Only warnings — suppress all info output
warn function console.warn Custom warning sink
info function console.info Custom info sink

Metric types

onMetrics receives a union (see OnMetrics in the exported types); a custom callback can switch on metrics.type:

type When Notable fields
"rsc-full" / "rsc-headless" / "html" per route render processingTime, fileSize, streamMetrics, batch
"worker-startup" once per worker workerType, startupTime
"module-resolution" per route load phase resolutionTime, resolveStartAt, moduleRunAt, moduleRunTime
"ssg-render" once per build pages, failed, renderTime
"inline-flight" after the post-write inline pass pages, inlineTime
"edge-bake" per edge-bake half kind (producer/consumer), outputPath, bakeTime

The watcher ignores metric types it doesn't know, so a newer plugin with an older consumer-pinned watcher degrades gracefully.

Backpressure Monitoring

The metric watcher automatically monitors stream backpressure, which occurs when:

  • The file writer is slower than the HTML generation
  • The worker communication queue is full
  • System resources are constrained

Backpressure warnings help identify performance bottlenecks and potential memory issues.