@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.
- package/CHANGELOG.md +124 -0
- package/dist/esnext/client.d.ts +4 -0
- package/dist/esnext/client.js +4 -0
- package/dist/esnext/components/CartProvider/CartProvider.client.js +23 -0
- package/dist/esnext/components/DevTools.d.ts +1 -0
- package/dist/esnext/components/DevTools.js +128 -0
- package/dist/esnext/components/Link/Link.client.js +1 -1
- package/dist/esnext/constants.d.ts +6 -0
- package/dist/esnext/constants.js +6 -0
- package/dist/esnext/entry-client.js +7 -4
- package/dist/esnext/entry-server.d.ts +1 -1
- package/dist/esnext/entry-server.js +29 -15
- package/dist/esnext/foundation/Analytics/Analytics.client.d.ts +3 -0
- package/dist/esnext/foundation/Analytics/Analytics.client.js +28 -0
- package/dist/esnext/foundation/Analytics/Analytics.server.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/Analytics.server.js +38 -0
- package/dist/esnext/foundation/Analytics/ClientAnalytics.d.ts +24 -0
- package/dist/esnext/foundation/Analytics/ClientAnalytics.js +91 -0
- package/dist/esnext/foundation/Analytics/ServerAnalyticsRoute.server.d.ts +2 -0
- package/dist/esnext/foundation/Analytics/ServerAnalyticsRoute.server.js +33 -0
- package/dist/esnext/foundation/Analytics/const.d.ts +8 -0
- package/dist/esnext/foundation/Analytics/const.js +8 -0
- package/dist/esnext/foundation/Analytics/hook.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/hook.js +7 -0
- package/dist/esnext/foundation/Analytics/index.d.ts +2 -0
- package/dist/esnext/foundation/Analytics/index.js +2 -0
- package/dist/esnext/foundation/Analytics/types.d.ts +5 -0
- package/dist/esnext/foundation/Analytics/types.js +1 -0
- package/dist/esnext/foundation/Analytics/utils.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/utils.js +8 -0
- package/dist/esnext/foundation/Boomerang/Boomerang.client.js +3 -1
- package/dist/esnext/foundation/Route/Route.server.js +4 -0
- package/dist/esnext/foundation/Router/BrowserRouter.client.js +68 -15
- package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
- package/dist/esnext/foundation/ShopifyProvider/types.d.ts +2 -5
- package/dist/esnext/foundation/fetchSync/client/fetchSync.d.ts +10 -0
- package/dist/esnext/foundation/fetchSync/client/fetchSync.js +27 -0
- package/dist/esnext/foundation/fetchSync/server/fetchSync.d.ts +8 -0
- package/dist/esnext/foundation/fetchSync/server/fetchSync.js +27 -0
- package/dist/esnext/foundation/fetchSync/types.d.ts +5 -0
- package/dist/esnext/foundation/fetchSync/types.js +1 -0
- package/dist/esnext/foundation/useQuery/hooks.d.ts +4 -2
- package/dist/esnext/foundation/useQuery/hooks.js +10 -6
- package/dist/esnext/foundation/useUrl/useUrl.js +8 -1
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +11 -5
- package/dist/esnext/framework/cache/in-memory.js +5 -5
- package/dist/esnext/framework/cache.d.ts +1 -2
- package/dist/esnext/framework/cache.js +67 -22
- package/dist/esnext/framework/plugin.js +10 -0
- package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +1 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +11 -2
- package/dist/esnext/hooks/useShopQuery/hooks.js +32 -25
- package/dist/esnext/index.d.ts +2 -0
- package/dist/esnext/index.js +2 -0
- package/dist/esnext/types.d.ts +6 -1
- package/dist/esnext/utilities/apiRoutes.d.ts +2 -3
- package/dist/esnext/utilities/apiRoutes.js +14 -9
- package/dist/esnext/utilities/hash.d.ts +2 -0
- package/dist/esnext/utilities/hash.js +7 -0
- package/dist/esnext/utilities/log/log-cache-api-status.js +1 -1
- package/dist/esnext/utilities/log/log-cache-header.js +1 -1
- package/dist/esnext/utilities/log/log-query-timeline.js +1 -1
- package/dist/esnext/utilities/storefrontApi.d.ts +4 -0
- package/dist/esnext/utilities/storefrontApi.js +21 -0
- package/dist/esnext/utilities/suspense.d.ts +5 -0
- package/dist/esnext/utilities/suspense.js +32 -0
- package/dist/esnext/utilities/template.js +1 -1
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/constants.d.ts +6 -0
- package/dist/node/constants.js +7 -1
- package/dist/node/entry-server.d.ts +1 -1
- package/dist/node/entry-server.js +28 -17
- package/dist/node/foundation/Analytics/Analytics.client.d.ts +3 -0
- package/dist/node/foundation/Analytics/Analytics.client.js +32 -0
- package/dist/node/foundation/Analytics/Analytics.server.d.ts +1 -0
- package/dist/node/foundation/Analytics/Analytics.server.js +45 -0
- package/dist/node/foundation/Analytics/ClientAnalytics.d.ts +24 -0
- package/dist/node/foundation/Analytics/ClientAnalytics.js +94 -0
- package/dist/node/foundation/Analytics/ServerAnalyticsRoute.server.d.ts +2 -0
- package/dist/node/foundation/Analytics/ServerAnalyticsRoute.server.js +37 -0
- package/dist/node/foundation/Analytics/const.d.ts +8 -0
- package/dist/node/foundation/Analytics/const.js +11 -0
- package/dist/node/foundation/Analytics/hook.d.ts +1 -0
- package/dist/node/foundation/Analytics/hook.js +11 -0
- package/dist/node/foundation/Analytics/index.d.ts +2 -0
- package/dist/node/foundation/Analytics/index.js +7 -0
- package/dist/node/foundation/Analytics/types.d.ts +5 -0
- package/dist/node/foundation/Analytics/types.js +2 -0
- package/dist/node/foundation/Analytics/utils.d.ts +1 -0
- package/dist/node/foundation/Analytics/utils.js +12 -0
- package/dist/node/foundation/Router/BrowserRouter.client.js +67 -14
- package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +2 -2
- package/dist/node/foundation/ShopifyProvider/types.d.ts +2 -5
- package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
- package/dist/node/framework/Hydration/ServerComponentRequest.server.js +13 -7
- package/dist/node/framework/cache/in-memory.js +5 -5
- package/dist/node/framework/cache.d.ts +1 -2
- package/dist/node/framework/cache.js +71 -27
- package/dist/node/framework/plugin.js +10 -0
- package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +1 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +11 -2
- package/dist/node/types.d.ts +6 -1
- package/dist/node/utilities/apiRoutes.d.ts +2 -3
- package/dist/node/utilities/apiRoutes.js +14 -9
- package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +6 -0
- package/dist/node/utilities/flattenConnection/flattenConnection.js +15 -0
- package/dist/node/utilities/flattenConnection/index.d.ts +1 -0
- package/dist/node/utilities/flattenConnection/index.js +5 -0
- package/dist/node/utilities/hash.d.ts +2 -0
- package/dist/node/utilities/hash.js +11 -0
- package/dist/node/utilities/image_size.d.ts +30 -0
- package/dist/node/utilities/image_size.js +110 -0
- package/dist/node/utilities/index.d.ts +11 -0
- package/dist/node/utilities/index.js +32 -0
- package/dist/node/utilities/isClient/index.d.ts +1 -0
- package/dist/node/utilities/isClient/index.js +5 -0
- package/dist/node/utilities/isClient/isClient.d.ts +4 -0
- package/dist/node/utilities/isClient/isClient.js +10 -0
- package/dist/node/utilities/isServer/index.d.ts +1 -0
- package/dist/node/utilities/isServer/index.js +5 -0
- package/dist/node/utilities/isServer/isServer.d.ts +4 -0
- package/dist/node/utilities/isServer/isServer.js +11 -0
- package/dist/node/utilities/load_script.d.ts +3 -0
- package/dist/node/utilities/load_script.js +27 -0
- package/dist/node/utilities/log/log-cache-api-status.js +1 -1
- package/dist/node/utilities/log/log-cache-header.js +2 -2
- package/dist/node/utilities/log/log-query-timeline.js +2 -2
- package/dist/node/utilities/measurement.d.ts +3 -0
- package/dist/node/utilities/measurement.js +103 -0
- package/dist/node/utilities/parseMetafieldValue/index.d.ts +1 -0
- package/dist/node/utilities/parseMetafieldValue/index.js +5 -0
- package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +6 -0
- package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +39 -0
- package/dist/node/utilities/storefrontApi.d.ts +4 -0
- package/dist/node/utilities/storefrontApi.js +25 -0
- package/dist/node/utilities/suspense.d.ts +12 -0
- package/dist/node/utilities/suspense.js +64 -0
- package/dist/node/utilities/template.js +1 -1
- package/dist/node/utilities/video_parameters.d.ts +47 -0
- package/dist/node/utilities/video_parameters.js +27 -0
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- package/package.json +1 -1
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +9 -21
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.development.server.js +51 -47
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.production.min.server.js +30 -29
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.development.server.js +51 -47
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.production.min.server.js +17 -17
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-client-proxy.js +55 -45
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +9 -21
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js +51 -47
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.node.server.js +51 -47
- 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,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 @@
|
|
|
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 @@
|
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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 '../../
|
|
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
|
|
4
|
+
export interface ShopifyContextValue extends Omit<ShopifyConfig, 'defaultLocale'> {
|
|
5
5
|
locale: `${LanguageCode}-${CountryCode}`;
|
|
6
6
|
languageCode: `${LanguageCode}`;
|
|
7
|
-
|
|
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 @@
|
|
|
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).
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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()
|
|
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')
|
|
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;
|