UNPKG

vite-plugin-react-server

Version:
131 lines (116 loc) 4.13 kB
import { PassThrough } from "node:stream"; import { parentPort } from "node:worker_threads"; import type { HtmlRenderState, HtmlWorkerMessage } from "../types.js"; import * as ReactDOMServer from "react-dom/server"; import React from "react"; import { createFromNodeStream, // @ts-ignore } from "react-server-dom-esm/client.node"; // Track active renders and streams const activeRenders = new Map<string, HtmlRenderState>(); const htmlContent = new Map<string, string>(); const htmlPromises = new Map<string, Promise<string>>(); export const messageHandler = async (message: HtmlWorkerMessage) => { try { switch (message.type) { case "RSC_CHUNK": { const { id, chunk, moduleRootPath, moduleBaseURL, htmlOutputPath, pipableStreamOptions } = message; const render = activeRenders.get(id); if (!render) { activeRenders.set(id, { chunks: [chunk], id, complete: false, rendered: false, moduleRootPath, moduleBaseURL, outDir: '', htmlOutputPath: htmlOutputPath, pipableStreamOptions: pipableStreamOptions, }); } else { render.chunks = [...render.chunks, chunk]; } break; } case "RSC_END": { const { id } = message; const render = activeRenders.get(id); if (!render) { throw new Error(`No render state found for ${id}`); } // Mark this render as complete render.complete = true; // Create a PassThrough stream to handle the chunks const rscStream = new PassThrough(); // Write all chunks to the stream for (const chunk of render.chunks) { rscStream.write(chunk); } rscStream.end(); // Create React elements from stream const reactElements = await createFromNodeStream( rscStream, render.moduleRootPath, render.moduleBaseURL ); // Create a promise that resolves when HTML is complete const htmlPromise = new Promise<string>((resolve) => { const collectStream = new PassThrough(); let html = ''; collectStream.on("data", (chunk) => { html += chunk.toString(); }); collectStream.on("end", () => { resolve(html); render.rendered = true; parentPort?.postMessage({ type: "ALL_READY", id, html, outputPath: render.htmlOutputPath, }); }); // Render to pipeable stream const stream = ReactDOMServer.renderToPipeableStream( reactElements as React.ReactNode, { ...render.pipableStreamOptions, // Calculate relative paths based on route depth bootstrapModules: render.pipableStreamOptions?.bootstrapModules?.map(path => { if (!path) return path; if(render.moduleBaseURL && render.moduleBaseURL !== '') { return new URL(path, render.moduleBaseURL).toString(); } const depth = id.split('/').filter(Boolean).length; const prefix = depth > 0 ? '../'.repeat(depth) : '/'; return path.startsWith('/') ? prefix + path.slice(1) : prefix + path; }), onShellReady() { parentPort?.postMessage({ type: "SHELL_READY", id }); } } ); // Pipe to collection stream stream.pipe(collectStream); }); htmlPromises.set(id, htmlPromise); // Clean up resources rscStream.destroy(); activeRenders.delete(id); htmlContent.delete(id); htmlPromises.delete(id); break; } case "SHUTDOWN": { console.log('Received shutdown signal'); parentPort?.close(); break; } } } catch (error) { console.error('Error in messageHandler:', error); throw error; } };