From f1a3c1c8a76c20b13366d3d02cf31b7aed4f43e6 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Fri, 3 May 2024 07:22:12 +0100 Subject: [PATCH] feat(RSC): Remove `entries.ts` file (#10533) Co-authored-by: Tobbe Lundberg --- .../src/__tests__/paths.test.ts | 4 -- packages/project-config/src/paths.ts | 3 -- packages/vite/package.json | 4 -- packages/vite/src/buildFeServer.ts | 4 -- packages/vite/src/buildRscClientAndServer.ts | 6 +-- packages/vite/src/clientSsr.ts | 13 +++-- packages/vite/src/entries.ts | 26 ---------- packages/vite/src/lib/entries.ts | 52 +++++++++++++++++++ packages/vite/src/rsc/rscBuildAnalyze.ts | 19 +++---- ...tEntriesFile.ts => rscBuildEntriesFile.ts} | 34 ++++++++++-- packages/vite/src/rsc/rscBuildForServer.ts | 14 ++--- packages/vite/src/rsc/rscWorker.ts | 32 +++--------- 12 files changed, 117 insertions(+), 94 deletions(-) delete mode 100644 packages/vite/src/entries.ts create mode 100644 packages/vite/src/lib/entries.ts rename packages/vite/src/rsc/{rscBuildClientEntriesFile.ts => rscBuildEntriesFile.ts} (60%) diff --git a/packages/project-config/src/__tests__/paths.test.ts b/packages/project-config/src/__tests__/paths.test.ts index d238f50b1a62..61fe3f039524 100644 --- a/packages/project-config/src/__tests__/paths.test.ts +++ b/packages/project-config/src/__tests__/paths.test.ts @@ -184,7 +184,6 @@ describe('paths', () => { viteConfig: null, entryClient: null, entryServer: null, - entries: null, graphql: path.join(FIXTURE_BASEDIR, 'web', 'src', 'graphql'), }, } @@ -469,7 +468,6 @@ describe('paths', () => { viteConfig: path.join(FIXTURE_BASEDIR, 'web', 'vite.config.ts'), entryClient: null, // doesn't exist in example-todo-main entryServer: null, // doesn't exist in example-todo-main - entries: null, // doesn't exist in example-todo-main }, } @@ -756,7 +754,6 @@ describe('paths', () => { ), entryClient: null, entryServer: null, - entries: null, dist: path.join(FIXTURE_BASEDIR, 'web', 'dist'), distEntryServer: path.join( FIXTURE_BASEDIR, @@ -1086,7 +1083,6 @@ describe('paths', () => { viteConfig: path.join(FIXTURE_BASEDIR, 'web', 'vite.config.ts'), entryClient: path.join(FIXTURE_BASEDIR, 'web/src/entry.client.tsx'), entryServer: null, - entries: null, }, } diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts index 949d8e30ee96..0bcdcda157a9 100644 --- a/packages/project-config/src/paths.ts +++ b/packages/project-config/src/paths.ts @@ -42,7 +42,6 @@ export interface WebPaths { viteConfig: string | null // because vite is opt-in only entryClient: string | null entryServer: string | null - entries: string | null postcss: string storybookConfig: string storybookPreviewConfig: string | null @@ -117,7 +116,6 @@ const PATH_WEB_DIR_CONFIG_WEBPACK = 'web/config/webpack.config.js' const PATH_WEB_DIR_CONFIG_VITE = 'web/vite.config' // .js,.ts const PATH_WEB_DIR_ENTRY_CLIENT = 'web/src/entry.client' // .jsx,.tsx const PATH_WEB_DIR_ENTRY_SERVER = 'web/src/entry.server' // .jsx,.tsx -const PATH_WEB_DIR_ENTRIES = 'web/src/entries' // .js,.ts const PATH_WEB_DIR_GRAPHQL = 'web/src/graphql' // .js,.ts const PATH_WEB_DIR_CONFIG_POSTCSS = 'web/config/postcss.config.js' @@ -259,7 +257,6 @@ export const getPaths = (BASE_DIR: string = getBaseDir()): Paths => { types: path.join(BASE_DIR, 'web/types'), entryClient: resolveFile(path.join(BASE_DIR, PATH_WEB_DIR_ENTRY_CLIENT)), // new vite/stream entry point for client entryServer: resolveFile(path.join(BASE_DIR, PATH_WEB_DIR_ENTRY_SERVER)), - entries: resolveFile(path.join(BASE_DIR, PATH_WEB_DIR_ENTRIES)), graphql: path.join(BASE_DIR, PATH_WEB_DIR_GRAPHQL), }, } diff --git a/packages/vite/package.json b/packages/vite/package.json index b4ab5c162f9c..f17551b8cb6d 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -14,10 +14,6 @@ "types": "./dist/index.d.ts", "default": "./cjsWrapper.js" }, - "./entries": { - "types": "./dist/entries.d.ts", - "default": "./dist/entries.js" - }, "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.js" diff --git a/packages/vite/src/buildFeServer.ts b/packages/vite/src/buildFeServer.ts index 0f08230a6fbf..806ac944394d 100644 --- a/packages/vite/src/buildFeServer.ts +++ b/packages/vite/src/buildFeServer.ts @@ -38,10 +38,6 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => { } if (rscEnabled) { - if (!rwPaths.web.entries) { - throw new Error('RSC entries file not found') - } - await buildRscClientAndServer() } diff --git a/packages/vite/src/buildRscClientAndServer.ts b/packages/vite/src/buildRscClientAndServer.ts index 6c3d96fe2aab..1e1e92637eae 100644 --- a/packages/vite/src/buildRscClientAndServer.ts +++ b/packages/vite/src/buildRscClientAndServer.ts @@ -1,7 +1,7 @@ import { rscBuildAnalyze } from './rsc/rscBuildAnalyze.js' import { rscBuildClient } from './rsc/rscBuildClient.js' -import { rscBuildClientEntriesMappings } from './rsc/rscBuildClientEntriesFile.js' import { rscBuildCopyCssAssets } from './rsc/rscBuildCopyCssAssets.js' +import { rscBuildEntriesMappings } from './rsc/rscBuildEntriesFile.js' import { rscBuildForServer } from './rsc/rscBuildForServer.js' import { rscBuildRwEnvVars } from './rsc/rscBuildRwEnvVars.js' @@ -28,9 +28,9 @@ export const buildRscClientAndServer = async () => { // Can we do this more similar to how it's done for streaming? await rscBuildCopyCssAssets(serverBuildOutput) - // Mappings from server to client asset file names + // Mappings from standard names to full asset names // Used by the RSC worker - await rscBuildClientEntriesMappings( + await rscBuildEntriesMappings( clientBuildOutput, serverBuildOutput, clientEntryFiles, diff --git a/packages/vite/src/clientSsr.ts b/packages/vite/src/clientSsr.ts index 490a40109d6b..26e10bf64c80 100644 --- a/packages/vite/src/clientSsr.ts +++ b/packages/vite/src/clientSsr.ts @@ -24,8 +24,9 @@ async function getEntries() { async function getFunctionComponent( rscId: string, ): Promise> { - const { getEntry } = (await getEntries()).default - const mod = await getEntry(rscId) + const { serverEntries } = await getEntries() + const entryPath = path.join(getPaths().web.distRsc, serverEntries[rscId]) + const mod = await import(makeFilePath(entryPath)) if (typeof mod === 'function') { return mod @@ -35,6 +36,11 @@ async function getFunctionComponent( return mod?.default } + // We remove any potential "__rwjs__" prefix as these only exist in the mapping + // not in the built files. E.g. "__rwjs__ServerEntry" -> "ServerEntry" as we don't + // export "__rwjs__ServerEntry" in the built file we simply export "ServerEntry" + rscId = rscId.replace(/^__rwjs__/, '') + if (typeof mod?.[rscId] === 'function') { return mod?.[rscId] } @@ -98,7 +104,7 @@ export function renderFromDist>( let ServerEntry: React.FunctionComponent try { - ServerEntry = await getFunctionComponent('ServerEntry') + ServerEntry = await getFunctionComponent('__rwjs__ServerEntry') } catch (error) { console.log('SsrComponent error', error) // For now we'll just swallow this error because not all projects will @@ -108,6 +114,7 @@ export function renderFromDist>( ServerEntry = () => createElement('div', {}, 'Loading') } + console.log('clientSsr.ts getEntries()', getEntries()) const clientEntries = (await getEntries()).clientEntries // TODO (RSC): Try removing the proxy here and see if it's really necessary. diff --git a/packages/vite/src/entries.ts b/packages/vite/src/entries.ts deleted file mode 100644 index 9d4f6f16185f..000000000000 --- a/packages/vite/src/entries.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { FunctionComponent } from 'react' - -export type GetEntry = ( - rscId: string, -) => Promise - -export type GetBuilder = ( - // FIXME (from original waku code) can we somehow avoid leaking internal - // implementation? - unstable_decodeId: (encodedId: string) => [id: string, name: string], -) => Promise<{ - [pathStr: string]: { - elements?: Iterable< - readonly [rscId: string, props: unknown, skipPrefetch?: boolean] - > - customCode?: string // optional code to inject - } -}> - -/** - * Used to look up the component to import when calling - * `renderFromRscServer('MyPage')` in Routes.tsx - */ -export function defineEntries(getEntry: GetEntry, getBuilder?: GetBuilder) { - return { getEntry, getBuilder } -} diff --git a/packages/vite/src/lib/entries.ts b/packages/vite/src/lib/entries.ts new file mode 100644 index 000000000000..cfffa1e41dce --- /dev/null +++ b/packages/vite/src/lib/entries.ts @@ -0,0 +1,52 @@ +import path from 'node:path' + +import type { PagesDependency } from '@redwoodjs/project-config' +import { + ensurePosixPath, + getPaths, + processPagesDir, +} from '@redwoodjs/project-config' + +import { makeFilePath } from '../utils' + +const getPathRelativeToSrc = (maybeAbsolutePath: string) => { + // If the path is already relative + if (!path.isAbsolute(maybeAbsolutePath)) { + return maybeAbsolutePath + } + + return `./${path.relative(getPaths().web.src, maybeAbsolutePath)}` +} + +const withRelativeImports = (page: PagesDependency) => { + return { + ...page, + relativeImport: ensurePosixPath(getPathRelativeToSrc(page.importPath)), + } +} + +export function getEntries() { + const entries: Record = {} + + // Add the various pages + const pages = processPagesDir().map(withRelativeImports) + for (const page of pages) { + entries[page.importName] = page.path + } + + // Add the ServerEntry entry, noting we use the "__rwjs__" prefix to avoid + // any potential conflicts with user-defined entries + const serverEntry = getPaths().web.entryServer + if (!serverEntry) { + throw new Error('Server Entry file not found') + } + entries['__rwjs__ServerEntry'] = serverEntry + + return entries +} + +export async function getEntriesFromDist(): Promise> { + const entriesDist = getPaths().web.distRscEntries + const { serverEntries } = await import(makeFilePath(entriesDist)) + return serverEntries +} diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index 2662a07d7331..37e7b45885e3 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -2,6 +2,7 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' +import { getEntries } from '../lib/entries.js' import { onWarn } from '../lib/onWarn.js' import { rscAnalyzePlugin } from '../plugins/vite-plugin-rsc-analyze.js' @@ -22,10 +23,6 @@ export async function rscBuildAnalyze() { const serverEntryFileSet = new Set() const componentImportMap = new Map() - if (!rwPaths.web.entries) { - throw new Error('RSC entries file not found') - } - if (!rwPaths.web.viteConfig) { throw new Error('Vite config not found') } @@ -69,16 +66,14 @@ export async function rscBuildAnalyze() { minify: false, manifest: 'rsc-build-manifest.json', write: false, - // TODO (RSC): In the future we want to generate the entries file - // automatically. Maybe by using `analyzeRoutes()` - // For the dev server we might need to generate these entries on the - // fly - so we will need something like a plugin or virtual module - // to generate these entries, rather than write to actual file. - // And so, we might as well use on-the-fly generation for regular - // builds too - ssr: rwPaths.web.entries, + // We generate the entries from the simple `getEntries` function that analyses + // the various pages plus the ServerEntry file. This may need revisiting when we + // spend time on improving dev support or expand the scope of the components + // that are looked up via the entries mappings. + ssr: true, rollupOptions: { onwarn: onWarn, + input: getEntries(), }, }, }) diff --git a/packages/vite/src/rsc/rscBuildClientEntriesFile.ts b/packages/vite/src/rsc/rscBuildEntriesFile.ts similarity index 60% rename from packages/vite/src/rsc/rscBuildClientEntriesFile.ts rename to packages/vite/src/rsc/rscBuildEntriesFile.ts index df2dcc9fb0a6..76ec7d47092a 100644 --- a/packages/vite/src/rsc/rscBuildClientEntriesFile.ts +++ b/packages/vite/src/rsc/rscBuildEntriesFile.ts @@ -1,7 +1,12 @@ import fs from 'fs/promises' +import type { OutputChunk } from 'rollup' +import { normalizePath } from 'vite' + import { getPaths } from '@redwoodjs/project-config' +import { getEntries } from '../lib/entries.js' + import type { rscBuildClient } from './rscBuildClient.js' import type { rscBuildForServer } from './rscBuildForServer.js' @@ -14,17 +19,18 @@ import type { rscBuildForServer } from './rscBuildForServer.js' // TODO(RSC_DC): This function should eventually be removed. // The dev server will need this implemented as a Vite plugin, // so worth waiting till implementation to swap out and just include the plugin for the prod build -export function rscBuildClientEntriesMappings( +export async function rscBuildEntriesMappings( clientBuildOutput: Awaited>, serverBuildOutput: Awaited>, clientEntryFiles: Record, ) { console.log('\n') - console.log('5. rscBuildClientEntriesMapping') - console.log('===============================\n') + console.log('5. rscBuildEntriesMapping') + console.log('=========================\n') const rwPaths = getPaths() + // RSC client component to client dist asset mapping const clientEntries: Record = {} for (const item of clientBuildOutput) { const { name, fileName } = item @@ -53,9 +59,27 @@ export function rscBuildClientEntriesMappings( } console.log('clientEntries', clientEntries) + await fs.appendFile( + rwPaths.web.distRscEntries, + `// client component mapping (dist/rsc -> dist/client)\nexport const clientEntries = ${JSON.stringify(clientEntries, undefined, 2)};\n\n`, + ) + + // Server component names to RSC server asset mapping + const entries = getEntries() + const serverEntries: Record = {} + for (const [name, sourceFile] of Object.entries(entries)) { + const buildOutputItem = serverBuildOutput.find((item) => { + return (item as OutputChunk).facadeModuleId === normalizePath(sourceFile) + }) + + if (buildOutputItem) { + serverEntries[name] = buildOutputItem.fileName + } + } - return fs.appendFile( + console.log('serverEntries', serverEntries) + await fs.appendFile( rwPaths.web.distRscEntries, - `export const clientEntries=${JSON.stringify(clientEntries)};`, + `// server component mapping (src -> dist/rsc)\nexport const serverEntries = ${JSON.stringify(serverEntries, undefined, 2)};\n\n`, ) } diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 6b1cc12888ec..a699c6541203 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -2,6 +2,7 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' +import { getEntries } from '../lib/entries.js' import { onWarn } from '../lib/onWarn.js' // import { rscCssPreinitPlugin } from '../plugins/vite-plugin-rsc-css-preinit.js' import { rscRoutesAutoLoader } from '../plugins/vite-plugin-rsc-routes-auto-loader.js' @@ -25,9 +26,8 @@ export async function rscBuildForServer( const rwPaths = getPaths() - if (!rwPaths.web.entries) { - throw new Error('RSC entries file not found') - } + const entryFiles = getEntries() + const entryFilesKeys = Object.keys(entryFiles) if (!rwPaths.web.entryServer) { throw new Error('Server Entry file not found') @@ -80,7 +80,7 @@ export async function rscBuildForServer( rollupOptions: { onwarn: onWarn, input: { - entries: rwPaths.web.entries, + ...entryFiles, ...clientEntryFiles, ...serverEntryFiles, ...customModules, @@ -108,9 +108,11 @@ export async function rscBuildForServer( return code }, entryFileNames: (chunkInfo) => { - // TODO (RSC) Probably don't want 'entries'. And definitely don't want it hardcoded + // Entries such as pages should be named like the other assets + if (entryFilesKeys.includes(chunkInfo.name)) { + return 'assets/[name]-[hash].mjs' + } if ( - chunkInfo.name === 'entries' || chunkInfo.name === 'entry.server' || chunkInfo.name === 'rsdw-server' || customModules[chunkInfo.name] diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index 2df6cd9096c2..6325dcc18f7e 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -17,7 +17,7 @@ import { createServer, resolveConfig } from 'vite' import { getPaths } from '@redwoodjs/project-config' -import type { defineEntries, GetEntry } from '../entries.js' +import { getEntries, getEntriesFromDist } from '../lib/entries.js' import { registerFwGlobalsAndShims } from '../lib/registerFwGlobalsAndShims.js' import { StatusError } from '../lib/StatusError.js' import { rscReloadPlugin } from '../plugins/vite-plugin-rsc-reload.js' @@ -38,7 +38,6 @@ const { renderToPipeableStream } = RSDWServer let absoluteClientEntries: Record = {} -type Entries = { default: ReturnType } type PipeableStream = { pipe(destination: T): T } const handleSetClientEntries = async ({ @@ -198,37 +197,22 @@ type ConfigType = Omit & { root: string } const configPromise: Promise = resolveConfig({}, 'serve') const getFunctionComponent = async (rscId: string) => { - let entriesFilePath: string | null - // TODO (RSC): Get rid of this when we only use the worker in dev mode const isDev = Object.keys(absoluteClientEntries).length === 0 + let entryModule: string | undefined if (isDev) { - entriesFilePath = getPaths().web.entries + entryModule = getEntries()[rscId] } else { - entriesFilePath = getPaths().web.distRscEntries - } - - if (!entriesFilePath) { - throw new Error('entries file not found at: ' + entriesFilePath) + const serverEntries = await getEntriesFromDist() + entryModule = path.join(getPaths().web.distRsc, serverEntries[rscId]) } - let getEntry: GetEntry - - if (isDev) { - const vite = await vitePromise - const { default: entriesFileModule } = - await vite.ssrLoadModule(entriesFilePath) - getEntry = entriesFileModule.getEntry - } else { - const { - default: { getEntry: getEntryProd }, - } = await (loadServerFile(entriesFilePath) as Promise) - - getEntry = getEntryProd + if (!entryModule) { + throw new StatusError('No entry found for ' + rscId, 404) } - const mod = await getEntry(rscId) + const mod = await loadServerFile(entryModule) if (typeof mod === 'function') { return mod