@shopify/hydrogen 0.20.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/esnext/client.d.ts +1 -0
  3. package/dist/esnext/client.js +1 -0
  4. package/dist/esnext/components/CartEstimatedCost/CartEstimatedCost.client.d.ts +1 -1
  5. package/dist/esnext/components/CartLinePrice/CartLinePrice.client.d.ts +1 -1
  6. package/dist/esnext/components/CartProvider/CartProvider.client.d.ts +7 -4
  7. package/dist/esnext/components/CartProvider/CartProvider.client.js +22 -12
  8. package/dist/esnext/components/Image/Image.d.ts +3 -3
  9. package/dist/esnext/components/Image/Image.js +12 -9
  10. package/dist/esnext/components/MediaFile/MediaFile.js +3 -2
  11. package/dist/esnext/components/Money/Money.client.d.ts +11 -5
  12. package/dist/esnext/components/Money/Money.client.js +16 -3
  13. package/dist/esnext/components/ProductPrice/ProductPrice.client.d.ts +1 -2
  14. package/dist/esnext/components/ProductPrice/ProductPrice.client.js +1 -2
  15. package/dist/esnext/components/Seo/NoIndexSeo.client.d.ts +3 -0
  16. package/dist/esnext/components/Seo/NoIndexSeo.client.js +10 -0
  17. package/dist/esnext/components/Seo/Seo.client.d.ts +4 -0
  18. package/dist/esnext/components/Seo/Seo.client.js +3 -0
  19. package/dist/esnext/components/index.d.ts +0 -3
  20. package/dist/esnext/components/index.js +0 -3
  21. package/dist/esnext/config.d.ts +3 -3
  22. package/dist/esnext/entry-client.js +0 -3
  23. package/dist/esnext/entry-server.d.ts +2 -2
  24. package/dist/esnext/entry-server.js +97 -73
  25. package/dist/esnext/foundation/Analytics/Analytics.client.js +1 -1
  26. package/dist/esnext/foundation/Analytics/ClientAnalytics.js +2 -2
  27. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.js +2 -2
  28. package/dist/esnext/foundation/Analytics/index.d.ts +0 -1
  29. package/dist/esnext/foundation/Analytics/index.js +0 -1
  30. package/dist/esnext/foundation/FileRoutes/FileRoutes.server.js +5 -10
  31. package/dist/esnext/foundation/Router/BrowserRouter.client.d.ts +2 -1
  32. package/dist/esnext/foundation/Router/BrowserRouter.client.js +1 -1
  33. package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +0 -2
  34. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
  35. package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.js +0 -1
  36. package/dist/esnext/foundation/useQuery/hooks.js +8 -9
  37. package/dist/esnext/foundation/useRouteParams/RouteParamsProvider.client.d.ts +2 -1
  38. package/dist/esnext/foundation/useRouteParams/RouteParamsProvider.client.js +1 -1
  39. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +2 -2
  40. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.d.ts +1 -10
  41. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.js +2 -20
  42. package/dist/esnext/framework/Hydration/rsc.js +55 -7
  43. package/dist/esnext/framework/cache/in-memory.js +0 -6
  44. package/dist/esnext/framework/cache-sub-request.d.ts +17 -0
  45. package/dist/esnext/framework/cache-sub-request.js +64 -0
  46. package/dist/esnext/framework/cache.d.ts +6 -6
  47. package/dist/esnext/framework/cache.js +36 -33
  48. package/dist/esnext/framework/middleware.js +1 -15
  49. package/dist/esnext/framework/plugin.js +4 -0
  50. package/dist/esnext/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
  51. package/dist/esnext/framework/plugins/vite-plugin-client-imports.js +25 -0
  52. package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +8 -3
  53. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-client-middleware.d.ts +1 -1
  54. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
  55. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +6 -47
  56. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-rsc.js +6 -11
  57. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-virtual-files.d.ts +7 -0
  58. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-virtual-files.js +99 -0
  59. package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +4 -4
  60. package/dist/esnext/framework/viteception.d.ts +4 -0
  61. package/dist/esnext/framework/viteception.js +15 -0
  62. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +1 -2
  63. package/dist/esnext/hooks/useShopQuery/hooks.js +0 -1
  64. package/dist/esnext/index.d.ts +24 -9
  65. package/dist/esnext/index.js +24 -13
  66. package/dist/esnext/storefront-api-types.d.ts +1 -1
  67. package/dist/esnext/streaming.server.d.ts +3 -1
  68. package/dist/esnext/types.d.ts +13 -5
  69. package/dist/esnext/utilities/apiRoutes.d.ts +4 -5
  70. package/dist/esnext/utilities/apiRoutes.js +2 -8
  71. package/dist/esnext/utilities/fetch.d.ts +1 -2
  72. package/dist/esnext/utilities/fetch.js +1 -3
  73. package/dist/esnext/utilities/graphql-tag.d.ts +1 -0
  74. package/dist/esnext/utilities/graphql-tag.js +6 -0
  75. package/dist/esnext/utilities/graphql-tracker.d.ts +1 -1
  76. package/dist/esnext/utilities/graphql-tracker.js +4 -0
  77. package/dist/esnext/utilities/html-encoding.d.ts +1 -0
  78. package/dist/esnext/utilities/html-encoding.js +8 -0
  79. package/dist/esnext/utilities/index.d.ts +1 -0
  80. package/dist/esnext/utilities/index.js +1 -0
  81. package/dist/esnext/utilities/log/log-cache-api-status.js +5 -1
  82. package/dist/esnext/version.d.ts +1 -1
  83. package/dist/esnext/version.js +1 -1
  84. package/dist/node/components/Image/Image.d.ts +3 -3
  85. package/dist/node/components/Image/Image.js +12 -9
  86. package/dist/node/entry-server.d.ts +2 -2
  87. package/dist/node/entry-server.js +97 -73
  88. package/dist/node/foundation/Analytics/Analytics.client.js +6 -6
  89. package/dist/node/foundation/Analytics/ClientAnalytics.js +2 -2
  90. package/dist/node/foundation/Router/BrowserRouter.client.d.ts +2 -1
  91. package/dist/node/foundation/Router/BrowserRouter.client.js +1 -1
  92. package/dist/node/foundation/ServerPropsProvider/ServerPropsProvider.js +0 -2
  93. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
  94. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +2 -2
  95. package/dist/node/framework/Hydration/ServerComponentResponse.server.d.ts +1 -10
  96. package/dist/node/framework/Hydration/ServerComponentResponse.server.js +2 -20
  97. package/dist/node/framework/Hydration/rsc.js +55 -7
  98. package/dist/node/framework/cache/in-memory.js +0 -6
  99. package/dist/node/framework/cache-sub-request.d.ts +17 -0
  100. package/dist/node/framework/cache-sub-request.js +95 -0
  101. package/dist/node/framework/cache.d.ts +6 -6
  102. package/dist/node/framework/cache.js +38 -35
  103. package/dist/node/framework/middleware.js +1 -15
  104. package/dist/node/framework/plugin.js +4 -0
  105. package/dist/node/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
  106. package/dist/node/framework/plugins/vite-plugin-client-imports.js +28 -0
  107. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +8 -3
  108. package/dist/node/framework/plugins/vite-plugin-hydrogen-client-middleware.d.ts +1 -1
  109. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
  110. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +5 -46
  111. package/dist/node/framework/plugins/vite-plugin-hydrogen-rsc.js +6 -11
  112. package/dist/node/framework/plugins/vite-plugin-hydrogen-virtual-files.d.ts +7 -0
  113. package/dist/node/framework/plugins/vite-plugin-hydrogen-virtual-files.js +105 -0
  114. package/dist/node/framework/plugins/vite-plugin-platform-entry.js +4 -4
  115. package/dist/node/framework/viteception.d.ts +4 -0
  116. package/dist/node/framework/viteception.js +19 -0
  117. package/dist/node/storefront-api-types.d.ts +1 -1
  118. package/dist/node/streaming.server.d.ts +3 -1
  119. package/dist/node/types.d.ts +13 -5
  120. package/dist/node/utilities/apiRoutes.d.ts +4 -5
  121. package/dist/node/utilities/apiRoutes.js +2 -8
  122. package/dist/node/utilities/fetch.d.ts +1 -2
  123. package/dist/node/utilities/fetch.js +1 -3
  124. package/dist/node/utilities/html-encoding.d.ts +1 -0
  125. package/dist/node/utilities/html-encoding.js +12 -0
  126. package/dist/node/utilities/index.d.ts +1 -0
  127. package/dist/node/utilities/index.js +3 -1
  128. package/dist/node/utilities/log/log-cache-api-status.js +5 -1
  129. package/dist/node/version.d.ts +1 -1
  130. package/dist/node/version.js +1 -1
  131. package/package.json +6 -5
  132. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +48 -6
  133. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +48 -6
  134. package/vendor/react-server-dom-vite/package.json +2 -1
  135. package/dist/esnext/components/ProductDescription/ProductDescription.client.d.ts +0 -13
  136. package/dist/esnext/components/ProductDescription/ProductDescription.client.js +0 -16
  137. package/dist/esnext/components/ProductDescription/index.d.ts +0 -1
  138. package/dist/esnext/components/ProductDescription/index.js +0 -1
  139. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.d.ts +0 -21
  140. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +0 -42
  141. package/dist/esnext/components/ProductMetafield/index.d.ts +0 -2
  142. package/dist/esnext/components/ProductMetafield/index.js +0 -1
  143. package/dist/esnext/components/ProductTitle/ProductTitle.client.d.ts +0 -13
  144. package/dist/esnext/components/ProductTitle/ProductTitle.client.js +0 -16
  145. package/dist/esnext/components/ProductTitle/index.d.ts +0 -1
  146. package/dist/esnext/components/ProductTitle/index.js +0 -1
  147. package/dist/esnext/components/UnitPrice/UnitPrice.client.d.ts +0 -15
  148. package/dist/esnext/components/UnitPrice/UnitPrice.client.js +0 -22
  149. package/dist/esnext/components/UnitPrice/index.d.ts +0 -1
  150. package/dist/esnext/components/UnitPrice/index.js +0 -1
  151. package/dist/esnext/utilities/findRoutePrefix.d.ts +0 -1
  152. package/dist/esnext/utilities/findRoutePrefix.js +0 -17
  153. package/dist/node/foundation/Analytics/index.d.ts +0 -2
  154. package/dist/node/foundation/Analytics/index.js +0 -7
  155. package/dist/node/utilities/findRoutePrefix.d.ts +0 -1
  156. package/dist/node/utilities/findRoutePrefix.js +0 -21
