vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
321 lines (290 loc) • 12.5 kB
text/typescript
import type { UserConfig, BuildEnvironmentOptions } from "vite";
import type { ResolvedUserOptions, AutoDiscoveredFiles } from "../types.js";
import type { OutputOptions } from "rollup";
import { join } from "node:path";
import { createLogger } from "vite";
import { getEnvValue, setEnvValue } from "../env/getEnvKey.js";
import { DEFAULT_CONFIG } from "../config/defaults.js";
// Cache for resolved environment configs to avoid recomputation
const stashedEnvironmentConfig: Record<string, BuildEnvironmentOptions | null> = {};
let originalConfig: UserConfig | null = null;
// TODO: Use virtual module approach with this.fileName in transformer
/**
* KEY DIFFERENCES FROM resolveUserConfig:
*
* 1. Uses BuildEnvironmentOptions type instead of UserConfig
* 2. Returns environment-specific configuration for Vite's Environment API
* 3. Must set preserveModules: true for preserveModulesRoot to work
* 4. Simplified input normalization (no moduleBase stripping in inputs)
* 5. Environment-specific output directories and settings
* 6. Uses Vite's natural module resolution and hashing
*
* FUTURE ENHANCEMENTS:
* - Could support callback functions for entryFileNames, chunkFileNames, and assetFileNames
* - This would allow custom hash generation based on file content as discussed in
* https://github.com/rollup/rollup/issues/5362
* - Would enable better cache busting and custom file naming strategies
*/
export type ResolveEnvironmentConfigProps = {
condition: "react-client" | "react-server";
config: UserConfig;
userOptions: ResolvedUserOptions;
autoDiscoveredFiles: AutoDiscoveredFiles;
ssr?: boolean;
forceResolve?: boolean;
};
export type ResolveEnvironmentConfigReturn =
| { type: "success"; environmentConfig: BuildEnvironmentOptions }
| { type: "error"; error: unknown };
export type ResolveEnvironmentConfigFn = (
props: ResolveEnvironmentConfigProps
) => ResolveEnvironmentConfigReturn;
/**
* Resolve environment-specific configuration for the Environment API.
* This is a sophisticated version that follows the same patterns as resolveUserConfig
* but adapted for Vite's Environment API requirements.
*/
export const resolveEnvironmentConfig: ResolveEnvironmentConfigFn =
function _resolveEnvironmentConfig({
condition,
config,
userOptions,
autoDiscoveredFiles,
ssr = undefined,
forceResolve = false,
}) {
// Handle config changes and caching
if (!forceResolve && originalConfig == null) {
originalConfig = config;
} else if (originalConfig != null && config !== originalConfig) {
forceResolve = true;
}
// Determine SSR mode with fallbacks
ssr =
typeof ssr === "boolean"
? ssr
: typeof config.build?.ssr === "boolean"
? config.build?.ssr
: condition === "react-server"
? true
: false;
if (condition === "react-server" && !ssr) {
const logger = config.customLogger ?? createLogger();
logger.warn(
"react-server build should be ssr, but it was manually set to false. This may not work as expected."
);
}
// Determine environment-specific directory
const envDir =
condition === "react-client" && ssr
? userOptions.build.client
: condition === "react-client"
? userOptions.build.static
: userOptions.build.server;
const envId = `${envDir}${ssr ? "-ssr" : ""}`;
// Check cache first
if (stashedEnvironmentConfig[envId] && !forceResolve) {
return {
type: "success",
environmentConfig: stashedEnvironmentConfig[envId]!,
};
}
try {
// Get environment variables (env vars take precedence over config)
const vitePrefix = config.envPrefix ?? DEFAULT_CONFIG.ENV_PREFIX;
const primaryPrefix =
typeof vitePrefix === "string" ? vitePrefix : vitePrefix[0];
const envBaseUrl = getEnvValue("BASE_URL", primaryPrefix);
const effectiveModuleBaseURL =
envBaseUrl != null && envBaseUrl !== ""
? envBaseUrl
: userOptions.moduleBaseURL;
const envPublicOrigin = getEnvValue("PUBLIC_ORIGIN", primaryPrefix);
const effectivePublicOrigin =
envPublicOrigin != null ? envPublicOrigin : userOptions.publicOrigin;
// Set process.env values to ensure they're available for server-side code
if (!getEnvValue("BASE_URL", primaryPrefix)) {
setEnvValue("BASE_URL", effectiveModuleBaseURL, primaryPrefix);
}
if (!getEnvValue("PUBLIC_ORIGIN", primaryPrefix)) {
setEnvValue("PUBLIC_ORIGIN", effectivePublicOrigin, primaryPrefix);
}
// Determine inputs based on condition and SSR with better normalization
let inputs: Record<string, string>;
if (condition === "react-client") {
if (ssr) {
// For SSR builds, exclude HTML files and use only client inputs
inputs = Object.fromEntries(
Object.entries(autoDiscoveredFiles.clientInputs).filter(
([, value]) => !value.endsWith(".html") && !value.endsWith(".htm")
)
);
} else {
// For static builds, use static inputs (which can include HTML)
inputs = autoDiscoveredFiles.staticInputs;
}
} else {
// For server builds, use server inputs (no HTML files)
inputs = autoDiscoveredFiles.serverInputs;
}
// Normalize inputs for this environment (following resolveUserConfig pattern)
const normalizedInputs = Object.fromEntries(
Object.entries(inputs).map(([key, value]) => [
key,
value.slice(Number(value.startsWith("/"))),
])
);
// Handle user-defined output options (following resolveUserConfig pattern)
const userDefinedOutput = config.build?.rollupOptions?.output;
const hasOtherOutput =
Array.isArray(userDefinedOutput) && userDefinedOutput.length > 1;
const hasValidOutput = userDefinedOutput && !hasOtherOutput;
const hasObjectOutput =
userDefinedOutput &&
!hasOtherOutput &&
typeof userDefinedOutput === "object" &&
userDefinedOutput !== null;
const userDefinedAssetFileNames = hasObjectOutput
? "assetFileNames" in userDefinedOutput
? userDefinedOutput.assetFileNames
: undefined
: // find the other asset file names
hasOtherOutput
? (userDefinedOutput.find((o) => o?.assetFileNames) as OutputOptions)
?.assetFileNames
: undefined;
const userDefinedChunkFileNames = hasValidOutput
? "chunkFileNames" in userDefinedOutput
? userDefinedOutput.chunkFileNames
: undefined
: undefined;
const userDefinedEntryFileNames = hasValidOutput
? "entryFileNames" in userDefinedOutput
? userDefinedOutput.entryFileNames
: undefined
: undefined;
// Rollup's preserveModulesRoot works in reverse of what you'd expect:
// - When user wants preservation (true): pass undefined to Rollup (don't strip anything)
// - When user wants stripping (false): pass moduleBase to Rollup (strip this path)
const preserveModulesRootString =
userOptions.build.preserveModulesRoot === false
? userOptions.moduleBase // Strip src/ from output paths
: undefined; // Keep src/ in output paths
// Basic rollup options - the file naming will be handled by resolveUserConfig mapping
const rollupOptions = {
input: normalizedInputs,
output: {
format: "esm" as const,
exports: "named" as const,
preserveModules: true, // Required for preserveModulesRoot to work
preserveModulesRoot: preserveModulesRootString,
// Note: entryFileNames, chunkFileNames, assetFileNames will be overridden by createEnvironmentPlugin mapping
entryFileNames: userDefinedEntryFileNames,
chunkFileNames: userDefinedChunkFileNames,
assetFileNames: userDefinedAssetFileNames,
},
};
if (condition === "react-client") {
// Client environment configuration
const clientEnvironmentConfig: BuildEnvironmentOptions = {
outDir: join(userOptions.build.outDir, envDir),
assetsDir: userOptions.build.assetsDir,
emptyOutDir: config.build?.emptyOutDir ?? true,
copyPublicDir:
typeof config.build?.copyPublicDir === "boolean"
? config.build?.copyPublicDir
: !ssr,
target: config.build?.target ?? "esnext",
minify: config.build?.minify ?? true,
manifest: config.build?.manifest ?? `.vite/manifest.json`,
ssrManifest: config.build?.ssrManifest ?? false,
ssrEmitAssets: config.build?.ssrEmitAssets ?? true,
cssCodeSplit:
typeof config.build?.cssCodeSplit === "boolean"
? config.build?.cssCodeSplit
: true,
modulePreload: config.build?.modulePreload ?? false,
rollupOptions,
};
stashedEnvironmentConfig[envId] = clientEnvironmentConfig;
return {
type: "success",
environmentConfig: clientEnvironmentConfig,
};
} else {
// Server environment configuration
const serverBuildEnvironmentOptions: BuildEnvironmentOptions = {
outDir: join(userOptions.build.outDir, envDir),
assetsDir: userOptions.build.assetsDir,
emptyOutDir: config.build?.emptyOutDir ?? true,
copyPublicDir:
typeof config.build?.copyPublicDir === "boolean"
? config.build?.copyPublicDir
: false,
target: config.build?.target ?? "esnext", // Use esnext for pure ESM - no helpers needed
minify: config.build?.minify ?? true,
manifest: config.build?.manifest ?? `.vite/manifest.json`,
ssrManifest: config.build?.ssrManifest ?? false,
ssrEmitAssets:
typeof config.build?.ssrEmitAssets === "boolean"
? config.build?.ssrEmitAssets
: true,
cssCodeSplit:
typeof config.build?.cssCodeSplit === "boolean"
? config.build?.cssCodeSplit
: true,
modulePreload: config.build?.modulePreload ?? false,
rollupOptions: {
...config.build?.rollupOptions,
input: normalizedInputs,
output: {
format: "esm" as const,
exports: "named" as const,
preserveModules: true, // Required for preserveModulesRoot to work
preserveModulesRoot: preserveModulesRootString,
// Note: entryFileNames, chunkFileNames, assetFileNames will be overridden by createEnvironmentPlugin mapping
entryFileNames: userDefinedEntryFileNames,
chunkFileNames: userDefinedChunkFileNames,
assetFileNames: userDefinedAssetFileNames,
},
preserveEntrySignatures:
config.build?.rollupOptions?.preserveEntrySignatures ?? "strict",
external: config.build?.rollupOptions?.external ?? [
"react",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"react-dom",
"react-server-dom-esm/server.node",
],
context: "module",
plugins: [
...(Array.isArray(config.build?.rollupOptions?.plugins)
? config.build.rollupOptions.plugins
: config.build?.rollupOptions?.plugins
? [config.build.rollupOptions.plugins]
: []),
{
name: "react-server-conditions",
buildStart() {
// Ensure react-server condition is available during server builds
if (condition === "react-server") {
// process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS || "") + " --conditions react-server";
}
},
},
],
},
};
stashedEnvironmentConfig[envId] = serverBuildEnvironmentOptions;
return {
type: "success",
environmentConfig: serverBuildEnvironmentOptions,
};
}
} catch (error) {
return {
type: "error",
error,
};
}
};