diff --git a/packages/plugin-rsc/e2e/fixture.ts b/packages/plugin-rsc/e2e/fixture.ts index 4c927a271..ac1d7385d 100644 --- a/packages/plugin-rsc/e2e/fixture.ts +++ b/packages/plugin-rsc/e2e/fixture.ts @@ -217,7 +217,7 @@ export async function setupInlineFixture(options: { let filepath = path.join(options.dest, filename) fs.mkdirSync(path.dirname(filepath), { recursive: true }) // strip indent - contents = contents.replace(/^\n/, '') + contents = contents.replace(/^\n*/, '').replace(/\s*$/, '\n') const indent = contents.match(/^\s*/)?.[0] ?? '' const strippedContents = contents .split('\n') diff --git a/packages/plugin-rsc/e2e/helper.ts b/packages/plugin-rsc/e2e/helper.ts index b1b167e38..702c5b7ec 100644 --- a/packages/plugin-rsc/e2e/helper.ts +++ b/packages/plugin-rsc/e2e/helper.ts @@ -42,3 +42,15 @@ export async function expectNoReload(page: Page) { }, } } + +export function expectNoPageError(page: Page) { + const errors: Error[] = [] + page.on('pageerror', (error) => { + errors.push(error) + }) + return { + [Symbol.dispose]: () => { + expect(errors).toEqual([]) + }, + } +} diff --git a/packages/plugin-rsc/e2e/ssr-thenable.test.ts b/packages/plugin-rsc/e2e/ssr-thenable.test.ts new file mode 100644 index 000000000..7bf9e14e4 --- /dev/null +++ b/packages/plugin-rsc/e2e/ssr-thenable.test.ts @@ -0,0 +1,64 @@ +import { test } from '@playwright/test' +import { setupInlineFixture, type Fixture, useFixture } from './fixture' +import { + expectNoPageError, + waitForHydration as waitForHydration_, +} from './helper' + +test.describe(() => { + const root = 'examples/e2e/temp/ssr-thenable' + + test.beforeAll(async () => { + await setupInlineFixture({ + src: 'examples/starter', + dest: root, + files: { + 'src/root.tsx': /* tsx */ ` + import { TestClientUse } from './client.tsx' + + export function Root() { + return ( + + + + + + + + + ) + } + `, + 'src/client.tsx': /* tsx */ ` + "use client"; + import React from 'react' + + const promise = Promise.resolve('ok') + + export function TestClientUse() { + const value = React.use(promise) + return {value} + } + `, + }, + }) + }) + + function defineSsrThenableTest(f: Fixture) { + test('ssr-thenable', async ({ page }) => { + using _ = expectNoPageError(page) + await page.goto(f.url()) + await waitForHydration_(page) + }) + } + + test.describe('dev', () => { + const f = useFixture({ root, mode: 'dev' }) + defineSsrThenableTest(f) + }) + + test.describe('build', () => { + const f = useFixture({ root, mode: 'build' }) + defineSsrThenableTest(f) + }) +}) diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx index 0fd5e92f3..2c7abbc76 100644 --- a/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.ssr.tsx @@ -24,7 +24,11 @@ export async function renderHTML( // deserialization needs to be kicked off inside ReactDOMServer context // for ReactDomServer preinit/preloading to work payload ??= ReactClient.createFromReadableStream(rscStream1) - return React.use(payload).root + return {React.use(payload).root} + } + + function FixSsrThenable(props: React.PropsWithChildren) { + return props.children } // render html (traditional SSR) diff --git a/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx b/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx index 1918f834b..8ce5f4e11 100644 --- a/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx +++ b/packages/plugin-rsc/examples/starter/src/framework/entry.ssr.tsx @@ -19,12 +19,21 @@ export async function renderHTML( const [rscStream1, rscStream2] = rscStream.tee() // deserialize RSC stream back to React VDOM - let payload: Promise + let payload: Promise | undefined function SsrRoot() { // deserialization needs to be kicked off inside ReactDOMServer context // for ReactDomServer preinit/preloading to work payload ??= ReactClient.createFromReadableStream(rscStream1) - return React.use(payload).root + return {React.use(payload).root} + } + + // Add an empty component in between `SsrRoot` and user `root` to avoid React SSR bugs. + // SsrRoot (use) + // => FixSsrThenable + // => root (which potentially has `lazy` + `use`) + // https://github.com/facebook/react/issues/33937#issuecomment-3091349011 + function FixSsrThenable(props: React.PropsWithChildren) { + return props.children } // render html (traditional SSR)