@@ -18,20 +18,27 @@ import { Analytics } from './foundation/Analytics/Analytics.server';
18
18
  import { ServerAnalyticsRoute } from './foundation/Analytics/ServerAnalyticsRoute.server';
19
19
  import { getSyncSessionApi } from './foundation/session/session';
20
20
  import { parseJSON } from './utilities/parse';
21
+ import { htmlEncode } from './utilities';
21
22
  const DOCTYPE = '<!DOCTYPE html>';
22
23
  const CONTENT_TYPE = 'Content-Type';
23
24
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
24
- export const renderHydrogen = (App, hydrogenConfig) => {
25
+ export const renderHydrogen = (App) => {
25
26
  const handleRequest = async function (rawRequest, options) {
26
27
  const { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, } = options;
27
28
  const request = new ServerComponentRequest(rawRequest);
28
29
  const url = new URL(request.url);
29
- if (!hydrogenConfig) {
30
- // @ts-ignore
31
- // eslint-disable-next-line node/no-missing-import
32
- const configFile = await import('virtual:hydrogen-config');
33
- hydrogenConfig = configFile.default;
34
- }
30
+ const { default: inlineHydrogenConfig } = await import(
31
+ // @ts-ignore
32
+ // eslint-disable-next-line node/no-missing-import
33
+ 'virtual__hydrogen.config.ts');
34
+ const { default: hydrogenRoutes } = await import(
35
+ // @ts-ignore
36
+ // eslint-disable-next-line node/no-missing-import
37
+ 'virtual__hydrogen-routes.server.jsx');
38
+ const hydrogenConfig = {
39
+ ...inlineHydrogenConfig,
40
+ routes: hydrogenRoutes,
41
+ };
35
42
  request.ctx.hydrogenConfig = hydrogenConfig;
36
43
  request.ctx.buyerIpHeader = buyerIpHeader;
37
44
  const log = getLoggerWithContext(request);
@@ -51,7 +58,7 @@ export const renderHydrogen = (App, hydrogenConfig) => {
51
58
  return ServerAnalyticsRoute(request, hydrogenConfig.serverAnalyticsConnectors);
52
59
  }
53
60
  const isReactHydrationRequest = url.pathname === RSC_PATHNAME;
54
- if (!isReactHydrationRequest && hydrogenConfig.routes) {
61
+ if (!isReactHydrationRequest) {
55
62
  const apiRoute = getApiRoute(url, hydrogenConfig.routes);
56
63
  // The API Route might have a default export, making it also a server component
57
64
  // If it does, only render the API route if the request method is GET
@@ -63,7 +70,10 @@ export const renderHydrogen = (App, hydrogenConfig) => {
63
70
  : apiResponse;
64
71
  }
65
72
  }
66
- const isStreamable = !isBotUA(url, request.headers.get('user-agent')) &&
73
+ const isStreamable = (hydrogenConfig.enableStreaming
74
+ ? hydrogenConfig.enableStreaming(request)
75
+ : true) &&
76
+ !isBotUA(url, request.headers.get('user-agent')) &&
67
77
  (!!streamableResponse || (await isStreamingSupported()));
68
78
  let template = typeof indexTemplate === 'function'
69
79
  ? await indexTemplate(url.toString())
@@ -95,7 +105,9 @@ export const renderHydrogen = (App, hydrogenConfig) => {
95
105
  }
96
106
  return render(url, params);
97
107
  };
98
- return handleRequest;
108
+ if (__WORKER__)
109
+ return handleRequest;
110
+ return ((rawRequest, options) => handleFetchResponseInNode(handleRequest(rawRequest, options), options.streamableResponse));
99
111
  };
100
112
  function getApiRoute(url, routes) {
101
113
  const apiRoutes = getApiRoutes(routes);
@@ -108,7 +120,7 @@ function getApiRoute(url, routes) {
108
120
  */
109
121
  async function render(url, { App, request, template, componentResponse, nonce, log }) {
110
122
  const state = { pathname: url.pathname, search: url.search };
111
- const { AppSSR } = buildAppSSR({
123
+ const { AppSSR, rscReadable } = buildAppSSR({
112
124
  App,
113
125
  log,
114
126
  state,
@@ -120,24 +132,21 @@ async function render(url, { App, request, template, componentResponse, nonce, l
120
132
  componentResponse.writeHead({ status: 500 });
121
133
  return template;
122
134
  }
123
- let html = await renderToBufferedString(AppSSR, { log, nonce }).catch(onErrorShell);
135
+ let [html, flight] = await Promise.all([
136
+ renderToBufferedString(AppSSR, { log, nonce }).catch(onErrorShell),
137
+ bufferReadableStream(rscReadable.getReader()).catch(() => null),
138
+ ]);
124
139
  const { headers, status, statusText } = getResponseOptions(componentResponse);
125
140
  /**
126
141
  * TODO: Also add `Vary` headers for `accept-language` and any other keys
127
142
  * we want to shard our full-page cache for all Hydrogen storefronts.
128
143
  */
129
144
  headers.set('cache-control', componentResponse.cacheControlHeader);
130
- if (componentResponse.customBody) {
131
- // This can be used to return sitemap.xml or any other custom response.
132
- postRequestTasks('ssr', status, request, componentResponse);
133
- return new Response(await componentResponse.customBody, {
134
- status,
135
- statusText,
136
- headers,
137
- });
138
- }
139
145
  headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
140
146
  html = applyHtmlHead(html, request.ctx.head, template);
147
+ if (flight) {
148
+ html = html.replace('</body>', () => flightContainer(flight) + '</body>');
149
+ }
141
150
  postRequestTasks('ssr', status, request, componentResponse);
142
151
  return new Response(html, {
143
152
  status,
@@ -154,7 +163,7 @@ async function stream(url, { App, request, response, componentResponse, template
154
163
  const state = { pathname: url.pathname, search: url.search };
155
164
  log.trace('start stream');
156
165
  const { noScriptTemplate, bootstrapScripts, bootstrapModules } = stripScriptsFromTemplate(template);
157
- const { AppSSR, rscReadable } = buildAppSSR({
166
+ const { AppSSR, rscReadable, rscDidError } = buildAppSSR({
158
167
  App,
159
168
  log,
160
169
  state,
@@ -164,13 +173,17 @@ async function stream(url, { App, request, response, componentResponse, template
164
173
  const rscToScriptTagReadable = new ReadableStream({
165
174
  start(controller) {
166
175
  log.trace('rsc start chunks');
167
- bufferReadableStream(rscReadable.getReader()).then(() => {
176
+ const encoder = new TextEncoder();
177
+ bufferReadableStream(rscReadable.getReader(), (chunk) => {
178
+ const metaTag = flightContainer(chunk);
179
+ controller.enqueue(encoder.encode(metaTag));
180
+ }).then(() => {
168
181
  log.trace('rsc finish chunks');
169
182
  return controller.close();
170
183
  });
171
184
  },
172
185
  });
173
- let didError;
186
+ let ssrDidError;
174
187
  if (__WORKER__) {
175
188
  const onCompleteAll = defer();
176
189
  const encoder = new TextEncoder();
@@ -184,7 +197,7 @@ async function stream(url, { App, request, response, componentResponse, template
184
197
  bootstrapScripts,
185
198
  bootstrapModules,
186
199
  onError(error) {
187
- didError = error;
200
+ ssrDidError = error;
188
201
  if (dev && !writable.closed && !!responseOptions.status) {
189
202
  writable.write(getErrorMarkup(error));
190
203
  }
@@ -205,8 +218,8 @@ async function stream(url, { App, request, response, componentResponse, template
205
218
  onCompleteAll.resolve(true);
206
219
  });
207
220
  /* eslint-disable no-inner-declarations */
208
- async function prepareForStreaming(flush) {
209
- Object.assign(responseOptions, getResponseOptions(componentResponse, didError));
221
+ function prepareForStreaming(flush) {
222
+ Object.assign(responseOptions, getResponseOptions(componentResponse, rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError));
210
223
  /**
211
224
  * TODO: This assumes `response.cache()` has been called _before_ any
212
225
  * queries which might be caught behind Suspense. Clarify this or add
@@ -217,21 +230,17 @@ async function stream(url, { App, request, response, componentResponse, template
217
230
  return false;
218
231
  }
219
232
  if (flush) {
220
- if (componentResponse.customBody) {
221
- writable.write(encoder.encode(await componentResponse.customBody));
222
- return false;
223
- }
224
233
  responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
225
234
  writable.write(encoder.encode(DOCTYPE));
226
- if (didError) {
235
+ if (rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError) {
227
236
  // This error was delayed until the headers were properly sent.
228
- writable.write(encoder.encode(getErrorMarkup(didError)));
237
+ writable.write(encoder.encode(getErrorMarkup((rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError))));
229
238
  }
230
239
  return true;
231
240
  }
232
241
  }
233
242
  /* eslint-enable no-inner-declarations */
234
- const shouldReturnApp = (_a = (await prepareForStreaming(componentResponse.canStream()))) !== null && _a !== void 0 ? _a : (await onCompleteAll.promise.then(prepareForStreaming));
243
+ const shouldReturnApp = (_a = prepareForStreaming(componentResponse.canStream())) !== null && _a !== void 0 ? _a : (await onCompleteAll.promise.then(prepareForStreaming));
235
244
  if (shouldReturnApp) {
236
245
  let bufferedSsr = '';
237
246
  let isPendingSsrWrite = false;
@@ -281,14 +290,14 @@ async function stream(url, { App, request, response, componentResponse, template
281
290
  * additional checks downstream?
282
291
  */
283
292
  response.setHeader('cache-control', componentResponse.cacheControlHeader);
284
- writeHeadToServerResponse(response, componentResponse, log, didError);
293
+ writeHeadToServerResponse(response, componentResponse, log, rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError);
285
294
  if (isRedirect(response)) {
286
295
  // Return redirects early without further rendering/streaming
287
296
  return response.end();
288
297
  }
289
298
  if (!componentResponse.canStream())
290
299
  return;
291
- startWritingHtmlToServerResponse(response, dev ? didError : undefined);
300
+ startWritingHtmlToServerResponse(response, dev ? rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError : undefined);
292
301
  setTimeout(() => {
293
302
  log.trace('node pipe response');
294
303
  pipe(response);
@@ -298,22 +307,19 @@ async function stream(url, { App, request, response, componentResponse, template
298
307
  return response.write(chunk);
299
308
  });
300
309
  },
301
- async onAllReady() {
310
+ onAllReady() {
302
311
  log.trace('node complete stream');
303
312
  if (componentResponse.canStream() || response.writableEnded) {
304
313
  postRequestTasks('str', response.statusCode, request, componentResponse);
305
314
  return;
306
315
  }
307
- writeHeadToServerResponse(response, componentResponse, log, didError);
316
+ writeHeadToServerResponse(response, componentResponse, log, rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError);
308
317
  postRequestTasks('str', response.statusCode, request, componentResponse);
309
318
  if (isRedirect(response)) {
310
319
  // Redirects found after any async code
311
320
  return response.end();
312
321
  }
313
- if (componentResponse.customBody) {
314
- return response.end(await componentResponse.customBody);
315
- }
316
- startWritingHtmlToServerResponse(response, dev ? didError : undefined);
322
+ startWritingHtmlToServerResponse(response, dev ? rscDidError !== null && rscDidError !== void 0 ? rscDidError : ssrDidError : undefined);
317
323
  bufferReadableStream(rscToScriptTagReadable.getReader()).then((scriptTags) => {
318
324
  // Piping ends the response so script tags
319
325
  // must be written before that.
@@ -331,7 +337,7 @@ async function stream(url, { App, request, response, componentResponse, template
331
337
  }
332
338
  },
333
339
  onError(error) {
334
- didError = error;
340
+ ssrDidError = error;
335
341
  if (dev && response.headersSent) {
336
342
  // Calling write would flush headers automatically.
337
343
  // Delay this error until headers are properly sent.
@@ -354,34 +360,18 @@ async function hydrate(url, { App, log, request, response, isStreamable, compone
354
360
  request,
355
361
  response: componentResponse,
356
362
  });
357
- if (__WORKER__) {
358
- const rscReadable = rscRenderToReadableStream(AppRSC);
359
- if (isStreamable && (await isStreamingSupported())) {
360
- postRequestTasks('rsc', 200, request, componentResponse);
361
- return new Response(rscReadable);
362
- }
363
- // Note: CFW does not support reader.piteTo nor iterable syntax
364
- const bufferedBody = await bufferReadableStream(rscReadable.getReader());
365
- postRequestTasks('rsc', 200, request, componentResponse);
366
- return new Response(bufferedBody, {
367
- headers: {
368
- 'cache-control': componentResponse.cacheControlHeader,
369
- },
370
- });
371
- }
372
- else if (response) {
373
- const rscWriter = await import(
374
- // @ts-ignore
375
- '@shopify/hydrogen/vendor/react-server-dom-vite/writer.node.server');
376
- const streamer = rscWriter.renderToPipeableStream(AppRSC);
377
- response.writeHead(200, 'ok', {
363
+ const rscReadable = rscRenderToReadableStream(AppRSC, {
364
+ onError(e) {
365
+ log.error(e);
366
+ },
367
+ });
368
+ const bufferedBody = await bufferReadableStream(rscReadable.getReader());
369
+ postRequestTasks('rsc', 200, request, componentResponse);
370
+ return new Response(bufferedBody, {
371
+ headers: {
378
372
  'cache-control': componentResponse.cacheControlHeader,
379
- });
380
- const stream = streamer.pipe(response);
381
- stream.on('finish', function () {
382
- postRequestTasks('rsc', response.statusCode, request, componentResponse);
383
- });
384
- }
373
+ },
374
+ });
385
375
  }
386
376
  function buildAppRSC({ App, log, state, request, response }) {
387
377
  const hydrogenServerProps = { request, response, log };
@@ -405,7 +395,13 @@ function buildAppSSR({ App, state, request, response, log }, htmlOptions) {
405
395
  request,
406
396
  response,
407
397
  });
408
- const [rscReadableForFizz, rscReadableForFlight] = rscRenderToReadableStream(AppRSC).tee();
398
+ let rscDidError;
399
+ const [rscReadableForFizz, rscReadableForFlight] = rscRenderToReadableStream(AppRSC, {
400
+ onError(e) {
401
+ rscDidError = e;
402
+ log.error(e);
403
+ },
404
+ }).tee();
409
405
  const rscResponse = createFromReadableStream(rscReadableForFizz);
410
406
  const RscConsumer = () => rscResponse.readRoot();
411
407
  const AppSSR = (React.createElement(Html, { ...htmlOptions },
@@ -416,7 +412,7 @@ function buildAppSSR({ App, state, request, response, log }, htmlOptions) {
416
412
  React.createElement(RscConsumer, null)),
417
413
  React.createElement(Suspense, { fallback: null },
418
414
  React.createElement(Analytics, null)))))));
419
- return { AppSSR, rscReadable: rscReadableForFlight };
415
+ return { AppSSR, rscReadable: rscReadableForFlight, rscDidError };
420
416
  }
421
417
  function PreloadQueries({ request, children, }) {
422
418
  const preloadQueries = request.getPreloadQueries();
@@ -496,7 +492,7 @@ function writeHeadToServerResponse(response, serverComponentResponse, log, error
496
492
  if (statusText) {
497
493
  response.statusMessage = statusText;
498
494
  }
499
- Object.entries(headers.raw()).forEach(([key, value]) => response.setHeader(key, value));
495
+ setServerHeaders(headers, response);
500
496
  }
501
497
  function isRedirect(response) {
502
498
  var _a, _b;
@@ -512,9 +508,37 @@ async function createNodeWriter() {
512
508
  const { PassThrough } = await import(streamImport);
513
509
  return new PassThrough();
514
510
  }
511
+ function flightContainer(chunk) {
512
+ return `<meta data-flight="${htmlEncode(chunk)}" />`;
513
+ }
515
514
  function postRequestTasks(type, status, request, componentResponse) {
516
515
  logServerResponse(type, request, status);
517
516
  logCacheControlHeaders(type, request, componentResponse);
518
517
  logQueryTimings(type, request);
519
518
  request.savePreloadQueries();
520
519
  }
520
+ /**
521
+ * Ensure Node.js environments handle the fetch Response correctly.
522
+ */
523
+ function handleFetchResponseInNode(fetchResponsePromise, nodeResponse) {
524
+ if (nodeResponse) {
525
+ fetchResponsePromise.then((response) => {
526
+ if (!response)
527
+ return;
528
+ setServerHeaders(response.headers, nodeResponse);
529
+ nodeResponse.statusCode = response.status;
530
+ if (response.body) {
531
+ nodeResponse.write(response.body);
532
+ }
533
+ nodeResponse.end();
534
+ });
535
+ }
536
+ return fetchResponsePromise;
537
+ }
538
+ // From fetch Headers to Node Response
539
+ function setServerHeaders(headers, nodeResponse) {
540
+ // Headers.raw is only implemented in node-fetch, which is used by Hydrogen in dev and prod.
541
+ // It is the only way for now to access `set-cookie` header as an array.
542
+ // https://github.com/Shopify/hydrogen/issues/1228
543
+ Object.entries(headers.raw()).forEach(([key, value]) => nodeResponse.setHeader(key, value));
544
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect } from 'react';
2
- import { ClientAnalytics } from './index';
2
+ import { ClientAnalytics } from './ClientAnalytics';
3
3
  export function Analytics({ analyticsDataFromServer, }) {
4
4
  useEffect(() => {
5
5
  const urlParams = new URLSearchParams(window.location.search);
@@ -1,13 +1,13 @@
1
1
  import { getNamedspacedEventname } from './utils';
2
- import { isServer } from '../../utilities';
3
2
  import { eventNames } from './const';
4
3
  import { EVENT_PATHNAME } from '../../constants';
4
+ import { META_ENV_SSR } from '../ssr-interop';
5
5
  const subscribers = {};
6
6
  let pageAnalyticsData = {};
7
7
  const guardDupEvents = {};
8
8
  const USAGE_ERROR = 'ClientAnalytics should only be used within the useEffect callback or event handlers';
9
9
  function isInvokedFromServer() {
10
- if (isServer()) {
10
+ if (META_ENV_SSR) {
11
11
  console.warn(USAGE_ERROR);
12
12
  return true;
13
13
  }
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from 'react';
2
- import { loadScript } from '../../../../utilities';
3
- import { ClientAnalytics } from '../../index';
2
+ import { loadScript } from '../../../../utilities/load_script';
3
+ import { ClientAnalytics } from '../../ClientAnalytics';
4
4
  import { useShop } from '../../../useShop';
5
5
  const URL = 'https://cdn.shopify.com/shopifycloud/boomerang/shopify-boomerang-hydrogen.min.js';
6
6
  export function PerformanceMetrics() {
@@ -1,2 +1 @@
1
- export { useServerAnalytics } from './hook';
2
1
  export { ClientAnalytics } from './ClientAnalytics';
@@ -1,2 +1 @@
1
- export { useServerAnalytics } from './hook';
2
1
  export { ClientAnalytics } from './ClientAnalytics';
@@ -4,28 +4,24 @@ import { log } from '../../utilities/log';
4
4
  import { extractPathFromRoutesKey } from '../../utilities/apiRoutes';
5
5
  import { useServerRequest } from '../ServerRequestProvider';
6
6
  import { RouteParamsProvider } from '../useRouteParams/RouteParamsProvider.client';
7
- import { findRoutePrefix } from '../../utilities/findRoutePrefix';
8
7
  /**
9
8
  * The `FileRoutes` component builds a set of default Hydrogen routes based on the output provided by Vite's
10
9
  * [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. You can have multiple
11
10
  * instances of this component to source file routes from multiple locations.
12
11
  */
13
12
  export function FileRoutes({ routes, basePath, dirPrefix }) {
14
- var _a;
15
13
  const request = useServerRequest();
16
14
  const { routeRendered, serverProps } = request.ctx.router;
17
15
  if (routeRendered)
18
16
  return null;
19
17
  if (!routes) {
20
18
  const fileRoutes = request.ctx.hydrogenConfig.routes;
21
- routes = (_a = fileRoutes === null || fileRoutes === void 0 ? void 0 : fileRoutes.files) !== null && _a !== void 0 ? _a : fileRoutes;
22
- dirPrefix !== null && dirPrefix !== void 0 ? dirPrefix : (dirPrefix = fileRoutes === null || fileRoutes === void 0 ? void 0 : fileRoutes.dirPrefix);
23
- basePath !== null && basePath !== void 0 ? basePath : (basePath = fileRoutes === null || fileRoutes === void 0 ? void 0 : fileRoutes.basePath);
19
+ routes = fileRoutes.files;
20
+ dirPrefix !== null && dirPrefix !== void 0 ? dirPrefix : (dirPrefix = fileRoutes.dirPrefix);
21
+ basePath !== null && basePath !== void 0 ? basePath : (basePath = fileRoutes.basePath);
24
22
  }
25
23
  basePath !== null && basePath !== void 0 ? basePath : (basePath = '/');
26
- /* eslint-disable react-hooks/rules-of-hooks */
27
24
  const pageRoutes = useMemo(() => createPageRoutes(routes, basePath, dirPrefix), [routes, basePath, dirPrefix]);
28
- /* eslint-enable react-hooks/rules-of-hooks */
29
25
  let foundRoute, foundRouteDetails;
30
26
  for (let i = 0; i < pageRoutes.length; i++) {
31
27
  foundRouteDetails = matchPath(serverProps.pathname, pageRoutes[i]);
@@ -42,13 +38,12 @@ export function FileRoutes({ routes, basePath, dirPrefix }) {
42
38
  }
43
39
  return null;
44
40
  }
45
- export function createPageRoutes(pages, topLevelPath = '*', dirPrefix) {
41
+ export function createPageRoutes(pages, topLevelPath = '*', dirPrefix = '') {
46
42
  const topLevelPrefix = topLevelPath.replace('*', '').replace(/\/$/, '');
47
43
  const keys = Object.keys(pages);
48
- const commonRoutePrefix = dirPrefix !== null && dirPrefix !== void 0 ? dirPrefix : findRoutePrefix(keys);
49
44
  const routes = keys
50
45
  .map((key) => {
51
- const path = extractPathFromRoutesKey(key, commonRoutePrefix);
46
+ const path = extractPathFromRoutesKey(key, dirPrefix);
52
47
  /**
53
48
  * Catch-all routes [...handle].jsx don't need an exact match
54
49
  * https://reactrouter.com/core/api/Route/exact-bool
@@ -1,5 +1,5 @@
1
1
  import { BrowserHistory, Location } from 'history';
2
- import React, { FC } from 'react';
2
+ import React, { FC, ReactNode } from 'react';
3
3
  declare type RouterContextValue = {
4
4
  history: BrowserHistory;
5
5
  location: Location;
@@ -7,6 +7,7 @@ declare type RouterContextValue = {
7
7
  export declare const RouterContext: React.Context<{} | RouterContextValue>;
8
8
  export declare const BrowserRouter: FC<{
9
9
  history?: BrowserHistory;
10
+ children: ReactNode;
10
11
  }>;
11
12
  export declare function useRouter(): RouterContextValue;
12
13
  export declare function useLocation(): Location;
@@ -5,7 +5,7 @@ import { useInternalServerProps } from '../useServerProps/use-server-props';
5
5
  export const RouterContext = createContext({});
6
6
  let isFirstLoad = true;
7
7
  const positions = {};
8
- export const BrowserRouter = ({ history: pHistory, children, }) => {
8
+ export const BrowserRouter = ({ history: pHistory, children }) => {
9
9
  if (META_ENV_SSR)
10
10
  return React.createElement(React.Fragment, null, children);
11
11
  /* eslint-disable react-hooks/rules-of-hooks */
@@ -1,4 +1,3 @@
1
- /* eslint-disable hydrogen/no-state-in-server-components */
2
1
  import React, { createContext, useMemo, useCallback,
3
2
  // @ts-ignore
4
3
  useTransition, useState, } from 'react';
@@ -64,4 +63,3 @@ export function ServerPropsProvider({ initialServerProps, setServerPropsForRsc,
64
63
  ]);
65
64
  return (React.createElement(ServerPropsContext.Provider, { value: value }, children));
66
65
  }
67
- /* eslint-enable hydrogen/no-state-in-server-components */
@@ -9,12 +9,27 @@ function requestCacheRSC() {
9
9
  return new Map();
10
10
  }
11
11
  requestCacheRSC.key = Symbol.for('HYDROGEN_REQUEST');
12
+ // Note: use this only during RSC/Flight rendering. The React dispatcher
13
+ // for SSR/Fizz rendering does not implement getCacheForType.
14
+ function getCacheForType(resource) {
15
+ var _a;
16
+ const dispatcher =
17
+ // @ts-ignore
18
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
19
+ .ReactCurrentDispatcher.current;
20
+ // @ts-ignore
21
+ if (__DEV__ && typeof jest !== 'undefined' && !dispatcher.getCacheForType) {
22
+ // Jest does not have access to the RSC runtime, mock it here:
23
+ // @ts-ignore
24
+ return ((_a = globalThis.__jestRscCache) !== null && _a !== void 0 ? _a : (globalThis.__jestRscCache = resource()));
25
+ }
26
+ return dispatcher.getCacheForType(resource);
27
+ }
12
28
  export function ServerRequestProvider({ isRSC, request, children, }) {
13
29
  if (isRSC) {
14
30
  // Save the request object in a React cache that is
15
31
  // scoped to this current rendering.
16
- // @ts-ignore
17
- const requestCache = React.unstable_getCacheForType(requestCacheRSC);
32
+ const requestCache = getCacheForType(requestCacheRSC);
18
33
  requestCache.set(requestCacheRSC.key, request);
19
34
  return children;
20
35
  }
@@ -27,7 +42,7 @@ export function useServerRequest() {
27
42
  try {
28
43
  // This cache only works during RSC rendering:
29
44
  // @ts-ignore
30
- const cache = React.unstable_getCacheForType(requestCacheRSC);
45
+ const cache = getCacheForType(requestCacheRSC);
31
46
  request = cache ? cache.get(requestCacheRSC.key) : null;
32
47
  }
33
48
  catch (_a) {
@@ -39,7 +39,6 @@ children, }) {
39
39
  }
40
40
  let actualShopifyConfig;
41
41
  if (typeof shopifyConfig === 'function') {
42
- // eslint-disable-next-line react-hooks/rules-of-hooks
43
42
  const result = useRequestCacheData(['hydrogen-shopify-config'], () => shopifyConfig(request));
44
43
  if (result.error) {
45
44
  if (result.error instanceof Error) {
@@ -1,8 +1,8 @@
1
- import { getLoggerWithContext, collectQueryCacheControlHeaders, collectQueryTimings, logCacheApiStatus, } from '../../utilities/log';
2
- import { deleteItemFromCache, generateSubRequestCacheControlHeader, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache';
3
- import { hashKey } from '../../utilities/hash';
1
+ import { getLoggerWithContext, collectQueryCacheControlHeaders, collectQueryTimings, } from '../../utilities/log';
2
+ import { deleteItemFromCache, generateSubRequestCacheControlHeader, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache-sub-request';
4
3
  import { runDelayedFunction } from '../../framework/runtime';
5
4
  import { useRequestCacheData, useServerRequest } from '../ServerRequestProvider';
5
+ import { CacheSeconds } from '../../framework/CachingStrategy';
6
6
  /**
7
7
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
8
8
  * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). You can use this
@@ -48,7 +48,6 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
48
48
  // to prevent losing the current React cycle.
49
49
  const request = useServerRequest();
50
50
  const log = getLoggerWithContext(request);
51
- const hashedKey = hashKey(key);
52
51
  const cacheResponse = await getItemFromCache(key);
53
52
  async function generateNewOutput() {
54
53
  return await queryFn();
@@ -59,15 +58,15 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
59
58
  /**
60
59
  * Important: Do this async
61
60
  */
62
- if (isStale(response, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache)) {
63
- logCacheApiStatus('STALE', hashedKey);
64
- const lockKey = `lock-${key}`;
61
+ if (isStale(key, response)) {
62
+ const lockKey = ['lock', ...(typeof key === 'string' ? [key] : key)];
65
63
  runDelayedFunction(async () => {
66
- logCacheApiStatus('UPDATING', hashedKey);
67
64
  const lockExists = await getItemFromCache(lockKey);
68
65
  if (lockExists)
69
66
  return;
70
- await setItemInCache(lockKey, true);
67
+ await setItemInCache(lockKey, true, CacheSeconds({
68
+ maxAge: 10,
69
+ }));
71
70
  try {
72
71
  const output = await generateNewOutput();
73
72
  if (shouldCacheResponse(output)) {
@@ -1,9 +1,10 @@
1
- import React, { FC } from 'react';
1
+ import React, { FC, ReactNode } from 'react';
2
2
  declare type RouteParamsContextValue = {
3
3
  routeParams: Record<string, string>;
4
4
  };
5
5
  export declare const RouteParamsContext: React.Context<RouteParamsContextValue>;
6
6
  export declare const RouteParamsProvider: FC<{
7
7
  routeParams: Record<string, string>;
8
+ children: ReactNode;
8
9
  }>;
9
10
  export {};
@@ -2,6 +2,6 @@ import React, { createContext } from 'react';
2
2
  export const RouteParamsContext = createContext({
3
3
  routeParams: {},
4
4
  });
5
- export const RouteParamsProvider = ({ children, routeParams, }) => {
5
+ export const RouteParamsProvider = ({ children, routeParams }) => {
6
6
  return (React.createElement(RouteParamsContext.Provider, { value: { routeParams } }, children));
7
7
  };
@@ -1,7 +1,7 @@
1
1
  import type { ShopifyContextValue } from '../../foundation/ShopifyProvider/types';
2
2
  import type { QueryCacheControlHeaders } from '../../utilities/log/log-cache-header';
3
3
  import type { QueryTiming } from '../../utilities/log/log-query-timeline';
4
- import type { HydrogenConfig, PreloadOptions, QueryKey } from '../../types';
4
+ import type { ResolvedHydrogenConfig, PreloadOptions, QueryKey } from '../../types';
5
5
  import { HelmetData as HeadData } from 'react-helmet-async';
6
6
  import { SessionSyncApi } from '../../foundation/session/session';
7
7
  export declare type PreloadQueryEntry = {
@@ -31,7 +31,7 @@ export declare class ServerComponentRequest extends Request {
31
31
  ctx: {
32
32
  cache: Map<string, any>;
33
33
  head: HeadData;
34
- hydrogenConfig?: HydrogenConfig;
34
+ hydrogenConfig?: ResolvedHydrogenConfig;
35
35
  shopifyConfig?: ShopifyContextValue;
36
36
  queryCacheControl: Array<QueryCacheControlHeaders>;
37
37
  queryTimings: Array<QueryTiming>;
@@ -2,15 +2,11 @@ import type { CachingStrategy } from '../../types';
2
2
  import React from 'react';
3
3
  export declare class ServerComponentResponse extends Response {
4
4
  private wait;
5
- private cacheOptions?;
5
+ private cacheOptions;
6
6
  customStatus?: {
7
7
  code?: number;
8
8
  text?: string;
9
9
  };
10
- /**
11
- * Allow custom body to be a string or a Promise.
12
- */
13
- customBody: string | Promise<string>;
14
10
  /**
15
11
  * Buffer the current response until all queries have resolved,
16
12
  * and prevent it from streaming back early.
@@ -27,9 +23,4 @@ export declare class ServerComponentResponse extends Response {
27
23
  redirect(location: string, status?: number): React.FunctionComponentElement<{
28
24
  to: string;
29
25
  }>;
30
- /**
31
- * Send the response from a Server Component. Renders React components to string,
32
- * and returns `null` to make React happy.
33
- */
34
- send(body: any): null;
35
26
  }