2023-12-16 09:28:19 +08:00
|
|
|
import type { Context } from 'hono'
|
|
|
|
import type { Env, MiddlewareHandler } from 'hono/types'
|
|
|
|
import React from 'react'
|
2024-03-06 21:43:31 +08:00
|
|
|
import { renderToString, type RenderToReadableStreamOptions } from 'react-dom/server'
|
2023-12-16 09:28:19 +08:00
|
|
|
import type { Props } from '.'
|
|
|
|
|
|
|
|
type RendererOptions = {
|
|
|
|
docType?: boolean | string
|
|
|
|
stream?: boolean | Record<string, string>
|
2024-03-06 21:43:31 +08:00
|
|
|
readableStreamOptions?: RenderToReadableStreamOptions
|
2023-12-16 09:28:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type BaseProps = {
|
|
|
|
c: Context
|
|
|
|
children: React.ReactElement
|
|
|
|
}
|
|
|
|
|
2024-04-16 19:58:39 +08:00
|
|
|
type ComponentProps = Props &
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
BaseProps & { Layout: React.FC<Record<string, any> & { children: React.ReactElement }> }
|
|
|
|
|
2023-12-16 09:28:19 +08:00
|
|
|
const RequestContext = React.createContext<Context | null>(null)
|
|
|
|
|
|
|
|
const createRenderer =
|
2024-04-16 19:58:39 +08:00
|
|
|
(
|
|
|
|
c: Context,
|
|
|
|
Layout: React.FC<{ children: React.ReactElement }>,
|
|
|
|
component?: React.FC<ComponentProps>,
|
|
|
|
options?: RendererOptions
|
|
|
|
) =>
|
2023-12-16 09:28:19 +08:00
|
|
|
async (children: React.ReactElement, props?: Props) => {
|
2024-04-16 19:58:39 +08:00
|
|
|
const node = component ? component({ children, Layout, c, ...props }) : children
|
2023-12-16 09:28:19 +08:00
|
|
|
|
|
|
|
if (options?.stream) {
|
2023-12-17 20:51:26 +08:00
|
|
|
const { renderToReadableStream } = await import('react-dom/server')
|
2023-12-16 09:28:19 +08:00
|
|
|
const stream = await renderToReadableStream(
|
2024-03-06 21:43:31 +08:00
|
|
|
React.createElement(RequestContext.Provider, { value: c }, node),
|
|
|
|
options.readableStreamOptions
|
2023-12-16 09:28:19 +08:00
|
|
|
)
|
2024-03-17 09:40:58 +08:00
|
|
|
if (options.stream === true) {
|
|
|
|
c.header('Transfer-Encoding', 'chunked')
|
|
|
|
c.header('Content-Type', 'text/html; charset=UTF-8')
|
|
|
|
} else {
|
|
|
|
for (const [key, value] of Object.entries(options.stream)) {
|
|
|
|
c.header(key, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return c.body(stream)
|
2023-12-16 09:28:19 +08:00
|
|
|
} else {
|
|
|
|
const docType =
|
|
|
|
typeof options?.docType === 'string'
|
|
|
|
? options.docType
|
|
|
|
: options?.docType === true
|
|
|
|
? '<!DOCTYPE html>'
|
|
|
|
: ''
|
|
|
|
const body =
|
|
|
|
docType + renderToString(React.createElement(RequestContext.Provider, { value: c }, node))
|
|
|
|
return c.html(body)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const reactRenderer = (
|
2024-04-16 19:58:39 +08:00
|
|
|
component?: React.FC<ComponentProps>,
|
2023-12-16 09:28:19 +08:00
|
|
|
options?: RendererOptions
|
|
|
|
): MiddlewareHandler =>
|
|
|
|
function reactRenderer(c, next) {
|
2024-04-16 19:58:39 +08:00
|
|
|
const Layout = (c.getLayout() ?? React.Fragment) as React.FC<{
|
|
|
|
children: React.ReactElement
|
|
|
|
}>
|
|
|
|
if (component) {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
c.setLayout((props: any) => {
|
|
|
|
return component({ ...props, Layout, c }, c)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
c.setRenderer(createRenderer(c, Layout, component, options))
|
2023-12-16 09:28:19 +08:00
|
|
|
return next()
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
export const useRequestContext = <E extends Env = any>(): Context<E> => {
|
|
|
|
const c = React.useContext(RequestContext)
|
|
|
|
if (!c) {
|
|
|
|
throw new Error('RequestContext is not provided.')
|
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|