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: Refactor build process #9588

Merged
merged 2 commits into from
Nov 29, 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
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
Loading