vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
294 lines (272 loc) • 9.48 kB
text/typescript
import { DEFAULT_CONFIG } from "../config/defaults.js";
import { toError } from "../error/toError.js";
import type {
CreateHandlerOptions,
PageComponentType,
PagePropOpt,
} from "../types.js";
import { resolvePage } from "./resolvePage.js";
import { resolveProps } from "./resolveProps.js";
import { routeToURL } from "../utils/routeToURL.js";
/**
* Resolves the page and props for a given route, works in combination with resolveComponents
* The special thing it does is that if the props is already in the page module, it will fallback to that.
* @param handlerOptions - The handler options.
* @returns The resolved page and props.
*/
export const resolvePageAndProps: ResolvePageAndPropsFn =
async function _resolvePageAndProps(handlerOptions) {
try {
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Starting resolution for route: ${handlerOptions.route}`
);
}
const url =
handlerOptions.url ??
routeToURL(
handlerOptions.route ?? "",
handlerOptions.moduleBaseURL ?? "/",
handlerOptions?.build?.rscOutputPath ??
DEFAULT_CONFIG.BUILD.rscOutputPath
);
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] URL resolved: ${url}. Starting page resolution`
);
}
const resolvePagePromise = resolvePage({
id: handlerOptions.pagePath,
exportName:
handlerOptions.pageExportName ?? DEFAULT_CONFIG.PAGE_EXPORT_NAME,
loader: handlerOptions.loader,
});
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Starting props resolution`
);
}
const resolvePropsPromise = resolveProps({
url,
id: handlerOptions.propsPath || handlerOptions.pagePath,
exportName:
handlerOptions.propsExportName ?? DEFAULT_CONFIG.PROPS_EXPORT_NAME,
loader: async (idWithExport?: string) => {
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Props loader called with: ${idWithExport}`
);
}
// Parse the id to extract the path and export name if using # syntax
let propsId = handlerOptions.propsPath || handlerOptions.pagePath;
let propsExportName = handlerOptions.propsExportName ?? DEFAULT_CONFIG.PROPS_EXPORT_NAME;
if (idWithExport && idWithExport.includes('#')) {
const [id, exportName] = idWithExport.split('#');
propsId = id;
if (exportName) {
propsExportName = exportName;
}
}
const resolvePageResult = await resolvePagePromise;
if (resolvePageResult.type === "error") {
if (handlerOptions.verbose) {
handlerOptions.logger?.error("resolveProps", {
error: resolvePageResult.error,
});
}
if (resolvePageResult.error != null) {
throw resolvePageResult.error;
}
throw new Error("Failed to resolve page in props loader");
}
if (
resolvePageResult.type === "success" &&
propsExportName in resolvePageResult.module
) {
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Props found in page module`
);
}
// return the module
return resolvePageResult.module;
}
if (propsId && propsId !== handlerOptions.pagePath) {
// Separate props file
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Loading props from separate file: ${propsId}`
);
}
const result = await handlerOptions.loader(propsId);
return result;
} else if (propsId === handlerOptions.pagePath) {
// Props might be in the page module
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Checking page module for props: ${propsId}`
);
}
if (resolvePageResult.type === "success") {
const pageModule = resolvePageResult.module;
if (propsExportName in pageModule) {
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Props found in page module`
);
}
return pageModule;
}
}
// Try loading the page module directly
const result = await handlerOptions.loader(propsId);
return result;
}
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Using default props with URL`
);
}
return {
[propsExportName]: { url: url },
};
},
});
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Waiting for both promises to resolve`
);
}
const [resolvePageResult, resolvePropsResult] = await Promise.all([
resolvePagePromise,
resolvePropsPromise,
]);
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Both promises resolved`
);
}
if (resolvePageResult.type != "success") {
if (handlerOptions.verbose) {
handlerOptions.logger?.error(
`[resolvePageAndProps] Page resolution failed: ${resolvePageResult.type}`
);
}
return resolvePageResult;
}
if (resolvePropsResult.type != "success") {
if (handlerOptions.verbose) {
handlerOptions.logger?.error(
`[resolvePageAndProps] Props resolution failed: ${resolvePropsResult.type}`
);
}
return resolvePropsResult;
}
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Both page and props resolved successfully`
);
}
let pageProps = resolvePropsResult.module?.[
handlerOptions.propsExportName as keyof typeof resolvePropsResult.module
] as any;
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Raw pageProps type: ${typeof pageProps}, isFunction: ${typeof pageProps === "function"}`
);
}
// If props is a function, call it with the URL
if (typeof pageProps === "function") {
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Props is a function, calling with url: ${url}`
);
}
try {
pageProps = pageProps(url);
// Await if it returns a Promise
if (pageProps instanceof Promise) {
pageProps = await pageProps;
}
} catch (error) {
if (handlerOptions.verbose) {
handlerOptions.logger?.error(
`[resolvePageAndProps] Error calling props function: ${error}`
);
}
pageProps = {};
}
}
if (handlerOptions.verbose) {
handlerOptions.logger?.info(
`[resolvePageAndProps] Extracted pageProps: ${JSON.stringify(Object.keys(pageProps || {}).length, null, 2)} keys`
);
handlerOptions.logger?.info(
`[resolvePageAndProps] resolvePropsResult.module keys: ${Object.keys(resolvePropsResult.module || {}).join(", ")}`
);
}
return {
type: "success" as const,
PageComponent: resolvePageResult.module[
handlerOptions.pageExportName
] as never,
pageProps: pageProps ?? {}, // Ensure pageProps is always an object, not undefined
};
} catch (error) {
if (handlerOptions.verbose) {
handlerOptions.logger?.error(
`[resolvePageAndProps] Error in resolvePageAndProps: ${error}`
);
}
return {
type: "error" as const,
error: toError(error),
};
}
};
type ResolvePageAndPropsResult<T extends PagePropOpt = PagePropOpt> =
| {
type: "success";
error?: never;
PageComponent: PageComponentType<T>;
pageProps: T;
}
| {
type: "error";
error: Error;
PageComponent?: never;
pageProps?: never;
}
| {
type: "skip";
error?: never;
PageComponent?: never;
pageProps?: never;
};
export type ResolvePageAndPropsFn = <T extends PagePropOpt = PagePropOpt>(
options: Pick<
CreateHandlerOptions,
| "pagePath"
| "pageExportName"
| "propsPath"
| "propsExportName"
| "loader"
| "verbose"
| "logger"
> & {
moduleBaseURL?: string;
route?: string;
url?: string;
build?: {
rscOutputPath: string;
outDir?: never;
server?: never;
client?: never;
static?: never;
pages?: never;
pageExportName?: never;
propsExportName?: never;
rootExportName?: never;
};
}
) => Promise<ResolvePageAndPropsResult<T>>;