UNPKG

vite-plugin-react-server

Version:
267 lines (264 loc) 10 kB
/** * vite-plugin-react-server * Copyright (c) Nico Brinkkemper * MIT License */ import 'vite'; import { resolveOptions } from '../config/resolveOptions.js'; import { resolveUserConfig } from '../config/resolveUserConfig.js'; import { stat, readFile, mkdir, writeFile } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { getBundleManifest } from '../helpers/getBundleManifest.js'; import { checkFilesExist } from '../checkFilesExist.js'; import { resolvePages } from '../config/resolvePages.js'; import { createInputNormalizer } from '../helpers/inputNormalizer.js'; import { createWorker } from '../worker/createWorker.js'; import { DEFAULT_CONFIG } from '../config/defaults.js'; import { MIME_TYPES } from '../config/mimeTypes.js'; let userOptions; let userConfig; let clientManifest = {}; let resolvedConfig; let root; let loader = (id) => import(id); let worker; let files; function reactClientPlugin(options) { const resolvedOptions = resolveOptions(options, true); if (resolvedOptions.type === "error") { throw resolvedOptions.error; } userOptions = resolvedOptions.userOptions; root = userOptions.projectRoot; return { name: "vite:react-client", async config(config, configEnv) { if (typeof config.root === "string" && config.root !== root && config.root !== process.cwd() && config.root !== "") { root = config.root; console.log("[vite:react-client] Root updated:", root); } if (configEnv.command === "serve" && !configEnv.isPreview && !worker) { worker = await createWorker({ projectRoot: root, workerPath: userOptions.rscWorkerPath, reverseCondition: true }); } const pages = await resolvePages(userOptions.build.pages); if (pages.type === "error") { throw pages.error; } if (pages.pages.length > 0) { files = await checkFilesExist(pages.pages, userOptions, root); } else { files = { pageMap: /* @__PURE__ */ new Map(), propsMap: /* @__PURE__ */ new Map(), propsSet: /* @__PURE__ */ new Set(), pageSet: /* @__PURE__ */ new Set(), urlMap: /* @__PURE__ */ new Map(), errors: [] }; } const resolvedConfig2 = resolveUserConfig({ isClient: true, config, configEnv, userOptions, files }); if (resolvedConfig2.type === "error") { throw resolvedConfig2.error; } userConfig = resolvedConfig2.userConfig; return userConfig; }, configResolved(config) { resolvedConfig = config; }, async generateBundle(_options, bundle) { clientManifest = getBundleManifest({ pluginContext: this, bundle, moduleBase: userOptions.moduleBase, preserveModulesRoot: userOptions.build.preserveModulesRoot }); const manifestPath = join( root, resolvedConfig.environments["client"].build.outDir, resolvedConfig.environments["client"].build.manifest ); await mkdir(dirname(manifestPath), { recursive: true }); return await writeFile( manifestPath, JSON.stringify(clientManifest, null, 2) ); }, async configurePreviewServer(server) { if (root !== server.config.root) { root = server.config.root; } if (typeof loader !== "function") { loader = (id) => import(id); } const normalize = createInputNormalizer({ root, removeExtension: false, preserveModulesRoot: userOptions.build.preserveModulesRoot ? userOptions.moduleBase : undefined }); server.middlewares.use(async (req, res, next) => { const [key, value] = normalize(req.url); const fileRoot = key.startsWith("node_modules") ? root : join(root, userOptions.build.outDir, userOptions.build.static); try { const filePath = join(fileRoot, value); const stats = await stat(filePath); if (stats.isFile()) { const ext = value.slice(value.lastIndexOf(".")); const contentType = MIME_TYPES[ext] || "application/octet-stream"; res.setHeader("Content-Type", contentType); const content = await readFile(filePath); res.end(content); return; } next(); } catch (error) { console.log("Error serving static file:", error); next(); } }); }, // setup dev server async configureServer(server) { if (typeof loader !== "function") { loader = server.ssrLoadModule; } if (!worker) { worker = await createWorker({ projectRoot: root, workerPath: userOptions.rscWorkerPath, condition: "react-client" }); } const normalize = createInputNormalizer({ root, removeExtension: false, preserveModulesRoot: userOptions.build.preserveModulesRoot ? userOptions.moduleBase : undefined }); server.middlewares.use(async (req, res, next) => { if (!req.url) { next(); return; } if (req.url.endsWith(".rsc") || req.headers.accept?.includes("text/x-component")) { try { const path = req.url?.includes("index.rsc") ? req.url.replace("index.rsc", "") : req.url?.replace(".rsc", ""); let [key, value] = normalize(path); let pageImport = DEFAULT_CONFIG.PAGE; let propsImport = DEFAULT_CONFIG.PROPS; const pathNoTrailing = path?.replace(/\/$/, ""); if (files.urlMap.has(req.url)) { pageImport = files.urlMap.get(req.url).page; propsImport = files.urlMap.get(req.url).props; } else if (files.urlMap.has(pathNoTrailing)) { pageImport = files.urlMap.get(pathNoTrailing).page; propsImport = files.urlMap.get(pathNoTrailing).props; } else if (files.urlMap.has(path)) { pageImport = files.urlMap.get(path).page; propsImport = files.urlMap.get(path).props; } else if (files.urlMap.has(value)) { pageImport = files.urlMap.get(value).page; propsImport = files.urlMap.get(value).props; } else if (files.urlMap.has(key)) { pageImport = files.urlMap.get(key).page; propsImport = files.urlMap.get(key).props; } else { console.warn(`Page/props import not found for any of the following (in order of priority): ${[req.url, pathNoTrailing, path, value, key].filter(Boolean).join(", ")} available pages:${Array.from(files.urlMap.keys()).join(", ")}`); } res.setHeader("Content-Type", "text/x-component"); res.setHeader("Transfer-Encoding", "chunked"); res.setHeader("Connection", "keep-alive"); let hasError = false; const timeout = setTimeout(() => { if (!hasError) { hasError = true; res.statusCode = 500; res.end("RSC render timeout"); } }, 5e3); const messageHandler = (message) => { try { switch (message.type) { case "RSC_CHUNK": if (!hasError) { res.write(message.chunk); } break; case "RSC_END": clearTimeout(timeout); if (!hasError) { res.end(); } worker.off("message", messageHandler); break; case "ERROR": clearTimeout(timeout); if (!hasError) { hasError = true; res.statusCode = 500; res.end(message.error); } worker.off("message", messageHandler); break; } } catch (error) { clearTimeout(timeout); if (!hasError) { hasError = true; res.statusCode = 500; res.end( error instanceof Error ? error.message : String(error) ); } worker.off("message", messageHandler); } }; worker.on("message", messageHandler); worker.once("error", (error) => { clearTimeout(timeout); if (!hasError) { hasError = true; res.statusCode = 500; res.end(error instanceof Error ? error.message : String(error)); } worker.off("message", messageHandler); }); worker.postMessage({ type: "RSC_RENDER", id: value, pageImport, propsImport, url: req.url ?? "/", pageExportName: userOptions.pageExportName ?? DEFAULT_CONFIG.PAGE_EXPORT_NAME, propsExportName: userOptions.propsExportName ?? DEFAULT_CONFIG.PROPS_EXPORT_NAME, outDir: userOptions.build.outDir, projectRoot: root, moduleRootPath: userOptions.build.preserveModulesRoot === true ? userOptions.moduleBase : "", moduleBaseURL: userOptions.moduleBaseURL, moduleBasePath: userOptions.moduleBasePath, moduleBase: userOptions.moduleBase, pipableStreamOptions: userOptions.pipableStreamOptions, cssFiles: [] }); } catch (error) { res.statusCode = 500; res.end(error instanceof Error ? error.message : String(error)); } } else { next(); } }); } }; } export { reactClientPlugin }; //# sourceMappingURL=plugin.js.map