Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(rsc): Don't pass any props to the user's Routes #11539

Merged
merged 5 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 5 additions & 16 deletions packages/router/src/rsc/ClientRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ const LocationAwareRouter = ({
// 'No route found for the current URL. Make sure you have a route ' +
// 'defined for the root of your React app.',
// )
const routesProps = { location: { pathname, search } }
return <RscRoutes routesProps={routesProps} />
return <RscRoutes pathname={pathname} search={search} />
}

const requestedRoute = pathRouteMap[activeRoutePath]
Expand All @@ -70,8 +69,6 @@ const LocationAwareRouter = ({
)
}

const routesProps = { location: { pathname, search } }

return (
<RouterContextProvider
useAuth={useAuth}
Expand All @@ -80,23 +77,15 @@ const LocationAwareRouter = ({
activeRouteName={requestedRoute.name}
>
<AuthenticatedRoute unauthenticated={unauthenticated}>
<RscRoutes routesProps={routesProps} />
<RscRoutes pathname={pathname} search={search} />
</AuthenticatedRoute>
</RouterContextProvider>
)
}

const routesProps = { location: { pathname, search } }
// TODO (RSC): I think that moving between private and public routes
// re-initializes RscFetcher. I wonder if there's an optimization to be made
// here. Maybe we can lift RscFetcher up so we can keep the same instance
// re-initializes RscRoutes. I wonder if there's an optimization to be made
// here. Maybe we can lift RscRoutes up so we can keep the same instance
// around and reuse it everywhere
return <RscRoutes routesProps={routesProps} />
}

export interface RscFetchProps extends Record<string, unknown> {
location: {
pathname: string
search: string
}
return <RscRoutes pathname={pathname} search={search} />
}
61 changes: 29 additions & 32 deletions packages/router/src/rsc/RscRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ const BASE_PATH = '/rw-rsc/'

const rscCache = new RscCache()

export interface RscProps extends Record<string, unknown> {
location: {
pathname: string
search: string
}
}

let updateCurrentRscCacheKey = (key: string) => {
let updateCurrentRscCacheKey = (key: SerializedLocation) => {
console.error('updateCurrentRscCacheKey called before it was set')
console.error('updateCurrentRscCacheKey key', key)
}
Expand All @@ -36,11 +29,15 @@ function onStreamFinished(
)
}

function rscFetchRoutes(serializedProps: string) {
type SerializedLocation =
| `__rwjs__pathname=${string}&__rwjs__search=${string}`
| `__rwjs__pathname=${string}&__rwjs__search=${string}::${string}`

function rscFetchRoutes(serializedLocation: SerializedLocation) {
console.log(
'rscFetchRoutes :: args:\n serializedProps: ' + serializedProps,
'rscFetchRoutes :: args:\n serializedProps: ' + serializedLocation,
)
const rscCacheKey = serializedProps
const rscCacheKey = serializedLocation

const cached = rscCache.get(rscCacheKey)
if (cached) {
Expand All @@ -50,14 +47,11 @@ function rscFetchRoutes(serializedProps: string) {
console.log('rscFetchRoutes :: cache miss for', rscCacheKey)
}

const searchParams = new URLSearchParams()
searchParams.set('props', serializedProps)

const rscId = '__rwjs__Routes'

// TODO (RSC): During SSR we should not fetch (Is this function really
// called during SSR?)
const responsePromise = fetch(BASE_PATH + rscId + '?' + searchParams, {
const responsePromise = fetch(BASE_PATH + rscId + '?' + serializedLocation, {
headers: {
'rw-rsc': '1',
},
Expand All @@ -80,11 +74,10 @@ function rscFetchRoutes(serializedProps: string) {
// `new Date()`, to make sure the cache key is unique so we trigger a
// rerender. It's needed to handle calling RSAs multiple times with the
// same arguments
const rscCacheKey = `${serializedProps}::${rsaId}::${new Date()}`
const rscCacheKey: SerializedLocation = `${serializedLocation}::${rsaId}::${new Date()}`

const searchParams = new URLSearchParams()
searchParams.set('action_id', rsaId)
searchParams.set('props', serializedProps)
const rscId = '_'

let body: Awaited<ReturnType<typeof encodeReply>> = ''
Expand All @@ -95,13 +88,16 @@ function rscFetchRoutes(serializedProps: string) {
console.error('Error encoding Server Action arguments', e)
}

const responsePromise = fetch(BASE_PATH + rscId + '?' + searchParams, {
method: 'POST',
body,
headers: {
'rw-rsc': '1',
const responsePromise = fetch(
BASE_PATH + rscId + '?' + searchParams + '&' + serializedLocation,
{
method: 'POST',
body,
headers: {
'rw-rsc': '1',
},
},
})
)

onStreamFinished(responsePromise, () => {
updateCurrentRscCacheKey(rscCacheKey)
Expand Down Expand Up @@ -133,7 +129,8 @@ function rscFetchRoutes(serializedProps: string) {
}

interface Props {
routesProps: RscProps
pathname: string
search: string
}

// TODO (RSC): This only works as long as we only have one RscRoutes component.
Expand All @@ -144,18 +141,18 @@ let externalPromise = new Promise<React.ReactElement>((resolve) => {
externalPromiseResolver = resolve
})

export const RscRoutes = ({ routesProps }: Props) => {
const serializedProps = JSON.stringify(routesProps)
export const RscRoutes = ({ pathname, search }: Props) => {
const serializedLocation: SerializedLocation = `__rwjs__pathname=${pathname}&__rwjs__search=${search}`
const [currentRscCacheKey, setCurrentRscCacheKey] = useState(() => {
console.log('RscRoutes :: useState initial value')
// Calling rscFetchRoutes here to prime the cache
rscFetchRoutes(serializedProps)
return serializedProps
rscFetchRoutes(serializedLocation)
return serializedLocation
})

useEffect(() => {
console.log('RscRoutes :: useEffect set updateCurrentRscCacheKey')
updateCurrentRscCacheKey = (key: string) => {
updateCurrentRscCacheKey = (key: SerializedLocation) => {
console.log('RscRoutes inside updateCurrentRscCacheKey', key)

externalPromise = new Promise<React.ReactElement>((resolve) => {
Expand All @@ -168,13 +165,13 @@ export const RscRoutes = ({ routesProps }: Props) => {
useEffect(() => {
console.log('RscRoutes :: useEffect about to call rscFetchRoutes')
// rscFetchRoutes will update rscCache with the fetched component
rscFetchRoutes(serializedProps)
rscFetchRoutes(serializedLocation)

externalPromise = new Promise<React.ReactElement>((resolve) => {
externalPromiseResolver = resolve
})
setCurrentRscCacheKey(serializedProps)
}, [serializedProps])
setCurrentRscCacheKey(serializedLocation)
}, [serializedLocation])

console.log('RscRoutes :: rendering cache entry for\n' + currentRscCacheKey)

Expand Down
2 changes: 1 addition & 1 deletion packages/router/src/rsc/ServerRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const Router: React.FC<RouterProps> = ({ paramTypes, children }) => {
}

// TODO (RSC): We allow users to implement their own `hasRole` function in
// `api/src/lib/auth.ts`. We should use that instead of implementing our
// `api/src/lib/auth.ts`. We should use that instead of implementing our own
// here. Should we just import it from there? Or should we allow users to
// pass it to us instead somehow?
function hasRole(requiredRoles: string | string[]): boolean {
Expand Down
44 changes: 44 additions & 0 deletions packages/vite/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Request as ExpressRequest } from 'express'
import { describe, it, expect } from 'vitest'

import { getFullUrl } from '../utils.js'

function mockExpressRequest(url: string) {
const req = {
originalUrl: url,
protocol: 'http',
headers: { host: 'localhost:8910' },
get: (header: 'host') => req.headers[header],
// Type casting here because a proper Request object has about 100 fields
// and I don't want to list them all here just for a test
} as ExpressRequest

return req
}

describe('getFullUrl', () => {
describe('rsc enabled', () => {
const rscEnabled = true

it("returns the original URL if the request's searchParams don't include rsc request props", () => {
const req = mockExpressRequest('/foo/bar?extra=baz')

const result = getFullUrl(req, rscEnabled)

expect(result).toBe('http://localhost:8910/foo/bar?extra=baz')
})

it("reads pathname and search parameters from the request's searchParams if they're available", () => {
const req = mockExpressRequest(
'/rw-rsc/__rwjs__Routes?__rwjs__pathname=' +
encodeURIComponent('/cux/cuux') +
'&__rwjs__search=' +
encodeURIComponent('extra=corge'),
)

const result = getFullUrl(req, rscEnabled)

expect(result).toBe('http://localhost:8910/cux/cuux?extra=corge')
})
})
})
2 changes: 1 addition & 1 deletion packages/vite/src/devFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async function createServer() {
const serverStorage = createServerStorage()

app.use('*', (req, _res, next) => {
const fullUrl = getFullUrl(req)
const fullUrl = getFullUrl(req, rscEnabled)

const perReqStore = createPerRequestMap({
// Convert express headers to fetch header
Expand Down
11 changes: 3 additions & 8 deletions packages/vite/src/rsc/rscRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import { renderToReadableStream } from 'react-server-dom-webpack/server.edge'

import { getPaths } from '@redwoodjs/project-config'

import type { RscFetchProps } from '../../../router/src/rsc/ClientRouter.tsx'
import { getEntriesFromDist } from '../lib/entries.js'
import { StatusError } from '../lib/StatusError.js'

export type RenderInput = {
rscId?: string | undefined
props: RscFetchProps | Record<string, unknown>
rsaId?: string | undefined
args?: unknown[] | undefined
}
Expand Down Expand Up @@ -121,18 +119,17 @@ async function renderRsc(input: RenderInput): Promise<ReadableStream> {
)
}

if (!input.rscId || !input.props) {
if (!input.rscId) {
throw new Error('Unexpected input. Missing rscId or props.')
}

console.log('renderRsc input', input)

const serverRoutes = await getRoutesComponent()
const model: RscModel = {
__rwjs__Routes: createElement(serverRoutes, input.props),
__rwjs__Routes: createElement(serverRoutes),
}

console.log('rscRenderer.ts renderRsc props', input.props)
console.log('rscRenderer.ts renderRsc model', model)

return renderToReadableStream(model, getBundlerConfig())
Expand Down Expand Up @@ -190,9 +187,7 @@ async function executeRsa(input: RenderInput): Promise<ReadableStream> {
const serverRoutes = await getRoutesComponent()
console.log('rscRenderer.ts executeRsa serverRoutes', serverRoutes)
const model: RscModel = {
__rwjs__Routes: createElement(serverRoutes, {
location: { pathname: '/', search: '' },
}),
__rwjs__Routes: createElement(serverRoutes),
__rwjs__rsa_data: data,
}
console.log('rscRenderer.ts executeRsa model', model)
Expand Down
9 changes: 1 addition & 8 deletions packages/vite/src/rsc/rscRequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type Router from 'find-my-way'
import type { HTTPMethod } from 'find-my-way'
import type { ViteDevServer } from 'vite'

import type { RscFetchProps } from '@redwoodjs/router/RscRouter'
import type { Middleware } from '@redwoodjs/web/dist/server/middleware'

import {
Expand Down Expand Up @@ -90,11 +89,6 @@ export function createRscRequestHandler(

const url = new URL(req.originalUrl || '', 'http://' + req.headers.host)
let rscId: string | undefined
// "location":{"pathname":"/about","search":""}
// These values come from packages/vite/src/ClientRouter.tsx
const props: RscFetchProps = JSON.parse(
url.searchParams.get('props') || '{}',
)
let rsaId: string | undefined
let args: unknown[] = []

Expand Down Expand Up @@ -188,14 +182,13 @@ export function createRscRequestHandler(
}

try {
const readable = await renderRscToStream({ rscId, props, rsaId, args })
const readable = await renderRscToStream({ rscId, rsaId, args })

Readable.fromWeb(readable).pipe(res)

// TODO (RSC): Can we reuse `readable` here somehow?
await sendRscFlightToStudio({
rscId,
props,
rsaId,
args,
basePath: BASE_PATH,
Expand Down
10 changes: 2 additions & 8 deletions packages/vite/src/rsc/rscStudioHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,14 @@ export const sendRscFlightToStudio = async (input: StudioRenderInput) => {
console.debug('Studio is not enabled')
return
}
const { rscId, props, rsaId, args, basePath, req, handleError } = input
const { rscId, rsaId, args, basePath, req, handleError } = input

try {
// surround renderRsc with performance metrics
const startedAt = Date.now()
const start = performance.now()

const readable = await renderRscToStream({
rscId,
props,
rsaId,
args,
})
const readable = await renderRscToStream({ rscId, rsaId, args })
const endedAt = Date.now()
const end = performance.now()
const duration = end - start
Expand All @@ -145,7 +140,6 @@ export const sendRscFlightToStudio = async (input: StudioRenderInput) => {
rsc: {
rscId,
rsaId,
props,
args,
},
request: {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/runFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export async function runFeServer() {
)

app.use('*', (req, _res, next) => {
const fullUrl = getFullUrl(req)
const fullUrl = getFullUrl(req, rscEnabled)
const headers = convertExpressHeaders(req.headersDistinct)
// Convert express headers to fetch headers
const perReqStore = createPerRequestMap({ headers, fullUrl })
Expand Down
Loading
Loading