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

React Server Components (RSC) #8451

Merged
merged 33 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2305c18
Copy waku files over and get rid of errors and warnings in them
Tobbe May 25, 2023
0ad1af0
Generate list of client entry files during build
Tobbe May 25, 2023
4a78ae6
Produce dist/ for both client and server
Tobbe May 27, 2023
f273353
Fix dist/index.html location and shim webpack
Tobbe May 29, 2023
b5b2870
Update react to canary
Tobbe May 30, 2023
cdd86e5
Change to exports in package.json
Tobbe May 30, 2023
552bae1
Use react/next types in the vite package
Tobbe May 30, 2023
23aa8bd
RSC wip
Tobbe May 30, 2023
3932674
Basic RSC example working
Tobbe May 31, 2023
c58c03a
Copy and rewrite node-loader to get around ESM issues
Tobbe Jun 24, 2023
dda3166
Switch to react/canary types
Tobbe Jul 4, 2023
5b8129e
No default when reading json straight from file
Tobbe Jul 4, 2023
5b0a377
package/docs can stay on React 17
Tobbe Jul 4, 2023
c25579f
Temporarily downgrade to React 18.2.0
Tobbe Jul 5, 2023
0e86578
Merge branch 'main' into tobbe-exp-rsc
Tobbe Jul 5, 2023
35ad6e7
Fix broken merge
Tobbe Jul 5, 2023
694e181
Unneeded change
Tobbe Jul 5, 2023
1d547d9
Remove graphql resolution
Tobbe Jul 5, 2023
b3ce97e
Reduce churn in vite tsconfig
Tobbe Jul 5, 2023
015e275
Fix react version in vite package.json
Tobbe Jul 5, 2023
71972f9
Separate build file
Tobbe Jul 5, 2023
6e41581
Separate runRscFeServer file
Tobbe Jul 5, 2023
06928e8
TODO (RSC) comments in all waku files
Tobbe Jul 5, 2023
14ce5f5
Limit churn in triggerRouteHooks
Tobbe Jul 5, 2023
efd2b81
Merge branch 'main' into tobbe-rsc
Tobbe Jul 5, 2023
393f87f
Merge branch 'main' into tobbe-rsc
Tobbe Jul 5, 2023
4378f61
Skip routes
Tobbe Jul 6, 2023
bdef810
tsconfig: Allow keeping jsx: preserve
Tobbe Jul 6, 2023
1219751
Merge branch 'tobbe-rsc' of https://github.com/Tobbe/redwood into tob…
Tobbe Jul 6, 2023
6b4994f
Merge branch 'main' into tobbe-rsc
Tobbe Jul 6, 2023
b483930
Merge branch 'main' into tobbe-rsc
Tobbe Jul 6, 2023
b5a02de
Merge branch 'main' into tobbe-rsc
jtoar Jul 6, 2023
230c586
vite: export buildFeServer
Tobbe Jul 6, 2023
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
1 change: 1 addition & 0 deletions packages/vite/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-var */
/// <reference types="react/canary" />

