Skip to content

Commit

Permalink
RSC: Refactor build process (#9588)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Nov 29, 2023
1 parent 6a6c4a8 commit 1f66831
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 187 deletions.
2 changes: 1 addition & 1 deletion packages/vite/src/buildFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => {
entries: rwPaths.web.entries,
webDist: rwPaths.web.dist,
webDistServer: rwPaths.web.distServer,
webDistEntries: rwPaths.web.distServerEntries,
webDistServerEntries: rwPaths.web.distServerEntries,
webRouteManifest: rwPaths.web.routeManifest,
})
}
Expand Down
205 changes: 28 additions & 177 deletions packages/vite/src/buildRscFeServer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import fs from 'fs/promises'
import path from 'path'

import react from '@vitejs/plugin-react'
import { build as viteBuild } from 'vite'
import type { Manifest as ViteBuildManifest } from 'vite'

import type { RouteSpec } from '@redwoodjs/internal/dist/routes'

import { onWarn } from './lib/onWarn'
import { rscBuild } from './rscBuild'
import { rscBuildAnalyze } from './rsc/rscBuildAnalyze'
import { rscBuildClient } from './rsc/rscBuildClient'
import { rscBuildClientEntriesMappings } from './rsc/rscBuildClientEntriesFile'
import { rscBuildCopyCssAssets } from './rsc/rscBuildCopyCssAssets'
import { rscBuildServer } from './rsc/rscBuildServer'
import type { RWRouteManifest } from './types'
import { serverBuild } from './waku-lib/build-server'
import { rscIndexPlugin } from './waku-lib/vite-plugin-rsc'

