From bbe04b05c2dae3c3f50b1b1582570db0458eadde Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 31 Mar 2022 21:55:35 +0200 Subject: [PATCH 1/3] firefox transform stream fallback --- packages/next/client/index.tsx | 33 ++++++++------- .../test/rsc.js | 3 ++ .../react-18-streaming-ssr/index.test.ts | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 941c51bcd9dbe05..ab6a3e249e02fa3 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -680,7 +680,7 @@ if (process.env.__NEXT_RSC) { const encoder = new TextEncoder() let initialServerDataBuffer: string[] | undefined = undefined - let initialServerDataWriter: WritableStreamDefaultWriter | undefined = + let initialServerDataWriter: ReadableStreamDefaultController | undefined = undefined let initialServerDataLoaded = false let initialServerDataFlushed = false @@ -693,7 +693,7 @@ if (process.env.__NEXT_RSC) { throw new Error('Unexpected server data: missing bootstrap script.') if (initialServerDataWriter) { - initialServerDataWriter.write(encoder.encode(seg[2])) + initialServerDataWriter.enqueue(encoder.encode(seg[2])) } else { initialServerDataBuffer.push(seg[2]) } @@ -708,19 +708,19 @@ if (process.env.__NEXT_RSC) { // Hence, we use two variables `initialServerDataLoaded` and // `initialServerDataFlushed` to make sure the writer will be closed and // `initialServerDataBuffer` will be cleared in the right time. - function nextServerDataRegisterWriter(writer: WritableStreamDefaultWriter) { + function nextServerDataRegisterWriter(ctr: ReadableStreamDefaultController) { if (initialServerDataBuffer) { initialServerDataBuffer.forEach((val) => { - writer.write(encoder.encode(val)) + ctr.enqueue(encoder.encode(val)) }) if (initialServerDataLoaded && !initialServerDataFlushed) { - writer.close() + ctr.close() initialServerDataFlushed = true initialServerDataBuffer = undefined } } - initialServerDataWriter = writer + initialServerDataWriter = ctr } // When `DOMContentLoaded`, we can close all pending writers to finish hydration. @@ -764,19 +764,22 @@ if (process.env.__NEXT_RSC) { if (response) return response if (initialServerDataBuffer) { - const t = new TransformStream() - const writer = t.writable.getWriter() - response = createFromFetch(Promise.resolve({ body: t.readable })) - nextServerDataRegisterWriter(writer) + const readable = new ReadableStream({ + start(controller) { + nextServerDataRegisterWriter(controller) + }, + }) + response = createFromFetch(Promise.resolve({ body: readable })) } else { const fetchPromise = serialized ? (() => { - const t = new TransformStream() - const writer = t.writable.getWriter() - writer.ready.then(() => { - writer.write(new TextEncoder().encode(serialized)) + const readable = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(serialized)) + controller.close() + }, }) - return Promise.resolve({ body: t.readable }) + return Promise.resolve({ body: readable }) })() : fetchFlight(getCacheKey()) response = createFromFetch(fetchPromise) diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index e96e8dd40f84962..2abb6bc6e65f618 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -120,10 +120,13 @@ export default function (context, { runtime, env }) { }) it('should handle streaming server components correctly', async () => { + const temp = process.env.BROWSER_NAME + process.env.BROWSER_NAME = 'firefox' const browser = await webdriver(context.appPort, '/streaming-rsc') const content = await browser.eval( `document.querySelector('#content').innerText` ) + process.env.BROWSER_NAME = temp expect(content).toMatchInlineSnapshot('"next_streaming_data"') }) diff --git a/test/production/react-18-streaming-ssr/index.test.ts b/test/production/react-18-streaming-ssr/index.test.ts index 7bc84746ffc5f10..8e00683074f9c07 100644 --- a/test/production/react-18-streaming-ssr/index.test.ts +++ b/test/production/react-18-streaming-ssr/index.test.ts @@ -1,6 +1,7 @@ import { createNext } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' +import webdriver from 'next-webdriver' describe('react 18 streaming SSR in minimal mode', () => { let next: NextInstance @@ -15,6 +16,34 @@ describe('react 18 streaming SSR in minimal mode', () => { return

static streaming

} `, + 'pages/streaming-rsc.js': ` + import { Suspense } from 'react' + + let result + let promise + function Data() { + if (result) return result + if (!promise) + promise = new Promise((res) => { + setTimeout(() => { + result = 'data' + res() + }, 500) + }) + throw promise + } + + export default function Page() { + return ( + + + + ) + } + + export const config = { + runtime: 'edge', + }`, }, nextConfig: { experimental: { @@ -46,6 +75,19 @@ describe('react 18 streaming SSR in minimal mode', () => { expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') }) + + // Firefox doesn't support TransformStream at the moment, + // this test is to guarantee using ReadableStream on client works with hydration + it('should hydrate streaming content with firefox browser', async () => { + const originBrowserName = process.env.BROWSER_NAME + process.env.BROWSER_NAME = 'firefox' + const browser = await webdriver(next.url, '/') + const content = await browser.eval( + `document.querySelector('#content').innerText` + ) + expect(content).toMatchInlineSnapshot('"data"') + process.env.BROWSER_NAME = originBrowserName + }) }) describe('react 18 streaming SSR with custom next configs', () => { From ef3b41c8e4d801079111444e4f6cf37086e53232 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 31 Mar 2022 22:06:43 +0200 Subject: [PATCH 2/3] add ci job --- .github/workflows/build_test_deploy.yml | 3 ++ .../react-18-streaming-ssr/index.test.ts | 42 ------------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 1a889988566c5fa..a33e5809d4a32f7 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -535,6 +535,9 @@ jobs: if: ${{needs.build.outputs.docsChange != 'docs only change'}} - run: node run-tests.js test/integration/production/test/index.test.js if: ${{needs.build.outputs.docsChange != 'docs only change'}} + # test rsc hydration on firefox due to limited support of TransformStream api + - run: yarn jest test/integration/react-streaming-and-server-components/test/index.test.js -t "should handle streaming server components correctly" + if: ${{needs.build.outputs.docsChange != 'docs only change'}} testSafari: name: Test Safari (production) diff --git a/test/production/react-18-streaming-ssr/index.test.ts b/test/production/react-18-streaming-ssr/index.test.ts index 8e00683074f9c07..7bc84746ffc5f10 100644 --- a/test/production/react-18-streaming-ssr/index.test.ts +++ b/test/production/react-18-streaming-ssr/index.test.ts @@ -1,7 +1,6 @@ import { createNext } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' -import webdriver from 'next-webdriver' describe('react 18 streaming SSR in minimal mode', () => { let next: NextInstance @@ -16,34 +15,6 @@ describe('react 18 streaming SSR in minimal mode', () => { return

static streaming

} `, - 'pages/streaming-rsc.js': ` - import { Suspense } from 'react' - - let result - let promise - function Data() { - if (result) return result - if (!promise) - promise = new Promise((res) => { - setTimeout(() => { - result = 'data' - res() - }, 500) - }) - throw promise - } - - export default function Page() { - return ( - - - - ) - } - - export const config = { - runtime: 'edge', - }`, }, nextConfig: { experimental: { @@ -75,19 +46,6 @@ describe('react 18 streaming SSR in minimal mode', () => { expect(res.status).toBe(404) expect(await res.text()).toContain('This page could not be found') }) - - // Firefox doesn't support TransformStream at the moment, - // this test is to guarantee using ReadableStream on client works with hydration - it('should hydrate streaming content with firefox browser', async () => { - const originBrowserName = process.env.BROWSER_NAME - process.env.BROWSER_NAME = 'firefox' - const browser = await webdriver(next.url, '/') - const content = await browser.eval( - `document.querySelector('#content').innerText` - ) - expect(content).toMatchInlineSnapshot('"data"') - process.env.BROWSER_NAME = originBrowserName - }) }) describe('react 18 streaming SSR with custom next configs', () => { From c53fc7bccdfaf06ce391cf00186a4df0480405d6 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 31 Mar 2022 22:40:04 +0200 Subject: [PATCH 3/3] remove browser name assignment --- .github/workflows/build_test_deploy.yml | 2 +- .../react-streaming-and-server-components/test/rsc.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index a33e5809d4a32f7..9a68d66e41c105f 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -536,7 +536,7 @@ jobs: - run: node run-tests.js test/integration/production/test/index.test.js if: ${{needs.build.outputs.docsChange != 'docs only change'}} # test rsc hydration on firefox due to limited support of TransformStream api - - run: yarn jest test/integration/react-streaming-and-server-components/test/index.test.js -t "should handle streaming server components correctly" + - run: xvfb-run yarn jest test/integration/react-streaming-and-server-components/test/index.test.js -t "should handle streaming server components correctly" if: ${{needs.build.outputs.docsChange != 'docs only change'}} testSafari: diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index 2abb6bc6e65f618..e96e8dd40f84962 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -120,13 +120,10 @@ export default function (context, { runtime, env }) { }) it('should handle streaming server components correctly', async () => { - const temp = process.env.BROWSER_NAME - process.env.BROWSER_NAME = 'firefox' const browser = await webdriver(context.appPort, '/streaming-rsc') const content = await browser.eval( `document.querySelector('#content').innerText` ) - process.env.BROWSER_NAME = temp expect(content).toMatchInlineSnapshot('"next_streaming_data"') })