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: Combine RSC server with StreamingSSR server #9553

Merged
merged 9 commits into from
Nov 20, 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
14 changes: 8 additions & 6 deletions packages/cli/src/commands/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ export const builder = async (yargs) => {
'./serveBothHandler.js'
)
await bothExperimentalServerFileHandler()
} else if (getConfig().experimental?.rsc?.enabled) {
const { bothRscServerHandler } = await import('./serveBothHandler.js')
await bothRscServerHandler(argv)
} else if (getConfig().experimental?.streamingSsr?.enabled) {
const { bothSsrServerHandler } = await import('./serveBothHandler.js')
await bothSsrServerHandler(argv)
} else if (
getConfig().experimental?.rsc?.enabled ||
getConfig().experimental?.streamingSsr?.enabled
) {
const { bothSsrRscServerHandler } = await import(
'./serveBothHandler.js'
)
await bothSsrRscServerHandler(argv)
} else {
// Wanted to use the new web-server package here, but can't because
// of backwards compatibility reasons. With `bothServerHandler` both
Expand Down
47 changes: 7 additions & 40 deletions packages/cli/src/commands/serveBothHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,10 @@ import { getConfig, getPaths } from '@redwoodjs/project-config'
export const bothExperimentalServerFileHandler = async () => {
logExperimentalHeader()

if (getConfig().experimental?.rsc?.enabled) {
logSkippingFastifyWebServer()

await execa(
'node',
['./node_modules/@redwoodjs/vite/dist/runRscFeServer.js'],
{
cwd: getPaths().base,
stdio: 'inherit',
shell: true,
}
)
} else if (getConfig().experimental?.streamingSsr?.enabled) {
if (
getConfig().experimental?.rsc?.enabled ||
getConfig().experimental?.streamingSsr?.enabled
) {
logSkippingFastifyWebServer()

await execa('yarn', ['rw-serve-fe'], {
Expand All @@ -47,41 +38,17 @@ export const bothExperimentalServerFileHandler = async () => {
}
}

export const bothRscServerHandler = async (argv) => {
const { apiServerHandler } = await import('./serveApiHandler.js')

// TODO (RSC) Allow specifying port, socket and apiRootPath
const apiPromise = apiServerHandler({
...argv,
port: 8911,
apiRootPath: '/',
})

// TODO (RSC) More gracefully handle Ctrl-C
const fePromise = execa(
'node',
['./node_modules/@redwoodjs/vite/dist/runRscFeServer.js'],
{
cwd: getPaths().base,
stdio: 'inherit',
shell: true,
}
)

await Promise.all([apiPromise, fePromise])
}

export const bothSsrServerHandler = async (argv) => {
export const bothSsrRscServerHandler = async (argv) => {
const { apiServerHandler } = await import('./serveApiHandler.js')

// TODO (STREAMING) Allow specifying port, socket and apiRootPath
// TODO Allow specifying port, socket and apiRootPath
const apiPromise = apiServerHandler({
...argv,
port: 8911,
apiRootPath: '/',
})

// TODO (STREAMING) More gracefully handle Ctrl-C
// TODO More gracefully handle Ctrl-C
// Right now you get a big red error box when you kill the process
const fePromise = execa('yarn', ['rw-serve-fe'], {
cwd: getPaths().web.base,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/config/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ module.exports = (webpackEnv) => {
}),
isEnvProduction &&
new WebpackManifestPlugin({
fileName: 'build-manifest.json',
fileName: 'client-build-manifest.json',
}),
isEnvProduction && new ChunkReferencesPlugin(),
...getSharedPlugins(isEnvProduction),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function (
{ types: t }: { types: typeof types },
{ bundler }: { bundler: BundlerEnum }
): PluginObj {
const manifestPath = join(getPaths().web.dist, 'build-manifest.json')
const manifestPath = join(getPaths().web.dist, 'client-build-manifest.json')
const buildManifest = require(manifestPath)

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/prerender/src/runPrerender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function insertChunkLoadingScript(

const buildManifest = JSON.parse(
fs.readFileSync(
path.join(getPaths().web.dist, 'build-manifest.json'),
path.join(getPaths().web.dist, 'client-build-manifest.json'),
'utf-8'
)
)
Expand Down
7 changes: 5 additions & 2 deletions packages/vite/src/buildFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => {
// https://github.com/microsoft/TypeScript/issues/53656 have both landed we
// should try to do this instead:
// const clientBuildManifest: ViteBuildManifest = await import(
// path.join(getPaths().web.dist, 'build-manifest.json'),
// path.join(getPaths().web.dist, 'client-build-manifest.json'),
// { with: { type: 'json' } }
// )
// NOTES:
Expand All @@ -136,7 +136,10 @@ export const buildFeServer = async ({ verbose, webDir }: 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, 'build-manifest.json')
const manifestPath = path.join(
getPaths().web.dist,
'client-build-manifest.json'
)
const buildManifestStr = await fs.readFile(manifestPath, 'utf-8')
const clientBuildManifest: ViteBuildManifest = JSON.parse(buildManifestStr)

Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export default function redwoodPluginVite(): PluginOption[] {
build: {
outDir: options.build?.outDir || rwPaths.web.dist,
emptyOutDir: true,
manifest: !env.ssrBuild ? 'build-manifest.json' : undefined,
manifest: !env.ssrBuild ? 'client-build-manifest.json' : undefined,
sourcemap: !env.ssrBuild && rwConfig.web.sourceMap, // Note that this can be boolean or 'inline'
rollupOptions: {
input: getRollupInput(!!env.ssrBuild),
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/src/rsc/rscRequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function createRscRequestHandler() {
// size somehow
// https://nextjs.org/docs/app/api-reference/functions/server-actions#size-limitation
if (req.headers['content-type']?.startsWith('multipart/form-data')) {
console.log('RSA: multipart/form-data')
const bb = busboy({ headers: req.headers })
const reply = decodeReplyFromBusboy(bb)

Expand Down Expand Up @@ -84,6 +85,7 @@ export function createRscRequestHandler() {
}
}
} else {
console.log('RSA: regular body')
let body = ''

for await (const chunk of req) {
Expand All @@ -97,6 +99,8 @@ export function createRscRequestHandler() {
}
}

console.log('rscRequestHandler: args', args)

if (rscId || rsfId) {
const handleError = (err: unknown) => {
if (hasStatusCode(err)) {
Expand Down
16 changes: 16 additions & 0 deletions packages/vite/src/rsc/rscWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,22 @@ function isSerializedFormData(data?: unknown): data is SerializedFormData {
}

async function renderRsc(input: RenderInput): Promise<PipeableStream> {
const rwPaths = getPaths()

const config = await configPromise
// TODO (RSC): Should root be configurable by the user? We probably need it
// to be different values in different contexts. Should we introduce more
// config options?
// config.root currently comes from the user's project, where it in turn
// comes from our `redwood()` vite plugin defined in index.ts. By default
// (i.e. in the redwood() plugin) it points to <base>/web/src. But we need it
// to be just <base>/, so for now we override it here.
config.root =
process.platform === 'win32'
? rwPaths.base.replaceAll('\\', '/')
: rwPaths.base
console.log('config.root', config.root)
console.log('rwPaths.base', rwPaths.base)
const bundlerConfig = new Proxy(
{},
{
Expand All @@ -322,6 +337,7 @@ async function renderRsc(input: RenderInput): Promise<PipeableStream> {
// filePath /Users/tobbe/dev/waku/examples/01_counter/dist/assets/rsc0.js
// name Counter
const id = resolveClientEntry(config, filePath)
console.log('Proxy id', id)
// id /assets/rsc0-beb48afe.js
return { id, chunks: [id], name, async: true }
},
Expand Down
64 changes: 42 additions & 22 deletions packages/vite/src/runFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// well in naming with @redwoodjs/api-server)
// Only things used during dev can be in @redwoodjs/vite. Everything else has
// to go in fe-server
// UPDATE: We decided to name the package @redwoodjs/web-server instead of
// fe-server. And it's already created, but this hasn't been moved over yet.

import fs from 'fs/promises'
import path from 'path'
Expand All @@ -15,6 +17,8 @@ import type { Manifest as ViteBuildManifest } from 'vite'

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

import { createRscRequestHandler } from './rsc/rscRequestHandler'
import { setClientEntries } from './rsc/rscWorkerCommunication'
import { createReactStreamingHandler } from './streaming/createReactStreamingHandler'
import { registerFwGlobals } from './streaming/registerGlobals'
import type { RWRouteManifest } from './types'
Expand All @@ -35,7 +39,7 @@ loadDotEnv({
defaults: path.join(getPaths().base, '.env.defaults'),
multiline: true,
})
//------------------------------------------------
// ------------------------------------------------

export async function runFeServer() {
const app = express()
Expand All @@ -44,6 +48,19 @@ export async function runFeServer() {

registerFwGlobals()

try {
// This will fail if we're not running in RSC mode (i.e. for Streaming SSR)
// TODO (RSC) Remove the try/catch, or at least the if-statement in there
// once RSC is always enabled
await setClientEntries('load')
} catch (e) {
if (rwConfig.experimental?.rsc?.enabled) {
console.error('Failed to load client entries')
console.error(e)
process.exit(1)
}
}

// 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 All @@ -63,10 +80,16 @@ export async function runFeServer() {
const routeManifest: RWRouteManifest = JSON.parse(routeManifestStr)

// TODO See above about using `import { with: { type: 'json' } }` instead
const manifestPath = path.join(getPaths().web.dist, 'build-manifest.json')
const manifestPath = path.join(rwPaths.web.dist, 'client-build-manifest.json')
const buildManifestStr = await fs.readFile(manifestPath, 'utf-8')
const buildManifest: ViteBuildManifest = JSON.parse(buildManifestStr)

if (rwConfig.experimental?.rsc?.enabled) {
console.log('='.repeat(80))
console.log('buildManifest', buildManifest)
console.log('='.repeat(80))
}

const indexEntry = Object.values(buildManifest).find((manifestItem) => {
return manifestItem.isEntry
})
Expand All @@ -75,11 +98,14 @@ export async function runFeServer() {
throw new Error('Could not find index.html in build manifest')
}

// 👉 1. Use static handler for assets
// 1. Use static handler for assets
// For CF workers, we'd need an equivalent of this
app.use('/', express.static(rwPaths.web.dist, { index: false }))
app.use(
'/assets',
express.static(rwPaths.web.dist + '/assets', { index: false })
)

// 👉 2. Proxy the api server
// 2. Proxy the api server
// TODO (STREAMING) we need to be able to specify whether proxying is required or not
// e.g. deploying to Netlify, we don't need to proxy but configure it in Netlify
// Also be careful of differences between v2 and v3 of the server
Expand All @@ -101,6 +127,8 @@ export async function runFeServer() {
const getStylesheetLinks = () => indexEntry.css || []
const clientEntry = '/' + indexEntry.file

// `routeManifest` is empty for RSC builds for now, so we're not doing SSR
// when we have RSC experimental support enabled
for (const route of Object.values(routeManifest)) {
const routeHandler = await createReactStreamingHandler({
route,
Expand All @@ -123,25 +151,17 @@ export async function runFeServer() {
app.get(expressPathDef, createServerAdapter(routeHandler))
}

const server = app.listen(
rwConfig.web.port,
process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::'
)

server.on('listening', () => {
let addressDetails = ''
const address = server.address()
// Mounting middleware at /rw-rsc will strip /rw-rsc from req.url
app.use('/rw-rsc', createRscRequestHandler())

if (typeof address === 'string') {
addressDetails = `(${address})`
} else if (address && typeof address === 'object') {
addressDetails = `(${address.address}:${address.port})`
}
// This is basically the route for / -> HomePage. Used by RSC
// Using .get() here to get exact path matching
app.get('/', express.static(rwPaths.web.dist))

console.log(
`Started production FE server on http://localhost:${rwConfig.web.port} ${addressDetails}`
)
})
app.listen(rwConfig.web.port)
console.log(
`Started production FE server on http://localhost:${rwConfig.web.port}`
)
}

runFeServer()
Loading
Loading