@shopify/hydrogen 0.17.1 → 0.18.0

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 (150) hide show
  1. package/CHANGELOG.md +136 -1
  2. package/config.js +1 -0
  3. package/dist/esnext/client.d.ts +2 -0
  4. package/dist/esnext/client.js +2 -0
  5. package/dist/esnext/components/AddToCartButton/AddToCartButton.client.js +2 -2
  6. package/dist/esnext/components/CartProvider/CartProvider.client.js +15 -14
  7. package/dist/esnext/components/CartProvider/{hooks.d.ts → hooks.client.d.ts} +0 -0
  8. package/dist/esnext/components/CartProvider/{hooks.js → hooks.client.js} +0 -0
  9. package/dist/esnext/components/CartProvider/index.d.ts +1 -1
  10. package/dist/esnext/components/CartProvider/index.js +1 -1
  11. package/dist/esnext/components/{DevTools.d.ts → DevTools.client.d.ts} +0 -0
  12. package/dist/esnext/components/{DevTools.js → DevTools.client.js} +3 -2
  13. package/dist/esnext/components/Image/Image.js +2 -0
  14. package/dist/esnext/components/Link/Link.client.js +11 -2
  15. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.js +1 -1
  16. package/dist/esnext/components/Metafield/Metafield.client.js +4 -5
  17. package/dist/esnext/components/ModelViewer/ModelViewer.client.d.ts +1 -1
  18. package/dist/esnext/components/ModelViewer/ModelViewer.client.js +2 -2
  19. package/dist/esnext/components/Money/Money.client.d.ts +5 -1
  20. package/dist/esnext/components/Money/Money.client.js +16 -3
  21. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +1 -1
  22. package/dist/esnext/components/ProductProvider/ProductOptionsProvider.client.js +1 -1
  23. package/dist/esnext/components/ProductProvider/ProductProvider.client.d.ts +7 -3
  24. package/dist/esnext/components/ProductProvider/ProductProvider.client.js +1 -1
  25. package/dist/esnext/components/ShopPayButton/ShopPayButton.client.js +6 -2
  26. package/dist/esnext/components/Video/Video.js +3 -1
  27. package/dist/esnext/config.d.ts +3 -0
  28. package/dist/esnext/config.js +1 -0
  29. package/dist/esnext/constants.js +1 -1
  30. package/dist/esnext/entry-client.js +3 -1
  31. package/dist/esnext/entry-server.d.ts +2 -2
  32. package/dist/esnext/entry-server.js +59 -56
  33. package/dist/esnext/foundation/Analytics/ClientAnalytics.d.ts +1 -0
  34. package/dist/esnext/foundation/Analytics/ClientAnalytics.js +7 -1
  35. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.d.ts +7 -0
  36. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.js +64 -0
  37. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server.d.ts +1 -0
  38. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server.js +24 -0
  39. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetricsDebug.client.d.ts +1 -0
  40. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetricsDebug.client.js +23 -0
  41. package/dist/esnext/foundation/Analytics/const.d.ts +1 -0
  42. package/dist/esnext/foundation/Analytics/const.js +1 -0
  43. package/dist/esnext/foundation/Cookie/Cookie.js +2 -1
  44. package/dist/esnext/foundation/FileRoutes/FileRoutes.server.d.ts +4 -4
  45. package/dist/esnext/foundation/FileRoutes/FileRoutes.server.js +18 -21
  46. package/dist/esnext/foundation/FileSessionStorage/FileSessionStorage.js +2 -1
  47. package/dist/esnext/foundation/Redirect/Redirect.client.js +1 -0
  48. package/dist/esnext/foundation/Route/Route.server.js +1 -10
  49. package/dist/esnext/foundation/Router/BrowserRouter.client.js +34 -5
  50. package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +5 -3
  51. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.d.ts +2 -2
  52. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +7 -2
  53. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.js +6 -1
  54. package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.d.ts +8 -1
  55. package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.js +31 -5
  56. package/dist/esnext/foundation/ShopifyProvider/types.d.ts +3 -4
  57. package/dist/esnext/foundation/fetchSync/client/fetchSync.js +2 -1
  58. package/dist/esnext/foundation/fetchSync/server/fetchSync.js +4 -2
  59. package/dist/esnext/foundation/ssr-interop.js +1 -1
  60. package/dist/esnext/foundation/useQuery/hooks.d.ts +1 -1
  61. package/dist/esnext/foundation/useQuery/hooks.js +2 -2
  62. package/dist/esnext/foundation/useShop/use-shop.d.ts +3 -1
  63. package/dist/esnext/foundation/useShop/use-shop.js +3 -1
  64. package/dist/esnext/foundation/useUrl/useUrl.js +7 -4
  65. package/dist/esnext/framework/Hydration/Html.js +1 -1
  66. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +3 -2
  67. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +16 -7
  68. package/dist/esnext/framework/middleware.d.ts +3 -4
  69. package/dist/esnext/framework/middleware.js +4 -4
  70. package/dist/esnext/framework/plugin.d.ts +2 -2
  71. package/dist/esnext/framework/plugin.js +3 -3
  72. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +1 -1
  73. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +2 -2
  74. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +59 -4
  75. package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +2 -2
  76. package/dist/esnext/hooks/useLoadScript/index.d.ts +1 -1
  77. package/dist/esnext/hooks/useLoadScript/index.js +1 -1
  78. package/dist/esnext/hooks/useLoadScript/{useLoadScript.d.ts → useLoadScript.client.d.ts} +0 -0
  79. package/dist/esnext/hooks/useLoadScript/{useLoadScript.js → useLoadScript.client.js} +2 -1
  80. package/dist/esnext/hooks/useMoney/hooks.d.ts +13 -1
  81. package/dist/esnext/hooks/useMoney/hooks.js +25 -1
  82. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.js +0 -2
  83. package/dist/esnext/hooks/useProduct/useProduct.js +1 -1
  84. package/dist/esnext/hooks/useProductOptions/index.d.ts +1 -1
  85. package/dist/esnext/hooks/useProductOptions/index.js +1 -1
  86. package/dist/esnext/hooks/useProductOptions/{useProductOptions.d.ts → useProductOptions.client.d.ts} +0 -0
  87. package/dist/esnext/hooks/useProductOptions/{useProductOptions.js → useProductOptions.client.js} +6 -23
  88. package/dist/esnext/hooks/useShopQuery/hooks.js +15 -4
  89. package/dist/esnext/index.d.ts +1 -0
  90. package/dist/esnext/index.js +1 -0
  91. package/dist/esnext/storefront-api-types.d.ts +60 -6
  92. package/dist/esnext/storefront-api-types.js +6 -2
  93. package/dist/esnext/types.d.ts +11 -4
  94. package/dist/esnext/utilities/apiRoutes.d.ts +4 -3
  95. package/dist/esnext/utilities/apiRoutes.js +51 -33
  96. package/dist/esnext/utilities/bot-ua.js +3 -0
  97. package/dist/esnext/utilities/empty-hydrogen-config.d.ts +2 -0
  98. package/dist/esnext/utilities/empty-hydrogen-config.js +2 -0
  99. package/dist/esnext/utilities/findRoutePrefix.d.ts +1 -0
  100. package/dist/esnext/utilities/findRoutePrefix.js +17 -0
  101. package/dist/esnext/utilities/log/utils.js +1 -1
  102. package/dist/esnext/utilities/parse.d.ts +1 -0
  103. package/dist/esnext/utilities/parse.js +9 -0
  104. package/dist/esnext/utilities/parseMetafieldValue/parseMetafieldValue.js +2 -1
  105. package/dist/esnext/utilities/storefrontApi.js +1 -0
  106. package/dist/esnext/version.d.ts +1 -1
  107. package/dist/esnext/version.js +1 -1
  108. package/dist/node/constants.js +1 -1
  109. package/dist/node/entry-server.d.ts +2 -2
  110. package/dist/node/entry-server.js +59 -56
  111. package/dist/node/foundation/Analytics/ClientAnalytics.d.ts +1 -0
  112. package/dist/node/foundation/Analytics/ClientAnalytics.js +7 -1
  113. package/dist/node/foundation/Analytics/const.d.ts +1 -0
  114. package/dist/node/foundation/Analytics/const.js +1 -0
  115. package/dist/node/foundation/Redirect/Redirect.client.js +1 -0
  116. package/dist/node/foundation/Router/BrowserRouter.client.js +34 -5
  117. package/dist/node/foundation/ServerPropsProvider/ServerPropsProvider.js +5 -3
  118. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.d.ts +2 -2
  119. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +7 -2
  120. package/dist/node/foundation/ShopifyProvider/types.d.ts +3 -4
  121. package/dist/node/foundation/ssr-interop.js +1 -1
  122. package/dist/node/framework/Hydration/Html.js +1 -1
  123. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +3 -2
  124. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +16 -7
  125. package/dist/node/framework/middleware.d.ts +3 -4
  126. package/dist/node/framework/middleware.js +4 -4
  127. package/dist/node/framework/plugin.d.ts +2 -2
  128. package/dist/node/framework/plugin.js +3 -3
  129. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +1 -1
  130. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +2 -2
  131. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +58 -3
  132. package/dist/node/framework/plugins/vite-plugin-platform-entry.js +2 -2
  133. package/dist/node/storefront-api-types.d.ts +60 -6
  134. package/dist/node/storefront-api-types.js +6 -2
  135. package/dist/node/types.d.ts +11 -4
  136. package/dist/node/utilities/apiRoutes.d.ts +4 -3
  137. package/dist/node/utilities/apiRoutes.js +53 -34
  138. package/dist/node/utilities/bot-ua.js +3 -0
  139. package/dist/node/utilities/findRoutePrefix.d.ts +1 -0
  140. package/dist/node/utilities/findRoutePrefix.js +21 -0
  141. package/dist/node/utilities/log/utils.js +1 -1
  142. package/dist/node/utilities/parse.d.ts +1 -0
  143. package/dist/node/utilities/parse.js +13 -0
  144. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +2 -1
  145. package/dist/node/utilities/storefrontApi.js +1 -0
  146. package/dist/node/version.d.ts +1 -1
  147. package/dist/node/version.js +1 -1
  148. package/package.json +9 -7
  149. package/dist/esnext/foundation/Boomerang/Boomerang.client.d.ts +0 -9
  150. package/dist/esnext/foundation/Boomerang/Boomerang.client.js +0 -66
