From 107592e27fdbdcd9342caf62d370d5cd6dab8a19 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 11 Sep 2023 16:50:16 +0200 Subject: [PATCH 1/3] RSC: Work around an issue in Rollup --- packages/vite/package.json | 1 + packages/vite/src/buildRscFeServer.ts | 2 ++ packages/vite/src/lib/onWarn.ts | 22 ++++++++++++++++++++++ packages/vite/src/rscBuild.ts | 2 ++ packages/vite/src/waku-lib/build-server.ts | 3 +++ packages/vite/src/waku-lib/builder.ts | 5 +++++ yarn.lock | 3 ++- 7 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/vite/src/lib/onWarn.ts diff --git a/packages/vite/package.json b/packages/vite/package.json index a7ac97cdcd62..50d605df771b 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -88,6 +88,7 @@ "@types/yargs-parser": "21.0.0", "glob": "10.3.1", "jest": "29.6.4", + "rollup": "3.27.2", "typescript": "5.2.2" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" diff --git a/packages/vite/src/buildRscFeServer.ts b/packages/vite/src/buildRscFeServer.ts index 1a3e16cc3670..350d93deb504 100644 --- a/packages/vite/src/buildRscFeServer.ts +++ b/packages/vite/src/buildRscFeServer.ts @@ -7,6 +7,7 @@ import type { Manifest as ViteBuildManifest } from 'vite' import type { RouteSpec } from '@redwoodjs/internal/dist/routes' +import { onWarn } from './lib/onWarn' import { rscBuild } from './rscBuild' import type { RWRouteManifest } from './types' import { serverBuild } from './waku-lib/build-server' @@ -45,6 +46,7 @@ export const buildRscFeServer = async ({ // TODO (RSC) Enable this when we switch to a server-first approach // emptyOutDir: false, // Already done when building server rollupOptions: { + onwarn: onWarn, input: { main: webHtml, ...clientEntryFiles, diff --git a/packages/vite/src/lib/onWarn.ts b/packages/vite/src/lib/onWarn.ts new file mode 100644 index 000000000000..83dea36aec13 --- /dev/null +++ b/packages/vite/src/lib/onWarn.ts @@ -0,0 +1,22 @@ +import type { LoggingFunction, RollupLog } from 'rollup' + +// Upstream issue: https://github.com/rollup/rollup/issues/4699 +export function onWarn(warning: RollupLog, defaultHandler: LoggingFunction) { + const fileName = warning.loc?.file + + if ( + warning.code === 'MODULE_LEVEL_DIRECTIVE' && + /"use (client|server)"/.test(warning.message) + ) { + return + } else if ( + warning.code === 'SOURCEMAP_ERROR' && + (fileName?.endsWith('.tsx') || fileName?.endsWith('.ts')) && + warning.loc?.column === 0 && + warning.loc?.line === 1 + ) { + return + } + + defaultHandler(warning) +} diff --git a/packages/vite/src/rscBuild.ts b/packages/vite/src/rscBuild.ts index 4fdbc764da55..fce5a80b7605 100644 --- a/packages/vite/src/rscBuild.ts +++ b/packages/vite/src/rscBuild.ts @@ -3,6 +3,7 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' +import { onWarn } from './lib/onWarn' import { rscAnalyzePlugin } from './waku-lib/vite-plugin-rsc' /** @@ -51,6 +52,7 @@ export async function rscBuild(viteConfigPath: string) { write: false, ssr: true, rollupOptions: { + onwarn: onWarn, input: { entries: rwPaths.web.entries, }, diff --git a/packages/vite/src/waku-lib/build-server.ts b/packages/vite/src/waku-lib/build-server.ts index 83e0f25a31e4..e21b62944376 100644 --- a/packages/vite/src/waku-lib/build-server.ts +++ b/packages/vite/src/waku-lib/build-server.ts @@ -4,6 +4,8 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' +import { onWarn } from '../lib/onWarn' + export async function serverBuild( entriesFile: string, clientEntryFiles: Record, @@ -60,6 +62,7 @@ export async function serverBuild( outDir: rwPaths.web.distServer, manifest: 'server-build-manifest.json', rollupOptions: { + onwarn: onWarn, input, output: { banner: (chunk) => { diff --git a/packages/vite/src/waku-lib/builder.ts b/packages/vite/src/waku-lib/builder.ts index 17aef1cdb43d..f8374ddb47cb 100644 --- a/packages/vite/src/waku-lib/builder.ts +++ b/packages/vite/src/waku-lib/builder.ts @@ -6,6 +6,8 @@ import path from 'node:path' import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' +import { onWarn } from '../lib/onWarn' + import { configFileConfig, resolveConfig } from './config' import { shutdown, @@ -57,6 +59,7 @@ export async function build() { write: false, ssr: true, rollupOptions: { + onwarn: onWarn, input: { entries: entriesFile, ...customModules, @@ -85,6 +88,7 @@ export async function build() { build: { ssr: true, rollupOptions: { + onwarn: onWarn, input: { entries: entriesFile, ...clientEntryFiles, @@ -123,6 +127,7 @@ export async function build() { build: { outDir: path.join(config.build.outDir, config.framework.outPublic), rollupOptions: { + onwarn: onWarn, input: { main: indexHtmlFile, ...clientEntryFiles, diff --git a/yarn.lock b/yarn.lock index be7ddbab5106..60a605fe97e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9159,6 +9159,7 @@ __metadata: jest: 29.6.4 react: 18.3.0-canary-035a41c4e-20230704 react-server-dom-webpack: 18.3.0-canary-035a41c4e-20230704 + rollup: 3.27.2 typescript: 5.2.2 vite: 4.4.9 yargs-parser: 21.1.1 @@ -31237,7 +31238,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^3.27.1": +"rollup@npm:3.27.2, rollup@npm:^3.27.1": version: 3.27.2 resolution: "rollup@npm:3.27.2" dependencies: From 429c77f680fba363fbe5d39687b83bc7ec0d8a22 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 11 Sep 2023 17:23:24 +0200 Subject: [PATCH 2/3] RSC: Add comments and small refactors to make code easier to follow --- 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)) { From 9618a2dcba75c99cf196de79052f690a99f242c9 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 11 Sep 2023 17:24:39 +0200 Subject: [PATCH 3/3] RSC: Fix RSF aka Server Actions support --- packages/vite/src/waku-lib/build-server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/waku-lib/build-server.ts b/packages/vite/src/waku-lib/build-server.ts index e21b62944376..ea63c97b12f4 100644 --- a/packages/vite/src/waku-lib/build-server.ts +++ b/packages/vite/src/waku-lib/build-server.ts @@ -76,8 +76,10 @@ export async function serverBuild( code += '"use client";' } - const serverKeys = Object.keys(serverEntryFiles) - if (chunk.moduleIds.some((id) => serverKeys.includes(id))) { + const serverValues = Object.values(serverEntryFiles) + console.log('serverValues', serverValues) + if (chunk.moduleIds.some((id) => serverValues.includes(id))) { + console.log('adding "use server" to', chunk.fileName) code += '"use server";' } return code