Skip to content

Commit 1f66831

Browse files
authored
RSC: Refactor build process (#9588)
1 parent 6a6c4a8 commit 1f66831

8 files changed

Lines changed: 178 additions & 187 deletions

packages/vite/src/buildFeServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => {
5858
entries: rwPaths.web.entries,
5959
webDist: rwPaths.web.dist,
6060
webDistServer: rwPaths.web.distServer,
61-
webDistEntries: rwPaths.web.distServerEntries,
61+
webDistServerEntries: rwPaths.web.distServerEntries,
6262
webRouteManifest: rwPaths.web.routeManifest,
6363
})
6464
}

packages/vite/src/buildRscFeServer.ts

Lines changed: 28 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import fs from 'fs/promises'
22
import path from 'path'
33

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

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

10-
import { onWarn } from './lib/onWarn'
11-
import { rscBuild } from './rscBuild'
8+
import { rscBuildAnalyze } from './rsc/rscBuildAnalyze'
9+
import { rscBuildClient } from './rsc/rscBuildClient'
10+
import { rscBuildClientEntriesMappings } from './rsc/rscBuildClientEntriesFile'
11+
import { rscBuildCopyCssAssets } from './rsc/rscBuildCopyCssAssets'
12+
import { rscBuildServer } from './rsc/rscBuildServer'
1213
import type { RWRouteManifest } from './types'
13-
import { serverBuild } from './waku-lib/build-server'
14-
import { rscIndexPlugin } from './waku-lib/vite-plugin-rsc'
1514

