Skip to content

Commit

Permalink
RSC: Build using rw build (#8893)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Jul 13, 2023
1 parent c44a260 commit f1d0dcb
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 157 deletions.
36 changes: 28 additions & 8 deletions packages/vite/src/buildFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ import { transformWithBabel } from '@redwoodjs/internal/dist/build/babel/api'
import { buildWeb } from '@redwoodjs/internal/dist/build/web'
import { findRouteHooksSrc } from '@redwoodjs/internal/dist/files'
import { getProjectRoutes } from '@redwoodjs/internal/dist/routes'
import { getAppRouteHook, getPaths } from '@redwoodjs/project-config'
import { getAppRouteHook, getConfig, getPaths } from '@redwoodjs/project-config'

import { buildRscFeServer } from './buildRscFeServer'
import { RWRouteManifest } from './types'

interface BuildOptions {
export interface BuildOptions {
verbose?: boolean
}

export const buildFeServer = async ({ verbose }: BuildOptions) => {
const rwPaths = getPaths()
const viteConfig = rwPaths.web.viteConfig
const rwConfig = getConfig()
const viteConfigPath = rwPaths.web.viteConfig

if (!viteConfig) {
if (!viteConfigPath) {
throw new Error(
'Vite config not found. You need to setup your project with Vite using `yarn rw setup vite`'
'Vite config not found. You need to setup your project with Vite ' +
'using `yarn rw setup vite`'
)
}

Expand All @@ -35,17 +38,34 @@ export const buildFeServer = async ({ verbose }: BuildOptions) => {
)
}

if (rwConfig.experimental?.rsc?.enabled) {
if (!rwPaths.web.entries) {
throw new Error('RSC entries file not found')
}

return await buildRscFeServer({
viteConfigPath,
webSrc: rwPaths.web.src,
webHtml: rwPaths.web.html,
entries: rwPaths.web.entries,
webDist: rwPaths.web.dist,
webDistServer: rwPaths.web.distServer,
webDistEntries: rwPaths.web.distServerEntries,
webRouteManifest: rwPaths.web.routeManifest,
})
}

// Step 1A: Generate the client bundle
await buildWeb({ verbose })

// TODO (STREAMING) When Streaming is released Vite will be the only bundler,
// so we can switch to a regular import
// @NOTE: Using dynamic import, because vite is still opt-in
const { build } = await import('vite')
const { build: viteBuild } = await import('vite')

// Step 1B: Generate the server output
await build({
configFile: viteConfig,
await viteBuild({
configFile: viteConfigPath,
build: {
// Because we configure the root to be web/src, we need to go up one level
outDir: rwPaths.web.distServer,
Expand Down
184 changes: 35 additions & 149 deletions packages/vite/src/buildRscFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,135 +6,47 @@ import { build as viteBuild } from 'vite'
import type { Manifest as ViteBuildManifest } from 'vite'

import { RouteSpec } from '@redwoodjs/internal/dist/routes'
import { getAppRouteHook, getPaths } from '@redwoodjs/project-config'

import { rscBuild } from './rscBuild'
import { RWRouteManifest } from './types'
import { serverBuild } from './waku-lib/build-server'
import { rscAnalyzePlugin, rscIndexPlugin } from './waku-lib/vite-plugin-rsc'

interface BuildOptions {
verbose?: boolean
import { rscIndexPlugin } from './waku-lib/vite-plugin-rsc'

interface Args {
viteConfigPath: string
webSrc: string
webHtml: string
entries: string
webDist: string
webDistServer: string
webDistEntries: string
webRouteManifest: string
}

export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
const rwPaths = getPaths()
const viteConfig = rwPaths.web.viteConfig

if (!viteConfig) {
throw new Error('Vite config not found')
}

if (!rwPaths.web.entries) {
throw new Error('RSC entries file not found')
}

const clientEntryFileSet = new Set<string>()
const serverEntryFileSet = new Set<string>()

/**
* RSC build
* Uses rscAnalyzePlugin to collect client and server entry points
* Starts building the AST in entries.ts
* Doesn't output any files, only collects a list of RSCs and RSFs
*/
await viteBuild({
configFile: viteConfig,
root: rwPaths.base,
plugins: [
react(),
{
name: 'rsc-test-plugin',
transform(_code, id) {
console.log('rsc-test-plugin id', id)
},
},
rscAnalyzePlugin(
(id) => clientEntryFileSet.add(id),
(id) => serverEntryFileSet.add(id)
),
],
// ssr: {
// // FIXME Without this, waku/router isn't considered to have client
// // entries, and "No client entry" error occurs.
// // Unless we fix this, RSC-capable packages aren't supported.
// // This also seems to cause problems with pnpm.
// // noExternal: ['@redwoodjs/web', '@redwoodjs/router'],
// },
build: {
manifest: 'rsc-build-manifest.json',
write: false,
ssr: true,
rollupOptions: {
input: {
entries: rwPaths.web.entries,
},
},
},
})

const clientEntryFiles = Object.fromEntries(
Array.from(clientEntryFileSet).map((filename, i) => [`rsc${i}`, filename])
)
const serverEntryFiles = Object.fromEntries(
Array.from(serverEntryFileSet).map((filename, i) => [`rsf${i}`, filename])
)

console.log('clientEntryFileSet', Array.from(clientEntryFileSet))
console.log('serverEntryFileSet', Array.from(serverEntryFileSet))
console.log('clientEntryFiles', clientEntryFiles)
console.log('serverEntryFiles', serverEntryFiles)

const clientEntryPath = rwPaths.web.entryClient

if (!clientEntryPath) {
throw new Error(
'Vite client entry point not found. Please check that your project ' +
'has an entry.client.{jsx,tsx} file in the web/src directory.'
)
}
export const buildRscFeServer = async ({
viteConfigPath,
webSrc,
webHtml,
entries,
webDist,
webDistServer,
webDistEntries,
webRouteManifest,
}: Args) => {
const { clientEntryFiles, serverEntryFiles } = await rscBuild(viteConfigPath)

const clientBuildOutput = await viteBuild({
configFile: viteConfig,
root: rwPaths.web.src,
plugins: [
// TODO (RSC) Update index.html to include the entry.client.js script
// TODO (RSC) Do the above in the exp-rsc setup command
// {
// name: 'redwood-plugin-vite',

// // ---------- Bundle injection ----------
// // Used by rollup during build to inject the entrypoint
// // but note index.html does not come through as an id during dev
// transform: (code: string, id: string) => {
// if (
// existsSync(clientEntryPath) &&
// // TODO (RSC) Is this even needed? We throw if we can't find it above
// // TODO (RSC) Consider making this async (if we do need it)
// normalizePath(id) === normalizePath(rwPaths.web.html)
// ) {
// const newCode = code.replace(
// '</head>',
// '<script type="module" src="entry.client.jsx"></script></head>'
// )
//
// return { code: newCode, map: null }
// } else {
// // Returning null as the map preserves the original sourcemap
// return { code, map: null }
// }
// },
// },
react(),
rscIndexPlugin(),
],
configFile: viteConfigPath,
root: webSrc,
plugins: [react(), rscIndexPlugin()],
build: {
outDir: rwPaths.web.dist,
outDir: webDist,
emptyOutDir: true, // Needed because `outDir` is not inside `root`
// TODO (RSC) Enable this when we switch to a server-first approach
// emptyOutDir: false, // Already done when building server
rollupOptions: {
input: {
main: rwPaths.web.html,
main: webHtml,
...clientEntryFiles,
},
preserveEntrySignatures: 'exports-only',
Expand All @@ -151,7 +63,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
}

const serverBuildOutput = await serverBuild(
rwPaths.web.entries,
entries,
clientEntryFiles,
serverEntryFiles,
{}
Expand All @@ -168,8 +80,8 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
})
.map((cssAsset) => {
return fs.copyFile(
path.join(rwPaths.web.distServer, cssAsset.fileName),
path.join(rwPaths.web.dist, cssAsset.fileName)
path.join(webDistServer, cssAsset.fileName),
path.join(webDist, cssAsset.fileName)
)
})
)
Expand All @@ -193,7 +105,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
console.log('clientEntries', clientEntries)

await fs.appendFile(
path.join(rwPaths.web.distServer, 'entries.js'),
webDistEntries,
`export const clientEntries=${JSON.stringify(clientEntries)};`
)

Expand Down Expand Up @@ -294,10 +206,7 @@ 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,
'client-build-manifest.json'
)
const manifestPath = path.join(webDist, 'client-build-manifest.json')
const manifestStr = await fs.readFile(manifestPath, 'utf-8')
const clientBuildManifest: ViteBuildManifest = JSON.parse(manifestStr)

Expand All @@ -316,7 +225,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
// E.g. /blog/post/{id:Int}
pathDefinition: route.path,
hasParams: route.hasParams,
routeHooks: FIXME_constructRouteHookPath(route.routeHooks),
routeHooks: null,
redirect: route.redirect
? {
to: route.redirect?.to,
Expand All @@ -329,28 +238,5 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
return acc
}, {})

await fs.writeFile(rwPaths.web.routeManifest, JSON.stringify(routeManifest))
}

// TODO (STREAMING) Hacky work around because when you don't have a App.routeHook, esbuild doesn't create
// the pages folder in the dist/server/routeHooks directory.
// @MARK need to change to .mjs here if we use esm
const FIXME_constructRouteHookPath = (rhSrcPath: string | null | undefined) => {
const rwPaths = getPaths()
if (!rhSrcPath) {
return null
}

if (getAppRouteHook()) {
return path.relative(rwPaths.web.src, rhSrcPath).replace('.ts', '.js')
} else {
return path
.relative(path.join(rwPaths.web.src, 'pages'), rhSrcPath)
.replace('.ts', '.js')
}
}

if (require.main === module) {
const verbose = process.argv.includes('--verbose')
buildFeServer({ verbose })
await fs.writeFile(webRouteManifest, JSON.stringify(routeManifest))
}
71 changes: 71 additions & 0 deletions packages/vite/src/rscBuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import react from '@vitejs/plugin-react'
import { build as viteBuild } from 'vite'

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

import { rscAnalyzePlugin } from './waku-lib/vite-plugin-rsc'

/**
* RSC build
* Uses rscAnalyzePlugin to collect client and server entry points
* Starts building the AST in entries.ts
* Doesn't output any files, only collects a list of RSCs and RSFs
*/
export async function rscBuild(viteConfigPath: string) {
const rwPaths = getPaths()
const clientEntryFileSet = new Set<string>()
const serverEntryFileSet = new Set<string>()

if (!rwPaths.web.entries) {
throw new Error('RSC entries file not found')
}

await viteBuild({
configFile: viteConfigPath,
root: rwPaths.base,
plugins: [
react(),
{
name: 'rsc-test-plugin',
transform(_code, id) {
console.log('rsc-test-plugin id', id)
},
},
rscAnalyzePlugin(
(id) => clientEntryFileSet.add(id),
(id) => serverEntryFileSet.add(id)
),
],
// ssr: {
// // FIXME Without this, waku/router isn't considered to have client
// // entries, and "No client entry" error occurs.
// // Unless we fix this, RSC-capable packages aren't supported.
// // This also seems to cause problems with pnpm.
// // noExternal: ['@redwoodjs/web', '@redwoodjs/router'],
// },
build: {
manifest: 'rsc-build-manifest.json',
write: false,
ssr: true,
rollupOptions: {
input: {
entries: rwPaths.web.entries,
},
},
},
})

const clientEntryFiles = Object.fromEntries(
Array.from(clientEntryFileSet).map((filename, i) => [`rsc${i}`, filename])
)
const serverEntryFiles = Object.fromEntries(
Array.from(serverEntryFileSet).map((filename, i) => [`rsf${i}`, filename])
)

console.log('clientEntryFileSet', Array.from(clientEntryFileSet))
console.log('serverEntryFileSet', Array.from(serverEntryFileSet))
console.log('clientEntryFiles', clientEntryFiles)
console.log('serverEntryFiles', serverEntryFiles)

return { clientEntryFiles, serverEntryFiles }
}

0 comments on commit f1d0dcb

Please sign in to comment.