UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

240 lines (239 loc) 11 kB
import enhancedResolve from "enhanced-resolve"; import path, { resolve } from "node:path"; import { INTERMEDIATE_SSR_BRIDGE_PATH } from "../lib/constants.mjs"; import { buildApp } from "./buildApp.mjs"; import { externalModules } from "./constants.mjs"; import { ssrBridgeWrapPlugin } from "./ssrBridgeWrapPlugin.mjs"; export const SSR_BRIDGE_ROLLDOWN_EXPERIMENTAL = { // Rolldown's lazy barrel optimization can keep code that references imports it // already pruned when `codeSplitting` is false. The SSR bridge intentionally // disables code splitting, so keep lazy barrel off for this build until the // upstream issue is fixed. // See: // - https://github.com/rolldown/rolldown/issues/9691#issuecomment-4666238497 // - https://github.com/rolldown/rolldown/issues/9964 lazyBarrel: false, }; export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clientFiles, serverFiles, clientEntryPoints, esbuildOptions, }) => ({ name: "rwsdk:config", enforce: "pre", config: async (config, { command }) => { const mode = process.env.NODE_ENV; // context(justinvdm, 2026-05-06): Only set a sourcemap default if the user // hasn't already configured it in their vite config. This lets users opt in // or out explicitly while still providing a sensible mode-aware default. const sourcemap = config.build?.sourcemap ?? mode === "development"; const workerConfig = { resolve: { conditions: [ "workerd", "react-server", "module", // context(justinvdm, 11 Jun 2025): Some packages meant for cloudflare workers, yet // their deps have only node import conditions, e.g. `agents` package (meant for CF), // has `pkce-challenge` package as a dep, which has only node import conditions. // https://github.com/crouchcd/pkce-challenge/blob/master/package.json#L17 // // @cloudflare/vite-plugin should take care of any relevant polyfills for deps with // node builtins imports that can be polyfilled though, so it is worth us including this condition here. // However, it does mean we will try to run packages meant for node that cannot be run on cloudflare workers. // That's the trade-off, but arguably worth it. "node", ], noExternal: true, }, define: { "import.meta.env.RWSDK_ENV": JSON.stringify("worker"), }, optimizeDeps: { noDiscovery: false, include: [ "rwsdk/__ssr_bridge", "rwsdk/auth", "rwsdk/constants", "rwsdk/db", "rwsdk/debug", "rwsdk/realtime/durableObject", "rwsdk/realtime/worker", "rwsdk/router", "rwsdk/worker", "rwsdk/use-synced-state/worker", "rwsdk/use-synced-state/hibernation/worker", ], exclude: [], entries: [workerEntryPathname], rolldownOptions: { transform: { jsx: "react-jsx", define: { "process.env.NODE_ENV": JSON.stringify(mode), __webpack_require__: "globalThis.__webpack_require__", }, }, }, }, build: { outDir: resolve(projectRootDir, "dist", "worker"), emitAssets: true, ssrManifest: true, emptyOutDir: false, ssr: true, }, }; const baseConfig = { appType: "custom", mode, logLevel: silent ? "silent" : "info", resolve: { tsconfigPaths: true, }, build: { minify: mode !== "development", sourcemap, }, define: { "process.env.NODE_ENV": JSON.stringify(mode), }, ssr: { target: "webworker", }, environments: { client: { consumer: "client", build: { outDir: resolve(projectRootDir, "dist", "client"), manifest: true, rolldownOptions: { input: [], }, }, define: { "import.meta.env.RWSDK_ENV": JSON.stringify("client"), }, optimizeDeps: { noDiscovery: false, include: [ "rwsdk/client", "rwsdk/constants", "rwsdk/debug", "rwsdk/realtime/client", "rwsdk/router", "rwsdk/turnstile", "rwsdk/use-synced-state/client", "rwsdk/use-synced-state/hibernation/client", ], entries: [], rolldownOptions: { transform: { jsx: "react-jsx", define: { "process.env.NODE_ENV": JSON.stringify(mode), __webpack_require__: "globalThis.__webpack_require__", }, }, }, }, resolve: { conditions: ["browser", "module"], }, }, ssr: { resolve: { conditions: ["workerd", "module", "browser"], noExternal: true, }, define: { "import.meta.env.RWSDK_ENV": JSON.stringify("ssr"), }, optimizeDeps: { noDiscovery: false, entries: [workerEntryPathname], exclude: externalModules, include: [ "rwsdk/__ssr", "rwsdk/__ssr_bridge", "rwsdk/client", "rwsdk/constants", "rwsdk/debug", "rwsdk/realtime/client", "rwsdk/router", "rwsdk/worker", "rwsdk/realtime/durableObject", "rwsdk/realtime/worker", "rwsdk/use-synced-state/client", "rwsdk/use-synced-state/hibernation/client", ], rolldownOptions: { transform: { jsx: "react-jsx", define: { "process.env.NODE_ENV": JSON.stringify(mode), __webpack_require__: "globalThis.__webpack_require__", }, }, }, }, build: { lib: { entry: { [path.basename(INTERMEDIATE_SSR_BRIDGE_PATH, ".js")]: enhancedResolve.sync(projectRootDir, "rwsdk/__ssr_bridge"), }, formats: ["es"], fileName: () => path.basename(INTERMEDIATE_SSR_BRIDGE_PATH), }, outDir: path.dirname(INTERMEDIATE_SSR_BRIDGE_PATH), rolldownOptions: { experimental: SSR_BRIDGE_ROLLDOWN_EXPERIMENTAL, output: { // context(justinvdm, 15 Sep 2025): The SSR bundle is a // pre-compiled artifact. When the linker pass bundles it into // the intermediate worker bundle (another pre-compiled // artifact), Rollup merges their top-level scopes. Since both // may have identical minified identifiers (e.g., `l0`), this // causes a redeclaration error. To solve this, we wrap the SSR // bundle in an exporting IIFE. This creates a scope boundary, // preventing symbol collisions while producing a valid, // tree-shakeable ES module. // // context(justinvdm, 19 Nov 2025): We use a custom plugin // (ssrBridgeWrapPlugin) to intelligently inject the IIFE *after* // any top-level external imports, ensuring they remain valid. codeSplitting: false, }, plugins: [ssrBridgeWrapPlugin()], }, }, }, worker: workerConfig, }, server: { hmr: true, }, builder: { async buildApp(builder) { await buildApp({ builder, projectRootDir, clientEntryPoints, clientFiles, serverFiles, workerEntryPathname, esbuildOptions, }); }, }, }; return baseConfig; }, configResolved(config) { // context(chrisvdm, 2025-09-20): Vitest and some Vite defaults set // `resolve.external` to Node built-ins for non-client environments. The // Cloudflare Vite plugin rejects any `resolve.external` value on Worker // environments. We clear it here after Vite has resolved the environment // configs but before the Cloudflare plugin validates them. `noExternal: true` // in the worker config ensures builtins are bundled rather than externalized. if (config.environments.worker?.resolve) { config.environments.worker.resolve.external = []; } }, });