1615
interface Args {
1716
viteConfigPath: string
@@ -20,7 +19,7 @@ interface Args {
2019
entries: string
2120
webDist: string
2221
webDistServer: string
23-
webDistEntries: string
22+
webDistServerEntries: string
2423
webRouteManifest: string
2524
}
2625

@@ -31,189 +30,41 @@ export const buildRscFeServer = async ({
3130
entries,
3231
webDist,
3332
webDistServer,
34-
webDistEntries,
33+
webDistServerEntries,
3534
webRouteManifest,
3635
}: Args) => {
37-
// Step 1: Analyze all files and generate a list of RSCs and RSFs
38-
const { clientEntryFiles, serverEntryFiles } = await rscBuild(viteConfigPath)
39-
40-
// Step 2: Generate the client bundle
41-
const clientBuildOutput = await viteBuild({
42-
// configFile: viteConfigPath,
43-
root: webSrc,
44-
plugins: [react(), rscIndexPlugin()],
45-
build: {
46-
outDir: webDist,
47-
emptyOutDir: true, // Needed because `outDir` is not inside `root`
48-
// TODO (RSC) Enable this when we switch to a server-first approach
49-
// emptyOutDir: false, // Already done when building server
50-
rollupOptions: {
51-
onwarn: onWarn,
52-
input: {
53-
main: webHtml,
54-
...clientEntryFiles,
55-
},
56-
preserveEntrySignatures: 'exports-only',
57-
output: {
58-
// This is not ideal. See
59-
// https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting
60-
// But we need it to prevent `import 'client-only'` from being
61-
// hoisted into App.tsx
62-
// TODO (RSC): Fix when https://github.com/rollup/rollup/issues/5235
63-
// is resolved
64-
hoistTransitiveImports: false,
65-
},
66-
},
67-
manifest: 'client-build-manifest.json',
68-
},
69-
esbuild: {
70-
logLevel: 'debug',
71-
},
72-
})
36+
// Analyze all files and generate a list of RSCs and RSFs
37+
const { clientEntryFiles, serverEntryFiles } = await rscBuildAnalyze(
38+
viteConfigPath
39+
)
7340

74-
if (!('output' in clientBuildOutput)) {
75-
throw new Error('Unexpected vite client build output')
76-
}
41+
// Generate the client bundle
42+
const clientBuildOutput = await rscBuildClient(
43+
webSrc,
44+
webHtml,
45+
webDist,
46+
clientEntryFiles
47+
)
7748

78-
// Step 3: Generate the server output
79-
const serverBuildOutput = await serverBuild(
49+
// Generate the server output
50+
const serverBuildOutput = await rscBuildServer(
8051
entries,
8152
clientEntryFiles,
8253
serverEntryFiles,
8354
{}
8455
)
8556

86-
// TODO (RSC) Some css is now duplicated in two files (i.e. for client
87-
// components). Probably don't want that.
88-
// Also not sure if this works on "soft" rerenders (i.e. not a full page
89-
// load)
90-
await Promise.all(
91-
serverBuildOutput.output
92-
.filter((item) => {
93-
return item.type === 'asset' && item.fileName.endsWith('.css')
94-
})
95-
.map((cssAsset) => {
96-
return fs.copyFile(
97-
path.join(webDistServer, cssAsset.fileName),
98-
path.join(webDist, cssAsset.fileName)
99-
)
100-
})
101-
)
102-
103-
const clientEntries: Record<string, string> = {}
104-
for (const item of clientBuildOutput.output) {
105-
const { name, fileName } = item
106-
const entryFile =
107-
name &&
108-
// TODO (RSC) Can't we just compare the names? `item.name === name`
109-
serverBuildOutput.output.find(
110-
(item) =>
111-
'moduleIds' in item &&
112-
item.moduleIds.includes(clientEntryFiles[name] as string)
113-
)?.fileName
57+
// Copy CSS assets from server to client
58+
await rscBuildCopyCssAssets(serverBuildOutput, webDist, webDistServer)
11459

115-
if (entryFile) {
116-
console.log('entryFile', entryFile)
117-
if (process.platform === 'win32') {
118-
const entryFileSlash = entryFile.replaceAll('\\', '/')
119-
console.log('entryFileSlash', entryFileSlash)
120-
// Prevent errors on Windows like
121-
// Error: No client entry found for D:/a/redwood/rsc-project/web/dist/server/assets/rsc0.js
122-
clientEntries[entryFileSlash] = fileName
123-
} else {
124-
clientEntries[entryFile] = fileName
125-
}
126-
}
127-
}
128-
129-
console.log('clientEntries', clientEntries)
130-
131-
await fs.appendFile(
132-
webDistEntries,
133-
`export const clientEntries=${JSON.stringify(clientEntries)};`
60+
// Mappings from server to client asset file names
61+
await rscBuildClientEntriesMappings(
62+
clientBuildOutput,
63+
serverBuildOutput,
64+
clientEntryFiles,
65+
webDistServerEntries
13466
)
13567

136-
// // Step 1A: Generate the client bundle
137-
// await buildWeb({ verbose })
138-
139-
// const rollupInput = {
140-
// entries: rwPaths.web.entryServer,
141-
// ...clientEntryFiles,
142-
// ...serverEntryFiles,
143-
// }
144-
145-
// Step 1B: Generate the server output
146-
// await build({
147-
// // TODO (RSC) I had this marked as 'FIXME'. I guess I just need to make
148-
// // sure we still include it, or at least make it possible for users to pass
149-
// // in their own config
150-
// // configFile: viteConfig,
151-
// ssr: {
152-
// noExternal: Array.from(clientEntryFileSet).map(
153-
// // TODO (RSC) I think the comment below is from waku. We don't care
154-
// // about pnpm, do we? Does it also affect yarn?
155-
// // FIXME this might not work with pnpm
156-
// // TODO (RSC) No idea what's going on here
157-
// (filename) => {
158-
// const nodeModulesPath = path.join(rwPaths.base, 'node_modules')
159-
// console.log('nodeModulesPath', nodeModulesPath)
160-
// const relativePath = path.relative(nodeModulesPath, filename)
161-
// console.log('relativePath', relativePath)
162-
// console.log('first split', relativePath.split('/')[0])
163-
164-
// return relativePath.split('/')[0]
165-
// }
166-
// ),
167-
// },
168-
// build: {
169-
// // Because we configure the root to be web/src, we need to go up one level
170-
// outDir: rwPaths.web.distServer,
171-
// // TODO (RSC) Maybe we should re-enable this. I can't remember anymore)
172-
// // What does 'ssr' even mean?
173-
// // ssr: rwPaths.web.entryServer,
174-
// rollupOptions: {
175-
// input: {
176-
// // TODO (RSC) entries: rwPaths.web.entryServer,
177-
// ...clientEntryFiles,
178-
// ...serverEntryFiles,
179-
// },
180-
// output: {
181-
// banner: (chunk) => {
182-
// console.log('chunk', chunk)
183-
184-
// // HACK to bring directives to the front
185-
// let code = ''
186-
187-
// if (chunk.moduleIds.some((id) => clientEntryFileSet.has(id))) {
188-
// code += '"use client";'
189-
// }
190-
191-
// if (chunk.moduleIds.some((id) => serverEntryFileSet.has(id))) {
192-
// code += '"use server";'
193-
// }
194-
195-
// console.log('code', code)
196-
// return code
197-
// },
198-
// entryFileNames: (chunkInfo) => {
199-
// console.log('chunkInfo', chunkInfo)
200-
201-
// // TODO (RSC) Don't hardcode 'entry.server'
202-
// if (chunkInfo.name === 'entry.server') {
203-
// return '[name].js'
204-
// }
205-
206-
// return 'assets/[name].js'
207-
// },
208-
// },
209-
// },
210-
// },
211-
// envFile: false,
212-
// logLevel: verbose ? 'info' : 'warn',
213-
// })
214-
215-
// Step 3: Generate route-manifest.json
216-
21768
// TODO When https://github.com/tc39/proposal-import-attributes and
21869
// https://github.com/microsoft/TypeScript/issues/53656 have both landed we
21970
// should try to do this instead:
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import { build as viteBuild } from 'vite'
33

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

6-
import { onWarn } from './lib/onWarn'
7-
import { rscAnalyzePlugin } from './waku-lib/vite-plugin-rsc'
6+
import { onWarn } from '../lib/onWarn'
7+
import { rscAnalyzePlugin } from '../waku-lib/vite-plugin-rsc'
88

99
/**
10-
* RSC build. Step 1 of 3.
10+
* RSC build. Step 1.
11+
* buildFeServer -> buildRscFeServer -> rscBuildAnalyze
1112
* Uses rscAnalyzePlugin to collect client and server entry points
1213
* Starts building the AST in entries.ts
1314
* Doesn't output any files, only collects a list of RSCs and RSFs
1415
*/
15-
export async function rscBuild(viteConfigPath: string) {
16+
export async function rscBuildAnalyze(viteConfigPath: string) {
1617
const rwPaths = getPaths()
1718
const clientEntryFileSet = new Set<string>()
1819
const serverEntryFileSet = new Set<string>()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import react from '@vitejs/plugin-react'
2+
import { build as viteBuild } from 'vite'
3+
4+
import { onWarn } from '../lib/onWarn'
5+
import { rscIndexPlugin } from '../waku-lib/vite-plugin-rsc'
6+
7+
/**
8+
* RSC build. Step 2.
9+
* buildFeServer -> buildRscFeServer -> rscBuildClient
10+
* Generate the client bundle
11+
*/
12+
export async function rscBuildClient(
13+
webSrc: string,
14+
webHtml: string,
15+
webDist: string,
16+
clientEntryFiles: Record<string, string>
17+
) {
18+
const clientBuildOutput = await viteBuild({
19+
// configFile: viteConfigPath,
20+
root: webSrc,
21+
plugins: [react(), rscIndexPlugin()],
22+
build: {
23+
outDir: webDist,
24+
emptyOutDir: true, // Needed because `outDir` is not inside `root`
25+
// TODO (RSC) Enable this when we switch to a server-first approach
26+
// emptyOutDir: false, // Already done when building server
27+
rollupOptions: {
28+
onwarn: onWarn,
29+
input: {
30+
main: webHtml,
31+
...clientEntryFiles,
32+
},
33+
preserveEntrySignatures: 'exports-only',
34+
output: {
35+
// This is not ideal. See
36+
// https://rollupjs.org/faqs/#why-do-additional-imports-turn-up-in-my-entry-chunks-when-code-splitting
37+
// But we need it to prevent `import 'client-only'` from being
38+
// hoisted into App.tsx
39+
// TODO (RSC): Fix when https://github.com/rollup/rollup/issues/5235
40+
// is resolved
41+
hoistTransitiveImports: false,
42+
},
43+
},
44+
manifest: 'client-build-manifest.json',
45+
},
46+
esbuild: {
47+
logLevel: 'debug',
48+
},
49+
})
50+
51+
if (!('output' in clientBuildOutput)) {
52+
throw new Error('Unexpected vite client build output')
53+
}
54+
55+
return clientBuildOutput.output
56+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import fs from 'fs/promises'
2+
3+
import type { rscBuildClient } from './rscBuildClient'
4+
import type { rscBuildServer } from './rscBuildServer'
5+
6+
/**
7+
* RSC build. Step 5.
8+
* Append a mapping of server asset names to client asset names to the
9+
* `web/dist/server/entries.js` file.
10+
*/
11+
export function rscBuildClientEntriesMappings(
12+
clientBuildOutput: Awaited<ReturnType<typeof rscBuildClient>>,
13+
serverBuildOutput: Awaited<ReturnType<typeof rscBuildServer>>,
14+
clientEntryFiles: Record<string, string>,
15+
webDistServerEntries: string
16+
) {
17+
const clientEntries: Record<string, string> = {}
18+
for (const item of clientBuildOutput) {
19+
const { name, fileName } = item
20+
const entryFile =
21+
name &&
22+
// TODO (RSC) Can't we just compare the names? `item.name === name`
23+
serverBuildOutput.find(
24+
(item) =>
25+
'moduleIds' in item &&
26+
item.moduleIds.includes(clientEntryFiles[name] as string)
27+
)?.fileName
28+
29+
if (entryFile) {
30+
console.log('entryFile', entryFile)
31+
if (process.platform === 'win32') {
32+
const entryFileSlash = entryFile.replaceAll('\\', '/')
33+
console.log('entryFileSlash', entryFileSlash)
34+
// Prevent errors on Windows like
35+
// Error: No client entry found for D:/a/redwood/rsc-project/web/dist/server/assets/rsc0.js
36+
clientEntries[entryFileSlash] = fileName
37+
} else {
38+
clientEntries[entryFile] = fileName
39+
}
40+
}
41+
}
42+
43+
console.log('clientEntries', clientEntries)
44+
45+
return fs.appendFile(
46+
webDistServerEntries,
47+
`export const clientEntries=${JSON.stringify(clientEntries)};`
48+
)
49+
}

0 commit comments

Comments
 (0)