@shopify/hydrogen 0.13.1 → 0.15.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 (169) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/esnext/client.d.ts +3 -0
  3. package/dist/esnext/client.js +3 -0
  4. package/dist/esnext/components/CartProvider/CartProvider.client.js +23 -0
  5. package/dist/esnext/components/DevTools.d.ts +1 -0
  6. package/dist/esnext/components/DevTools.js +128 -0
  7. package/dist/esnext/components/Link/Link.client.d.ts +6 -0
  8. package/dist/esnext/components/Link/Link.client.js +85 -3
  9. package/dist/esnext/components/LocalizationProvider/LocalizationContext.client.d.ts +1 -1
  10. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.d.ts +16 -0
  11. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.js +7 -2
  12. package/dist/esnext/components/Seo/DefaultPageSeo.client.js +1 -2
  13. package/dist/esnext/constants.d.ts +2 -0
  14. package/dist/esnext/constants.js +2 -0
  15. package/dist/esnext/entry-client.js +20 -4
  16. package/dist/esnext/entry-server.d.ts +1 -1
  17. package/dist/esnext/entry-server.js +42 -23
  18. package/dist/esnext/foundation/Analytics/Analytics.client.d.ts +3 -0
  19. package/dist/esnext/foundation/Analytics/Analytics.client.js +28 -0
  20. package/dist/esnext/foundation/Analytics/Analytics.server.d.ts +1 -0
  21. package/dist/esnext/foundation/Analytics/Analytics.server.js +38 -0
  22. package/dist/esnext/foundation/Analytics/ClientAnalytics.d.ts +24 -0
  23. package/dist/esnext/foundation/Analytics/ClientAnalytics.js +91 -0
  24. package/dist/esnext/foundation/Analytics/ServerAnalyticsRoute.server.d.ts +2 -0
  25. package/dist/esnext/foundation/Analytics/ServerAnalyticsRoute.server.js +33 -0
  26. package/dist/esnext/foundation/Analytics/const.d.ts +8 -0
  27. package/dist/esnext/foundation/Analytics/const.js +8 -0
  28. package/dist/esnext/foundation/Analytics/hook.d.ts +1 -0
  29. package/dist/esnext/foundation/Analytics/hook.js +7 -0
  30. package/dist/esnext/foundation/Analytics/index.d.ts +2 -0
  31. package/dist/esnext/foundation/Analytics/index.js +2 -0
  32. package/dist/esnext/foundation/Analytics/types.d.ts +5 -0
  33. package/dist/esnext/{components/LocalizationProvider/LocalizationQuery.js → foundation/Analytics/types.js} +0 -0
  34. package/dist/esnext/foundation/Analytics/utils.d.ts +1 -0
  35. package/dist/esnext/foundation/Analytics/utils.js +8 -0
  36. package/dist/esnext/foundation/Boomerang/Boomerang.client.js +3 -1
  37. package/dist/esnext/foundation/Route/Route.server.js +4 -0
  38. package/dist/esnext/foundation/Router/BrowserRouter.client.js +68 -15
  39. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
  40. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.d.ts +6 -1
  41. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.js +27 -22
  42. package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.js +4 -1
  43. package/dist/esnext/foundation/ShopifyProvider/types.d.ts +5 -6
  44. package/dist/esnext/foundation/constants.d.ts +1 -1
  45. package/dist/esnext/foundation/constants.js +1 -1
  46. package/dist/esnext/foundation/fetchSync/client/fetchSync.d.ts +10 -0
  47. package/dist/esnext/foundation/fetchSync/client/fetchSync.js +27 -0
  48. package/dist/esnext/foundation/fetchSync/server/fetchSync.d.ts +8 -0
  49. package/dist/esnext/foundation/fetchSync/server/fetchSync.js +27 -0
  50. package/dist/esnext/foundation/fetchSync/types.d.ts +5 -0
  51. package/dist/esnext/foundation/fetchSync/types.js +1 -0
  52. package/dist/esnext/foundation/useQuery/hooks.d.ts +4 -2
  53. package/dist/esnext/foundation/useQuery/hooks.js +10 -6
  54. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  55. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +14 -7
  56. package/dist/esnext/framework/cache/in-memory.js +5 -5
  57. package/dist/esnext/framework/cache.d.ts +1 -10
  58. package/dist/esnext/framework/cache.js +67 -30
  59. package/dist/esnext/framework/plugin.js +10 -0
  60. package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +1 -1
  61. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +9 -2
  62. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.d.ts +1 -1
  63. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +1 -1
  64. package/dist/esnext/hooks/useShopQuery/hooks.js +47 -16
  65. package/dist/esnext/index.d.ts +2 -0
  66. package/dist/esnext/index.js +2 -0
  67. package/dist/esnext/storefront-api-types.d.ts +150 -3
  68. package/dist/esnext/storefront-api-types.js +16 -0
  69. package/dist/esnext/types.d.ts +6 -1
  70. package/dist/esnext/utilities/apiRoutes.d.ts +1 -1
  71. package/dist/esnext/utilities/apiRoutes.js +3 -2
  72. package/dist/esnext/utilities/hash.d.ts +2 -0
  73. package/dist/esnext/utilities/hash.js +7 -0
  74. package/dist/esnext/utilities/log/log-cache-api-status.js +1 -1
  75. package/dist/esnext/utilities/log/log-cache-header.js +1 -1
  76. package/dist/esnext/utilities/log/log-query-timeline.js +1 -1
  77. package/dist/esnext/utilities/suspense.d.ts +5 -0
  78. package/dist/esnext/utilities/suspense.js +32 -0
  79. package/dist/esnext/utilities/template.js +1 -1
  80. package/dist/esnext/version.d.ts +1 -1
  81. package/dist/esnext/version.js +1 -1
  82. package/dist/node/constants.d.ts +2 -0
  83. package/dist/node/constants.js +3 -1
  84. package/dist/node/entry-server.d.ts +1 -1
  85. package/dist/node/entry-server.js +41 -25
  86. package/dist/node/foundation/Analytics/Analytics.client.d.ts +3 -0
  87. package/dist/node/foundation/Analytics/Analytics.client.js +32 -0
  88. package/dist/node/foundation/Analytics/Analytics.server.d.ts +1 -0
  89. package/dist/node/foundation/Analytics/Analytics.server.js +45 -0
  90. package/dist/node/foundation/Analytics/ClientAnalytics.d.ts +24 -0
  91. package/dist/node/foundation/Analytics/ClientAnalytics.js +94 -0
  92. package/dist/node/foundation/Analytics/ServerAnalyticsRoute.server.d.ts +2 -0
  93. package/dist/node/foundation/Analytics/ServerAnalyticsRoute.server.js +37 -0
  94. package/dist/node/foundation/Analytics/const.d.ts +8 -0
  95. package/dist/node/foundation/Analytics/const.js +11 -0
  96. package/dist/node/foundation/Analytics/hook.d.ts +1 -0
  97. package/dist/node/foundation/Analytics/hook.js +11 -0
  98. package/dist/node/foundation/Analytics/index.d.ts +2 -0
  99. package/dist/node/foundation/Analytics/index.js +7 -0
  100. package/dist/node/foundation/Analytics/types.d.ts +5 -0
  101. package/dist/node/foundation/Analytics/types.js +2 -0
  102. package/dist/node/foundation/Analytics/utils.d.ts +1 -0
  103. package/dist/node/foundation/Analytics/utils.js +12 -0
  104. package/dist/node/foundation/Router/BrowserRouter.client.js +67 -14
  105. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +2 -2
  106. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.d.ts +6 -1
  107. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.js +27 -22
  108. package/dist/node/foundation/ShopifyProvider/types.d.ts +5 -6
  109. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  110. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +16 -9
  111. package/dist/node/framework/cache/in-memory.js +5 -5
  112. package/dist/node/framework/cache.d.ts +1 -10
  113. package/dist/node/framework/cache.js +71 -36
  114. package/dist/node/framework/plugin.js +10 -0
  115. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +1 -1
  116. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +9 -2
  117. package/dist/node/storefront-api-types.d.ts +150 -3
  118. package/dist/node/storefront-api-types.js +17 -1
  119. package/dist/node/types.d.ts +6 -1
  120. package/dist/node/utilities/apiRoutes.d.ts +1 -1
  121. package/dist/node/utilities/apiRoutes.js +3 -2
  122. package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +6 -0
  123. package/dist/node/utilities/flattenConnection/flattenConnection.js +15 -0
  124. package/dist/node/utilities/flattenConnection/index.d.ts +1 -0
  125. package/dist/node/utilities/flattenConnection/index.js +5 -0
  126. package/dist/node/utilities/hash.d.ts +2 -0
  127. package/dist/node/utilities/hash.js +11 -0
  128. package/dist/node/utilities/image_size.d.ts +30 -0
  129. package/dist/node/utilities/image_size.js +110 -0
  130. package/dist/node/utilities/index.d.ts +11 -0
  131. package/dist/node/utilities/index.js +32 -0
  132. package/dist/node/utilities/isClient/index.d.ts +1 -0
  133. package/dist/node/utilities/isClient/index.js +5 -0
  134. package/dist/node/utilities/isClient/isClient.d.ts +4 -0
  135. package/dist/node/utilities/isClient/isClient.js +10 -0
  136. package/dist/node/utilities/isServer/index.d.ts +1 -0
  137. package/dist/node/utilities/isServer/index.js +5 -0
  138. package/dist/node/utilities/isServer/isServer.d.ts +4 -0
  139. package/dist/node/utilities/isServer/isServer.js +11 -0
  140. package/dist/node/utilities/load_script.d.ts +3 -0
  141. package/dist/node/utilities/load_script.js +27 -0
  142. package/dist/node/utilities/log/log-cache-api-status.js +1 -1
  143. package/dist/node/utilities/log/log-cache-header.js +2 -2
  144. package/dist/node/utilities/log/log-query-timeline.js +2 -2
  145. package/dist/node/utilities/measurement.d.ts +3 -0
  146. package/dist/node/utilities/measurement.js +103 -0
  147. package/dist/node/utilities/parseMetafieldValue/index.d.ts +1 -0
  148. package/dist/node/utilities/parseMetafieldValue/index.js +5 -0
  149. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +6 -0
  150. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +39 -0
  151. package/dist/node/utilities/suspense.d.ts +12 -0
  152. package/dist/node/utilities/suspense.js +64 -0
  153. package/dist/node/utilities/template.js +1 -1
  154. package/dist/node/utilities/video_parameters.d.ts +47 -0
  155. package/dist/node/utilities/video_parameters.js +27 -0
  156. package/dist/node/version.d.ts +1 -1
  157. package/dist/node/version.js +1 -1
  158. package/package.json +3 -3
  159. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +9 -21
  160. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.development.server.js +51 -47
  161. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.production.min.server.js +30 -29
  162. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.development.server.js +51 -47
  163. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.production.min.server.js +17 -17
  164. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-client-proxy.js +55 -45
  165. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +9 -21
  166. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js +51 -47
  167. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.node.server.js +51 -47
  168. package/vendor/react-server-dom-vite/package.json +3 -3
  169. package/dist/esnext/components/LocalizationProvider/LocalizationQuery.d.ts +0 -23
@@ -14,34 +14,39 @@ export function ServerStateProvider({ serverState, setServerState, children, })
14
14
  * the `pending` flag also provided by the hook to display in the UI.
15
15
  */
16
16
  startTransition(() => {
17
- return setServerState((prev) => {
18
- let newValue;
19
- if (typeof input === 'function') {
20
- newValue = input(prev);
21
- }
22
- else if (typeof input === 'string') {
23
- newValue = { [input]: propValue };
24
- }
25
- else {
26
- newValue = input;
27
- }
28
- if (__DEV__) {
29
- const privateProp = PRIVATE_PROPS.find((prop) => prop in newValue);
30
- if (privateProp) {
31
- console.warn(`Custom "${privateProp}" property in server state is ignored. Use a different name.`);
32
- }
33
- }
34
- return {
35
- ...prev,
36
- ...newValue,
37
- };
38
- });
17
+ return setServerState((prev) => getNewServerState(prev, input, propValue));
39
18
  });
40
19
  }, [setServerState, startTransition]);
20
+ const getProposedServerStateCallback = useCallback((input, propValue) => {
21
+ return getNewServerState(serverState, input, propValue);
22
+ }, [serverState]);
23
+ function getNewServerState(prev, input, propValue) {
24
+ let newValue;
25
+ if (typeof input === 'function') {
26
+ newValue = input(prev);
27
+ }
28
+ else if (typeof input === 'string') {
29
+ newValue = { [input]: propValue };
30
+ }
31
+ else {
32
+ newValue = input;
33
+ }
34
+ if (__DEV__) {
35
+ const privateProp = PRIVATE_PROPS.find((prop) => prop in newValue);
36
+ if (privateProp) {
37
+ console.warn(`Custom "${privateProp}" property in server state is ignored. Use a different name.`);
38
+ }
39
+ }
40
+ return {
41
+ ...prev,
42
+ ...newValue,
43
+ };
44
+ }
41
45
  const value = useMemo(() => ({
42
46
  pending,
43
47
  serverState,
44
48
  setServerState: setServerStateCallback,
49
+ getProposedServerState: getProposedServerStateCallback,
45
50
  }), [serverState, setServerStateCallback, pending]);
