vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
200 lines (186 loc) • 6.05 kB
text/typescript
import type { PageName, PropsName, ResolvedUserOptions } from "../types.js";
import { resolveUrlOption } from "../config/resolveUrlOption.js";
import type { AutoDiscoveredFiles } from "../types.js";
import { createLogger } from "vite";
type GetRouteFilesSuccess = {
type: "success";
page: string;
props?: string | undefined;
root?: string | undefined;
html?: string | undefined;
};
type GetRouteFilesError = {
type: "error";
error: unknown;
};
/**
* Gets page and props file paths for a route using cached discovery + dynamic fallback.
*
* ## RUNTIME RESOLUTION STRATEGY
* This function implements a two-tier lookup strategy:
*
* 1. **FAST PATH**: Check auto-discovered urlMap cache first
* - Built by resolveBuildPages.ts from build.pages array
* - O(1) lookup, no I/O, very fast
* - Used for routes that were pre-discovered at build time
*
* 2. **FALLBACK PATH**: Dynamic resolution via resolveUrlOption
* - Only when route not found in urlMap cache
* - Calls Page/props resolver functions with the route URL
* - Enables dynamic routing beyond what's in build.pages
* - Slower due to function calls + potential I/O
*
* ## Auto-Discovery Integration:
* Results from dynamic resolution are cached back into autoDiscoveredFiles.urlMap
* for future requests, so each route is only resolved dynamically once.
*
* ## Function Resolver Support:
* This is where router functions like `(url) => "./src/pages/" + url + ".tsx"`
* are actually called. The dynamic nature allows runtime route resolution but
* creates complexity for static analysis and build-time discovery.
*/
export const getRouteFiles = async (
route: string,
autoDiscoveredFiles: AutoDiscoveredFiles,
userOptions: Pick<
ResolvedUserOptions,
| PageName
| PropsName
| "Root"
| "Html"
| "moduleBasePath"
| "verbose"
| "pageExportName"
| "propsExportName"
| "rootExportName"
| "htmlExportName"
>,
logger = createLogger()
): Promise<GetRouteFilesSuccess | GetRouteFilesError> => {
if (autoDiscoveredFiles.urlMap.has(route)) {
const cached = autoDiscoveredFiles.urlMap.get(route)!;
const { page, props } = cached;
let { root, html } = cached;
if (userOptions.verbose) {
logger.info(`[getRouteFiles] Found cached route: ${route}, page: "${page}", props: "${props}"`);
}
// If the cached page is undefined, fall back to resolveUrlOption
if (!page) {
if (userOptions.verbose) {
logger.info("[getRouteFiles] Cached page is undefined, falling back to resolveUrlOption");
}
// Fall through to the resolveUrlOption logic below
} else {
if (userOptions.verbose) {
if (page && page !== "") {
logger.info(`[getRouteFiles] Page: \"${page}\"`);
}
if (props && props !== "") {
logger.info(`[getRouteFiles] Props: \"${props}\"`);
}
if (root && root !== "") {
logger.info(`[getRouteFiles] Root: \"${root}\"`);
}
if (html && html !== "") {
logger.info(`[getRouteFiles] Html: \"${html}\"`);
}
}
// For cached routes, we still need to resolve Root and Html dynamically if they are functions
// and not already cached
if (!root && userOptions.Root) {
const {
type: rootType,
error: rootError,
Root,
} = await resolveUrlOption(userOptions, "Root", route);
if (rootType === "error") {
return { type: "error", error: rootError };
}
root = Root;
// Update cache with resolved root path
autoDiscoveredFiles.urlMap.set(route, { page, props, root, html });
}
if (!html && userOptions.Html) {
const {
type: htmlType,
error: htmlError,
Html,
} = await resolveUrlOption(userOptions, "Html", route);
if (htmlType === "error") {
return { type: "error", error: htmlError };
}
html = Html;
// Update cache with resolved html path
autoDiscoveredFiles.urlMap.set(route, { page, props, root, html });
}
return { type: "success", page, props, root, html };
}
}
if (userOptions.verbose) {
logger.info("[getRouteFiles] Not in urlMap, resolving Page option");
}
const { type, error, Page } = await resolveUrlOption(
userOptions,
"Page",
route
);
if (type === "error") {
return { type: "error", error };
}
if (userOptions.verbose) {
logger.info(`[getRouteFiles] Page resolved to: ${Page}`);
}
// Resolve Root and Html components
let root: string | undefined;
let html: string | undefined;
if (userOptions.Root) {
const {
type: rootType,
error: rootError,
Root,
} = await resolveUrlOption(userOptions, "Root", route);
if (rootType === "error") {
return { type: "error", error: rootError };
}
root = Root;
}
if (userOptions.Html) {
const {
type: htmlType,
error: htmlError,
Html,
} = await resolveUrlOption(userOptions, "Html", route);
if (htmlType === "error") {
return { type: "error", error: htmlError };
}
html = Html;
}
if (!userOptions.props) {
if (userOptions.verbose) {
logger.info("[getRouteFiles] No props option, returning page only");
}
autoDiscoveredFiles.urlMap.set(route, {
page: Page,
props: undefined,
root,
html,
});
return { type: "success", page: Page, props: undefined, root, html };
}
if (userOptions.verbose) {
logger.info("[getRouteFiles] Resolving props option");
}
const {
type: propsType,
error: propsError,
props,
} = await resolveUrlOption(userOptions, "props", route);
if (propsType === "error") {
return { type: "error", error: propsError };
}
if (userOptions.verbose) {
logger.info(`[getRouteFiles] Props resolved to: ${props}`);
}
autoDiscoveredFiles.urlMap.set(route, { page: Page, props, root, html });
return { type: "success", page: Page, props, root, html };
};