@shopify/hydrogen 0.18.0 → 0.21.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 (145) hide show
  1. package/CHANGELOG.md +200 -0
  2. package/config.d.ts +1 -0
  3. package/dist/esnext/components/CartEstimatedCost/CartEstimatedCost.client.d.ts +1 -1
  4. package/dist/esnext/components/CartLineImage/CartLineImage.client.d.ts +4 -7
  5. package/dist/esnext/components/CartLineImage/CartLineImage.client.js +1 -2
  6. package/dist/esnext/components/CartLinePrice/CartLinePrice.client.d.ts +1 -1
  7. package/dist/esnext/components/CartProvider/CartProvider.client.d.ts +3 -1
  8. package/dist/esnext/components/CartProvider/CartProvider.client.js +22 -20
  9. package/dist/esnext/components/CartProvider/cart-queries.d.ts +10 -9
  10. package/dist/esnext/components/CartProvider/cart-queries.js +58 -743
  11. package/dist/esnext/components/CartProvider/hooks.client.js +4 -2
  12. package/dist/esnext/components/CartProvider/types.d.ts +2 -0
  13. package/dist/esnext/components/Image/Image.d.ts +78 -34
  14. package/dist/esnext/components/Image/Image.js +54 -51
  15. package/dist/esnext/components/Image/index.d.ts +1 -0
  16. package/dist/esnext/components/LocalizationProvider/LocalizationClientProvider.client.js +2 -15
  17. package/dist/esnext/components/LocalizationProvider/LocalizationContext.client.d.ts +0 -1
  18. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.d.ts +2 -6
  19. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.js +10 -4
  20. package/dist/esnext/components/MediaFile/MediaFile.d.ts +2 -2
  21. package/dist/esnext/components/MediaFile/MediaFile.js +2 -2
  22. package/dist/esnext/components/Money/Money.client.d.ts +11 -5
  23. package/dist/esnext/components/Money/Money.client.js +16 -3
  24. package/dist/esnext/components/ProductPrice/ProductPrice.client.d.ts +1 -2
  25. package/dist/esnext/components/ProductPrice/ProductPrice.client.js +1 -2
  26. package/dist/esnext/components/Video/Video.d.ts +3 -3
  27. package/dist/esnext/components/Video/Video.js +7 -4
  28. package/dist/esnext/components/index.d.ts +0 -3
  29. package/dist/esnext/components/index.js +0 -3
  30. package/dist/esnext/entry-server.d.ts +13 -1
  31. package/dist/esnext/entry-server.js +18 -51
  32. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
  33. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.js +2 -0
  34. package/dist/esnext/foundation/fetchSync/server/fetchSync.d.ts +1 -1
  35. package/dist/esnext/foundation/fetchSync/server/fetchSync.js +1 -1
  36. package/dist/esnext/foundation/useQuery/hooks.js +8 -9
  37. package/dist/esnext/foundation/useSession/useSession.d.ts +1 -1
  38. package/dist/esnext/foundation/useSession/useSession.js +1 -1
  39. package/dist/esnext/framework/Hydration/Html.js +3 -1
  40. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.d.ts +1 -1
  41. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.js +2 -1
  42. package/dist/esnext/framework/Hydration/rsc.d.ts +0 -3
  43. package/dist/esnext/framework/Hydration/rsc.js +40 -12
  44. package/dist/esnext/framework/cache/in-memory.js +0 -6
  45. package/dist/esnext/framework/cache-sub-request.d.ts +17 -0
  46. package/dist/esnext/framework/cache-sub-request.js +64 -0
  47. package/dist/esnext/framework/cache.d.ts +6 -6
  48. package/dist/esnext/framework/cache.js +36 -33
  49. package/dist/esnext/framework/plugin.js +5 -30
  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.d.ts +1 -1
  53. package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +73 -3
  54. package/dist/esnext/framework/plugins/vite-plugin-hydration-auto-import.js +1 -4
  55. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +6 -4
  56. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +1 -1
  57. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +2 -3
  58. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-rsc.d.ts +1 -0
  59. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-rsc.js +35 -0
  60. package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +1 -1
  61. package/dist/esnext/framework/plugins/vite-plugin-ssr-interop.js +6 -3
  62. package/dist/esnext/hooks/useCountry/useCountry.d.ts +1 -11
  63. package/dist/esnext/hooks/useCountry/useCountry.js +1 -1
  64. package/dist/esnext/index.d.ts +4 -0
  65. package/dist/esnext/index.js +4 -0
  66. package/dist/esnext/node.d.ts +1 -0
  67. package/dist/esnext/node.js +1 -0
  68. package/dist/esnext/storefront-api-types.d.ts +5 -3
  69. package/dist/esnext/storefront-api-types.js +5 -3
  70. package/dist/esnext/types.d.ts +4 -3
  71. package/dist/esnext/utilities/bot-ua.js +4 -0
  72. package/dist/esnext/utilities/html-encoding.d.ts +2 -0
  73. package/dist/esnext/utilities/html-encoding.js +16 -0
  74. package/dist/esnext/utilities/image_size.d.ts +4 -22
  75. package/dist/esnext/utilities/image_size.js +15 -33
  76. package/dist/esnext/utilities/index.d.ts +2 -1
  77. package/dist/esnext/utilities/index.js +2 -1
  78. package/dist/esnext/utilities/log/log-cache-api-status.js +5 -1
  79. package/dist/esnext/version.d.ts +1 -1
  80. package/dist/esnext/version.js +1 -1
  81. package/dist/node/components/Image/Image.d.ts +84 -0
  82. package/dist/node/components/Image/Image.js +86 -0
  83. package/dist/node/components/Image/index.d.ts +2 -0
  84. package/dist/node/components/Image/index.js +5 -0
  85. package/dist/node/entry-server.d.ts +13 -1
  86. package/dist/node/entry-server.js +18 -51
  87. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
  88. package/dist/node/framework/Hydration/Html.js +3 -1
  89. package/dist/node/framework/Hydration/ServerComponentResponse.server.d.ts +1 -1
  90. package/dist/node/framework/Hydration/ServerComponentResponse.server.js +2 -1
  91. package/dist/node/framework/Hydration/rsc.d.ts +0 -3
  92. package/dist/node/framework/Hydration/rsc.js +40 -12
  93. package/dist/node/framework/cache/in-memory.js +0 -6
  94. package/dist/node/framework/cache-sub-request.d.ts +17 -0
  95. package/dist/node/framework/cache-sub-request.js +95 -0
  96. package/dist/node/framework/cache.d.ts +6 -6
  97. package/dist/node/framework/cache.js +38 -35
  98. package/dist/node/framework/plugin.js +5 -53
  99. package/dist/node/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
  100. package/dist/node/framework/plugins/vite-plugin-client-imports.js +28 -0
  101. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.d.ts +1 -1
  102. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +76 -3
  103. package/dist/node/framework/plugins/vite-plugin-hydration-auto-import.js +1 -4
  104. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +6 -4
  105. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +1 -1
  106. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +2 -3
  107. package/dist/node/framework/plugins/vite-plugin-hydrogen-rsc.d.ts +1 -0
  108. package/dist/node/framework/plugins/vite-plugin-hydrogen-rsc.js +41 -0
  109. package/dist/node/framework/plugins/vite-plugin-platform-entry.js +1 -1
  110. package/dist/node/framework/plugins/vite-plugin-ssr-interop.js +6 -3
  111. package/dist/node/storefront-api-types.d.ts +5 -3
  112. package/dist/node/storefront-api-types.js +5 -3
  113. package/dist/node/types.d.ts +4 -3
  114. package/dist/node/utilities/bot-ua.js +4 -0
  115. package/dist/node/utilities/html-encoding.d.ts +2 -0
  116. package/dist/node/utilities/html-encoding.js +21 -0
  117. package/dist/node/utilities/image_size.d.ts +4 -22
  118. package/dist/node/utilities/image_size.js +16 -58
  119. package/dist/node/utilities/index.d.ts +2 -1
  120. package/dist/node/utilities/index.js +4 -2
  121. package/dist/node/utilities/log/log-cache-api-status.js +5 -1
  122. package/dist/node/version.d.ts +1 -1
  123. package/dist/node/version.js +1 -1
  124. package/entry-server.d.ts +1 -1
  125. package/package.json +3 -3
  126. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +200 -31
  127. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-client-proxy.js +2 -0
  128. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +200 -31
  129. package/vendor/react-server-dom-vite/package.json +2 -1
  130. package/dist/esnext/components/ProductDescription/ProductDescription.client.d.ts +0 -13
  131. package/dist/esnext/components/ProductDescription/ProductDescription.client.js +0 -16
  132. package/dist/esnext/components/ProductDescription/index.d.ts +0 -1
  133. package/dist/esnext/components/ProductDescription/index.js +0 -1
  134. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.d.ts +0 -21
  135. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +0 -42
  136. package/dist/esnext/components/ProductMetafield/index.d.ts +0 -2
  137. package/dist/esnext/components/ProductMetafield/index.js +0 -1
  138. package/dist/esnext/components/ProductTitle/ProductTitle.client.d.ts +0 -13
  139. package/dist/esnext/components/ProductTitle/ProductTitle.client.js +0 -16
  140. package/dist/esnext/components/ProductTitle/index.d.ts +0 -1
  141. package/dist/esnext/components/ProductTitle/index.js +0 -1
  142. package/dist/esnext/components/UnitPrice/UnitPrice.client.d.ts +0 -15
  143. package/dist/esnext/components/UnitPrice/UnitPrice.client.js +0 -22
  144. package/dist/esnext/components/UnitPrice/index.d.ts +0 -1
  145. package/dist/esnext/components/UnitPrice/index.js +0 -1
