Skip to content

Commit 8610d58

Browse files
authored
RSC: Initial css support (#8887)
1 parent f5fc2e2 commit 8610d58

File tree

11 files changed

+422
-5
lines changed

11 files changed

+422
-5
lines changed

packages/vite/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
"types": "./dist/client.d.ts",
2727
"default": "./dist/client.js"
2828
},
29+
"./assets": {
30+
"types": "./dist/fully-react/assets.d.ts",
31+
"default": "./dist/fully-react/assets.js"
32+
},
33+
"./rwRscGlobal": {
34+
"types": "./dist/fully-react/rwRscGlobal.d.ts",
35+
"default": "./dist/fully-react/rwRscGlobal.js"
36+
},
2937
"./buildFeServer": {
3038
"types": "./dist/buildFeServer.d.ts",
3139
"default": "./dist/buildFeServer.js"

packages/vite/src/buildRscFeServer.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
5757
// // noExternal: ['@redwoodjs/web', '@redwoodjs/router'],
5858
// },
5959
build: {
60+
manifest: 'rsc-build-manifest.json',
6061
write: false,
6162
ssr: true,
6263
rollupOptions: {
@@ -135,7 +136,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
135136
},
136137
preserveEntrySignatures: 'exports-only',
137138
},
138-
manifest: 'build-manifest.json',
139+
manifest: 'client-build-manifest.json',
139140
},
140141
esbuild: {
141142
logLevel: 'debug',
@@ -154,11 +155,29 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
154155
{}
155156
)
156157

