From 8c30c09572fb09678c4b02468ca6b7049a9de71a Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Tue, 21 Dec 2021 14:25:44 +0100 Subject: [PATCH 01/11] feat: provide request object to rendering tree --- packages/hydrogen/package.json | 4 +- packages/hydrogen/src/entry-server.tsx | 35 +++++++++++++- .../ServerRequestProvider.tsx | 46 +++++++++++++++++++ .../foundation/ServerRequestProvider/index.ts | 1 + packages/hydrogen/src/foundation/index.tsx | 1 + .../ServerComponentRequest.server.ts | 2 + yarn.lock | 16 ++++--- 7 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx create mode 100644 packages/hydrogen/src/foundation/ServerRequestProvider/index.ts diff --git a/packages/hydrogen/package.json b/packages/hydrogen/package.json index 4f7822b759..5fcb273cc8 100644 --- a/packages/hydrogen/package.json +++ b/packages/hydrogen/package.json @@ -96,8 +96,8 @@ "react-error-boundary": "^3.1.3", "react-helmet-async": "^1.0.9", "react-query": "^3.18.1", - "react-client": "link:../../../../react/build/node_modules/react-client", - "react-server": "link:../../../../react/build/node_modules/react-server", + "react-client": "file:../../../../react/build/node_modules/react-client", + "react-server": "file:../../../../react/build/node_modules/react-server", "react-ssr-prepass": "^1.4.0", "vite-plugin-inspect": "^0.3.6" } diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index d84383aff4..e9ddf57912 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -19,6 +19,10 @@ import {ServerComponentResponse} from './framework/Hydration/ServerComponentResp import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server'; import {dehydrate} from 'react-query/hydration'; import {getCacheControlHeader} from './framework/cache'; +import { + ServerRequestProvider, + requestHydrationCache, +} from './foundation/ServerRequestProvider'; import type {ServerResponse} from 'http'; import { @@ -193,6 +197,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { context, request, dev, + isHydration: true, }); response.socket!.on('error', (error: any) => { @@ -221,22 +226,50 @@ function buildReactApp({ context, request, dev, + isHydration, }: { App: ComponentType; state: any; context: any; request: ServerComponentRequest; dev: boolean | undefined; + isHydration?: boolean; }) { // const helmetContext = {} as FilledContext; const componentResponse = new ServerComponentResponse(); + function Wrapper({children}: any) { + if (isHydration) { + // Save the request object in a React cache that is + // scoped to this current rendering. + + // @ts-ignore + const requestCache = React.unstable_getCacheForType( + requestHydrationCache + ); + + requestCache.set(requestHydrationCache.key, request); + + return children; + } + + // Use a normal provider in SSR to make the request object + // available in the current rendering. + return ( + + {children} + + ); + } + const ReactApp = (props: any) => ( // - + + + // ); diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx new file mode 100644 index 0000000000..0b2f371a4a --- /dev/null +++ b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx @@ -0,0 +1,46 @@ +import React, {createContext, ReactNode, useContext} from 'react'; +import type {ServerComponentRequest} from '../../framework/Hydration/ServerComponentRequest.server'; + +// Context to inject current request in SSR +const RequestContext = createContext(null as any); + +// Cache to inject current request in RSC +export function requestHydrationCache() { + return new Map(); +} + +requestHydrationCache.key = Symbol.for('request'); + +export function useServerRequest() { + let request: ServerComponentRequest; + try { + // Context only works in SSR rendering + request = useContext(RequestContext); + } catch (error) { + // If normal context failed it means this is not an SSR request. + // Try getting RSC cache instead: + // @ts-ignore + const cache = React.unstable_getCacheForType(requestHydrationCache); + request = cache ? cache.get(requestHydrationCache.key) : null; + } + + if (!request) { + throw new Error('No ServerRequest Context found'); + } + + return request; +} + +export function ServerRequestProvider({ + request, + children, +}: { + request: ServerComponentRequest; + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/index.ts b/packages/hydrogen/src/foundation/ServerRequestProvider/index.ts new file mode 100644 index 0000000000..19cc78994f --- /dev/null +++ b/packages/hydrogen/src/foundation/ServerRequestProvider/index.ts @@ -0,0 +1 @@ +export * from './ServerRequestProvider'; diff --git a/packages/hydrogen/src/foundation/index.tsx b/packages/hydrogen/src/foundation/index.tsx index 90c8fe4b3d..cbde043289 100644 --- a/packages/hydrogen/src/foundation/index.tsx +++ b/packages/hydrogen/src/foundation/index.tsx @@ -3,3 +3,4 @@ export {ShopifyServerProvider} from './ShopifyProvider/ShopifyServerProvider.ser export * from './ServerStateProvider'; export {useShop} from './useShop'; export {DefaultRoutes} from './Router'; +export {useServerRequest} from './ServerRequestProvider'; diff --git a/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts b/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts index a2ba5c2846..cff0eb7fc6 100644 --- a/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts +++ b/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts @@ -7,6 +7,7 @@ */ export class ServerComponentRequest extends Request { public cookies: Map; + public context: {cache: Map; [key: string]: any}; constructor(input: any); constructor(input: RequestInfo, init?: RequestInit); @@ -20,6 +21,7 @@ export class ServerComponentRequest extends Request { }); } + this.context = {cache: new Map()}; this.cookies = this.parseCookies(); } diff --git a/yarn.lock b/yarn.lock index 51d331a6b2..4892b762d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11157,9 +11157,11 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -"react-client@link:../../react/build/node_modules/react-client": - version "0.0.0" - uid "" +"react-client@file:../../react/build/node_modules/react-client": + version "0.1.0" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" react-dom@0.0.0-experimental-0cc724c77-20211125: version "0.0.0-experimental-0cc724c77-20211125" @@ -11260,9 +11262,11 @@ react-router@5.2.1: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -"react-server@link:../../react/build/node_modules/react-server": - version "0.0.0" - uid "" +"react-server@file:../../react/build/node_modules/react-server": + version "0.1.0" + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" react-ssr-prepass@^1.4.0: version "1.4.0" From 2837bef242c81dca09bd5a01f15ec9d2fcd0c7da Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Tue, 21 Dec 2021 19:03:34 +0100 Subject: [PATCH 02/11] wip: example of useServerRequest --- packages/dev/src/RSCTest/App.server.jsx | 2 ++ packages/dev/src/RSCTest/CAsync1.server.jsx | 15 +++++++++++++ packages/dev/src/RSCTest/CAsync2.server.jsx | 7 ++++++ packages/dev/src/RSCTest/async-stuff.js | 25 +++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 packages/dev/src/RSCTest/CAsync1.server.jsx create mode 100644 packages/dev/src/RSCTest/CAsync2.server.jsx create mode 100644 packages/dev/src/RSCTest/async-stuff.js diff --git a/packages/dev/src/RSCTest/App.server.jsx b/packages/dev/src/RSCTest/App.server.jsx index 4cfd547fcf..948096bb43 100644 --- a/packages/dev/src/RSCTest/App.server.jsx +++ b/packages/dev/src/RSCTest/App.server.jsx @@ -4,6 +4,7 @@ import {Suspense} from 'react'; import C1 from './C1.client'; import C2 from './C2.client'; import CShared from './CShared'; +import CAsync1 from './CAsync1.server'; export default function App({...serverState}) { // const pages = import.meta.globEager('./pages/**/*.server.[jt]sx'); @@ -15,6 +16,7 @@ export default function App({...serverState}) { + ); diff --git a/packages/dev/src/RSCTest/CAsync1.server.jsx b/packages/dev/src/RSCTest/CAsync1.server.jsx new file mode 100644 index 0000000000..7e77dfcb1f --- /dev/null +++ b/packages/dev/src/RSCTest/CAsync1.server.jsx @@ -0,0 +1,15 @@ +import runAsync from './async-stuff'; +import CAsync2 from './CAsync2.server'; + +export default function CAsync1() { + const result = runAsync('CAsync1'); + + return ( +
+ c-async-1 {String(result)} +
+ +
+
+ ); +} diff --git a/packages/dev/src/RSCTest/CAsync2.server.jsx b/packages/dev/src/RSCTest/CAsync2.server.jsx new file mode 100644 index 0000000000..d8f06cadbf --- /dev/null +++ b/packages/dev/src/RSCTest/CAsync2.server.jsx @@ -0,0 +1,7 @@ +import runAsync from './async-stuff'; + +export default function CAsync2() { + const result = runAsync('CAsync2'); + + return
c-async-2 {String(result)}
; +} diff --git a/packages/dev/src/RSCTest/async-stuff.js b/packages/dev/src/RSCTest/async-stuff.js new file mode 100644 index 0000000000..f23f75d0c6 --- /dev/null +++ b/packages/dev/src/RSCTest/async-stuff.js @@ -0,0 +1,25 @@ +import {useServerRequest} from '@shopify/hydrogen'; + +export default function (key, ms = 1000) { + const request = useServerRequest(); + const cache = request.context.cache; + + const cached = cache.get(key); + + if (cached) { + if (cached instanceof Promise) { + throw cached; + } + + return cached; + } + + console.log('---FETCHING', key); + const promise = new Promise((r) => setTimeout(() => r(true), ms)); + cache.set(key, promise); + promise.then((result) => { + cache.set(key, result); + }); + + throw promise; +} From 00462a964436adfefc3f0ba0a15c773f15567106 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Tue, 21 Dec 2021 19:22:30 +0100 Subject: [PATCH 03/11] feat: inline RSC response in the SSR HTML response --- packages/hydrogen/src/entry-server.tsx | 46 ++++++++++++++++++- .../src/framework/Hydration/Cache.client.ts | 28 ++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index e9ddf57912..0cffa9e97f 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -90,12 +90,44 @@ const renderHydrogen: ServerHandler = (App, hook) => { * Stream a response to the client. NOTE: This omits custom `` * information, so this method should not be used by crawlers. */ - const stream: Streamer = function ( + const stream: Streamer = async function ( url: URL, {context, request, response, template, dev} ) { const state = {pathname: url.pathname, search: url.search}; + // App for RSC rendering + const {ReactApp: HydrationReactApp} = buildReactApp({ + App, + state, + context, + request, + dev, + isHydration: true, + }); + + let flightResponse = ''; + if (rscRenderToPipeableStream) { + // Node.js branch + + const {pipe} = rscRenderToPipeableStream( + + ); + + const {PassThrough} = await import('stream'); + const writer = new PassThrough(); + writer.setEncoding('utf-8'); + writer.on('data', (chunk: string) => { + // TODO: wrap each chunk in ` + ); + } + if (componentResponse.canStream() || response.writableEnded) return; writeHeadToServerResponse(response, componentResponse, didError); diff --git a/packages/hydrogen/src/framework/Hydration/Cache.client.ts b/packages/hydrogen/src/framework/Hydration/Cache.client.ts index 86d1b4ecda..d8ca8da9ff 100644 --- a/packages/hydrogen/src/framework/Hydration/Cache.client.ts +++ b/packages/hydrogen/src/framework/Hydration/Cache.client.ts @@ -1,6 +1,9 @@ // @ts-ignore import {unstable_getCacheForType, unstable_useCacheRefresh} from 'react'; -import {createFromFetch} from '../Hydration/rsc-client-hydrator'; +import { + createFromFetch, + createFromReadableStream, +} from '../Hydration/rsc-client-hydrator'; import type {FlightResponse} from '../Hydration/rsc-client-config'; function createResponseCache() { @@ -23,7 +26,28 @@ export function useServerResponse(state: any) { return response; } - response = createFromFetch(fetch('/react?state=' + encodeURIComponent(key))); + // @ts-ignore + if (window.__flight) { + // The flight response was inlined during SSR, use it directly. + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + // @ts-ignore + controller.enqueue(encoder.encode(window.__flight)); + controller.close(); + }, + }); + + response = createFromReadableStream(stream); + + // @ts-ignore + delete window.__flight; + } else { + // Request a new flight response. + response = createFromFetch( + fetch('/react?state=' + encodeURIComponent(key)) + ); + } cache.set(key, response); return response; From ff72d26c65436a6c55ea34514445b38d7ec8a29e Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Thu, 6 Jan 2022 15:26:07 +0100 Subject: [PATCH 04/11] fix: add default value to the SSR provider --- packages/dev/src/RSCTest/async-stuff.js | 2 +- .../ServerRequestProvider/ServerRequestProvider.tsx | 4 +++- .../Hydration/ServerComponentRequest.server.ts | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/dev/src/RSCTest/async-stuff.js b/packages/dev/src/RSCTest/async-stuff.js index f23f75d0c6..86fb148d2a 100644 --- a/packages/dev/src/RSCTest/async-stuff.js +++ b/packages/dev/src/RSCTest/async-stuff.js @@ -14,7 +14,7 @@ export default function (key, ms = 1000) { return cached; } - console.log('---FETCHING', key); + console.log('---FETCHING', key, request?.id || 'undefined'); const promise = new Promise((r) => setTimeout(() => r(true), ms)); cache.set(key, promise); promise.then((result) => { diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx index 0b2f371a4a..be395bb7cb 100644 --- a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx +++ b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx @@ -2,7 +2,9 @@ import React, {createContext, ReactNode, useContext} from 'react'; import type {ServerComponentRequest} from '../../framework/Hydration/ServerComponentRequest.server'; // Context to inject current request in SSR -const RequestContext = createContext(null as any); +const RequestContext = createContext({ + context: {cache: new Map()}, +} as ServerComponentRequest); // Cache to inject current request in RSC export function requestHydrationCache() { diff --git a/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts b/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts index cff0eb7fc6..efecff1ab2 100644 --- a/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts +++ b/packages/hydrogen/src/framework/Hydration/ServerComponentRequest.server.ts @@ -1,3 +1,12 @@ +let reqCounter = 0; // For debugging +const generateId = + typeof crypto !== 'undefined' && + // @ts-ignore + !!crypto.randomUUID + ? // @ts-ignore + () => crypto.randomUUID() as string + : () => `req${++reqCounter}`; + /** * This augments the `Request` object from the Fetch API: * @see https://developer.mozilla.org/en-US/docs/Web/API/Request @@ -7,6 +16,7 @@ */ export class ServerComponentRequest extends Request { public cookies: Map; + public id: string; public context: {cache: Map; [key: string]: any}; constructor(input: any); @@ -23,6 +33,7 @@ export class ServerComponentRequest extends Request { this.context = {cache: new Map()}; this.cookies = this.parseCookies(); + this.id = generateId(); } private parseCookies() { From c8e5efb9c0b50da707483cd3eaa65fbfc70357ef Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Thu, 6 Jan 2022 18:52:41 +0100 Subject: [PATCH 05/11] feat: stream rsc in script tags --- packages/hydrogen/src/entry-server.tsx | 30 ++++++------- .../src/framework/Hydration/Cache.client.ts | 45 ++++++++++++------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index 0cffa9e97f..ee7aabd98f 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -106,7 +106,6 @@ const renderHydrogen: ServerHandler = (App, hook) => { isHydration: true, }); - let flightResponse = ''; if (rscRenderToPipeableStream) { // Node.js branch @@ -114,12 +113,21 @@ const renderHydrogen: ServerHandler = (App, hook) => { ); + let flightResponseBuffer = ''; const {PassThrough} = await import('stream'); const writer = new PassThrough(); writer.setEncoding('utf-8'); writer.on('data', (chunk: string) => { - // TODO: wrap each chunk in `); + } else { + flightResponseBuffer += chunk; + } }); pipe(writer); } else { @@ -142,7 +150,9 @@ const renderHydrogen: ServerHandler = (App, hook) => { let didError: Error | undefined; - const head = template.match(/(.+?)<\/head>/s)![1]; + const head = + (template.match(/(.+?)<\/head>/s)![1] || '') + + ''; // Initialize the Flight container const {pipe, abort} = renderToPipeableStream( @@ -175,18 +185,6 @@ const renderHydrogen: ServerHandler = (App, hook) => { ); }, onCompleteAll() { - if (componentResponse.canStream() && !response.writableEnded) { - // SSR has finished so RSC is also done. Attach the RSC - // response in a script tag at the end. - // TODO: this could be streamed line by line as it comes - // instead of batching it at the end. - response.write( - `` - ); - } - if (componentResponse.canStream() || response.writableEnded) return; writeHeadToServerResponse(response, componentResponse, didError); diff --git a/packages/hydrogen/src/framework/Hydration/Cache.client.ts b/packages/hydrogen/src/framework/Hydration/Cache.client.ts index d8ca8da9ff..034465a0e7 100644 --- a/packages/hydrogen/src/framework/Hydration/Cache.client.ts +++ b/packages/hydrogen/src/framework/Hydration/Cache.client.ts @@ -6,6 +6,33 @@ import { } from '../Hydration/rsc-client-hydrator'; import type {FlightResponse} from '../Hydration/rsc-client-config'; +declare global { + // eslint-disable-next-line no-var + var __flight: Array; +} + +let rscReader: ReadableStream | null; + +if (window.__flight) { + const stream = new TransformStream(); + rscReader = stream.readable; + const writer = stream.writable.getWriter(); + const encoder = new TextEncoder(); + const write = (chunk: string) => { + writer.write(encoder.encode(chunk)); + return 0; + }; + + window.__flight.forEach(write); + window.__flight.push = write; + + document.addEventListener('DOMContentLoaded', function () { + if (!writer.closed) { + writer.close(); + } + }); +} + function createResponseCache() { return new Map(); } @@ -26,22 +53,10 @@ export function useServerResponse(state: any) { return response; } - // @ts-ignore - if (window.__flight) { + if (rscReader) { // The flight response was inlined during SSR, use it directly. - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - // @ts-ignore - controller.enqueue(encoder.encode(window.__flight)); - controller.close(); - }, - }); - - response = createFromReadableStream(stream); - - // @ts-ignore - delete window.__flight; + response = createFromReadableStream(rscReader); + rscReader = null; } else { // Request a new flight response. response = createFromFetch( From 83854c82f27ef3e014fe682f987c97bb3fe470eb Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Fri, 7 Jan 2022 12:47:00 +0100 Subject: [PATCH 06/11] refactor: move code around and cleanup --- packages/hydrogen/src/entry-server.tsx | 70 +++++++------------ .../ServerRequestProvider.tsx | 36 +++++++--- 2 files changed, 50 insertions(+), 56 deletions(-) diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index ee7aabd98f..25abe8a6b4 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -8,7 +8,6 @@ import { import {renderToString} from 'react-dom/server'; import {getErrorMarkup} from './utilities/error'; import ssrPrepass from 'react-ssr-prepass'; -// import {StaticRouter} from 'react-router-dom'; import type {ServerHandler} from './types'; import type {ReactQueryHydrationContext} from './foundation/ShopifyProvider/types'; // import {FilledContext, HelmetProvider} from 'react-helmet-async'; @@ -19,10 +18,7 @@ import {ServerComponentResponse} from './framework/Hydration/ServerComponentResp import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server'; import {dehydrate} from 'react-query/hydration'; import {getCacheControlHeader} from './framework/cache'; -import { - ServerRequestProvider, - requestHydrationCache, -} from './foundation/ServerRequestProvider'; +import {ServerRequestProvider} from './foundation/ServerRequestProvider'; import type {ServerResponse} from 'http'; import { @@ -37,6 +33,19 @@ import { */ const isWorker = Boolean(renderToReadableStream); +const wrapInFlightContainer = ({ + init, + chunk, + nonce, +}: { + init?: boolean; + chunk?: string; + nonce?: string; +}) => + `window.__flight${ + init ? '=[]' : `.push(\`${chunk}\`)` + }`; + /** * If a query is taking too long, or something else went wrong, * send back a response containing the Suspense fallback and rely @@ -97,21 +106,19 @@ const renderHydrogen: ServerHandler = (App, hook) => { const state = {pathname: url.pathname, search: url.search}; // App for RSC rendering - const {ReactApp: HydrationReactApp} = buildReactApp({ + const {ReactApp: ReactAppRSC} = buildReactApp({ App, state, context, request, dev, - isHydration: true, + isRSC: true, }); if (rscRenderToPipeableStream) { // Node.js branch - const {pipe} = rscRenderToPipeableStream( - - ); + const {pipe} = rscRenderToPipeableStream(); let flightResponseBuffer = ''; const {PassThrough} = await import('stream'); @@ -124,7 +131,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { flightResponseBuffer = ''; } - response.write(``); + response.write(wrapInFlightContainer({chunk})); } else { flightResponseBuffer += chunk; } @@ -152,7 +159,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { const head = (template.match(/(.+?)<\/head>/s)![1] || '') + - ''; // Initialize the Flight container + wrapInFlightContainer({init: true}); const {pipe, abort} = renderToPipeableStream( @@ -239,7 +246,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { context, request, dev, - isHydration: true, + isRSC: true, }); response.socket!.on('error', (error: any) => { @@ -268,51 +275,22 @@ function buildReactApp({ context, request, dev, - isHydration, + isRSC = false, }: { App: ComponentType; state: any; context: any; request: ServerComponentRequest; dev: boolean | undefined; - isHydration?: boolean; + isRSC?: boolean; }) { // const helmetContext = {} as FilledContext; const componentResponse = new ServerComponentResponse(); - function Wrapper({children}: any) { - if (isHydration) { - // Save the request object in a React cache that is - // scoped to this current rendering. - - // @ts-ignore - const requestCache = React.unstable_getCacheForType( - requestHydrationCache - ); - - requestCache.set(requestHydrationCache.key, request); - - return children; - } - - // Use a normal provider in SSR to make the request object - // available in the current rendering. - return ( - - {children} - - ); - } - const ReactApp = (props: any) => ( - // - + - - // + ); return {/*helmetContext,*/ ReactApp, componentResponse}; diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx index be395bb7cb..09f0c03813 100644 --- a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx +++ b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx @@ -1,29 +1,29 @@ -import React, {createContext, ReactNode, useContext} from 'react'; +import React, {createContext, useContext} from 'react'; import type {ServerComponentRequest} from '../../framework/Hydration/ServerComponentRequest.server'; // Context to inject current request in SSR -const RequestContext = createContext({ +const RequestContextSSR = createContext({ context: {cache: new Map()}, } as ServerComponentRequest); // Cache to inject current request in RSC -export function requestHydrationCache() { +function requestCacheRSC() { return new Map(); } -requestHydrationCache.key = Symbol.for('request'); +requestCacheRSC.key = Symbol.for('HYDROGEN_REQUEST'); export function useServerRequest() { let request: ServerComponentRequest; try { // Context only works in SSR rendering - request = useContext(RequestContext); + request = useContext(RequestContextSSR); } catch (error) { // If normal context failed it means this is not an SSR request. // Try getting RSC cache instead: // @ts-ignore - const cache = React.unstable_getCacheForType(requestHydrationCache); - request = cache ? cache.get(requestHydrationCache.key) : null; + const cache = React.unstable_getCacheForType(requestCacheRSC); + request = cache ? cache.get(requestCacheRSC.key) : null; } if (!request) { @@ -34,15 +34,31 @@ export function useServerRequest() { } export function ServerRequestProvider({ + isRSC, request, children, }: { + isRSC: boolean; request: ServerComponentRequest; - children: ReactNode; + children: JSX.Element; }) { + if (isRSC) { + // Save the request object in a React cache that is + // scoped to this current rendering. + + // @ts-ignore + const requestCache = React.unstable_getCacheForType(requestCacheRSC); + + requestCache.set(requestCacheRSC.key, request); + + return children; + } + + // Use a normal provider in SSR to make the request object + // available in the current rendering. return ( - + {children} - + ); } From b7043ce33a0fffca44d0ccffaa578566bda7d244 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 12 Jan 2022 18:03:40 +0100 Subject: [PATCH 07/11] fix: flush RSC right after writing head --- packages/hydrogen/package.json | 2 - packages/hydrogen/src/entry-server.tsx | 38 ++++++++++++------- .../src/framework/Hydration/Cache.client.ts | 2 +- yarn.lock | 12 ------ 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/hydrogen/package.json b/packages/hydrogen/package.json index a3209e42ab..8b3cd01f7b 100644 --- a/packages/hydrogen/package.json +++ b/packages/hydrogen/package.json @@ -83,8 +83,6 @@ "vite": "^2.7.1" }, "dependencies": { - "react-client": "link:../../../react/build/node_modules/react-client", - "react-server": "link:../../../react/build/node_modules/react-server", "path-to-regexp": "^6.2.0", "@vitejs/plugin-react": "^1.1.1", "connect": "^3.7.0", diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index 44cef7e51b..a31a5be6e1 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -159,31 +159,37 @@ const renderHydrogen: ServerHandler = (App, hook) => { isRSC: true, }); - if (rscRenderToPipeableStream) { + let flightResponseBuffer = ''; + const flush = (writable: {write: (chunk: string) => void}, chunk = '') => { + if (flightResponseBuffer) { + chunk = flightResponseBuffer + chunk; + flightResponseBuffer = ''; + } + + if (chunk) { + writable.write(wrapInFlightContainer({chunk})); + } + }; + + if (__WORKER__) { + // Worker branch + // TODO implement RSC with TransformStream? + } else { // Node.js branch const {pipe} = rscRenderToPipeableStream(); - let flightResponseBuffer = ''; - const {PassThrough} = await import('stream'); - const writer = new PassThrough(); + const writer = await createNodeWriter(); writer.setEncoding('utf-8'); writer.on('data', (chunk: string) => { if (response.headersSent) { - if (flightResponseBuffer) { - chunk = flightResponseBuffer + chunk; - flightResponseBuffer = ''; - } - - response.write(wrapInFlightContainer({chunk})); + flush(response, chunk); } else { flightResponseBuffer += chunk; } }); + pipe(writer); - } else { - // Worker branch - // TODO implement RSC with TransformStream? } // App for SSR rendering @@ -335,6 +341,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { startWritingHtmlToServerResponse( response, pipe, + flush, dev ? didError : undefined ); }, @@ -362,6 +369,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { startWritingHtmlToServerResponse( response, pipe, + flush, dev ? didError : undefined ); } @@ -577,6 +585,7 @@ export default renderHydrogen; function startWritingHtmlToServerResponse( response: ServerResponse, pipe: (r: ServerResponse) => void, + flush: (w: ServerResponse) => void, error?: Error ) { if (!response.headersSent) { @@ -584,7 +593,8 @@ function startWritingHtmlToServerResponse( response.write(''); } - pipe(response); + pipe(response); // Writes synchronously + flush(response); // Uses the written if (error) { // This error was delayed until the headers were properly sent. diff --git a/packages/hydrogen/src/framework/Hydration/Cache.client.ts b/packages/hydrogen/src/framework/Hydration/Cache.client.ts index 28793ed3c1..efa3f4b09b 100644 --- a/packages/hydrogen/src/framework/Hydration/Cache.client.ts +++ b/packages/hydrogen/src/framework/Hydration/Cache.client.ts @@ -13,7 +13,7 @@ declare global { let rscReader: ReadableStream | null; -if (window.__flight) { +if (window.__flight && window.__flight.length > 0) { const stream = new TransformStream(); rscReader = stream.readable; const writer = stream.writable.getWriter(); diff --git a/yarn.lock b/yarn.lock index e111d94cd9..4963eebe73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11121,12 +11121,6 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -"react-client@file:../../react/build/node_modules/react-client": - version "0.1.0" - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - react-dom@0.0.0-experimental-0cc724c77-20211125: version "0.0.0-experimental-0cc724c77-20211125" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-0cc724c77-20211125.tgz#f48ee44d803b7ba6a888f1cd5ef4402e84f46dba" @@ -11212,12 +11206,6 @@ react-router@5.2.1: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -"react-server@file:../../react/build/node_modules/react-server": - version "0.1.0" - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - react@0.0.0-experimental-0cc724c77-20211125: version "0.0.0-experimental-0cc724c77-20211125" resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-0cc724c77-20211125.tgz#ed650068fbccae10a4cb7b78981517cf7a249f22" From 04f20a11d82b2dd34b2bb958d948afe3c6c4f4b3 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 12 Jan 2022 18:31:28 +0100 Subject: [PATCH 08/11] feat: replace RenderCacheProvider with new ServerRequestProvider cache --- packages/hydrogen/src/entry-server.tsx | 2 - .../RenderCacheContext.tsx | 6 -- .../RenderCacheProvider.tsx | 14 --- .../foundation/RenderCacheProvider/hook.ts | 50 ----------- .../foundation/RenderCacheProvider/hook.tsx | 48 ---------- .../foundation/RenderCacheProvider/index.ts | 1 - .../foundation/RenderCacheProvider/types.ts | 23 ----- .../ServerRequestProvider.tsx | 88 ++++++++++++++----- .../foundation/ServerRequestProvider/types.ts | 13 +++ .../hydrogen/src/foundation/useQuery/hooks.ts | 7 +- 10 files changed, 81 insertions(+), 171 deletions(-) delete mode 100644 packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheContext.tsx delete mode 100644 packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheProvider.tsx delete mode 100644 packages/hydrogen/src/foundation/RenderCacheProvider/hook.ts delete mode 100644 packages/hydrogen/src/foundation/RenderCacheProvider/hook.tsx delete mode 100644 packages/hydrogen/src/foundation/RenderCacheProvider/index.ts delete mode 100644 packages/hydrogen/src/foundation/RenderCacheProvider/types.ts create mode 100644 packages/hydrogen/src/foundation/ServerRequestProvider/types.ts diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index a31a5be6e1..62b1bbc609 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -480,14 +480,12 @@ function buildReactApp({ log: Logger; isRSC?: boolean; }) { - const renderCache = {}; const helmetContext = {} as FilledContext; const componentResponse = new ServerComponentResponse(); const hydrogenServerProps = { request, response: componentResponse, helmetContext: helmetContext, - cache: renderCache, log, }; diff --git a/packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheContext.tsx b/packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheContext.tsx deleted file mode 100644 index af9c3b9173..0000000000 --- a/packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheContext.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import {createContext} from 'react'; -import type {RenderCacheProviderProps} from './types'; - -export const RenderCacheContext = createContext({ - cache: {}, -}); diff --git a/packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheProvider.tsx b/packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheProvider.tsx deleted file mode 100644 index 5789a81caf..0000000000 --- a/packages/hydrogen/src/foundation/RenderCacheProvider/RenderCacheProvider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import {RenderCacheContext} from './RenderCacheContext'; -import type {RenderCacheProviderProps} from './types'; - -export function RenderCacheProvider({ - cache, - children, -}: RenderCacheProviderProps) { - return ( - - {children} - - ); -} diff --git a/packages/hydrogen/src/foundation/RenderCacheProvider/hook.ts b/packages/hydrogen/src/foundation/RenderCacheProvider/hook.ts deleted file mode 100644 index 6081d46ba8..0000000000 --- a/packages/hydrogen/src/foundation/RenderCacheProvider/hook.ts +++ /dev/null @@ -1,50 +0,0 @@ -// import {useContext} from 'react'; -// import {RenderCacheContext} from './RenderCacheContext'; -import {hashKey} from '../../framework/cache'; - -import type {QueryKey} from '../../types'; -import type {RenderCacheProviderProps, RenderCacheResult} from './types'; - -const context = {cache: {}}; - -/** - * Returns the unique identifier for the current rendering request - */ -export function useRenderCache(): RenderCacheProviderProps { - // const context = useContext(RenderCacheContext); - - if (!context) { - throw new Error('No RenderCache Context found'); - } - - return context; -} - -/** - * Returns data stored in the render cache - * It will throw the promise if data is not ready - */ -export function useRenderCacheData( - key: QueryKey, - fetcher: () => Promise -): RenderCacheResult { - const cacheKey = hashKey(key); - const {cache} = useRenderCache(); - - if (!cache[cacheKey]) { - let data: RenderCacheResult; - let promise: Promise>; - - cache[cacheKey] = () => { - if (data !== undefined) return data; - if (!promise) { - promise = fetcher().then( - (r) => (data = {data: r}), - (e) => (data = {error: e}) - ); - } - throw promise; - }; - } - return cache[cacheKey]() as RenderCacheResult; -} diff --git a/packages/hydrogen/src/foundation/RenderCacheProvider/hook.tsx b/packages/hydrogen/src/foundation/RenderCacheProvider/hook.tsx deleted file mode 100644 index 6b50114cfb..0000000000 --- a/packages/hydrogen/src/foundation/RenderCacheProvider/hook.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import {useContext} from 'react'; -import {RenderCacheContext} from './RenderCacheContext'; -import {hashKey} from '../../framework/cache'; - -import type {QueryKey} from '../../types'; -import type {RenderCacheProviderProps, RenderCacheResult} from './types'; - -/** - * Returns the unique identifier for the current rendering request - */ -export function useRenderCache(): RenderCacheProviderProps { - const context = useContext(RenderCacheContext); - - if (!context) { - throw new Error('No RenderCache Context found'); - } - - return context; -} - -/** - * Returns data stored in the render cache - * It will throw the promise if data is not ready - */ -export function useRenderCacheData( - key: QueryKey, - fetcher: () => Promise -): RenderCacheResult { - const cacheKey = hashKey(key); - const {cache} = useRenderCache(); - - if (!cache[cacheKey]) { - let data: RenderCacheResult; - let promise: Promise>; - - cache[cacheKey] = () => { - if (data !== undefined) return data; - if (!promise) { - promise = fetcher().then( - (r) => (data = {data: r}), - (e) => (data = {error: e}) - ); - } - throw promise; - }; - } - return cache[cacheKey]() as RenderCacheResult; -} diff --git a/packages/hydrogen/src/foundation/RenderCacheProvider/index.ts b/packages/hydrogen/src/foundation/RenderCacheProvider/index.ts deleted file mode 100644 index 895566df29..0000000000 --- a/packages/hydrogen/src/foundation/RenderCacheProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {RenderCacheProvider} from './RenderCacheProvider'; diff --git a/packages/hydrogen/src/foundation/RenderCacheProvider/types.ts b/packages/hydrogen/src/foundation/RenderCacheProvider/types.ts deleted file mode 100644 index 98b2846ccb..0000000000 --- a/packages/hydrogen/src/foundation/RenderCacheProvider/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type RenderCache = { - [key: string]: () => unknown; -}; - -export type RenderCacheProviderProps = { - /** A cache to hold all queries performed within a render request */ - cache: RenderCache; - children?: React.ReactNode; -}; - -export type RenderCacheResult = - | RenderCacheResultSuccess - | RenderCacheResultError; - -type RenderCacheResultSuccess = { - data: T; - error?: never; -}; - -type RenderCacheResultError = { - data?: never; - error: Response; -}; diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx index 09f0c03813..76946a8d69 100644 --- a/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx +++ b/packages/hydrogen/src/foundation/ServerRequestProvider/ServerRequestProvider.tsx @@ -1,8 +1,14 @@ import React, {createContext, useContext} from 'react'; +import {hashKey} from '../../framework/cache'; import type {ServerComponentRequest} from '../../framework/Hydration/ServerComponentRequest.server'; +import type {QueryKey} from '../../types'; // Context to inject current request in SSR const RequestContextSSR = createContext({ + // Initial value is required due to a bug: + // https://github.com/Shopify/hydrogen/issues/415 + time: 0, + id: 'initial-value', context: {cache: new Map()}, } as ServerComponentRequest); @@ -13,6 +19,38 @@ function requestCacheRSC() { requestCacheRSC.key = Symbol.for('HYDROGEN_REQUEST'); +type ServerRequestProviderProps = { + isRSC: boolean; + request: ServerComponentRequest; + children: JSX.Element; +}; + +export function ServerRequestProvider({ + isRSC, + request, + children, +}: ServerRequestProviderProps) { + if (isRSC) { + // Save the request object in a React cache that is + // scoped to this current rendering. + + // @ts-ignore + const requestCache = React.unstable_getCacheForType(requestCacheRSC); + + requestCache.set(requestCacheRSC.key, request); + + return children; + } + + // Use a normal provider in SSR to make the request object + // available in the current rendering. + return ( + + {children} + + ); +} + export function useServerRequest() { let request: ServerComponentRequest; try { @@ -33,32 +71,36 @@ export function useServerRequest() { return request; } -export function ServerRequestProvider({ - isRSC, - request, - children, -}: { - isRSC: boolean; - request: ServerComponentRequest; - children: JSX.Element; -}) { - if (isRSC) { - // Save the request object in a React cache that is - // scoped to this current rendering. +type RequestCacheResult = + | {data: T; error?: never} // success + | {data?: never; error: Response}; // failure - // @ts-ignore - const requestCache = React.unstable_getCacheForType(requestCacheRSC); +/** + * Returns data stored in the request cache. + * It will throw the promise if data is not ready. + */ +export function useRequestCacheData( + key: QueryKey, + fetcher: () => Promise +): RequestCacheResult { + const {cache} = useServerRequest().context; + const cacheKey = hashKey(key); - requestCache.set(requestCacheRSC.key, request); + if (!cache.has(cacheKey)) { + let data: RequestCacheResult; + let promise: Promise>; - return children; + cache.set(cacheKey, () => { + if (data !== undefined) return data; + if (!promise) { + promise = fetcher().then( + (r) => (data = {data: r}), + (e) => (data = {error: e}) + ); + } + throw promise; + }); } - // Use a normal provider in SSR to make the request object - // available in the current rendering. - return ( - - {children} - - ); + return cache.get(cacheKey).call() as RequestCacheResult; } diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/types.ts b/packages/hydrogen/src/foundation/ServerRequestProvider/types.ts new file mode 100644 index 0000000000..b4cdb7bbe2 --- /dev/null +++ b/packages/hydrogen/src/foundation/ServerRequestProvider/types.ts @@ -0,0 +1,13 @@ +export type RequestCacheResult = + | RequestCacheResultSuccess + | RequestCacheResultError; + +type RequestCacheResultSuccess = { + data: T; + error?: never; +}; + +type RequestCacheResultError = { + data?: never; + error: Response; +}; diff --git a/packages/hydrogen/src/foundation/useQuery/hooks.ts b/packages/hydrogen/src/foundation/useQuery/hooks.ts index fa2dda77e2..b77efb376f 100644 --- a/packages/hydrogen/src/foundation/useQuery/hooks.ts +++ b/packages/hydrogen/src/foundation/useQuery/hooks.ts @@ -7,9 +7,8 @@ import { setItemInCache, } from '../../framework/cache'; import {runDelayedFunction} from '../../framework/runtime'; -import {useRenderCacheData} from '../RenderCacheProvider/hook'; +import {useRequestCacheData} from '../ServerRequestProvider'; -import type {RenderCacheResult} from '../RenderCacheProvider/types'; export interface HydrogenUseQueryOptions { cache: CacheOptions; } @@ -26,8 +25,8 @@ export function useQuery( queryFn: () => Promise, /** Options including `cache` to manage the cache behavior of the sub-request. */ queryOptions?: HydrogenUseQueryOptions -): RenderCacheResult { - return useRenderCacheData( +) { + return useRequestCacheData( key, cachedQueryFnBuilder(key, queryFn, queryOptions) ); From bd04b70208852f98969c10c11a2496e19092d15c Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 12 Jan 2022 18:56:57 +0100 Subject: [PATCH 09/11] refactor: cleanup --- packages/hydrogen/src/entry-server.tsx | 11 ++++++----- .../src/foundation/ServerRequestProvider/types.ts | 13 ------------- 2 files changed, 6 insertions(+), 18 deletions(-) delete mode 100644 packages/hydrogen/src/foundation/ServerRequestProvider/types.ts diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index 62b1bbc609..9f274f0c37 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -38,7 +38,7 @@ declare global { var __WORKER__: boolean; } -const wrapInFlightContainer = ({ +function flightContainer({ init, chunk, nonce, @@ -46,10 +46,11 @@ const wrapInFlightContainer = ({ init?: boolean; chunk?: string; nonce?: string; -}) => - `window.__flight${ +}) { + return `window.__flight${ init ? '=[]' : `.push(\`${chunk}\`)` }`; +} /** * If a query is taking too long, or something else went wrong, @@ -167,7 +168,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { } if (chunk) { - writable.write(wrapInFlightContainer({chunk})); + writable.write(flightContainer({chunk})); } }; @@ -214,7 +215,7 @@ const renderHydrogen: ServerHandler = (App, hook) => { diff --git a/packages/hydrogen/src/foundation/ServerRequestProvider/types.ts b/packages/hydrogen/src/foundation/ServerRequestProvider/types.ts deleted file mode 100644 index b4cdb7bbe2..0000000000 --- a/packages/hydrogen/src/foundation/ServerRequestProvider/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type RequestCacheResult = - | RequestCacheResultSuccess - | RequestCacheResultError; - -type RequestCacheResultSuccess = { - data: T; - error?: never; -}; - -type RequestCacheResultError = { - data?: never; - error: Response; -}; From 6f1e8e7fa70d37e5d5450481e30ec4babb6663ae Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 12 Jan 2022 19:33:42 +0100 Subject: [PATCH 10/11] fix: suspense breaking hydration --- packages/hydrogen/src/entry-server.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index 9f274f0c37..db689ca686 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -490,11 +490,19 @@ function buildReactApp({ log, }; - const ReactApp = (props: any) => ( - - - - ); + const ReactApp = (props: any) => { + const AppContent = ( + + + + ); + + if (isRSC) return AppContent; + + // Note: The wrapper in SSR is + // required to match hydration in browser + return {AppContent}; + }; return {helmetContext, ReactApp, componentResponse}; } From c56d37401958fd3ef4be3980821b2644256e4a79 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 12 Jan 2022 20:18:51 +0100 Subject: [PATCH 11/11] fix: normalize RSC chunks --- packages/hydrogen/src/entry-server.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx index db689ca686..fd04ab01d0 100644 --- a/packages/hydrogen/src/entry-server.tsx +++ b/packages/hydrogen/src/entry-server.tsx @@ -47,8 +47,10 @@ function flightContainer({ chunk?: string; nonce?: string; }) { + const normalizedChunk = chunk?.replace(/\\/g, String.raw`\\`); + return `window.__flight${ - init ? '=[]' : `.push(\`${chunk}\`)` + init ? '=[]' : `.push(\`${normalizedChunk}\`)` }`; }