vite-plugin-react-server
Version:
Vite plugin for React Server Components (RSC)
241 lines (232 loc) • 6.45 kB
text/typescript
import { toError } from "../error/toError.js";
import type { GenericModuleLoader, PagePropOpt, PropsName } from "../types.js";
type ResolvePropsOptions = {
id: string;
url: string;
exportName: string;
loader: GenericModuleLoader;
};
type ValidPropTypes<T> =
| T
| Promise<T>
| Array<T[keyof T]>
| Array<[string, T[keyof T]]>
| ((url: string) => ValidPropTypes<T>);
// prototype classes
type ResolvePropsResult<T extends PagePropOpt, N extends string> =
| {
type: "success";
key: string;
module?: {
[key in N]:
| ValidPropTypes<T>
| ((url: string) => ValidPropTypes<T>)
| (new (url: string) => T);
};
}
| { type: "error"; key: string; error: Error; module?: never }
| { type: "skip"; key: string; module?: never; error?: never };
/**
* Resolves props from a module, handling both real and virtual modules.
*
* During development (ssrLoadModule):
* - Real modules have exports available directly on the module object
* - Virtual modules have exports stored in temporaryReferences
*
* During build (createBuildLoader):
* - Transformed modules (with ast/code) have exports as direct properties
* - The exports array contains just the names of those exports
* - We store the module in temporaryReferences for later use
* - We access exports directly from the module object
*
* Props can be:
* 1. A function that takes a URL and returns props
* 2. A direct object of props
* 3. A renamed export (where the actual export name differs from the expected name)
*
* @param options.propsModule - The module object from ssrLoadModule or createBuildLoader
* @param options.path - The normalized path to the module
* @param options.url - The URL route this page handles
* @param options.exportName - The name of the export to resolve (e.g. 'props')
* @param options.temporaryReferences - WeakMap used to store and retrieve virtual module references
*
* @returns A result object containing:
* - type: "success" | "error" | "skip"
* - key: The export name if successful
* - props: The resolved props if successful
* - error: Error message if failed
*/
export const resolveProps = async <
T extends PagePropOpt = PagePropOpt,
N extends string = PropsName
>({
id,
url,
exportName,
loader,
}: ResolvePropsOptions): Promise<ResolvePropsResult<T, N>> => {
// Check if this is a stashed page that needs special handling
const propsLoadResult = await (async (): Promise<
| {
type: "success";
key: string;
module: { [key in N]: ValidPropTypes<T> };
}
| { type: "error"; error: Error; module?: never }
> => {
try {
const result = await loader(`${id}#${exportName}`);
return {
type: "success",
key: id,
module: result as { [key in N]: T },
};
} catch (error) {
return {
type: "error",
error: toError(error),
};
}
})();
if (propsLoadResult.type !== "success") {
return {
key: id,
...propsLoadResult,
};
}
const { module } = propsLoadResult;
const props = module[exportName as N];
// handle different props use-cases
if (module instanceof Error) {
return {
key: id,
type: "error",
error: module,
};
} else if (!(exportName in module)) {
return {
key: id,
type: "success",
module: { [exportName]: { url, ...module } } as never,
};
} else if (!props) {
return {
key: id,
type: "success",
module: { [exportName]: { url } } as never,
};
} else if (props instanceof Error) {
return {
key: id,
type: "error",
error: props,
};
} else if (typeof props === "function") {
// Handle both class constructors and regular functions
try {
let propsResult;
if (props.prototype && props.prototype.constructor) {
// Class constructor case
propsResult = new (props as unknown as new (url: string) => T)(url);
} else {
// Regular function case
propsResult = props(url);
}
// Handle recursive props validation
if (propsResult instanceof Promise) {
propsResult = await propsResult;
}
if (typeof propsResult === "function") {
// Recursively handle nested functions
return resolveProps<T, N>({
id,
url,
exportName,
loader: async () =>
({ [exportName]: propsResult } as {
[key in N]: ValidPropTypes<T>;
}),
});
} else if (Array.isArray(propsResult)) {
// Handle array case
return {
type: "success",
key: id,
module: {
[exportName]: Object.fromEntries(
(propsResult as Array<[string, T[keyof T]]>).map((prop) =>
typeof prop === "string" ? [prop, prop] : prop
)
) as T,
} as never,
};
}
return {
type: "success",
key: id,
module: { [exportName]: propsResult } as never,
};
} catch (error) {
return {
type: "error",
key: id,
error: toError(error),
};
}
} else if (props instanceof Promise) {
// object case
try {
return {
type: "success",
key: id,
module: { [exportName]: await props } as never,
};
} catch (error) {
return {
type: "error",
key: id,
error: toError(error),
};
}
} else if (Array.isArray(props)) {
// array case
try {
return {
type: "success",
key: id,
module: {
[exportName]: Object.fromEntries(
(props as Array<[string, T[keyof T]]>).map((prop) =>
typeof prop === "string" ? [prop, prop] : prop
)
) as T,
} as never,
};
} catch (error) {
return {
type: "error",
key: id,
error: toError(error),
};
}
} else if (typeof props === "string") {
try {
return {
type: "success",
key: id,
module: { [exportName]: JSON.parse(props) } as never,
};
} catch (error) {
return {
type: "error",
key: id,
error: toError(error),
};
}
}
return {
type: "success",
key: id,
module: { [exportName]: props } as never,
};
};