Skip to content

Commit

Permalink
RSC: Initial css support (#8887)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Jul 12, 2023
1 parent f5fc2e2 commit 8610d58
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 5 deletions.
8 changes: 8 additions & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
},
"./assets": {
"types": "./dist/fully-react/assets.d.ts",
"default": "./dist/fully-react/assets.js"
},
"./rwRscGlobal": {
"types": "./dist/fully-react/rwRscGlobal.d.ts",
"default": "./dist/fully-react/rwRscGlobal.js"
},
"./buildFeServer": {
"types": "./dist/buildFeServer.d.ts",
"default": "./dist/buildFeServer.js"
Expand Down
30 changes: 26 additions & 4 deletions packages/vite/src/buildRscFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
// // noExternal: ['@redwoodjs/web', '@redwoodjs/router'],
// },
build: {
manifest: 'rsc-build-manifest.json',
write: false,
ssr: true,
rollupOptions: {
Expand Down Expand Up @@ -135,7 +136,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
},
preserveEntrySignatures: 'exports-only',
},
manifest: 'build-manifest.json',
manifest: 'client-build-manifest.json',
},
esbuild: {
logLevel: 'debug',
Expand All @@ -154,11 +155,29 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
{}
)

// TODO (RSC) Some css is now duplicated in two files (i.e. for client
// components). Probably don't want that.
// Also not sure if this works on "soft" rerenders (i.e. not a full page
// load)
await Promise.all(
serverBuildOutput.output
.filter((item) => {
return item.type === 'asset' && item.fileName.endsWith('.css')
})
.map((cssAsset) => {
return fs.copyFile(
path.join(rwPaths.web.distServer, cssAsset.fileName),
path.join(rwPaths.web.dist, cssAsset.fileName)
)
})
)

