From d3475cd7041b20106bf73968a1d59dcae611afcd Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 11 Sep 2023 20:02:21 +0200 Subject: [PATCH] RSC: Make code and execution easier to follow. Improve error handling. (#9154) 1. Add code comments to make the code easier to follow 2. Add console logs to make execution easier to follow 3. Add custom Error to improve error handling --- packages/vite/src/buildRscFeServer.ts | 3 ++ packages/vite/src/client.ts | 29 ++++++++++++++++--- packages/vite/src/lib/StatusError.ts | 5 ++++ packages/vite/src/rscBuild.ts | 2 +- .../vite/src/waku-lib/rsc-handler-worker.ts | 6 +++- packages/vite/src/waku-lib/rsc-utils.ts | 8 +++-- packages/vite/src/waku-lib/vite-plugin-rsc.ts | 4 ++- 7 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 packages/vite/src/lib/StatusError.ts diff --git a/packages/vite/src/buildRscFeServer.ts b/packages/vite/src/buildRscFeServer.ts index 350d93deb504..0f36a4837e12 100644 --- a/packages/vite/src/buildRscFeServer.ts +++ b/packages/vite/src/buildRscFeServer.ts @@ -34,8 +34,10 @@ export const buildRscFeServer = async ({ webDistEntries, webRouteManifest, }: Args) => { + // Step 1: Analyze all files and generate a list of RSCs and RSFs const { clientEntryFiles, serverEntryFiles } = await rscBuild(viteConfigPath) + // Step 2: Generate the client bundle const clientBuildOutput = await viteBuild({ // configFile: viteConfigPath, root: webSrc, @@ -64,6 +66,7 @@ export const buildRscFeServer = async ({ throw new Error('Unexpected vite client build output') } + // Step 3: Generate the server output const serverBuildOutput = await serverBuild( entries, clientEntryFiles, diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index af3c5fa503c4..ffea7d56bce7 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -3,6 +3,20 @@ import type { ReactElement } from 'react' import { createFromFetch, encodeReply } from 'react-server-dom-webpack/client' +import { StatusError } from './lib/StatusError' + +const checkStatus = async ( + responsePromise: Promise +): Promise => { + const response = await responsePromise + + if (!response.ok) { + throw new StatusError(response.statusText, response.status) + } + + return response +} + export function serve(rscId: string, basePath = '/RSC/') { type SetRerender = ( rerender: (next: [ReactElement, string]) => void @@ -24,24 +38,30 @@ export function serve(rscId: string, basePath = '/RSC/') { const options = { async callServer(rsfId: string, args: unknown[]) { + console.log('client.ts :: callServer rsfId', rsfId, 'args', args) const isMutating = !!mutationMode const searchParams = new URLSearchParams() searchParams.set('action_id', rsfId) let id: string + if (isMutating) { id = rscId searchParams.set('props', serializedProps) } else { id = '_' } + const response = fetch(basePath + id + '/' + searchParams, { method: 'POST', body: await encodeReply(args), }) + const data = createFromFetch(response, options) + if (isMutating) { rerender?.([data, serializedProps]) } + return data }, } @@ -54,11 +74,12 @@ export function serve(rscId: string, basePath = '/RSC/') { 'fetchRSC before createFromFetch', basePath + rscId + '/' + searchParams ) - const data = createFromFetch( - prefetched || fetch(basePath + rscId + '/' + searchParams), - options - ) + + const response = + prefetched || fetch(basePath + rscId + '/' + searchParams) + const data = createFromFetch(checkStatus(response), options) console.log('fetchRSC after createFromFetch. data:', data) + return [data, setRerender] } ) diff --git a/packages/vite/src/lib/StatusError.ts b/packages/vite/src/lib/StatusError.ts new file mode 100644 index 000000000000..81b1b45e20b9 --- /dev/null +++ b/packages/vite/src/lib/StatusError.ts @@ -0,0 +1,5 @@ +export class StatusError extends Error { + constructor(message: string, public statusCode: number) { + super(message) + } +} diff --git a/packages/vite/src/rscBuild.ts b/packages/vite/src/rscBuild.ts index fce5a80b7605..75725ef1c333 100644 --- a/packages/vite/src/rscBuild.ts +++ b/packages/vite/src/rscBuild.ts @@ -7,7 +7,7 @@ import { onWarn } from './lib/onWarn' import { rscAnalyzePlugin } from './waku-lib/vite-plugin-rsc' /** - * RSC build + * RSC build. Step 1 of 3. * 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 diff --git a/packages/vite/src/waku-lib/rsc-handler-worker.ts b/packages/vite/src/waku-lib/rsc-handler-worker.ts index 9bdaa6ff0a29..f52971d87f6b 100644 --- a/packages/vite/src/waku-lib/rsc-handler-worker.ts +++ b/packages/vite/src/waku-lib/rsc-handler-worker.ts @@ -12,6 +12,7 @@ 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 { configFileConfig, resolveConfig } from './config' @@ -226,7 +227,8 @@ const getFunctionComponent = async ( if (typeof mod?.default === 'function') { return mod?.default } - throw new Error('No function component found') + // 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 = {} @@ -287,6 +289,8 @@ export async function renderRSC(input: RenderInput): Promise { } ) + console.log('renderRSC input', input) + if (input.rsfId && input.args) { const [fileId, name] = input.rsfId.split('#') const fname = path.join(config.root, fileId) diff --git a/packages/vite/src/waku-lib/rsc-utils.ts b/packages/vite/src/waku-lib/rsc-utils.ts index df13c0adeccc..6ff2354938e0 100644 --- a/packages/vite/src/waku-lib/rsc-utils.ts +++ b/packages/vite/src/waku-lib/rsc-utils.ts @@ -56,14 +56,17 @@ import('${moduleId}');` } // HACK Patching stream is very fragile. -export const transformRsfId = (prefixToRemove: string) => - new Transform({ +export const transformRsfId = (prefixToRemove: string) => { + console.log('prefixToRemove', prefixToRemove) + + return new Transform({ transform(chunk, encoding, callback) { if (encoding !== ('buffer' as any)) { throw new Error('Unknown encoding') } const data = chunk.toString() const lines = data.split('\n') + console.log('lines', lines) let changed = false for (let i = 0; i < lines.length; ++i) { const match = lines[i].match( @@ -77,3 +80,4 @@ export const transformRsfId = (prefixToRemove: string) => callback(null, changed ? Buffer.from(lines.join('\n')) : chunk) }, }) +} diff --git a/packages/vite/src/waku-lib/vite-plugin-rsc.ts b/packages/vite/src/waku-lib/vite-plugin-rsc.ts index d22dbec40500..f9c54c98df32 100644 --- a/packages/vite/src/waku-lib/vite-plugin-rsc.ts +++ b/packages/vite/src/waku-lib/vite-plugin-rsc.ts @@ -9,6 +9,7 @@ import type { ResolveFunction } from '../react-server-dom-webpack/node-loader' import { codeToInject } from './rsc-utils.js' +// Used in Step 2 of the build process, for the client bundle export function rscIndexPlugin(): Plugin { return { name: 'rsc-index-plugin', @@ -118,6 +119,7 @@ export function rscReloadPlugin(fn: (type: 'full-reload') => void): Plugin { } return false } + return { name: 'reload-plugin', configResolved(config) { @@ -144,7 +146,7 @@ export function rscAnalyzePlugin( serverEntryCallback: (id: string) => void ): Plugin { return { - name: 'rsc-bundle-plugin', + name: 'rsc-analyze-plugin', transform(code, id) { const ext = path.extname(id) if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {