diff --git a/packages/vite/src/buildFeServer.ts b/packages/vite/src/buildFeServer.ts index 7ad32d00a1f8..2e070f0bdcd5 100644 --- a/packages/vite/src/buildFeServer.ts +++ b/packages/vite/src/buildFeServer.ts @@ -9,21 +9,24 @@ import { transformWithBabel } from '@redwoodjs/internal/dist/build/babel/api' import { buildWeb } from '@redwoodjs/internal/dist/build/web' import { findRouteHooksSrc } from '@redwoodjs/internal/dist/files' import { getProjectRoutes } from '@redwoodjs/internal/dist/routes' -import { getAppRouteHook, getPaths } from '@redwoodjs/project-config' +import { getAppRouteHook, getConfig, getPaths } from '@redwoodjs/project-config' +import { buildRscFeServer } from './buildRscFeServer' import { RWRouteManifest } from './types' -interface BuildOptions { +export interface BuildOptions { verbose?: boolean } export const buildFeServer = async ({ verbose }: BuildOptions) => { const rwPaths = getPaths() - const viteConfig = rwPaths.web.viteConfig + const rwConfig = getConfig() + const viteConfigPath = rwPaths.web.viteConfig - if (!viteConfig) { + if (!viteConfigPath) { throw new Error( - 'Vite config not found. You need to setup your project with Vite using `yarn rw setup vite`' + 'Vite config not found. You need to setup your project with Vite ' + + 'using `yarn rw setup vite`' ) } @@ -35,17 +38,34 @@ export const buildFeServer = async ({ verbose }: BuildOptions) => { ) } + if (rwConfig.experimental?.rsc?.enabled) { + if (!rwPaths.web.entries) { + throw new Error('RSC entries file not found') + } + + return await buildRscFeServer({ + viteConfigPath, + webSrc: rwPaths.web.src, + webHtml: rwPaths.web.html, + entries: rwPaths.web.entries, + webDist: rwPaths.web.dist, + webDistServer: rwPaths.web.distServer, + webDistEntries: rwPaths.web.distServerEntries, + webRouteManifest: rwPaths.web.routeManifest, + }) + } + // Step 1A: Generate the client bundle await buildWeb({ verbose }) // TODO (STREAMING) When Streaming is released Vite will be the only bundler, // so we can switch to a regular import // @NOTE: Using dynamic import, because vite is still opt-in - const { build } = await import('vite') + const { build: viteBuild } = await import('vite') // Step 1B: Generate the server output - await build({ - configFile: viteConfig, + await viteBuild({ + configFile: viteConfigPath, build: { // Because we configure the root to be web/src, we need to go up one level outDir: rwPaths.web.distServer, diff --git a/packages/vite/src/buildRscFeServer.ts b/packages/vite/src/buildRscFeServer.ts index 266d2a264ae0..a34379c5599f 100644 --- a/packages/vite/src/buildRscFeServer.ts +++ b/packages/vite/src/buildRscFeServer.ts @@ -6,135 +6,47 @@ import { build as viteBuild } from 'vite' import type { Manifest as ViteBuildManifest } from 'vite' import { RouteSpec } from '@redwoodjs/internal/dist/routes' -import { getAppRouteHook, getPaths } from '@redwoodjs/project-config' +import { rscBuild } from './rscBuild' import { RWRouteManifest } from './types' import { serverBuild } from './waku-lib/build-server' -import { rscAnalyzePlugin, rscIndexPlugin } from './waku-lib/vite-plugin-rsc' - -interface BuildOptions { - verbose?: boolean +import { rscIndexPlugin } from './waku-lib/vite-plugin-rsc' + +interface Args { + viteConfigPath: string + webSrc: string + webHtml: string + entries: string + webDist: string + webDistServer: string + webDistEntries: string + webRouteManifest: string } -export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => { - const rwPaths = getPaths() - const viteConfig = rwPaths.web.viteConfig - - if (!viteConfig) { - throw new Error('Vite config not found') - } - - if (!rwPaths.web.entries) { - throw new Error('RSC entries file not found') - } - - const clientEntryFileSet = new Set() - const serverEntryFileSet = new Set() - - /** - * RSC build - * 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 - */ - await viteBuild({ - configFile: viteConfig, - root: rwPaths.base, - plugins: [ - react(), - { - name: 'rsc-test-plugin', - transform(_code, id) { - console.log('rsc-test-plugin id', id) - }, - }, - rscAnalyzePlugin( - (id) => clientEntryFileSet.add(id), - (id) => serverEntryFileSet.add(id) - ), - ], - // ssr: { - // // FIXME Without this, waku/router isn't considered to have client - // // entries, and "No client entry" error occurs. - // // Unless we fix this, RSC-capable packages aren't supported. - // // This also seems to cause problems with pnpm. - // // noExternal: ['@redwoodjs/web', '@redwoodjs/router'], - // }, - build: { - manifest: 'rsc-build-manifest.json', - write: false, - ssr: true, - rollupOptions: { - input: { - entries: rwPaths.web.entries, - }, - }, - }, - }) - - const clientEntryFiles = Object.fromEntries( - Array.from(clientEntryFileSet).map((filename, i) => [`rsc${i}`, filename]) - ) - const serverEntryFiles = Object.fromEntries( - Array.from(serverEntryFileSet).map((filename, i) => [`rsf${i}`, filename]) - ) - - console.log('clientEntryFileSet', Array.from(clientEntryFileSet)) - console.log('serverEntryFileSet', Array.from(serverEntryFileSet)) - console.log('clientEntryFiles', clientEntryFiles) - console.log('serverEntryFiles', serverEntryFiles) - - const clientEntryPath = rwPaths.web.entryClient - - if (!clientEntryPath) { - throw new Error( - 'Vite client entry point not found. Please check that your project ' + - 'has an entry.client.{jsx,tsx} file in the web/src directory.' - ) - } +export const buildRscFeServer = async ({ + viteConfigPath, + webSrc, + webHtml, + entries, + webDist, + webDistServer, + webDistEntries, + webRouteManifest, +}: Args) => { + const { clientEntryFiles, serverEntryFiles } = await rscBuild(viteConfigPath) const clientBuildOutput = await viteBuild({ - configFile: viteConfig, - root: rwPaths.web.src, - plugins: [ - // TODO (RSC) Update index.html to include the entry.client.js script - // TODO (RSC) Do the above in the exp-rsc setup command - // { - // name: 'redwood-plugin-vite', - - // // ---------- Bundle injection ---------- - // // Used by rollup during build to inject the entrypoint - // // but note index.html does not come through as an id during dev - // transform: (code: string, id: string) => { - // if ( - // existsSync(clientEntryPath) && - // // TODO (RSC) Is this even needed? We throw if we can't find it above - // // TODO (RSC) Consider making this async (if we do need it) - // normalizePath(id) === normalizePath(rwPaths.web.html) - // ) { - // const newCode = code.replace( - // '', - // '' - // ) - // - // return { code: newCode, map: null } - // } else { - // // Returning null as the map preserves the original sourcemap - // return { code, map: null } - // } - // }, - // }, - react(), - rscIndexPlugin(), - ], + configFile: viteConfigPath, + root: webSrc, + plugins: [react(), rscIndexPlugin()], build: { - outDir: rwPaths.web.dist, + outDir: webDist, emptyOutDir: true, // Needed because `outDir` is not inside `root` // TODO (RSC) Enable this when we switch to a server-first approach // emptyOutDir: false, // Already done when building server rollupOptions: { input: { - main: rwPaths.web.html, + main: webHtml, ...clientEntryFiles, }, preserveEntrySignatures: 'exports-only', @@ -151,7 +63,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => { } const serverBuildOutput = await serverBuild( - rwPaths.web.entries, + entries, clientEntryFiles, serverEntryFiles, {} @@ -168,8 +80,8 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => { }) .map((cssAsset) => { return fs.copyFile( - path.join(rwPaths.web.distServer, cssAsset.fileName), - path.join(rwPaths.web.dist, cssAsset.fileName) + path.join(webDistServer, cssAsset.fileName), + path.join(webDist, cssAsset.fileName) ) }) ) @@ -193,7 +105,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => { console.log('clientEntries', clientEntries) await fs.appendFile( - path.join(rwPaths.web.distServer, 'entries.js'), + webDistEntries, `export const clientEntries=${JSON.stringify(clientEntries)};` ) @@ -294,10 +206,7 @@ export const buildFeServer = async ({ verbose: _verbose }: 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, - 'client-build-manifest.json' - ) + const manifestPath = path.join(webDist, 'client-build-manifest.json') const manifestStr = await fs.readFile(manifestPath, 'utf-8') const clientBuildManifest: ViteBuildManifest = JSON.parse(manifestStr) @@ -316,7 +225,7 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => { // E.g. /blog/post/{id:Int} pathDefinition: route.path, hasParams: route.hasParams, - routeHooks: FIXME_constructRouteHookPath(route.routeHooks), + routeHooks: null, redirect: route.redirect ? { to: route.redirect?.to, @@ -329,28 +238,5 @@ export const buildFeServer = async ({ verbose: _verbose }: BuildOptions) => { return acc }, {}) - await fs.writeFile(rwPaths.web.routeManifest, JSON.stringify(routeManifest)) -} - -// TODO (STREAMING) Hacky work around because when you don't have a App.routeHook, esbuild doesn't create -// the pages folder in the dist/server/routeHooks directory. -// @MARK need to change to .mjs here if we use esm -const FIXME_constructRouteHookPath = (rhSrcPath: string | null | undefined) => { - const rwPaths = getPaths() - if (!rhSrcPath) { - return null - } - - if (getAppRouteHook()) { - return path.relative(rwPaths.web.src, rhSrcPath).replace('.ts', '.js') - } else { - return path - .relative(path.join(rwPaths.web.src, 'pages'), rhSrcPath) - .replace('.ts', '.js') - } -} - -if (require.main === module) { - const verbose = process.argv.includes('--verbose') - buildFeServer({ verbose }) + await fs.writeFile(webRouteManifest, JSON.stringify(routeManifest)) } diff --git a/packages/vite/src/rscBuild.ts b/packages/vite/src/rscBuild.ts new file mode 100644 index 000000000000..d1cd6f4f7002 --- /dev/null +++ b/packages/vite/src/rscBuild.ts @@ -0,0 +1,71 @@ +import react from '@vitejs/plugin-react' +import { build as viteBuild } from 'vite' + +import { getPaths } from '@redwoodjs/project-config' + +import { rscAnalyzePlugin } from './waku-lib/vite-plugin-rsc' + +/** + * RSC build + * 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 + */ +export async function rscBuild(viteConfigPath: string) { + const rwPaths = getPaths() + const clientEntryFileSet = new Set() + const serverEntryFileSet = new Set() + + if (!rwPaths.web.entries) { + throw new Error('RSC entries file not found') + } + + await viteBuild({ + configFile: viteConfigPath, + root: rwPaths.base, + plugins: [ + react(), + { + name: 'rsc-test-plugin', + transform(_code, id) { + console.log('rsc-test-plugin id', id) + }, + }, + rscAnalyzePlugin( + (id) => clientEntryFileSet.add(id), + (id) => serverEntryFileSet.add(id) + ), + ], + // ssr: { + // // FIXME Without this, waku/router isn't considered to have client + // // entries, and "No client entry" error occurs. + // // Unless we fix this, RSC-capable packages aren't supported. + // // This also seems to cause problems with pnpm. + // // noExternal: ['@redwoodjs/web', '@redwoodjs/router'], + // }, + build: { + manifest: 'rsc-build-manifest.json', + write: false, + ssr: true, + rollupOptions: { + input: { + entries: rwPaths.web.entries, + }, + }, + }, + }) + + const clientEntryFiles = Object.fromEntries( + Array.from(clientEntryFileSet).map((filename, i) => [`rsc${i}`, filename]) + ) + const serverEntryFiles = Object.fromEntries( + Array.from(serverEntryFileSet).map((filename, i) => [`rsf${i}`, filename]) + ) + + console.log('clientEntryFileSet', Array.from(clientEntryFileSet)) + console.log('serverEntryFileSet', Array.from(serverEntryFileSet)) + console.log('clientEntryFiles', clientEntryFiles) + console.log('serverEntryFiles', serverEntryFiles) + + return { clientEntryFiles, serverEntryFiles } +}