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: Transform client components during build #10048

Merged
merged 11 commits into from
Feb 22, 2024
5 changes: 4 additions & 1 deletion packages/vite/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ const checkStatus = async (
const BASE_PATH = '/rw-rsc/'

export function serve<Props>(rscId: string) {
console.log('serve rscId', rscId)

type SetRerender = (
rerender: (next: [ReactElement, string]) => void
) => () => void

const fetchRSC = cache(
(serializedProps: string): readonly [React.ReactElement, SetRerender] => {
console.log('fetchRSC serializedProps', serializedProps)
Expand Down Expand Up @@ -97,7 +100,7 @@ export function serve<Props>(rscId: string) {
// Create temporary client component that wraps the ServerComponent returned
// by the `createFromFetch` call.
const ServerComponent = (props: Props) => {
console.log('ServerComponent props', props)
console.log('ServerComponent', rscId, 'props', props)

// FIXME we blindly expect JSON.stringify usage is deterministic
const serializedProps = JSON.stringify(props || {})
Expand Down
31 changes: 25 additions & 6 deletions packages/vite/src/react-server-dom-webpack/node-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
// Copied from https://github.com/facebook/react/blob/8ec962d825fc948ffda5ab863e639cd4158935ba/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js
// and converted to TypeScript.

import path from 'node:path'

import * as acorn from 'acorn-loose'

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

interface ResolveContext {
parentURL: string | void
conditions: Array<string>
Expand Down Expand Up @@ -374,13 +378,25 @@ async function parseExportNamesIntoNames(
async function transformClientModule(
body: any,
url: string,
loader: LoadFunction
loader: LoadFunction,
clientEntryFiles?: Record<string, string>
): Promise<string> {
const names: Array<string> = []

// This will insert the names into the `names` array
await parseExportNamesIntoNames(body, names, url, loader)

const entryRecord = Object.entries(clientEntryFiles || {}).find(
([_key, value]) => value === url
)

// TODO (RSC): Check if we always find a record. If we do, we should
// throw an error if it's undefined

const loadId = entryRecord
? path.join(getPaths().web.distServer, 'assets', entryRecord[0] + '.js')
: url

let newSrc =
"const CLIENT_REFERENCE = Symbol.for('react.client.reference');\n"

Expand Down Expand Up @@ -420,7 +436,7 @@ async function transformClientModule(

newSrc += '},{'
newSrc += '$$typeof: {value: CLIENT_REFERENCE},'
newSrc += '$$id: {value: ' + JSON.stringify(url + '#' + name) + '}'
newSrc += '$$id: {value: ' + JSON.stringify(loadId + '#' + name) + '}'
newSrc += '});\n'
}

Expand Down Expand Up @@ -461,7 +477,8 @@ async function loadClientImport(
async function transformModuleIfNeeded(
source: string,
url: string,
loader: LoadFunction
loader: LoadFunction,
clientEntryFile?: Record<string, string>
): Promise<string> {
// Do a quick check for the exact string. If it doesn't exist, don't
// bother parsing.
Expand Down Expand Up @@ -511,7 +528,7 @@ async function transformModuleIfNeeded(
}

if (useClient) {
return transformClientModule(body, url, loader)
return transformClientModule(body, url, loader, clientEntryFile)
}

return transformServerModule(source, body, url)
Expand Down Expand Up @@ -552,7 +569,8 @@ export async function transformSource(
export async function load(
url: string,
context: LoadContext | null,
defaultLoad: LoadFunction
defaultLoad: LoadFunction,
clientEntryFiles?: Record<string, string>
): Promise<{ format: string; shortCircuit?: boolean; source: Source }> {
const result = await defaultLoad(url, context, defaultLoad)

Expand All @@ -564,7 +582,8 @@ export async function load(
const newSrc = await transformModuleIfNeeded(
result.source,
url,
defaultLoad
defaultLoad,
clientEntryFiles
)

return {
Expand Down
9 changes: 9 additions & 0 deletions packages/vite/src/rsc/rscBuildServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { getConfig, getPaths } from '@redwoodjs/project-config'
import { getEnvVarDefinitions } from '../envVarDefinitions'
import { onWarn } from '../lib/onWarn'

import { rscTransformPlugin } from './rscVitePlugins'

/**
* RSC build. Step 3.
* buildFeServer -> buildRscFeServer -> rscBuildClient
Expand Down Expand Up @@ -91,6 +93,13 @@ export async function rscBuildServer(
}),
},
}),
// The rscTransformPlugin maps paths like
// /Users/tobbe/.../rw-app/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js
// to
// /Users/tobbe/.../rw-app/web/dist/server/assets/rsc0.js
// That's why it needs the `clientEntryFiles` data
// (It does other things as well, but that's why it needs clientEntryFiles)
rscTransformPlugin(clientEntryFiles),
],
build: {
ssr: true,
Expand Down
6 changes: 4 additions & 2 deletions packages/vite/src/rsc/rscVitePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export function rscIndexPlugin(): Plugin {
}
}

export function rscTransformPlugin(): Plugin {
export function rscTransformPlugin(
clientEntryFiles: Record<string, string>
): Plugin {
return {
name: 'rsc-transform-plugin',
// TODO(RSC): Seems like resolveId() is never called. Can we remove it?
Expand Down Expand Up @@ -115,7 +117,7 @@ export function rscTransformPlugin(): Plugin {
return { format: 'module', source }
}

const mod = await RSDWNodeLoader.load(id, null, load)
const mod = await RSDWNodeLoader.load(id, null, load, clientEntryFiles)

return mod.source
},
Expand Down
12 changes: 6 additions & 6 deletions packages/vite/src/rsc/rscWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { defineEntries } from '../entries'
import { registerFwGlobals } from '../lib/registerGlobals'
import { StatusError } from '../lib/StatusError'

import { rscTransformPlugin, rscReloadPlugin } from './rscVitePlugins'
import { rscReloadPlugin } from './rscVitePlugins'
import type {
RenderInput,
MessageRes,
Expand Down Expand Up @@ -162,7 +162,6 @@ registerFwGlobals()
// `envFile: false`?
const vitePromise = createServer({
plugins: [
rscTransformPlugin(),
rscReloadPlugin((type) => {
if (!parentPort) {
throw new Error('parentPort is undefined')
Expand Down Expand Up @@ -260,17 +259,18 @@ const resolveClientEntry = (
config: Awaited<ReturnType<typeof resolveConfig>>,
filePath: string
) => {
const clientEntry = absoluteClientEntries[filePath]
const filePathSlash = filePath.replaceAll('\\', '/')
const clientEntry = absoluteClientEntries[filePathSlash]

console.log('absoluteClientEntries', absoluteClientEntries)
console.log('filePath', filePath)
console.log('filePath', filePathSlash)

if (!clientEntry) {
if (absoluteClientEntries['*'] === '*') {
return config.base + path.relative(config.root, filePath)
return config.base + path.relative(config.root, filePathSlash)
}

throw new Error('No client entry found for ' + filePath)
throw new Error('No client entry found for ' + filePathSlash)
}

return clientEntry
Expand Down
Loading