Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ssr): Collect CSS links during dev #9382

Merged
merged 5 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions packages/vite/src/devFeServer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { createServerAdapter } from '@whatwg-node/server'
import express from 'express'
import type { ViteDevServer } from 'vite'
import { createServer as createViteServer } from 'vite'

import type { RouteSpec } from '@redwoodjs/internal/dist/routes'
import { getProjectRoutes } from '@redwoodjs/internal/dist/routes'
import type { Paths } from '@redwoodjs/project-config'
import { getConfig, getPaths } from '@redwoodjs/project-config'

import { collectCssPaths, componentsModules } from './streaming/collectCss'
import { createReactStreamingHandler } from './streaming/createReactStreamingHandler'
import { registerFwGlobals } from './streaming/registerGlobals'
import { ensureProcessDirWeb } from './utils'
Expand Down Expand Up @@ -55,18 +59,12 @@ async function createServer() {

const routes = getProjectRoutes()

// TODO (STREAMING) CSS is handled by Vite in dev mode, we don't need to
// worry about it in dev but..... it causes a flash of unstyled content.
// For now I'm just injecting index css here
// Look at collectStyles in packages/vite/src/fully-react/find-styles.ts
const FIXME_HardcodedIndexCss = ['index.css']

for (const route of routes) {
const routeHandler = await createReactStreamingHandler(
{
route,
clientEntryPath: rwPaths.web.entryClient as string,
cssLinks: FIXME_HardcodedIndexCss,
getStylesheetLinks: () => getCssLinks(rwPaths, route, vite),
},
vite
)
Expand Down Expand Up @@ -100,3 +98,21 @@ process.stdin.on('data', async (data) => {
})
}
})

/**
* This function is used to collect the CSS links for a given route.
*
* Passed as a getter to the createReactStreamingHandler function, because
* at the time of creating the handler, the ViteDevServer hasn't analysed the module graph yet
*/
function getCssLinks(rwPaths: Paths, route: RouteSpec, vite: ViteDevServer) {
const appAndRouteModules = componentsModules(
[rwPaths.web.app, route.filePath].filter(Boolean) as string[],
vite
)

const collectedCss = collectCssPaths(appAndRouteModules)

const cssLinks = Array.from(collectedCss)
return cssLinks
}
4 changes: 2 additions & 2 deletions packages/vite/src/runFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ export async function runFeServer() {
})
)

const collectedCss = indexEntry.css || []
const getStylesheetLinks = () => indexEntry.css || []
const clientEntry = '/' + indexEntry.file

for (const route of Object.values(routeManifest)) {
const routeHandler = await createReactStreamingHandler({
route,
clientEntryPath: clientEntry,
cssLinks: collectedCss,
getStylesheetLinks,
})

// if it is a 404, register it at the end somehow.
Expand Down
40 changes: 40 additions & 0 deletions packages/vite/src/streaming/collectCss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ViteDevServer, ModuleNode } from 'vite'

/**
* Collect SSR CSS for Vite
*/
export const componentsModules = (
components: string[],
vite: ViteDevServer
) => {
const matchedModules: Set<ModuleNode> = new Set()
components.forEach((component) => {
const modules = vite.moduleGraph.getModulesByFile(component)
modules?.forEach((mod) => {
matchedModules.add(mod)
})
})
return matchedModules
}

export const collectCssPaths = (
mods: Set<ModuleNode>,
cssLinks = new Set<string>(),
checkedComponents = new Set()
) => {
for (const mod of mods) {
if (
mod.file?.endsWith('.scss') ||
mod.file?.endsWith('.css') ||
mod.file?.endsWith('.less') // technically less is not supported oob by vite
) {
cssLinks.add(mod.url)
}
if (mod.importedModules.size > 0 && !checkedComponents.has(mod.id)) {
checkedComponents.add(mod.id)
collectCssPaths(mod.importedModules, cssLinks, checkedComponents)
}
}

return cssLinks
}
14 changes: 10 additions & 4 deletions packages/vite/src/streaming/createReactStreamingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@ import { getAppRouteHook, getPaths } from '@redwoodjs/project-config'
import { matchPath } from '@redwoodjs/router'
import type { TagDescriptor } from '@redwoodjs/web'

// import { stripQueryStringAndHashFromPath } from '../utils'

import { reactRenderToStreamResponse } from './streamHelpers'
import { loadAndRunRouteHooks } from './triggerRouteHooks'

interface CreateReactStreamingHandlerOptions {
route: RWRouteManifestItem
clientEntryPath: string
cssLinks: string[]
getStylesheetLinks: () => string[]
}

const checkUaForSeoCrawler = isbot.spawn()
checkUaForSeoCrawler.exclude(['chrome-lighthouse'])

export const createReactStreamingHandler = async (
{ route, clientEntryPath, cssLinks }: CreateReactStreamingHandlerOptions,
{
route,
clientEntryPath,
getStylesheetLinks,
}: CreateReactStreamingHandlerOptions,
viteDevServer?: ViteDevServer
) => {
const { redirect, routeHooks, bundle } = route
Expand Down Expand Up @@ -103,6 +105,10 @@ export const createReactStreamingHandler = async (
req.headers.get('user-agent') || ''
)

// Using a function to get the CSS links because we need to wait for the
// vite dev server to analyze the module graph
const cssLinks = getStylesheetLinks()

const reactResponse = await reactRenderToStreamResponse(
{
ServerEntry,
Expand Down
14 changes: 12 additions & 2 deletions packages/web/src/components/htmlTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ const extractFromAssetMap = (key: 'css' | 'meta') => {
return null
}

function addSlashIfNeeded(path: string): string {
if (path.startsWith('http') || path.startsWith('/')) {
return path
} else {
return '/' + path
}
}

/** CSS is a specialised metatag */
export const Css = ({ css }: { css: string[] }) => {
const cssLinks = css || extractFromAssetMap('css') || []
const cssLinks = (css || extractFromAssetMap('css') || []).map(
addSlashIfNeeded
)

return (
<>
{cssLinks.map((cssLink, index) => {
return (
<link rel="stylesheet" key={`css-${index}`} href={`/${cssLink}`} />
<link rel="stylesheet" key={`css-${index}`} href={`${cssLink}`} />
)
})}
</>
Expand Down
Loading