UNPKG

rwsdk

Version:

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

192 lines (191 loc) 9.7 kB
import debug from "debug"; import fs from "node:fs/promises"; import path from "node:path"; import { normalizeModulePath } from "../lib/normalizeModulePath.mjs"; import { transformClientComponents } from "./transformClientComponents.mjs"; import { transformServerFunctions } from "./transformServerFunctions.mjs"; const log = debug("rwsdk:vite:rsc-directives-plugin"); export const getLoader = (filePath) => { const ext = path.extname(filePath).slice(1); switch (ext) { case "mjs": case "cjs": return "js"; case "mts": case "cts": return "ts"; case "jsx": return "jsx"; case "tsx": return "tsx"; case "ts": return "ts"; case "js": default: return "js"; } }; export const directivesPlugin = ({ projectRootDir, clientFiles, serverFiles, }) => { let devServer; let isAfterFirstResponse = false; let isBuild = false; return { name: "rwsdk:rsc-directives", configResolved(config) { isBuild = config.command === "build"; }, configureServer(server) { devServer = server; devServer.middlewares.use((_req, res, next) => { // context(justinvdm, 15 Jun 2025): We want to watch for new client and server modules // and invalidate their respective module lookups when this happens, but // we only want to do this after the first render if (!isAfterFirstResponse) { res.on("finish", () => { if (!isAfterFirstResponse) { isAfterFirstResponse = true; log("Dev server first response completed"); } }); } next(); }); }, async transform(code, id) { // Skip during directive scanning to avoid performance issues if (process.env.RWSDK_DIRECTIVE_SCAN_ACTIVE) { return; } if (isBuild && this.environment?.name === "worker" && process.env.RWSDK_BUILD_PASS !== "worker") { return; } const normalizedId = normalizeModulePath(id, projectRootDir); const clientResult = await transformClientComponents(code, normalizedId, { environmentName: this.environment.name, clientFiles, }); if (clientResult) { process.env.VERBOSE && log("Client component transformation successful for id=%s", id); return { code: clientResult.code, map: clientResult.map, }; } const serverResult = transformServerFunctions(code, normalizedId, this.environment.name, serverFiles); if (serverResult) { process.env.VERBOSE && log("Server function transformation successful for id=%s", id); return { code: serverResult.code, map: serverResult.map, }; } // Removed: too noisy even in verbose mode }, configEnvironment(env, config) { if (isBuild && env === "worker" && process.env.RWSDK_BUILD_PASS !== "worker") { return; } process.env.VERBOSE && log("Configuring environment: env=%s", env); config.optimizeDeps ??= {}; config.optimizeDeps.esbuildOptions ??= {}; config.optimizeDeps.esbuildOptions.plugins ??= []; config.optimizeDeps.esbuildOptions.plugins.push({ name: "rsc-directives-esbuild-transform", setup(build) { log("Setting up esbuild plugin for environment: %s", env); build.onLoad({ filter: /\.(js|ts|jsx|tsx|mts|mjs|cjs)$/ }, async (args) => { process.env.VERBOSE && log("Esbuild onLoad called for environment=%s, path=%s", env, args.path); const normalizedPath = normalizeModulePath(args.path, projectRootDir); // context(justinvdm,2025-06-15): If we're in app code, // we will be doing the transform work in the vite plugin hooks, // the only reason we're in esbuild land for app code is for // dependency discovery, so we can skip transform work // and use heuristics instead - see below inside if block if (!args.path.includes("node_modules")) { if (clientFiles.has(normalizedPath)) { // context(justinvdm,2025-06-15): If this is a client file: // * for ssr and client envs we can skip so esbuild looks at the // original source code to discovery dependencies // * for worker env, the transform would have just created // references and dropped all imports, so we can just return empty code if (env === "client" || env === "ssr") { log("Esbuild onLoad skipping client module in app code for client or ssr env, path=%s", args.path); return undefined; } else { log("Esbuild onLoad returning empty code for server module in app code for worker env, path=%s to bypass esbuild dependency discovery", args.path); return { contents: "", loader: "js", }; } } else if (serverFiles.has(normalizedPath)) { // context(justinvdm,2025-06-15): If this is a server file: // * for worker env, we can skip so esbuild looks at the // original source code to discovery dependencies // * for ssr and client envs, the transform would have just created // references and dropped all imports, so we can just return empty code if (env === "worker") { log("Esbuild onLoad skipping server module in app code for worker env, path=%s", args.path); return undefined; } else if (env === "ssr" || env === "client") { log("Esbuild onLoad returning empty code for server module in app code for ssr or client env, path=%s", args.path); return { contents: "", loader: "js", }; } } } let code; try { code = await fs.readFile(args.path, "utf-8"); } catch { process.env.VERBOSE && log("Failed to read file: %s, environment=%s", args.path, env); return undefined; } const clientResult = await transformClientComponents(code, normalizedPath, { environmentName: env, clientFiles, isEsbuild: true, }); if (clientResult) { process.env.VERBOSE && log("Esbuild client component transformation successful for environment=%s, path=%s", env, args.path); process.env.VERBOSE && log("Esbuild client component transformation for environment=%s, path=%s, code: %j", env, args.path, clientResult.code); return { contents: clientResult.code, loader: getLoader(args.path), }; } const serverResult = transformServerFunctions(code, normalizedPath, env, serverFiles); if (serverResult) { process.env.VERBOSE && log("Esbuild server function transformation successful for environment=%s, path=%s", env, args.path); return { contents: serverResult.code, loader: getLoader(args.path), }; } process.env.VERBOSE && log("Esbuild no transformation applied for environment=%s, path=%s", env, args.path); }); }, }); process.env.VERBOSE && log("Environment configuration complete for env=%s", env); }, }; };