vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
178 lines (158 loc) • 5.02 kB
text/typescript
import { toError } from "../error/toError.js";
import type { GenericModuleLoader, HtmlComponentType, RootComponentType } from "../types.js";
export type ComponentName = "Root" | "Html";
type ResolveComponentResult<T = unknown> =
| {
type: "success";
component: T;
error?: never;
}
| { type: "error"; error: Error; component?: never }
| { type: "skip"; error?: never; component?: never };
type ResolveComponentOptions = {
componentPath: string;
exportName: string;
loader: GenericModuleLoader;
};
/**
* Resolves a component (Root or Html) from a string path.
*
* This function handles:
* - String paths: "src/Root.tsx"
* - Fragment syntax: "src/components.tsx#MyRoot"
* - Export name resolution
*
* @param options.componentPath - The path to the component file
* @param options.exportName - The name of the export to resolve (e.g. 'Root', 'Html')
* @param options.loader - The loader function to use for loading the module
*
* @returns A result object containing the resolved component or error
*/
export async function resolveComponent<T = RootComponentType | HtmlComponentType>(
options: ResolveComponentOptions
): Promise<ResolveComponentResult<T>> {
const { componentPath, exportName, loader } = options;
try {
// Handle fragment syntax (e.g., "src/components.tsx#MyRoot")
let modulePath = componentPath;
let moduleExportName = exportName;
if (componentPath.includes('#')) {
const [path, fragmentExport] = componentPath.split('#');
modulePath = path;
moduleExportName = fragmentExport;
}
// Load the module
const module = await loader(`${modulePath}#${moduleExportName}`);
if (module == null) {
return {
type: "error",
error: new Error(`Module ${modulePath} not found`),
};
}
if (module instanceof Error) {
return {
type: "error",
error: module,
};
}
// Get the component from the module
const component = module[moduleExportName];
if (!(moduleExportName in module)) {
if ("error" in module) {
return {
type: "error",
error: toError(module["error"]),
};
}
return {
type: "error",
error: new Error(
`Export "${moduleExportName}" not found in module ${modulePath}. ` +
(moduleExportName !== "default"
? `Did you use \`export default\`? Use \`export function ${moduleExportName}(...)\` or set pageExportName: "default" in your plugin config.`
: `The module does not have a default export.`)
),
};
}
if (!component) {
return {
type: "error",
error: new Error(
`Export "${moduleExportName}" is null or undefined in module ${modulePath}.`
),
};
}
if (component instanceof Error) {
return {
type: "error",
error: component,
};
}
return {
type: "success",
component: component as T,
};
} catch (error) {
return {
type: "error",
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
/**
* Resolves Root and Html components from user options.
*
* This function checks if Root/Html are strings and resolves them to components.
* If they're already components, it returns them as-is.
*
* @param options - Object containing Root, Html, and resolution options
* @returns Resolved components or original values if not strings
*/
export async function resolveComponentOptions(options: {
Root: RootComponentType | string;
Html: HtmlComponentType | string;
rootExportName: string;
htmlExportName: string;
loader: GenericModuleLoader;
}): Promise<{
Root: RootComponentType;
Html: HtmlComponentType;
errors: Error[];
}> {
const errors: Error[] = [];
let resolvedRoot = options.Root;
let resolvedHtml = options.Html;
// Resolve Root if it's a string
if (typeof options.Root === "string") {
const rootResult = await resolveComponent<RootComponentType>({
componentPath: options.Root,
exportName: options.rootExportName,
loader: options.loader,
});
if (rootResult.type === "success") {
resolvedRoot = rootResult.component;
} else if (rootResult.type === "error") {
errors.push(rootResult.error);
// Keep original value as fallback
}
}
// Resolve Html if it's a string
if (typeof options.Html === "string") {
const htmlResult = await resolveComponent<HtmlComponentType>({
componentPath: options.Html,
exportName: options.htmlExportName,
loader: options.loader,
});
if (htmlResult.type === "success") {
resolvedHtml = htmlResult.component;
} else if (htmlResult.type === "error") {
errors.push(htmlResult.error);
// Keep original value as fallback
}
}
return {
Root: resolvedRoot as RootComponentType,
Html: resolvedHtml as HtmlComponentType,
errors,
};
}