Transformer
Contributor documentation. Covers how the transformer plugin processes
"use client"and"use server"directives.
Overview
The transformer runs in Vite's "post" enforcement phase via transformModuleIfNeeded:
export function reactTransformPlugin(options: StreamPluginOptions): Plugin {
return {
name: "vite-plugin-react-server:transformer",
enforce: "post",
transform(code, id) {
return transformModuleIfNeeded(code, id, options);
},
};
}
Transformation Steps
- Module identification — should this file be transformed?
- AST parsing — parse with JSX/TypeScript support
- Directive detection — find
"use client"/"use server"directives - Code transformation — apply environment-specific transforms
- Code generation — output with
retainLines: true
Directive Rules
const DIRECTIVE_CONFIGS = {
client: {
functionLevel: false, // "use client" only at file level
validate: (params) => params.index === 0,
warning: "'use client' directive is only allowed at the top of a file",
},
server: {
functionLevel: true, // "use server" at file or function level
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",
},
};
What Gets Transformed
"use server" files → server build
Exports get registerServerReference:
// Before
"use server";
export async function add(a, b) { return a + b; }
// After (dist/server/)
import { registerServerReference } from "react-server-dom-esm/server";
function add(a, b) { return a + b; }
registerServerReference(add, "/src/actions.server.ts", "add");
export { add };
"use client" files → server build
Implementation stripped, replaced with registerClientReference:
// Before
"use client";
export function Counter() { /* ... */ }
// After (dist/server/)
import { registerClientReference } from "react-server-dom-esm/server";
const Counter = registerClientReference(
function() { throw new Error("Cannot call Counter() from the server..."); },
"/components/Counter.client-CnBCzH8H.js",
"Counter"
);
export { Counter };
"use client" files → client/static builds
Directive is removed. Code is otherwise unchanged (minified in static build).
"use server" files → client/static builds
Server actions are excluded entirely.
Client-Module Classification
The transformer never re-implements directive detection. It calls detectClientModule({ source, moduleId, parseFn: this.parse }) from plugin/loader/directives/detectClientModule.ts — the single source of truth shared with the dev-server file watcher, the worker react-loader, build auto-discover, and the configurable loader.* defaults. The parseFn argument lets the transformer use Rollup's AST; the other call sites fall through to the parser-free sourceHasTopLevelClientDirective scanner. Both paths agree on every well-authored case.
The filename half of the classifier:
// plugin/loader/directives/detectClientModule.ts:23
const CLIENT_FILENAME_PATTERN = /(^|[\/.])client\.[cm]?[jt]sx?$/;
Matches the dotted-suffix convention (Foo.client.tsx) and the standalone basename client.tsx. The leading-(^|[/.]) anchor keeps it strict — clientUtils.ts is not matched.
Auto-Discovery Patterns
The build pulls client modules from two discoverers chained into one input record (see docs/internals/architecture.md for the full pipeline):
createGlobAutoDiscover("**/*.client.*")— filename convention.createDirectiveClientAutoDiscover()(plugin/config/autoDiscover/createDirectiveClientAutoDiscover.ts) — directive-only modules. WalksmoduleBase, skipsnode_modules, skips files already covered by the filename convention, then admits files wheresourceHasTopLevelClientDirective(source)returnstrue. Also skips any file referenced by<projectRoot>/index.html<script type="module" src>so Vite's ownindex.htmlmanifest entry isn't deduplicated away —processCssFilesForPagesreads global CSS off that entry.
Other pattern matchers used elsewhere in the build:
const patterns = {
serverPattern: /\.server\.(js|ts|jsx|tsx)$/,
pagePattern: /[Pp]age\.(js|ts|jsx|tsx)$/,
propsPattern: /[Pp]rops\.(js|ts|jsx|tsx)$/,
cssPattern: /\.css$/,
};
Custom Detection
Override directive detection through the plugin's loader config:
{
loader: {
isServerFunctionCode: (code, moduleId) => boolean,
isClientComponentCode: (code, moduleId) => boolean,
getDirectiveType: (directive, moduleId) => "client" | "server" | undefined,
}
}
Reference-Registration Config
The transformer's output (the register*Reference calls and the import they
come from) is also configurable through loader.*. Defaults
(plugin/config/defaults.tsx):
| Option | Default | Purpose |
|---|---|---|
registerClientReferenceName | "registerClientReference" | Symbol used to register "use client" modules in the server build. |
registerServerReferenceName | "registerServerReference" | Symbol used to register "use server" exports. |
importClientPath | "react-server-dom-esm/server" | Module the client-reference helper is imported from. |
importServerPath | "react-server-dom-esm/server" | Module the server-reference helper is imported from. |
Override these to target a different RSC runtime or a renamed reference helper.