UNPKG

vite-plugin-react-server

Version:
796 lines (755 loc) 33.8 kB
import { createLogger, defaultClientConditions, defaultServerConditions, type ConfigEnv, type UserConfig, } from "vite"; import type { ResolvedUserConfig, ResolvedUserOptions, AutoDiscoveredFiles, } from "../types.js"; import { join, resolve } from "node:path"; import { readFileSync, existsSync } from "node:fs"; import type { OutputOptions, PreRenderedAsset, PreRenderedChunk } from "rollup"; import { DEFAULT_CONFIG } from "./defaults.js"; import { getNodeEnv } from "./getNodeEnv.js"; import { getEnvValue, setEnvValue } from "../env/getEnvKey.js"; import { mergeClientPackagesNoExternal, mergeClientPackagesOptimizeDepsExclude, } from "../clientPackages/index.js"; import { createRollupLikeHash } from "./createRollupLikeHash.js"; const stashedUserConfig: Record<string, ResolvedUserConfig | null> = {}; let originalConfig: UserConfig | null = null; export type ResolveUserConfigProps = { condition: "react-client" | "react-server"; config: UserConfig; configEnv: ConfigEnv; userOptions: ResolvedUserOptions; autoDiscoveredFiles: AutoDiscoveredFiles; forceResolve?: boolean; ssr?: boolean; }; export type ResolveUserConfigReturn = | { type: "success"; userConfig: ResolvedUserConfig } | { type: "error"; error: unknown }; export type ResolveUserConfigFn = ( props: ResolveUserConfigProps ) => ResolveUserConfigReturn; export const resolveUserConfig: ResolveUserConfigFn = function _resolveUserConfig({ condition, config, configEnv, userOptions, autoDiscoveredFiles, forceResolve = false, ssr = undefined, }) { if (!forceResolve && originalConfig == null) { originalConfig = config; } else if (originalConfig != null && config !== originalConfig) { forceResolve = true; } ssr = typeof ssr === "boolean" ? ssr : typeof config.build?.ssr === "boolean" ? config.build?.ssr : condition === "react-server" ? true : typeof configEnv.isSsrBuild === "boolean" ? configEnv.isSsrBuild : false; if (condition === "react-server" && !ssr) { const logger = config.customLogger ?? createLogger(); logger.warn( "react-server build should be ssr, but is was manually set to false. This may not work as expected." ); } const envDir = condition === "react-client" && ssr ? userOptions.build.client : condition === "react-client" ? userOptions.build.static : userOptions.build.server; const envId = `${envDir}${ssr ? "-ssr" : ""}`; if (stashedUserConfig[envId] && !forceResolve) { return { type: "success", userConfig: stashedUserConfig[envId], }; } // Get existing inputs const handleSsrEntryName = ( info: PreRenderedChunk, input: string | null, fallback: ( info: PreRenderedChunk, ssr: boolean, sourceContent?: string ) => string, ssr: boolean ) => { // Read source content for consistent hashing across all environments let sourceContent: string | undefined; if (input) { try { const sourcePath = resolve(userOptions.projectRoot, input); if (existsSync(sourcePath)) { sourceContent = readFileSync(sourcePath, "utf-8"); } } catch (error) { // Fallback to filename-based hashing if source file can't be read } } if (!ssr || !input) { if (typeof fallback === "function") { return fallback(info, false, sourceContent); } return userOptions.normalizer(info.name)[0]; } const normalized = userOptions.normalizer(input); let value = normalized[1]; if (value.startsWith(userOptions.moduleBasePath)) { value = value.slice(userOptions.moduleBasePath.length); } // Apply the same hash function for server environment to ensure consistency // This ensures that client components have the same hash across all environments if (typeof fallback === "function") { return fallback(info, true, sourceContent); } return userOptions.normalizer(info.name)[0]; }; const handleSsrAssetName = ( info: PreRenderedAsset, input: string | null, fallback: (info: PreRenderedAsset, ssr: boolean) => string, ssr: boolean ) => { if (info.source === "") { return ""; } // Check if this is a CSS file const isCssFile = input?.endsWith(".css") || info.names?.some((name) => name.endsWith(".css")); if (!ssr || !input || isCssFile) { if (typeof fallback === "function") { return fallback(info, ssr); } return userOptions.normalizer(info.names[0])[0]; } // First check if we have a static manifest entry for consistent module ID resolution const normalized = userOptions.normalizer(input); let value = normalized[1]; if (value.startsWith(userOptions.moduleBasePath)) { value = value.slice(userOptions.moduleBasePath.length); } // Note: staticManifest is not available during auto-discovery phase // It's loaded later during the build process // Fall back to the user's assetFile function for consistent behavior return fallback(info, ssr); }; 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; const stashedReturns: Record<string, string> = {}; // 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) // For static builds: use empty string to preserve only module names (not paths) to prevent _virtual files const preserveModulesRootString = userOptions.build.preserveModulesRoot === false ? userOptions.moduleBase // Strip src/ from output paths : ""; // Keep src/ in output paths by setting root to empty string // For static builds (browser/ESM): bundle everything - no need to preserve modules or node_modules structure // For client/server environments (SSR): preserve modules to maintain module structure for server-side rendering // Use preserveModules: true for SSR, but for static builds use false to prevent _virtual files const shouldPreserveModules = ssr || condition === "react-server"; const pluginOutput = { // For static builds: false to bundle everything and prevent _virtual files // For SSR builds: use preserveModules with preserveModulesRoot set to preserve only module names (not paths) // This prevents _virtual files by flattening the output structure preserveModules: shouldPreserveModules, // For static builds: undefined (not needed when preserveModules is false) // For SSR builds: set to empty string to preserve only module names, preventing _virtual files preserveModulesRoot: shouldPreserveModules ? (preserveModulesRootString === "" ? "" : preserveModulesRootString) : undefined, entryFileNames: userDefinedEntryFileNames ?? ((info) => { const input = info.facadeModuleId ?? info.name + userOptions.build.moduleExtension; const inputId = input + (ssr ? "-ssr" : ""); if (!stashedReturns[inputId]) { const r = handleSsrEntryName( info, input, userOptions.build.entryFile, ssr ); stashedReturns[inputId] = r ?? info.name; } // in the case of empty basePath, it will not be sliced from the path, so, we need to slice it here // at the last possible moment as to not confuse the rest of the logic around the basePath return stashedReturns[inputId].slice( Number(stashedReturns[inputId].startsWith("/")) ); }), assetFileNames: userDefinedAssetFileNames ?? ((info) => { const input = info.originalFileNames[0]; const inputId = input + (ssr ? "-ssr" : ""); if (!stashedReturns[inputId]) { const r = handleSsrAssetName( info, input, userOptions.build.assetFile, ssr ); stashedReturns[inputId] = r ?? join( userOptions.build.preserveModulesRoot === false && userOptions.build.assetsDir ? userOptions.build.assetsDir : "", userOptions.normalizer(input)[0] ); } // in the case of empty basePath, it will not be sliced from the path, so, we need to slice it here // at the last possible moment as to not confuse the rest of the logic around the basePath return stashedReturns[inputId].slice( Number(stashedReturns[inputId].startsWith("/")) ); }), chunkFileNames: userDefinedChunkFileNames ?? ((info) => { const input = info.facadeModuleId ?? info.name + userOptions.autoDiscover.modulePattern.source; const inputId = input + (ssr ? "-ssr" : ""); if (!stashedReturns[inputId]) { const r = handleSsrEntryName( info, input, userOptions.build.chunkFile, ssr ); stashedReturns[inputId] = r ?? info.name; } // in the case of empty basePath, it will not be sliced from the path, so, we need to slice it here // at the last possible moment as to not confuse the rest of the logic around the basePath return stashedReturns[inputId].slice( Number(stashedReturns[inputId].startsWith("/")) ); }), format: "esm", exports: "named", } satisfies OutputOptions; const newOutput = Array.isArray(config.build?.rollupOptions?.output) ? [...(config.build?.rollupOptions?.output || null), pluginOutput] : typeof config.build?.rollupOptions?.output === "object" && config.build?.rollupOptions?.output !== null ? [config.build?.rollupOptions?.output, pluginOutput] : pluginOutput; const vitePrefix = config.envPrefix ?? DEFAULT_CONFIG.ENV_PREFIX; // Get environment variables (env vars take precedence over config) 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; // Single source of truth for the build/runtime mode. // // Rule (per maintainer): if the user EXPLICITLY set the mode, honor it; // otherwise mirror NODE_ENV. No throwing on a "contradiction" — explicit // intent always wins. // // What counts as "explicit" (empirically verified against Vite 6): // - `config.mode` is undefined unless the user passes `--mode <m>` (Vite // propagates the CLI arg into `config.mode`) OR authors `mode` in their // vite config. It is NOT pre-populated with the command default, so its // mere presence is a reliable "user set this" signal. // - `configEnv.mode`, by contrast, IS pre-populated with the command // default ("production" for `vite build`, "development" for serve), so // it cannot distinguish explicit intent and must not gate this choice. // // When no explicit mode is given we mirror NODE_ENV (normalized by // getNodeEnv). This makes `NODE_ENV=development vite build` produce a dev // build even though Vite's command default mode is "production". const explicitMode = typeof config.mode === "string" && config.mode !== "" ? config.mode : undefined; const mode = explicitMode ?? getNodeEnv(); // Calculate effective values based on command and environment // Prioritize userOptions.projectRoot when explicitly set, regardless of config.root // This ensures Environment API environments use their specific project roots const effectiveProjectRoot = userOptions.projectRoot || config.root || ""; // Calculate moduleRootPath based on command and environment // During serve (development): use moduleBasePath (source paths) // During build (production): use build output paths let effectiveModuleRootPath: string; if (configEnv.command === "serve") { // In development/serve mode, use moduleBasePath for source paths // preserve module root, if true, set as moduleBasePath effectiveModuleRootPath = userOptions.moduleBasePath || "/"; } else { // In production/build mode, use build output paths // The RSC stream contains references like "src/components/Link.client.tsx" // But the compiled files are in dist/client/components/Link.client-CE-JGRqa.js // the manifest maps them from the client folder, so this the correct root for a build effectiveModuleRootPath = join( effectiveProjectRoot, userOptions.build.outDir, userOptions.build.client, !userOptions.moduleBasePath.startsWith("/") ? "/" : "" ); if ( !userOptions.build.preserveModulesRoot && !userOptions.moduleBasePath.startsWith(userOptions.moduleBase) ) { effectiveModuleRootPath = join( effectiveModuleRootPath, userOptions.moduleBase, userOptions.moduleBasePath === "" ? "/" : userOptions.moduleBasePath ); } } const minify = config.build?.minify; // Packages that internally use the per-file `"use client"` convention // (e.g. @chakra-ui/react). The discovery plugin (registered first in // both orchestrators) populates this list with auto-detected packages // before this hook runs; users can also add to it manually via the // `clientPackages` option. // // Three things happen here: // 1. `optimizeDeps.exclude` keeps esbuild's pre-bundle from stripping // the per-file `"use client"` directives before our transform. // 2. `noExternal` (legacy `ssr.noExternal` AND Vite-6 environment- // aware `resolve.noExternal`) makes Rollup inline these packages // into the server bundle, where our transform converts each // `"use client"` module to a `registerClientReference` stub. // 3. Each `"use client"` module emits as a chunk under // `dist/client/node_modules/<pkg>/...` thanks to Vite's natural // preserved-modules handling — those paths match the moduleIDs // we generate in `createTransformerPlugin`, so the html-worker // can resolve client refs at SSG-render time. const clientPackages: readonly string[] = (userOptions as { clientPackages?: readonly string[] }).clientPackages ?? []; const mergedNoExternal = mergeClientPackagesNoExternal( clientPackages, config.ssr?.noExternal ); const srrConfig = { ...config.ssr, target: config.ssr?.target ?? "node", noExternal: mergedNoExternal, optimizeDeps: { ...config.ssr?.optimizeDeps, include: config.ssr?.optimizeDeps?.include ?? [ "react", "react-dom", "react-server-dom-esm/client", ], exclude: mergeClientPackagesOptimizeDepsExclude( clientPackages, config.ssr?.optimizeDeps?.exclude ), }, resolve: { ...config.ssr?.resolve, externalConditions: config.ssr?.resolve?.externalConditions ?? [ "react-server", ], }, }; let publicOrigin = effectivePublicOrigin ?? DEFAULT_CONFIG.PUBLIC_ORIGIN; const port = typeof config.server?.port === "number" ? config.server?.port : 5173; const strictPort = config.server?.strictPort ?? true; const host = typeof config.server?.host === "string" ? config.server?.host : "localhost"; const base = effectiveModuleBaseURL ?? config.base ?? DEFAULT_CONFIG.MODULE_BASE_URL; if (configEnv.command === "serve" && !configEnv.isPreview) { // In dev mode, use empty publicOrigin so the client uses window.location.origin. // This avoids hardcoding a port that may change if the configured port is taken. publicOrigin = ""; } // Use the single authoritative `mode` resolved above (NOT configEnv.mode) // so the React build define and vprs's internal mode can never diverge — // in particular so a config-file `mode` or `NODE_ENV` propagates into the // `process.env.NODE_ENV` / `import.meta.env.MODE` defines that select the // dev-vs-prod React build. const isDev = mode === 'development' || configEnv.command === 'serve'; const ssrDefine = { [`process.env.${primaryPrefix}BASE_URL`]: `"${base}"`, [`process.env.${primaryPrefix}PUBLIC_ORIGIN`]: `"${publicOrigin}"`, [`process.env.NODE_ENV`]: `"${mode}"`, [`process.env.VITE_DEV`]: isDev ? 'true' : 'false', [`process.env.VITE_PROD`]: isDev ? 'false' : 'true', }; const define = { ...config.define, // Standard Vite env vars [`import.meta.env.DEV`]: isDev ? 'true' : 'false', [`import.meta.env.PROD`]: isDev ? 'false' : 'true', [`import.meta.env.MODE`]: `"${mode}"`, [`import.meta.env.SSR`]: 'false', // Will be overridden per-environment // Custom env vars [`import.meta.env.BASE_URL`]: `"${base}"`, [`import.meta.env.PUBLIC_ORIGIN`]: `"${publicOrigin}"`, ...ssrDefine, }; // Set process.env values to ensure they're available in process.env for server-side code if (!getEnvValue("BASE_URL", primaryPrefix)) { setEnvValue("BASE_URL", base, primaryPrefix); } if (!getEnvValue("PUBLIC_ORIGIN", primaryPrefix)) { setEnvValue("PUBLIC_ORIGIN", publicOrigin, primaryPrefix); } if (condition === "react-client") { // client plugin build options (client plugin still outputs server files) const clientConfig = { ...config, root: effectiveProjectRoot, mode: mode, base: base, envPrefix: vitePrefix, resolve: { ...config.resolve, // Per-environment conditions are the load-bearing fix for the // dev-server case where the process was started with a global // `--conditions react-server` (the conventional `dev:rsc` script). // Without this explicit override, Node's module resolver sees // `react-server` for every environment and the client graph // pulls the `react-server` build of `react/jsx-runtime` — which // does not export `jsx` / `jsxs` — and `@chakra-ui/react`-style // packages' transitive CJS deps (`hoist-non-react-statics`, // etc.) lose their interop shims because they get resolved // through the SSR conditions path. Spelling the conditions out // here scopes `react-server` to the server env only. conditions: ssr ? [...defaultServerConditions] : [...defaultClientConditions], // For static builds (browser/ESM): don't externalize anything - bundle everything // For client/server builds (SSR): externalize React modules as usual external: ssr ? (config.resolve?.external ?? [ "react", "react-dom", "react-server-dom-esm/client", ]) : undefined, // Bundle everything for static builds // Vite 6 environments honor `resolve.noExternal` per-env, while the // legacy `ssr.noExternal` doesn't propagate. Mirror clientPackages // here too so the SSR env (outputs dist/client/) bundles them in // alongside user-authored .client.tsx files. noExternal: mergedNoExternal, }, define: define, ssr: srrConfig, server: { ...config.server, // common default for stricter server operations // and ensures tests that use a server will fail early // also, we can't set the public origin without a port port: port, strictPort: strictPort, host: host, }, // client build options build: { ...config.build, modulePreload: config.build?.modulePreload ?? false, emptyOutDir: config.build?.emptyOutDir ?? true, outDir: config.build?.outDir ?? join(userOptions.build.outDir, envDir), assetsDir: config.build?.assetsDir ?? userOptions.build.assetsDir, copyPublicDir: typeof config.build?.copyPublicDir === "boolean" ? config.build?.copyPublicDir : !ssr, // modern browsers target: config.build?.target ?? ["esnext"], minify: minify, rollupOptions: { ...config.build?.rollupOptions, // Use HTML + client entries for non-SSR client builds (static) // and pure client module entries for SSR client builds. input: Object.fromEntries( Object.entries( ssr ? autoDiscoveredFiles.clientInputs : autoDiscoveredFiles.staticInputs ).map(([key, value]) => [ key, value.slice(Number(value.startsWith("/"))), ]) ), output: newOutput, preserveEntrySignatures: config.build?.rollupOptions?.preserveEntrySignatures ?? "exports-only", // For static builds (browser/ESM): bundle everything including node_modules to avoid _virtual files // For client/server builds (SSR): externalize node_modules as usual external: ssr ? (id: string, _parent?: string, _isResolved?: boolean) => { // Don't externalize virtual modules - let Vite inline them during build // Virtual modules are Vite's internal helpers that should be inlined, not externalized if (id.includes("_virtual/") || id.startsWith("_virtual")) { return false; // Let Vite handle virtual modules by inlining them } // Use user's external config or default to fsevents const userExternal = config.build?.rollupOptions?.external ?? ["fsevents"]; if (Array.isArray(userExternal)) { return userExternal.includes(id); } if (typeof userExternal === "function") { return userExternal(id, _parent, _isResolved ?? false); } return false; } : (() => { // For static builds, only externalize fsevents (macOS-specific, not needed in browser) // Don't externalize node_modules - they should be bundled to avoid _virtual files const userExternal = config.build?.rollupOptions?.external ?? ["fsevents"]; if (Array.isArray(userExternal)) { // Only keep fsevents, filter out everything else (including node_modules) return userExternal.filter((ext) => typeof ext === "string" && ext === "fsevents"); } // If user provided a function or RegExp, wrap it to only allow fsevents if (typeof userExternal === "function") { return (id: string) => { if (id === "fsevents") return true; return false; // Don't externalize anything else for static builds }; } // Default: only fsevents return ["fsevents"]; })(), }, ssr: ssr, 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, }, } satisfies ResolvedUserConfig; stashedUserConfig[envId] = clientConfig; return { type: "success", userConfig: clientConfig, }; } else { const serverConfig = { ...config, root: effectiveProjectRoot, mode: mode, base: userOptions.moduleBaseURL, envPrefix: vitePrefix, resolve: { ...config.resolve, // The server env owns the `react-server` condition. Spelling it // out here (instead of relying on a process-wide // `--conditions react-server` flag the caller set in // `NODE_OPTIONS`) lets the client / ssr envs resolve the // default React build of `react/jsx-runtime` in the same Vite // process — which is what fixes the client interop regression // surfaced when a client component imports a client package // (Chakra, emotion, …) under a global `--conditions // react-server` shell. conditions: [ "react-server", ...defaultServerConditions.filter((c) => c !== "react-server"), ], externalConditions: config.resolve?.externalConditions ?? [ "react-server", ], // Force whitelisted client packages into the server bundle so the // RSC transform can convert their `"use client"` modules to client // references. Without this, Vite's default SSR externalization // leaves them as bare `import` statements that Node loads at SSG // time straight from `node_modules`, bypassing every transform. noExternal: mergedNoExternal, }, define: define, ssr: srrConfig, // server build options build: { ...config.build, modulePreload: config.build?.modulePreload ?? false, emptyOutDir: config.build?.emptyOutDir ?? true, outDir: config.build?.outDir ?? join(userOptions.build.outDir, envDir), target: config.build?.target ?? "esnext", // Use esnext for pure ESM - no helpers needed minify: minify, ssr: ssr, manifest: config.build?.manifest ?? `.vite/manifest.json`, ssrManifest: config.build?.ssrManifest ?? false, ssrEmitAssets: typeof config.build?.ssrEmitAssets === "boolean" ? config.build?.ssrEmitAssets : true, copyPublicDir: typeof config.build?.copyPublicDir === "boolean" ? config.build?.copyPublicDir : !ssr, assetsDir: config.build?.assetsDir ?? userOptions.build.assetsDir, // Ensure CSS files are output to static directory cssCodeSplit: typeof config.build?.cssCodeSplit === "boolean" ? config.build?.cssCodeSplit : true, rollupOptions: { ...config.build?.rollupOptions, input: autoDiscoveredFiles.serverInputs, preserveEntrySignatures: config.build?.rollupOptions?.preserveEntrySignatures ?? "strict", // Externalize core React deps for the server bundle external: config.build?.rollupOptions?.external ?? [ "react", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom", "react-server-dom-esm/server", "react-server-dom-esm/server.node", ], // Use the same output configuration as client environment for consistent hashing // This ensures client components have the same module IDs across all environments output: newOutput, // Configure Rollup context for server builds to use react-server conditions context: "module", // Set Node.js conditions for server builds plugins: [ ...(Array.isArray(config.build?.rollupOptions?.plugins) ? config.build.rollupOptions.plugins : config.build?.rollupOptions?.plugins ? [config.build.rollupOptions.plugins] : []), { name: "react-server-conditions", buildStart() { if (typeof this.environment.name !== "string") { this.warn( "Environment name is not a string, skipping react-server-conditions plugin" ); return; } if (this.environment.name === "server") { // Ensure react-server condition is available during server builds if (!process.env.NODE_OPTIONS?.includes("react-server")) { if(this.environment.config.define) { this.environment.config.define = { ...this.environment.config.define, "process.env.NODE_OPTIONS": `"${process.env.NODE_OPTIONS} --conditions react-server"` } } else { this.environment.config.define = { "process.env.NODE_OPTIONS": `"${process.env.NODE_OPTIONS} --conditions react-server"` } } // process.env.NODE_OPTIONS = // (process.env.NODE_OPTIONS || "") + // " --conditions react-server"; } } else { if (process.env.NODE_OPTIONS?.includes("react-server")) { if(this.environment.config.define && this.environment.config.define["process.env.NODE_OPTIONS"] && this.environment.config.define["process.env.NODE_OPTIONS"].includes("react-server")) { this.environment.config.define = { ...this.environment.config.define, "process.env.NODE_OPTIONS": this.environment.config.define["process.env.NODE_OPTIONS"].replace(/--conditions[=\s]react-server/, "") } } else { this.environment.config.define = { "process.env.NODE_OPTIONS": "" } } //process.env.NODE_OPTIONS = //process.env.NODE_OPTIONS.replace( //" --conditions react-server", //"" //); } } }, }, // Add content hash plugin for consistent hashing across environments { name: "vite:plugin-react-server/content-hash", augmentChunkHash(chunk) { // Add a consistent salt based on the chunk's source file to make hash generation deterministic if (chunk.facadeModuleId) { try { const sourcePath = resolve( userOptions.projectRoot, chunk.facadeModuleId ); if (existsSync(sourcePath)) { const sourceContent = readFileSync(sourcePath, "utf-8"); // Use a short hash of the source content as salt return createRollupLikeHash(sourceContent, "hex"); } } catch (error) { // Fallback: use the module ID as salt return chunk.facadeModuleId; } } return; }, }, ], }, }, } satisfies ResolvedUserConfig; stashedUserConfig[envId] = serverConfig; return { type: "success", userConfig: serverConfig, }; } };