diff --git a/packages/dev/src/components/Welcome.server.jsx b/packages/dev/src/components/Welcome.server.jsx
index 92d96a2948..6a85d06b75 100644
--- a/packages/dev/src/components/Welcome.server.jsx
+++ b/packages/dev/src/components/Welcome.server.jsx
@@ -1,5 +1,6 @@
import {useShopQuery, flattenConnection, Link} from '@shopify/hydrogen';
import gql from 'graphql-tag';
+import {Suspense} from 'react';
function ExternalIcon() {
return (
@@ -31,7 +32,20 @@ function DocsButton({url, label}) {
);
}
-function StorefrontInfo({shopName, totalProducts, totalCollections}) {
+function BoxFallback() {
+ return (
+
+ );
+}
+
+function StorefrontInfo() {
+ const {data} = useShopQuery({query: QUERY});
+ const shopName = data ? data.shop.name : '';
+ const products = data && flattenConnection(data.products);
+ const collections = data && flattenConnection(data.collections);
+ const totalProducts = products && products.length;
+ const totalCollections = collections && collections.length;
+
const pluralize = (count, noun, suffix = 's') =>
`${count} ${noun}${count === 1 ? '' : suffix}`;
return (
@@ -71,7 +85,14 @@ function StorefrontInfo({shopName, totalProducts, totalCollections}) {
);
}
-function TemplateLinks({firstProductPath, firstCollectionPath}) {
+function TemplateLinks() {
+ const {data} = useShopQuery({query: QUERY});
+ const products = data && flattenConnection(data.products);
+ const collections = data && flattenConnection(data.collections);
+
+ const firstProduct = products && products.length ? products[0].handle : '';
+ const firstCollection = collections[0] ? collections[0].handle : '';
+
return (
@@ -80,7 +101,7 @@ function TemplateLinks({firstProductPath, firstCollectionPath}) {
-
Collection template
@@ -88,7 +109,7 @@ function TemplateLinks({firstProductPath, firstCollectionPath}) {
-
Product template
@@ -111,16 +132,6 @@ function TemplateLinks({firstProductPath, firstCollectionPath}) {
* A server component that displays the content on the homepage of the Hydrogen app
*/
export default function Welcome() {
- const {data} = useShopQuery({query: QUERY});
- const shopName = data ? data.shop.name : '';
- const products = data && flattenConnection(data.products);
- const collections = data && flattenConnection(data.collections);
-
- const firstProduct = products && products.length ? products[0].handle : '';
- const totalProducts = products && products.length;
- const firstCollection = collections[0] ? collections[0].handle : '';
- const totalCollections = collections && collections.length;
-
return (
@@ -143,15 +154,12 @@ export default function Welcome() {
-
-
+ }>
+
+
+ }>
+
+
);
diff --git a/packages/dev/src/pages/index.server.jsx b/packages/dev/src/pages/index.server.jsx
index e77abb4cd2..1c5945a1ff 100644
--- a/packages/dev/src/pages/index.server.jsx
+++ b/packages/dev/src/pages/index.server.jsx
@@ -11,6 +11,94 @@ import Layout from '../components/Layout.server';
import FeaturedCollection from '../components/FeaturedCollection';
import ProductCard from '../components/ProductCard';
import Welcome from '../components/Welcome.server';
+import {Suspense} from 'react';
+
+export default function Index({country = {isoCode: 'US'}}) {
+ return (
+ }>
+
+
+ }>
+
+
+ }>
+
+
+
+
+ );
+}
+
+function BoxFallback() {
+ return ;
+}
+
+function FeaturedProductsBox({country}) {
+ const {data} = useShopQuery({
+ query: QUERY,
+ variables: {
+ country: country.isoCode,
+ },
+ });
+
+ const collections = data ? flattenConnection(data.collections) : [];
+ const featuredProductsCollection = collections[0];
+ const featuredProducts = featuredProductsCollection
+ ? flattenConnection(featuredProductsCollection.products)
+ : null;
+
+ return (
+
+ {featuredProductsCollection ? (
+ <>
+
+
+ {featuredProductsCollection.title}
+
+
+
+ Shop all
+
+
+
+
+ {featuredProducts.map((product) => (
+
+ ))}
+
+
+
+ Shop all
+
+
+ >
+ ) : null}
+
+ );
+}
+
+function FeaturedCollectionBox({country}) {
+ const {data} = useShopQuery({
+ query: QUERY,
+ variables: {
+ country: country.isoCode,
+ },
+ });
+
+ const collections = data ? flattenConnection(data.collections) : [];
+ const featuredCollection =
+ collections && collections.length > 1 ? collections[1] : collections[0];
+
+ return ;
+}
function GradientBackground() {
return (
@@ -67,66 +155,6 @@ function GradientBackground() {
);
}
-export default function Index({country = {isoCode: 'US'}}) {
- const {data} = useShopQuery({
- query: QUERY,
- variables: {
- country: country.isoCode,
- },
- });
-
- const collections = data ? flattenConnection(data.collections) : [];
- const featuredProductsCollection = collections[0];
- const featuredProducts = featuredProductsCollection
- ? flattenConnection(featuredProductsCollection.products)
- : null;
- const featuredCollection =
- collections && collections.length > 1 ? collections[1] : collections[0];
-
- return (
- }>
-
-
-
- {featuredProductsCollection ? (
- <>
-
-
- {featuredProductsCollection.title}
-
-
-
- Shop all
-
-
-
-
- {featuredProducts.map((product) => (
-
- ))}
-
-
-
- Shop all
-
-
- >
- ) : null}
-
-
-
-
- );
-}
-
const QUERY = gql`
query indexContent(
$country: CountryCode
diff --git a/packages/hydrogen/package.json b/packages/hydrogen/package.json
index 73d1c4b1c0..5e4094b365 100644
--- a/packages/hydrogen/package.json
+++ b/packages/hydrogen/package.json
@@ -63,7 +63,7 @@
"@rollup/plugin-graphql": "^1.0.0",
"@types/connect": "^3.4.34",
"@types/graphql": "^14.5.0",
- "@types/node": "^15.12.4",
+ "@types/node": "^16.11.7",
"@types/node-fetch": "^2.5.9",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
diff --git a/packages/hydrogen/src/entry-client.tsx b/packages/hydrogen/src/entry-client.tsx
index ca8226191e..bcfc513db8 100644
--- a/packages/hydrogen/src/entry-client.tsx
+++ b/packages/hydrogen/src/entry-client.tsx
@@ -4,7 +4,7 @@ import {createRoot} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import type {ClientHandler, ShopifyConfig} from './types';
import {ErrorBoundary} from 'react-error-boundary';
-import {useServerResponse} from './framework/Hydration/Cache.client';
+import {useServerResponse} from './framework/Hydration/rsc';
import {
ServerStateProvider,
ServerStateRouter,
diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx
index 5b876b98c2..d9e1f22add 100644
--- a/packages/hydrogen/src/entry-server.tsx
+++ b/packages/hydrogen/src/entry-server.tsx
@@ -13,24 +13,21 @@ import {
import {getErrorMarkup} from './utilities/error';
import {defer} from './utilities/defer';
import type {ServerHandler} from './types';
-import {FilledContext} from 'react-helmet-async';
+import type {FilledContext} from 'react-helmet-async';
import {Html} from './framework/Hydration/Html';
import {Renderer, Hydrator, Streamer} from './types';
import {ServerComponentResponse} from './framework/Hydration/ServerComponentResponse.server';
import {ServerComponentRequest} from './framework/Hydration/ServerComponentRequest.server';
import {getCacheControlHeader} from './framework/cache';
import {ServerRequestProvider} from './foundation/ServerRequestProvider';
+import {setShopifyConfig} from './foundation/useShop';
import type {ServerResponse} from 'http';
-import type {PassThrough as PassThroughType} from 'stream';
+import type {PassThrough as PassThroughType, Writable} from 'stream';
// @ts-ignore
-import * as rscRenderer from '@shopify/hydrogen/vendor/react-server-dom-vite/writer';
-import {setShopifyConfig} from './foundation/useShop';
-
-const {
- renderToPipeableStream: rscRenderToPipeableStream,
- renderToReadableStream: rscRenderToReadableStream,
-} = rscRenderer;
+import {renderToReadableStream as rscRenderToReadableStream} from '@shopify/hydrogen/vendor/react-server-dom-vite/writer.browser.server';
+// @ts-ignore
+import {createFromReadableStream} from '@shopify/hydrogen/vendor/react-server-dom-vite';
declare global {
// This is provided by a Vite plugin
@@ -39,22 +36,6 @@ declare global {
var __WORKER__: boolean;
}
-function flightContainer({
- init,
- chunk,
- nonce,
-}: {
- init?: boolean;
- chunk?: string;
- nonce?: string;
-}) {
- const normalizedChunk = chunk?.replace(/\\/g, String.raw`\\`);
-
- return ``;
-}
-
/**
* If a query is taking too long, or something else went wrong,
* send back a response containing the Suspense fallback and rely
@@ -70,7 +51,10 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
* and returning any initial state that needs to be hydrated into the client version of the app.
* NOTE: This is currently only used for SEO bots or Worker runtime (where Stream is not yet supported).
*/
- const render: Renderer = async function (url, {request, template, dev}) {
+ const render: Renderer = async function (
+ url,
+ {request, template, nonce, dev}
+ ) {
const log = getLoggerFromContext(request);
const state = {pathname: url.pathname, search: url.search};
@@ -85,7 +69,7 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
,
- {log}
+ {log, nonce}
);
const {headers, status, statusText} = getResponseOptions(componentResponse);
@@ -113,6 +97,7 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
const params = {url, ...extractHeadElements(helmetContext)};
const {bodyAttributes, htmlAttributes, ...head} = params;
+ head.script = (head.script || '') + flightContainer({init: true, nonce});
html = html
.replace(
@@ -137,13 +122,14 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
*/
const stream: Streamer = async function (
url: URL,
- {request, response, template, dev}
+ {request, response, template, nonce, dev}
) {
const log = getLoggerFromContext(request);
const state = {pathname: url.pathname, search: url.search};
+ let didError: Error | undefined;
// App for RSC rendering
- const {ReactApp: ReactAppRSC} = buildReactApp({
+ const {ReactApp: ReactAppRSC, componentResponse} = buildReactApp({
App,
state,
request,
@@ -151,78 +137,46 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
isRSC: true,
});
- let isContainerInitialized = false;
- let flightResponseBuffer = '';
- const flushRSC = (
- writable: {write: (chunk: string) => void},
- chunk = ''
- ) => {
- if (!isContainerInitialized) {
- isContainerInitialized = true;
- writable.write(flightContainer({init: true}));
- }
-
- if (flightResponseBuffer) {
- chunk = flightResponseBuffer + chunk;
- flightResponseBuffer = '';
- }
-
- if (chunk) {
- writable.write(flightContainer({chunk}));
- }
- };
-
- if (__WORKER__) {
- // Worker branch
- // TODO implement RSC with TransformStream?
- } else {
- // Node.js branch
-
- const {pipe} = rscRenderToPipeableStream();
-
- const writer = await createNodeWriter();
- writer.setEncoding('utf-8');
- writer.on('data', (chunk: string) => {
- if (response.headersSent) {
- flushRSC(response, chunk);
- } else {
- flightResponseBuffer += chunk;
- }
- });
-
- pipe(writer);
+ const [rscReadableForFizz, rscReadableForFlight] = (
+ rscRenderToReadableStream() as ReadableStream
+ ).tee();
+
+ const rscResponse = createFromReadableStream(rscReadableForFizz);
+ function RscConsumer() {
+ return (
+
+ {rscResponse.readRoot()}
+
+ );
}
- // App for SSR rendering
- const {ReactApp, componentResponse} = buildReactApp({
- App,
- state,
- request,
- log,
- });
-
- if (!__WORKER__ && response) {
- response.socket!.on('error', (error: any) => {
- log.fatal(error);
- });
- }
-
- let didError: Error | undefined;
-
const ReactAppSSR = (
-
+
);
+ const rscToScriptTagReadable = new ReadableStream({
+ start(controller) {
+ let init = true;
+ const encoder = new TextEncoder();
+ bufferReadableStream(rscReadableForFlight.getReader(), (chunk) => {
+ const scriptTag = flightContainer({init, chunk, nonce});
+ controller.enqueue(encoder.encode(scriptTag));
+ init = false;
+ }).then(() => controller.close());
+ },
+ });
+
if (__WORKER__) {
- const deferred = defer();
+ const deferredShouldReturnApp = defer();
const encoder = new TextEncoder();
const transform = new TransformStream();
const writable = transform.writable.getWriter();
const responseOptions = {} as ResponseOptions;
- const readable: ReadableStream = renderToReadableStream(ReactAppSSR, {
+ const ssrReadable: ReadableStream = renderToReadableStream(ReactAppSSR, {
+ nonce,
onCompleteShell() {
Object.assign(
responseOptions,
@@ -239,7 +193,7 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
if (isRedirect(responseOptions)) {
// Return redirects early without further rendering/streaming
- return deferred.resolve(false);
+ return deferredShouldReturnApp.resolve(false);
}
if (!componentResponse.canStream()) return;
@@ -251,7 +205,7 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
dev ? didError : undefined
);
- deferred.resolve(true);
+ deferredShouldReturnApp.resolve(true);
},
async onCompleteAll() {
if (componentResponse.canStream()) return;
@@ -263,12 +217,12 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
if (isRedirect(responseOptions)) {
// Redirects found after any async code
- return deferred.resolve(false);
+ return deferredShouldReturnApp.resolve(false);
}
if (componentResponse.customBody) {
writable.write(encoder.encode(await componentResponse.customBody));
- return deferred.resolve(false);
+ return deferredShouldReturnApp.resolve(false);
}
startWritingHtmlToStream(
@@ -278,12 +232,12 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
dev ? didError : undefined
);
- deferred.resolve(true);
+ deferredShouldReturnApp.resolve(true);
},
onError(error: any) {
didError = error;
- if (dev && deferred.status === 'pending') {
+ if (dev && deferredShouldReturnApp.status === 'pending') {
writable.write(getErrorMarkup(error));
}
@@ -291,18 +245,59 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
},
});
- const shouldUseStream = await deferred.promise;
+ if (await deferredShouldReturnApp.promise) {
+ let bufferedSsr = '';
+ let isPendingSsrWrite = false;
+ const writingSSR = bufferReadableStream(
+ ssrReadable.getReader(),
+ (chunk) => {
+ bufferedSsr += chunk;
+
+ if (!isPendingSsrWrite) {
+ isPendingSsrWrite = true;
+ setTimeout(() => {
+ isPendingSsrWrite = false;
+ // React can write fractional chunks synchronously.
+ // This timeout ensures we only write full HTML tags
+ // in order to allow RSC writing concurrently.
+ if (bufferedSsr) {
+ writable.write(encoder.encode(bufferedSsr));
+ bufferedSsr = '';
+ }
+ }, 0);
+ }
+ }
+ );
- if (shouldUseStream) {
- writable.releaseLock();
- readable.pipeTo(transform.writable);
+ const writingRSC = bufferReadableStream(
+ rscToScriptTagReadable.getReader(),
+ (scriptTag) => writable.write(encoder.encode(scriptTag))
+ );
+
+ Promise.all([writingSSR, writingRSC]).then(() => {
+ // Last SSR write might be pending, delay closing the writable one tick
+ setTimeout(() => writable.close(), 0);
+ logServerResponse('str', log, request, responseOptions.status);
+ });
+ } else {
+ writable.close();
+ logServerResponse('str', log, request, responseOptions.status);
}
- logServerResponse('str', log, request, responseOptions.status);
+ if (await isStreamingSupported()) {
+ return new Response(transform.readable, responseOptions);
+ }
+
+ const bufferedBody = await bufferReadableStream(
+ transform.readable.getReader()
+ );
+
+ return new Response(bufferedBody, responseOptions);
+ } else if (response) {
+ response.socket!.on('error', log.fatal);
- return new Response(transform.readable, responseOptions);
- } else {
const {pipe} = renderToPipeableStream(ReactAppSSR, {
+ nonce,
onCompleteShell() {
/**
* TODO: This assumes `response.cache()` has been called _before_ any
@@ -327,10 +322,18 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
startWritingHtmlToServerResponse(
response,
- pipe,
- flushRSC,
dev ? didError : undefined
);
+
+ // Piping ends the response so let RSC go first.
+ // Generally, RSC reader should finish before SSR because
+ // the latter is also reading RSC until it finishes.
+ // However, this might not be the case in small apps that
+ // are written in SSR at once.
+ setTimeout(() => pipe(response), 0);
+ bufferReadableStream(rscToScriptTagReadable.getReader(), (chunk) =>
+ response.write(chunk)
+ );
},
async onCompleteAll() {
clearTimeout(streamTimeout);
@@ -352,10 +355,17 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
startWritingHtmlToServerResponse(
response,
- pipe,
- flushRSC,
dev ? didError : undefined
);
+
+ bufferReadableStream(rscToScriptTagReadable.getReader()).then(
+ (scriptTags) => {
+ // Piping ends the response so script tags
+ // must be written before that.
+ response.write(scriptTags);
+ pipe(response);
+ }
+ );
},
onError(error: any) {
didError = error;
@@ -396,18 +406,12 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
isRSC: true,
});
- if (!__WORKER__ && response) {
- response.socket!.on('error', (error: any) => {
- log.fatal(error);
- });
- }
-
if (__WORKER__) {
const readable = rscRenderToReadableStream(
) as ReadableStream;
- if (isStreamable) {
+ if (isStreamable && (await isStreamingSupported())) {
logServerResponse('rsc', log, request, 200);
return new Response(readable);
}
@@ -418,8 +422,17 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
logServerResponse('rsc', log, request, 200);
return new Response(bufferedBody);
- } else {
- const stream = rscRenderToPipeableStream().pipe(response);
+ } else if (response) {
+ response.socket!.on('error', log.fatal);
+
+ const rscWriter = await import(
+ // @ts-ignore
+ '@shopify/hydrogen/vendor/react-server-dom-vite/writer.node.server'
+ );
+
+ const stream = rscWriter
+ .renderToPipeableStream()
+ .pipe(response) as Writable;
stream.on('finish', function () {
logServerResponse('rsc', log, request, response!.statusCode);
@@ -434,7 +447,10 @@ const renderHydrogen: ServerHandler = (App, {shopifyConfig}) => {
};
};
-async function bufferReadableStream(reader: ReadableStreamDefaultReader) {
+async function bufferReadableStream(
+ reader: ReadableStreamDefaultReader,
+ cb?: (chunk: string) => void
+) {
const decoder = new TextDecoder();
let result = '';
@@ -442,7 +458,14 @@ async function bufferReadableStream(reader: ReadableStreamDefaultReader) {
const {done, value} = await reader.read();
if (done) break;
- result += typeof value === 'string' ? value : decoder.decode(value);
+ const stringValue =
+ typeof value === 'string' ? value : decoder.decode(value);
+
+ result += stringValue;
+
+ if (cb) {
+ cb(stringValue);
+ }
}
return result;
@@ -466,7 +489,7 @@ function buildReactApp({
const hydrogenServerProps = {
request,
response: componentResponse,
- helmetContext: helmetContext,
+ helmetContext,
log,
};
@@ -507,7 +530,7 @@ function extractHeadElements(helmetContext: FilledContext) {
async function renderToBufferedString(
ReactApp: JSX.Element,
- {log}: {log: Logger}
+ {log, nonce}: {log: Logger; nonce?: string}
): Promise {
return new Promise(async (resolve, reject) => {
const errorTimeout = setTimeout(() => {
@@ -517,6 +540,7 @@ async function renderToBufferedString(
if (__WORKER__) {
const deferred = defer();
const readable = renderToReadableStream(ReactApp, {
+ nonce,
onCompleteAll() {
clearTimeout(errorTimeout);
/**
@@ -540,6 +564,7 @@ async function renderToBufferedString(
const writer = await createNodeWriter();
const {pipe} = renderToPipeableStream(ReactApp, {
+ nonce,
/**
* When hydrating, we have to wait until `onCompleteAll` to avoid having
* `template` and `script` tags inserted and rendered as part of the hydration response.
@@ -567,8 +592,6 @@ export default renderHydrogen;
function startWritingHtmlToServerResponse(
response: ServerResponse,
- pipe: (r: ServerResponse) => void,
- flushRSC: (w: ServerResponse) => void,
error?: Error
) {
if (!response.headersSent) {
@@ -576,9 +599,6 @@ function startWritingHtmlToServerResponse(
response.write('');
}
- pipe(response);
- flushRSC(response);
-
if (error) {
// This error was delayed until the headers were properly sent.
response.write(getErrorMarkup(error));
@@ -690,3 +710,48 @@ async function createNodeWriter() {
const {PassThrough} = await import(streamImport);
return new PassThrough() as InstanceType;
}
+
+function flightContainer({
+ init,
+ chunk,
+ nonce,
+}: {
+ chunk?: string;
+ init?: boolean;
+ nonce?: string;
+}) {
+ let script = `';
+}
+
+let cachedStreamingSupport: boolean;
+async function isStreamingSupported() {
+ if (cachedStreamingSupport === undefined) {
+ try {
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.close();
+ },
+ });
+
+ // This will throw in CFW until streaming
+ // is supported. It works in Miniflare.
+ await new Response(rs).text();
+
+ cachedStreamingSupport = true;
+ } catch (_) {
+ cachedStreamingSupport = false;
+ }
+ }
+
+ return cachedStreamingSupport;
+}
diff --git a/packages/hydrogen/src/framework/Hydration/Cache.client.ts b/packages/hydrogen/src/framework/Hydration/rsc.ts
similarity index 91%
rename from packages/hydrogen/src/framework/Hydration/Cache.client.ts
rename to packages/hydrogen/src/framework/Hydration/rsc.ts
index dd093e5db3..3385b197fa 100644
--- a/packages/hydrogen/src/framework/Hydration/Cache.client.ts
+++ b/packages/hydrogen/src/framework/Hydration/rsc.ts
@@ -1,3 +1,6 @@
+// TODO should we move this file to src/foundation
+// so it is considered ESM instead of CJS?
+
// @ts-ignore
import {unstable_getCacheForType, unstable_useCacheRefresh} from 'react';
import {
@@ -13,7 +16,7 @@ declare global {
let rscReader: ReadableStream | null;
-if (window.__flight && window.__flight.length > 0) {
+if (__flight && __flight.length > 0) {
const contentLoaded = new Promise((resolve) =>
document.addEventListener('DOMContentLoaded', resolve)
);
@@ -27,8 +30,8 @@ if (window.__flight && window.__flight.length > 0) {
return 0;
};
- window.__flight.forEach(write);
- window.__flight.push = write;
+ __flight.forEach(write);
+ __flight.push = write;
contentLoaded.then(() => controller.close());
},
diff --git a/packages/hydrogen/src/framework/middleware.ts b/packages/hydrogen/src/framework/middleware.ts
index 9189f50a2f..470ef15ab3 100644
--- a/packages/hydrogen/src/framework/middleware.ts
+++ b/packages/hydrogen/src/framework/middleware.ts
@@ -84,6 +84,18 @@ export function hydrogenMiddleware({
globalThis.Headers = fetch.Headers;
}
+ if (!globalThis.ReadableStream) {
+ const {ReadableStream, WritableStream, TransformStream} = await import(
+ 'stream/web'
+ );
+
+ Object.assign(globalThis, {
+ ReadableStream,
+ WritableStream,
+ TransformStream,
+ });
+ }
+
/**
* Dynamically import ServerComponentResponse after the `fetch`
* polyfill has loaded above.
diff --git a/packages/hydrogen/src/framework/plugins/vite-plugin-hydrogen-config.ts b/packages/hydrogen/src/framework/plugins/vite-plugin-hydrogen-config.ts
index 383a65f3ad..4a36286bce 100644
--- a/packages/hydrogen/src/framework/plugins/vite-plugin-hydrogen-config.ts
+++ b/packages/hydrogen/src/framework/plugins/vite-plugin-hydrogen-config.ts
@@ -73,5 +73,36 @@ export default () => {
envPrefix: ['VITE_', 'PUBLIC_'],
}),
+
+ // TODO: Remove when react-dom/fizz is fixed
+ generateBundle: process.env.WORKER
+ ? (options, bundle) => {
+ // There's only one key in bundle, normally `worker.js`
+ const [bundleKey] = Object.keys(bundle);
+ const workerBundle = bundle[bundleKey];
+ // It's always a chunk, this is just for TypeScript
+ if (workerBundle.type === 'chunk') {
+ // React fizz and flight try to access an undefined value.
+ // This puts a guard before accessing it.
+ workerBundle.code = workerBundle.code.replace(
+ /\((\w+)\.locked\)/gm,
+ '($1 && $1.locked)'
+ );
+
+ // `renderToReadableStream` is bugged in React.
+ // This adds a workaround until these issues are fixed:
+ // https://github.com/facebook/react/issues/22772
+ // https://github.com/facebook/react/issues/23113
+ workerBundle.code = workerBundle.code.replace(
+ /var \w+\s*=\s*(\w+)\.completedRootSegment;/g,
+ 'if($1.status===5)return\n$1.status=5;\n$&'
+ );
+ workerBundle.code = workerBundle.code.replace(
+ /(\w+)\.allPendingTasks\s*={2,3}\s*0\s*\&\&\s*\w+\.pingedTasks\.length/g,
+ '$1.status=0;\n$&'
+ );
+ }
+ }
+ : undefined,
} as Plugin;
};
diff --git a/packages/hydrogen/src/handle-event.ts b/packages/hydrogen/src/handle-event.ts
index 4641890691..2c39ae9989 100644
--- a/packages/hydrogen/src/handle-event.ts
+++ b/packages/hydrogen/src/handle-event.ts
@@ -17,9 +17,10 @@ export interface HandleEventOptions {
indexTemplate: string | ((url: string) => Promise);
assetHandler?: (event: HydrogenFetchEvent, url: URL) => Promise;
cache?: Cache;
- streamableResponse: ServerResponse;
+ streamableResponse?: ServerResponse;
dev?: boolean;
context?: RuntimeContext;
+ nonce?: string;
}
export default async function handleEvent(
@@ -33,6 +34,7 @@ export default async function handleEvent(
dev,
cache,
context,
+ nonce,
}: HandleEventOptions
) {
const url = new URL(request.url);
@@ -71,9 +73,9 @@ export default async function handleEvent(
);
}
- // TODO: use __WORKER__ boolean to enable streaming once CFW supports `new Response(stream)`
const isStreamable =
- !!streamableResponse && !isBotUA(url, request.headers.get('user-agent'));
+ !isBotUA(url, request.headers.get('user-agent')) &&
+ (!!streamableResponse || supportsReadableStream());
if (isReactHydrationRequest) {
return hydrate(url, {
@@ -94,6 +96,7 @@ export default async function handleEvent(
request,
response: streamableResponse,
template,
+ nonce,
dev,
});
}
@@ -101,6 +104,7 @@ export default async function handleEvent(
return render(url, {
request,
template,
+ nonce,
dev,
});
}
@@ -163,3 +167,12 @@ const botUserAgents = [
* Creates a regex based on the botUserAgents array
*/
const botUARegex = new RegExp(botUserAgents.join('|'), 'i');
+
+function supportsReadableStream() {
+ try {
+ new ReadableStream();
+ return true;
+ } catch (_) {
+ return false;
+ }
+}
diff --git a/packages/hydrogen/src/types.ts b/packages/hydrogen/src/types.ts
index a360cae650..402b4f6085 100644
--- a/packages/hydrogen/src/types.ts
+++ b/packages/hydrogen/src/types.ts
@@ -7,6 +7,7 @@ export type Renderer = (
options: {
request: ServerComponentRequest;
template: string;
+ nonce?: string;
dev?: boolean;
}
) => Promise;
@@ -15,8 +16,9 @@ export type Streamer = (
url: URL,
options: {
request: ServerComponentRequest;
- response: ServerResponse;
+ response?: ServerResponse;
template: string;
+ nonce?: string;
dev?: boolean;
}
) => void;
diff --git a/packages/hydrogen/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js b/packages/hydrogen/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js
index c6ad3b655e..8ec64afa69 100644
--- a/packages/hydrogen/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js
+++ b/packages/hydrogen/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js
@@ -839,7 +839,10 @@ function performWork(request) {
}
}
+let reentrant = false;
function flushCompletedChunks(request, destination) {
+ if (reentrant) return;
+ reentrant = true;
try {
// We emit module chunks first in the stream so that
// they can be preloaded as early as possible.
@@ -893,6 +896,7 @@ function flushCompletedChunks(request, destination) {
errorChunks.splice(0, i);
} finally {
+ reentrant = false;
}
if (request.pendingChunks === 0) {
diff --git a/yarn.lock b/yarn.lock
index da70679eb3..76e91d78ce 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2880,11 +2880,16 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10"
integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==
-"@types/node@^15.12.4", "@types/node@^15.6.1":
+"@types/node@^15.6.1":
version "15.14.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa"
integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==
+"@types/node@^16.11.7":
+ version "16.11.19"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.19.tgz#1afa165146997b8286b6eabcb1c2d50729055169"
+ integrity sha512-BPAcfDPoHlRQNKktbsbnpACGdypPFBuX4xQlsWDE7B8XXcfII+SpOLay3/qZmCLb39kV5S1RTYwXdkx2lwLYng==
+
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"