import type { Remix } from './component.ts'
/**
* Serializable primitive types that can be passed as props to hydrated components
*/
export type SerializablePrimitive = string | number | boolean | null | undefined
/**
* Serializable object types that can be passed as props to hydrated components
*/
export type SerializableObject = {
[key: string]: SerializableValue
}
/**
* Serializable array types that can be passed as props to hydrated components
*/
export type SerializableArray = SerializableValue[]
/**
* All serializable values that can be passed as props to hydrated components.
* This includes primitives, objects, arrays, and Remix Elements.
*/
export type SerializableValue =
| SerializablePrimitive
| SerializableObject
| SerializableArray
| Remix.RemixNode
/**
* Props that can be serialized and sent to hydrated components.
* All values must be serializable (primitives, objects, arrays, or Remix Elements).
*/
export type SerializableProps = {
[K in string]: SerializableValue
}
// Whitelist of allowed simple component return types (non-function)
type SimpleRenderable = Element | string | number | bigint | boolean | null | undefined
/**
* Metadata added to hydrated components
*/
export type HydrationMetadata = {
$hydrated: true
$moduleUrl: string
$exportName: string
}
/**
* A hydrated component preserves the exact function type with added metadata
*/
export type HydratedComponent<
H extends (this: Remix.Handle, props: any) => any = (this: Remix.Handle, props: any) => any,
> = H & HydrationMetadata
/**
* Marks a component for client-side hydration.
*
* @param href Module URL with optional export name (format: "/js/module.js#ExportName")
* @param component Component function that will be hydrated on the client
* @returns The component augmented with hydration metadata
*
* @example
* ```tsx
* export const Counter = hydrated(
* '/js/counter.js#Counter',
* function (this: Handle, { initialCount }: { initialCount: number }) {
* let count = initialCount
*
* return ({ label }: { label: string }) => (
*
* )
* }
* )
* ```
*/
// Overload for stateful components (setup returns render function)
export function hydrated<
SetupProps extends SerializableProps = {},
RenderProps extends SerializableProps = {},
>(
href: string,
component: (this: Remix.Handle, props: SetupProps) => (props: RenderProps) => Remix.RemixNode,
): HydratedComponent<
(this: Remix.Handle, props: SetupProps) => (props: RenderProps) => Remix.RemixNode
>
// Overload for simple components (setup returns JSX directly)
export function hydrated<
Props extends SerializableProps = {},
Render extends SimpleRenderable = SimpleRenderable,
>(
href: string,
component: (this: Remix.Handle, props: Props) => Render,
): HydratedComponent<(this: Remix.Handle, props: Props) => Render>
// Implementation
export function hydrated(href: string, component: any): any {
// Parse module URL and export name
let [moduleUrl, exportName] = href.split('#')
if (!moduleUrl) {
throw new Error('hydrated() requires a module URL')
}
// Use component name as fallback if no export name provided
let finalExportName = exportName || component.name
if (!finalExportName) {
throw new Error(
'hydrated() requires either an export name in the href (e.g., "/js/module.js#ComponentName") or a named component function',
)
}
// Augment the component with hydration metadata
component.$hydrated = true
component.$moduleUrl = moduleUrl
component.$exportName = finalExportName
return component
}
/**
* Type guard to check if a component is hydrated
*/
export function isHydratedComponent any>(
component: any,
): component is HydratedComponent {
return Boolean(component && typeof component === 'function' && component.$hydrated === true)
}