@shopify/hydrogen 0.14.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/esnext/client.d.ts +4 -0
  3. package/dist/esnext/client.js +4 -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.js +1 -1
  8. package/dist/esnext/constants.d.ts +6 -0
  9. package/dist/esnext/constants.js +6 -0
  10. package/dist/esnext/entry-client.js +7 -4
  11. package/dist/esnext/entry-server.d.ts +1 -1
  12. package/dist/esnext/entry-server.js +29 -15
  13. package/dist/esnext/foundation/Analytics/Analytics.client.d.ts +3 -0
  14. package/dist/esnext/foundation/Analytics/Analytics.client.js +28 -0
  15. package/dist/esnext/foundation/Analytics/Analytics.server.d.ts +1 -0
  16. package/dist/esnext/foundation/Analytics/Analytics.server.js +38 -0
  17. package/dist/esnext/foundation/Analytics/ClientAnalytics.d.ts +24 -0
  18. package/dist/esnext/foundation/Analytics/ClientAnalytics.js +91 -0
  19. package/dist/esnext/foundation/Analytics/ServerAnalyticsRoute.server.d.ts +2 -0
  20. package/dist/esnext/foundation/Analytics/ServerAnalyticsRoute.server.js +33 -0
  21. package/dist/esnext/foundation/Analytics/const.d.ts +8 -0
  22. package/dist/esnext/foundation/Analytics/const.js +8 -0
  23. package/dist/esnext/foundation/Analytics/hook.d.ts +1 -0
  24. package/dist/esnext/foundation/Analytics/hook.js +7 -0
  25. package/dist/esnext/foundation/Analytics/index.d.ts +2 -0
  26. package/dist/esnext/foundation/Analytics/index.js +2 -0
  27. package/dist/esnext/foundation/Analytics/types.d.ts +5 -0
  28. package/dist/esnext/foundation/Analytics/types.js +1 -0
  29. package/dist/esnext/foundation/Analytics/utils.d.ts +1 -0
  30. package/dist/esnext/foundation/Analytics/utils.js +8 -0
  31. package/dist/esnext/foundation/Boomerang/Boomerang.client.js +3 -1
  32. package/dist/esnext/foundation/Route/Route.server.js +4 -0
  33. package/dist/esnext/foundation/Router/BrowserRouter.client.js +68 -15
  34. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
  35. package/dist/esnext/foundation/ShopifyProvider/types.d.ts +2 -5
  36. package/dist/esnext/foundation/fetchSync/client/fetchSync.d.ts +10 -0
  37. package/dist/esnext/foundation/fetchSync/client/fetchSync.js +27 -0
  38. package/dist/esnext/foundation/fetchSync/server/fetchSync.d.ts +8 -0
  39. package/dist/esnext/foundation/fetchSync/server/fetchSync.js +27 -0
  40. package/dist/esnext/foundation/fetchSync/types.d.ts +5 -0
  41. package/dist/esnext/foundation/fetchSync/types.js +1 -0
  42. package/dist/esnext/foundation/useQuery/hooks.d.ts +4 -2
  43. package/dist/esnext/foundation/useQuery/hooks.js +10 -6
  44. package/dist/esnext/foundation/useUrl/useUrl.js +8 -1
  45. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  46. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +11 -5
  47. package/dist/esnext/framework/cache/in-memory.js +5 -5
  48. package/dist/esnext/framework/cache.d.ts +1 -2
  49. package/dist/esnext/framework/cache.js +67 -22
  50. package/dist/esnext/framework/plugin.js +10 -0
  51. package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +1 -1
  52. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +11 -2
  53. package/dist/esnext/hooks/useShopQuery/hooks.js +32 -25
  54. package/dist/esnext/index.d.ts +2 -0
  55. package/dist/esnext/index.js +2 -0
  56. package/dist/esnext/types.d.ts +6 -1
  57. package/dist/esnext/utilities/apiRoutes.d.ts +2 -3
  58. package/dist/esnext/utilities/apiRoutes.js +14 -9
  59. package/dist/esnext/utilities/hash.d.ts +2 -0
  60. package/dist/esnext/utilities/hash.js +7 -0
  61. package/dist/esnext/utilities/log/log-cache-api-status.js +1 -1
  62. package/dist/esnext/utilities/log/log-cache-header.js +1 -1
  63. package/dist/esnext/utilities/log/log-query-timeline.js +1 -1
  64. package/dist/esnext/utilities/storefrontApi.d.ts +4 -0
  65. package/dist/esnext/utilities/storefrontApi.js +21 -0
  66. package/dist/esnext/utilities/suspense.d.ts +5 -0
  67. package/dist/esnext/utilities/suspense.js +32 -0
  68. package/dist/esnext/utilities/template.js +1 -1
  69. package/dist/esnext/version.d.ts +1 -1
  70. package/dist/esnext/version.js +1 -1
  71. package/dist/node/constants.d.ts +6 -0
  72. package/dist/node/constants.js +7 -1
  73. package/dist/node/entry-server.d.ts +1 -1
  74. package/dist/node/entry-server.js +28 -17
  75. package/dist/node/foundation/Analytics/Analytics.client.d.ts +3 -0
  76. package/dist/node/foundation/Analytics/Analytics.client.js +32 -0
  77. package/dist/node/foundation/Analytics/Analytics.server.d.ts +1 -0
  78. package/dist/node/foundation/Analytics/Analytics.server.js +45 -0
  79. package/dist/node/foundation/Analytics/ClientAnalytics.d.ts +24 -0
  80. package/dist/node/foundation/Analytics/ClientAnalytics.js +94 -0
  81. package/dist/node/foundation/Analytics/ServerAnalyticsRoute.server.d.ts +2 -0
  82. package/dist/node/foundation/Analytics/ServerAnalyticsRoute.server.js +37 -0
  83. package/dist/node/foundation/Analytics/const.d.ts +8 -0
  84. package/dist/node/foundation/Analytics/const.js +11 -0
  85. package/dist/node/foundation/Analytics/hook.d.ts +1 -0
  86. package/dist/node/foundation/Analytics/hook.js +11 -0
  87. package/dist/node/foundation/Analytics/index.d.ts +2 -0
  88. package/dist/node/foundation/Analytics/index.js +7 -0
  89. package/dist/node/foundation/Analytics/types.d.ts +5 -0
  90. package/dist/node/foundation/Analytics/types.js +2 -0
  91. package/dist/node/foundation/Analytics/utils.d.ts +1 -0
  92. package/dist/node/foundation/Analytics/utils.js +12 -0
  93. package/dist/node/foundation/Router/BrowserRouter.client.js +67 -14
  94. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +2 -2
  95. package/dist/node/foundation/ShopifyProvider/types.d.ts +2 -5
  96. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  97. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +13 -7
  98. package/dist/node/framework/cache/in-memory.js +5 -5
  99. package/dist/node/framework/cache.d.ts +1 -2
  100. package/dist/node/framework/cache.js +71 -27
  101. package/dist/node/framework/plugin.js +10 -0
  102. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +1 -1
  103. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +11 -2
  104. package/dist/node/types.d.ts +6 -1
  105. package/dist/node/utilities/apiRoutes.d.ts +2 -3
  106. package/dist/node/utilities/apiRoutes.js +14 -9
  107. package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +6 -0
  108. package/dist/node/utilities/flattenConnection/flattenConnection.js +15 -0
  109. package/dist/node/utilities/flattenConnection/index.d.ts +1 -0
  110. package/dist/node/utilities/flattenConnection/index.js +5 -0
  111. package/dist/node/utilities/hash.d.ts +2 -0
  112. package/dist/node/utilities/hash.js +11 -0
  113. package/dist/node/utilities/image_size.d.ts +30 -0
  114. package/dist/node/utilities/image_size.js +110 -0
  115. package/dist/node/utilities/index.d.ts +11 -0
  116. package/dist/node/utilities/index.js +32 -0
  117. package/dist/node/utilities/isClient/index.d.ts +1 -0
  118. package/dist/node/utilities/isClient/index.js +5 -0
  119. package/dist/node/utilities/isClient/isClient.d.ts +4 -0
  120. package/dist/node/utilities/isClient/isClient.js +10 -0
  121. package/dist/node/utilities/isServer/index.d.ts +1 -0
  122. package/dist/node/utilities/isServer/index.js +5 -0
  123. package/dist/node/utilities/isServer/isServer.d.ts +4 -0
  124. package/dist/node/utilities/isServer/isServer.js +11 -0
  125. package/dist/node/utilities/load_script.d.ts +3 -0
  126. package/dist/node/utilities/load_script.js +27 -0
  127. package/dist/node/utilities/log/log-cache-api-status.js +1 -1
  128. package/dist/node/utilities/log/log-cache-header.js +2 -2
  129. package/dist/node/utilities/log/log-query-timeline.js +2 -2
  130. package/dist/node/utilities/measurement.d.ts +3 -0
  131. package/dist/node/utilities/measurement.js +103 -0
  132. package/dist/node/utilities/parseMetafieldValue/index.d.ts +1 -0
  133. package/dist/node/utilities/parseMetafieldValue/index.js +5 -0
  134. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +6 -0
  135. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +39 -0
  136. package/dist/node/utilities/storefrontApi.d.ts +4 -0
  137. package/dist/node/utilities/storefrontApi.js +25 -0
  138. package/dist/node/utilities/suspense.d.ts +12 -0
  139. package/dist/node/utilities/suspense.js +64 -0
  140. package/dist/node/utilities/template.js +1 -1
  141. package/dist/node/utilities/video_parameters.d.ts +47 -0
  142. package/dist/node/utilities/video_parameters.js +27 -0
  143. package/dist/node/version.d.ts +1 -1
  144. package/dist/node/version.js +1 -1
  145. package/package.json +1 -1
  146. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +9 -21
  147. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.development.server.js +51 -47
  148. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.production.min.server.js +30 -29
  149. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.development.server.js +51 -47
  150. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.production.min.server.js +17 -17
  151. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-client-proxy.js +55 -45
  152. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +9 -21
  153. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js +51 -47
  154. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.node.server.js +51 -47
  155. package/vendor/react-server-dom-vite/package.json +3 -3
