diff --git a/packages/vite/src/rsc/rscRenderer.ts b/packages/vite/src/rsc/rscRenderer.ts new file mode 100644 index 000000000000..f5e97e60bd70 --- /dev/null +++ b/packages/vite/src/rsc/rscRenderer.ts @@ -0,0 +1,197 @@ +import path from 'node:path' +import type { ReadableStream } from 'node:stream/web' + +import { createElement } from 'react' + +import { renderToReadableStream } from 'react-server-dom-webpack/server.edge' + +import type { ServerAuthState } from '@redwoodjs/auth/dist/AuthProvider/ServerAuthProvider.js' +import { getPaths } from '@redwoodjs/project-config' + +import type { RscFetchProps } from '../../../router/src/rsc/ClientRouter.tsx' +import { getEntriesFromDist } from '../lib/entries.js' +import { StatusError } from '../lib/StatusError.js' + +export type RenderInput = { + rscId?: string | undefined + props: RscFetchProps | Record + rsaId?: string | undefined + args?: unknown[] | undefined + serverState: { + headersInit: Record + fullUrl: string + serverAuthState: ServerAuthState + } +} + +let absoluteClientEntries: Record = {} + +async function loadServerFile(filePath: string) { + console.log('rscRenderer.ts loadServerFile filePath', filePath) + return import(`file://${filePath}`) +} + +const getRoutesComponent: any = async () => { + const serverEntries = await getEntriesFromDist() + console.log('rscRenderer.ts serverEntries', serverEntries) + + const routesPath = path.join( + getPaths().web.distRsc, + serverEntries['__rwjs__Routes'], + ) + + if (!routesPath) { + throw new StatusError('No entry found for __rwjs__Routes', 404) + } + + const routes = await loadServerFile(routesPath) + + return routes.default +} + +export async function setClientEntries(): Promise { + const entriesFile = getPaths().web.distRscEntries + 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) + + // Convert to absolute paths + absoluteClientEntries = Object.fromEntries( + Object.entries(clientEntries).map(([key, val]) => { + let fullKey = path.join(baseDir, key) + + if (process.platform === 'win32') { + fullKey = fullKey.replaceAll('\\', '/') + } + + return [fullKey, '/' + val] + }), + ) + + console.log( + 'setClientEntries :: absoluteClientEntries', + absoluteClientEntries, + ) +} + +function getBundlerConfig() { + // TODO (RSC): Try removing the proxy here and see if it's really necessary. + // Looks like it'd work to just have a regular object with a getter. + // Remove the proxy and see what breaks. + const bundlerConfig = new Proxy( + {}, + { + get(_target, encodedId: string) { + console.log('Proxy get encodedId', encodedId) + const [filePath, name] = encodedId.split('#') as [string, string] + // filePath /Users/tobbe/dev/waku/examples/01_counter/dist/assets/rsc0.js + // name Counter + + const filePathSlash = filePath.replaceAll('\\', '/') + const id = absoluteClientEntries[filePathSlash] + + console.log('absoluteClientEntries', absoluteClientEntries) + console.log('filePath', filePathSlash) + + if (!id) { + throw new Error('No client entry found for ' + filePathSlash) + } + + console.log('rscRenderer proxy id', id) + // id /assets/rsc0-beb48afe.js + return { id, chunks: [id], name, async: true } + }, + }, + ) + + return bundlerConfig +} + +export async function renderRsc(input: RenderInput): Promise { + if (input.rsaId || !input.args) { + throw new Error( + "Unexpected input. Can't request both RSCs and execute RSAs at the same time.", + ) + } + + if (!input.rscId || !input.props) { + throw new Error('Unexpected input. Missing rscId or props.') + } + + console.log('renderRsc input', input) + + const serverRoutes = await getRoutesComponent() + // TODO (RSC): Should this have the same shape as for handleRsa? + const model = createElement(serverRoutes, input.props) + + console.log('rscRenderer.ts renderRsc renderRsc props', input.props) + console.log('rscRenderer.ts renderRsc model', model) + + return renderToReadableStream(model, getBundlerConfig()) + // TODO (RSC): We used to transform() the stream here to remove + // "prefixToRemove", which was the common base path to all filenames. We + // then added it back in handleRsa with a simple + // `path.join(config.root, fileId)`. I removed all of that for now to + // simplify the code. But if we wanted to add it back in the future to save + // some bytes in all the Flight data we could. +} + +interface SerializedFormData { + __formData__: boolean + state: Record +} + +function isSerializedFormData(data?: unknown): data is SerializedFormData { + return !!data && (data as SerializedFormData)?.__formData__ +} + +export async function executeRsa(input: RenderInput): Promise { + console.log('handleRsa input', input) + + if (!input.rsaId || !input.args) { + throw new Error('Unexpected input') + } + + const [fileName, actionName] = input.rsaId.split('#') + console.log('Server Action fileName', fileName, 'actionName', actionName) + const module = await loadServerFile(fileName) + + 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 method = module[actionName] || module + console.log('rscRenderer.ts method', method) + console.log('rscRenderer.ts args', ...input.args) + + const data = await method(...input.args) + console.log('rscRenderer.ts rsa return data', data) + + const serverRoutes = await getRoutesComponent() + console.log('rscRenderer.ts handleRsa serverRoutes', serverRoutes) + const model = { + Routes: createElement(serverRoutes, { + location: { pathname: '/', search: '' }, + }), + __rwjs__rsa_data: data, + } + console.log('rscRenderer.ts handleRsa model', model) + + return renderToReadableStream(model, getBundlerConfig()) +} diff --git a/packages/vite/src/rsc/rscStudioHandlers.ts b/packages/vite/src/rsc/rscStudioHandlers.ts index e118b0421ab7..d5a213234740 100644 --- a/packages/vite/src/rsc/rscStudioHandlers.ts +++ b/packages/vite/src/rsc/rscStudioHandlers.ts @@ -8,7 +8,7 @@ import { getAuthState, getRequestHeaders } from '@redwoodjs/server-store' import { getFullUrlForFlightRequest } from '../utils.js' -import type { RenderInput } from './rscWorkerCommunication.js' +import type { RenderInput } from './rscRenderer.js' import { renderRsc } from './rscWorkerCommunication.js' const isTest = () => { diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index 5308cd08cb2a..eeb6b194007a 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -4,31 +4,18 @@ // couldn't do SSR because it would be missing client-side React functions // like `useState` and `createContext`. import { Buffer } from 'node:buffer' -import path from 'node:path' -import type { ReadableStream } from 'node:stream/web' import { parentPort } from 'node:worker_threads' -import { createElement } from 'react' - -import { renderToReadableStream } from 'react-server-dom-webpack/server.edge' - -import { getPaths } from '@redwoodjs/project-config' import { createPerRequestMap, createServerStorage, } from '@redwoodjs/server-store' -import { getEntriesFromDist } from '../lib/entries.js' import { registerFwGlobalsAndShims } from '../lib/registerFwGlobalsAndShims.js' -import { StatusError } from '../lib/StatusError.js' -import type { - MessageReq, - MessageRes, - RenderInput, -} from './rscWorkerCommunication.js' +import { executeRsa, renderRsc, setClientEntries } from './rscRenderer.js' +import type { MessageReq, MessageRes } from './rscWorkerCommunication.js' -let absoluteClientEntries: Record = {} const serverStorage = createServerStorage() const handleSetClientEntries = async ({ @@ -68,7 +55,7 @@ const handleRender = async ({ id, input }: MessageReq & { type: 'render' }) => { // @MARK run render with map initialised const readable = input.rscId ? await renderRsc(input) - : await handleRsa(input) + : await executeRsa(input) const writable = new WritableStream({ write(chunk) { @@ -112,11 +99,6 @@ const handleRender = async ({ id, input }: MessageReq & { type: 'render' }) => { // server. So we have to register them here again. registerFwGlobalsAndShims() -async function loadServerFile(filePath: string) { - console.log('rscWorker.ts loadServerFile filePath', filePath) - return import(`file://${filePath}`) -} - if (!parentPort) { throw new Error('parentPort is undefined') } @@ -130,168 +112,3 @@ parentPort.on('message', (message: MessageReq) => { handleRender(message) } }) - -const getRoutesComponent: any = async () => { - const serverEntries = await getEntriesFromDist() - console.log('rscWorker.ts serverEntries', serverEntries) - - const routesPath = path.join( - getPaths().web.distRsc, - serverEntries['__rwjs__Routes'], - ) - - if (!routesPath) { - throw new StatusError('No entry found for __rwjs__Routes', 404) - } - - const routes = await loadServerFile(routesPath) - - return routes.default -} - -async function setClientEntries(): Promise { - const entriesFile = getPaths().web.distRscEntries - 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) - - // Convert to absolute paths - absoluteClientEntries = Object.fromEntries( - Object.entries(clientEntries).map(([key, val]) => { - let fullKey = path.join(baseDir, key) - - if (process.platform === 'win32') { - fullKey = fullKey.replaceAll('\\', '/') - } - - return [fullKey, '/' + val] - }), - ) - - console.log( - 'setClientEntries :: absoluteClientEntries', - absoluteClientEntries, - ) -} - -function getBundlerConfig() { - // TODO (RSC): Try removing the proxy here and see if it's really necessary. - // Looks like it'd work to just have a regular object with a getter. - // Remove the proxy and see what breaks. - const bundlerConfig = new Proxy( - {}, - { - get(_target, encodedId: string) { - console.log('Proxy get encodedId', encodedId) - const [filePath, name] = encodedId.split('#') as [string, string] - // filePath /Users/tobbe/dev/waku/examples/01_counter/dist/assets/rsc0.js - // name Counter - - const filePathSlash = filePath.replaceAll('\\', '/') - const id = absoluteClientEntries[filePathSlash] - - console.log('absoluteClientEntries', absoluteClientEntries) - console.log('filePath', filePathSlash) - - if (!id) { - throw new Error('No client entry found for ' + filePathSlash) - } - - console.log('rscWorker proxy id', id) - // id /assets/rsc0-beb48afe.js - return { id, chunks: [id], name, async: true } - }, - }, - ) - - return bundlerConfig -} - -async function renderRsc(input: RenderInput): Promise { - if (input.rsaId || !input.args) { - throw new Error( - "Unexpected input. Can't request both RSCs and execute RSAs at the same time.", - ) - } - - if (!input.rscId || !input.props) { - throw new Error('Unexpected input. Missing rscId or props.') - } - - console.log('renderRsc input', input) - - const serverRoutes = await getRoutesComponent() - // TODO (RSC): Should this have the same shape as for handleRsa? - const model = createElement(serverRoutes, input.props) - - console.log('rscWorker.ts renderRsc renderRsc props', input.props) - console.log('rscWorker.ts renderRsc model', model) - - return renderToReadableStream(model, getBundlerConfig()) - // TODO (RSC): We used to transform() the stream here to remove - // "prefixToRemove", which was the common base path to all filenames. We - // then added it back in handleRsa with a simple - // `path.join(config.root, fileId)`. I removed all of that for now to - // simplify the code. But if we wanted to add it back in the future to save - // some bytes in all the Flight data we could. -} - -interface SerializedFormData { - __formData__: boolean - state: Record -} - -function isSerializedFormData(data?: unknown): data is SerializedFormData { - return !!data && (data as SerializedFormData)?.__formData__ -} - -async function handleRsa(input: RenderInput): Promise { - console.log('handleRsa input', input) - - if (!input.rsaId || !input.args) { - throw new Error('Unexpected input') - } - - const [fileName, actionName] = input.rsaId.split('#') - console.log('Server Action fileName', fileName, 'actionName', actionName) - const module = await loadServerFile(fileName) - - 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 method = module[actionName] || module - console.log('rscWorker.ts method', method) - console.log('rscWorker.ts args', ...input.args) - - const data = await method(...input.args) - console.log('rscWorker.ts rsa return data', data) - - const serverRoutes = await getRoutesComponent() - console.log('rscWorker.ts handleRsa serverRoutes', serverRoutes) - const model = { - Routes: createElement(serverRoutes, { - location: { pathname: '/', search: '' }, - }), - __rwjs__rsa_data: data, - } - console.log('rscWorker.ts handleRsa model', model) - - return renderToReadableStream(model, getBundlerConfig()) -} diff --git a/packages/vite/src/rsc/rscWorkerCommunication.ts b/packages/vite/src/rsc/rscWorkerCommunication.ts index 6449182b659b..f05f23367535 100644 --- a/packages/vite/src/rsc/rscWorkerCommunication.ts +++ b/packages/vite/src/rsc/rscWorkerCommunication.ts @@ -4,9 +4,7 @@ import { PassThrough } from 'node:stream' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' -import type { ServerAuthState } from '@redwoodjs/auth/dist/AuthProvider/ServerAuthProvider.js' - -import type { RscFetchProps } from '../../../router/src/rsc/ClientRouter.tsx' +import type { RenderInput } from './rscRenderer.js' const workerPath = path.join( // __dirname. Use fileURLToPath for windows compatibility @@ -23,18 +21,6 @@ const worker = new Worker(workerPath, { ], }) -export type RenderInput = { - rscId?: string | undefined - props: RscFetchProps | Record - rsaId?: string | undefined - args?: unknown[] | undefined - serverState: { - headersInit: Record - fullUrl: string - serverAuthState: ServerAuthState - } -} - export type MessageReq = | { id: number