@@ -18,6 +18,7 @@ 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';
@@ -63,7 +64,10 @@ export const renderHydrogen = (App, hydrogenConfig) => {
63
64
  : apiResponse;
64
65
  }
65
66
  }
66
- const isStreamable = !isBotUA(url, request.headers.get('user-agent')) &&
67
+ const isStreamable = (hydrogenConfig.enableStreaming
68
+ ? hydrogenConfig.enableStreaming(request)
69
+ : true) &&
70
+ !isBotUA(url, request.headers.get('user-agent')) &&
67
71
  (!!streamableResponse || (await isStreamingSupported()));
68
72
  let template = typeof indexTemplate === 'function'
69
73
  ? await indexTemplate(url.toString())
@@ -142,11 +146,7 @@ async function render(url, { App, request, template, componentResponse, nonce, l
142
146
  headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
143
147
  html = applyHtmlHead(html, request.ctx.head, template);
144
148
  if (flight) {
145
- html = html.replace('</body>', () => `${flightContainer({
146
- init: true,
147
- nonce,
148
- chunk: flight,
149
- })}</body>`);
149
+ html = html.replace('</body>', () => flightContainer(flight) + '</body>');
150
150
  }
151
151
  postRequestTasks('ssr', status, request, componentResponse);
152
152
  return new Response(html, {
@@ -174,12 +174,10 @@ async function stream(url, { App, request, response, componentResponse, template
174
174
  const rscToScriptTagReadable = new ReadableStream({
175
175
  start(controller) {
176
176
  log.trace('rsc start chunks');
177
- let init = true;
178
177
  const encoder = new TextEncoder();
179
178
  bufferReadableStream(rscReadable.getReader(), (chunk) => {
180
- const scriptTag = flightContainer({ init, chunk, nonce });
181
- controller.enqueue(encoder.encode(scriptTag));
182
- init = false;
179
+ const metaTag = flightContainer(chunk);
180
+ controller.enqueue(encoder.encode(metaTag));
183
181
  }).then(() => {
184
182
  log.trace('rsc finish chunks');
185
183
  return controller.close();
@@ -370,34 +368,15 @@ async function hydrate(url, { App, log, request, response, isStreamable, compone
370
368
  request,
371
369
  response: componentResponse,
372
370
  });
373
- if (__WORKER__) {
374
- const rscReadable = rscRenderToReadableStream(AppRSC);
375
- if (isStreamable && (await isStreamingSupported())) {
376
- postRequestTasks('rsc', 200, request, componentResponse);
377
- return new Response(rscReadable);
378
- }
379
- // Note: CFW does not support reader.piteTo nor iterable syntax
380
- const bufferedBody = await bufferReadableStream(rscReadable.getReader());
381
- postRequestTasks('rsc', 200, request, componentResponse);
382
- return new Response(bufferedBody, {
383
- headers: {
384
- 'cache-control': componentResponse.cacheControlHeader,
385
- },
386
- });
387
- }
388
- else if (response) {
389
- const rscWriter = await import(
390
- // @ts-ignore
391
- '@shopify/hydrogen/vendor/react-server-dom-vite/writer.node.server');
392
- const streamer = rscWriter.renderToPipeableStream(AppRSC);
393
- response.writeHead(200, 'ok', {
371
+ const rscReadable = rscRenderToReadableStream(AppRSC);
372
+ // Note: CFW does not support reader.piteTo nor iterable syntax
373
+ const bufferedBody = await bufferReadableStream(rscReadable.getReader());
374
+ postRequestTasks('rsc', 200, request, componentResponse);
375
+ return new Response(bufferedBody, {
376
+ headers: {
394
377
  'cache-control': componentResponse.cacheControlHeader,
395
- });
396
- const stream = streamer.pipe(response);
397
- stream.on('finish', function () {
398
- postRequestTasks('rsc', response.statusCode, request, componentResponse);
399
- });
400
- }
378
+ },
379
+ });
401
380
  }
402
381
  function buildAppRSC({ App, log, state, request, response }) {
403
382
  const hydrogenServerProps = { request, response, log };
@@ -528,20 +507,8 @@ async function createNodeWriter() {
528
507
  const { PassThrough } = await import(streamImport);
529
508
  return new PassThrough();
530
509
  }
531
- function flightContainer({ init, chunk, nonce, }) {
532
- let script = `<script${nonce ? ` nonce="${nonce}"` : ''}>`;
533
- if (init) {
534
- script += 'var __flight=[];';
535
- }
536
- if (chunk) {
537
- const normalizedChunk = chunk
538
- // 1. Duplicate the escape char (\) for already escaped characters (e.g. \n or \").
539
- .replace(/\\/g, String.raw `\\`)
540
- // 2. Escape existing backticks to allow wrapping the whole thing in `...`.
541
- .replace(/`/g, String.raw `\``);
542
- script += `__flight.push(\`${normalizedChunk}\`)`;
543
- }
544
- return script + '</script>';
510
+ function flightContainer(chunk) {
511
+ return `<meta data-flight="${htmlEncode(chunk)}" />`;
545
512
  }
546
513
  function postRequestTasks(type, status, request, componentResponse) {
547
514
  logServerResponse(type, request, status);
@@ -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) {
@@ -31,6 +31,8 @@ export function ServerStateProvider({ serverState, setServerState, children, })
31
31
  else {
32
32
  newValue = input;
33
33
  }
34
+ if (!newValue)
35
+ return { ...prev };
34
36
  if (__DEV__) {
35
37
  const privateProp = PRIVATE_PROPS.find((prop) => prop in newValue);
36
38
  if (privateProp) {
@@ -1,7 +1,7 @@
1
1
  import { type HydrogenUseQueryOptions } from '../../useQuery/hooks';
2
2
  import type { FetchResponse } from '../types';
3
3
  /**
4
- * The `fetchSync` hook makes third-party API requests and is the recommended way to make simple fetch calls on the server.
4
+ * The `fetchSync` hook makes API requests and is the recommended way to make simple fetch calls on the server and the client.
5
5
  * It's designed similar to the [Web API's `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), only in a way
6
6
  * that supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html).
7
7
  */
@@ -1,7 +1,7 @@
1
1
  import { parseJSON } from '../../../utilities/parse';
2
2
  import { useQuery } from '../../useQuery/hooks';
3
3
  /**
4
- * The `fetchSync` hook makes third-party API requests and is the recommended way to make simple fetch calls on the server.
4
+ * The `fetchSync` hook makes API requests and is the recommended way to make simple fetch calls on the server and the client.
5
5
  * It's designed similar to the [Web API's `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), only in a way
6
6
  * that supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html).
7
7
  */
@@ -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,2 +1,2 @@
1
1
  /** The `useSession` hook reads session data in server components. */
2
- export declare const useSession: () => Record<string, string> | undefined;
2
+ export declare const useSession: () => Record<string, string>;
@@ -3,6 +3,6 @@ import { useServerRequest } from '../ServerRequestProvider';
3
3
  export const useSession = function () {
4
4
  var _a;
5
5
  const request = useServerRequest();
6
- const session = (_a = request.ctx.session) === null || _a === void 0 ? void 0 : _a.get();
6
+ const session = ((_a = request.ctx.session) === null || _a === void 0 ? void 0 : _a.get()) || {};
7
7
  return session;
8
8
  };
@@ -27,7 +27,9 @@ export function Html({ children, template, htmlAttrs, bodyAttrs }) {
27
27
  if (import.meta.env.DEV) {
28
28
  // Fix React Refresh for async scripts.
29
29
  // https://github.com/vitejs/vite/issues/6759
30
- head = head.replace(/>(\s*?import[\s\w]+?['"]\/@react-refresh)/, ' async="">$1');
30
+ head =
31
+ '<script></script>' + // Fix for Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1737882
32
+ head.replace(/>(\s*?import[\s\w]+?['"]\/@react-refresh)/, ' async="">$1');
31
33
  }
32
34
  return (React.createElement("html", { ...attrsToProps(getHtmlAttrs(template)), ...htmlAttrs },
33
35
  React.createElement("head", { dangerouslySetInnerHTML: { __html: head } }),
@@ -2,7 +2,7 @@ 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;
@@ -6,6 +6,7 @@ export class ServerComponentResponse extends Response {
6
6
  constructor() {
7
7
  super(...arguments);
8
8
  this.wait = false;
9
+ this.cacheOptions = CacheSeconds();
9
10
  /**
10
11
  * Allow custom body to be a string or a Promise.
11
12
  */
@@ -25,7 +26,7 @@ export class ServerComponentResponse extends Response {
25
26
  this.cacheOptions = options;
26
27
  }
27
28
  get cacheControlHeader() {
28
- return generateCacheControlHeader(this.cacheOptions || CacheSeconds());
29
+ return generateCacheControlHeader(this.cacheOptions);
29
30
  }
30
31
  writeHead({ status, statusText, headers, } = {}) {
31
32
  if (status || statusText) {
@@ -1,6 +1,3 @@
1
- declare global {
2
- var __flight: Array<string>;
3
- }
4
1
  /**
5
2
  * Much of this is borrowed from React's demo implementation:
6
3
  * @see https://github.com/reactjs/server-components-demo/blob/main/src/Cache.client.js
@@ -1,13 +1,42 @@
1
1
  // TODO should we move this file to src/foundation
2
2
  // so it is considered ESM instead of CJS?
3
- // @ts-ignore
4
- import { unstable_getCacheForType, unstable_useCacheRefresh } from 'react';
5
3
  import { createFromFetch, createFromReadableStream,
6
4
  // @ts-ignore
7
5
  } from '@shopify/hydrogen/vendor/react-server-dom-vite';
8
6
  import { RSC_PATHNAME } from '../../constants';
7
+ import { htmlDecode } from '../../utilities';
9
8
  let rscReader;
10
- if (globalThis.__flight && __flight.length > 0) {
9
+ // Hydrate an SSR response from <meta> tags placed in the DOM.
10
+ const flightChunks = [];
11
+ const FLIGHT_ATTRIBUTE = 'data-flight';
12
+ function addElementToFlightChunks(el) {
13
+ const chunk = el.getAttribute(FLIGHT_ATTRIBUTE);
14
+ if (chunk) {
15
+ flightChunks.push(htmlDecode(chunk));
16
+ }
17
+ }
18
+ // Get initial payload
19
+ document
20
+ .querySelectorAll('[' + FLIGHT_ATTRIBUTE + ']')
21
+ .forEach(addElementToFlightChunks);
22
+ // Create a mutation observer on the document to detect when new
23
+ // <meta data-flight> tags are added, and add them to the array.
24
+ const observer = new MutationObserver((mutations) => {
25
+ mutations.forEach((mutation) => {
26
+ mutation.addedNodes.forEach((node) => {
27
+ if (node instanceof HTMLElement &&
28
+ node.tagName === 'META' &&
29
+ node.hasAttribute(FLIGHT_ATTRIBUTE)) {
30
+ addElementToFlightChunks(node);
31
+ }
32
+ });
33
+ });
34
+ });
35
+ observer.observe(document.documentElement, {
36
+ childList: true,
37
+ subtree: true,
38
+ });
39
+ if (flightChunks.length > 0) {
11
40
  const contentLoaded = new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve));
12
41
  try {
13
42
  rscReader = new ReadableStream({
@@ -17,9 +46,12 @@ if (globalThis.__flight && __flight.length > 0) {
17
46
  controller.enqueue(encoder.encode(chunk));
18
47
  return 0;
19
48
  };
20
- __flight.forEach(write);
21
- __flight.push = write;
22
- contentLoaded.then(() => controller.close());
49
+ flightChunks.forEach(write);
50
+ flightChunks.push = write;
51
+ contentLoaded.then(() => {
52
+ controller.close();
53
+ observer.disconnect();
54
+ });
23
55
  },
24
56
  });
25
57
  }
@@ -27,9 +59,7 @@ if (globalThis.__flight && __flight.length > 0) {
27
59
  // Old browser, will try a new hydration request later
28
60
  }
29
61
  }
30
- function createResponseCache() {
31
- return new Map();
32
- }
62
+ const cache = new Map();
33
63
  /**
34
64
  * Much of this is borrowed from React's demo implementation:
35
65
  * @see https://github.com/reactjs/server-components-demo/blob/main/src/Cache.client.js
@@ -38,7 +68,6 @@ function createResponseCache() {
38
68
  */
39
69
  export function useServerResponse(state) {
40
70
  const key = JSON.stringify(state);
41
- const cache = unstable_getCacheForType(createResponseCache);
42
71
  let response = cache.get(key);
43
72
  if (response) {
44
73
  return response;
@@ -67,6 +96,5 @@ export function useServerResponse(state) {
67
96
  return response;
68
97
  }
69
98
  export function useRefresh() {
70
- const refreshCache = unstable_useCacheRefresh();
71
- refreshCache();
99
+ cache.clear();
72
100
  }
@@ -1,4 +1,3 @@
1
- import { logCacheApiStatus } from '../../utilities/log';
2
1
  /**
3
2
  * This is an in-memory implementation of `Cache` that *barely*
4
3
  * works and is only meant to be used during development.
@@ -8,7 +7,6 @@ export class InMemoryCache {
8
7
  this.store = new Map();
9
8
  }
10
9
  put(request, response) {
11
- logCacheApiStatus('PUT-dev', request.url);
12
10
  this.store.set(request.url, {
13
11
  value: response,
14
12
  date: new Date(),
@@ -18,7 +16,6 @@ export class InMemoryCache {
18
16
  var _a, _b;
19
17
  const match = this.store.get(request.url);
20
18
  if (!match) {
21
- logCacheApiStatus('MISS-dev', request.url);
22
19
  return;
23
20
  }
24
21
  const { value, date } = match;
@@ -28,7 +25,6 @@ export class InMemoryCache {
28
25
  const age = (new Date().valueOf() - date.valueOf()) / 1000;
29
26
  const isMiss = age > maxAge + swr;
30
27
  if (isMiss) {
31
- logCacheApiStatus('MISS-dev', request.url);
32
28
  this.store.delete(request.url);
33
29
  return;
34
30
  }
@@ -36,7 +32,6 @@ export class InMemoryCache {
36
32
  const headers = new Headers(value.headers);
37
33
  headers.set('cache', isStale ? 'STALE' : 'HIT');
38
34
  headers.set('date', date.toUTCString());
39
- logCacheApiStatus(`${headers.get('cache')}-dev`, request.url);
40
35
  const response = new Response(value.body, {
41
36
  headers,
42
37
  });
@@ -44,7 +39,6 @@ export class InMemoryCache {
44
39
  }
45
40
  delete(request) {
46
41
  this.store.delete(request.url);
47
- logCacheApiStatus('DELETE-dev', request.url);
48
42
  }
49
43
  keys(request) {
50
44
  const cacheKeys = [];
@@ -0,0 +1,17 @@
1
+ import type { QueryKey, CachingStrategy } from '../types';
2
+ export declare function generateSubRequestCacheControlHeader(userCacheOptions?: CachingStrategy): string;
3
+ /**
4
+ * Get an item from the cache. If a match is found, returns a tuple
5
+ * containing the `JSON.parse` version of the response as well
6
+ * as the response itself so it can be checked for staleness.
7
+ */
8
+ export declare function getItemFromCache(key: QueryKey): Promise<undefined | [any, Response]>;
9
+ /**
10
+ * Put an item into the cache.
11
+ */
12
+ export declare function setItemInCache(key: QueryKey, value: any, userCacheOptions?: CachingStrategy): Promise<void>;
13
+ export declare function deleteItemFromCache(key: QueryKey): Promise<void>;
14
+ /**
15
+ * Manually check the response to see if it's stale.
16
+ */
17
+ export declare function isStale(key: QueryKey, response: Response): boolean;
@@ -0,0 +1,64 @@
1
+ import { getCache } from './runtime';
2
+ import { hashKey } from '../utilities/hash';
3
+ import * as CacheApi from './cache';
4
+ import { CacheSeconds } from './CachingStrategy';
5
+ /**
6
+ * Wrapper Cache functions for sub queries
7
+ */
8
+ /**
9
+ * Cache API is weird. We just need a full URL, so we make one up.
10
+ */
11
+ function getKeyUrl(key) {
12
+ return `https://shopify.dev/?${key}`;
13
+ }
14
+ function getCacheOption(userCacheOptions) {
15
+ return userCacheOptions || CacheSeconds();
16
+ }
17
+ export function generateSubRequestCacheControlHeader(userCacheOptions) {
18
+ return CacheApi.generateDefaultCacheControlHeader(getCacheOption(userCacheOptions));
19
+ }
20
+ /**
21
+ * Get an item from the cache. If a match is found, returns a tuple
22
+ * containing the `JSON.parse` version of the response as well
23
+ * as the response itself so it can be checked for staleness.
24
+ */
25
+ export async function getItemFromCache(key) {
26
+ const cache = getCache();
27
+ if (!cache) {
28
+ return;
29
+ }
30
+ const url = getKeyUrl(hashKey(key));
31
+ const request = new Request(url);
32
+ const response = await CacheApi.getItemFromCache(request);
33
+ if (!response) {
34
+ return;
35
+ }
36
+ return [await response.json(), response];
37
+ }
38
+ /**
39
+ * Put an item into the cache.
40
+ */
41
+ export async function setItemInCache(key, value, userCacheOptions) {
42
+ const cache = getCache();
43
+ if (!cache) {
44
+ return;
45
+ }
46
+ const url = getKeyUrl(hashKey(key));
47
+ const request = new Request(url);
48
+ const response = new Response(JSON.stringify(value));
49
+ await CacheApi.setItemInCache(request, response, getCacheOption(userCacheOptions));
50
+ }
51
+ export async function deleteItemFromCache(key) {
52
+ const cache = getCache();
53
+ if (!cache)
54
+ return;
55
+ const url = getKeyUrl(hashKey(key));
56
+ const request = new Request(url);
57
+ await CacheApi.deleteItemFromCache(request);
58
+ }
59
+ /**
60
+ * Manually check the response to see if it's stale.
61
+ */
62
+ export function isStale(key, response) {
63
+ return CacheApi.isStale(new Request(getKeyUrl(hashKey(key))), response);
64
+ }
@@ -1,17 +1,17 @@
1
- import type { QueryKey, CachingStrategy } from '../types';
2
- export declare function generateSubRequestCacheControlHeader(userCacheOptions?: CachingStrategy): string;
1
+ import type { CachingStrategy } from '../types';
2
+ export declare function generateDefaultCacheControlHeader(userCacheOptions?: CachingStrategy): string;
3
3
  /**
4
4
  * Get an item from the cache. If a match is found, returns a tuple
5
5
  * containing the `JSON.parse` version of the response as well
6
6
  * as the response itself so it can be checked for staleness.
7
7
  */
8
- export declare function getItemFromCache(key: QueryKey): Promise<undefined | [any, Response]>;
8
+ export declare function getItemFromCache(request: Request): Promise<Response | undefined>;
9
9
  /**
10
10
  * Put an item into the cache.
11
11
  */
12
- export declare function setItemInCache(key: QueryKey, value: any, userCacheOptions?: CachingStrategy): Promise<void>;
13
- export declare function deleteItemFromCache(key: QueryKey): Promise<void>;
12
+ export declare function setItemInCache(request: Request, response: Response, userCacheOptions: CachingStrategy): Promise<void>;
13
+ export declare function deleteItemFromCache(request: Request): Promise<void>;
14
14
  /**
15
15
  * Manually check the response to see if it's stale.
16
16
  */
17
- export declare function isStale(response: Response, userCacheOptions?: CachingStrategy): boolean;
17
+ export declare function isStale(request: Request, response: Response): boolean;
@@ -1,6 +1,5 @@
1
1
  import { getCache } from './runtime';
2
2
  import { CacheSeconds, generateCacheControlHeader, } from '../framework/CachingStrategy';
3
- import { hashKey } from '../utilities/hash';
4
3
  import { logCacheApiStatus } from '../utilities/log';
5
4
  function getCacheControlSetting(userCacheOptions, options) {
6
5
  if (userCacheOptions && options) {
@@ -13,45 +12,35 @@ function getCacheControlSetting(userCacheOptions, options) {
13
12
  return userCacheOptions || CacheSeconds();
14
13
  }
15
14
  }
16
- export function generateSubRequestCacheControlHeader(userCacheOptions) {
15
+ export function generateDefaultCacheControlHeader(userCacheOptions) {
17
16
  return generateCacheControlHeader(getCacheControlSetting(userCacheOptions));
18
17
  }
19
- /**
20
- * Cache API is weird. We just need a full URL, so we make one up.
21
- */
22
- function getKeyUrl(key) {
23
- return `https://shopify.dev/?${key}`;
24
- }
25
18
  /**
26
19
  * Get an item from the cache. If a match is found, returns a tuple
27
20
  * containing the `JSON.parse` version of the response as well
28
21
  * as the response itself so it can be checked for staleness.
29
22
  */
30
- export async function getItemFromCache(key) {
23
+ export async function getItemFromCache(request) {
31
24
  const cache = getCache();
32
25
  if (!cache) {
33
26
  return;
34
27
  }
35
- const url = getKeyUrl(hashKey(key));
36
- const request = new Request(url);
37
28
  const response = await cache.match(request);
38
29
  if (!response) {
39
- logCacheApiStatus('MISS', url);
30
+ logCacheApiStatus('MISS', request.url);
40
31
  return;
41
32
  }
42
- logCacheApiStatus('HIT', url);
43
- return [await response.json(), response];
33
+ logCacheApiStatus('HIT', request.url);
34
+ return response;
44
35
  }
45
36
  /**
46
37
  * Put an item into the cache.
47
38
  */
48
- export async function setItemInCache(key, value, userCacheOptions) {
39
+ export async function setItemInCache(request, response, userCacheOptions) {
49
40
  const cache = getCache();
50
41
  if (!cache) {
51
42
  return;
52
43
  }
53
- const url = getKeyUrl(hashKey(key));
54
- const request = new Request(url);
55
44
  /**
56
45
  * We are manually managing staled request by adding this workaround.
57
46
  * Why? cache control header support is dependent on hosting platform
@@ -91,34 +80,48 @@ export async function setItemInCache(key, value, userCacheOptions) {
91
80
  * `isStale` function will use the above information to test for stale-ness of a cached response
92
81
  */
93
82
  const cacheControl = getCacheControlSetting(userCacheOptions);
94
- const headers = new Headers({
95
- 'cache-control': generateSubRequestCacheControlHeader(getCacheControlSetting(cacheControl, {
96
- maxAge: (cacheControl.maxAge || 0) + (cacheControl.staleWhileRevalidate || 0),
97
- })),
98
- 'cache-put-date': new Date().toUTCString(),
99
- });
100
- const response = new Response(JSON.stringify(value), { headers });
101
- logCacheApiStatus('PUT', url);
83
+ // The padded cache-control to mimic stale-while-revalidate
84
+ request.headers.set('cache-control', generateDefaultCacheControlHeader(getCacheControlSetting(cacheControl, {
85
+ maxAge: (cacheControl.maxAge || 0) + (cacheControl.staleWhileRevalidate || 0),
86
+ })));
87
+ // The cache-control we want to set on response
88
+ const cacheControlString = generateDefaultCacheControlHeader(getCacheControlSetting(cacheControl));
89
+ // CF will override cache-control, so we need to keep a
90
+ // non-modified real-cache-control
91
+ response.headers.set('cache-control', cacheControlString);
92
+ response.headers.set('real-cache-control', cacheControlString);
93
+ response.headers.set('cache-put-date', new Date().toUTCString());
94
+ logCacheApiStatus('PUT', request.url);
102
95
  await cache.put(request, response);
103
96
  }
104
- export async function deleteItemFromCache(key) {
97
+ export async function deleteItemFromCache(request) {
105
98
  const cache = getCache();
106
99
  if (!cache)
107
100
  return;
108
- const url = getKeyUrl(hashKey(key));
109
- const request = new Request(url);
110
- logCacheApiStatus('DELETE', url);
101
+ logCacheApiStatus('DELETE', request.url);
111
102
  await cache.delete(request);
112
103
  }
113
104
  /**
114
105
  * Manually check the response to see if it's stale.
115
106
  */
116
- export function isStale(response, userCacheOptions) {
117
- const responseMaxAge = getCacheControlSetting(userCacheOptions).maxAge || 0;
107
+ export function isStale(request, response) {
118
108
  const responseDate = response.headers.get('cache-put-date');
119
- if (!responseDate)
109
+ const cacheControl = response.headers.get('real-cache-control');
110
+ let responseMaxAge = 0;
111
+ if (cacheControl) {
112
+ const maxAgeMatch = cacheControl.match(/max-age=(\d*)/);
113
+ if (maxAgeMatch && maxAgeMatch.length > 1) {
114
+ responseMaxAge = parseFloat(maxAgeMatch[1]);
115
+ }
116
+ }
117
+ if (!responseDate) {
120
118
  return false;
119
+ }
121
120
  const ageInMs = new Date().valueOf() - new Date(responseDate).valueOf();
122
121
  const age = ageInMs / 1000;
123
- return age > responseMaxAge;
122
+ const result = age > responseMaxAge;
123
+ if (result) {
124
+ logCacheApiStatus('STALE', request.url);
125
+ }
126
+ return result;
124
127
  }