vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
117 lines (112 loc) • 3.77 kB
text/typescript
import { resolveOptions } from "../config/resolveOptions.js";
import type { InputNormalizer, ResolvedUserOptions, StreamPluginOptions } from "../types.js";
import { type Manifest, type Plugin } from "vite";
import { transformModuleIfNeeded } from "../loader/react-loader.js";
import { DEFAULT_CONFIG } from "../config/defaults.js";
import { createInputNormalizer } from "../helpers/inputNormalizer.js";
import { tryManifest } from "../helpers/tryManifest.js";
import { join } from "node:path";
/**
* Plugin for transforming React Client Components.
*
* Core responsibilities:
* 1. Detects "use client" directives
* 2. Transforms client components for RSC boundaries
* 3. Adds client reference metadata for RSC
*
* When a component is marked with "use client", it:
* - Gets transformed into a client reference
* - Maintains module ID for RSC boundaries
* - Preserves class/function behavior
*
* @example
* ```ts
* export default defineConfig({
* plugins: [
* viteReactClientTransformPlugin({
* projectRoot: process.cwd(),
* })
* ]
* });
* ```
*/
export function reactTransformPlugin(options: StreamPluginOptions): Plugin {
let normalizer: InputNormalizer;
let clientManifest: Manifest;
let isDev:boolean;
let userOptions: ResolvedUserOptions
return {
name: "vite:react-transform",
enforce: "pre", // Run before Vite's transforms
config(config, configEnv) {
const resolvedOptionsResult = resolveOptions(
options,
config.build?.outDir?.startsWith(
join(options.build?.outDir ?? DEFAULT_CONFIG.BUILD.outDir, options.build?.client ?? DEFAULT_CONFIG.BUILD.client)
) ?? false
);
isDev = configEnv.mode === "development" && configEnv.command === "serve"
if (resolvedOptionsResult.type === "error") throw resolvedOptionsResult.error;
userOptions = resolvedOptionsResult.userOptions;
normalizer = createInputNormalizer({
root: resolvedOptionsResult.userOptions.projectRoot,
preserveModulesRoot: undefined,
removeExtension: false,
});
},
async transform(code, id, options) {
const ssr = options?.ssr ?? false;
if (!ssr) return null;
if (!id.match(DEFAULT_CONFIG.FILE_REGEX)) return null;
if (!code.match('"use client"')) return null;
// Get the client manifest
const clientManifestResult = tryManifest({
root: userOptions.projectRoot,
outDir: join(
userOptions.build.outDir,
userOptions.build.client
),
ssrManifest: false,
});
if (clientManifestResult.type === "error") throw clientManifestResult.error;
clientManifest = clientManifestResult.manifest;
const [key, value] = normalizer(id);
const transformed = await transformModuleIfNeeded(
code,
id,
// Pass null for nextLoad since we don't need module loading in the plugin
null
);
if (!transformed) return null;
if (isDev) {
return {
code: transformed,
map: null,
};
}
const moduleIdIndex = transformed.indexOf(value);
if (moduleIdIndex === -1) {
console.warn(
`[vite-plugin-react-server] Could not find module id in transformed code. Ignoring.`,
{
code,
id,
transformed,
}
);
return null;
}
const clientPath = clientManifest[key]?.file;
if (!clientPath) {
console.warn(
`[vite-plugin-react-server] Could not find client path for ${value}. Ignoring.`
);
return null;
}
return {
code: transformed.replace(key, clientPath),
map: null,
};
},
};
}