46
51
  return (React.createElement(ServerStateContext.Provider, { value: value }, children));
47
52
  }
@@ -4,8 +4,11 @@ import { DEFAULT_LOCALE } from '../constants';
4
4
  import { useServerRequest } from '../ServerRequestProvider';
5
5
  function makeShopifyContext(shopifyConfig) {
6
6
  var _a, _b;
7
+ const locale = (_a = shopifyConfig.defaultLocale) !== null && _a !== void 0 ? _a : DEFAULT_LOCALE;
8
+ const languageCode = locale.split(/[-_]/)[0];
7
9
  return {
8
- locale: (_a = shopifyConfig.defaultLocale) !== null && _a !== void 0 ? _a : DEFAULT_LOCALE,
10
+ locale: locale.toUpperCase(),
11
+ languageCode: languageCode.toUpperCase(),
9
12
  storeDomain: (_b = shopifyConfig === null || shopifyConfig === void 0 ? void 0 : shopifyConfig.storeDomain) === null || _b === void 0 ? void 0 : _b.replace(/^https?:\/\//, ''),
10
13
  storefrontToken: shopifyConfig.storefrontToken,
11
14
  storefrontApiVersion: shopifyConfig.storefrontApiVersion,
@@ -1,11 +1,10 @@
1
+ import type { CountryCode, LanguageCode } from '../../storefront-api-types';
1
2
  import type { ReactNode } from 'react';
2
3
  import type { ShopifyConfig } from '../../types';
3
- export declare type ShopifyContextValue = {
4
- locale: string;
5
- storeDomain: ShopifyConfig['storeDomain'];
6
- storefrontToken: ShopifyConfig['storefrontToken'];
7
- storefrontApiVersion: string;
8
- };
4
+ export interface ShopifyContextValue extends Omit<ShopifyConfig, 'defaultLocale'> {
5
+ locale: `${LanguageCode}-${CountryCode}`;
6
+ languageCode: `${LanguageCode}`;
7
+ }
9
8
  export declare type ShopifyProviderProps = {
10
9
  /** The contents of the `shopify.config.js` file. */
11
10
  shopifyConfig: ShopifyConfig;
@@ -1 +1 @@
1
- export declare const DEFAULT_LOCALE = "en-us";
1
+ export declare const DEFAULT_LOCALE = "EN-US";
@@ -1,3 +1,3 @@
1
1
  // Note: do not mix this export with other app-only logic
2
2
  // to avoid importing unnecessary code in the plugins.
3
- export const DEFAULT_LOCALE = 'en-us';
3
+ export const DEFAULT_LOCALE = 'EN-US';
@@ -0,0 +1,10 @@
1
+ import type { FetchResponse } from '../types';
2
+ /**
3
+ * Fetch a URL for use in a client component Suspense boundary.
4
+ */
5
+ export declare function fetchSync(url: string, options?: RequestInit): FetchResponse;
6
+ /**
7
+ * Preload a URL for use in a client component Suspense boundary.
8
+ * Useful for placing higher in the tree to avoid waterfalls.
9
+ */
10
+ export declare function preload(url: string, options?: RequestInit): void;
@@ -0,0 +1,27 @@
1
+ import { suspendFunction, preloadFunction } from '../../../utilities/suspense';
2
+ /**
3
+ * Fetch a URL for use in a client component Suspense boundary.
4
+ */
5
+ export function fetchSync(url, options) {
6
+ const [text, response] = suspendFunction([url, options], async () => {
7
+ const response = await globalThis.fetch(url, options);
8
+ const text = await response.text();
9
+ return [text, response];
10
+ });
11
+ return {
12
+ response,
13
+ json: () => JSON.parse(text),
14
+ text: () => text,
15
+ };
16
+ }
17
+ /**
18
+ * Preload a URL for use in a client component Suspense boundary.
19
+ * Useful for placing higher in the tree to avoid waterfalls.
20
+ */
21
+ export function preload(url, options) {
22
+ preloadFunction([url, options], async () => {
23
+ const response = await globalThis.fetch(url, options);
24
+ const text = await response.text();
25
+ return [text, response];
26
+ });
27
+ }
@@ -0,0 +1,8 @@
1
+ import { type HydrogenUseQueryOptions } from '../../useQuery/hooks';
2
+ import type { FetchResponse } from '../types';
3
+ /**
4
+ * The `fetchSync` hook makes third-party API requests and is the recommended way to make simple fetch calls on the server.
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
+ * that supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html).
7
+ */
8
+ export declare function fetchSync(url: string, options?: Omit<RequestInit, 'cache'> & HydrogenUseQueryOptions): FetchResponse;
@@ -0,0 +1,27 @@
1
+ import { useQuery } from '../../useQuery/hooks';
2
+ /**
3
+ * The `fetchSync` hook makes third-party API requests and is the recommended way to make simple fetch calls on the server.
4
+ * It's designed similar to the [Web API's `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), only in a way
5
+ * that supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html).
6
+ */
7
+ export function fetchSync(url, options) {
8
+ const { cache, preload, shouldCacheResponse, ...requestInit } = options !== null && options !== void 0 ? options : {};
9
+ const { data: useQueryResponse, error } = useQuery([url, requestInit], async () => {
10
+ const response = await globalThis.fetch(url, requestInit);
11
+ const text = await response.text();
12
+ return [text, response];
13
+ }, {
14
+ cache,
15
+ preload,
16
+ shouldCacheResponse,
17
+ });
18
+ if (error) {
19
+ throw error;
20
+ }
21
+ const [data, response] = useQueryResponse;
22
+ return {
23
+ response,
24
+ json: () => JSON.parse(data),
25
+ text: () => data,
26
+ };
27
+ }
@@ -0,0 +1,5 @@
1
+ export interface FetchResponse {
2
+ response: Response;
3
+ json: () => any;
4
+ text: () => any;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -15,9 +15,11 @@ export interface HydrogenUseQueryOptions {
15
15
  }
16
16
  /**
17
17
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
18
- * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). It's based
19
- * on [react-query](https://react-query.tanstack.com/reference/useQuery). You can use this
18
+ * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). You can use this
20
19
  * hook to call any third-party APIs from a server component.
20
+ *
21
+ * \> Note:
22
+ * \> If you're making a simple fetch call on the server, then we recommend using the [`fetchSync`](/api/hydrogen/hooks/global/fetchsync) hook instead.
21
23
  */
22
24
  export declare function useQuery<T>(
23
25
  /** A string or array to uniquely identify the current query. */
@@ -1,12 +1,15 @@
1
- import { getLoggerWithContext, collectQueryCacheControlHeaders, collectQueryTimings, } from '../../utilities/log';
1
+ import { getLoggerWithContext, collectQueryCacheControlHeaders, collectQueryTimings, logCacheApiStatus, } from '../../utilities/log';
2
2
  import { deleteItemFromCache, generateSubRequestCacheControlHeader, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache';
3
+ import { hashKey } from '../../utilities/hash';
3
4
  import { runDelayedFunction } from '../../framework/runtime';
4
5
  import { useRequestCacheData, useServerRequest } from '../ServerRequestProvider';
5
6
  /**
6
7
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
7
- * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). It's based
8
- * on [react-query](https://react-query.tanstack.com/reference/useQuery). You can use this
8
+ * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). You can use this
9
9
  * hook to call any third-party APIs from a server component.
10
+ *
11
+ * \> Note:
12
+ * \> If you're making a simple fetch call on the server, then we recommend using the [`fetchSync`](/api/hydrogen/hooks/global/fetchsync) hook instead.
10
13
  */
11
14
  export function useQuery(
12
15
  /** A string or array to uniquely identify the current query. */
@@ -45,6 +48,7 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
45
48
  // to prevent losing the current React cycle.
46
49
  const request = useServerRequest();
47
50
  const log = getLoggerWithContext(request);
51
+ const hashedKey = hashKey(key);
48
52
  const cacheResponse = await getItemFromCache(key);
49
53
  async function generateNewOutput() {
50
54
  return await queryFn();
@@ -55,11 +59,11 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
55
59
  /**
56
60
  * Important: Do this async
57
61
  */
58
- if (isStale(response)) {
59
- log.debug('[useQuery] cache stale; generating new response in background');
62
+ if (isStale(response, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache)) {
63
+ logCacheApiStatus('STALE', hashedKey);
60
64
  const lockKey = `lock-${key}`;
61
65
  runDelayedFunction(async () => {
62
- log.debug(`[stale regen] fetching cache lock`);
66
+ logCacheApiStatus('UPDATING', hashedKey);
63
67
  const lockExists = await getItemFromCache(lockKey);
64
68
  if (lockExists)
65
69
  return;
@@ -34,6 +34,7 @@ export declare class ServerComponentRequest extends Request {
34
34
  queryCacheControl: Array<QueryCacheControlHeaders>;
35
35
  queryTimings: Array<QueryTiming>;
36
36
  preloadQueries: PreloadQueriesByURL;
37
+ analyticsData: any;
37
38
  router: RouterContextData;
38
39
  buyerIpHeader?: string;
39
40
  [key: string]: any;
@@ -1,5 +1,5 @@
1
1
  import { getTime } from '../../utilities/timing';
2
- import { hashKey } from '../cache';
2
+ import { hashKey } from '../../utilities/hash';
3
3
  import { HelmetData as HeadData } from 'react-helmet-async';
4
4
  import { RSC_PATHNAME } from '../../constants';
5
5
  let reqCounter = 0; // For debugging
@@ -27,8 +27,11 @@ export class ServerComponentRequest extends Request {
27
27
  else {
28
28
  super(getUrlFromNodeRequest(input), getInitFromNodeRequest(input));
29
29
  }
30
+ const referer = this.headers.get('referer');
30
31
  this.time = getTime();
31
32
  this.id = generateId();
33
+ this.preloadURL =
34
+ this.isRscRequest() && referer && referer !== '' ? referer : this.url;
32
35
  this.ctx = {
33
36
  cache: new Map(),
34
37
  head: new HeadData({}),
@@ -39,18 +42,21 @@ export class ServerComponentRequest extends Request {
39
42
  },
40
43
  queryCacheControl: [],
41
44
  queryTimings: [],
45
+ analyticsData: {
46
+ url: this.url,
47
+ normalizedRscUrl: this.preloadURL,
48
+ },
42
49
  preloadQueries: new Map(),
43
50
  };
44
51
  this.cookies = this.parseCookies();
45
- const referer = this.headers.get('referer');
46
- this.preloadURL =
47
- this.isRscRequest() && referer && referer !== '' ? referer : this.url;
48
52
  }
49
53
  parseCookies() {
50
54
  const cookieString = this.headers.get('cookie') || '';
51
55
  return new Map(cookieString
52
56
  .split(';')
53
- .map((chunk) => chunk.trim().split(/=(.+)/)));
57
+ .map((chunk) => chunk.trim())
58
+ .filter((chunk) => chunk !== '')
59
+ .map((chunk) => chunk.split(/=(.+)/)));
54
60
  }
55
61
  isRscRequest() {
56
62
  const url = new URL(this.url);
@@ -123,8 +129,9 @@ function getInitFromNodeRequest(request) {
123
129
  ? request.body
124
130
  : undefined,
125
131
  };
126
- if (!init.headers.has('x-forwarded-for')) {
127
- init.headers.set('x-forwarded-for', request.socket.remoteAddress);
132
+ const remoteAddress = request.socket.remoteAddress;
133
+ if (!init.headers.has('x-forwarded-for') && remoteAddress) {
134
+ init.headers.set('x-forwarded-for', remoteAddress);
128
135
  }
129
136
  return init;
130
137
  }
@@ -8,7 +8,7 @@ export class InMemoryCache {
8
8
  this.store = new Map();
9
9
  }
10
10
  put(request, response) {
11
- logCacheApiStatus('PUT', request.url);
11
+ logCacheApiStatus('PUT-dev', request.url);
12
12
  this.store.set(request.url, {
13
13
  value: response,
14
14
  date: new Date(),
@@ -18,7 +18,7 @@ export class InMemoryCache {
18
18
  var _a, _b;
19
19
  const match = this.store.get(request.url);
20
20
  if (!match) {
21
- logCacheApiStatus('MISS', request.url);
21
+ logCacheApiStatus('MISS-dev', request.url);
22
22
  return;
23
23
  }
24
24
  const { value, date } = match;
@@ -28,7 +28,7 @@ export class InMemoryCache {
28
28
  const age = (new Date().valueOf() - date.valueOf()) / 1000;
29
29
  const isMiss = age > maxAge + swr;
30
30
  if (isMiss) {
31
- logCacheApiStatus('MISS', request.url);
31
+ logCacheApiStatus('MISS-dev', request.url);
32
32
  this.store.delete(request.url);
33
33
  return;
34
34
  }
@@ -36,7 +36,7 @@ export class InMemoryCache {
36
36
  const headers = new Headers(value.headers);
37
37
  headers.set('cache', isStale ? 'STALE' : 'HIT');
38
38
  headers.set('date', date.toUTCString());
39
- logCacheApiStatus(headers.get('cache'), request.url);
39
+ logCacheApiStatus(`${headers.get('cache')}-dev`, request.url);
40
40
  const response = new Response(value.body, {
41
41
  headers,
42
42
  });
@@ -44,7 +44,7 @@ export class InMemoryCache {
44
44
  }
45
45
  delete(request) {
46
46
  this.store.delete(request.url);
47
- logCacheApiStatus('DELETE', request.url);
47
+ logCacheApiStatus('DELETE-dev', request.url);
48
48
  }
49
49
  keys(request) {
50
50
  const cacheKeys = [];
@@ -1,14 +1,5 @@
1
1
  import type { QueryKey, CachingStrategy } from '../types';
2
2
  export declare function generateSubRequestCacheControlHeader(userCacheOptions?: CachingStrategy): string;
3
- /**
4
- * Use a preview header during development.
5
- * TODO: Support an override of this to force the cache
6
- * header to be present during dev. ENV var maybe?
7
- */
8
- export declare function getCacheControlHeader({ dev }: {
9
- dev?: boolean;
10
- }): "cache-control-preview" | "cache-control";
11
- export declare function hashKey(key: QueryKey): string;
12
3
  /**
13
4
  * Get an item from the cache. If a match is found, returns a tuple
14
5
  * containing the `JSON.parse` version of the response as well
@@ -23,4 +14,4 @@ export declare function deleteItemFromCache(key: QueryKey): Promise<void>;
23
14
  /**
24
15
  * Manually check the response to see if it's stale.
25
16
  */
26
- export declare function isStale(response: Response): boolean;
17
+ export declare function isStale(response: Response, userCacheOptions?: CachingStrategy): boolean;
@@ -1,22 +1,20 @@
1
1
  import { getCache } from './runtime';
2
2
  import { CacheSeconds, generateCacheControlHeader, } from '../framework/CachingStrategy';
3
- export function generateSubRequestCacheControlHeader(userCacheOptions) {
4
- return generateCacheControlHeader(userCacheOptions || CacheSeconds());
5
- }
6
- /**
7
- * Use a preview header during development.
8
- * TODO: Support an override of this to force the cache
9
- * header to be present during dev. ENV var maybe?
10
- */
11
- export function getCacheControlHeader({ dev }) {
12
- return dev ? 'cache-control-preview' : 'cache-control';
3
+ import { hashKey } from '../utilities/hash';
4
+ import { logCacheApiStatus } from '../utilities/log';
5
+ function getCacheControlSetting(userCacheOptions, options) {
6
+ if (userCacheOptions && options) {
7
+ return {
8
+ ...userCacheOptions,
9
+ ...options,
10
+ };
11
+ }
12
+ else {
13
+ return userCacheOptions || CacheSeconds();
14
+ }
13
15
  }
14
- export function hashKey(key) {
15
- const rawKey = key instanceof Array ? key : [key];
16
- /**
17
- * TODO: Smarter hash
18
- */
19
- return rawKey.map((k) => JSON.stringify(k)).join('');
16
+ export function generateSubRequestCacheControlHeader(userCacheOptions) {
17
+ return generateCacheControlHeader(getCacheControlSetting(userCacheOptions));
20
18
  }
21
19
  /**
22
20
  * Cache API is weird. We just need a full URL, so we make one up.
@@ -37,8 +35,11 @@ export async function getItemFromCache(key) {
37
35
  const url = getKeyUrl(hashKey(key));
38
36
  const request = new Request(url);
39
37
  const response = await cache.match(request);
40
- if (!response)
38
+ if (!response) {
39
+ logCacheApiStatus('MISS', url);
41
40
  return;
41
+ }
42
+ logCacheApiStatus('HIT', url);
42
43
  return [await response.json(), response];
43
44
  }
44
45
  /**
@@ -51,14 +52,53 @@ export async function setItemInCache(key, value, userCacheOptions) {
51
52
  }
52
53
  const url = getKeyUrl(hashKey(key));
53
54
  const request = new Request(url);
55
+ /**
56
+ * We are manually managing staled request by adding this workaround.
57
+ * Why? cache control header support is dependent on hosting platform
58
+ *
59
+ * For example:
60
+ *
61
+ * Cloudflare's Cache API does not support `stale-while-revalidate`.
62
+ * Cloudflare cache control header has a very odd behaviour.
63
+ * Say we have the following cache control header on a request:
64
+ *
65
+ * public, max-age=15, stale-while-revalidate=30
66
+ *
67
+ * When there is a cache.match HIT, the cache control header would become
68
+ *
69
+ * public, max-age=14400, stale-while-revalidate=30
70
+ *
71
+ * == `stale-while-revalidate` workaround ==
72
+ * Update response max-age so that:
73
+ *
74
+ * max-age = max-age + stale-while-revalidate
75
+ *
76
+ * For example:
77
+ *
78
+ * public, max-age=1, stale-while-revalidate=9
79
+ * |
80
+ * V
81
+ * public, max-age=10, stale-while-revalidate=9
82
+ *
83
+ * Store the following information in the response header:
84
+ *
85
+ * cache-put-date - UTC time string of when this request is PUT into cache
86
+ *
87
+ * Note on `cache-put-date`: The `response.headers.get('date')` isn't static. I am
88
+ * not positive what date this is returning but it is never over 500 ms
89
+ * after subtracting from the current timestamp.
90
+ *
91
+ * `isStale` function will use the above information to test for stale-ness of a cached response
92
+ */
93
+ const cacheControl = getCacheControlSetting(userCacheOptions);
54
94
  const headers = new Headers({
55
- 'cache-control': generateSubRequestCacheControlHeader(userCacheOptions),
95
+ 'cache-control': generateSubRequestCacheControlHeader(getCacheControlSetting(cacheControl, {
96
+ maxAge: (cacheControl.maxAge || 0) + (cacheControl.staleWhileRevalidate || 0),
97
+ })),
98
+ 'cache-put-date': new Date().toUTCString(),
56
99
  });
57
100
  const response = new Response(JSON.stringify(value), { headers });
58
- /**
59
- * WARNING: Cloudflare's Cache API does not support `stale-while-revalidate`
60
- * so this implementation will not work as expected on that platform.
61
- */
101
+ logCacheApiStatus('PUT', url);
62
102
  await cache.put(request, response);
63
103
  }
64
104
  export async function deleteItemFromCache(key) {
@@ -67,20 +107,17 @@ export async function deleteItemFromCache(key) {
67
107
  return;
68
108
  const url = getKeyUrl(hashKey(key));
69
109
  const request = new Request(url);
110
+ logCacheApiStatus('DELETE', url);
70
111
  await cache.delete(request);
71
112
  }
72
113
  /**
73
114
  * Manually check the response to see if it's stale.
74
115
  */
75
- export function isStale(response) {
76
- const responseDate = response.headers.get('date');
77
- const responseCacheControl = response.headers.get('cache-control');
78
- if (!responseDate || !responseCacheControl)
79
- return false;
80
- const responseMaxAgeMatch = responseCacheControl.match(/max-age=(\d+)/);
81
- if (!responseMaxAgeMatch)
116
+ export function isStale(response, userCacheOptions) {
117
+ const responseMaxAge = getCacheControlSetting(userCacheOptions).maxAge || 0;
118
+ const responseDate = response.headers.get('cache-put-date');
119
+ if (!responseDate)
82
120
  return false;
83
- const responseMaxAge = parseInt(responseMaxAgeMatch[1]);
84
121
  const ageInMs = new Date().valueOf() - new Date(responseDate).valueOf();
85
122
  const age = ageInMs / 1000;
86
123
  return age > responseMaxAge;
@@ -12,6 +12,15 @@ import react from '@vitejs/plugin-react';
12
12
  import path from 'path';
13
13
  import cssModulesRsc from './plugins/vite-plugin-css-modules-rsc';
14
14
  export default (shopifyConfig, pluginOptions = {}) => {
15
+ let hydrogenUiPath;
16
+ try {
17
+ hydrogenUiPath = path.join(
18
+ // eslint-disable-next-line node/no-missing-require
19
+ path.dirname(require.resolve('@shopify/hydrogen-ui/client')));
20
+ }
21
+ catch (error) {
22
+ // hydrogen-ui isn't installed, so don't worry about it
23
+ }
15
24
  return [
16
25
  process.env.VITE_INSPECT && inspect(),
17
26
  hydrogenConfig(),
@@ -24,6 +33,7 @@ export default (shopifyConfig, pluginOptions = {}) => {
24
33
  rsc({
25
34
  clientComponentPaths: [
26
35
  path.join(path.dirname(require.resolve('@shopify/hydrogen/package.json'))),
36
+ ...[hydrogenUiPath].filter(Boolean),
27
37
  ],
28
38
  isServerComponentImporterAllowed(importer, source) {
29
39
  // Always allow the entry server (e.g. App.server.jsx) to be imported
@@ -20,7 +20,7 @@ export default function cssModulesRsc() {
20
20
  enforce: 'post',
21
21
  transform(code, id) {
22
22
  if (id.includes('.module.') && cssMap.has(id)) {
23
- return code.replace(/export default .*$/gms, `import React from 'react'; export const StyleTag = () => React.createElement('style', {}, \`${cssMap.get(id)}\`);`);
23
+ return code.replace(/export default .*$/gms, `import React from 'react'; export const StyleTag = () => React.createElement('style', {dangerouslySetInnerHTML: {__html: ${JSON.stringify(cssMap.get(id))}}});`);
24
24
  }
25
25
  },
26
26
  },
@@ -36,11 +36,18 @@ export default () => {
36
36
  // Reload when updating local Hydrogen lib
37
37
  server: process.env.LOCAL_DEV && {
38
38
  watch: {
39
- ignored: ['!**/node_modules/@shopify/hydrogen/**'],
39
+ ignored: [
40
+ '!**/node_modules/@shopify/hydrogen/**',
41
+ '!**/node_modules/@shopify/hydrogen-ui/**',
42
+ ],
40
43
  },
41
44
  },
42
45
  optimizeDeps: {
43
- exclude: ['@shopify/hydrogen/client', '@shopify/hydrogen/entry-client'],
46
+ exclude: [
47
+ '@shopify/hydrogen/client',
48
+ '@shopify/hydrogen/entry-client',
49
+ '@shopify/hydrogen-ui',
50
+ ],
44
51
  include: [
45
52
  /**
46
53
  * Additionally, the following dependencies have trouble loading the
@@ -15,7 +15,7 @@ metafields?: PartialDeep<MetafieldConnection>): {
15
15
  key?: string | undefined;
16
16
  namespace?: string | undefined;
17
17
  parentResource?: import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").ProductVariant> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Product> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Blog> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Article> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Customer> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Order> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Collection> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Page> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Shop> | undefined;
18
- reference?: import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").MediaImage> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").ProductVariant> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Product> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Page> | null | undefined;
18
+ reference?: import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Video> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").MediaImage> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").ProductVariant> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Product> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Page> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").GenericFile> | null | undefined;
19
19
  type?: string | undefined;
20
20
  updatedAt?: string | undefined;
21
21
  }[];
@@ -8,7 +8,7 @@ export interface UseShopQueryResponse<T> {
8
8
  /**
9
9
  * The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API. It must be a descendent of a `ShopifyProvider` component.
10
10
  */
11
- export declare function useShopQuery<T>({ query, variables, cache, locale, preload, }: {
11
+ export declare function useShopQuery<T>({ query, variables, cache, preload, }: {
12
12
  /** A string of the GraphQL query.
13
13
  * If no query is provided, useShopQuery will make no calls to the Storefront API.
14
14
  */