mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: load critical CSS on development (#765)
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import type { Mode } from '../types'
|
||||
|
||||
const VITE_PROXY_PATH = '/vite-server'
|
||||
const CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/tuono_internal__critical_css'
|
||||
|
||||
interface CriticalCssProps {
|
||||
routeId?: string
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the critical CSS for the given route
|
||||
* This is required in order to avoid FOUC during development
|
||||
* since vite does not support CSS injection without JS waterfall
|
||||
*/
|
||||
export function CriticalCss({
|
||||
routeId,
|
||||
mode,
|
||||
}: CriticalCssProps): JSX.Element | null {
|
||||
if (!routeId || mode !== 'Dev') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<link
|
||||
href={`${CRITICAL_CSS_PATH}?componentId=${routeId}`}
|
||||
precedence="high"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import type { JSX } from 'react'
|
||||
|
||||
import { useRoute } from '../hooks/useRoute'
|
||||
|
||||
import type { Mode } from '../types'
|
||||
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
import { NotFound } from './NotFound'
|
||||
import { useRouterContext } from './RouterContext'
|
||||
@@ -9,16 +11,26 @@ import { useRouterContext } from './RouterContext'
|
||||
interface MatchesProps<TServerPayloadData = unknown> {
|
||||
// user defined props
|
||||
serverInitialData: TServerPayloadData
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
export function Matches({ serverInitialData }: MatchesProps): JSX.Element {
|
||||
export function Matches({
|
||||
serverInitialData,
|
||||
mode,
|
||||
}: MatchesProps): JSX.Element {
|
||||
const { location } = useRouterContext()
|
||||
|
||||
const route = useRoute(location.pathname)
|
||||
|
||||
if (!route) {
|
||||
return <NotFound />
|
||||
return <NotFound mode={mode} />
|
||||
}
|
||||
|
||||
return <RouteMatch route={route} serverInitialData={serverInitialData} />
|
||||
return (
|
||||
<RouteMatch
|
||||
route={route}
|
||||
mode={mode}
|
||||
serverInitialData={serverInitialData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,17 +3,25 @@ import type { JSX } from 'react'
|
||||
import { useRouterContext } from '../components/RouterContext'
|
||||
import { ROOT_ROUTE_ID } from '../route'
|
||||
|
||||
import type { Mode } from '../types'
|
||||
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
import { NotFoundDefaultContent } from './NotFoundDefaultContent'
|
||||
import { CriticalCss } from './CriticalCss'
|
||||
|
||||
export function NotFound(): JSX.Element | null {
|
||||
export function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {
|
||||
const { router } = useRouterContext()
|
||||
|
||||
const custom404Route = router.routesById['/404']
|
||||
|
||||
// Check if exists a custom 404 error page
|
||||
if (custom404Route) {
|
||||
return <RouteMatch route={custom404Route} serverInitialData={{}} />
|
||||
return (
|
||||
<>
|
||||
<CriticalCss routeId={custom404Route.id} mode={mode} />
|
||||
<RouteMatch route={custom404Route} mode={mode} serverInitialData={{}} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const RootLayout = router.routesById[ROOT_ROUTE_ID]?.component
|
||||
@@ -22,6 +30,7 @@ export function NotFound(): JSX.Element | null {
|
||||
|
||||
return (
|
||||
<RootLayout data={null} isLoading={false}>
|
||||
<CriticalCss routeId="__root__" mode={mode} />
|
||||
<NotFoundDefaultContent />
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { JSX } from 'react'
|
||||
import { memo, Suspense, useMemo } from 'react'
|
||||
|
||||
import type { Mode } from '../types'
|
||||
import type { Route } from '../route'
|
||||
|
||||
import { useServerPayloadData } from '../hooks/useServerPayloadData'
|
||||
|
||||
import { useRouterContext } from './RouterContext'
|
||||
import { CriticalCss } from './CriticalCss'
|
||||
|
||||
interface RouteMatchProps<TServerPayloadData = unknown> {
|
||||
route: Route
|
||||
// User defined server side props
|
||||
serverInitialData: TServerPayloadData
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,6 +24,7 @@ interface RouteMatchProps<TServerPayloadData = unknown> {
|
||||
export const RouteMatch = ({
|
||||
route,
|
||||
serverInitialData,
|
||||
mode,
|
||||
}: RouteMatchProps): JSX.Element => {
|
||||
const { data } = useServerPayloadData(route, serverInitialData)
|
||||
const { isTransitioning } = useRouterContext()
|
||||
@@ -35,8 +39,10 @@ export const RouteMatch = ({
|
||||
routes={routes}
|
||||
data={routeData}
|
||||
isLoading={isTransitioning}
|
||||
mode={mode}
|
||||
>
|
||||
<Suspense>
|
||||
<CriticalCss routeId={route.id} mode={mode} />
|
||||
<route.component data={routeData} isLoading={isTransitioning} />
|
||||
</Suspense>
|
||||
</TraverseRootComponents>
|
||||
@@ -49,6 +55,7 @@ interface TraverseRootComponentsProps<TData = unknown> {
|
||||
isLoading: boolean
|
||||
children?: React.ReactNode
|
||||
index?: number
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,18 +70,22 @@ const TraverseRootComponents = memo(
|
||||
data,
|
||||
isLoading,
|
||||
index = 0,
|
||||
mode,
|
||||
children,
|
||||
}: TraverseRootComponentsProps): React.JSX.Element => {
|
||||
if (routes.length > index) {
|
||||
const Parent = (routes[index] as Route).component
|
||||
const route = routes[index] as Route
|
||||
const Parent = route.component
|
||||
|
||||
return (
|
||||
<Parent data={data} isLoading={isLoading}>
|
||||
<CriticalCss routeId={route.id} mode={mode} />
|
||||
<TraverseRootComponents
|
||||
routes={routes}
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
index={index + 1}
|
||||
mode={mode}
|
||||
>
|
||||
{children}
|
||||
</TraverseRootComponents>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import type { ServerInitialLocation } from '../types'
|
||||
import type { ServerInitialLocation, Mode } from '../types'
|
||||
import type { Router } from '../router'
|
||||
|
||||
import { RouterContextProvider } from './RouterContext'
|
||||
@@ -10,19 +10,21 @@ interface RouterProviderProps {
|
||||
router: Router
|
||||
serverInitialLocation: ServerInitialLocation
|
||||
serverInitialData: unknown
|
||||
mode?: Mode
|
||||
}
|
||||
|
||||
export function RouterProvider({
|
||||
router,
|
||||
serverInitialLocation,
|
||||
serverInitialData,
|
||||
mode,
|
||||
}: RouterProviderProps): JSX.Element {
|
||||
return (
|
||||
<RouterContextProvider
|
||||
router={router}
|
||||
serverInitialLocation={serverInitialLocation}
|
||||
>
|
||||
<Matches serverInitialData={serverInitialData} />
|
||||
<Matches serverInitialData={serverInitialData} mode={mode} />
|
||||
</RouterContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ export function sanitizePathname(pathname: string): string {
|
||||
return pathname
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Returns the route that matches the given pathname
|
||||
*
|
||||
* This hook is also implemented on server side to match the bundle
|
||||
* file to load at the first rendering.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ReactNode, ComponentType } from 'react'
|
||||
|
||||
export type Mode = 'Dev' | 'Prod'
|
||||
|
||||
export interface Segment {
|
||||
type: 'pathname' | 'param' | 'wildcard'
|
||||
value: string
|
||||
|
||||
Reference in New Issue
Block a user