vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
242 lines (239 loc) • 8.39 kB
JavaScript
/**
* vite-plugin-react-server
* Copyright (c) Nico Brinkkemper
* MIT License
*/
import { mkdir, writeFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { Transform } from 'node:stream';
import { createHandler } from '../../helpers/createHandler.js';
import { createLogger } from 'vite';
import React__default from 'react';
import { collectManifestClientFiles } from '../../collect-manifest-client-files.js';
async function renderPages(routes, files, options) {
const failedRoutes = /* @__PURE__ */ new Map();
const completedRoutes = /* @__PURE__ */ new Set();
const clientCss = options.clientCss ?? [];
const partialPageData = /* @__PURE__ */ new Map();
const moduleRootPath = options.moduleBasePath !== "" && !options.moduleRootPath.endsWith(options.moduleBasePath) ? join(options.moduleRootPath, options.moduleBasePath) : options.moduleRootPath;
const mergeAndSendPageData = async (route, resolve) => {
const partial = partialPageData.get(route);
if (!partial?.html || !partial.rsc) {
return;
}
const pageData = {
route,
html: partial.html,
rsc: partial.rsc
};
let routeHtmlPath = route === "/" ? options.htmlOutputPath : options.htmlOutputPath.replace(
"index.html",
join(route, "index.html")
);
if (routeHtmlPath.startsWith("/")) {
routeHtmlPath = routeHtmlPath.slice(1);
}
const routeRscPath = routeHtmlPath.slice(0, -5) + ".rsc";
await mkdir(dirname(routeHtmlPath), { recursive: true });
await writeFile(routeRscPath, partial.rsc.content);
await writeFile(routeHtmlPath, partial.html.raw);
await options.onPage?.(pageData);
completedRoutes.add(route);
if (completedRoutes.size === routes.length) {
resolve();
}
};
try {
const allRoutesComplete = new Promise((resolve, reject) => {
options.worker.on("message", async (msg) => {
switch (msg.type) {
case "ALL_READY": {
const { id, html } = msg;
try {
const partial = partialPageData.get(id) || { route: id };
partial.html = {
raw: html,
transformed: typeof options.transformIndexHtml === "function" ? String(await options.transformIndexHtml(id, {
path: id,
filename: join(id, "index.html")
}) || "") : "",
// Will be set by main thread transform
assets: []
};
partialPageData.set(id, partial);
await mergeAndSendPageData(id, resolve);
} catch (error) {
failedRoutes.set(id, error);
reject(error);
}
break;
}
case "ERROR": {
console.error("Worker error for route:", msg.id, msg.error);
failedRoutes.set(msg.id, new Error(msg.error));
reject(new Error(msg.error));
break;
}
}
});
});
for (const route of routes) {
const routeFiles = files.urlMap.get(route);
if (!routeFiles) {
console.error("No files found for route:", route);
failedRoutes.set(route, new Error(`No files found for ${route}`));
continue;
}
if (options.pipableStreamOptions?.importMap?.imports) {
for (let [, value] of Object.entries(
options.pipableStreamOptions?.importMap?.imports
)) {
options.onClientJSFile?.(value, route);
}
}
const getCss = async (id) => {
const cssFiles = collectManifestClientFiles({
manifest: options.serverManifest,
root: options.root,
pagePath: id
}).cssFiles;
return cssFiles;
};
const pagePath = files.urlMap.get(route)?.page;
const propsPath = files.urlMap.get(route)?.props;
if (!pagePath) {
throw new Error(`No page path found for ${route}`);
}
const rscResult = await createHandler({
root: options.root,
url: route,
route,
getCss,
loader: options.loader,
cssFiles: clientCss,
moduleBase: options.moduleBase,
moduleBasePath: options.moduleBasePath,
moduleRootPath,
moduleBaseURL: options.moduleBaseURL,
pipableStreamOptions: options.pipableStreamOptions ?? {},
inlineCss: options.inlineCss,
Html: React__default.Fragment,
CssCollector: options.CssCollector,
pagePath,
propsPath,
pageExportName: options.pageExportName,
propsExportName: options.propsExportName,
logger: createLogger()
});
const htmlResult = await createHandler({
root: options.root,
url: route,
route,
getCss,
loader: options.loader,
cssFiles: clientCss,
moduleBase: options.moduleBase,
moduleBasePath: options.moduleBasePath,
moduleRootPath,
moduleBaseURL: options.moduleBaseURL,
pipableStreamOptions: options.pipableStreamOptions,
inlineCss: options.inlineCss,
Html: options.Html,
CssCollector: options.CssCollector,
pagePath,
propsPath,
pageExportName: options.pageExportName,
propsExportName: options.propsExportName,
logger: createLogger()
});
if (rscResult.type !== "success" || htmlResult.type !== "success") {
if (rscResult.type !== "success") {
if (rscResult.type !== "skip") {
console.error("Handler failed for route:", route, rscResult.error);
}
}
if (htmlResult.type !== "success") {
if (htmlResult.type !== "skip") {
console.error("Handler failed for route:", route, htmlResult.error);
}
}
failedRoutes.set(route, new Error(`Handler failed for ${route}`));
continue;
}
await Promise.all([
// Handle RSC stream
new Promise((resolve, reject) => {
const chunks = [];
const rscTransform = new Transform({
transform(chunk, _encoding, callback) {
try {
if (chunk) {
chunks.push(Buffer.from(chunk));
callback(null, chunk);
}
} catch (error) {
callback(error);
}
},
async flush(callback) {
try {
const rscContent = Buffer.concat(chunks).toString("utf-8");
const partial = partialPageData.get(route) || { route };
partial.rsc = {
modules: [],
// Will be parsed by the client
content: rscContent
};
partialPageData.set(route, partial);
await mergeAndSendPageData(route, resolve);
callback();
resolve();
} catch (error) {
callback(error);
reject(error);
}
}
});
rscResult.stream.pipe(rscTransform);
}),
// Send HTML stream to worker
new Promise((resolve) => {
const htmlTransform = new Transform({
transform(chunk, _encoding, callback) {
try {
options.worker.postMessage({
type: "RSC_CHUNK",
id: route,
chunk: chunk.toString(),
moduleRootPath,
moduleBaseURL: options.moduleBaseURL,
htmlOutputPath: options.htmlOutputPath,
pipableStreamOptions: options.pipableStreamOptions
});
callback(null, chunk);
} catch (error) {
callback(error);
}
},
flush(callback) {
options.worker.postMessage({
type: "RSC_END",
id: route
});
callback();
resolve();
}
});
htmlResult.stream.pipe(htmlTransform);
})
]);
}
await allRoutesComplete;
} catch (error) {
console.error("Render error:", error);
throw error;
}
return { failedRoutes, completedRoutes };
}
export { renderPages };
//# sourceMappingURL=renderPages.js.map