vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
236 lines (224 loc) • 7.63 kB
text/typescript
import { resolve } from "node:path";
import type { CreateHandlerOptions } from "../types.js";
import { type Connect } from "vite";
import { MIME_TYPES } from "../config/mimeTypes.js";
import { requestToRoute } from "./requestToRoute.js";
import { routeToURL } from "../utils/routeToURL.js";
/**
* # Request info
*
* Does the initial work to check if the request is for html, rsc, json, js, css, server-action, or something else not handled by this plugin.
*
* @param req
* @param handlerOptions
* @param hostDir
* @returns
*/
export function requestInfo(
req: Connect.IncomingMessage,
handlerOptions: Pick<
CreateHandlerOptions,
| "normalizer"
| "build"
| "autoDiscover"
| "verbose"
| "moduleBasePath"
| "moduleBaseURL"
| "verbose"
| "logger"
>,
hostDir: string,
) {
const route = requestToRoute(req, {
moduleBasePath: handlerOptions.moduleBasePath,
moduleBaseURL: handlerOptions.moduleBaseURL,
build: handlerOptions.build,
});
if (!route) {
return {
route: "/",
url: routeToURL("/", handlerOptions.moduleBaseURL, handlerOptions.build.rscOutputPath),
ext: "",
};
}
// Use the cleaned route for normalization, not the raw req.url
// This ensures base URL is properly stripped before normalization
const [, value] = handlerOptions.normalizer(route);
if (handlerOptions.verbose) {
if (value && value !== "") {
handlerOptions.logger.info(`[requestInfo] Value: \"${value}\"`);
}
if (hostDir && hostDir !== "") {
handlerOptions.logger.info(`[requestInfo] Host Dir: \"${hostDir}\"`);
}
if (req.url && req.url !== "") {
handlerOptions.logger.info(`[requestInfo] Request URL: \"${req.url}\"`);
}
}
const dotIndex = value.lastIndexOf(".");
const ext = dotIndex === -1 ? "" : value.slice(dotIndex);
// handle index.html
const isVendor = handlerOptions.autoDiscover.vendorPattern.test(value);
const isVirtual = handlerOptions.autoDiscover.virtualPattern.test(value);
const isJS = handlerOptions.autoDiscover.modulePattern.test(value);
const isHtml = handlerOptions.autoDiscover.htmlPattern.test(value);
const isCss = handlerOptions.autoDiscover.cssPattern.test(value);
const isJson = handlerOptions.autoDiscover.jsonPattern.test(value);
const isRsc = handlerOptions.autoDiscover.rscPattern.test(value);
const hasJsHeader =
req.headers["sec-fetch-dest"] === "script" ||
req.headers["accept"]?.includes("*/*") ||
req.headers["accept"]?.includes("text/javascript");
const hasJsonHeader = req.headers["accept"]?.includes("application/json");
const hasHtmlHeader = req.headers.accept?.includes("text/html");
const hasRscHeader = req.headers.accept?.includes("text/x-component");
// Support ?_rsc query param (e.g. /?_rsc for browser debugging) as alternative to Accept header (useful for browser debugging)
const hasRscQueryParam = /[?&]_rsc\b/.test(req.url || "");
const hasCssHeader = req.headers.accept?.includes("text/css");
const isFolder = !ext;
const isFormContentType =
req.headers["content-type"]?.includes(
"application/x-www-form-urlencoded"
) || !!req.headers["content-type"]?.includes("multipart/form-data");
// Server action detection
const hasRscActionHeader = !!req.headers["x-rsc-action"];
const hasServerActionHeaders =
req.method === "POST" &&
(hasRscActionHeader || (
(req.headers["sec-fetch-dest"] === "empty" ||
req.headers["sec-fetch-dest"] === "") &&
req.headers["sec-fetch-mode"] === "cors"
));
const isServerActionRequest = hasServerActionHeaders;
const isFormActionRequest =
!isServerActionRequest &&
(req.method === "POST" ||
(isFormContentType &&
req.headers["sec-fetch-dest"] === "document" &&
req.headers["sec-fetch-mode"] === "navigate"));
const isJsRequest =
!isFormActionRequest &&
!isJson &&
!isHtml &&
!isCss &&
!isRsc &&
(isJS || hasJsHeader);
const isJsonRequest = isJson || (hasJsonHeader && !isJsRequest);
// Form action detection
const isHtmlRequest =
!hasRscQueryParam && (isHtml ||
hasHtmlHeader ||
(isFolder &&
!hasRscHeader &&
!isRsc &&
!isJsRequest &&
!isFormActionRequest));
const isRscRequest =
hasRscQueryParam || (!isJsRequest && !isHtmlRequest && (isRsc || hasRscHeader));
const isCssRequest =
!isHtmlRequest &&
!isRscRequest &&
!isJsRequest &&
!isJsonRequest &&
(isCss || hasCssHeader);
// Use the normalized value for file path construction
// The normalizer should have already stripped base URLs properly
const routeForFilePath = value;
let filePath = resolve(hostDir, routeForFilePath);
let contentType;
if (isServerActionRequest) {
// For server actions, we'll get the actual file path from the request body
// The route is just a placeholder
filePath = resolve(hostDir, routeForFilePath);
contentType = "application/json; charset=utf-8";
} else if (isHtmlRequest) {
if (!isHtml) {
filePath = resolve(
hostDir,
routeForFilePath,
handlerOptions.build.htmlOutputPath
);
}
contentType = "text/html; charset=utf-8";
} else if (isRscRequest) {
if (!isRsc) {
// Value doesn't end with .rsc, append the rsc output path
filePath = resolve(
hostDir,
routeForFilePath,
handlerOptions.build.rscOutputPath
);
}
contentType = "text/x-component; charset=utf-8";
} else if (isCssRequest) {
if (!isCss) {
filePath = resolve(hostDir, routeForFilePath + ".css");
}
contentType = "text/css; charset=utf-8";
} else if (isJsRequest) {
if (!isJS) {
filePath = resolve(hostDir, routeForFilePath + ".js");
}
contentType = "application/javascript; charset=utf-8";
} else if (isJsonRequest) {
if (!isJson) {
filePath = resolve(hostDir, routeForFilePath + ".json");
}
contentType = "application/json; charset=utf-8";
} else {
const mimeType = MIME_TYPES[ext];
if (mimeType) {
contentType = mimeType + "; charset=utf-8";
} else {
contentType = "application/octet-stream";
}
}
if (handlerOptions.verbose) {
if (isFormActionRequest) {
handlerOptions.logger.info(`[react-dev-server] (form-action) ${route}`);
} else if (isServerActionRequest) {
handlerOptions.logger.info(`[react-dev-server] (server-action) ${route}`);
} else if (isHtmlRequest) {
handlerOptions.logger.info(`[react-dev-server] (html) ${route}`);
} else if (isRscRequest) {
handlerOptions.logger.info(`[react-dev-server] (rsc) ${route}`);
} else if (isCssRequest) {
handlerOptions.logger.info(`[react-dev-server] (css) ${route}`);
} else if (isJsRequest) {
handlerOptions.logger.info(`[react-dev-server] (js) ${route}`);
} else if (isJsonRequest) {
handlerOptions.logger.info(`[react-dev-server] (json) ${route}`);
} else {
handlerOptions.logger.info(`[react-dev-server] (other) ${route}`);
}
}
return {
route,
url: routeToURL(route, handlerOptions.moduleBaseURL, handlerOptions.build.rscOutputPath),
ext,
isHtmlRequest,
isRscRequest,
isCssRequest,
isCss,
isHtml,
isRsc,
isFolder,
contentType,
filePath,
isJS,
isVendor,
isVirtual,
hasJsHeader,
isJsRequest,
isJson,
isJsonRequest,
hasCssHeader,
hasJsonHeader,
hasHtmlHeader,
hasRscHeader,
hasServerActionHeaders,
isServerActionRequest,
isFormContentType,
isFormActionRequest,
};
}