mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
fix: improve critical css generation (#770)
This commit is contained in:
@@ -207,6 +207,7 @@ export async function routeGenerator(
|
|||||||
rustHandlersNodes.includes(node.path || '')
|
rustHandlersNodes.includes(node.path || '')
|
||||||
? 'hasHandler: true'
|
? 'hasHandler: true'
|
||||||
: '',
|
: '',
|
||||||
|
`filePath: '${node.path || '/'}'`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(',')}
|
.join(',')}
|
||||||
|
|||||||
@@ -46,6 +46,94 @@ const isCssUrlWithoutSideEffects = (url: string): boolean => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizePath = (modulePath: string): string => {
|
||||||
|
return modulePath.startsWith('node_modules')
|
||||||
|
? path.join(process.cwd(), modulePath)
|
||||||
|
: modulePath
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStylesForModule = async (
|
||||||
|
viteDevServer: ViteDevServer,
|
||||||
|
moduleUrl: string,
|
||||||
|
/**
|
||||||
|
* All the CSS modules are preloaded and saved in this manifest
|
||||||
|
*/
|
||||||
|
cssModulesManifest: Record<string, string>,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const styles: Record<string, string> = {}
|
||||||
|
const deps: Set<ModuleNode> = new Set()
|
||||||
|
|
||||||
|
const moduleFilePath = normalizePath(moduleUrl)
|
||||||
|
try {
|
||||||
|
let node: ModuleNode | undefined =
|
||||||
|
await viteDevServer.moduleGraph.getModuleByUrl(moduleFilePath)
|
||||||
|
|
||||||
|
// If the module is only present in the client module graph, the module
|
||||||
|
// won't have been found on the first request to the server. If so, we
|
||||||
|
// request the module so it's in the module graph, then try again.
|
||||||
|
if (!node) {
|
||||||
|
try {
|
||||||
|
await viteDevServer.transformRequest(moduleFilePath)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
node = await viteDevServer.moduleGraph.getModuleByUrl(moduleFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
console.error(`Could not resolve module for file: ${moduleFilePath}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await findNodeDependencies(viteDevServer, node, deps)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dep of deps) {
|
||||||
|
if (
|
||||||
|
dep.file &&
|
||||||
|
isCssFile(dep.file) &&
|
||||||
|
!isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const css = isCssModulesFile(dep.file)
|
||||||
|
? cssModulesManifest[dep.file]
|
||||||
|
: ((
|
||||||
|
await viteDevServer.ssrLoadModule(
|
||||||
|
// We need the ?inline query in Vite v6 when loading CSS in SSR
|
||||||
|
// since it does not expose the default export for CSS in a
|
||||||
|
// server environment.
|
||||||
|
injectQuery(normalizePath(dep.file), 'inline'),
|
||||||
|
)
|
||||||
|
).default as string)
|
||||||
|
|
||||||
|
if (css === undefined) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
styles[dep.url] = css
|
||||||
|
} catch {
|
||||||
|
// this can happen with dynamically imported modules
|
||||||
|
console.warn(`Could not load ${dep.file}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
Object.entries(styles)
|
||||||
|
.map(([fileName, css]) => [
|
||||||
|
`\n/* ${fileName
|
||||||
|
// Escape comment syntax in file paths
|
||||||
|
.replace(/\/\*/g, '/\\*')
|
||||||
|
.replace(/\*\//g, '*\\/')} */`,
|
||||||
|
css,
|
||||||
|
])
|
||||||
|
.flat()
|
||||||
|
.join('\n') || undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function transform the componentId into a file path.
|
* This function transform the componentId into a file path.
|
||||||
* File extension is not required for the vite.moduleGraph URL search.
|
* File extension is not required for the vite.moduleGraph URL search.
|
||||||
@@ -80,77 +168,7 @@ export const getStylesForComponentId = async (
|
|||||||
|
|
||||||
const fileUrl = path.join(process.cwd(), relativeFilePath)
|
const fileUrl = path.join(process.cwd(), relativeFilePath)
|
||||||
|
|
||||||
const styles: Record<string, string> = {}
|
return await getStylesForModule(viteDevServer, fileUrl, cssModulesManifest)
|
||||||
const deps: Set<ModuleNode> = new Set()
|
|
||||||
|
|
||||||
try {
|
|
||||||
let node: ModuleNode | undefined =
|
|
||||||
await viteDevServer.moduleGraph.getModuleByUrl(fileUrl)
|
|
||||||
|
|
||||||
// If the module is only present in the client module graph, the module
|
|
||||||
// won't have been found on the first request to the server. If so, we
|
|
||||||
// request the module so it's in the module graph, then try again.
|
|
||||||
if (!node) {
|
|
||||||
try {
|
|
||||||
await viteDevServer.transformRequest(fileUrl)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
node = await viteDevServer.moduleGraph.getModuleByUrl(fileUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
console.error(`Could not resolve module for file: ${fileUrl}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await findNodeDependencies(viteDevServer, node, deps)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dep of deps) {
|
|
||||||
if (
|
|
||||||
dep.file &&
|
|
||||||
isCssFile(dep.file) &&
|
|
||||||
!isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const css = isCssModulesFile(dep.file)
|
|
||||||
? cssModulesManifest[dep.file]
|
|
||||||
: ((
|
|
||||||
await viteDevServer.ssrLoadModule(
|
|
||||||
// We need the ?inline query in Vite v6 when loading CSS in SSR
|
|
||||||
// since it does not expose the default export for CSS in a
|
|
||||||
// server environment.
|
|
||||||
injectQuery(dep.url, 'inline'),
|
|
||||||
)
|
|
||||||
).default as string)
|
|
||||||
|
|
||||||
if (css === undefined) {
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
styles[dep.url] = css
|
|
||||||
} catch {
|
|
||||||
// this can happen with dynamically imported modules
|
|
||||||
console.warn(`Could not load ${dep.file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
Object.entries(styles)
|
|
||||||
.map(([fileName, css]) => [
|
|
||||||
`\n/* ${fileName
|
|
||||||
// Escape comment syntax in file paths
|
|
||||||
.replace(/\/\*/g, '/\\*')
|
|
||||||
.replace(/\*\//g, '*\\/')} */`,
|
|
||||||
css,
|
|
||||||
])
|
|
||||||
.flat()
|
|
||||||
.join('\n') || undefined
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ const Postscatchall = createRoute({ component: PostscatchallImport })
|
|||||||
const IndexRoute = Index.update({
|
const IndexRoute = Index.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostscatchallRoute = Postscatchall.update({
|
const PostscatchallRoute = Postscatchall.update({
|
||||||
path: '/posts/[...catch_all]',
|
path: '/posts/[...catch_all]',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
hasHandler: true,
|
hasHandler: true,
|
||||||
|
filePath: '/posts/[...catch_all]',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ const Index = createRoute({ component: IndexImport })
|
|||||||
const AboutRoute = About.update({
|
const AboutRoute = About.update({
|
||||||
path: '/about',
|
path: '/about',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/about',
|
||||||
})
|
})
|
||||||
|
|
||||||
const IndexRoute = Index.update({
|
const IndexRoute = Index.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|||||||
+6
@@ -36,31 +36,37 @@ const PostsMyPost = createRoute({ component: PostsMyPostImport })
|
|||||||
|
|
||||||
const PostslayoutRoute = Postslayout.update({
|
const PostslayoutRoute = Postslayout.update({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/posts/__layout',
|
||||||
})
|
})
|
||||||
|
|
||||||
const AboutRoute = About.update({
|
const AboutRoute = About.update({
|
||||||
path: '/about',
|
path: '/about',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/about',
|
||||||
})
|
})
|
||||||
|
|
||||||
const IndexRoute = Index.update({
|
const IndexRoute = Index.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostspostRoute = Postspost.update({
|
const PostspostRoute = Postspost.update({
|
||||||
path: '/posts/[post]',
|
path: '/posts/[post]',
|
||||||
getParentRoute: () => PostslayoutRoute,
|
getParentRoute: () => PostslayoutRoute,
|
||||||
|
filePath: '/posts/[post]',
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostsIndexRoute = PostsIndex.update({
|
const PostsIndexRoute = PostsIndex.update({
|
||||||
path: '/posts',
|
path: '/posts',
|
||||||
getParentRoute: () => PostslayoutRoute,
|
getParentRoute: () => PostslayoutRoute,
|
||||||
|
filePath: '/posts/',
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostsMyPostRoute = PostsMyPost.update({
|
const PostsMyPostRoute = PostsMyPost.update({
|
||||||
path: '/posts/my-post',
|
path: '/posts/my-post',
|
||||||
getParentRoute: () => PostslayoutRoute,
|
getParentRoute: () => PostslayoutRoute,
|
||||||
|
filePath: '/posts/my-post',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|||||||
@@ -25,16 +25,19 @@ const PostsMyPost = createRoute({ component: PostsMyPostImport })
|
|||||||
const AboutRoute = About.update({
|
const AboutRoute = About.update({
|
||||||
path: '/about',
|
path: '/about',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/about',
|
||||||
})
|
})
|
||||||
|
|
||||||
const IndexRoute = Index.update({
|
const IndexRoute = Index.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostsMyPostRoute = PostsMyPost.update({
|
const PostsMyPostRoute = PostsMyPost.update({
|
||||||
path: '/posts/my-post',
|
path: '/posts/my-post',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/posts/my-post',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ const Index = createRoute({ component: IndexImport })
|
|||||||
const AboutRoute = About.update({
|
const AboutRoute = About.update({
|
||||||
path: '/about',
|
path: '/about',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/about',
|
||||||
})
|
})
|
||||||
|
|
||||||
const IndexRoute = Index.update({
|
const IndexRoute = Index.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
|
filePath: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const VITE_PROXY_PATH = '/vite-server'
|
|||||||
const CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/tuono_internal__critical_css'
|
const CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/tuono_internal__critical_css'
|
||||||
|
|
||||||
interface CriticalCssProps {
|
interface CriticalCssProps {
|
||||||
routeId?: string
|
routeFilePath?: string
|
||||||
mode?: Mode
|
mode?: Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,16 +16,16 @@ interface CriticalCssProps {
|
|||||||
* since vite does not support CSS injection without JS waterfall
|
* since vite does not support CSS injection without JS waterfall
|
||||||
*/
|
*/
|
||||||
export function CriticalCss({
|
export function CriticalCss({
|
||||||
routeId,
|
routeFilePath,
|
||||||
mode,
|
mode,
|
||||||
}: CriticalCssProps): JSX.Element | null {
|
}: CriticalCssProps): JSX.Element | null {
|
||||||
if (!routeId || mode !== 'Dev') {
|
if (!routeFilePath || mode !== 'Dev') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<link
|
<link
|
||||||
href={`${CRITICAL_CSS_PATH}?componentId=${routeId}`}
|
href={`${CRITICAL_CSS_PATH}?componentId=${routeFilePath}`}
|
||||||
precedence="high"
|
precedence="high"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {
|
|||||||
if (custom404Route) {
|
if (custom404Route) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CriticalCss routeId={custom404Route.id} mode={mode} />
|
<CriticalCss routeFilePath={custom404Route.filePath} mode={mode} />
|
||||||
<RouteMatch route={custom404Route} mode={mode} serverInitialData={{}} />
|
<RouteMatch route={custom404Route} mode={mode} serverInitialData={{}} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -30,7 +30,7 @@ export function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RootLayout data={null} isLoading={false}>
|
<RootLayout data={null} isLoading={false}>
|
||||||
<CriticalCss routeId="__root__" mode={mode} />
|
<CriticalCss routeFilePath="__root__" mode={mode} />
|
||||||
<NotFoundDefaultContent />
|
<NotFoundDefaultContent />
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const RouteMatch = ({
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<CriticalCss routeId={route.id} mode={mode} />
|
<CriticalCss routeFilePath={route.filePath} mode={mode} />
|
||||||
<route.component data={routeData} isLoading={isTransitioning} />
|
<route.component data={routeData} isLoading={isTransitioning} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TraverseRootComponents>
|
</TraverseRootComponents>
|
||||||
@@ -77,9 +77,13 @@ const TraverseRootComponents = memo(
|
|||||||
const route = routes[index] as Route
|
const route = routes[index] as Route
|
||||||
const Parent = route.component
|
const Parent = route.component
|
||||||
|
|
||||||
|
// Fallback to the route id if the filePath is not defined
|
||||||
|
// as is the case for the root route
|
||||||
|
const routeFilePath = route.filePath || route.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Parent data={data} isLoading={isLoading}>
|
<Parent data={data} isLoading={isLoading}>
|
||||||
<CriticalCss routeId={route.id} mode={mode} />
|
<CriticalCss routeFilePath={routeFilePath} mode={mode} />
|
||||||
<TraverseRootComponents
|
<TraverseRootComponents
|
||||||
routes={routes}
|
routes={routes}
|
||||||
data={data}
|
data={data}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface RouteOptions {
|
|||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
getParentRoute?: () => Route
|
getParentRoute?: () => Route
|
||||||
path?: string
|
path?: string
|
||||||
|
filePath?: string
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
hasHandler?: boolean
|
hasHandler?: boolean
|
||||||
}
|
}
|
||||||
@@ -19,11 +20,28 @@ export const ROOT_ROUTE_ID = '__root__'
|
|||||||
export class Route {
|
export class Route {
|
||||||
options: RouteOptions
|
options: RouteOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The route id is used to identify the route in the router
|
||||||
|
* and is used to match the route with the URL.
|
||||||
|
*
|
||||||
|
* For now is the `path`
|
||||||
|
*/
|
||||||
id?: string
|
id?: string
|
||||||
isRoot: boolean
|
isRoot: boolean
|
||||||
|
/**
|
||||||
|
* Used for identify the route by matching the URL
|
||||||
|
*/
|
||||||
path?: string
|
path?: string
|
||||||
fullPath!: string
|
fullPath!: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to identify the route in the file system
|
||||||
|
* Used i.e. for finding the criticalCss to load
|
||||||
|
*
|
||||||
|
* The path does not include the file extension
|
||||||
|
*/
|
||||||
|
filePath?: string
|
||||||
|
|
||||||
children?: Array<Route>
|
children?: Array<Route>
|
||||||
parentRoute?: Route
|
parentRoute?: Route
|
||||||
originalIndex?: number
|
originalIndex?: number
|
||||||
@@ -70,11 +88,10 @@ export class Route {
|
|||||||
id = joinPaths(['/', id])
|
id = joinPaths(['/', id])
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = id === ROOT_ROUTE_ID ? '/' : path
|
this.filePath = this.options.filePath
|
||||||
|
|
||||||
this.path = path
|
this.path = path
|
||||||
this.id = id
|
this.id = id
|
||||||
this.fullPath = fullPath || ''
|
this.fullPath = path || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
addChildren(routes: Array<Route>): this {
|
addChildren(routes: Array<Route>): this {
|
||||||
|
|||||||
Reference in New Issue
Block a user