const clientEntries: Record<string, string> = {}
for (const item of clientBuildOutput.output) {
const { name, fileName } = item
const entryFile =
name &&
// TODO (RSC) Can't we just compare the names? `item.name === name`
serverBuildOutput.output.find(
(item) =>
'moduleIds' in item &&
Expand Down Expand Up @@ -273,9 +292,12 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
// * With `assert` and `@babel/plugin-syntax-import-assertions` the
// code compiled and ran properly, but Jest tests failed, complaining
// about the syntax.
const manifestPath = path.join(getPaths().web.dist, 'build-manifest.json')
const buildManifestStr = await fs.readFile(manifestPath, 'utf-8')
const clientBuildManifest: ViteBuildManifest = JSON.parse(buildManifestStr)
const manifestPath = path.join(
getPaths().web.dist,
'client-build-manifest.json'
)
const manifestStr = await fs.readFile(manifestPath, 'utf-8')
const clientBuildManifest: ViteBuildManifest = JSON.parse(manifestStr)

// TODO (RSC) We don't have support for a router yet, so skip all routes
const routesList = [] as RouteSpec[] // getProjectRoutes()
Expand Down
63 changes: 63 additions & 0 deletions packages/vite/src/fully-react/DevRwRscServerGlobal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { relative } from 'node:path'

import { lazy } from 'react'

import { getPaths } from '@redwoodjs/project-config'

import { collectStyles } from './find-styles'
import { RwRscServerGlobal } from './RwRscServerGlobal'

// import viteDevServer from '../dev-server'
const viteDevServer: any = {}

export class DevRwRscServerGlobal extends RwRscServerGlobal {
/** @type {import('vite').ViteDevServer} */
viteServer

constructor() {
super()
this.viteServer = viteDevServer
// this.routeManifest = viteDevServer.routesManifest
}

bootstrapModules() {
// return [`/@fs${import.meta.env.CLIENT_ENTRY}`]
// TODO (RSC) No idea if this is correct or even what format CLIENT_ENTRY has.
return [`/@fs${getPaths().web.entryClient}`]
}

bootstrapScriptContent() {
return undefined
}

async loadModule(id: string) {
return await viteDevServer.ssrLoadModule(id)
}

lazyComponent(id: string) {
const importPath = `/@fs${id}`
return lazy(
async () =>
await this.viteServer.ssrLoadModule(/* @vite-ignore */ importPath)
)
}

chunkId(chunk: string) {
// return relative(this.srcAppRoot, chunk)
return relative(getPaths().web.src, chunk)
}

async findAssetsForModules(modules: string[]) {
const styles = await collectStyles(
this.viteServer,
modules.filter((i) => !!i)
)

return [...Object.entries(styles ?? {}).map(([key, _value]) => key)]
}

async findAssets() {
const deps = this.getDependenciesForURL('/')
return await this.findAssetsForModules(deps)
}
}
53 changes: 53 additions & 0 deletions packages/vite/src/fully-react/ProdRwRscServerGlobal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { readFileSync } from 'node:fs'
import { join, relative } from 'node:path'

import type { Manifest as BuildManifest } from 'vite'

import { getPaths } from '@redwoodjs/project-config'

import { findAssetsInManifest } from './findAssetsInManifest'
import { RwRscServerGlobal } from './RwRscServerGlobal'

function readJSON(path: string) {
return JSON.parse(readFileSync(path, 'utf-8'))
}

export class ProdRwRscServerGlobal extends RwRscServerGlobal {
serverManifest: BuildManifest

constructor() {
super()

const rwPaths = getPaths()

this.serverManifest = readJSON(
join(rwPaths.web.distServer, 'server-build-manifest.json')
)
}

chunkId(chunk: string) {
return relative(getPaths().web.src, chunk)
}

async findAssetsForModules(modules: string[]) {
return modules?.map((i) => this.findAssetsForModule(i)).flat() ?? []
}

findAssetsForModule(module: string) {
return [
...findAssetsInManifest(this.serverManifest, module).filter(
(asset) => !asset.endsWith('.js') && !asset.endsWith('.mjs')
),
]
}

async findAssets(): Promise<string[]> {
// TODO (RSC) This is a hack. We need to figure out how to get the
// dependencies for the current page.
const deps = Object.keys(this.serverManifest).filter((name) =>
/\.(tsx|jsx|js)$/.test(name)
)

return await this.findAssetsForModules(deps)
}
}
20 changes: 20 additions & 0 deletions packages/vite/src/fully-react/RwRscServerGlobal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { lazy } from 'react'

export class RwRscServerGlobal {
async loadModule(id: string) {
return await import(/* @vite-ignore */ id)
}

lazyComponent(id: string) {
return lazy(() => this.loadModule(id))
}

// Will be implemented by subclasses
async findAssets(_id: string): Promise<any[]> {
return []
}

getDependenciesForURL(_route: string): string[] {
return []
}
}
83 changes: 83 additions & 0 deletions packages/vite/src/fully-react/assets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copied from
// https://github.com/nksaraf/fully-react/blob/4f738132a17d94486c8da19d8729044c3998fc54/packages/fully-react/src/shared/assets.tsx
// And then modified to work with our codebase

import React, { use } from 'react'

const linkProps = [
['js', { rel: 'modulepreload', crossOrigin: '' }],
['jsx', { rel: 'modulepreload', crossOrigin: '' }],
['ts', { rel: 'modulepreload', crossOrigin: '' }],
['tsx', { rel: 'modulepreload', crossOrigin: '' }],
['css', { rel: 'stylesheet', precedence: 'high' }],
['woff', { rel: 'preload', as: 'font', type: 'font/woff', crossOrigin: '' }],
[
'woff2',
{ rel: 'preload', as: 'font', type: 'font/woff2', crossOrigin: '' },
],
['gif', { rel: 'preload', as: 'image', type: 'image/gif' }],
['jpg', { rel: 'preload', as: 'image', type: 'image/jpeg' }],
['jpeg', { rel: 'preload', as: 'image', type: 'image/jpeg' }],
['png', { rel: 'preload', as: 'image', type: 'image/png' }],
['webp', { rel: 'preload', as: 'image', type: 'image/webp' }],
['svg', { rel: 'preload', as: 'image', type: 'image/svg+xml' }],
['ico', { rel: 'preload', as: 'image', type: 'image/x-icon' }],
['avif', { rel: 'preload', as: 'image', type: 'image/avif' }],
['mp4', { rel: 'preload', as: 'video', type: 'video/mp4' }],
['webm', { rel: 'preload', as: 'video', type: 'video/webm' }],
] as const

type Linkprop = (typeof linkProps)[number][1]

const linkPropsMap = new Map<string, Linkprop>(linkProps)

/**
* Generates a link tag for a given file. This will load stylesheets and preload
* everything else. It uses the file extension to determine the type.
*/
export const Asset = ({ file }: { file: string }) => {
const ext = file.split('.').pop()
const props = ext ? linkPropsMap.get(ext) : null

if (!props) {
return null
}

return <link href={file} {...props} />
}

export function Assets() {
// TODO (RSC) Currently we only handle server assets.
// Will probably need to handle client assets as well.
// Do we also need special code for SSR?
// if (isClient) return <ClientAssets />

// @ts-expect-error Need experimental types here for this to work
return <ServerAssets />
}

const findAssets = async () => {
return [...new Set([...(await rwRscGlobal.findAssets(''))]).values()]
}

const AssetList = ({ assets }: { assets: string[] }) => {
return (
<>
{assets.map((asset) => {
return <Asset file={asset} key={asset} />
})}
</>
)
}

async function ServerAssets() {
const allAssets = await findAssets()

return <AssetList assets={allAssets} />
}

export function ClientAssets() {
const allAssets = use(findAssets())

return <AssetList assets={allAssets} />
}
Loading

0 comments on commit 8610d58

Please sign in to comment.