158+
// TODO (RSC) Some css is now duplicated in two files (i.e. for client
159+
// components). Probably don't want that.
160+
// Also not sure if this works on "soft" rerenders (i.e. not a full page
161+
// load)
162+
await Promise.all(
163+
serverBuildOutput.output
164+
.filter((item) => {
165+
return item.type === 'asset' && item.fileName.endsWith('.css')
166+
})
167+
.map((cssAsset) => {
168+
return fs.copyFile(
169+
path.join(rwPaths.web.distServer, cssAsset.fileName),
170+
path.join(rwPaths.web.dist, cssAsset.fileName)
171+
)
172+
})
173+
)
174+
157175
const clientEntries: Record<string, string> = {}
158176
for (const item of clientBuildOutput.output) {
159177
const { name, fileName } = item
160178
const entryFile =
161179
name &&
180+
// TODO (RSC) Can't we just compare the names? `item.name === name`
162181
serverBuildOutput.output.find(
163182
(item) =>
164183
'moduleIds' in item &&
@@ -273,9 +292,12 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
273292
// * With `assert` and `@babel/plugin-syntax-import-assertions` the
274293
// code compiled and ran properly, but Jest tests failed, complaining
275294
// about the syntax.
276-
const manifestPath = path.join(getPaths().web.dist, 'build-manifest.json')
277-
const buildManifestStr = await fs.readFile(manifestPath, 'utf-8')
278-
const clientBuildManifest: ViteBuildManifest = JSON.parse(buildManifestStr)
295+
const manifestPath = path.join(
296+
getPaths().web.dist,
297+
'client-build-manifest.json'
298+
)
299+
const manifestStr = await fs.readFile(manifestPath, 'utf-8')
300+
const clientBuildManifest: ViteBuildManifest = JSON.parse(manifestStr)
279301

280302
// TODO (RSC) We don't have support for a router yet, so skip all routes
281303
const routesList = [] as RouteSpec[] // getProjectRoutes()
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { relative } from 'node:path'
2+
3+
import { lazy } from 'react'
4+
5+
import { getPaths } from '@redwoodjs/project-config'
6+
7+
import { collectStyles } from './find-styles'
8+
import { RwRscServerGlobal } from './RwRscServerGlobal'
9+
10+
// import viteDevServer from '../dev-server'
11+
const viteDevServer: any = {}
12+
13+
export class DevRwRscServerGlobal extends RwRscServerGlobal {
14+
/** @type {import('vite').ViteDevServer} */
15+
viteServer
16+
17+
constructor() {
18+
super()
19+
this.viteServer = viteDevServer
20+
// this.routeManifest = viteDevServer.routesManifest
21+
}
22+
23+
bootstrapModules() {
24+
// return [`/@fs${import.meta.env.CLIENT_ENTRY}`]
25+
// TODO (RSC) No idea if this is correct or even what format CLIENT_ENTRY has.
26+
return [`/@fs${getPaths().web.entryClient}`]
27+
}
28+
29+
bootstrapScriptContent() {
30+
return undefined
31+
}
32+
33+
async loadModule(id: string) {
34+
return await viteDevServer.ssrLoadModule(id)
35+
}
36+
37+
lazyComponent(id: string) {
38+
const importPath = `/@fs${id}`
39+
return lazy(
40+
async () =>
41+
await this.viteServer.ssrLoadModule(/* @vite-ignore */ importPath)
42+
)
43+
}
44+
45+
chunkId(chunk: string) {
46+
// return relative(this.srcAppRoot, chunk)
47+
return relative(getPaths().web.src, chunk)
48+
}
49+
50+
async findAssetsForModules(modules: string[]) {
51+
const styles = await collectStyles(
52+
this.viteServer,
53+
modules.filter((i) => !!i)
54+
)
55+
56+
return [...Object.entries(styles ?? {}).map(([key, _value]) => key)]
57+
}
58+
59+
async findAssets() {
60+
const deps = this.getDependenciesForURL('/')
61+
return await this.findAssetsForModules(deps)
62+
}
63+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { readFileSync } from 'node:fs'
2+
import { join, relative } from 'node:path'
3+
4+
import type { Manifest as BuildManifest } from 'vite'
5+
6+
import { getPaths } from '@redwoodjs/project-config'
7+
8+
import { findAssetsInManifest } from './findAssetsInManifest'
9+
import { RwRscServerGlobal } from './RwRscServerGlobal'
10+
11+
function readJSON(path: string) {
12+
return JSON.parse(readFileSync(path, 'utf-8'))
13+
}
14+
15+
export class ProdRwRscServerGlobal extends RwRscServerGlobal {
16+
serverManifest: BuildManifest
17+
18+
constructor() {
19+
super()
20+
21+
const rwPaths = getPaths()
22+
23+
this.serverManifest = readJSON(
24+
join(rwPaths.web.distServer, 'server-build-manifest.json')
25+
)
26+
}
27+
28+
chunkId(chunk: string) {
29+
return relative(getPaths().web.src, chunk)
30+
}
31+
32+
async findAssetsForModules(modules: string[]) {
33+
return modules?.map((i) => this.findAssetsForModule(i)).flat() ?? []
34+
}
35+
36+
findAssetsForModule(module: string) {
37+
return [
38+
...findAssetsInManifest(this.serverManifest, module).filter(
39+
(asset) => !asset.endsWith('.js') && !asset.endsWith('.mjs')
40+
),
41+
]
42+
}
43+
44+
async findAssets(): Promise<string[]> {
45+
// TODO (RSC) This is a hack. We need to figure out how to get the
46+
// dependencies for the current page.
47+
const deps = Object.keys(this.serverManifest).filter((name) =>
48+
/\.(tsx|jsx|js)$/.test(name)
49+
)
50+
51+
return await this.findAssetsForModules(deps)
52+
}
53+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { lazy } from 'react'
2+
3+
export class RwRscServerGlobal {
4+
async loadModule(id: string) {
5+
return await import(/* @vite-ignore */ id)
6+
}
7+
8+
lazyComponent(id: string) {
9+
return lazy(() => this.loadModule(id))
10+
}
11+
12+
// Will be implemented by subclasses
13+
async findAssets(_id: string): Promise<any[]> {
14+
return []
15+
}
16+
17+
getDependenciesForURL(_route: string): string[] {
18+
return []
19+
}
20+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copied from
2+
// https://github.com/nksaraf/fully-react/blob/4f738132a17d94486c8da19d8729044c3998fc54/packages/fully-react/src/shared/assets.tsx
3+
// And then modified to work with our codebase
4+
5+
import React, { use } from 'react'
6+
7+
const linkProps = [
8+
['js', { rel: 'modulepreload', crossOrigin: '' }],
9+
['jsx', { rel: 'modulepreload', crossOrigin: '' }],
10+
['ts', { rel: 'modulepreload', crossOrigin: '' }],
11+
['tsx', { rel: 'modulepreload', crossOrigin: '' }],
12+
['css', { rel: 'stylesheet', precedence: 'high' }],
13+
['woff', { rel: 'preload', as: 'font', type: 'font/woff', crossOrigin: '' }],
14+
[
15+
'woff2',
16+
{ rel: 'preload', as: 'font', type: 'font/woff2', crossOrigin: '' },
17+
],
18+
['gif', { rel: 'preload', as: 'image', type: 'image/gif' }],
19+
['jpg', { rel: 'preload', as: 'image', type: 'image/jpeg' }],
20+
['jpeg', { rel: 'preload', as: 'image', type: 'image/jpeg' }],
21+
['png', { rel: 'preload', as: 'image', type: 'image/png' }],
22+
['webp', { rel: 'preload', as: 'image', type: 'image/webp' }],
23+
['svg', { rel: 'preload', as: 'image', type: 'image/svg+xml' }],
24+
['ico', { rel: 'preload', as: 'image', type: 'image/x-icon' }],
25+
['avif', { rel: 'preload', as: 'image', type: 'image/avif' }],
26+
['mp4', { rel: 'preload', as: 'video', type: 'video/mp4' }],
27+
['webm', { rel: 'preload', as: 'video', type: 'video/webm' }],
28+
] as const
29+
30+
type Linkprop = (typeof linkProps)[number][1]
31+
32+
const linkPropsMap = new Map<string, Linkprop>(linkProps)
33+
34+
/**
35+
* Generates a link tag for a given file. This will load stylesheets and preload
36+
* everything else. It uses the file extension to determine the type.
37+
*/
38+
export const Asset = ({ file }: { file: string }) => {
39+
const ext = file.split('.').pop()
40+
const props = ext ? linkPropsMap.get(ext) : null
41+
42+
if (!props) {
43+
return null
44+
}
45+
46+
return <link href={file} {...props} />
47+
}
48+
49+
export function Assets() {
50+
// TODO (RSC) Currently we only handle server assets.
51+
// Will probably need to handle client assets as well.
52+
// Do we also need special code for SSR?
53+
// if (isClient) return <ClientAssets />
54+
55+
// @ts-expect-error Need experimental types here for this to work
56+
return <ServerAssets />
57+
}
58+
59+
const findAssets = async () => {
60+
return [...new Set([...(await rwRscGlobal.findAssets(''))]).values()]
61+
}
62+
63+
const AssetList = ({ assets }: { assets: string[] }) => {
64+
return (
65+
<>
66+
{assets.map((asset) => {
67+
return <Asset file={asset} key={asset} />
68+
})}
69+
</>
70+
)
71+
}
72+
73+
async function ServerAssets() {
74+
const allAssets = await findAssets()
75+
76+
return <AssetList assets={allAssets} />
77+
}
78+
79+
export function ClientAssets() {
80+
const allAssets = use(findAssets())
81+
82+
return <AssetList assets={allAssets} />
83+
}

0 commit comments

Comments
 (0)