diff --git a/.changesets/10412.md b/.changesets/10412.md new file mode 100644 index 000000000000..7f4d10a204fd --- /dev/null +++ b/.changesets/10412.md @@ -0,0 +1,7 @@ +- SSR: Better ServerEntry types (#10412) by @Tobbe + +When enabling SSR the setup command will generate an `entry.server.tsx` file in +the user's app. This file exports a `ServerEntry` component that takes `css` +and ` meta` as props. The `meta` props used to be typed as `any`, making it +difficult to use with confidence. This PR makes the type be `TagDescriptor[]` +which is more correct. diff --git a/__fixtures__/test-project-rsa/web/src/entry.server.tsx b/__fixtures__/test-project-rsa/web/src/entry.server.tsx index a52b268b771d..2ef279387fd2 100644 --- a/__fixtures__/test-project-rsa/web/src/entry.server.tsx +++ b/__fixtures__/test-project-rsa/web/src/entry.server.tsx @@ -1,9 +1,11 @@ +import type { TagDescriptor } from '@redwoodjs/web' + import App from './App' import { Document } from './Document' interface Props { css: string[] - meta?: any[] + meta?: TagDescriptor[] } export const ServerEntry: React.FC = ({ css, meta }) => { diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/entry.server.tsx b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/entry.server.tsx index a52b268b771d..2ef279387fd2 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/entry.server.tsx +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/entry.server.tsx @@ -1,9 +1,11 @@ +import type { TagDescriptor } from '@redwoodjs/web' + import App from './App' import { Document } from './Document' interface Props { css: string[] - meta?: any[] + meta?: TagDescriptor[] } export const ServerEntry: React.FC = ({ css, meta }) => { diff --git a/packages/cli/src/commands/experimental/templates/streamingSsr/entry.server.tsx.template b/packages/cli/src/commands/experimental/templates/streamingSsr/entry.server.tsx.template index a52b268b771d..2ef279387fd2 100644 --- a/packages/cli/src/commands/experimental/templates/streamingSsr/entry.server.tsx.template +++ b/packages/cli/src/commands/experimental/templates/streamingSsr/entry.server.tsx.template @@ -1,9 +1,11 @@ +import type { TagDescriptor } from '@redwoodjs/web' + import App from './App' import { Document } from './Document' interface Props { css: string[] - meta?: any[] + meta?: TagDescriptor[] } export const ServerEntry: React.FC = ({ css, meta }) => { diff --git a/packages/vite/src/middleware/register.ts b/packages/vite/src/middleware/register.ts index 1d498daa55e6..4680bf807246 100644 --- a/packages/vite/src/middleware/register.ts +++ b/packages/vite/src/middleware/register.ts @@ -4,7 +4,8 @@ import type { ViteDevServer } from 'vite' import { getPaths } from '@redwoodjs/project-config' -import { makeFilePath } from '../utils' +import type { EntryServer } from '../types' +import { makeFilePath, ssrLoadEntryServer } from '../utils' import type { MiddlewareRequest } from './MiddlewareRequest' import { MiddlewareResponse } from './MiddlewareResponse' @@ -12,13 +13,9 @@ import type { Middleware, MiddlewareClass, MiddlewareInvokeOptions, + MiddlewareReg, } from './types' -// Tuple of [mw, '*.{extension}'] -export type MiddlewareReg = Array< - [Middleware | MiddlewareClass, string] | Middleware | MiddlewareClass -> - type GroupedMw = Record const validateMw = (mw: MiddlewareClass | Middleware): Middleware => { @@ -108,15 +105,10 @@ export const createMiddlewareRouter = async ( ): Promise> => { const rwPaths = getPaths() - const entryServerPath = rwPaths.web.entryServer - - if (!entryServerPath) { - throw new Error('Entry server not found. Could not load middleware') - } + let entryServerImport: EntryServer - let entryServerImport: Record<'registerMiddleware', () => MiddlewareReg> if (vite) { - entryServerImport = await vite.ssrLoadModule(entryServerPath) + entryServerImport = await ssrLoadEntryServer(vite) } else { // This imports from dist! entryServerImport = await import(makeFilePath(rwPaths.web.distEntryServer)) diff --git a/packages/vite/src/middleware/types.ts b/packages/vite/src/middleware/types.ts index 26e7962ffed9..68cbce1ef0ee 100644 --- a/packages/vite/src/middleware/types.ts +++ b/packages/vite/src/middleware/types.ts @@ -18,3 +18,8 @@ export type MiddlewareInvokeOptions = { cssPaths?: Array params?: Record } + +// Tuple of [mw, '*.{extension}'] +export type MiddlewareReg = Array< + [Middleware | MiddlewareClass, string] | Middleware | MiddlewareClass +> diff --git a/packages/vite/src/streaming/createReactStreamingHandler.ts b/packages/vite/src/streaming/createReactStreamingHandler.ts index 252200828048..ef7abfb82873 100644 --- a/packages/vite/src/streaming/createReactStreamingHandler.ts +++ b/packages/vite/src/streaming/createReactStreamingHandler.ts @@ -15,7 +15,8 @@ import type { TagDescriptor } from '@redwoodjs/web' import { invoke } from '../middleware/invokeMiddleware.js' import { MiddlewareResponse } from '../middleware/MiddlewareResponse.js' import type { Middleware } from '../middleware/types.js' -import { makeFilePath } from '../utils.js' +import type { EntryServer } from '../types.js' +import { makeFilePath, ssrLoadEntryServer } from '../utils.js' import { reactRenderToStreamResponse } from './streamHelpers.js' import { loadAndRunRouteHooks } from './triggerRouteHooks.js' @@ -43,8 +44,8 @@ export const createReactStreamingHandler = async ( const isProd = !viteDevServer const middlewareRouter: Router.Instance = await getMiddlewareRouter() - let entryServerImport: any - let fallbackDocumentImport: any + let entryServerImport: EntryServer + let fallbackDocumentImport: Record // Load the entries for prod only once, not in each handler invocation // Dev is the opposite, we load it everytime to pick up changes @@ -118,9 +119,7 @@ export const createReactStreamingHandler = async ( // Do this inside the handler for **dev-only**. // This makes sure that changes to entry-server are picked up on refresh if (!isProd) { - entryServerImport = await viteDevServer.ssrLoadModule( - rwPaths.web.entryServer as string, // already validated in dev server - ) + entryServerImport = await ssrLoadEntryServer(viteDevServer) fallbackDocumentImport = await viteDevServer.ssrLoadModule( rwPaths.web.document, ) diff --git a/packages/vite/src/streaming/streamHelpers.ts b/packages/vite/src/streaming/streamHelpers.ts index 350d961c2677..65878a9e8116 100644 --- a/packages/vite/src/streaming/streamHelpers.ts +++ b/packages/vite/src/streaming/streamHelpers.ts @@ -19,14 +19,15 @@ import { } from '@redwoodjs/web/dist/components/ServerInject' import type { MiddlewareResponse } from '../middleware/MiddlewareResponse.js' +import type { ServerEntryType } from '../types.js' import { createBufferedTransformStream } from './transforms/bufferedTransform.js' import { createTimeoutTransform } from './transforms/cancelTimeoutTransform.js' import { createServerInjectionTransform } from './transforms/serverInjectionTransform.js' interface RenderToStreamArgs { - ServerEntry: any - FallbackDocument: any + ServerEntry: ServerEntryType + FallbackDocument: React.FunctionComponent currentPathName: string metaTags: TagDescriptor[] cssLinks: string[] @@ -120,8 +121,7 @@ export async function reactRenderToStreamResponse( { value: injectToPage, }, - ServerEntry({ - url: path, + React.createElement(ServerEntry, { css: cssLinks, meta: metaTags, }), diff --git a/packages/vite/src/types.ts b/packages/vite/src/types.ts index 8d29722d3863..66cbed6819c7 100644 --- a/packages/vite/src/types.ts +++ b/packages/vite/src/types.ts @@ -10,7 +10,27 @@ * **All** of these properties are used by the prod FE server */ import type { RWRouteManifestItem } from '@redwoodjs/internal' +import type { TagDescriptor } from '@redwoodjs/web' + +import type { MiddlewareReg } from './middleware/types' export type RWRouteManifest = Record type PathDefinition = string + +export type ServerEntryType = React.FunctionComponent<{ + css: string[] + meta: TagDescriptor[] +}> + +export type EntryServer = + | { + registerMiddleware?: () => MiddlewareReg + ServerEntry: ServerEntryType + default: never + } + | { + registerMiddleware?: () => MiddlewareReg + ServerEntry: never + default: ServerEntryType + } diff --git a/packages/vite/src/utils.ts b/packages/vite/src/utils.ts index 96a08153b98c..c446bb2b708f 100644 --- a/packages/vite/src/utils.ts +++ b/packages/vite/src/utils.ts @@ -1,5 +1,9 @@ +import type { ViteDevServer } from 'vite' + import { getPaths } from '@redwoodjs/project-config' +import type { EntryServer } from './types' + export function stripQueryStringAndHashFromPath(url: string) { return url.split('?')[0].split('#')[0] } @@ -19,7 +23,22 @@ export function ensureProcessDirWeb(webDir: string = getPaths().web.base) { process.chdir(webDir) } } + export function makeFilePath(path: string): string { // Without this, absolute paths can't be imported on Windows return 'file:///' + path } + +export async function ssrLoadEntryServer(viteDevServer: ViteDevServer) { + const rwPaths = getPaths() + + if (!rwPaths.web.entryServer) { + throw new Error('entryServer not defined') + } + + return viteDevServer.ssrLoadModule( + rwPaths.web.entryServer, + // Have to type cast here because ssrLoadModule just returns a generic + // Record type + ) as Promise +} diff --git a/packages/web/ambient.d.ts b/packages/web/ambient.d.ts index 4a10ccf48777..8eba595af95c 100644 --- a/packages/web/ambient.d.ts +++ b/packages/web/ambient.d.ts @@ -2,6 +2,8 @@ import type { NormalizedCacheObject } from '@apollo/client' import type { HelmetServerState } from 'react-helmet-async' +import type { TagDescriptor } from '@redwoodjs/web' + declare global { var __REDWOOD__PRERENDERING: boolean var __REDWOOD__HELMET_CONTEXT: { helmet?: HelmetServerState }