declare global {
var RWJS_ENV: {
Expand Down
5 changes: 5 additions & 0 deletions packages/vite/modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'react-server-dom-webpack/node-loader'
declare module 'react-server-dom-webpack/server'
declare module 'react-server-dom-webpack/server.node.unbundled'
declare module 'react-server-dom-webpack/client'
declare module 'acorn-loose'
17 changes: 16 additions & 1 deletion packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@
"dist",
"inject"
],
"main": "dist/index.js",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./client": {
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
}
},
"bin": {
"rw-dev-fe": "./dist/devFeServer.js",
"rw-serve-fe": "./dist/runFeServer.js",
Expand All @@ -33,20 +43,25 @@
"@redwoodjs/internal": "5.0.0",
"@redwoodjs/project-config": "5.0.0",
"@redwoodjs/web": "5.0.0",
"@swc/core": "1.3.60",
"@vitejs/plugin-react": "4.0.1",
"acorn-loose": "^8.3.0",
"buffer": "6.0.3",
"core-js": "3.31.0",
"dotenv-defaults": "5.0.2",
"express": "4.18.2",
"http-proxy-middleware": "2.0.6",
"isbot": "3.6.8",
"react": "18.3.0-canary-035a41c4e-20230704",
"react-server-dom-webpack": "18.3.0-canary-035a41c4e-20230704",
"vite": "4.3.9",
"vite-plugin-environment": "1.1.3",
"yargs-parser": "21.1.1"
},
"devDependencies": {
"@babel/cli": "7.22.5",
"@types/express": "4",
"@types/react": "18.2.14",
"@types/yargs-parser": "21.0.0",
"glob": "10.3.1",
"jest": "29.5.0",
Expand Down
327 changes: 327 additions & 0 deletions packages/vite/src/buildRscFeServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
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 { RouteSpec } from '@redwoodjs/internal/dist/routes'
import { getAppRouteHook, getPaths } from '@redwoodjs/project-config'

import { RWRouteManifest } from './types'
import { serverBuild } from './waku-lib/build-server'
import { rscAnalyzePlugin, rscIndexPlugin } from './waku-lib/vite-plugin-rsc'

interface BuildOptions {
verbose?: boolean
}

export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => {
const rwPaths = getPaths()

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({
// ...configFileConfig,
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: {
write: false,
ssr: true,
rollupOptions: {
input: {
// entries: rwPaths.web.entryServer,
entries: path.join(rwPaths.web.src, 'entries.ts'),
},
},
},
})

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.'
)
}

const clientBuildOutput = await viteBuild({
// ...configFileConfig,
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(),
],
build: {
outDir: rwPaths.web.dist,
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,
...clientEntryFiles,
},
preserveEntrySignatures: 'exports-only',
},
manifest: 'build-manifest.json',
},
esbuild: {
logLevel: 'debug',
},
})

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

const serverBuildOutput = await serverBuild(
// rwPaths.web.entryServer,
path.join(rwPaths.web.src, 'entries.ts'),
clientEntryFiles,
serverEntryFiles,
{}
)

const clientEntries: Record<string, string> = {}
for (const item of clientBuildOutput.output) {
const { name, fileName } = item
const entryFile =
name &&
serverBuildOutput.output.find(
(item) =>
'moduleIds' in item &&
item.moduleIds.includes(clientEntryFiles[name] as string)
)?.fileName
if (entryFile) {
clientEntries[entryFile] = fileName
}
}

console.log('clientEntries', clientEntries)

await fs.appendFile(
path.join(rwPaths.web.distServer, 'entries.js'),
`export const clientEntries=${JSON.stringify(clientEntries)};`
)

// // 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:
// const clientBuildManifest: ViteBuildManifest = await import(
// path.join(getPaths().web.dist, 'build-manifest.json'),
// { with: { type: 'json' } }
// )
// NOTES:
// * There's a related babel plugin here
// https://babeljs.io/docs/babel-plugin-syntax-import-attributes
// * Included in `preset-env` if you set `shippedProposals: true`
// * We had this before, but with `assert` instead of `with`. We really
// should be using `with`. See motivation in issues linked above.
// * 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 buildManifestStr = await fs.readFile(manifestPath, 'utf-8')
const clientBuildManifest: ViteBuildManifest = JSON.parse(buildManifestStr)

// TODO (RSC) We don't have support for a router yet, so skip all routes
const routesList = [] as RouteSpec[] // getProjectRoutes()

// This is all a no-op for now
const routeManifest = routesList.reduce<RWRouteManifest>((acc, route) => {
acc[route.path] = {
name: route.name,
bundle: route.relativeFilePath
? clientBuildManifest[route.relativeFilePath].file
: null,
matchRegexString: route.matchRegexString,
// NOTE this is the path definition, not the actual path
// E.g. /blog/post/{id:Int}
pathDefinition: route.path,
hasParams: route.hasParams,
routeHooks: FIXME_constructRouteHookPath(route.routeHooks),
redirect: route.redirect
? {
to: route.redirect?.to,
permanent: false,
}
: null,
renderMode: route.renderMode,
}

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 })
}
Loading