vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
120 lines (112 loc) • 4.33 kB
text/typescript
import type { PageName, PropsName, RootName, HtmlName, ResolvedUserOptions } from "../types.js";
type SupportedOptionNames = PropsName | PageName | RootName | HtmlName;
type ResolvePageAndPropsOptionsSuccess<N extends SupportedOptionNames> = {
[optionName in N]: string;
} & { type: "success"; error?: never };
type ResolvePageAndPropsOptionsError<N extends SupportedOptionNames> = {
[optionName in N]?: never;
} & { type: "error"; error: unknown };
export type ResolvePageAndPropsReturn<N extends SupportedOptionNames> =
| ResolvePageAndPropsOptionsSuccess<N>
| ResolvePageAndPropsOptionsError<N>;
export type ResolvePageAndPropsOptionsFn = <N extends SupportedOptionNames>(
options: Pick<
ResolvedUserOptions,
PropsName | PageName | RootName | HtmlName | "pageExportName" | "propsExportName" | "rootExportName" | "htmlExportName"
>,
optionName: N,
url: string
) => Promise<ResolvePageAndPropsReturn<N>>;
/**
* Resolves Page or props options to file paths.
*
* ## Usage Pattern:
* 1. **BUILD TIME**: Called by `resolveBuildPages.ts` for each page in `build.pages`
* - Resolves all Page/props paths statically
* - Builds urlMap/pageMap/propsMap caches for fast runtime lookup
*
* 2. **RUNTIME**: Called by `getRouteFiles.ts` as dynamic fallback
* - Only when route not found in auto-discovered urlMap cache
* - Enables dynamic routing for routes not in build.pages
*
* ## Supported Option Types:
* - **String**: Direct file path (e.g., "./src/HomePage.tsx")
* - **Function**: Router function that takes URL and returns file path
* - Sync: `(url) => "./src/pages/" + url + ".tsx"`
* - Async: `(url) => Promise.resolve("./src/pages/" + url + ".tsx")`
*
* ## Discovery Limitation:
* Function-based resolvers create a chicken-and-egg problem for auto-discovery:
* - Build-time discovery needs to know all possible URLs to call resolvers
* - URL discovery typically comes from scanning filesystem for Page files
* - But function resolvers can't be statically analyzed without URLs
*
* This is why extending this pattern to Html/Root is complex when
* build.pages is not specified - the system can't discover what files exist
* without first knowing what URLs to resolve.
*/
export const resolveUrlOption: ResolvePageAndPropsOptionsFn = async function _resolveUrlOption(
options,
optionName,
url
) {
let configName = optionName as string;
if(optionName === options.pageExportName) {
configName = "Page" as never;
} else if(optionName === options.propsExportName) {
configName = "props" as never;
} else if(optionName === options.rootExportName) {
configName = "Root" as never;
} else if(optionName === options.htmlExportName) {
configName = "Html" as never;
}
if(!(configName === "Page" || configName === "props" || configName === "Root" || configName === "Html")) {
throw new Error(`Invalid option name: ${optionName}`);
}
try {
let optionValue: any;
switch (configName) {
case "Page":
optionValue = options.Page;
break;
case "props":
optionValue = options.props;
break;
case "Root":
optionValue = options.Root;
break;
case "Html":
optionValue = options.Html;
break;
default:
return { type: "error", error: new Error(`Unknown config name: ${configName}`) };
}
switch (typeof optionValue) {
case "function": {
const fn = optionValue;
const result = fn(url);
if (typeof result === "string") {
return { type: "success", [configName]: result } as any;
}
if (result instanceof Promise) {
try {
const promiseResult = await result;
if (typeof promiseResult === "string") {
return { type: "success", [configName]: promiseResult } as any;
}
} catch (error) {
return { type: "error", error: error as Error };
}
}
break;
}
case "string":
return { type: "success", [configName]: optionValue } as any;
default:
break;
}
return { type: "error", error: new Error(`${configName} must be string or function that returns a string`) };
} catch (error) {
return { type: "error", error: error as Error };
}
}