@@ -17,17 +17,27 @@ import { stripScriptsFromTemplate } from './utilities/template';
17
17
  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
+ import { parseJSON } from './utilities/parse';
20
21
  const DOCTYPE = '<!DOCTYPE html>';
21
22
  const CONTENT_TYPE = 'Content-Type';
22
23
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
23
- export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConnectors, session, }) => {
24
+ export const renderHydrogen = (App, hydrogenConfig) => {
24
25
  const handleRequest = async function (rawRequest, options) {
25
26
  const { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, } = options;
26
27
  const request = new ServerComponentRequest(rawRequest);
27
- request.ctx.buyerIpHeader = buyerIpHeader;
28
28
  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
+ }
35
+ request.ctx.hydrogenConfig = hydrogenConfig;
36
+ request.ctx.buyerIpHeader = buyerIpHeader;
29
37
  const log = getLoggerWithContext(request);
30
- const sessionApi = session ? session(log) : undefined;
38
+ const sessionApi = hydrogenConfig.session
39
+ ? hydrogenConfig.session(log)
40
+ : undefined;
31
41
  const componentResponse = new ServerComponentResponse();
32
42
  request.ctx.session = getSyncSessionApi(request, componentResponse, log, sessionApi);
33
43
  /**
@@ -38,16 +48,16 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
38
48
  setConfig({ dev });
39
49
  if (url.pathname === EVENT_PATHNAME ||
40
50
  EVENT_PATHNAME_REGEX.test(url.pathname)) {
41
- return ServerAnalyticsRoute(request, serverAnalyticsConnectors);
51
+ return ServerAnalyticsRoute(request, hydrogenConfig.serverAnalyticsConnectors);
42
52
  }
43
53
  const isReactHydrationRequest = url.pathname === RSC_PATHNAME;
44
- if (!isReactHydrationRequest && routes) {
45
- const apiRoute = getApiRoute(url, { routes });
54
+ if (!isReactHydrationRequest && hydrogenConfig.routes) {
55
+ const apiRoute = getApiRoute(url, hydrogenConfig.routes);
46
56
  // The API Route might have a default export, making it also a server component
47
57
  // If it does, only render the API route if the request method is GET
48
58
  if (apiRoute &&
49
59
  (!apiRoute.hasServerComponent || request.method !== 'GET')) {
50
- const apiResponse = await renderApiRoute(request, apiRoute, shopifyConfig, sessionApi);
60
+ const apiResponse = await renderApiRoute(request, apiRoute, hydrogenConfig.shopify, sessionApi);
51
61
  return apiResponse instanceof Request
52
62
  ? handleRequest(apiResponse, options)
53
63
  : apiResponse;
@@ -65,7 +75,6 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
65
75
  App,
66
76
  log,
67
77
  dev,
68
- routes,
69
78
  nonce,
70
79
  request,
71
80
  template,
@@ -88,7 +97,7 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
88
97
  };
89
98
  return handleRequest;
90
99
  };
91
- function getApiRoute(url, { routes }) {
100
+ function getApiRoute(url, routes) {
92
101
  const apiRoutes = getApiRoutes(routes);
93
102
  return getApiRouteFromURL(url, apiRoutes);
94
103
  }
@@ -97,15 +106,14 @@ function getApiRoute(url, { routes }) {
97
106
  * and returning any initial state that needs to be hydrated into the client version of the app.
98
107
  * NOTE: This is currently only used for SEO bots or Worker runtime (where Stream is not yet supported).
99
108
  */
100
- async function render(url, { App, routes, request, componentResponse, log, template, nonce, dev, }) {
109
+ async function render(url, { App, request, template, componentResponse, nonce, log }) {
101
110
  const state = { pathname: url.pathname, search: url.search };
102
111
  const { AppSSR, rscReadable } = buildAppSSR({
103
112
  App,
113
+ log,
104
114
  state,
105
115
  request,
106
116
  response: componentResponse,
107
- routes,
108
- log,
109
117
  }, { template });
110
118
  function onErrorShell(error) {
111
119
  log.error(error);
@@ -121,7 +129,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
121
129
  * TODO: Also add `Vary` headers for `accept-language` and any other keys
122
130
  * we want to shard our full-page cache for all Hydrogen storefronts.
123
131
  */
124
- headers['cache-control'] = componentResponse.cacheControlHeader;
132
+ headers.set('cache-control', componentResponse.cacheControlHeader);
125
133
  if (componentResponse.customBody) {
126
134
  // This can be used to return sitemap.xml or any other custom response.
127
135
  postRequestTasks('ssr', status, request, componentResponse);
@@ -131,7 +139,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
131
139
  headers,
132
140
  });
133
141
  }
134
- headers[CONTENT_TYPE] = HTML_CONTENT_TYPE;
142
+ headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
135
143
  html = applyHtmlHead(html, request.ctx.head, template);
136
144
  if (flight) {
137
145
  html = html.replace('</body>', () => `${flightContainer({
@@ -151,18 +159,17 @@ async function render(url, { App, routes, request, componentResponse, log, templ
151
159
  * Stream a response to the client. NOTE: This omits custom `<head>`
152
160
  * information, so this method should not be used by crawlers.
153
161
  */
154
- async function stream(url, { App, routes, request, response, componentResponse, log, template, nonce, dev, }) {
162
+ async function stream(url, { App, request, response, componentResponse, template, nonce, dev, log, }) {
155
163
  var _a;
156
164
  const state = { pathname: url.pathname, search: url.search };
157
165
  log.trace('start stream');
158
166
  const { noScriptTemplate, bootstrapScripts, bootstrapModules } = stripScriptsFromTemplate(template);
159
167
  const { AppSSR, rscReadable } = buildAppSSR({
160
168
  App,
169
+ log,
161
170
  state,
162
171
  request,
163
172
  response: componentResponse,
164
- log,
165
- routes,
166
173
  }, { template: noScriptTemplate });
167
174
  const rscToScriptTagReadable = new ReadableStream({
168
175
  start(controller) {
@@ -213,6 +220,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
213
220
  log.trace('worker complete stream');
214
221
  onCompleteAll.resolve(true);
215
222
  });
223
+ /* eslint-disable no-inner-declarations */
216
224
  async function prepareForStreaming(flush) {
217
225
  Object.assign(responseOptions, getResponseOptions(componentResponse, didError));
218
226
  /**
@@ -220,8 +228,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
220
228
  * queries which might be caught behind Suspense. Clarify this or add
221
229
  * additional checks downstream?
222
230
  */
223
- responseOptions.headers['cache-control'] =
224
- componentResponse.cacheControlHeader;
231
+ responseOptions.headers.set('cache-control', componentResponse.cacheControlHeader);
225
232
  if (isRedirect(responseOptions)) {
226
233
  return false;
227
234
  }
@@ -230,7 +237,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
230
237
  writable.write(encoder.encode(await componentResponse.customBody));
231
238
  return false;
232
239
  }
233
- responseOptions.headers[CONTENT_TYPE] = HTML_CONTENT_TYPE;
240
+ responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
234
241
  writable.write(encoder.encode(DOCTYPE));
235
242
  if (didError) {
236
243
  // This error was delayed until the headers were properly sent.
@@ -239,6 +246,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
239
246
  return true;
240
247
  }
241
248
  }
249
+ /* eslint-enable no-inner-declarations */
242
250
  const shouldReturnApp = (_a = (await prepareForStreaming(componentResponse.canStream()))) !== null && _a !== void 0 ? _a : (await onCompleteAll.promise.then(prepareForStreaming));
243
251
  if (shouldReturnApp) {
244
252
  let bufferedSsr = '';
@@ -353,15 +361,14 @@ async function stream(url, { App, routes, request, response, componentResponse,
353
361
  /**
354
362
  * Stream a hydration response to the client.
355
363
  */
356
- async function hydrate(url, { App, routes, request, response, componentResponse, isStreamable, log, }) {
357
- const state = JSON.parse(url.searchParams.get('state') || '{}');
364
+ async function hydrate(url, { App, log, request, response, isStreamable, componentResponse, }) {
365
+ const state = parseJSON(url.searchParams.get('state') || '{}');
358
366
  const { AppRSC } = buildAppRSC({
359
367
  App,
368
+ log,
360
369
  state,
361
370
  request,
362
371
  response: componentResponse,
363
- log,
364
- routes,
365
372
  });
366
373
  if (__WORKER__) {
367
374
  const rscReadable = rscRenderToReadableStream(AppRSC);
@@ -392,9 +399,12 @@ async function hydrate(url, { App, routes, request, response, componentResponse,
392
399
  });
393
400
  }
394
401
  }
395
- function buildAppRSC({ App, state, request, response, log, routes, }) {
402
+ function buildAppRSC({ App, log, state, request, response }) {
396
403
  const hydrogenServerProps = { request, response, log };
397
- const serverProps = { ...state, ...hydrogenServerProps, routes };
404
+ const serverProps = {
405
+ ...state,
406
+ ...hydrogenServerProps,
407
+ };
398
408
  request.ctx.router.serverProps = serverProps;
399
409
  const AppRSC = (React.createElement(ServerRequestProvider, { request: request, isRSC: true },
400
410
  React.createElement(PreloadQueries, { request: request },
@@ -403,14 +413,13 @@ function buildAppRSC({ App, state, request, response, log, routes, }) {
403
413
  React.createElement(Analytics, null)))));
404
414
  return { AppRSC };
405
415
  }
406
- function buildAppSSR({ App, state, request, response, log, routes }, htmlOptions) {
416
+ function buildAppSSR({ App, state, request, response, log }, htmlOptions) {
407
417
  const { AppRSC } = buildAppRSC({
408
418
  App,
419
+ log,
409
420
  state,
410
421
  request,
411
422
  response,
412
- log,
413
- routes,
414
423
  });
415
424
  const [rscReadableForFizz, rscReadableForFlight] = rscRenderToReadableStream(AppRSC).tee();
416
425
  const rscResponse = createFromReadableStream(rscReadableForFizz);
@@ -431,28 +440,23 @@ function PreloadQueries({ request, children, }) {
431
440
  return React.createElement(React.Fragment, null, children);
432
441
  }
433
442
  async function renderToBufferedString(ReactApp, { log, nonce }) {
434
- return new Promise(async (resolve, reject) => {
435
- if (__WORKER__) {
436
- try {
437
- const ssrReadable = await ssrRenderToReadableStream(ReactApp, {
438
- nonce,
439
- onError: (error) => log.error(error),
440
- });
441
- /**
442
- * We want to wait until `allReady` resolves before fetching the
443
- * stream body. Otherwise, React 18's streaming JS script/template tags
444
- * will be included in the output and cause issues when loading
445
- * the Client Components in the browser.
446
- */
447
- await ssrReadable.allReady;
448
- resolve(bufferReadableStream(ssrReadable.getReader()));
449
- }
450
- catch (error) {
451
- reject(error);
452
- }
453
- }
454
- else {
455
- const writer = await createNodeWriter();
443
+ if (__WORKER__) {
444
+ const ssrReadable = await ssrRenderToReadableStream(ReactApp, {
445
+ nonce,
446
+ onError: (error) => log.error(error),
447
+ });
448
+ /**
449
+ * We want to wait until `allReady` resolves before fetching the
450
+ * stream body. Otherwise, React 18's streaming JS script/template tags
451
+ * will be included in the output and cause issues when loading
452
+ * the Client Components in the browser.
453
+ */
454
+ await ssrReadable.allReady;
455
+ return bufferReadableStream(ssrReadable.getReader());
456
+ }
457
+ else {
458
+ const writer = await createNodeWriter();
459
+ return new Promise((resolve, reject) => {
456
460
  const { pipe } = ssrRenderToPipeableStream(ReactApp, {
457
461
  nonce,
458
462
  /**
@@ -470,8 +474,8 @@ async function renderToBufferedString(ReactApp, { log, nonce }) {
470
474
  onShellError: reject,
471
475
  onError: (error) => log.error(error),
472
476
  });
473
- }
474
- });
477
+ });
478
+ }
475
479
  }
476
480
  export default renderHydrogen;
477
481
  function startWritingHtmlToServerResponse(response, error) {
@@ -487,8 +491,7 @@ function startWritingHtmlToServerResponse(response, error) {
487
491
  function getResponseOptions({ headers, status, customStatus }, error) {
488
492
  var _a, _b;
489
493
  const responseInit = {};
490
- // @ts-ignore
491
- responseInit.headers = Object.fromEntries(headers.entries());
494
+ responseInit.headers = headers;
492
495
  if (error) {
493
496
  responseInit.status = 500;
494
497
  }
@@ -509,7 +512,7 @@ function writeHeadToServerResponse(response, serverComponentResponse, log, error
509
512
  if (statusText) {
510
513
  response.statusMessage = statusText;
511
514
  }
512
- Object.entries(headers).forEach(([key, value]) => response.setHeader(key, value));
515
+ Object.entries(headers.raw()).forEach(([key, value]) => response.setHeader(key, value));
513
516
  }
514
517
  function isRedirect(response) {
515
518
  var _a, _b;
@@ -19,6 +19,7 @@ export declare const ClientAnalytics: {
19
19
  REMOVE_FROM_CART: string;
20
20
  UPDATE_CART: string;
21
21
  DISCOUNT_CODE_UPDATED: string;
22
+ PERFORMANCE: string;
22
23
  };
23
24
  };
24
25
  export {};
@@ -78,7 +78,13 @@ function subscribe(eventname, callbackFunction) {
78
78
  };
79
79
  }
80
80
  function pushToServer(init, searchParam) {
81
- return fetch(`${EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`, init);
81
+ return fetch(`${EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`, Object.assign({
82
+ method: 'post',
83
+ headers: {
84
+ 'cache-control': 'no-cache',
85
+ 'Content-Type': 'application/json',
86
+ },
87
+ }, init));
82
88
  }
83
89
  export const ClientAnalytics = {
84
90
  pushToPageAnalyticsData,
@@ -0,0 +1,7 @@
1
+ declare global {
2
+ interface Window {
3
+ BOOMR: any;
4
+ BOOMR_onload: any;
5
+ }
6
+ }
7
+ export declare function PerformanceMetrics(): null;
@@ -0,0 +1,64 @@
1
+ import { useEffect } from 'react';
2
+ import { loadScript } from '../../../../utilities';
3
+ import { ClientAnalytics } from '../../index';
4
+ import { useShop } from '../../../useShop';
5
+ const URL = 'https://cdn.shopify.com/shopifycloud/boomerang/shopify-boomerang-hydrogen.min.js';
6
+ export function PerformanceMetrics() {
7
+ const { storeDomain } = useShop();
8
+ useEffect(() => {
9
+ try {
10
+ (function () {
11
+ if (window.BOOMR &&
12
+ (window.BOOMR.version || window.BOOMR.snippetExecuted)) {
13
+ return;
14
+ }
15
+ // Executes only on first mount
16
+ window.BOOMR = window.BOOMR || {};
17
+ window.BOOMR.hydrogenPerformanceEvent = (data) => {
18
+ ClientAnalytics.publish(ClientAnalytics.eventNames.PERFORMANCE, true, data);
19
+ ClientAnalytics.pushToServer({
20
+ body: JSON.stringify(data),
21
+ }, ClientAnalytics.eventNames.PERFORMANCE);
22
+ };
23
+ window.BOOMR.storeDomain = storeDomain;
24
+ function boomerangSaveLoadTime(e) {
25
+ window.BOOMR_onload = (e && e.timeStamp) || Date.now();
26
+ }
27
+ // @ts-ignore
28
+ function boomerangInit(e) {
29
+ e.detail.BOOMR.init();
30
+ e.detail.BOOMR.t_end = Date.now();
31
+ }
32
+ if (window.addEventListener) {
33
+ window.addEventListener('load', boomerangSaveLoadTime, false);
34
+ // @ts-ignore
35
+ }
36
+ else if (window.attachEvent) {
37
+ // @ts-ignore
38
+ window.attachEvent('onload', boomerangSaveLoadTime);
39
+ }
40
+ if (document.addEventListener) {
41
+ document.addEventListener('onBoomerangLoaded', boomerangInit);
42
+ // @ts-ignore
43
+ }
44
+ else if (document.attachEvent) {
45
+ // @ts-ignore
46
+ document.attachEvent('onpropertychange', function (e) {
47
+ if (!e)
48
+ e = event;
49
+ if (e.propertyName === 'onBoomerangLoaded')
50
+ boomerangInit(e);
51
+ });
52
+ }
53
+ })();
54
+ loadScript(URL).catch(() => {
55
+ // ignore if boomerang doesn't load
56
+ // most likely because of an ad blocker
57
+ });
58
+ }
59
+ catch (err) {
60
+ // Do nothing
61
+ }
62
+ }, [storeDomain]);
63
+ return null;
64
+ }
@@ -0,0 +1 @@
1
+ export declare function request(request: Request, data?: any, contentType?: string): void;
@@ -0,0 +1,24 @@
1
+ export function request(request, data, contentType) {
2
+ const url = new URL(request.url);
3
+ if (url.search === '?performance' && contentType === 'json') {
4
+ const initTime = new Date().getTime();
5
+ fetch('https://monorail-edge.shopifysvc.com/v1/produce', {
6
+ method: 'post',
7
+ headers: {
8
+ 'content-type': 'text/plain',
9
+ 'x-forwarded-for': request.headers.get('x-forwarded-for') || '',
10
+ 'user-agent': request.headers.get('user-agent') || '',
11
+ },
12
+ body: JSON.stringify({
13
+ schema_id: 'hydrogen_buyer_performance/2.0',
14
+ payload: data,
15
+ metadata: {
16
+ event_created_at_ms: initTime,
17
+ event_sent_at_ms: new Date().getTime(),
18
+ },
19
+ }),
20
+ }).catch((error) => {
21
+ // send to bugsnag? oxygen?
22
+ });
23
+ }
24
+ }
@@ -0,0 +1 @@
1
+ export declare function PerformanceMetricsDebug(): null;
@@ -0,0 +1,23 @@
1
+ import { useEffect } from 'react';
2
+ import { ClientAnalytics } from '../../index';
3
+ const PAD = 10;
4
+ let isInit = false;
5
+ export function PerformanceMetricsDebug() {
6
+ useEffect(() => {
7
+ if (!isInit) {
8
+ isInit = true;
9
+ ClientAnalytics.subscribe(ClientAnalytics.eventNames.PERFORMANCE, (data) => {
10
+ console.group(`Performance - ${data.page_load_type} load`);
11
+ logMetricIf('TTFB:', data.response_start - data.navigation_start);
12
+ logMetricIf('FCP:', data.first_contentful_paint);
13
+ logMetricIf('LCP:', data.largest_contentful_paint);
14
+ logMetricIf('Duration:', data.response_end - data.navigation_start);
15
+ console.groupEnd();
16
+ });
17
+ }
18
+ });
19
+ return null;
20
+ }
21
+ function logMetricIf(lable, data) {
22
+ data && console.log(`${lable.padEnd(PAD)}${Math.round(data)} ms`);
23
+ }
@@ -5,4 +5,5 @@ export declare const eventNames: {
5
5
  REMOVE_FROM_CART: string;
6
6
  UPDATE_CART: string;
7
7
  DISCOUNT_CODE_UPDATED: string;
8
+ PERFORMANCE: string;
8
9
  };
@@ -5,4 +5,5 @@ export const eventNames = {
5
5
  REMOVE_FROM_CART: 'remove-from-cart',
6
6
  UPDATE_CART: 'update-cart',
7
7
  DISCOUNT_CODE_UPDATED: 'discount-code-updated',
8
+ PERFORMANCE: 'performance',
8
9
  };
@@ -1,5 +1,6 @@
1
1
  import { parse, stringify as stringifyCookie } from 'worktop/cookie';
2
2
  import { log } from '../../utilities/log';
3
+ import { parseJSON } from '../../utilities/parse';
3
4
  const reservedCookieNames = ['mac', 'user_session_id'];
4
5
  /** The `Cookie` component helps you build your own custom cookie and session implementations. All
5
6
  * [Hydrogen session storage mechanisms](https://shopify.dev/custom-storefronts/hydrogen/framework/sessions#types-of-session-storage) use the
@@ -26,7 +27,7 @@ export class Cookie {
26
27
  }
27
28
  parse(cookie) {
28
29
  try {
29
- const data = JSON.parse(parse(cookie)[this.name]);
30
+ const data = parseJSON(parse(cookie)[this.name]);
30
31
  this.data = data;
31
32
  }
32
33
  catch (e) {
@@ -1,22 +1,22 @@
1
1
  import type { ImportGlobEagerOutput } from '../../types';
2
2
  interface FileRoutesProps {
3
3
  /** The routes defined by Vite's [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. */
4
- routes: ImportGlobEagerOutput;
4
+ routes?: ImportGlobEagerOutput;
5
5
  /** A path that's prepended to all file routes. You can modify `basePath` if you want to prefix all file routes. For example, you can prefix all file routes with a locale. */
6
6
  basePath?: string;
7
7
  /** The portion of the file route path that shouldn't be a part of the URL. You need to modify this if you want to import routes from a location other than the default `src/routes`. */
8
- dirPrefix?: string;
8
+ dirPrefix?: string | RegExp;
9
9
  }
10
10
  /**
11
11
  * The `FileRoutes` component builds a set of default Hydrogen routes based on the output provided by Vite's
12
12
  * [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. You can have multiple
13
13
  * instances of this component to source file routes from multiple locations.
14
14
  */
15
- export declare function FileRoutes({ routes, basePath, dirPrefix, }: FileRoutesProps): JSX.Element | null;
15
+ export declare function FileRoutes({ routes, basePath, dirPrefix }: FileRoutesProps): JSX.Element | null;
16
16
  interface HydrogenRoute {
17
17
  component: any;
18
18
  path: string;
19
19
  exact: boolean;
20
20
  }
21
- export declare function createPageRoutes(pages: ImportGlobEagerOutput, topLevelPath: string | undefined, dirPrefix: string): HydrogenRoute[];
21
+ export declare function createPageRoutes(pages: ImportGlobEagerOutput, topLevelPath?: string, dirPrefix?: string | RegExp): HydrogenRoute[];
22
22
  export {};
@@ -1,19 +1,31 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { matchPath } from '../../utilities/matchPath';
3
3
  import { log } from '../../utilities/log';
4
+ import { extractPathFromRoutesKey } from '../../utilities/apiRoutes';
4
5
  import { useServerRequest } from '../ServerRequestProvider';
5
6
  import { RouteParamsProvider } from '../useRouteParams/RouteParamsProvider.client';
7
+ import { findRoutePrefix } from '../../utilities/findRoutePrefix';
6
8
  /**
7
9
  * The `FileRoutes` component builds a set of default Hydrogen routes based on the output provided by Vite's
8
10
  * [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. You can have multiple
9
11
  * instances of this component to source file routes from multiple locations.
10
12
  */
11
- export function FileRoutes({ routes, basePath = '/', dirPrefix = './routes', }) {
13
+ export function FileRoutes({ routes, basePath, dirPrefix }) {
14
+ var _a;
12
15
  const request = useServerRequest();
13
16
  const { routeRendered, serverProps } = request.ctx.router;
14
17
  if (routeRendered)
15
18
  return null;
16
- const pageRoutes = useMemo(() => createPageRoutes(routes, basePath, dirPrefix), [routes, basePath]);
19
+ if (!routes) {
20
+ 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);
24
+ }
25
+ basePath !== null && basePath !== void 0 ? basePath : (basePath = '/');
26
+ /* eslint-disable react-hooks/rules-of-hooks */
27
+ const pageRoutes = useMemo(() => createPageRoutes(routes, basePath, dirPrefix), [routes, basePath, dirPrefix]);
28
+ /* eslint-enable react-hooks/rules-of-hooks */
17
29
  let foundRoute, foundRouteDetails;
18
30
  for (let i = 0; i < pageRoutes.length; i++) {
19
31
  foundRouteDetails = matchPath(serverProps.pathname, pageRoutes[i]);
@@ -32,26 +44,11 @@ export function FileRoutes({ routes, basePath = '/', dirPrefix = './routes', })
32
44
  }
33
45
  export function createPageRoutes(pages, topLevelPath = '*', dirPrefix) {
34
46
  const topLevelPrefix = topLevelPath.replace('*', '').replace(/\/$/, '');
35
- const routes = Object.keys(pages)
47
+ const keys = Object.keys(pages);
48
+ const commonRoutePrefix = dirPrefix !== null && dirPrefix !== void 0 ? dirPrefix : findRoutePrefix(keys);
49
+ const routes = keys
36
50
  .map((key) => {
37
- let path = key
38
- .replace(dirPrefix, '')
39
- .replace(/\.server\.(t|j)sx?$/, '')
40
- /**
41
- * Replace /index with /
42
- */
43
- .replace(/\/index$/i, '/')
44
- /**
45
- * Only lowercase the first letter. This allows the developer to use camelCase
46
- * dynamic paths while ensuring their standard routes are normalized to lowercase.
47
- */
48
- .replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase())
49
- /**
50
- * Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
51
- */
52
- .replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
53
- if (path.endsWith('/') && path !== '/')
54
- path = path.substring(0, path.length - 1);
51
+ const path = extractPathFromRoutesKey(key, commonRoutePrefix);
55
52
  /**
56
53
  * Catch-all routes [...handle].jsx don't need an exact match
57
54
  * https://reactrouter.com/core/api/Route/exact-bool
@@ -2,6 +2,7 @@ import { Cookie } from '../Cookie/Cookie';
2
2
  import { v4 as uid } from 'uuid';
3
3
  import path from 'path';
4
4
  import { promises as fsp } from 'fs';
5
+ import { parseJSON } from '../../utilities/parse';
5
6
  async function wait() {
6
7
  return new Promise((resolve) => setTimeout(resolve));
7
8
  }
@@ -77,7 +78,7 @@ async function getFile(file, expires, log) {
77
78
  try {
78
79
  const textContent = await fsp.readFile(file, { encoding: 'utf-8' });
79
80
  try {
80
- content = JSON.parse(textContent);
81
+ content = parseJSON(textContent);
81
82
  }
82
83
  catch (error) {
83
84
  log.warn(`Cannot parse existing session file: ${file}`);
@@ -9,6 +9,7 @@ export default function Redirect({ to }) {
9
9
  else {
10
10
  navigate(to);
11
11
  }
12
+ // eslint-disable-next-line react-hooks/exhaustive-deps
12
13
  }, []);
13
14
  return null;
14
15
  }
@@ -1,15 +1,12 @@
1
1
  import React, { cloneElement } from 'react';
2
2
  import { useServerRequest } from '../ServerRequestProvider';
3
3
  import { matchPath } from '../../utilities/matchPath';
4
- import { Boomerang } from '../Boomerang/Boomerang.client';
5
4
  import { RouteParamsProvider } from '../useRouteParams/RouteParamsProvider.client';
6
- import { useServerAnalytics } from '../Analytics';
7
5
  /**
8
6
  * The `Route` component is used to set up a route in Hydrogen that's independent of the file system. Routes are
9
7
  * matched in the order that they're defined.
10
8
  */
11
9
  export function Route({ path, page }) {
12
- var _a;
13
10
  const request = useServerRequest();
14
11
  const { routeRendered, serverProps } = request.ctx.router;
15
12
  if (routeRendered)
@@ -25,13 +22,7 @@ export function Route({ path, page }) {
25
22
  if (match) {
26
23
  request.ctx.router.routeRendered = true;
27
24
  request.ctx.router.routeParams = match.params;
28
- const name = (_a = page === null || page === void 0 ? void 0 : page.type) === null || _a === void 0 ? void 0 : _a.name;
29
- useServerAnalytics({
30
- templateName: name,
31
- });
32
- return (React.createElement(RouteParamsProvider, { routeParams: match.params },
33
- cloneElement(page, { params: match.params || {}, ...serverProps }),
34
- name ? React.createElement(Boomerang, { pageTemplate: name }) : null));
25
+ return (React.createElement(RouteParamsProvider, { routeParams: match.params }, cloneElement(page, { params: match.params || {}, ...serverProps })));
35
26
  }
36
27
  return null;
37
28
  }