mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
refactor: create TuonoContext (#413)
This commit is contained in:
committed by
GitHub
parent
d5cc65983e
commit
dcedb1c6c0
@@ -16,8 +16,8 @@ fn has_dynamic_path(route: &str) -> bool {
|
||||
#[derive(SerdeSerialize)]
|
||||
/// This is the payload sent to the client for hydration
|
||||
pub struct Payload<'a> {
|
||||
router: Location,
|
||||
props: &'a dyn Serialize,
|
||||
location: Location,
|
||||
data: &'a dyn Serialize,
|
||||
mode: Mode,
|
||||
#[serde(rename(serialize = "jsBundles"))]
|
||||
js_bundles: Option<Vec<&'a String>>,
|
||||
@@ -28,7 +28,7 @@ pub struct Payload<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Payload<'a> {
|
||||
pub fn new(req: &'a Request, props: &'a dyn Serialize) -> Payload<'a> {
|
||||
pub fn new(req: &'a Request, data: &'a dyn Serialize) -> Payload<'a> {
|
||||
let config = GLOBAL_CONFIG
|
||||
.get()
|
||||
.expect("Failed to load the current config");
|
||||
@@ -42,8 +42,8 @@ impl<'a> Payload<'a> {
|
||||
};
|
||||
|
||||
Payload {
|
||||
router: req.location(),
|
||||
props,
|
||||
location: req.location(),
|
||||
data,
|
||||
mode,
|
||||
js_bundles: None,
|
||||
css_bundles: None,
|
||||
@@ -79,7 +79,7 @@ impl<'a> Payload<'a> {
|
||||
let mut js_bundles_sources = vec![&main_bundle.file];
|
||||
let mut css_bundles_sources = main_bundle.css.iter().collect::<Vec<&String>>();
|
||||
|
||||
let pathname = &self.router.pathname();
|
||||
let pathname = &self.location.pathname();
|
||||
|
||||
let bundle_data = manifest.get(*pathname);
|
||||
|
||||
@@ -233,8 +233,8 @@ mod tests {
|
||||
let location = Location::from(uri);
|
||||
|
||||
Payload {
|
||||
router: location,
|
||||
props: &None::<Option<()>>,
|
||||
location,
|
||||
data: &None::<Option<()>>,
|
||||
mode,
|
||||
js_bundles: None,
|
||||
css_bundles: None,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type * as React from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
|
||||
import { useRouter } from '../hooks/useRouter'
|
||||
import useRoute from '../hooks/useRoute'
|
||||
import { useRoute } from '../hooks/useRoute'
|
||||
|
||||
interface TuonoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import type * as React from 'react'
|
||||
|
||||
import useRoute from '../hooks/useRoute'
|
||||
import { useRoute } from '../hooks/useRoute'
|
||||
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
import NotFound from './NotFound'
|
||||
import { useRouterContext } from './RouterContext'
|
||||
|
||||
interface MatchesProps<TServerSideProps = unknown> {
|
||||
interface MatchesProps<TServerPayloadData = unknown> {
|
||||
// user defined props
|
||||
serverSideProps: TServerSideProps
|
||||
serverInitialData: TServerPayloadData
|
||||
}
|
||||
|
||||
export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
|
||||
export function Matches({
|
||||
serverInitialData,
|
||||
}: MatchesProps): React.JSX.Element {
|
||||
const { location } = useRouterContext()
|
||||
|
||||
const route = useRoute(location.pathname)
|
||||
@@ -20,5 +22,5 @@ export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
|
||||
return <NotFound />
|
||||
}
|
||||
|
||||
return <RouteMatch route={route} serverSideProps={serverSideProps} />
|
||||
return <RouteMatch route={route} serverInitialData={serverInitialData} />
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function NotFound(): React.JSX.Element {
|
||||
|
||||
// Check if exists a custom 404 error page
|
||||
if (custom404Route) {
|
||||
return <RouteMatch route={custom404Route} serverSideProps={{}} />
|
||||
return <RouteMatch route={custom404Route} serverInitialData={{}} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { cleanup, render, screen } from '@testing-library/react'
|
||||
import type { Route } from '../route'
|
||||
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
interface Props {
|
||||
@@ -41,8 +42,8 @@ describe('Test RouteMatch component', () => {
|
||||
})
|
||||
|
||||
test('It should correctly render nested routes', () => {
|
||||
vi.mock('../hooks/useServerSideProps.tsx', () => ({
|
||||
useServerSideProps: (): { data: unknown; isLoading: boolean } => {
|
||||
vi.mock('../hooks/useServerPayloadData.ts', () => ({
|
||||
useServerPayloadData: (): { data: unknown; isLoading: boolean } => {
|
||||
return {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
@@ -50,7 +51,7 @@ describe('Test RouteMatch component', () => {
|
||||
},
|
||||
}))
|
||||
|
||||
render(<RouteMatch route={route} serverSideProps={{}} />)
|
||||
render(<RouteMatch route={route} serverInitialData={{}} />)
|
||||
expect(screen.getByTestId('root')).toHaveTextContent(
|
||||
'root route parent route current route',
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import type { Route } from '../route'
|
||||
import { useServerSideProps } from '../hooks/useServerSideProps'
|
||||
import { useServerPayloadData } from '../hooks/useServerPayloadData'
|
||||
|
||||
interface RouteMatchProps<TServerSideProps = unknown> {
|
||||
interface RouteMatchProps<TServerPayloadData = unknown> {
|
||||
route: Route
|
||||
// User defined server side props
|
||||
serverSideProps: TServerSideProps
|
||||
serverInitialData: TServerPayloadData
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,9 +16,9 @@ interface RouteMatchProps<TServerSideProps = unknown> {
|
||||
*/
|
||||
export const RouteMatch = ({
|
||||
route,
|
||||
serverSideProps,
|
||||
serverInitialData: serverInitialData,
|
||||
}: RouteMatchProps): React.JSX.Element => {
|
||||
const { data, isLoading } = useServerSideProps(route, serverSideProps)
|
||||
const { data, isLoading } = useServerPayloadData(route, serverInitialData)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const routes = React.useMemo(() => loadParentComponents(route), [route.id])
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createContext, useState, useEffect, useContext, useMemo } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import type { Router } from '../router'
|
||||
import type { ServerRouterInfo, ServerProps } from '../types'
|
||||
import type { ServerInitialLocation } from '../types'
|
||||
|
||||
const isServerSide = typeof window === 'undefined'
|
||||
|
||||
@@ -17,53 +17,51 @@ export interface ParsedLocation {
|
||||
interface RouterContextValue {
|
||||
router: Router
|
||||
location: ParsedLocation
|
||||
serverSideProps?: ServerProps
|
||||
updateLocation: (loc: ParsedLocation) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const RouterContext = createContext<RouterContextValue>(null!)
|
||||
const RouterContext = createContext({} as RouterContextValue)
|
||||
|
||||
function getInitialLocation(
|
||||
serverSideProps?: ServerRouterInfo,
|
||||
serverPayloadLocation: ServerInitialLocation,
|
||||
): ParsedLocation {
|
||||
if (typeof document === 'undefined') {
|
||||
if (isServerSide) {
|
||||
return {
|
||||
pathname: serverSideProps?.pathname || '',
|
||||
pathname: serverPayloadLocation.pathname || '',
|
||||
hash: '',
|
||||
href: serverSideProps?.href || '',
|
||||
searchStr: serverSideProps?.searchStr || '',
|
||||
href: serverPayloadLocation.href || '',
|
||||
searchStr: serverPayloadLocation.searchStr || '',
|
||||
// TODO: Polyfill URLSearchParams
|
||||
search: {},
|
||||
}
|
||||
}
|
||||
|
||||
const { location } = window
|
||||
const { pathname, hash, href, search } = window.location
|
||||
return {
|
||||
pathname: location.pathname,
|
||||
hash: location.hash,
|
||||
href: location.href,
|
||||
searchStr: location.search,
|
||||
search: Object.fromEntries(new URLSearchParams(location.search)),
|
||||
pathname,
|
||||
hash,
|
||||
href,
|
||||
searchStr: search,
|
||||
search: Object.fromEntries(new URLSearchParams(search)),
|
||||
}
|
||||
}
|
||||
|
||||
interface RouterContextProviderProps {
|
||||
router: Router
|
||||
serverInitialLocation: ServerInitialLocation
|
||||
children: ReactNode
|
||||
serverSideProps?: ServerProps
|
||||
}
|
||||
|
||||
export function RouterContextProvider({
|
||||
router,
|
||||
serverInitialLocation,
|
||||
children,
|
||||
serverSideProps,
|
||||
}: RouterContextProviderProps): ReactNode {
|
||||
// Allow the router to update options on the router instance
|
||||
router.update({ ...router.options } as Parameters<typeof router.update>[0])
|
||||
|
||||
const [location, setLocation] = useState<ParsedLocation>(() =>
|
||||
getInitialLocation(serverSideProps?.router),
|
||||
getInitialLocation(serverInitialLocation),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -94,14 +92,11 @@ export function RouterContextProvider({
|
||||
|
||||
const contextValue: RouterContextValue = useMemo(
|
||||
() => ({
|
||||
serverSideProps: isServerSide
|
||||
? serverSideProps
|
||||
: window.__TUONO_SSR_PROPS__,
|
||||
router,
|
||||
location,
|
||||
updateLocation: setLocation,
|
||||
}),
|
||||
[location, router, serverSideProps],
|
||||
[location, router],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -111,6 +106,9 @@ export function RouterContextProvider({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @warning THIS SHOULD NOT BE EXPOSED TO USERLAND
|
||||
*/
|
||||
export function useRouterContext(): RouterContextValue {
|
||||
return useContext(RouterContext)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import type { ServerProps } from '../types'
|
||||
import type { ServerInitialLocation } from '../types'
|
||||
import type { Router } from '../router'
|
||||
|
||||
import { RouterContextProvider } from './RouterContext'
|
||||
@@ -8,19 +8,21 @@ import { Matches } from './Matches'
|
||||
|
||||
interface RouterProviderProps {
|
||||
router: Router
|
||||
serverProps?: ServerProps
|
||||
serverInitialLocation: ServerInitialLocation
|
||||
serverInitialData: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is used in the tuono app entry point
|
||||
*/
|
||||
export function RouterProvider({
|
||||
router,
|
||||
serverProps,
|
||||
serverInitialLocation,
|
||||
serverInitialData,
|
||||
}: RouterProviderProps): JSX.Element {
|
||||
return (
|
||||
<RouterContextProvider router={router} serverSideProps={serverProps}>
|
||||
<Matches serverSideProps={serverProps?.props} />
|
||||
<RouterContextProvider
|
||||
router={router}
|
||||
serverInitialLocation={serverInitialLocation}
|
||||
>
|
||||
<Matches serverInitialData={serverInitialData} />
|
||||
</RouterContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { Router } from './router'
|
||||
import type { ServerProps } from './types'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TUONO__ROUTER__: Router
|
||||
__TUONO_SSR_PROPS__?: ServerProps
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
|
||||
import useRoute from './useRoute'
|
||||
import { useRoute } from './useRoute'
|
||||
|
||||
describe('useRoute', () => {
|
||||
afterEach(cleanup)
|
||||
+2
-2
@@ -22,9 +22,9 @@ export function sanitizePathname(pathname: string): string {
|
||||
*
|
||||
* File: crates/tuono_lib/src/payload.rs
|
||||
*
|
||||
* Optimizations should occour on both
|
||||
* Optimizations should occur on both
|
||||
*/
|
||||
export default function useRoute(pathname?: string): Route | undefined {
|
||||
export function useRoute(pathname?: string): Route | undefined {
|
||||
const {
|
||||
router: { routesById },
|
||||
} = useRouterContext()
|
||||
+13
-13
@@ -7,11 +7,6 @@ import { useRouterContext } from '../components/RouterContext'
|
||||
|
||||
const isServer = typeof document === 'undefined'
|
||||
|
||||
interface UseServerSidePropsReturn<TData> {
|
||||
data: TData
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
interface TuonoApi {
|
||||
data?: unknown
|
||||
info: {
|
||||
@@ -25,17 +20,22 @@ const fetchClientSideData = async (): Promise<TuonoApi> => {
|
||||
return data
|
||||
}
|
||||
|
||||
interface UseServerPayloadDataResult<TData> {
|
||||
data: TData
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
* Use the props provided by the SSR and dehydrate the
|
||||
* props for client side usage.
|
||||
*
|
||||
* In case is a client fetch the remote data API
|
||||
*/
|
||||
export function useServerSideProps<T>(
|
||||
export function useServerPayloadData<TServerPayloadData>(
|
||||
route: Route,
|
||||
// User defined props
|
||||
serverSideProps: T,
|
||||
): UseServerSidePropsReturn<T> {
|
||||
// User defined data
|
||||
serverInitialData: TServerPayloadData,
|
||||
): UseServerPayloadDataResult<TServerPayloadData> {
|
||||
const isFirstRendering = useRef<boolean>(true)
|
||||
const { location, updateLocation } = useRouterContext()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(
|
||||
@@ -47,8 +47,8 @@ export function useServerSideProps<T>(
|
||||
!isFirstRendering.current,
|
||||
)
|
||||
|
||||
const [data, setData] = useState<T | undefined>(
|
||||
(serverSideProps ?? window.__TUONO_SSR_PROPS__?.props) as T,
|
||||
const [data, setData] = useState<TServerPayloadData | undefined>(
|
||||
serverInitialData,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,7 +80,7 @@ export function useServerSideProps<T>(
|
||||
updateLocation(parsedLocation)
|
||||
return
|
||||
}
|
||||
setData(response.data as T)
|
||||
setData(response.data as TServerPayloadData)
|
||||
} catch (error) {
|
||||
throw Error('Failed loading Server Side Data', { cause: error })
|
||||
} finally {
|
||||
@@ -95,5 +95,5 @@ export function useServerSideProps<T>(
|
||||
}
|
||||
}, [location.pathname, route.options.hasHandler, updateLocation])
|
||||
|
||||
return { isLoading, data: data as T }
|
||||
return { isLoading, data: data as TServerPayloadData }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export { RouterProvider } from './components/RouterProvider'
|
||||
export { useRouterContext } from './components/RouterContext'
|
||||
export { default as Link } from './components/Link'
|
||||
export { createRouter, type RouterType } from './router'
|
||||
export { createRouter } from './router'
|
||||
export type { RouterInstanceType } from './router'
|
||||
export { createRoute, createRootRoute } from './route'
|
||||
export { useRouter } from './hooks/useRouter'
|
||||
export type { RouteProps, RouteComponent, ServerProps } from './types'
|
||||
export type { RouteProps, RouteComponent } from './types'
|
||||
|
||||
@@ -21,7 +21,8 @@ export function createRouter(options: CreateRouterOptions): Router {
|
||||
return new Router(options)
|
||||
}
|
||||
|
||||
export type RouterType = InstanceType<typeof Router>
|
||||
export type RouterInstanceType = InstanceType<typeof Router>
|
||||
|
||||
export class Router {
|
||||
options?: RouterOptions
|
||||
basePath = '/'
|
||||
|
||||
@@ -5,26 +5,19 @@ export interface Segment {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface ServerRouterInfo {
|
||||
/**
|
||||
* Provided by the rust server and used in the ssr env
|
||||
* @see tuono {@link ServerPayloadLocation}
|
||||
*/
|
||||
export interface ServerInitialLocation {
|
||||
href: string
|
||||
pathname: string
|
||||
searchStr: string
|
||||
}
|
||||
|
||||
export interface ServerProps<TProps = unknown> {
|
||||
router: ServerRouterInfo
|
||||
props: TProps
|
||||
jsBundles: Array<string>
|
||||
cssBundles: Array<string>
|
||||
mode: 'Dev' | 'Prod'
|
||||
devServerConfig: {
|
||||
port: number
|
||||
host: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RouteProps<TData = unknown> {
|
||||
data: TData
|
||||
|
||||
isLoading: boolean
|
||||
|
||||
children?: ReactNode
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const SERVER_PAYLOAD_VARIABLE_NAME = '__TUONO_SERVER_PAYLOAD__'
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import type { ServerPayload } from './types'
|
||||
import type { SERVER_PAYLOAD_VARIABLE_NAME } from './constants'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
[SERVER_PAYLOAD_VARIABLE_NAME]?: ServerPayload
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { JSX } from 'react'
|
||||
import { useRouterContext } from 'tuono-router'
|
||||
|
||||
import { useTuonoContextServerPayload } from './TuonoContext'
|
||||
|
||||
const VITE_PROXY_PATH = '/vite-server'
|
||||
const DEFAULT_SERVER_CONFIG = { host: 'localhost', port: 3000 }
|
||||
|
||||
export const DevResources = (): JSX.Element => {
|
||||
const { serverSideProps } = useRouterContext()
|
||||
const { host, port } =
|
||||
serverSideProps?.devServerConfig ?? DEFAULT_SERVER_CONFIG
|
||||
const { devServerConfig } = useTuonoContextServerPayload()
|
||||
const { host, port } = devServerConfig ?? DEFAULT_SERVER_CONFIG
|
||||
|
||||
const viteBaseUrl = `http://${host}:${port}${VITE_PROXY_PATH}`
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { JSX } from 'react'
|
||||
import { useRouterContext } from 'tuono-router'
|
||||
|
||||
import { useTuonoContextServerPayload } from './TuonoContext'
|
||||
|
||||
export const ProdResources = (): JSX.Element => {
|
||||
const { serverSideProps } = useRouterContext()
|
||||
const { cssBundles, jsBundles } = useTuonoContextServerPayload()
|
||||
|
||||
return (
|
||||
<>
|
||||
{serverSideProps?.cssBundles.map((cssHref) => (
|
||||
{cssBundles?.map((cssHref) => (
|
||||
<link
|
||||
key={cssHref}
|
||||
rel="stylesheet"
|
||||
@@ -16,7 +17,7 @@ export const ProdResources = (): JSX.Element => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{serverSideProps?.jsBundles.map((scriptSrc) => (
|
||||
{jsBundles?.map((scriptSrc) => (
|
||||
<script key={scriptSrc} type="module" src={`/${scriptSrc}`}></script>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { JSX } from 'react'
|
||||
import { RouterProvider } from 'tuono-router'
|
||||
import type { RouterInstanceType } from 'tuono-router'
|
||||
|
||||
import { useTuonoContextServerPayload } from './TuonoContext'
|
||||
|
||||
interface RouterContextProviderWrapperProps {
|
||||
router: RouterInstanceType
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is needed to get the data from {@link TuonoContext}
|
||||
* since the provider is also located in {@link TuonoEntryPoint}
|
||||
* hence the context cannot be accessed directly there
|
||||
*
|
||||
* @see https://github.com/tuono-labs/tuono/issues/410
|
||||
*/
|
||||
export function RouterContextProviderWrapper({
|
||||
router,
|
||||
}: RouterContextProviderWrapperProps): JSX.Element {
|
||||
const serverPayload = useTuonoContextServerPayload()
|
||||
|
||||
return (
|
||||
<RouterProvider
|
||||
router={router}
|
||||
serverInitialLocation={serverPayload.location}
|
||||
serverInitialData={serverPayload.data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { JSX, ReactNode } from 'react'
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
|
||||
import type { ServerPayload } from '../types'
|
||||
import { SERVER_PAYLOAD_VARIABLE_NAME } from '../constants'
|
||||
|
||||
const isServerSide = typeof window === 'undefined'
|
||||
|
||||
interface TuonoContextValue {
|
||||
serverPayload: ServerPayload
|
||||
}
|
||||
|
||||
const TuonoContext = createContext({} as TuonoContextValue)
|
||||
|
||||
interface TuonoContextProviderProps {
|
||||
serverPayload?: ServerPayload
|
||||
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @warning THIS SHOULD NOT BE EXPOSED TO USERLAND
|
||||
*
|
||||
* @see https://github.com/tuono-labs/tuono/issues/410
|
||||
*/
|
||||
export function TuonoContextProvider({
|
||||
serverPayload,
|
||||
children,
|
||||
}: TuonoContextProviderProps): JSX.Element {
|
||||
const contextValue: TuonoContextValue = useMemo(() => {
|
||||
// At least one of these two should be defined
|
||||
const _serverPayload = (
|
||||
isServerSide ? serverPayload : window[SERVER_PAYLOAD_VARIABLE_NAME]
|
||||
) as ServerPayload
|
||||
|
||||
return {
|
||||
// Maybe this logic should be integrated using defaults
|
||||
serverPayload: _serverPayload,
|
||||
}
|
||||
}, [serverPayload])
|
||||
|
||||
return <TuonoContext value={contextValue}>{children}</TuonoContext>
|
||||
}
|
||||
|
||||
/**
|
||||
* @warning THIS SHOULD NOT BE EXPOSED TO USERLAND
|
||||
*/
|
||||
export function useTuonoContextServerPayload(): TuonoContextValue['serverPayload'] {
|
||||
return useContext(TuonoContext).serverPayload
|
||||
}
|
||||
@@ -1,18 +1,26 @@
|
||||
import { type JSX, StrictMode } from 'react'
|
||||
import { type RouterType, type ServerProps, RouterProvider } from 'tuono-router'
|
||||
import { StrictMode } from 'react'
|
||||
import type { JSX } from 'react'
|
||||
import type { RouterInstanceType } from 'tuono-router'
|
||||
|
||||
import type { ServerPayload } from '../types'
|
||||
|
||||
import { TuonoContextProvider } from './TuonoContext'
|
||||
import { RouterContextProviderWrapper } from './RouterContextProviderWrapper'
|
||||
|
||||
interface TuonoEntryPointProps {
|
||||
router: RouterType
|
||||
serverProps?: ServerProps
|
||||
router: RouterInstanceType
|
||||
serverPayload?: ServerPayload
|
||||
}
|
||||
|
||||
export function TuonoEntryPoint({
|
||||
router,
|
||||
serverProps,
|
||||
serverPayload,
|
||||
}: TuonoEntryPointProps): JSX.Element {
|
||||
return (
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} serverProps={serverProps} />
|
||||
<TuonoContextProvider serverPayload={serverPayload}>
|
||||
<RouterContextProviderWrapper router={router} />
|
||||
</TuonoContextProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import { useRouterContext } from 'tuono-router'
|
||||
import { SERVER_PAYLOAD_VARIABLE_NAME } from '../constants'
|
||||
|
||||
import { DevResources } from './DevResources'
|
||||
import { ProdResources } from './ProdResources'
|
||||
import { useTuonoContextServerPayload } from './TuonoContext'
|
||||
|
||||
export function TuonoScripts(): JSX.Element {
|
||||
const { serverSideProps } = useRouterContext()
|
||||
const serverPayload = useTuonoContextServerPayload()
|
||||
|
||||
return (
|
||||
<>
|
||||
<script>{`window.__TUONO_SSR_PROPS__=${JSON.stringify(serverSideProps)}`}</script>
|
||||
{serverSideProps?.mode === 'Dev' && <DevResources />}
|
||||
{serverSideProps?.mode === 'Prod' && <ProdResources />}
|
||||
<script>{`window['${SERVER_PAYLOAD_VARIABLE_NAME}']=${JSON.stringify(serverPayload)}`}</script>
|
||||
{serverPayload.mode === 'Dev' && <DevResources />}
|
||||
{serverPayload.mode === 'Prod' && <ProdResources />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,9 +42,10 @@ import type { ReadableStream } from 'node:stream/web'
|
||||
|
||||
import { renderToReadableStream } from 'react-dom/server'
|
||||
import { createRouter } from 'tuono-router'
|
||||
import type { createRoute, ServerProps } from 'tuono-router'
|
||||
import type { createRoute } from 'tuono-router'
|
||||
|
||||
import { TuonoEntryPoint } from '../shared/TuonoEntryPoint'
|
||||
import type { ServerPayload } from '../types'
|
||||
|
||||
import { streamToString } from './utils'
|
||||
|
||||
@@ -52,12 +53,12 @@ type RouteTree = ReturnType<typeof createRoute>
|
||||
|
||||
export function serverSideRendering(routeTree: RouteTree) {
|
||||
return async function render(payload: string | undefined): Promise<string> {
|
||||
const serverProps = (payload ? JSON.parse(payload) : {}) as ServerProps
|
||||
const serverPayload = (payload ? JSON.parse(payload) : {}) as ServerPayload
|
||||
|
||||
const router = createRouter({ routeTree }) // Render the app
|
||||
|
||||
const stream = await renderToReadableStream(
|
||||
<TuonoEntryPoint router={router} serverProps={serverProps} />,
|
||||
<TuonoEntryPoint router={router} serverPayload={serverPayload} />,
|
||||
)
|
||||
|
||||
await stream.allReady
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
/**
|
||||
* Provided by the rust server and used in the ssr env
|
||||
* @see tuono-router {@link ServerInitialLocation}
|
||||
*/
|
||||
export interface ServerPayloadLocation {
|
||||
href: string
|
||||
pathname: string
|
||||
searchStr: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @see crates/tuono_lib/src/payload.rs
|
||||
*/
|
||||
export interface ServerPayload<TData = unknown> {
|
||||
mode: 'Prod' | 'Dev'
|
||||
|
||||
location: ServerPayloadLocation
|
||||
|
||||
data: TData
|
||||
|
||||
/** Available only on 'Prod' mode */
|
||||
jsBundles: Array<string> | null
|
||||
cssBundles: Array<string> | null
|
||||
|
||||
/** Available only on 'Dev' mode */
|
||||
devServerConfig?: {
|
||||
port: number
|
||||
host: string
|
||||
}
|
||||
}
|
||||
|
||||
/* the above type could be refined with an union like this
|
||||
(
|
||||
| {
|
||||
mode: 'Prod'
|
||||
jsBundles: Array<string>
|
||||
cssBundles: Array<string>
|
||||
}
|
||||
| {
|
||||
mode: 'Dev'
|
||||
devServerConfig: {
|
||||
port: number
|
||||
host: string
|
||||
}
|
||||
}
|
||||
)
|
||||
*/
|
||||
|
||||
export interface TuonoProps<T> {
|
||||
data?: T
|
||||
isLoading: boolean
|
||||
|
||||
Reference in New Issue
Block a user