rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
240 lines (239 loc) • 11 kB
JavaScript
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 = [];
}
},
});