UNPKG

vite-plugin-react-server

Version:
319 lines (291 loc) 9.76 kB
import { parentPort, MessageChannel, workerData } from "node:worker_threads"; import { messageHandler } from "./messageHandler.server.js"; import { register } from "node:module"; import { register as registerTsx } from "tsx/esm/api"; import { resolve } from "node:path"; import { pluginRoot } from "../../root.js"; import type { HmrAcceptMessage, ReadyMessage } from "../types.js"; import type { CssFileMessage, HmrUpdateMessage, InitializedEnvLoaderMessage, InitializedReactLoaderMessage, RscWorkerInputMessage, } from "./types.js"; import { DEFAULT_CONFIG } from "../../config/defaults.js"; import { createLogger } from "vite"; import { handleError } from "../../error/handleError.js"; import { sendMessage } from "../sendMessage.js"; import { setMaxListenersOnPort, unrefPort } from "../../stream/setMaxListeners.js"; // Initialize worker if (!parentPort) { throw new Error("This module must be run as a worker"); } // In test mode, we want errors to propagate up immediately const logger = createLogger(workerData.resolvedConfig?.logLevel ?? "info", { prefix: "rsc-worker", }); // Set up loader message handlers const cssLoaderMessageHandler = (msg: CssFileMessage) => { messageHandler(msg); }; const envLoaderMessageHandler = (msg: InitializedEnvLoaderMessage) => { messageHandler(msg); }; const reactLoaderMessageHandler = (msg: InitializedReactLoaderMessage) => { messageHandler(msg); }; try { // Check if we're in build mode - if so, skip loader registration since files are already built const isBuildMode = workerData.configEnv?.command === "build" if (isBuildMode) { logger.info("Build mode detected - skipping loader registration since files are already built"); } else { logger.info("Development/test mode detected - registering loaders for source file processing"); } // Create channels for each loader const reactLoaderChannel = new MessageChannel(); const cssLoaderChannel = new MessageChannel(); const envLoaderChannel = new MessageChannel(); // Increase max listeners to prevent warnings during development setMaxListenersOnPort(reactLoaderChannel.port1, 500); setMaxListenersOnPort(reactLoaderChannel.port2, 500); setMaxListenersOnPort(cssLoaderChannel.port1, 500); setMaxListenersOnPort(cssLoaderChannel.port2, 500); setMaxListenersOnPort(envLoaderChannel.port1, 500); setMaxListenersOnPort(envLoaderChannel.port2, 500); // Unref all ports so they don't keep the event loop alive unrefPort(reactLoaderChannel.port1); unrefPort(reactLoaderChannel.port2); unrefPort(cssLoaderChannel.port1); unrefPort(cssLoaderChannel.port2); unrefPort(envLoaderChannel.port1); unrefPort(envLoaderChannel.port2); // Set up message handlers before transferring ports (only needed if not in build mode) if (!isBuildMode) { reactLoaderChannel.port2.on("message", reactLoaderMessageHandler); reactLoaderChannel.port2.on("messageerror", (error: Error) => { logger.error("React loader message serialization failed.", { error }); if (parentPort) { parentPort.postMessage({ type: "ERROR", id: "react-loader", error: { message: "Message serialization failed in react loader", name: "MessageError", stack: undefined, }, }); } }); cssLoaderChannel.port2.on("message", cssLoaderMessageHandler); cssLoaderChannel.port2.on("messageerror", (error: Error) => { logger.error("CSS loader message serialization failed.", { error }); if (parentPort) { parentPort.postMessage({ type: "ERROR", id: "css-loader", error: { message: "Message serialization failed in CSS loader", name: "MessageError", stack: undefined, }, }); } }); envLoaderChannel.port2.on("message", envLoaderMessageHandler); envLoaderChannel.port2.on("messageerror", (error: Error) => { logger.error("Env loader message serialization failed.", { error }); if (parentPort) { parentPort.postMessage({ type: "ERROR", id: "env-loader", error: { message: "Message serialization failed in env loader", name: "MessageError", stack: undefined, }, }); } }); } const root = workerData.userOptions?.projectRoot || workerData.resolvedConfig?.root || process.cwd(); const reactLoaderPath = "file://" + (workerData.userOptions.reactLoaderPath ? resolve( root, workerData.userOptions.reactLoaderPath ) : resolve( root, DEFAULT_CONFIG.REACT_LOADER_PATH )); logger.info(`Using reactLoaderPath: ${reactLoaderPath}`); const cssLoaderPath = "file://" + (workerData.userOptions.cssLoaderPath ? resolve( root, workerData.userOptions.cssLoaderPath ) : resolve( root, DEFAULT_CONFIG.CSS_LOADER_PATH )); const envLoaderPath = "file://" + (workerData.userOptions.envLoaderPath ? resolve( root, workerData.userOptions.envLoaderPath ) : resolve( root, DEFAULT_CONFIG.ENV_LOADER_PATH )); // Only register loaders if not in build mode if (!isBuildMode) { try { register(cssLoaderPath, { parentURL: pluginRoot, data: { id: "css-loader", port: cssLoaderChannel.port1, userOptions: workerData.userOptions, resolvedConfig: workerData.resolvedConfig, }, transferList: [cssLoaderChannel.port1], }); } catch (err) { const handledError = handleError({ error: err, logger, panicThreshold: workerData.userOptions.panicThreshold, context: `register(${cssLoaderPath})`, }); if (handledError != null) throw handledError; } // Register tsx registerTsx(); try { // Register loaders with their ports register(reactLoaderPath, { parentURL: pluginRoot, data: { id: "react-loader", port: reactLoaderChannel.port1, userOptions: workerData.userOptions, resolvedConfig: workerData.resolvedConfig, }, transferList: [reactLoaderChannel.port1], }); } catch (err) { const handledError = handleError({ error: err, logger, panicThreshold: workerData.userOptions.panicThreshold, context: `register(${reactLoaderPath})`, }); if (handledError != null) throw handledError; } // Register env-loader (ensure this the last) try { register(envLoaderPath, { parentURL: pluginRoot, data: { id: "env-loader", port: envLoaderChannel.port1, resolvedConfig: workerData.resolvedConfig, userOptions: workerData.userOptions, }, transferList: [envLoaderChannel.port1], }); } catch (err) { const handledError = handleError({ error: err, logger, panicThreshold: workerData.userOptions.panicThreshold, context: `register(${envLoaderPath})`, }); if (handledError != null) throw handledError; } } else { // In build mode, just register tsx for basic TypeScript support registerTsx(); } // Increase max listeners on parentPort to prevent warnings setMaxListenersOnPort(parentPort, 500); // Handle all messages through the unified messageHandler parentPort!.on("message", messageHandler); parentPort!.on("messageerror", (error: Error) => { console.error( "[rsc-worker] Parent port message serialization failed:", error ); // Can't send via parentPort since that's what failed, so just log }); const { hmrPort } = workerData; if (hmrPort) { // Start the message port hmrPort.start(); // Listen for HMR messages hmrPort.on("message", (message: RscWorkerInputMessage) => { if (message.type === "HMR_UPDATE") { // Invalidate the module in the worker parentPort!.postMessage({ type: "HMR_UPDATE", id: message.id, routes: message.routes, } satisfies HmrUpdateMessage); } else if (message.type === "HMR_ACCEPT") { // Handle the update parentPort!.postMessage({ type: "HMR_ACCEPT", id: message.id, routes: message.routes, } satisfies HmrAcceptMessage); } }); // Handle HMR port message errors hmrPort.on("messageerror", (error: Error) => { logger.error("HMR port message serialization failed.", { error }); if (parentPort) { parentPort.postMessage({ type: "ERROR", id: "hmr-port", error: { message: "Message serialization failed in HMR port", name: "MessageError", stack: undefined, }, }); } }); } // Notify parent that we're ready parentPort!.postMessage({ type: "READY", env: process.env["NODE_ENV"], pid: process.pid, id: "worker/rsc", } satisfies ReadyMessage); if (process.env["NODE_ENV"] === "production") { throw new Error("This module should not run in production mode."); } } catch (err) { const handledError = handleError({ error: err, logger, panicThreshold: workerData.userOptions.panicThreshold, context: "rsc-worker", }); // In dev mode, try to send error message before exiting if (parentPort && handledError != null) { sendMessage({ type: "ERROR", id: "worker/rsc", error: handledError, }, parentPort); } }