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

RSC: Build using rw build #8893

Merged
merged 3 commits into from
Jul 13, 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
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 }
}