@@ -0,0 +1,91 @@
1
+ import { getNamedspacedEventname } from './utils';
2
+ import { isServer } from '../../utilities';
3
+ import { eventNames } from './const';
4
+ import { EVENT_PATHNAME } from '../../constants';
5
+ const subscribers = {};
6
+ let pageAnalyticsData = {};
7
+ const guardDupEvents = {};
8
+ const USAGE_ERROR = 'ClientAnalytics should only be used within the useEffect callback or event handlers';
9
+ function isInvokedFromServer() {
10
+ if (isServer()) {
11
+ console.warn(USAGE_ERROR);
12
+ return true;
13
+ }
14
+ return false;
15
+ }
16
+ function pushToPageAnalyticsData(data, namespace) {
17
+ if (isInvokedFromServer())
18
+ return;
19
+ if (namespace) {
20
+ pageAnalyticsData[namespace] = Object.assign({}, pageAnalyticsData[namespace] || {}, data);
21
+ }
22
+ else {
23
+ pageAnalyticsData = Object.assign({}, pageAnalyticsData, data);
24
+ }
25
+ }
26
+ function getPageAnalyticsData() {
27
+ if (isInvokedFromServer())
28
+ return;
29
+ return pageAnalyticsData;
30
+ }
31
+ function resetPageAnalyticsData() {
32
+ if (isInvokedFromServer())
33
+ return;
34
+ pageAnalyticsData = {};
35
+ }
36
+ function publish(eventname, guardDup = false, payload) {
37
+ if (isInvokedFromServer())
38
+ return;
39
+ const namedspacedEventname = getNamedspacedEventname(eventname);
40
+ const subs = subscribers[namedspacedEventname];
41
+ const combinedPayload = Object.assign({}, pageAnalyticsData, payload);
42
+ // De-dup events due to re-renders
43
+ if (guardDup) {
44
+ const eventGuardTimeout = guardDupEvents[namedspacedEventname];
45
+ if (eventGuardTimeout) {
46
+ clearTimeout(eventGuardTimeout);
47
+ }
48
+ const namespacedTimeout = setTimeout(() => {
49
+ publishEvent(subs, combinedPayload);
50
+ }, 100);
51
+ guardDupEvents[namedspacedEventname] = namespacedTimeout;
52
+ }
53
+ else {
54
+ publishEvent(subs, combinedPayload);
55
+ }
56
+ }
57
+ function publishEvent(subs, payload) {
58
+ if (subs) {
59
+ Object.keys(subs).forEach((key) => {
60
+ subs[key](payload);
61
+ });
62
+ }
63
+ }
64
+ function subscribe(eventname, callbackFunction) {
65
+ if (isInvokedFromServer())
66
+ return { unsubscribe: () => { } };
67
+ const namedspacedEventname = getNamedspacedEventname(eventname);
68
+ const subs = subscribers[namedspacedEventname];
69
+ if (!subs) {
70
+ subscribers[namedspacedEventname] = {};
71
+ }
72
+ const subscriberId = Date.now().toString();
73
+ subscribers[namedspacedEventname][subscriberId] = callbackFunction;
74
+ return {
75
+ unsubscribe: () => {
76
+ delete subscribers[namedspacedEventname][subscriberId];
77
+ },
78
+ };
79
+ }
80
+ function pushToServer(init, searchParam) {
81
+ return fetch(`${EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`, init);
82
+ }
83
+ export const ClientAnalytics = {
84
+ pushToPageAnalyticsData,
85
+ getPageAnalyticsData,
86
+ resetPageAnalyticsData,
87
+ publish,
88
+ subscribe,
89
+ pushToServer,
90
+ eventNames,
91
+ };
@@ -0,0 +1,2 @@
1
+ import type { ServerAnalyticsConnector } from '../../types';
2
+ export declare function ServerAnalyticsRoute(request: Request, serverAnalyticsConnectors?: Array<ServerAnalyticsConnector>): Response;
@@ -0,0 +1,33 @@
1
+ import { log } from '../../utilities/log';
2
+ export function ServerAnalyticsRoute(request, serverAnalyticsConnectors) {
3
+ if (request.headers.get('Content-Length') === '0') {
4
+ serverAnalyticsConnectors === null || serverAnalyticsConnectors === void 0 ? void 0 : serverAnalyticsConnectors.forEach((connector) => {
5
+ connector.request(request);
6
+ });
7
+ }
8
+ else if (request.headers.get('Content-Type') === 'application/json') {
9
+ Promise.resolve(request.json())
10
+ .then((data) => {
11
+ serverAnalyticsConnectors === null || serverAnalyticsConnectors === void 0 ? void 0 : serverAnalyticsConnectors.forEach((connector) => {
12
+ connector.request(request, data, 'json');
13
+ });
14
+ })
15
+ .catch((error) => {
16
+ log.warn('Fail to resolve server analytics: ', error);
17
+ });
18
+ }
19
+ else {
20
+ Promise.resolve(request.text())
21
+ .then((data) => {
22
+ serverAnalyticsConnectors === null || serverAnalyticsConnectors === void 0 ? void 0 : serverAnalyticsConnectors.forEach((connector) => {
23
+ connector.request(request, data, 'text');
24
+ });
25
+ })
26
+ .catch((error) => {
27
+ log.warn('Fail to resolve server analytics: ', error);
28
+ });
29
+ }
30
+ return new Response(null, {
31
+ status: 200,
32
+ });
33
+ }
@@ -0,0 +1,8 @@
1
+ export declare const eventNames: {
2
+ PAGE_VIEW: string;
3
+ VIEWED_PRODUCT: string;
4
+ ADD_TO_CART: string;
5
+ REMOVE_FROM_CART: string;
6
+ UPDATE_CART: string;
7
+ DISCOUNT_CODE_UPDATED: string;
8
+ };
@@ -0,0 +1,8 @@
1
+ export const eventNames = {
2
+ PAGE_VIEW: 'page-view',
3
+ VIEWED_PRODUCT: 'viewed-product',
4
+ ADD_TO_CART: 'add-to-cart',
5
+ REMOVE_FROM_CART: 'remove-from-cart',
6
+ UPDATE_CART: 'update-cart',
7
+ DISCOUNT_CODE_UPDATED: 'discount-code-updated',
8
+ };
@@ -0,0 +1 @@
1
+ export declare function useServerAnalytics(data?: any): any;
@@ -0,0 +1,7 @@
1
+ import { useServerRequest } from '../ServerRequestProvider';
2
+ export function useServerAnalytics(data) {
3
+ const request = useServerRequest();
4
+ if (data)
5
+ request.ctx.analyticsData = Object.assign({}, request.ctx.analyticsData, data);
6
+ return request.ctx.analyticsData;
7
+ }
@@ -0,0 +1,2 @@
1
+ export { useServerAnalytics } from './hook';
2
+ export { ClientAnalytics } from './ClientAnalytics';
@@ -0,0 +1,2 @@
1
+ export { useServerAnalytics } from './hook';
2
+ export { ClientAnalytics } from './ClientAnalytics';
@@ -0,0 +1,5 @@
1
+ export declare type Subscriber = {
2
+ unsubscribe: () => void;
3
+ };
4
+ export declare type SubscriberFunction = (payload: any) => void;
5
+ export declare type Subscribers = Record<string, Record<string, SubscriberFunction>>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function getNamedspacedEventname(eventname: string): string;
@@ -0,0 +1,8 @@
1
+ import { eventNames } from './const';
2
+ const RESERVED_EVENT_NAMES = Object.values(eventNames);
3
+ export function getNamedspacedEventname(eventname) {
4
+ // Any event name that is not in the reserved space will be prefix with `c-`
5
+ return RESERVED_EVENT_NAMES.indexOf(eventname) === -1
6
+ ? `c-${eventname}`
7
+ : eventname;
8
+ }
@@ -4,7 +4,9 @@ import { useShop } from '../useShop';
4
4
  const URL = 'https://cdn.shopify.com/shopifycloud/boomerang/shopify-boomerang-hydrogen.min.js';
