UNPKG

rwsdk

Version:

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

117 lines (116 loc) 5.72 kB
import MagicString from "magic-string"; import debug from "debug"; import { hasDirective } from "./hasDirective.mjs"; import { findExports } from "./findSpecifiers.mjs"; const logVite = debug("rwsdk:vite:transform-client-components:vite"); const logEsbuild = debug("rwsdk:vite:transform-client-components:esbuild"); export async function transformClientComponents(code, normalizedId, ctx) { const log = ctx.isEsbuild ? logEsbuild : logVite; log("Called transformClientComponents for id: id=%s", normalizedId); if (!hasDirective(code, "use client")) { log("Skipping: no 'use client' directive in id=%s", normalizedId); process.env.VERBOSE && log(":VERBOSE: Returning code unchanged for id=%s:\n%s", normalizedId, code); return; } log("Processing 'use client' module: id=%s", normalizedId); ctx.addClientModule?.(ctx.environmentName, normalizedId); // Parse exports using the findExports helper const exportInfos = findExports(normalizedId, code, log); const processedExports = []; let defaultExportInfo; // Helper to get the computed local name (with alias suffix if present) function getComputedLocalName(info) { return `${info.local}${info.alias ? `_${info.alias}` : ""}`; } // Convert ExportInfo to ProcessedExportInfo exportInfos.forEach((exportInfo, idx) => { if (exportInfo.isDefault) { defaultExportInfo = { local: exportInfo.alias || "default", exported: "default", isDefault: true, statementIdx: idx, }; } else { // For aliases like "export { MyComponent as CustomName }", we need: // - local: "MyComponent" (the original name) // - exported: "CustomName" (the alias name) // - alias: "CustomName" (to generate MyComponent_CustomName) const hasAlias = exportInfo.alias && exportInfo.originalName; processedExports.push({ local: exportInfo.originalName || exportInfo.name, // Use originalName if available exported: exportInfo.name, // The exported name (alias if present) isDefault: false, statementIdx: idx, alias: hasAlias ? exportInfo.alias : undefined, }); } }); // 3. Client/SSR files: just remove the directive if (ctx.environmentName === "ssr" || ctx.environmentName === "client") { log(":isEsbuild=%s: Handling SSR virtual module: %s", !!ctx.isEsbuild, normalizedId); // Remove 'use client' directive using magic-string const s = new MagicString(code); // Find and remove "use client" directives const directiveRegex = /^(\s*)(['"]use client['"])\s*;?\s*\n?/gm; let match; while ((match = directiveRegex.exec(code)) !== null) { const start = match.index; const end = match.index + match[0].length; s.remove(start, end); process.env.VERBOSE && log("Removed 'use client' directive from normalizedId=%s", normalizedId); break; // Only remove the first one } const sourceMap = s.generateMap({ source: normalizedId, includeContent: true, hires: true, }); process.env.VERBOSE && log(":VERBOSE: SSR transformed code for %s:\n%s", normalizedId, s.toString()); return { code: s.toString(), map: sourceMap, }; } // 4. Non-SSR files: replace all implementation with registerClientReference logic // Generate completely new code for worker/client environments const s = new MagicString(""); // Add import declaration s.append('import { registerClientReference } from "rwsdk/worker";\n'); // Compute unique computed local names first const computedLocalNames = new Map(processedExports.map((info) => [getComputedLocalName(info), info])); // Add registerClientReference assignments for unique names for (const [computedLocalName, correspondingInfo] of computedLocalNames) { log(":isEsbuild=%s: Registering client reference for named export: %s as %s", !!ctx.isEsbuild, correspondingInfo.local, correspondingInfo.exported); s.append(`const ${computedLocalName} = registerClientReference("${normalizedId}", "${correspondingInfo.exported}");\n`); } // Add grouped export statement for named exports (preserving order and alias) if (processedExports.length > 0) { const exportNames = Array.from(computedLocalNames.entries()).map(([computedLocalName, correspondingInfo]) => correspondingInfo.local === correspondingInfo.exported ? computedLocalName : `${computedLocalName} as ${correspondingInfo.exported}`); log(":isEsbuild=%s: Exporting named exports: %O", !!ctx.isEsbuild, exportNames); s.append(`export { ${exportNames.join(", ")} };\n`); } // Add default export if present if (defaultExportInfo) { log(":isEsbuild=%s: Registering client reference for default export: %s", !!ctx.isEsbuild, defaultExportInfo.exported); s.append(`export default registerClientReference("${normalizedId}", "${defaultExportInfo.exported}");\n`); } const sourceMap = s.generateMap({ source: normalizedId, includeContent: true, hires: true, }); const finalResult = s.toString(); process.env.VERBOSE && log(":VERBOSE: Transformed code (env=%s, normalizedId=%s):\n%s", normalizedId, ctx.environmentName, finalResult); return { code: finalResult, map: sourceMap, }; }