diff --git a/packages/hydrogen/src/entry-server.tsx b/packages/hydrogen/src/entry-server.tsx
index 157990b25f..adfdf69301 100644
--- a/packages/hydrogen/src/entry-server.tsx
+++ b/packages/hydrogen/src/entry-server.tsx
@@ -7,12 +7,10 @@ import {
getLoggerWithContext,
} from './utilities/log';
import {getErrorMarkup} from './utilities/error';
-import {defer} from './utilities/defer';
import type {
- RendererOptions,
- StreamerOptions,
- HydratorOptions,
- ImportGlobEagerOutput,
+ AssembleHtmlParams,
+ RunSsrParams,
+ RunRscParams,
ResolvedHydrogenConfig,
ResolvedHydrogenRoutes,
} from './types';
@@ -83,13 +81,13 @@ export interface RequestHandler {
export const renderHydrogen = (App: any) => {
const handleRequest: RequestHandler = async function (rawRequest, options) {
const {
- indexTemplate,
- streamableResponse,
dev,
+ nonce,
cache,
context,
- nonce,
buyerIpHeader,
+ indexTemplate,
+ streamableResponse: nodeResponse,
} = options;
const request = new ServerComponentRequest(rawRequest);
@@ -115,18 +113,13 @@ export const renderHydrogen = (App: any) => {
request.ctx.hydrogenConfig = hydrogenConfig;
request.ctx.buyerIpHeader = buyerIpHeader;
+ const response = new ServerComponentResponse();
const log = getLoggerWithContext(request);
const sessionApi = hydrogenConfig.session
? hydrogenConfig.session(log)
: undefined;
- const componentResponse = new ServerComponentResponse();
- request.ctx.session = getSyncSessionApi(
- request,
- componentResponse,
- log,
- sessionApi
- );
+ request.ctx.session = getSyncSessionApi(request, response, log, sessionApi);
/**
* Inject the cache & context into the module loader so we can pull it out for subrequests.
@@ -145,28 +138,40 @@ export const renderHydrogen = (App: any) => {
);
}
- const isReactHydrationRequest = url.pathname === RSC_PATHNAME;
-
- if (!isReactHydrationRequest) {
- const apiRoute = getApiRoute(url, hydrogenConfig.routes);
-
- // The API Route might have a default export, making it also a server component
- // If it does, only render the API route if the request method is GET
- if (
- apiRoute &&
- (!apiRoute.hasServerComponent || request.method !== 'GET')
- ) {
- const apiResponse = await renderApiRoute(
- request,
- apiRoute,
- hydrogenConfig.shopify,
- sessionApi
- );
+ const isRSCRequest = url.pathname === RSC_PATHNAME;
+ const apiRoute = !isRSCRequest && getApiRoute(url, hydrogenConfig.routes);
- return apiResponse instanceof Request
- ? handleRequest(apiResponse, options)
- : apiResponse;
- }
+ // The API Route might have a default export, making it also a server component
+ // If it does, only render the API route if the request method is GET
+ if (
+ apiRoute &&
+ (!apiRoute.hasServerComponent || request.method !== 'GET')
+ ) {
+ const apiResponse = await renderApiRoute(
+ request,
+ apiRoute,
+ hydrogenConfig.shopify,
+ sessionApi
+ );
+
+ return apiResponse instanceof Request
+ ? handleRequest(apiResponse, options)
+ : apiResponse;
+ }
+
+ const state: Record = isRSCRequest
+ ? parseJSON(url.searchParams.get('state') || '{}')
+ : {pathname: url.pathname, search: url.search};
+
+ const rsc = runRSC({App, state, log, request, response});
+
+ if (isRSCRequest) {
+ const buffered = await bufferReadableStream(rsc.readable.getReader());
+ postRequestTasks('rsc', 200, request, response);
+
+ return new Response(buffered, {
+ headers: {'cache-control': response.cacheControlHeader},
+ });
}
const isStreamable =
@@ -174,43 +179,21 @@ export const renderHydrogen = (App: any) => {
? hydrogenConfig.enableStreaming(request)
: true) &&
!isBotUA(url, request.headers.get('user-agent')) &&
- (!!streamableResponse || (await isStreamingSupported()));
+ (!!nodeResponse || (await isStreamingSupported()));
- let template =
- typeof indexTemplate === 'function'
- ? await indexTemplate(url.toString())
- : indexTemplate;
+ if (!isStreamable) response.doNotStream();
- if (template && typeof template !== 'string') {
- template = template.default;
- }
-
- const params = {
- App,
+ return runSSR({
log,
dev,
+ rsc,
nonce,
+ state,
request,
- template,
- isStreamable,
- componentResponse,
- response: streamableResponse,
- };
-
- if (isReactHydrationRequest) {
- return hydrate(url, params);
- }
-
- /**
- * Stream back real-user responses, but for bots/etc,
- * use `render` instead. This is because we need to inject
- * things for SEO reasons.
- */
- if (isStreamable) {
- return stream(url, params);
- }
-
- return render(url, params);
+ response,
+ nodeResponse,
+ template: await getTemplate(indexTemplate, url),
+ });
};
if (__WORKER__) return handleRequest;
@@ -222,123 +205,113 @@ export const renderHydrogen = (App: any) => {
)) as RequestHandler;
};
-function getApiRoute(url: URL, routes: ResolvedHydrogenRoutes) {
- const apiRoutes = getApiRoutes(routes);
- return getApiRouteFromURL(url, apiRoutes);
-}
-
-/**
- * The render function is responsible for turning the provided `App` into an HTML string,
- * 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).
- */
-async function render(
- url: URL,
- {App, request, template, componentResponse, nonce, log}: RendererOptions
+async function getTemplate(
+ indexTemplate:
+ | string
+ | ((url: string) => Promise),
+ url: URL
) {
- const state = {pathname: url.pathname, search: url.search};
-
- const {AppSSR, rscReadable} = buildAppSSR(
- {
- App,
- log,
- state,
- request,
- response: componentResponse,
- },
- {template}
- );
+ let template =
+ typeof indexTemplate === 'function'
+ ? await indexTemplate(url.toString())
+ : indexTemplate;
- function onErrorShell(error: Error) {
- log.error(error);
- componentResponse.writeHead({status: 500});
- return template;
+ if (template && typeof template !== 'string') {
+ template = template.default;
}
- let [html, flight] = await Promise.all([
- renderToBufferedString(AppSSR, {log, nonce}).catch(onErrorShell),
- bufferReadableStream(rscReadable.getReader()).catch(() => null),
- ]);
-
- const {headers, status, statusText} = getResponseOptions(componentResponse);
+ return template;
+}
- /**
- * TODO: Also add `Vary` headers for `accept-language` and any other keys
- * we want to shard our full-page cache for all Hydrogen storefronts.
- */
- headers.set('cache-control', componentResponse.cacheControlHeader);
- headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
+function getApiRoute(url: URL, routes: ResolvedHydrogenRoutes) {
+ const apiRoutes = getApiRoutes(routes);
+ return getApiRouteFromURL(url, apiRoutes);
+}
- html = applyHtmlHead(html, request.ctx.head, template);
+function assembleHtml({
+ ssrHtml,
+ rscPayload,
+ request,
+ template,
+}: AssembleHtmlParams) {
+ let html = applyHtmlHead(ssrHtml, request.ctx.head, template);
- if (flight) {
+ if (rscPayload) {
html = html.replace(
'',
- () => flightContainer(flight as string) + '
'
+ // This must be a function to avoid replacing
+ // special patterns like `$1` in `String.replace`.
+ () => flightContainer(rscPayload) + ''
);
}
- postRequestTasks('ssr', status, request, componentResponse);
-
- return new Response(html, {
- status,
- statusText,
- headers,
- });
+ return html;
}
/**
- * Stream a response to the client. NOTE: This omits custom `
`
- * information, so this method should not be used by crawlers.
+ * Run the SSR/Fizz part of the App. If streaming is disabled,
+ * this buffers the output and applies SEO enhancements.
*/
-async function stream(
- url: URL,
- {
- App,
- request,
- response,
- componentResponse,
- template,
- nonce,
- dev,
- log,
- }: StreamerOptions
-) {
- const state = {pathname: url.pathname, search: url.search};
- log.trace('start stream');
+async function runSSR({
+ rsc,
+ state,
+ request,
+ response,
+ nodeResponse,
+ template,
+ nonce,
+ dev,
+ log,
+}: RunSsrParams) {
+ let ssrDidError: Error | undefined;
+ const didError = () => rsc.didError() ?? ssrDidError;
+
+ const [rscReadableForFizz, rscReadableForFlight] = rsc.readable.tee();
+ const rscResponse = createFromReadableStream(rscReadableForFizz);
+ const RscConsumer = () => rscResponse.readRoot();
const {noScriptTemplate, bootstrapScripts, bootstrapModules} =
stripScriptsFromTemplate(template);
- const {AppSSR, rscReadable, rscDidError} = buildAppSSR(
- {
- App,
- log,
- state,
- request,
- response: componentResponse,
- },
- {template: noScriptTemplate}
+ const AppSSR = (
+
+
+ {}}
+ >
+
+
+
+
+
+
+
+
+
+
+
);
- const rscToScriptTagReadable = new ReadableStream({
- start(controller) {
- log.trace('rsc start chunks');
- const encoder = new TextEncoder();
- bufferReadableStream(rscReadable.getReader(), (chunk) => {
- const metaTag = flightContainer(chunk);
- controller.enqueue(encoder.encode(metaTag));
- }).then(() => {
- log.trace('rsc finish chunks');
- return controller.close();
- });
- },
- });
-
- let ssrDidError: Error | undefined;
+ log.trace('start ssr');
+
+ const rscReadable = response.canStream()
+ ? new ReadableStream({
+ start(controller) {
+ log.trace('rsc start chunks');
+ const encoder = new TextEncoder();
+ bufferReadableStream(rscReadableForFlight.getReader(), (chunk) => {
+ const metaTag = flightContainer(chunk);
+ controller.enqueue(encoder.encode(metaTag));
+ }).then(() => {
+ log.trace('rsc finish chunks');
+ return controller.close();
+ });
+ },
+ })
+ : rscReadableForFlight;
if (__WORKER__) {
- const onCompleteAll = defer();
const encoder = new TextEncoder();
const transform = new TransformStream();
const writable = transform.writable.getWriter();
@@ -373,106 +346,94 @@ async function stream(
);
}
- log.trace('worker ready to stream');
-
- ssrReadable.allReady.then(() => {
- log.trace('worker complete stream');
- onCompleteAll.resolve(true);
- });
+ if (response.canStream()) log.trace('worker ready to stream');
+ ssrReadable.allReady.then(() => log.trace('worker complete ssr'));
- /* eslint-disable no-inner-declarations */
- function prepareForStreaming(flush: boolean) {
- Object.assign(
- responseOptions,
- getResponseOptions(componentResponse, rscDidError ?? ssrDidError)
- );
+ const prepareForStreaming = () => {
+ Object.assign(responseOptions, getResponseOptions(response, didError()));
/**
* TODO: This assumes `response.cache()` has been called _before_ any
* queries which might be caught behind Suspense. Clarify this or add
* additional checks downstream?
*/
- responseOptions.headers.set(
- 'cache-control',
- componentResponse.cacheControlHeader
- );
+ /**
+ * TODO: Also add `Vary` headers for `accept-language` and any other keys
+ * we want to shard our full-page cache for all Hydrogen storefronts.
+ */
+ responseOptions.headers.set('cache-control', response.cacheControlHeader);
if (isRedirect(responseOptions)) {
return false;
}
- if (flush) {
- responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
- writable.write(encoder.encode(DOCTYPE));
-
- if (rscDidError ?? ssrDidError) {
- // This error was delayed until the headers were properly sent.
- writable.write(
- encoder.encode(
- getErrorMarkup((rscDidError ?? ssrDidError) as Error)
- )
- );
- }
+ responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
+ writable.write(encoder.encode(DOCTYPE));
- return true;
+ const error = didError();
+ if (error) {
+ // This error was delayed until the headers were properly sent.
+ writable.write(encoder.encode(dev ? getErrorMarkup(error) : template));
}
- }
- /* eslint-enable no-inner-declarations */
- const shouldReturnApp =
- prepareForStreaming(componentResponse.canStream()) ??
- (await onCompleteAll.promise.then(prepareForStreaming));
+ return true;
+ };
+
+ const shouldFlushBody = response.canStream()
+ ? prepareForStreaming()
+ : await ssrReadable.allReady.then(prepareForStreaming);
- if (shouldReturnApp) {
+ if (shouldFlushBody) {
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 = '';
+ response.canStream()
+ ? (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);
}
- }, 0);
- }
- }
+ }
+ : undefined
);
const writingRSC = bufferReadableStream(
- rscToScriptTagReadable.getReader(),
- (scriptTag) => writable.write(encoder.encode(scriptTag))
+ rscReadable.getReader(),
+ response.canStream()
+ ? (scriptTag) => writable.write(encoder.encode(scriptTag))
+ : undefined
);
- Promise.all([writingSSR, writingRSC]).then(() => {
+ Promise.all([writingSSR, writingRSC]).then(([ssrHtml, rscPayload]) => {
+ if (!response.canStream()) {
+ const html = assembleHtml({ssrHtml, rscPayload, request, template});
+ writable.write(encoder.encode(html));
+ }
+
// Last SSR write might be pending, delay closing the writable one tick
setTimeout(() => writable.close(), 0);
- postRequestTasks(
- 'str',
- responseOptions.status,
- request,
- componentResponse
- );
+ postRequestTasks('str', responseOptions.status, request, response);
});
} else {
+ // Redirects do not write body
writable.close();
- postRequestTasks(
- 'str',
- responseOptions.status,
- request,
- componentResponse
- );
+ postRequestTasks('str', responseOptions.status, request, response);
}
- if (await isStreamingSupported()) {
+ if (response.canStream()) {
return new Response(transform.readable, responseOptions);
}
@@ -481,116 +442,101 @@ async function stream(
);
return new Response(bufferedBody, responseOptions);
- } else if (response) {
+ } else if (nodeResponse) {
const {pipe} = ssrRenderToPipeableStream(AppSSR, {
nonce,
bootstrapScripts,
bootstrapModules,
onShellReady() {
log.trace('node ready to stream');
+
/**
* TODO: This assumes `response.cache()` has been called _before_ any
* queries which might be caught behind Suspense. Clarify this or add
* additional checks downstream?
*/
- response.setHeader(
- 'cache-control',
- componentResponse.cacheControlHeader
- );
-
- writeHeadToServerResponse(
- response,
- componentResponse,
- log,
- rscDidError ?? ssrDidError
- );
+ writeHeadToNodeResponse(nodeResponse, response, log, didError());
- if (isRedirect(response)) {
+ if (isRedirect(nodeResponse)) {
// Return redirects early without further rendering/streaming
- return response.end();
+ return nodeResponse.end();
}
- if (!componentResponse.canStream()) return;
+ if (!response.canStream()) return;
- startWritingHtmlToServerResponse(
- response,
- dev ? rscDidError ?? ssrDidError : undefined
- );
+ startWritingToNodeResponse(nodeResponse, dev ? didError() : undefined);
setTimeout(() => {
log.trace('node pipe response');
- pipe(response);
+ pipe(nodeResponse);
}, 0);
- bufferReadableStream(rscToScriptTagReadable.getReader(), (chunk) => {
+ bufferReadableStream(rscReadable.getReader(), (chunk) => {
log.trace('rsc chunk');
- return response.write(chunk);
+ return nodeResponse.write(chunk);
});
},
- onAllReady() {
- log.trace('node complete stream');
-
- if (componentResponse.canStream() || response.writableEnded) {
- postRequestTasks(
- 'str',
- response.statusCode,
- request,
- componentResponse
- );
+ async onAllReady() {
+ log.trace('node complete ssr');
+
+ if (response.canStream() || nodeResponse.writableEnded) {
+ postRequestTasks('str', nodeResponse.statusCode, request, response);
+
return;
}
- writeHeadToServerResponse(
- response,
- componentResponse,
- log,
- rscDidError ?? ssrDidError
- );
+ writeHeadToNodeResponse(nodeResponse, response, log, didError());
- postRequestTasks(
- 'str',
- response.statusCode,
- request,
- componentResponse
- );
-
- if (isRedirect(response)) {
+ if (isRedirect(nodeResponse)) {
// Redirects found after any async code
- return response.end();
+ return nodeResponse.end();
}
- startWritingHtmlToServerResponse(
- response,
- dev ? rscDidError ?? ssrDidError : undefined
+ const bufferedResponse = await createNodeWriter();
+ const bufferedRscPromise = bufferReadableStream(
+ rscReadable.getReader()
);
- bufferReadableStream(rscToScriptTagReadable.getReader()).then(
- (scriptTags) => {
- // Piping ends the response so script tags
- // must be written before that.
- response.write(scriptTags);
- pipe(response);
+ let ssrHtml = '';
+ bufferedResponse.on('data', (chunk) => (ssrHtml += chunk.toString()));
+ bufferedResponse.once('error', (error) => (ssrDidError = error));
+ bufferedResponse.once('end', async () => {
+ const rscPayload = await bufferedRscPromise;
+
+ const error = didError();
+ startWritingToNodeResponse(nodeResponse, dev ? error : undefined);
+
+ let html = template;
+
+ if (!error) {
+ html = assembleHtml({ssrHtml, rscPayload, request, template});
+ postRequestTasks('ssr', nodeResponse.statusCode, request, response);
}
- );
+
+ nodeResponse.write(html);
+ nodeResponse.end();
+ });
+
+ pipe(bufferedResponse);
},
onShellError(error: any) {
log.error(error);
- if (!response.writableEnded) {
- writeHeadToServerResponse(response, componentResponse, log, error);
- startWritingHtmlToServerResponse(response, dev ? error : undefined);
+ if (!nodeResponse.writableEnded) {
+ writeHeadToNodeResponse(nodeResponse, response, log, error);
+ startWritingToNodeResponse(nodeResponse, dev ? error : undefined);
- response.write(template);
- response.end();
+ nodeResponse.write(template);
+ nodeResponse.end();
}
},
onError(error: any) {
ssrDidError = error;
- if (dev && response.headersSent) {
+ if (dev && nodeResponse.headersSent) {
// Calling write would flush headers automatically.
// Delay this error until headers are properly sent.
- response.write(getErrorMarkup(error));
+ nodeResponse.write(getErrorMarkup(error));
}
log.error(error);
@@ -600,68 +546,10 @@ async function stream(
}
/**
- * Stream a hydration response to the client.
+ * Run the RSC/Flight part of the App
*/
-async function hydrate(
- url: URL,
- {
- App,
- log,
- request,
- response,
- isStreamable,
- componentResponse,
- }: HydratorOptions
-) {
- const state = parseJSON(url.searchParams.get('state') || '{}');
-
- const {AppRSC} = buildAppRSC({
- App,
- log,
- state,
- request,
- response: componentResponse,
- });
-
- const rscReadable = rscRenderToReadableStream(AppRSC, {
- onError(e) {
- log.error(e);
- },
- });
-
- const bufferedBody = await bufferReadableStream(rscReadable.getReader());
-
- postRequestTasks('rsc', 200, request, componentResponse);
-
- return new Response(bufferedBody, {
- headers: {
- 'cache-control': componentResponse.cacheControlHeader,
- },
- });
-}
-
-type SharedServerProps = {
- state?: object | null;
- request: ServerComponentRequest;
- response: ServerComponentResponse;
- log: Logger;
-};
-
-type BuildAppOptions = {
- App: React.JSXElementConstructor;
-} & SharedServerProps;
-
-export type AppProps = SharedServerProps & {
- routes?: ImportGlobEagerOutput;
-};
-
-function buildAppRSC({App, log, state, request, response}: BuildAppOptions) {
- const hydrogenServerProps = {request, response, log};
- const serverProps = {
- ...state,
- ...hydrogenServerProps,
- };
-
+function runRSC({App, state, log, request, response}: RunRscParams) {
+ const serverProps = {...state, request, response, log};
request.ctx.router.serverProps = serverProps;
const AppRSC = (
@@ -675,57 +563,15 @@ function buildAppRSC({App, log, state, request, response}: BuildAppOptions) {
);
- return {AppRSC};
-}
-
-function buildAppSSR(
- {App, state, request, response, log}: BuildAppOptions,
- htmlOptions: Omit[0], 'children'> & {}
-) {
- const {AppRSC} = buildAppRSC({
- App,
- log,
- state,
- request,
- response,
+ let rscDidError: Error;
+ const rscReadable = rscRenderToReadableStream(AppRSC, {
+ onError(e) {
+ rscDidError = e;
+ log.error(e);
+ },
});
- let rscDidError;
-
- const [rscReadableForFizz, rscReadableForFlight] = rscRenderToReadableStream(
- AppRSC,
- {
- onError(e) {
- rscDidError = e;
- log.error(e);
- },
- }
- ).tee();
-
- const rscResponse = createFromReadableStream(rscReadableForFizz);
- const RscConsumer = () => rscResponse.readRoot();
-
- const AppSSR = (
-
-
- {}}
- >
-
-
-
-
-
-
-
-
-
-
-
- );
-
- return {AppSSR, rscReadable: rscReadableForFlight, rscDidError};
+ return {readable: rscReadable, didError: () => rscDidError};
}
function PreloadQueries({
@@ -741,64 +587,20 @@ function PreloadQueries({
return <>{children}>;
}
-async function renderToBufferedString(
- ReactApp: JSX.Element,
- {log, nonce}: {log: Logger; nonce?: string}
-): Promise {
- if (__WORKER__) {
- const ssrReadable = await ssrRenderToReadableStream(ReactApp, {
- nonce,
- onError: (error) => log.error(error),
- });
-
- /**
- * We want to wait until `allReady` resolves before fetching the
- * stream body. Otherwise, React 18's streaming JS script/template tags
- * will be included in the output and cause issues when loading
- * the Client Components in the browser.
- */
- await ssrReadable.allReady;
-
- return bufferReadableStream(ssrReadable.getReader());
- } else {
- const writer = await createNodeWriter();
-
- return new Promise((resolve, reject) => {
- const {pipe} = ssrRenderToPipeableStream(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.
- */
- onAllReady() {
- let data = '';
- writer.on('data', (chunk) => (data += chunk.toString()));
- writer.once('error', reject);
- writer.once('end', () => resolve(data));
- // Tell React to start writing to the writer
- pipe(writer);
- },
- onShellError: reject,
- onError: (error) => log.error(error),
- });
- });
- }
-}
-
export default renderHydrogen;
-function startWritingHtmlToServerResponse(
- response: ServerResponse,
+function startWritingToNodeResponse(
+ nodeResponse: ServerResponse,
error?: Error
) {
- if (!response.headersSent) {
- response.setHeader(CONTENT_TYPE, HTML_CONTENT_TYPE);
- response.write(DOCTYPE);
+ if (!nodeResponse.headersSent) {
+ nodeResponse.setHeader(CONTENT_TYPE, HTML_CONTENT_TYPE);
+ nodeResponse.write(DOCTYPE);
}
if (error) {
// This error was delayed until the headers were properly sent.
- response.write(getErrorMarkup(error));
+ nodeResponse.write(getErrorMarkup(error));
}
}
@@ -829,26 +631,33 @@ function getResponseOptions(
return responseInit;
}
-function writeHeadToServerResponse(
- response: ServerResponse,
- serverComponentResponse: ServerComponentResponse,
+function writeHeadToNodeResponse(
+ nodeResponse: ServerResponse,
+ componentResponse: ServerComponentResponse,
log: Logger,
error?: Error
) {
- if (response.headersSent) return;
- log.trace('writeHeadToServerResponse');
+ if (nodeResponse.headersSent) return;
+ log.trace('writeHeadToNodeResponse');
+
+ /**
+ * TODO: Also add `Vary` headers for `accept-language` and any other keys
+ * we want to shard our full-page cache for all Hydrogen storefronts.
+ */
+ nodeResponse.setHeader('cache-control', componentResponse.cacheControlHeader);
const {headers, status, statusText} = getResponseOptions(
- serverComponentResponse,
+ componentResponse,
error
);
- response.statusCode = status;
+
+ nodeResponse.statusCode = status;
if (statusText) {
- response.statusMessage = statusText;
+ nodeResponse.statusMessage = statusText;
}
- setServerHeaders(headers, response);
+ setNodeHeaders(headers, nodeResponse);
}
function isRedirect(response: {status?: number; statusCode?: number}) {
@@ -874,10 +683,10 @@ function postRequestTasks(
type: RenderType,
status: number,
request: ServerComponentRequest,
- componentResponse: ServerComponentResponse
+ response: ServerComponentResponse
) {
logServerResponse(type, request, status);
- logCacheControlHeaders(type, request, componentResponse);
+ logCacheControlHeaders(type, request, response);
logQueryTimings(type, request);
request.savePreloadQueries();
}
@@ -893,7 +702,7 @@ function handleFetchResponseInNode(
fetchResponsePromise.then((response) => {
if (!response) return;
- setServerHeaders(response.headers, nodeResponse);
+ setNodeHeaders(response.headers, nodeResponse);
nodeResponse.statusCode = response.status;
@@ -909,7 +718,7 @@ function handleFetchResponseInNode(
}
// From fetch Headers to Node Response
-function setServerHeaders(headers: Headers, nodeResponse: ServerResponse) {
+function setNodeHeaders(headers: Headers, nodeResponse: ServerResponse) {
// Headers.raw is only implemented in node-fetch, which is used by Hydrogen in dev and prod.
// It is the only way for now to access `set-cookie` header as an array.
// https://github.com/Shopify/hydrogen/issues/1228
diff --git a/packages/hydrogen/src/types.ts b/packages/hydrogen/src/types.ts
index c0286178b8..427d23587a 100644
--- a/packages/hydrogen/src/types.ts
+++ b/packages/hydrogen/src/types.ts
@@ -10,29 +10,33 @@ import type {
} from './storefront-api-types';
import type {SessionStorageAdapter} from './foundation/session/session';
-type CommonOptions = {
- App: any;
+export type AssembleHtmlParams = {
+ ssrHtml: string;
+ rscPayload?: string;
routes?: ImportGlobEagerOutput;
request: ServerComponentRequest;
- componentResponse: ServerComponentResponse;
- log: Logger;
- dev?: boolean;
-};
-
-export type RendererOptions = CommonOptions & {
template: string;
- nonce?: string;
};
-export type StreamerOptions = CommonOptions & {
- response?: ServerResponse;
+export type RunSsrParams = {
+ state: Record;
+ rsc: {readable: ReadableStream; didError: () => Error | undefined};
+ routes?: ImportGlobEagerOutput;
+ request: ServerComponentRequest;
+ response: ServerComponentResponse;
+ log: Logger;
+ dev?: boolean;
template: string;
nonce?: string;
+ nodeResponse?: ServerResponse;
};
-export type HydratorOptions = CommonOptions & {
- response?: ServerResponse;
- isStreamable: boolean;
+export type RunRscParams = {
+ App: any;
+ state: Record;
+ log: Logger;
+ request: ServerComponentRequest;
+ response: ServerComponentResponse;
};
export type ShopifyConfig = {