UNPKG

rwsdk

Version:

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

465 lines (464 loc) 18.6 kB
/** * context(justinvdm, 2025-05-14): * * ## Problem * React Server Components (RSC) and traditional SSR require different module * resolution: * - RSC modules must resolve with the "react-server" export condition * - SSR modules must resolve without it * * This presents a challenge for our framework, where the same modules often * need to run in both modes — within a single Cloudflare Worker runtime. We * can't split execution contexts or afford duplicated builds. * * Vite provides an elegant way to manage distinct resolution graphs via its * `environments` feature (`client`, `ssr`, `worker`, etc.). Each environment * can use different export conditions, plugins, and optimizeDeps configs. * * However, using separate environments implies separate output bundles. In our * case, that would nearly double the final bundle size — which is not viable * given Cloudflare Workers' strict 3MB limit. * * ## Solution * We run both RSC and SSR from a single Vite `worker` environment. To simulate * distinct resolution graphs, we virtualize SSR imports using a prefix. * * How it works: * - Maintain an SSR subgraph as part of the worker environment's module graph. * Any time we see "use client", we enter the subgraph. * - We keep the graphs separate by rewriting imports in SSR graph to map to virtual files. * - Bare imports to deps get resolved using a custom resolver so that we use * import conditions relevant to SSR - note the lack of "react-server" * condition: ["workerd", "edge", "import", "default"] * - All imports within the subgraph get their path rewritten with the SSR * module namespace prefix so that we stay within the subgraph. */ import path from "path"; import fs from "fs/promises"; import { createModuleResolver } from "./moduleResolver.mjs"; import MagicString from "magic-string"; import debug from "debug"; import { ROOT_DIR, DIST_DIR } from "../lib/constants.mjs"; import { transformClientComponents } from "./transformClientComponents.mjs"; import { transformServerReferences } from "./transformServerReferences.mjs"; import { findImportSpecifiers } from "./findImportSpecifiers.mjs"; import { ensureConfigArrays } from "./ensureConfigArrays.mjs"; import { isBareImport } from "./isBareImport.mjs"; export const SSR_NAMESPACE = "virtual:rwsdk:ssr"; export const SSR_NAMESPACE_PREFIX = SSR_NAMESPACE + ":"; export const SSR_URL_PREFIX = "/" + SSR_NAMESPACE_PREFIX; export const SSR_ESBUILD_NAMESPACE = "__rwsdk_ssr_esbuild_namespace__"; export const SSR_RESOLVER_CONDITION_NAMES = [ "workerd", "edge", "import", "default", ]; const log = debug("rwsdk:vite:virtualized-ssr"); const logInfo = log.extend("info"); const logError = log.extend("error"); const logResolve = log.extend("resolve"); const logTransform = log.extend("transform"); const logEsbuild = debug("rwsdk:vite:virtualized-ssr:esbuild"); const logEsbuildTransform = logEsbuild.extend("transform"); const IGNORED_IMPORT_PATTERNS = [ /^cloudflare:.*/, /^react\/jsx-runtime$/, /^react\/jsx-dev-runtime$/, ]; export const createSSRDepResolver = ({ projectRootDir, }) => createModuleResolver({ roots: [projectRootDir, ROOT_DIR], name: "ssrDep", conditionNames: SSR_RESOLVER_CONDITION_NAMES, }); async function resolveSSRPath({ path, importer = ".", context, logFn, }) { logFn?.(":resolveSSRPath: called with path=%s, importer=%s", path, importer); if (!path.startsWith(SSR_NAMESPACE)) { logFn?.(":resolveSSRPath: Skipping non-SSR path: path=%s", path); return; } const raw = ensureNoSSRNamespace(path); if (isBareImport(raw)) { const ssrResolved = context.resolveDep(raw); if (ssrResolved !== false) { const resolved = ensureSSRNamespace(ssrResolved); logFn?.(":resolveSSRPath: SSR resolver succeeded for bare import import='%s', resolved to resolved='%s'", path, resolved); return resolved; } } const moduleResolved = await context.resolveModule(raw, ensureNoSSRNamespace(importer)); if (moduleResolved) { if (moduleResolved.startsWith(DIST_DIR)) { if (moduleResolved.includes("__rwsdkssr")) { const resolved = ensureSSRNamespace(moduleResolved); logFn?.(":resolveSSRPath: Module resolved to an SDK path that contains __rwsdkssr, returning resolved path *with* SSR namespace: moduleResolved=%s", moduleResolved); return resolved; } else { logFn?.(":resolveSSRPath: Module resolved to an SDK path, returning resolved path *without* SSR namespace: moduleResolved=%s", moduleResolved); return moduleResolved; } } const resolved = ensureSSRNamespace(moduleResolved); logFn?.(":resolveSSRPath: Module resolver succeeded for import import='%s' from importer=%s, resolved to moduleResolved='%s'", raw, importer, resolved); return resolved; } else { logFn?.(":resolveSSRPath: Module resolver failed for import import='%s' from importer=%s, returning raw path without SSR namespace", raw, importer); return raw; } } async function rewriteSSRImports({ code, id, logFn, }) { const isSDKPath = ensureNoSSRNamespace(id).startsWith(DIST_DIR); logFn?.(":rewriteSSRImports: called for id: id=%s, isSDKPath=%s", id, isSDKPath); const imports = findImportSpecifiers(id, code, IGNORED_IMPORT_PATTERNS, logFn); logFn?.(":rewriteSSRImports: Found %d imports in id=%s", imports.length, id); const ms = new MagicString(code); let modified = false; for (const i of imports) { const raw = i.raw; logFn?.(":rewriteSSRImports: Processing import '%s' at [%d, %d] in id=%s", raw, i.s, i.e, id); const realPath = ensureNoSSRNamespace(raw); if (isSDKPath && !isBareImport(realPath) && !path.isAbsolute(realPath) && !realPath.includes("__rwsdkssr")) { logFn?.(":rewriteSSRImports: Skipping import because it is a relative import within the SDK: import='%s', in id=%s", raw, id); continue; } const virtualId = ensureSSRNamespace(raw); ms.overwrite(i.s, i.e, virtualId); logFn?.(":rewriteSSRImports: Rewrote import import='%s' to virtualId='%s' in id=%s", raw, virtualId, id); modified = true; } if (modified) { logFn?.(":rewriteSSRImports: Rewriting complete for id=%s", id); return ms; } else { logFn?.(":rewriteSSRImports: No changes made for id=%s", id); return null; } } function isSSRModule({ id, code, logFn, esbuild, }) { const logger = logFn ?? (() => { }); if (id.includes("__rwsdkssr")) { logger(":isSSRModule: Detected SSR module (includes __rwsdk_ssr): id=%s esbuild=%s", id, !!esbuild); return true; } if (id.startsWith(SSR_NAMESPACE)) { logger(":isSSRModule: Detected SSR module (SSR_NAMESPACE): id=%s esbuild=%s", id, !!esbuild); return true; } return false; } export const isSSRPath = (filePath) => { return (filePath.startsWith(SSR_NAMESPACE_PREFIX) || filePath.startsWith(SSR_URL_PREFIX)); }; export const ensureNoSSRNamespace = (filePath) => { if (filePath.startsWith(SSR_NAMESPACE_PREFIX)) { return filePath.slice(SSR_NAMESPACE_PREFIX.length); } if (filePath.startsWith(SSR_URL_PREFIX)) { return filePath.slice(SSR_URL_PREFIX.length); } return filePath; }; export const ensureSSRNamespace = (filePath) => { return SSR_NAMESPACE_PREFIX + ensureNoSSRNamespace(filePath); }; function detectLoader(filePath) { const ext = path.extname(filePath).toLowerCase(); return ext === ".tsx" || ext === ".jsx" ? "tsx" : ext === ".ts" ? "ts" : "js"; } async function esbuildResolveSSRModule({ path, context, importer, }) { logEsbuild(":esbuildResolveSSRModule: called with path=%s", path); const resolved = await resolveSSRPath({ path, importer, context, logFn: logEsbuild, }); if (!resolved) { logEsbuild(":esbuildResolveSSRModule: path=%s skipped", path); return undefined; } const result = { path: resolved, }; if (resolved.startsWith(SSR_NAMESPACE)) { result.namespace = SSR_ESBUILD_NAMESPACE; } else if (isBareImport(resolved)) { result.external = true; } else { result.namespace = "file"; } logEsbuild(":esbuildResolveSSRModule: resolved result for path=%s: result=%O", path, result); return result; } async function esbuildLoadAndTransformSSRModule({ filePath, context, logFn, }) { const realPath = ensureNoSSRNamespace(filePath); if (isBareImport(realPath)) { logFn("⏭️ Skipping bare import: %s", realPath); return undefined; } let inputCode; try { inputCode = await fs.readFile(realPath, "utf-8"); } catch (err) { logFn("❌ Failed to read file: %s", realPath); return undefined; } const isSSR = isSSRModule({ id: filePath, code: inputCode, logFn, esbuild: true, }); let code = inputCode; let modified = false; const clientResult = await transformClientComponents(code, filePath, { environmentName: "worker", isEsbuild: true, }); if (clientResult) { logFn("🔎 Client component transform complete for %s", filePath); code = clientResult.code; modified = true; } else { logFn("⏭️ No client component transform needed for %s", filePath); } const serverResult = await transformServerReferences(code, filePath, { environmentName: "worker", isEsbuild: true, isSSR, topLevelRoot: context.projectRootDir, }); if (serverResult) { logFn("🔎 Server reference transform complete for %s", filePath); code = serverResult.code; modified = true; } else { logFn("⏭️ No server reference transform needed for %s", filePath); } if (isSSR) { let rewritten = await rewriteSSRImports({ code: inputCode, id: filePath, context, logFn, }); if (rewritten) { logFn("🔎 Import rewriting complete for %s", filePath); code = rewritten.toString(); modified = true; } else { logFn("⏭️ No import rewriting needed for %s", filePath); } } if (!modified) { if (isSSR) { logFn("⏭️ Returning code unmodified for SSR module %s", filePath); } else { logFn("⏭️ Returning code unmodified for non-SSR module %s", filePath); return undefined; } } else { logFn("🔎 Returning modified code for %s", filePath); if (process.env.VERBOSE) { logFn(":VERBOSE: Code for modified %s:\n%s", filePath, code); } } return { contents: code, loader: detectLoader(filePath), resolveDir: path.dirname(realPath), }; } function virtualizedSSREsbuildPlugin(context) { return { name: "virtualized-ssr-esbuild-plugin", setup(build) { build.onResolve({ filter: /.*/, namespace: SSR_ESBUILD_NAMESPACE }, async (args) => { logEsbuild(":esbuild:onResolve:namespace called with args: %O", args); const result = await esbuildResolveSSRModule({ context, path: args.path, importer: args.importer, }); logEsbuild(":esbuild:onResolve:namespace resolved result for path=%s: result=%O", args.path, result); return result; }); build.onLoad({ filter: /.*/, namespace: SSR_ESBUILD_NAMESPACE }, async (args) => { logEsbuild(":esbuild:onLoad:namespace called with args: %O", args); return esbuildLoadAndTransformSSRModule({ filePath: args.path, context, logFn: logEsbuildTransform, }); }); build.onResolve({ filter: /^virtual:rwsdk:ssr:/ }, async (args) => { logEsbuild(":esbuild:onResolve:prefix called with args: %O", args); const result = await esbuildResolveSSRModule({ context, path: args.path, importer: args.importer, }); logEsbuild(":esbuild:onResolve:prefix resolved result for path=%s: result=%O", args.path, result); return result; }); build.onLoad({ filter: /\.(js|jsx|ts|tsx|mjs|mts)$/ }, async (args) => { logEsbuild(":esbuild:onLoad:entry called with args: %O", args); const result = await esbuildLoadAndTransformSSRModule({ filePath: args.path, context, logFn: logEsbuildTransform, }); if (process.env.VERBOSE) { logEsbuild(":esbuild:onLoad:entry result: %O", result); } return result; }); }, }; } export function virtualizedSSRPlugin({ projectRootDir, }) { logInfo("🚀 Initializing VirtualizedSSR plugin with root: %s", projectRootDir); logInfo("📂 Plugin will handle client/server module resolution in a single Vite worker environment"); const context = { projectRootDir, config: undefined, resolveModule: () => false, resolveDep: () => false, }; return { name: "rwsdk:virtualized-ssr", async configEnvironment(env, config) { logInfo(":configEnvironment: Configuring environment: %s", env); if (env !== "worker") { logInfo(":configEnvironment: Skipping non-worker environment"); return; } ensureConfigArrays(config); context.resolveModule = createModuleResolver({ getAliases: () => getAliases(config.resolve ?? {}), roots: [projectRootDir], name: "resolveModule", conditionNames: SSR_RESOLVER_CONDITION_NAMES, }); context.resolveDep = createSSRDepResolver({ projectRootDir, }); logInfo(":configEnvironment: Setting up aliases for worker environment"); logInfo(":configEnvironment: Configuration state:"); logInfo(":configEnvironment: - Project root: %s", projectRootDir); logInfo(":configEnvironment: - Virtual SSR namespace: %s", SSR_NAMESPACE); config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins.filter((p) => p?.name !== "virtualized-ssr-esbuild-plugin"); config.optimizeDeps.esbuildOptions.plugins.unshift(virtualizedSSREsbuildPlugin(context)); logInfo("✅ Updated Vite config to use only esbuild plugin for SSR virtual deps"); context.config = config; }, async resolveId(id, importer) { if (this.environment.name !== "worker") { return; } logResolve(":plugin:resolveId: called with id: %s", id); if (!isSSRPath(id)) { logResolve(":plugin:resolveId: Skipping non-SSR namespace: %s", id); return id; } const realPath = ensureNoSSRNamespace(id); const result = await resolveSSRPath({ path: id, importer, context, logFn: logResolve, }); if (!result) { logResolve(":plugin:resolveId: path=%s skipped", realPath); return; } logResolve(":plugin:resolveId: resolved result for id=%s: result=%O", id, result); return result; }, load(id) { if (this.environment.name !== "worker") { return; } logResolve(":plugin:load: called with id: %s", id); if (!isSSRPath(id)) { logResolve("plugin:load: Skipping non-SSR namespaced id: %s", id); return; } const moduleId = ensureNoSSRNamespace(id); try { logResolve("📄 load() reading file as separate SSR module: %s", moduleId); return fs.readFile(moduleId, "utf-8"); } catch { logResolve("❌ load() read failed for: %s", moduleId); return null; } }, async transform(code, id) { logTransform(":plugin:transform: called with id: %s", id); if (this.environment.name !== "worker") { return null; } logTransform("📝 Transform: %s", id); const isSSR = isSSRModule({ id, code, logFn: logTransform, esbuild: false, }); if (!isSSR) { logTransform("⏭️ Skipping non-SSR module: %s", id); return null; } logTransform("🔎 Processing imports in SSR module: %s", id); const rewritten = await rewriteSSRImports({ code, id, context, logFn: logTransform, }); if (!rewritten) { logTransform("⏭️ No changes made for %s", id); return null; } else { logTransform("🔎 Rewrote imports for %s", id); if (process.env.VERBOSE) { logTransform(":VERBOSE: Rewritten code for %s:\n%s", id, rewritten.toString()); } } return { code: rewritten.toString(), map: rewritten.generateMap(), }; }, }; } function getAliases(resolveConfig) { if (!resolveConfig?.alias) return []; const alias = resolveConfig.alias; if (Array.isArray(alias)) { return alias; } // Convert object form to array, ensuring replacement is a string return Object.entries(alias).map(([find, replacement]) => ({ find, replacement: String(replacement), })); }