5
5
  export function Boomerang({ pageTemplate }) {
6
6
  const { storeDomain } = useShop();
7
- const templateName = pageTemplate !== null ? pageTemplate.toLowerCase() : 'not-set';
7
+ const templateName = pageTemplate && pageTemplate !== null
8
+ ? pageTemplate.toLowerCase()
9
+ : 'not-set';
8
10
  useEffect(() => {
9
11
  (function () {
10
12
  function boomerangAddVar() {
@@ -3,6 +3,7 @@ import { useServerRequest } from '../ServerRequestProvider';
3
3
  import { matchPath } from '../../utilities/matchPath';
4
4
  import { Boomerang } from '../Boomerang/Boomerang.client';
5
5
  import { RouteParamsProvider } from '../useRouteParams/RouteParamsProvider.client';
6
+ import { useServerAnalytics } from '../Analytics';
6
7
  /**
7
8
  * The `Route` component is used to set up a route in Hydrogen that's independent of the file system. Routes are
8
9
  * matched in the order that they're defined.
@@ -25,6 +26,9 @@ export function Route({ path, page }) {
25
26
  request.ctx.router.routeRendered = true;
26
27
  request.ctx.router.routeParams = match.params;
27
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
+ });
28
32
  return (React.createElement(RouteParamsProvider, { routeParams: match.params },
29
33
  cloneElement(page, { params: match.params || {}, ...serverProps }),
30
34
  name ? React.createElement(Boomerang, { pageTemplate: name }) : null));
@@ -1,36 +1,28 @@
1
1
  import { createBrowserHistory } from 'history';
2
- import React, { createContext, useContext, useMemo, useState, useEffect, } from 'react';
2
+ import React, { createContext, useContext, useMemo, useState, useEffect, useLayoutEffect, useCallback, } from 'react';
3
3
  import { META_ENV_SSR } from '../ssr-interop';
4
4
  import { useServerState } from '../useServerState';
5
5
  export const RouterContext = createContext({});
6
- let currentPath = '';
7
6
  let isFirstLoad = true;
7
+ const positions = {};
8
8
  export const BrowserRouter = ({ history: pHistory, children, }) => {
9
9
  if (META_ENV_SSR)
10
10
  return React.createElement(React.Fragment, null, children);
11
11
  const history = useMemo(() => pHistory || createBrowserHistory(), [pHistory]);
12
12
  const [location, setLocation] = useState(history.location);
13
13
  const { pending, serverState, setServerState } = useServerState();
14
- useEffect(() => {
15
- // The app has just loaded
16
- if (isFirstLoad)
17
- isFirstLoad = false;
18
- // A navigation event has just happened
19
- else if (!pending && currentPath !== serverState.pathname) {
20
- window.scrollTo(0, 0);
21
- }
22
- currentPath = serverState.pathname;
23
- }, [pending]);
24
- useEffect(() => {
14
+ useScrollRestoration(location, pending, serverState);
15
+ useLayoutEffect(() => {
25
16
  const unlisten = history.listen(({ location: newLocation }) => {
17
+ positions[location.key] = window.scrollY;
26
18
  setServerState({
27
19
  pathname: newLocation.pathname,
28
- search: location.search || undefined,
20
+ search: location.search,
29
21
  });
30
22
  setLocation(newLocation);
31
23
  });
32
24
  return () => unlisten();
33
- }, [history]);
25
+ }, [history, location]);
34
26
  return (React.createElement(RouterContext.Provider, { value: {
35
27
  history,
36
28
  location,
@@ -46,3 +38,64 @@ export function useRouter() {
46
38
  export function useLocation() {
47
39
  return useRouter().location;
48
40
  }
41
+ /**
42
+ * Run a callback before browser unload.
43
+ */
44
+ function useBeforeUnload(callback) {
45
+ React.useEffect(() => {
46
+ window.addEventListener('beforeunload', callback);
47
+ return () => {
48
+ window.removeEventListener('beforeunload', callback);
49
+ };
50
+ }, [callback]);
51
+ }
52
+ function useScrollRestoration(location, pending, serverState) {
53
+ /**
54
+ * Browsers have an API for scroll restoration. We wait for the page to load first,
55
+ * in case the browser is able to restore scroll position automatically, and then
56
+ * set it to manual mode.
57
+ */
58
+ useEffect(() => {
59
+ window.history.scrollRestoration = 'manual';
60
+ }, []);
61
+ /**
62
+ * If the page is reloading, allow the browser to handle its own scroll restoration.
63
+ */
64
+ useBeforeUnload(useCallback(() => {
65
+ window.history.scrollRestoration = 'auto';
66
+ }, []));
67
+ useLayoutEffect(() => {
68
+ // The app has just loaded
69
+ if (isFirstLoad) {
70
+ isFirstLoad = false;
71
+ return;
72
+ }
73
+ const position = positions[location.key];
74
+ /**
75
+ * When serverState gets updated, `pending` is true while the fetch is in progress.
76
+ * When that resolves, the serverState is updated. We should wait until the internal
77
+ * location pointer and serverState match, and pending is false, to do any scrolling.
78
+ */
79
+ const finishedNavigating = !pending &&
80
+ location.pathname === serverState.pathname &&
81
+ location.search === serverState.search;
82
+ if (!finishedNavigating) {
83
+ return;
84
+ }
85
+ // If there is a location hash, scroll to it
86
+ if (location.hash) {
87
+ const element = document.querySelector(location.hash);
88
+ if (element) {
89
+ element.scrollIntoView();
90
+ return;
91
+ }
92
+ }
93
+ // If we have a matching position, scroll to it
94
+ if (position) {
95
+ window.scrollTo(0, position);
96
+ return;
97
+ }
98
+ // Scroll to the top of new pages
99
+ window.scrollTo(0, 0);
100
+ }, [location, pending, serverState]);
101
+ }
@@ -1,6 +1,6 @@
1
1
  import React, { createContext, useContext } from 'react';
2
2
  import { getTime } from '../../utilities/timing';
3
- import { hashKey } from '../../framework/cache';
3
+ import { hashKey } from '../../utilities/hash';
4
4
  import { collectQueryTimings } from '../../utilities/log';
5
5
  // Context to inject current request in SSR
6
6
  const RequestContextSSR = createContext(null);
@@ -1,13 +1,10 @@
1
1
  import type { CountryCode, LanguageCode } from '../../storefront-api-types';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { ShopifyConfig } from '../../types';
4
- export declare type ShopifyContextValue = {
4
+ export interface ShopifyContextValue extends Omit<ShopifyConfig, 'defaultLocale'> {
5
5
  locale: `${LanguageCode}-${CountryCode}`;
6
6
  languageCode: `${LanguageCode}`;
7
- storeDomain: ShopifyConfig['storeDomain'];
8
- storefrontToken: ShopifyConfig['storefrontToken'];
9
- storefrontApiVersion: string;
10
- };
7
+ }
11
8
  export declare type ShopifyProviderProps = {
12
9
  /** The contents of the `shopify.config.js` file. */
13
10
  shopifyConfig: ShopifyConfig;
@@ -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;
@@ -1,4 +1,6 @@
1
+ import { useMemo } from 'react';
1
2
  import { RSC_PATHNAME } from '../../constants';
3
+ import { useLocation } from '../Router/BrowserRouter.client';
2
4
  import { useEnvContext, META_ENV_SSR } from '../ssr-interop';
3
5
  /**
4
6
  * The `useUrl` hook retrieves the current URL in a server or client component.
@@ -14,5 +16,10 @@ export function useUrl() {
14
16
  }
15
17
  return new URL(serverUrl);
16
18
  }
17
- return new URL(window.location.href);
19
+ /**
20
+ * We return a `URL` object instead of passing through `location` because
21
+ * the URL object contains important info like hostname, etc.
22
+ */
23
+ const location = useLocation();
24
+ return useMemo(() => new URL(window.location.href), [location]);
18
25
  }
@@ -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);
@@ -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,6 +1,5 @@
1
1
  import type { QueryKey, CachingStrategy } from '../types';
2
2
  export declare function generateSubRequestCacheControlHeader(userCacheOptions?: CachingStrategy): string;
3
- export declare function hashKey(key: QueryKey): string;
4
3
  /**
5
4
  * Get an item from the cache. If a match is found, returns a tuple
6
5
  * containing the `JSON.parse` version of the response as well
@@ -15,4 +14,4 @@ export declare function deleteItemFromCache(key: QueryKey): Promise<void>;
15
14
  /**
16
15
  * Manually check the response to see if it's stale.
17
16
  */
18
- export declare function isStale(response: Response): boolean;
17
+ export declare function isStale(response: Response, userCacheOptions?: CachingStrategy): boolean;