diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index 10475aa1a83f..105474462bc9 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -47,12 +47,14 @@ export const builder = async (yargs) => { './serveBothHandler.js' ) await bothExperimentalServerFileHandler() - } else if (getConfig().experimental?.rsc?.enabled) { - const { bothRscServerHandler } = await import('./serveBothHandler.js') - await bothRscServerHandler(argv) - } else if (getConfig().experimental?.streamingSsr?.enabled) { - const { bothSsrServerHandler } = await import('./serveBothHandler.js') - await bothSsrServerHandler(argv) + } else if ( + getConfig().experimental?.rsc?.enabled || + getConfig().experimental?.streamingSsr?.enabled + ) { + const { bothSsrRscServerHandler } = await import( + './serveBothHandler.js' + ) + await bothSsrRscServerHandler(argv) } else { // Wanted to use the new web-server package here, but can't because // of backwards compatibility reasons. With `bothServerHandler` both diff --git a/packages/cli/src/commands/serveBothHandler.js b/packages/cli/src/commands/serveBothHandler.js index 79b6921f6bd5..c7892917116a 100644 --- a/packages/cli/src/commands/serveBothHandler.js +++ b/packages/cli/src/commands/serveBothHandler.js @@ -14,19 +14,10 @@ import { getConfig, getPaths } from '@redwoodjs/project-config' export const bothExperimentalServerFileHandler = async () => { logExperimentalHeader() - if (getConfig().experimental?.rsc?.enabled) { - logSkippingFastifyWebServer() - - await execa( - 'node', - ['./node_modules/@redwoodjs/vite/dist/runRscFeServer.js'], - { - cwd: getPaths().base, - stdio: 'inherit', - shell: true, - } - ) - } else if (getConfig().experimental?.streamingSsr?.enabled) { + if ( + getConfig().experimental?.rsc?.enabled || + getConfig().experimental?.streamingSsr?.enabled + ) { logSkippingFastifyWebServer() await execa('yarn', ['rw-serve-fe'], { @@ -47,41 +38,17 @@ export const bothExperimentalServerFileHandler = async () => { } } -export const bothRscServerHandler = async (argv) => { - const { apiServerHandler } = await import('./serveApiHandler.js') - - // TODO (RSC) Allow specifying port, socket and apiRootPath - const apiPromise = apiServerHandler({ - ...argv, - port: 8911, - apiRootPath: '/', - }) - - // TODO (RSC) More gracefully handle Ctrl-C - const fePromise = execa( - 'node', - ['./node_modules/@redwoodjs/vite/dist/runRscFeServer.js'], - { - cwd: getPaths().base, - stdio: 'inherit', - shell: true, - } - ) - - await Promise.all([apiPromise, fePromise]) -} - -export const bothSsrServerHandler = async (argv) => { +export const bothSsrRscServerHandler = async (argv) => { const { apiServerHandler } = await import('./serveApiHandler.js') - // TODO (STREAMING) Allow specifying port, socket and apiRootPath + // TODO Allow specifying port, socket and apiRootPath const apiPromise = apiServerHandler({ ...argv, port: 8911, apiRootPath: '/', }) - // TODO (STREAMING) More gracefully handle Ctrl-C + // TODO More gracefully handle Ctrl-C // Right now you get a big red error box when you kill the process const fePromise = execa('yarn', ['rw-serve-fe'], { cwd: getPaths().web.base, diff --git a/packages/core/config/webpack.common.js b/packages/core/config/webpack.common.js index 6a5073366cc8..0e224add37e0 100644 --- a/packages/core/config/webpack.common.js +++ b/packages/core/config/webpack.common.js @@ -281,7 +281,7 @@ module.exports = (webpackEnv) => { }), isEnvProduction && new WebpackManifestPlugin({ - fileName: 'build-manifest.json', + fileName: 'client-build-manifest.json', }), isEnvProduction && new ChunkReferencesPlugin(), ...getSharedPlugins(isEnvProduction), diff --git a/packages/prerender/src/babelPlugins/__tests__/__fixtures__/viteDistDir/build-manifest.json b/packages/prerender/src/babelPlugins/__tests__/__fixtures__/viteDistDir/client-build-manifest.json similarity index 100% rename from packages/prerender/src/babelPlugins/__tests__/__fixtures__/viteDistDir/build-manifest.json rename to packages/prerender/src/babelPlugins/__tests__/__fixtures__/viteDistDir/client-build-manifest.json diff --git a/packages/prerender/src/babelPlugins/__tests__/__fixtures__/webpackDistDir/build-manifest.json b/packages/prerender/src/babelPlugins/__tests__/__fixtures__/webpackDistDir/client-build-manifest.json similarity index 100% rename from packages/prerender/src/babelPlugins/__tests__/__fixtures__/webpackDistDir/build-manifest.json rename to packages/prerender/src/babelPlugins/__tests__/__fixtures__/webpackDistDir/client-build-manifest.json diff --git a/packages/prerender/src/babelPlugins/babel-plugin-redwood-prerender-media-imports.ts b/packages/prerender/src/babelPlugins/babel-plugin-redwood-prerender-media-imports.ts index d9c58d27d812..17aebf6c7db4 100644 --- a/packages/prerender/src/babelPlugins/babel-plugin-redwood-prerender-media-imports.ts +++ b/packages/prerender/src/babelPlugins/babel-plugin-redwood-prerender-media-imports.ts @@ -47,7 +47,7 @@ export default function ( { types: t }: { types: typeof types }, { bundler }: { bundler: BundlerEnum } ): PluginObj { - const manifestPath = join(getPaths().web.dist, 'build-manifest.json') + const manifestPath = join(getPaths().web.dist, 'client-build-manifest.json') const buildManifest = require(manifestPath) return { diff --git a/packages/prerender/src/runPrerender.tsx b/packages/prerender/src/runPrerender.tsx index 63319ffcbc0d..e9a2f702037e 100644 --- a/packages/prerender/src/runPrerender.tsx +++ b/packages/prerender/src/runPrerender.tsx @@ -168,7 +168,7 @@ function insertChunkLoadingScript( const buildManifest = JSON.parse( fs.readFileSync( - path.join(getPaths().web.dist, 'build-manifest.json'), + path.join(getPaths().web.dist, 'client-build-manifest.json'), 'utf-8' ) ) diff --git a/packages/vite/src/buildFeServer.ts b/packages/vite/src/buildFeServer.ts index 8aa0a67f48cb..b242dc9c7ae1 100644 --- a/packages/vite/src/buildFeServer.ts +++ b/packages/vite/src/buildFeServer.ts @@ -124,7 +124,7 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => { // https://github.com/microsoft/TypeScript/issues/53656 have both landed we // should try to do this instead: // const clientBuildManifest: ViteBuildManifest = await import( - // path.join(getPaths().web.dist, 'build-manifest.json'), + // path.join(getPaths().web.dist, 'client-build-manifest.json'), // { with: { type: 'json' } } // ) // NOTES: @@ -136,7 +136,10 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => { // * With `assert` and `@babel/plugin-syntax-import-assertions` the // code compiled and ran properly, but Jest tests failed, complaining // about the syntax. - const manifestPath = path.join(getPaths().web.dist, 'build-manifest.json') + const manifestPath = path.join( + getPaths().web.dist, + 'client-build-manifest.json' + ) const buildManifestStr = await fs.readFile(manifestPath, 'utf-8') const clientBuildManifest: ViteBuildManifest = JSON.parse(buildManifestStr) diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index a7443cae632c..7580fabff23c 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -239,7 +239,7 @@ export default function redwoodPluginVite(): PluginOption[] { build: { outDir: options.build?.outDir || rwPaths.web.dist, emptyOutDir: true, - manifest: !env.ssrBuild ? 'build-manifest.json' : undefined, + manifest: !env.ssrBuild ? 'client-build-manifest.json' : undefined, sourcemap: !env.ssrBuild && rwConfig.web.sourceMap, // Note that this can be boolean or 'inline' rollupOptions: { input: getRollupInput(!!env.ssrBuild), diff --git a/packages/vite/src/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index abffbf8e6ae5..3b428cc5ed2f 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -52,6 +52,7 @@ export function createRscRequestHandler() { // size somehow // https://nextjs.org/docs/app/api-reference/functions/server-actions#size-limitation if (req.headers['content-type']?.startsWith('multipart/form-data')) { + console.log('RSA: multipart/form-data') const bb = busboy({ headers: req.headers }) const reply = decodeReplyFromBusboy(bb) @@ -84,6 +85,7 @@ export function createRscRequestHandler() { } } } else { + console.log('RSA: regular body') let body = '' for await (const chunk of req) { @@ -97,6 +99,8 @@ export function createRscRequestHandler() { } } + console.log('rscRequestHandler: args', args) + if (rscId || rsfId) { const handleError = (err: unknown) => { if (hasStatusCode(err)) { diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index 63686e91a3c8..b6c5e32b301b 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -312,7 +312,22 @@ function isSerializedFormData(data?: unknown): data is SerializedFormData { } async function renderRsc(input: RenderInput): Promise { + const rwPaths = getPaths() + const config = await configPromise + // TODO (RSC): Should root be configurable by the user? We probably need it + // to be different values in different contexts. Should we introduce more + // config options? + // config.root currently comes from the user's project, where it in turn + // comes from our `redwood()` vite plugin defined in index.ts. By default + // (i.e. in the redwood() plugin) it points to /web/src. But we need it + // to be just /, so for now we override it here. + config.root = + process.platform === 'win32' + ? rwPaths.base.replaceAll('\\', '/') + : rwPaths.base + console.log('config.root', config.root) + console.log('rwPaths.base', rwPaths.base) const bundlerConfig = new Proxy( {}, { @@ -322,6 +337,7 @@ async function renderRsc(input: RenderInput): Promise { // filePath /Users/tobbe/dev/waku/examples/01_counter/dist/assets/rsc0.js // name Counter const id = resolveClientEntry(config, filePath) + console.log('Proxy id', id) // id /assets/rsc0-beb48afe.js return { id, chunks: [id], name, async: true } }, diff --git a/packages/vite/src/runFeServer.ts b/packages/vite/src/runFeServer.ts index 76df352825bc..37655cf1da47 100644 --- a/packages/vite/src/runFeServer.ts +++ b/packages/vite/src/runFeServer.ts @@ -2,6 +2,8 @@ // well in naming with @redwoodjs/api-server) // Only things used during dev can be in @redwoodjs/vite. Everything else has // to go in fe-server +// UPDATE: We decided to name the package @redwoodjs/web-server instead of +// fe-server. And it's already created, but this hasn't been moved over yet. import fs from 'fs/promises' import path from 'path' @@ -15,6 +17,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 { createReactStreamingHandler } from './streaming/createReactStreamingHandler' import { registerFwGlobals } from './streaming/registerGlobals' import type { RWRouteManifest } from './types' @@ -35,7 +39,7 @@ loadDotEnv({ defaults: path.join(getPaths().base, '.env.defaults'), multiline: true, }) -//------------------------------------------------ +// ------------------------------------------------ export async function runFeServer() { const app = express() @@ -44,6 +48,19 @@ export async function runFeServer() { registerFwGlobals() + try { + // This will fail if we're not running in RSC mode (i.e. for Streaming SSR) + // TODO (RSC) Remove the try/catch, or at least the if-statement in there + // once RSC is always enabled + await setClientEntries('load') + } catch (e) { + if (rwConfig.experimental?.rsc?.enabled) { + console.error('Failed to load client entries') + console.error(e) + process.exit(1) + } + } + // TODO When https://github.com/tc39/proposal-import-attributes and // https://github.com/microsoft/TypeScript/issues/53656 have both landed we // should try to do this instead: @@ -63,10 +80,16 @@ export async function runFeServer() { const routeManifest: RWRouteManifest = JSON.parse(routeManifestStr) // TODO See above about using `import { with: { type: 'json' } }` instead - const manifestPath = path.join(getPaths().web.dist, 'build-manifest.json') + const manifestPath = path.join(rwPaths.web.dist, 'client-build-manifest.json') const buildManifestStr = await fs.readFile(manifestPath, 'utf-8') const buildManifest: ViteBuildManifest = JSON.parse(buildManifestStr) + if (rwConfig.experimental?.rsc?.enabled) { + console.log('='.repeat(80)) + console.log('buildManifest', buildManifest) + console.log('='.repeat(80)) + } + const indexEntry = Object.values(buildManifest).find((manifestItem) => { return manifestItem.isEntry }) @@ -75,11 +98,14 @@ export async function runFeServer() { throw new Error('Could not find index.html in build manifest') } - // 👉 1. Use static handler for assets + // 1. Use static handler for assets // For CF workers, we'd need an equivalent of this - app.use('/', express.static(rwPaths.web.dist, { index: false })) + app.use( + '/assets', + express.static(rwPaths.web.dist + '/assets', { index: false }) + ) - // 👉 2. Proxy the api server + // 2. Proxy the api server // TODO (STREAMING) we need to be able to specify whether proxying is required or not // e.g. deploying to Netlify, we don't need to proxy but configure it in Netlify // Also be careful of differences between v2 and v3 of the server @@ -101,6 +127,8 @@ export async function runFeServer() { const getStylesheetLinks = () => indexEntry.css || [] const clientEntry = '/' + indexEntry.file + // `routeManifest` is empty for RSC builds for now, so we're not doing SSR + // when we have RSC experimental support enabled for (const route of Object.values(routeManifest)) { const routeHandler = await createReactStreamingHandler({ route, @@ -123,25 +151,17 @@ export async function runFeServer() { app.get(expressPathDef, createServerAdapter(routeHandler)) } - const server = app.listen( - rwConfig.web.port, - process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::' - ) - - server.on('listening', () => { - let addressDetails = '' - const address = server.address() + // Mounting middleware at /rw-rsc will strip /rw-rsc from req.url + app.use('/rw-rsc', createRscRequestHandler()) - if (typeof address === 'string') { - addressDetails = `(${address})` - } else if (address && typeof address === 'object') { - addressDetails = `(${address.address}:${address.port})` - } + // This is basically the route for / -> HomePage. Used by RSC + // Using .get() here to get exact path matching + app.get('/', express.static(rwPaths.web.dist)) - console.log( - `Started production FE server on http://localhost:${rwConfig.web.port} ${addressDetails}` - ) - }) + app.listen(rwConfig.web.port) + console.log( + `Started production FE server on http://localhost:${rwConfig.web.port}` + ) } runFeServer() diff --git a/packages/vite/src/runRscFeServer.ts b/packages/vite/src/runRscFeServer.ts deleted file mode 100644 index 2be7c4960360..000000000000 --- a/packages/vite/src/runRscFeServer.ts +++ /dev/null @@ -1,117 +0,0 @@ -// TODO (STREAMING) Move this to a new package called @redwoodjs/fe-server (goes -// well in naming with @redwoodjs/api-server) -// Only things used during dev can be in @redwoodjs/vite. Everything else has -// to go in fe-server - -import fs from 'fs/promises' -import path from 'path' - -// @ts-expect-error We will remove dotenv-defaults from this package anyway -import { config as loadDotEnv } from 'dotenv-defaults' -import express from 'express' -import { createProxyMiddleware } from 'http-proxy-middleware' -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' - -/** - * TODO (STREAMING) - * We have this server in the vite package only temporarily. - * We will need to decide where to put it, so that rwjs/internal and other heavy dependencies - * can be removed from the final docker image - */ - -// --- @MARK This should be removed once we have re-architected the rw serve command --- -// We need the dotenv, so that prisma knows the DATABASE env var -// Normally the RW cli loads this for us, but we expect this file to be run directly -// without using the CLI. Remember to remove dotenv-defaults dependency from this package -loadDotEnv({ - path: path.join(getPaths().base, '.env'), - defaults: path.join(getPaths().base, '.env.defaults'), - multiline: true, -}) -// ------------------------------------------------ - -export async function runFeServer() { - const app = express() - const rwPaths = getPaths() - const rwConfig = getConfig() - - registerFwGlobals() - - await setClientEntries('load') - - // TODO When https://github.com/tc39/proposal-import-attributes and - // https://github.com/microsoft/TypeScript/issues/53656 have both landed we - // should try to do this instead: - // const routeManifest: RWRouteManifest = await import( - // rwPaths.web.routeManifest, { with: { type: 'json' } } - // ) - // NOTES: - // * There's a related babel plugin here - // https://babeljs.io/docs/babel-plugin-syntax-import-attributes - // * Included in `preset-env` if you set `shippedProposals: true` - // * We had this before, but with `assert` instead of `with`. We really - // should be using `with`. See motivation in issues linked above. - // * With `assert` and `@babel/plugin-syntax-import-assertions` the - // code compiled and ran properly, but Jest tests failed, complaining - // about the syntax. - // const routeManifestStr = await fs.readFile(rwPaths.web.routeManifest, 'utf-8') - // const routeManifest: RWRouteManifest = JSON.parse(routeManifestStr) - - // TODO See above about using `import { with: { type: 'json' } }` instead - const manifestPath = path.join(rwPaths.web.dist, 'client-build-manifest.json') - const buildManifestStr = await fs.readFile(manifestPath, 'utf-8') - const buildManifest: ViteBuildManifest = JSON.parse(buildManifestStr) - - console.log('='.repeat(80)) - console.log('buildManifest', buildManifest) - console.log('='.repeat(80)) - - const indexEntry = Object.values(buildManifest).find((manifestItem) => { - return manifestItem.isEntry - }) - - if (!indexEntry) { - throw new Error('Could not find index.html in build manifest') - } - - // 1. Use static handler for assets - // For CF workers, we'd need an equivalent of this - app.use('/assets', express.static(rwPaths.web.dist + '/assets')) - - // 2. Proxy the api server - // TODO (STREAMING) we need to be able to specify whether proxying is required or not - // e.g. deploying to Netlify, we don't need to proxy but configure it in Netlify - // Also be careful of differences between v2 and v3 of the server - app.use( - rwConfig.web.apiUrl, - // @WARN! Be careful, between v2 and v3 of http-proxy-middleware - // the syntax has changed https://github.com/chimurai/http-proxy-middleware - createProxyMiddleware({ - changeOrigin: true, - pathRewrite: { - [`^${rwConfig.web.apiUrl}`]: '', // remove base path - }, - // Using 127.0.0.1 to force ipv4. With `localhost` you don't really know - // if it's going to be ipv4 or ipv6 - target: `http://127.0.0.1:${rwConfig.api.port}`, - }) - ) - - // Mounting middleware at /rw-rsc will strip /rw-rsc from req.url - app.use('/rw-rsc', createRscRequestHandler()) - - app.use(express.static(rwPaths.web.dist)) - - app.listen(rwConfig.web.port) - console.log( - `Started production FE server on http://localhost:${rwConfig.web.port}` - ) -} - -runFeServer() diff --git a/packages/vite/src/waku-lib/rsc-utils.ts b/packages/vite/src/waku-lib/rsc-utils.ts index 6ff2354938e0..9dfc1f98fb1a 100644 --- a/packages/vite/src/waku-lib/rsc-utils.ts +++ b/packages/vite/src/waku-lib/rsc-utils.ts @@ -57,6 +57,7 @@ import('${moduleId}');` // HACK Patching stream is very fragile. export const transformRsfId = (prefixToRemove: string) => { + // Should be something like /home/runner/work/redwood/test-project-rsa console.log('prefixToRemove', prefixToRemove) return new Transform({