interface Args {
viteConfigPath: string
Expand All @@ -20,7 +19,7 @@ interface Args {
entries: string
webDist: string
webDistServer: string
webDistEntries: string
webDistServerEntries: string
webRouteManifest: string
}

Expand All @@ -31,189 +30,41 @@ export const buildRscFeServer = async ({
entries,
webDist,
webDistServer,
webDistEntries,
webDistServerEntries,
webRouteManifest,
}: Args) => {
// Step 1: Analyze all files and generate a list of RSCs and RSFs
const { clientEntryFiles, serverEntryFiles } = await rscBuild(viteConfigPath)

// Step 2: Generate the client bundle
const clientBuildOutput = await viteBuild({
// configFile: viteConfigPath,
root: webSrc,
plugins: [react(), rscIndexPlugin()],
build: {
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: {
onwarn: onWarn,
input: {
main: webHtml,
...clientEntryFiles,
},
preserveEntrySignatures: 'exports-only',
output: {
// This is not ideal. See
// https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting
// But we need it to prevent `import 'client-only'` from being
// hoisted into App.tsx
// TODO (RSC): Fix when https://github.com/rollup/rollup/issues/5235
// is resolved
hoistTransitiveImports: false,
},
},
manifest: 'client-build-manifest.json',
},
esbuild: {
logLevel: 'debug',
},
})
// Analyze all files and generate a list of RSCs and RSFs
const { clientEntryFiles, serverEntryFiles } = await rscBuildAnalyze(
viteConfigPath
)

if (!('output' in clientBuildOutput)) {
throw new Error('Unexpected vite client build output')
}
// Generate the client bundle
const clientBuildOutput = await rscBuildClient(
webSrc,
webHtml,
webDist,
clientEntryFiles
)

// Step 3: Generate the server output
const serverBuildOutput = await serverBuild(
// Generate the server output
const serverBuildOutput = await rscBuildServer(
entries,
clientEntryFiles,
serverEntryFiles,
{}
)

// 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(webDistServer, cssAsset.fileName),
path.join(webDist, 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 &&
item.moduleIds.includes(clientEntryFiles[name] as string)
)?.fileName
// Copy CSS assets from server to client
await rscBuildCopyCssAssets(serverBuildOutput, webDist, webDistServer)

if (entryFile) {
console.log('entryFile', entryFile)
if (process.platform === 'win32') {
const entryFileSlash = entryFile.replaceAll('\\', '/')
console.log('entryFileSlash', entryFileSlash)
// Prevent errors on Windows like
// Error: No client entry found for D:/a/redwood/rsc-project/web/dist/server/assets/rsc0.js
clientEntries[entryFileSlash] = fileName
} else {
clientEntries[entryFile] = fileName
}
}
}

console.log('clientEntries', clientEntries)

await fs.appendFile(
webDistEntries,
`export const clientEntries=${JSON.stringify(clientEntries)};`
// Mappings from server to client asset file names
await rscBuildClientEntriesMappings(
clientBuildOutput,
serverBuildOutput,
clientEntryFiles,
webDistServerEntries
)

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

// const rollupInput = {
// entries: rwPaths.web.entryServer,
// ...clientEntryFiles,
// ...serverEntryFiles,
// }

// Step 1B: Generate the server output
// await build({
// // TODO (RSC) I had this marked as 'FIXME'. I guess I just need to make
// // sure we still include it, or at least make it possible for users to pass
// // in their own config
// // configFile: viteConfig,
// ssr: {
// noExternal: Array.from(clientEntryFileSet).map(
// // TODO (RSC) I think the comment below is from waku. We don't care
// // about pnpm, do we? Does it also affect yarn?
// // FIXME this might not work with pnpm
// // TODO (RSC) No idea what's going on here
// (filename) => {
// const nodeModulesPath = path.join(rwPaths.base, 'node_modules')
// console.log('nodeModulesPath', nodeModulesPath)
// const relativePath = path.relative(nodeModulesPath, filename)
// console.log('relativePath', relativePath)
// console.log('first split', relativePath.split('/')[0])

// return relativePath.split('/')[0]
// }
// ),
// },
// build: {
// // Because we configure the root to be web/src, we need to go up one level
// outDir: rwPaths.web.distServer,
// // TODO (RSC) Maybe we should re-enable this. I can't remember anymore)
// // What does 'ssr' even mean?
// // ssr: rwPaths.web.entryServer,
// rollupOptions: {
// input: {
// // TODO (RSC) entries: rwPaths.web.entryServer,
// ...clientEntryFiles,
// ...serverEntryFiles,
// },
// output: {
// banner: (chunk) => {
// console.log('chunk', chunk)

// // HACK to bring directives to the front
// let code = ''

// if (chunk.moduleIds.some((id) => clientEntryFileSet.has(id))) {
// code += '"use client";'
// }

// if (chunk.moduleIds.some((id) => serverEntryFileSet.has(id))) {
// code += '"use server";'
// }

// console.log('code', code)
// return code
// },
// entryFileNames: (chunkInfo) => {
// console.log('chunkInfo', chunkInfo)

// // TODO (RSC) Don't hardcode 'entry.server'
// if (chunkInfo.name === 'entry.server') {
// return '[name].js'
// }

// return 'assets/[name].js'
// },
// },
// },
// },
// envFile: false,
// logLevel: verbose ? 'info' : 'warn',
// })

// Step 3: Generate route-manifest.json

// TODO When https://github.com/tc39/proposal-import-attributes and
// https://github.com/microsoft/TypeScript/issues/53656 have both landed we
// should try to do this instead:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { build as viteBuild } from 'vite'

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

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

/**
* RSC build. Step 1 of 3.
* RSC build. Step 1.
* buildFeServer -> buildRscFeServer -> rscBuildAnalyze
* 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) {
export async function rscBuildAnalyze(viteConfigPath: string) {
const rwPaths = getPaths()
const clientEntryFileSet = new Set<string>()
const serverEntryFileSet = new Set<string>()
Expand Down
56 changes: 56 additions & 0 deletions packages/vite/src/rsc/rscBuildClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import react from '@vitejs/plugin-react'
import { build as viteBuild } from 'vite'

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

/**
* RSC build. Step 2.
* buildFeServer -> buildRscFeServer -> rscBuildClient
* Generate the client bundle
*/
export async function rscBuildClient(
webSrc: string,
webHtml: string,
webDist: string,
clientEntryFiles: Record<string, string>
) {
const clientBuildOutput = await viteBuild({
// configFile: viteConfigPath,
root: webSrc,
plugins: [react(), rscIndexPlugin()],
build: {
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: {
onwarn: onWarn,
input: {
main: webHtml,
...clientEntryFiles,
},
preserveEntrySignatures: 'exports-only',
output: {
// This is not ideal. See
// https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting
// But we need it to prevent `import 'client-only'` from being
// hoisted into App.tsx
// TODO (RSC): Fix when https://github.com/rollup/rollup/issues/5235
// is resolved
hoistTransitiveImports: false,
},
},
manifest: 'client-build-manifest.json',
},
esbuild: {
logLevel: 'debug',
},
})

if (!('output' in clientBuildOutput)) {
throw new Error('Unexpected vite client build output')
}

return clientBuildOutput.output
}
49 changes: 49 additions & 0 deletions packages/vite/src/rsc/rscBuildClientEntriesFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'fs/promises'

import type { rscBuildClient } from './rscBuildClient'
import type { rscBuildServer } from './rscBuildServer'

/**
* RSC build. Step 5.
* Append a mapping of server asset names to client asset names to the
* `web/dist/server/entries.js` file.
*/
export function rscBuildClientEntriesMappings(
clientBuildOutput: Awaited<ReturnType<typeof rscBuildClient>>,
serverBuildOutput: Awaited<ReturnType<typeof rscBuildServer>>,
clientEntryFiles: Record<string, string>,
webDistServerEntries: string
) {
const clientEntries: Record<string, string> = {}
for (const item of clientBuildOutput) {
const { name, fileName } = item
const entryFile =
name &&
// TODO (RSC) Can't we just compare the names? `item.name === name`
serverBuildOutput.find(
(item) =>
'moduleIds' in item &&
item.moduleIds.includes(clientEntryFiles[name] as string)
)?.fileName

if (entryFile) {
console.log('entryFile', entryFile)
if (process.platform === 'win32') {
const entryFileSlash = entryFile.replaceAll('\\', '/')
console.log('entryFileSlash', entryFileSlash)
// Prevent errors on Windows like
// Error: No client entry found for D:/a/redwood/rsc-project/web/dist/server/assets/rsc0.js
clientEntries[entryFileSlash] = fileName
} else {
clientEntries[entryFile] = fileName
}
}
}

console.log('clientEntries', clientEntries)

return fs.appendFile(
webDistServerEntries,
`export const clientEntries=${JSON.stringify(clientEntries)};`
)
}
Loading

0 comments on commit 1f66831

Please sign in to comment.