diff --git a/packages/cli/src/commands/serveBothHandler.js b/packages/cli/src/commands/serveBothHandler.js index 28ace745bbcf..349c4563f900 100644 --- a/packages/cli/src/commands/serveBothHandler.js +++ b/packages/cli/src/commands/serveBothHandler.js @@ -19,10 +19,7 @@ export const bothExperimentalServerFileHandler = async () => { await execa( 'node', - [ - '--conditions react-server', - './node_modules/@redwoodjs/vite/dist/runRscFeServer.js', - ], + ['./node_modules/@redwoodjs/vite/dist/runRscFeServer.js'], { cwd: getPaths().base, stdio: 'inherit', @@ -64,9 +61,9 @@ export const bothRscServerHandler = async (argv) => { const fePromise = execa( 'node', [ + // TODO (RSC): Do we need these on the worker thread? '--experimental-loader @redwoodjs/vite/node-loader', '--experimental-loader @redwoodjs/vite/react-node-loader', - '--conditions react-server', './node_modules/@redwoodjs/vite/dist/runRscFeServer.js', ], { diff --git a/packages/vite/src/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index 115f257848d5..e03db1368a7b 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -3,7 +3,8 @@ import type { Request, Response } from 'express' import RSDWServer from 'react-server-dom-webpack/server.node.unbundled' import { hasStatusCode } from '../lib/StatusError' -import { renderRSC } from '../waku-lib/rsc-handler-worker' + +import { renderRsc } from './rscWorkerCommunication' const { decodeReply, decodeReplyFromBusboy } = RSDWServer @@ -40,12 +41,39 @@ export function createRscRequestHandler() { } if (rsfId) { + console.log('headers', req.headers) if (req.headers['content-type']?.startsWith('multipart/form-data')) { const bb = busboy({ headers: req.headers }) const reply = decodeReplyFromBusboy(bb) req.pipe(bb) args = await reply + + // TODO (RSC): Loop over args (to not only look at args[0]) + // TODO (RSC): Verify that this works with node16 (MDN says FormData is + // only supported in node18 and up) + if (args[0] instanceof FormData) { + const serializedFormData: Record = {} + + for (const [key, value] of args[0]) { + // Several form fields can share the same name. This should be + // represented as an array of the values of all those fields + if (serializedFormData[key] !== undefined) { + if (!Array.isArray(serializedFormData[key])) { + serializedFormData[key] = [serializedFormData[key]] + } + + serializedFormData[key].push(value) + } else { + serializedFormData[key] = value + } + } + + args[0] = { + __formData__: true, + state: serializedFormData, + } + } } else { let body = '' @@ -82,7 +110,7 @@ export function createRscRequestHandler() { } try { - const pipeable = await renderRSC({ rscId, props, rsfId, args }) + const pipeable = await renderRsc({ rscId, props, rsfId, args }) // TODO (RSC): See if we can/need to do more error handling here // pipeable.on(handleError) pipeable.pipe(res) diff --git a/packages/vite/src/rsc/rsc-worker.ts b/packages/vite/src/rsc/rscWorker.ts similarity index 73% rename from packages/vite/src/rsc/rsc-worker.ts rename to packages/vite/src/rsc/rscWorker.ts index b8dc72d9f5ad..19d2c8f72493 100644 --- a/packages/vite/src/rsc/rsc-worker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -1,7 +1,10 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder -// import fs from 'node:fs' +// This is a dedicated worker for RSCs. +// It's needed because the main process can't be loaded with +// `--condition react-server`. If we did try to do that the main process +// couldn't do SSR because it would be missing client-side React functions +// like `useState` and `createContext`. import path from 'node:path' -import type { Writable } from 'node:stream' +import { Writable } from 'node:stream' import { parentPort } from 'node:worker_threads' import { createElement } from 'react' @@ -21,7 +24,11 @@ import { } from '../waku-lib/vite-plugin-rsc' // import type { unstable_GetCustomModules } from '../waku-server' -import type { RenderInput, MessageRes } from './rsc-handler' +import type { + RenderInput, + MessageRes, + MessageReq, +} from './rscWorkerCommunication' // import type { RenderInput, MessageReq, MessageRes } from './rsc-handler' // import { transformRsfId, generatePrefetchCode } from './rsc-utils' @@ -30,75 +37,77 @@ const { renderToPipeableStream } = RSDWServer type Entries = { default: ReturnType } type PipeableStream = { pipe(destination: T): T } -// const handleSetClientEntries = async ( -// mesg: MessageReq & { type: 'setClientEntries' } -// ) => { -// const { id, value } = mesg -// try { -// await setClientEntries(value) +const handleSetClientEntries = async ({ + id, + value, +}: MessageReq & { type: 'setClientEntries' }) => { + try { + await setClientEntries(value) -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } + if (!parentPort) { + throw new Error('parentPort is undefined') + } -// const message: MessageRes = { id, type: 'end' } -// parentPort.postMessage(message) -// } catch (err) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } + const message: MessageRes = { id, type: 'end' } + parentPort.postMessage(message) + } catch (err) { + if (!parentPort) { + throw new Error('parentPort is undefined') + } -// const message: MessageRes = { id, type: 'err', err } -// parentPort.postMessage(message) -// } -// } + const message: MessageRes = { id, type: 'err', err } + parentPort.postMessage(message) + } +} -// const handleRender = async (message: MessageReq & { type: 'render' }) => { -// const { id, input } = message +const handleRender = async ({ id, input }: MessageReq & { type: 'render' }) => { + console.log('handleRender', id, input) + + try { + const pipeable = await renderRsc(input) + + const writable = new Writable({ + write(chunk, encoding, callback) { + if (encoding !== ('buffer' as any)) { + throw new Error('Unknown encoding') + } + + if (!parentPort) { + throw new Error('parentPort is undefined') + } + + const buffer: Buffer = chunk + const message: MessageRes = { + id, + type: 'buf', + buf: buffer.buffer, + offset: buffer.byteOffset, + len: buffer.length, + } + parentPort.postMessage(message, [message.buf]) + callback() + }, + final(callback) { + if (!parentPort) { + throw new Error('parentPort is undefined') + } + + const message: MessageRes = { id, type: 'end' } + parentPort.postMessage(message) + callback() + }, + }) -// try { -// const pipeable = await renderRSC(input) -// const writable = new Writable({ -// write(chunk, encoding, callback) { -// if (encoding !== ('buffer' as any)) { -// throw new Error('Unknown encoding') -// } - -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const buffer: Buffer = chunk -// const msg: MessageRes = { -// id, -// type: 'buf', -// buf: buffer.buffer, -// offset: buffer.byteOffset, -// len: buffer.length, -// } -// parentPort.postMessage(msg, [msg.buf]) -// callback() -// }, -// final(callback) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const mesg: MessageRes = { id, type: 'end' } -// parentPort.postMessage(mesg) -// callback() -// }, -// }) -// pipeable.pipe(writable) -// } catch (err) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } + pipeable.pipe(writable) + } catch (err) { + if (!parentPort) { + throw new Error('parentPort is undefined') + } -// const mesg: MessageRes = { id, type: 'err', err } -// parentPort.postMessage(mesg) -// } -// } + const message: MessageRes = { id, type: 'err', err } + parentPort.postMessage(message) + } +} // const handleGetCustomModules = async ( // mesg: MessageReq & { type: 'getCustomModules' } @@ -162,38 +171,42 @@ const vitePromise = createServer({ appType: 'custom', }) -// const shutdown = async () => { -// const vite = await vitePromise -// await vite.close() -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } +const shutdown = async () => { + const vite = await vitePromise + await vite.close() + if (!parentPort) { + throw new Error('parentPort is undefined') + } -// parentPort.close() -// } + parentPort.close() +} const loadServerFile = async (fname: string) => { const vite = await vitePromise return vite.ssrLoadModule(fname) } -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } +if (!parentPort) { + throw new Error('parentPort is undefined') +} -// parentPort.on('message', (mesg: MessageReq) => { -// if (mesg.type === 'shutdown') { -// shutdown() -// } else if (mesg.type === 'setClientEntries') { -// handleSetClientEntries(mesg) -// } else if (mesg.type === 'render') { -// handleRender(mesg) -// } else if (mesg.type === 'getCustomModules') { -// handleGetCustomModules(mesg) -// } else if (mesg.type === 'build') { -// handleBuild(mesg) -// } -// }) +parentPort.on('message', (message: MessageReq) => { + console.log('message', message) + console.log('message.input.args', (message as any).input?.args) + console.log('message.input.args[0]', (message as any).input?.args?.[0]) + + if (message.type === 'shutdown') { + shutdown() + } else if (message.type === 'setClientEntries') { + handleSetClientEntries(message) + } else if (message.type === 'render') { + handleRender(message) + // } else if (message.type === 'getCustomModules') { + // handleGetCustomModules(message) + // } else if (message.type === 'build') { + // handleBuild(message) + } +}) const configPromise = resolveConfig('serve') @@ -253,7 +266,7 @@ const resolveClientEntry = ( return clientEntry } -export async function setClientEntries( +async function setClientEntries( value: 'load' | Record ): Promise { if (value !== 'load') { @@ -286,7 +299,16 @@ export async function setClientEntries( ) } -export async function renderRSC(input: RenderInput): Promise { +interface SerializedFormData { + __formData__: boolean + state: Record +} + +function isSerializedFormData(data?: unknown): data is SerializedFormData { + return !!data && (data as SerializedFormData)?.__formData__ +} + +async function renderRsc(input: RenderInput): Promise { const config = await configPromise const bundlerConfig = new Proxy( {}, @@ -303,13 +325,31 @@ export async function renderRSC(input: RenderInput): Promise { } ) - console.log('renderRSC input', input) + console.log('renderRsc input', input) if (input.rsfId && input.args) { const [fileId, name] = input.rsfId.split('#') const fname = path.join(config.root, fileId) - const mod = await loadServerFile(fname) - const data = await (mod[name] || mod)(...input.args) + console.log('Server Action, fileId', fileId, 'name', name, 'fname', fname) + const module = await loadServerFile(fname) + + if (isSerializedFormData(input.args[0])) { + const formData = new FormData() + + Object.entries(input.args[0].state).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => { + formData.append(key, v) + }) + } else { + formData.append(key, value) + } + }) + + input.args[0] = formData + } + + const data = await (module[name] || module)(...input.args) if (!input.rscId) { return renderToPipeableStream(data, bundlerConfig) } diff --git a/packages/vite/src/rsc/rsc-handler.ts b/packages/vite/src/rsc/rscWorkerCommunication.ts similarity index 91% rename from packages/vite/src/rsc/rsc-handler.ts rename to packages/vite/src/rsc/rscWorkerCommunication.ts index 997d4a954ced..6570c0125c51 100644 --- a/packages/vite/src/rsc/rsc-handler.ts +++ b/packages/vite/src/rsc/rscWorkerCommunication.ts @@ -1,9 +1,9 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder +import path from 'node:path' import { PassThrough } from 'node:stream' import type { Readable } from 'node:stream' import { Worker } from 'node:worker_threads' -const worker = new Worker(new URL('rsc-worker.js', import.meta.url), { +const worker = new Worker(path.join(__dirname, 'rscWorker.js'), { execArgv: ['--conditions', 'react-server'], }) @@ -100,7 +100,10 @@ export function setClientEntries( }) } -export function renderRSC(input: RenderInput): Readable { +export function renderRsc(input: RenderInput): Readable { + // TODO (RSC): What's the biggest number JS handles here? What happens when + // it overflows? Will it just start over at 0? If so, we should be fine. If + // not, we need to figure out a more robust way to handle this. const id = nextId++ const passthrough = new PassThrough() diff --git a/packages/vite/src/runRscFeServer.ts b/packages/vite/src/runRscFeServer.ts index 998d51523ebf..6ef02ae1f01e 100644 --- a/packages/vite/src/runRscFeServer.ts +++ b/packages/vite/src/runRscFeServer.ts @@ -15,8 +15,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 { registerFwGlobals } from './streaming/registerGlobals' -import { setClientEntries } from './waku-lib/rsc-handler-worker' /** * TODO (STREAMING) diff --git a/packages/vite/src/waku-lib/builder.ts b/packages/vite/src/waku-lib/builder.ts index 1fb00a8f77ed..0a41a36164df 100644 --- a/packages/vite/src/waku-lib/builder.ts +++ b/packages/vite/src/waku-lib/builder.ts @@ -7,14 +7,14 @@ import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' import { onWarn } from '../lib/onWarn' - -import { configFileConfig, resolveConfig } from './config' import { shutdown, setClientEntries, getCustomModulesRSC, - buildRSC, -} from '../rsc/rsc-handler' + // buildRsc, +} from '../rsc/rscWorkerCommunication' + +import { configFileConfig, resolveConfig } from './config' import { rscIndexPlugin, rscAnalyzePlugin } from './vite-plugin-rsc' export async function build() { @@ -169,7 +169,7 @@ export async function build() { ) await setClientEntries(absoluteClientEntries) - await buildRSC() + // await buildRsc() const origPackageJson = require(path.join(config.root, 'package.json')) const packageJson = { diff --git a/packages/vite/src/waku-lib/rsc-handler-worker.ts b/packages/vite/src/waku-lib/rsc-handler-worker.ts deleted file mode 100644 index ef1443f30160..000000000000 --- a/packages/vite/src/waku-lib/rsc-handler-worker.ts +++ /dev/null @@ -1,460 +0,0 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder -// import fs from 'node:fs' -import path from 'node:path' -import type { Writable } from 'node:stream' -import { parentPort } from 'node:worker_threads' - -import { createElement } from 'react' - -import RSDWServer from 'react-server-dom-webpack/server' -import { createServer } from 'vite' - -import { getPaths } from '@redwoodjs/project-config' - -import type { defineEntries } from '../entries' -import { StatusError } from '../lib/StatusError' -// import type { unstable_GetCustomModules } from '../waku-server' -import type { RenderInput, MessageRes } from '../rsc/rsc-handler' - -import { configFileConfig, resolveConfig } from './config' -// import type { RenderInput, MessageReq, MessageRes } from './rsc-handler' -// import { transformRsfId, generatePrefetchCode } from './rsc-utils' -import { transformRsfId } from './rsc-utils' -import { rscTransformPlugin, rscReloadPlugin } from './vite-plugin-rsc' - -const { renderToPipeableStream } = RSDWServer - -type Entries = { default: ReturnType } -type PipeableStream = { pipe(destination: T): T } - -// const handleSetClientEntries = async ( -// mesg: MessageReq & { type: 'setClientEntries' } -// ) => { -// const { id, value } = mesg -// try { -// await setClientEntries(value) - -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const message: MessageRes = { id, type: 'end' } -// parentPort.postMessage(message) -// } catch (err) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const message: MessageRes = { id, type: 'err', err } -// parentPort.postMessage(message) -// } -// } - -// const handleRender = async (message: MessageReq & { type: 'render' }) => { -// const { id, input } = message - -// try { -// const pipeable = await renderRSC(input) -// const writable = new Writable({ -// write(chunk, encoding, callback) { -// if (encoding !== ('buffer' as any)) { -// throw new Error('Unknown encoding') -// } - -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const buffer: Buffer = chunk -// const msg: MessageRes = { -// id, -// type: 'buf', -// buf: buffer.buffer, -// offset: buffer.byteOffset, -// len: buffer.length, -// } -// parentPort.postMessage(msg, [msg.buf]) -// callback() -// }, -// final(callback) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const mesg: MessageRes = { id, type: 'end' } -// parentPort.postMessage(mesg) -// callback() -// }, -// }) -// pipeable.pipe(writable) -// } catch (err) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const mesg: MessageRes = { id, type: 'err', err } -// parentPort.postMessage(mesg) -// } -// } - -// const handleGetCustomModules = async ( -// mesg: MessageReq & { type: 'getCustomModules' } -// ) => { -// const { id } = mesg -// try { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const modules = await getCustomModulesRSC() -// const mesg: MessageRes = { id, type: 'customModules', modules } -// parentPort.postMessage(mesg) -// } catch (err) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const mesg: MessageRes = { id, type: 'err', err } -// parentPort.postMessage(mesg) -// } -// } - -// const handleBuild = async (mesg: MessageReq & { type: 'build' }) => { -// const { id } = mesg -// try { -// await buildRSC() - -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const mesg: MessageRes = { id, type: 'end' } -// parentPort.postMessage(mesg) -// } catch (err) { -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// const mesg: MessageRes = { id, type: 'err', err } -// parentPort.postMessage(mesg) -// } -// } - -const vitePromise = createServer({ - ...configFileConfig, - plugins: [ - rscTransformPlugin(), - rscReloadPlugin((type) => { - if (!parentPort) { - throw new Error('parentPort is undefined') - } - - const message: MessageRes = { type } - parentPort.postMessage(message) - }), - ], - resolve: { - conditions: ['react-server'], - }, - appType: 'custom', -}) - -// const shutdown = async () => { -// const vite = await vitePromise -// await vite.close() -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// parentPort.close() -// } - -const loadServerFile = async (fname: string) => { - const vite = await vitePromise - return vite.ssrLoadModule(fname) -} - -// if (!parentPort) { -// throw new Error('parentPort is undefined') -// } - -// parentPort.on('message', (mesg: MessageReq) => { -// if (mesg.type === 'shutdown') { -// shutdown() -// } else if (mesg.type === 'setClientEntries') { -// handleSetClientEntries(mesg) -// } else if (mesg.type === 'render') { -// handleRender(mesg) -// } else if (mesg.type === 'getCustomModules') { -// handleGetCustomModules(mesg) -// } else if (mesg.type === 'build') { -// handleBuild(mesg) -// } -// }) - -const configPromise = resolveConfig('serve') - -const getEntriesFile = async ( - config: Awaited>, - isBuild: boolean -) => { - const rwPaths = getPaths() - - if (isBuild) { - return path.join( - config.root, - config.build.outDir, - config.framework.entriesJs - ) - } - - return rwPaths.web.distServerEntries -} - -const getFunctionComponent = async ( - rscId: string, - config: Awaited>, - isBuild: boolean -) => { - const entriesFile = await getEntriesFile(config, isBuild) - const { - default: { getEntry }, - } = await (loadServerFile(entriesFile) as Promise) - const mod = await getEntry(rscId) - if (typeof mod === 'function') { - return mod - } - if (typeof mod?.default === 'function') { - return mod?.default - } - // TODO (RSC): Making this a 404 error is marked as "HACK" in waku's source - throw new StatusError('No function component found', 404) -} - -let absoluteClientEntries: Record = {} - -const resolveClientEntry = ( - config: Awaited>, - filePath: string -) => { - const clientEntry = absoluteClientEntries[filePath] - - if (!clientEntry) { - if (absoluteClientEntries['*'] === '*') { - return config.base + path.relative(config.root, filePath) - } - - throw new Error('No client entry found for ' + filePath) - } - - return clientEntry -} - -export async function setClientEntries( - value: 'load' | Record -): Promise { - if (value !== 'load') { - absoluteClientEntries = value - return - } - const config = await configPromise - const entriesFile = await getEntriesFile(config, false) - console.log('setClientEntries :: entriesFile', entriesFile) - const { clientEntries } = await loadServerFile(entriesFile) - console.log('setClientEntries :: clientEntries', clientEntries) - if (!clientEntries) { - throw new Error('Failed to load clientEntries') - } - const baseDir = path.dirname(entriesFile) - absoluteClientEntries = Object.fromEntries( - Object.entries(clientEntries).map(([key, val]) => { - let fullKey = path.join(baseDir, key) - if (process.platform === 'win32') { - fullKey = fullKey.replaceAll('\\', '/') - } - console.log('fullKey', fullKey, 'value', config.base + val) - return [fullKey, config.base + val] - }) - ) - - console.log( - 'setClientEntries :: absoluteClientEntries', - absoluteClientEntries - ) -} - -export async function renderRSC(input: RenderInput): Promise { - const config = await configPromise - const bundlerConfig = new Proxy( - {}, - { - get(_target, encodedId: string) { - console.log('Proxy get', encodedId) - const [filePath, name] = encodedId.split('#') as [string, string] - // filePath /Users/tobbe/dev/waku/examples/01_counter/dist/assets/rsc0.js - // name Counter - const id = resolveClientEntry(config, filePath) - // id /assets/rsc0-beb48afe.js - return { id, chunks: [id], name, async: true } - }, - } - ) - - console.log('renderRSC input', input) - - if (input.rsfId && input.args) { - const [fileId, name] = input.rsfId.split('#') - const fname = path.join(config.root, fileId) - const mod = await loadServerFile(fname) - const data = await (mod[name] || mod)(...input.args) - if (!input.rscId) { - return renderToPipeableStream(data, bundlerConfig) - } - // continue for mutation mode - } - - if (input.rscId && input.props) { - const component = await getFunctionComponent(input.rscId, config, false) - return renderToPipeableStream( - createElement(component, input.props), - bundlerConfig - ).pipe(transformRsfId(config.root)) - } - - throw new Error('Unexpected input') -} - -// async function getCustomModulesRSC(): Promise<{ [name: string]: string }> { -// const config = await configPromise -// const entriesFile = await getEntriesFile(config, false) -// const { -// default: { unstable_getCustomModules: getCustomModules }, -// } = await (loadServerFile(entriesFile) as Promise<{ -// default: Entries['default'] & { -// unstable_getCustomModules?: unstable_GetCustomModules -// } -// }>) -// if (!getCustomModules) { -// return {} -// } -// const modules = await getCustomModules() -// return modules -// } - -// // FIXME this may take too much responsibility -// async function buildRSC(): Promise { -// const config = await resolveConfig('build') -// const basePath = config.base + config.framework.rscPrefix -// const distEntriesFile = await getEntriesFile(config, true) -// const { -// default: { getBuilder }, -// } = await (loadServerFile(distEntriesFile) as Promise) -// if (!getBuilder) { -// console.warn( -// "getBuilder is undefined. It's recommended for optimization and sometimes required." -// ) -// return -// } - -// // FIXME this doesn't seem an ideal solution -// const decodeId = (encodedId: string): [id: string, name: string] => { -// const [filePath, name] = encodedId.split('#') as [string, string] -// const id = resolveClientEntry(config, filePath) -// return [id, name] -// } - -// const pathMap = await getBuilder(decodeId) -// const clientModuleMap = new Map>() -// const addClientModule = (pathStr: string, id: string) => { -// let idSet = clientModuleMap.get(pathStr) -// if (!idSet) { -// idSet = new Set() -// clientModuleMap.set(pathStr, idSet) -// } -// idSet.add(id) -// } -// await Promise.all( -// Object.entries(pathMap).map(async ([pathStr, { elements }]) => { -// for (const [rscId, props] of elements || []) { -// // FIXME we blindly expect JSON.stringify usage is deterministic -// const serializedProps = JSON.stringify(props) -// const searchParams = new URLSearchParams() -// searchParams.set('props', serializedProps) -// const destFile = path.join( -// config.root, -// config.build.outDir, -// config.framework.outPublic, -// config.framework.rscPrefix, -// decodeURIComponent(rscId), -// decodeURIComponent(`${searchParams}`) -// ) -// fs.mkdirSync(path.dirname(destFile), { recursive: true }) -// const bundlerConfig = new Proxy( -// {}, -// { -// get(_target, encodedId: string) { -// const [id, name] = decodeId(encodedId) -// addClientModule(pathStr, id) -// return { id, chunks: [id], name, async: true } -// }, -// } -// ) -// const component = await getFunctionComponent(rscId, config, true) -// const pipeable = renderToPipeableStream( -// createElement(component, props as any), -// bundlerConfig -// ).pipe(transformRsfId(path.join(config.root, config.build.outDir))) -// await new Promise((resolve, reject) => { -// const stream = fs.createWriteStream(destFile) -// stream.on('finish', resolve) -// stream.on('error', reject) -// pipeable.pipe(stream) -// }) -// } -// }) -// ) - -// const publicIndexHtmlFile = path.join( -// config.root, -// config.build.outDir, -// config.framework.outPublic, -// config.framework.indexHtml -// ) -// const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { -// encoding: 'utf8', -// }) -// await Promise.all( -// Object.entries(pathMap).map(async ([pathStr, { elements, customCode }]) => { -// const destFile = path.join( -// config.root, -// config.build.outDir, -// config.framework.outPublic, -// pathStr, -// pathStr.endsWith('/') ? 'index.html' : '' -// ) -// let data = '' -// if (fs.existsSync(destFile)) { -// data = fs.readFileSync(destFile, { encoding: 'utf8' }) -// } else { -// fs.mkdirSync(path.dirname(destFile), { recursive: true }) -// data = publicIndexHtml -// } -// const code = -// generatePrefetchCode( -// basePath, -// Array.from(elements || []).flatMap(([rscId, props, skipPrefetch]) => { -// if (skipPrefetch) { -// return [] -// } -// return [[rscId, props]] -// }), -// clientModuleMap.get(pathStr) || [] -// ) + (customCode || '') -// if (code) { -// // HACK is this too naive to inject script code? -// data = data.replace(/<\/body>/, ``) -// } -// fs.writeFileSync(destFile, data, { encoding: 'utf8' }) -// }) -// ) -// }