vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
233 lines (232 loc) • 11.5 kB
JavaScript
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 = {};
let originalConfig = null;
/**
* 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 = 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;
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)
?.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",
exports: "named",
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 = {
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 = {
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",
exports: "named",
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,
};
}
};