@shopify/hydrogen 0.17.0 → 0.17.3
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 +33 -1
- package/dist/esnext/client.d.ts +2 -0
- package/dist/esnext/client.js +2 -0
- package/dist/esnext/components/ModelViewer/ModelViewer.client.d.ts +1 -1
- package/dist/esnext/components/ModelViewer/ModelViewer.client.js +1 -1
- package/dist/esnext/components/ProductProvider/ProductProvider.client.js +1 -1
- package/dist/esnext/entry-server.js +6 -8
- package/dist/esnext/foundation/Analytics/ClientAnalytics.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/ClientAnalytics.js +7 -1
- package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.d.ts +7 -0
- package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.js +64 -0
- package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server.js +24 -0
- package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetricsDebug.client.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetricsDebug.client.js +23 -0
- package/dist/esnext/foundation/Analytics/const.d.ts +1 -0
- package/dist/esnext/foundation/Analytics/const.js +1 -0
- package/dist/esnext/foundation/Cookie/Cookie.d.ts +1 -1
- package/dist/esnext/foundation/FileRoutes/FileRoutes.server.js +2 -18
- package/dist/esnext/foundation/FileSessionStorage/FileSessionStorage.js +1 -1
- package/dist/esnext/foundation/Route/Route.server.js +1 -10
- package/dist/esnext/foundation/Router/BrowserRouter.client.js +26 -6
- package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +12 -23
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +1 -0
- package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +2 -2
- package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.js +0 -2
- package/dist/esnext/index.d.ts +1 -0
- package/dist/esnext/index.js +1 -0
- package/dist/esnext/utilities/apiRoutes.d.ts +1 -0
- package/dist/esnext/utilities/apiRoutes.js +23 -18
- package/dist/esnext/utilities/bot-ua.js +3 -0
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/entry-server.js +6 -8
- package/dist/node/foundation/Analytics/ClientAnalytics.d.ts +1 -0
- package/dist/node/foundation/Analytics/ClientAnalytics.js +7 -1
- package/dist/node/foundation/Analytics/const.d.ts +1 -0
- package/dist/node/foundation/Analytics/const.js +1 -0
- package/dist/node/foundation/Router/BrowserRouter.client.js +26 -6
- package/dist/node/foundation/ServerPropsProvider/ServerPropsProvider.js +11 -22
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +1 -0
- package/dist/node/framework/plugins/vite-plugin-platform-entry.js +2 -2
- package/dist/node/utilities/apiRoutes.d.ts +1 -0
- package/dist/node/utilities/apiRoutes.js +25 -19
- package/dist/node/utilities/bot-ua.js +3 -0
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- package/package.json +2 -2
- package/dist/esnext/foundation/Boomerang/Boomerang.client.d.ts +0 -9
- package/dist/esnext/foundation/Boomerang/Boomerang.client.js +0 -66
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1096](https://github.com/Shopify/hydrogen/pull/1096) [`0a15376e`](https://github.com/Shopify/hydrogen/commit/0a15376ec806054ddd5848d9dbfa6e50a85beb49) Thanks [@wizardlyhel](https://github.com/wizardlyhel)! - Make performance data available with ClientAnalytics and optional for developers to include
|
|
8
|
+
|
|
9
|
+
* [#1209](https://github.com/Shopify/hydrogen/pull/1209) [`d0dada0a`](https://github.com/Shopify/hydrogen/commit/d0dada0a0b3170d2cb885d2f29bbbef0c6d9e9e4) Thanks [@blittle](https://github.com/blittle)! - Make metafields optional within the ProductProvider. Fixes #1127
|
|
10
|
+
|
|
11
|
+
## 0.17.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#1161](https://github.com/Shopify/hydrogen/pull/1161) [`6b963fb1`](https://github.com/Shopify/hydrogen/commit/6b963fb1fdd2824683870c8ff3258447bf7fedea) Thanks [@merwan7](https://github.com/merwan7)! - Adds ability to add multiple cookies in one response
|
|
16
|
+
|
|
17
|
+
* [#1162](https://github.com/Shopify/hydrogen/pull/1162) [`5446d544`](https://github.com/Shopify/hydrogen/commit/5446d544f151e233e909e6a6f002e87863ae6151) Thanks [@arlyxiao](https://github.com/arlyxiao)! - Upgrade body-parser
|
|
18
|
+
|
|
19
|
+
- [#1200](https://github.com/Shopify/hydrogen/pull/1200) [`7fb7ee49`](https://github.com/Shopify/hydrogen/commit/7fb7ee497091df3177d53e8745edcae6ba99a87d) Thanks [@blittle](https://github.com/blittle)! - Add bot user agents for Seoradar and Adresults, resolves #1199
|
|
20
|
+
|
|
21
|
+
* [#1167](https://github.com/Shopify/hydrogen/pull/1167) [`0a5ac1cb`](https://github.com/Shopify/hydrogen/commit/0a5ac1cbec449eefe48041ed6aceaac375dfa601) Thanks [@benjaminsehl](https://github.com/benjaminsehl)! - Only warn in console on missing Model3D alt tag, do not throw error
|
|
22
|
+
|
|
23
|
+
- [#1152](https://github.com/Shopify/hydrogen/pull/1152) [`d3e3e695`](https://github.com/Shopify/hydrogen/commit/d3e3e695457e6eb2a3ebf9767e0f10cc3570e880) Thanks [@jplhomer](https://github.com/jplhomer)! - Fix scroll restoration when server props are changed
|
|
24
|
+
|
|
25
|
+
## 0.17.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- [#1145](https://github.com/Shopify/hydrogen/pull/1145) [`865b66e9`](https://github.com/Shopify/hydrogen/commit/865b66e95d67965543bcb92f0f9f15b5742f3596) Thanks [@jplhomer](https://github.com/jplhomer)! - Fix search params on navigation
|
|
30
|
+
|
|
31
|
+
* [#1139](https://github.com/Shopify/hydrogen/pull/1139) [`93525637`](https://github.com/Shopify/hydrogen/commit/9352563761c0405f2e2b39cb6b8b8f577f2522b9) Thanks [@blittle](https://github.com/blittle)! - Fix the scroll restoration on page transitions
|
|
32
|
+
|
|
33
|
+
- [#1144](https://github.com/Shopify/hydrogen/pull/1144) [`dec5eb8e`](https://github.com/Shopify/hydrogen/commit/dec5eb8e34e75c806aa1cfea935814d228ab29d6) Thanks [@wizardlyhel](https://github.com/wizardlyhel)! - fix 0.17 build
|
|
34
|
+
|
|
3
35
|
## 0.17.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
|
@@ -333,7 +365,7 @@
|
|
|
333
365
|
}, []);
|
|
334
366
|
```
|
|
335
367
|
|
|
336
|
-
See an example on how this could be done inside the Demo Store template [country selector](https://github.com/Shopify/hydrogen/blob/v1.x-2022-07/
|
|
368
|
+
See an example on how this could be done inside the Demo Store template [country selector](https://github.com/Shopify/hydrogen/blob/v1.x-2022-07/templates/template-hydrogen-default/src/components/CountrySelector.client.jsx)
|
|
337
369
|
|
|
338
370
|
- [#698](https://github.com/Shopify/hydrogen/pull/698) [`6f30b9a1`](https://github.com/Shopify/hydrogen/commit/6f30b9a1327f06d648a01dd94d539c7dcb3061e0) Thanks [@jplhomer](https://github.com/jplhomer)! - Basic end-to-end tests have been added to the default Hydrogen template. You can run tests in development:
|
|
339
371
|
|
package/dist/esnext/client.d.ts
CHANGED
|
@@ -11,3 +11,5 @@ export { useRouteParams } from './foundation/useRouteParams/useRouteParams';
|
|
|
11
11
|
export { useNavigate } from './foundation/useNavigate/useNavigate';
|
|
12
12
|
export { fetchSync } from './foundation/fetchSync/client/fetchSync';
|
|
13
13
|
export { suspendFunction, preloadFunction } from './utilities/suspense';
|
|
14
|
+
export { PerformanceMetrics } from './foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client';
|
|
15
|
+
export { PerformanceMetricsDebug } from './foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetricsDebug.client';
|
package/dist/esnext/client.js
CHANGED
|
@@ -11,3 +11,5 @@ export { useRouteParams } from './foundation/useRouteParams/useRouteParams';
|
|
|
11
11
|
export { useNavigate } from './foundation/useNavigate/useNavigate';
|
|
12
12
|
export { fetchSync } from './foundation/fetchSync/client/fetchSync';
|
|
13
13
|
export { suspendFunction, preloadFunction } from './utilities/suspense';
|
|
14
|
+
export { PerformanceMetrics } from './foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client';
|
|
15
|
+
export { PerformanceMetricsDebug } from './foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetricsDebug.client';
|
|
@@ -112,7 +112,7 @@ interface ModelViewerProps {
|
|
|
112
112
|
/** The callback to invoke when the 'scene-graph-ready' event is triggered. Refer to [scene-graph-ready in the <model-viewer> documentation](https://modelviewer.dev/docs/index.html#entrydocs-scenegraph-events-sceneGraphReady). */
|
|
113
113
|
onSceneGraphReady?: (event: Event) => void;
|
|
114
114
|
}
|
|
115
|
-
declare type PropsWeControl = 'src' | '
|
|
115
|
+
declare type PropsWeControl = 'src' | 'poster';
|
|
116
116
|
declare global {
|
|
117
117
|
namespace JSX {
|
|
118
118
|
interface IntrinsicElements {
|
|
@@ -99,7 +99,7 @@ export function ModelViewer(props) {
|
|
|
99
99
|
throw new Error(`<ModelViewer/> requires 'data.sources' prop to be an array, with an object that has a property 'url' on it`);
|
|
100
100
|
}
|
|
101
101
|
if (!data.alt) {
|
|
102
|
-
|
|
102
|
+
console.warn(`<ModelViewer/> requires the 'data.alt' prop for accessibility`);
|
|
103
103
|
}
|
|
104
104
|
return (React.createElement("model-viewer", { ref: callbackRef, ...passthroughProps, class: className, id: id, src: data.sources[0].url, alt: data.alt, "camera-controls": (_c = passthroughProps.cameraControls) !== null && _c !== void 0 ? _c : true, poster: passthroughProps.poster || ((_d = data.previewImage) === null || _d === void 0 ? void 0 : _d.url), autoplay: (_e = passthroughProps.autoplay) !== null && _e !== void 0 ? _e : true, loading: passthroughProps.loading, reveal: passthroughProps.reveal, ar: passthroughProps.ar, "ar-modes": passthroughProps.arModes, "ar-scale": passthroughProps.arScale, "ar-placement": passthroughProps.arPlacement, "ios-src": passthroughProps.iosSrc, "touch-action": passthroughProps.touchAction, "disable-zoom": passthroughProps.disableZoom, "orbit-sensitivity": passthroughProps.orbitSensitivity, "auto-rotate": passthroughProps.autoRotate, "auto-rotate-delay": passthroughProps.autoRotateDelay, "rotation-per-second": passthroughProps.rotationPerSecond, "interaction-policy": passthroughProps.interactionPolicy, "interaction-prompt": passthroughProps.interactionPrompt, "interaction-prompt-style": passthroughProps.interactionPromptStyle, "interaction-prompt-threshold": passthroughProps.interactionPromptThreshold, "camera-orbit": passthroughProps.cameraOrbit, "camera-target": passthroughProps.cameraTarget, "field-of-view": passthroughProps.fieldOfView, "max-camera-orbit": passthroughProps.maxCameraOrbit, "min-camera-orbit": passthroughProps.minCameraOrbit, "max-field-of-view": passthroughProps.maxFieldOfView, "min-field-of-view": passthroughProps.minFieldOfView, bounds: passthroughProps.bounds, "interpolation-decay": (_f = passthroughProps.interpolationDecay) !== null && _f !== void 0 ? _f : 100, "skybox-image": passthroughProps.skyboxImage, "environment-image": passthroughProps.environmentImage, exposure: passthroughProps.exposure, "shadow-intensity": (_g = passthroughProps.shadowIntensity) !== null && _g !== void 0 ? _g : 0, "shadow-softness": (_h = passthroughProps.shadowSoftness) !== null && _h !== void 0 ? _h : 0, "animation-name": passthroughProps.animationName, "animation-crossfade-duration": passthroughProps.animationCrossfadeDuration, "variant-name": passthroughProps.variantName, orientation: passthroughProps.orientation, scale: passthroughProps.scale }, children));
|
|
105
105
|
}
|
|
@@ -8,7 +8,7 @@ import { ProductOptionsProvider } from './ProductOptionsProvider.client';
|
|
|
8
8
|
* this component can use the `useProduct` hook.
|
|
9
9
|
*/
|
|
10
10
|
export function ProductProvider({ children, data: product, initialVariantId, }) {
|
|
11
|
-
const metafields = useParsedMetafields(product.metafields);
|
|
11
|
+
const metafields = useParsedMetafields(product.metafields || {});
|
|
12
12
|
// @ts-expect-error The types here are broken on main, need to come back and fix them sometime
|
|
13
13
|
const providerValue = useMemo(() => {
|
|
14
14
|
return {
|
|
@@ -121,7 +121,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
121
121
|
* TODO: Also add `Vary` headers for `accept-language` and any other keys
|
|
122
122
|
* we want to shard our full-page cache for all Hydrogen storefronts.
|
|
123
123
|
*/
|
|
124
|
-
headers
|
|
124
|
+
headers.set('cache-control', componentResponse.cacheControlHeader);
|
|
125
125
|
if (componentResponse.customBody) {
|
|
126
126
|
// This can be used to return sitemap.xml or any other custom response.
|
|
127
127
|
postRequestTasks('ssr', status, request, componentResponse);
|
|
@@ -131,7 +131,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
131
131
|
headers,
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
|
-
headers
|
|
134
|
+
headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
|
|
135
135
|
html = applyHtmlHead(html, request.ctx.head, template);
|
|
136
136
|
if (flight) {
|
|
137
137
|
html = html.replace('</body>', () => `${flightContainer({
|
|
@@ -220,8 +220,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
220
220
|
* queries which might be caught behind Suspense. Clarify this or add
|
|
221
221
|
* additional checks downstream?
|
|
222
222
|
*/
|
|
223
|
-
responseOptions.headers
|
|
224
|
-
componentResponse.cacheControlHeader;
|
|
223
|
+
responseOptions.headers.set('cache-control', componentResponse.cacheControlHeader);
|
|
225
224
|
if (isRedirect(responseOptions)) {
|
|
226
225
|
return false;
|
|
227
226
|
}
|
|
@@ -230,7 +229,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
230
229
|
writable.write(encoder.encode(await componentResponse.customBody));
|
|
231
230
|
return false;
|
|
232
231
|
}
|
|
233
|
-
responseOptions.headers
|
|
232
|
+
responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
|
|
234
233
|
writable.write(encoder.encode(DOCTYPE));
|
|
235
234
|
if (didError) {
|
|
236
235
|
// This error was delayed until the headers were properly sent.
|
|
@@ -487,8 +486,7 @@ function startWritingHtmlToServerResponse(response, error) {
|
|
|
487
486
|
function getResponseOptions({ headers, status, customStatus }, error) {
|
|
488
487
|
var _a, _b;
|
|
489
488
|
const responseInit = {};
|
|
490
|
-
|
|
491
|
-
responseInit.headers = Object.fromEntries(headers.entries());
|
|
489
|
+
responseInit.headers = headers;
|
|
492
490
|
if (error) {
|
|
493
491
|
responseInit.status = 500;
|
|
494
492
|
}
|
|
@@ -509,7 +507,7 @@ function writeHeadToServerResponse(response, serverComponentResponse, log, error
|
|
|
509
507
|
if (statusText) {
|
|
510
508
|
response.statusMessage = statusText;
|
|
511
509
|
}
|
|
512
|
-
Object.entries(headers).forEach(([key, value]) => response.setHeader(key, value));
|
|
510
|
+
Object.entries(headers.raw()).forEach(([key, value]) => response.setHeader(key, value));
|
|
513
511
|
}
|
|
514
512
|
function isRedirect(response) {
|
|
515
513
|
var _a, _b;
|
|
@@ -78,7 +78,13 @@ function subscribe(eventname, callbackFunction) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
function pushToServer(init, searchParam) {
|
|
81
|
-
return fetch(`${EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`,
|
|
81
|
+
return fetch(`${EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`, Object.assign({
|
|
82
|
+
method: 'post',
|
|
83
|
+
headers: {
|
|
84
|
+
'cache-control': 'no-cache',
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
}, init));
|
|
82
88
|
}
|
|
83
89
|
export const ClientAnalytics = {
|
|
84
90
|
pushToPageAnalyticsData,
|
package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { loadScript } from '../../../../utilities';
|
|
3
|
+
import { ClientAnalytics } from '../../index';
|
|
4
|
+
import { useShop } from '../../../useShop';
|
|
5
|
+
const URL = 'https://cdn.shopify.com/shopifycloud/boomerang/shopify-boomerang-hydrogen.min.js';
|
|
6
|
+
export function PerformanceMetrics() {
|
|
7
|
+
const { storeDomain } = useShop();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
try {
|
|
10
|
+
(function () {
|
|
11
|
+
if (window.BOOMR &&
|
|
12
|
+
(window.BOOMR.version || window.BOOMR.snippetExecuted)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Executes only on first mount
|
|
16
|
+
window.BOOMR = window.BOOMR || {};
|
|
17
|
+
window.BOOMR.hydrogenPerformanceEvent = (data) => {
|
|
18
|
+
ClientAnalytics.publish(ClientAnalytics.eventNames.PERFORMANCE, true, data);
|
|
19
|
+
ClientAnalytics.pushToServer({
|
|
20
|
+
body: JSON.stringify(data),
|
|
21
|
+
}, ClientAnalytics.eventNames.PERFORMANCE);
|
|
22
|
+
};
|
|
23
|
+
window.BOOMR.storeDomain = storeDomain;
|
|
24
|
+
function boomerangSaveLoadTime(e) {
|
|
25
|
+
window.BOOMR_onload = (e && e.timeStamp) || Date.now();
|
|
26
|
+
}
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
function boomerangInit(e) {
|
|
29
|
+
e.detail.BOOMR.init();
|
|
30
|
+
e.detail.BOOMR.t_end = Date.now();
|
|
31
|
+
}
|
|
32
|
+
if (window.addEventListener) {
|
|
33
|
+
window.addEventListener('load', boomerangSaveLoadTime, false);
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
}
|
|
36
|
+
else if (window.attachEvent) {
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
window.attachEvent('onload', boomerangSaveLoadTime);
|
|
39
|
+
}
|
|
40
|
+
if (document.addEventListener) {
|
|
41
|
+
document.addEventListener('onBoomerangLoaded', boomerangInit);
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
}
|
|
44
|
+
else if (document.attachEvent) {
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
document.attachEvent('onpropertychange', function (e) {
|
|
47
|
+
if (!e)
|
|
48
|
+
e = event;
|
|
49
|
+
if (e.propertyName === 'onBoomerangLoaded')
|
|
50
|
+
boomerangInit(e);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
loadScript(URL).catch(() => {
|
|
55
|
+
// ignore if boomerang doesn't load
|
|
56
|
+
// most likely because of an ad blocker
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Do nothing
|
|
61
|
+
}
|
|
62
|
+
}, [storeDomain]);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function request(request: Request, data?: any, contentType?: string): void;
|
package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function request(request, data, contentType) {
|
|
2
|
+
const url = new URL(request.url);
|
|
3
|
+
if (url.search === '?performance' && contentType === 'json') {
|
|
4
|
+
const initTime = new Date().getTime();
|
|
5
|
+
fetch('https://monorail-edge.shopifysvc.com/v1/produce', {
|
|
6
|
+
method: 'post',
|
|
7
|
+
headers: {
|
|
8
|
+
'content-type': 'text/plain',
|
|
9
|
+
'x-forwarded-for': request.headers.get('x-forwarded-for') || '',
|
|
10
|
+
'user-agent': request.headers.get('user-agent') || '',
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify({
|
|
13
|
+
schema_id: 'hydrogen_buyer_performance/2.0',
|
|
14
|
+
payload: data,
|
|
15
|
+
metadata: {
|
|
16
|
+
event_created_at_ms: initTime,
|
|
17
|
+
event_sent_at_ms: new Date().getTime(),
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}).catch((error) => {
|
|
21
|
+
// send to bugsnag? oxygen?
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function PerformanceMetricsDebug(): null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { ClientAnalytics } from '../../index';
|
|
3
|
+
const PAD = 10;
|
|
4
|
+
let isInit = false;
|
|
5
|
+
export function PerformanceMetricsDebug() {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!isInit) {
|
|
8
|
+
isInit = true;
|
|
9
|
+
ClientAnalytics.subscribe(ClientAnalytics.eventNames.PERFORMANCE, (data) => {
|
|
10
|
+
console.group(`Performance - ${data.page_load_type} load`);
|
|
11
|
+
logMetricIf('TTFB:', data.response_start - data.navigation_start);
|
|
12
|
+
logMetricIf('FCP:', data.first_contentful_paint);
|
|
13
|
+
logMetricIf('LCP:', data.largest_contentful_paint);
|
|
14
|
+
logMetricIf('Duration:', data.response_end - data.navigation_start);
|
|
15
|
+
console.groupEnd();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function logMetricIf(lable, data) {
|
|
22
|
+
data && console.log(`${lable.padEnd(PAD)}${Math.round(data)} ms`);
|
|
23
|
+
}
|
|
@@ -14,7 +14,7 @@ export declare type CookieOptions = {
|
|
|
14
14
|
* within the [defined path](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#path_attribute).
|
|
15
15
|
*/
|
|
16
16
|
path?: string;
|
|
17
|
-
/** [A date
|
|
17
|
+
/** [A date on which the cookie will expire](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_the_lifetime_of_a_cookie).
|
|
18
18
|
* If the date is in the past, then the browser will remove the cookie.
|
|
19
19
|
*/
|
|
20
20
|
expires?: Date;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { matchPath } from '../../utilities/matchPath';
|
|
3
3
|
import { log } from '../../utilities/log';
|
|
4
|
+
import { extractPathFromRoutesKey } from '../../utilities/apiRoutes';
|
|
4
5
|
import { useServerRequest } from '../ServerRequestProvider';
|
|
5
6
|
import { RouteParamsProvider } from '../useRouteParams/RouteParamsProvider.client';
|
|
6
7
|
/**
|
|
@@ -34,24 +35,7 @@ export function createPageRoutes(pages, topLevelPath = '*', dirPrefix) {
|
|
|
34
35
|
const topLevelPrefix = topLevelPath.replace('*', '').replace(/\/$/, '');
|
|
35
36
|
const routes = Object.keys(pages)
|
|
36
37
|
.map((key) => {
|
|
37
|
-
|
|
38
|
-
.replace(dirPrefix, '')
|
|
39
|
-
.replace(/\.server\.(t|j)sx?$/, '')
|
|
40
|
-
/**
|
|
41
|
-
* Replace /index with /
|
|
42
|
-
*/
|
|
43
|
-
.replace(/\/index$/i, '/')
|
|
44
|
-
/**
|
|
45
|
-
* Only lowercase the first letter. This allows the developer to use camelCase
|
|
46
|
-
* dynamic paths while ensuring their standard routes are normalized to lowercase.
|
|
47
|
-
*/
|
|
48
|
-
.replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase())
|
|
49
|
-
/**
|
|
50
|
-
* Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
|
|
51
|
-
*/
|
|
52
|
-
.replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
|
|
53
|
-
if (path.endsWith('/') && path !== '/')
|
|
54
|
-
path = path.substring(0, path.length - 1);
|
|
38
|
+
const path = extractPathFromRoutesKey(key, dirPrefix);
|
|
55
39
|
/**
|
|
56
40
|
* Catch-all routes [...handle].jsx don't need an exact match
|
|
57
41
|
* https://reactrouter.com/core/api/Route/exact-bool
|
|
@@ -29,7 +29,7 @@ async function startFileLock(promise) {
|
|
|
29
29
|
export const FileSessionStorage = function (
|
|
30
30
|
/** The name of the cookie stored in the browser. */
|
|
31
31
|
name,
|
|
32
|
-
/** A directory to store the session files
|
|
32
|
+
/** A directory to store the session files in. Each session is stored in a separate file on the file system. */
|
|
33
33
|
dir,
|
|
34
34
|
/** An optional object to configure [how the cookie is persisted in the browser](https://shopify.dev/api/hydrogen/components/framework/cookie#cookie-options). */
|
|
35
35
|
cookieOptions) {
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import React, { cloneElement } from 'react';
|
|
2
2
|
import { useServerRequest } from '../ServerRequestProvider';
|
|
3
3
|
import { matchPath } from '../../utilities/matchPath';
|
|
4
|
-
import { Boomerang } from '../Boomerang/Boomerang.client';
|
|
5
4
|
import { RouteParamsProvider } from '../useRouteParams/RouteParamsProvider.client';
|
|
6
|
-
import { useServerAnalytics } from '../Analytics';
|
|
7
5
|
/**
|
|
8
6
|
* The `Route` component is used to set up a route in Hydrogen that's independent of the file system. Routes are
|
|
9
7
|
* matched in the order that they're defined.
|
|
10
8
|
*/
|
|
11
9
|
export function Route({ path, page }) {
|
|
12
|
-
var _a;
|
|
13
10
|
const request = useServerRequest();
|
|
14
11
|
const { routeRendered, serverProps } = request.ctx.router;
|
|
15
12
|
if (routeRendered)
|
|
@@ -25,13 +22,7 @@ export function Route({ path, page }) {
|
|
|
25
22
|
if (match) {
|
|
26
23
|
request.ctx.router.routeRendered = true;
|
|
27
24
|
request.ctx.router.routeParams = match.params;
|
|
28
|
-
|
|
29
|
-
useServerAnalytics({
|
|
30
|
-
templateName: name,
|
|
31
|
-
});
|
|
32
|
-
return (React.createElement(RouteParamsProvider, { routeParams: match.params },
|
|
33
|
-
cloneElement(page, { params: match.params || {}, ...serverProps }),
|
|
34
|
-
name ? React.createElement(Boomerang, { pageTemplate: name }) : null));
|
|
25
|
+
return (React.createElement(RouteParamsProvider, { routeParams: match.params }, cloneElement(page, { params: match.params || {}, ...serverProps })));
|
|
35
26
|
}
|
|
36
27
|
return null;
|
|
37
28
|
}
|
|
@@ -10,19 +10,27 @@ export const BrowserRouter = ({ history: pHistory, children, }) => {
|
|
|
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
|
+
const [locationChanged, setLocationChanged] = useState(false);
|
|
13
14
|
const { pending, locationServerProps, setLocationServerProps } = useInternalServerProps();
|
|
14
|
-
useScrollRestoration(
|
|
15
|
+
useScrollRestoration({
|
|
16
|
+
location,
|
|
17
|
+
pending,
|
|
18
|
+
serverProps: locationServerProps,
|
|
19
|
+
locationChanged,
|
|
20
|
+
onFinishNavigating: () => setLocationChanged(false),
|
|
21
|
+
});
|
|
15
22
|
useLayoutEffect(() => {
|
|
16
23
|
const unlisten = history.listen(({ location: newLocation }) => {
|
|
17
24
|
positions[location.key] = window.scrollY;
|
|
18
25
|
setLocationServerProps({
|
|
19
26
|
pathname: newLocation.pathname,
|
|
20
|
-
search:
|
|
27
|
+
search: newLocation.search,
|
|
21
28
|
});
|
|
22
29
|
setLocation(newLocation);
|
|
30
|
+
setLocationChanged(true);
|
|
23
31
|
});
|
|
24
32
|
return () => unlisten();
|
|
25
|
-
}, [history, location]);
|
|
33
|
+
}, [history, location, setLocationChanged]);
|
|
26
34
|
return (React.createElement(RouterContext.Provider, { value: {
|
|
27
35
|
history,
|
|
28
36
|
location,
|
|
@@ -49,7 +57,7 @@ function useBeforeUnload(callback) {
|
|
|
49
57
|
};
|
|
50
58
|
}, [callback]);
|
|
51
59
|
}
|
|
52
|
-
function useScrollRestoration(location, pending, serverProps) {
|
|
60
|
+
function useScrollRestoration({ location, pending, serverProps, locationChanged, onFinishNavigating, }) {
|
|
53
61
|
/**
|
|
54
62
|
* Browsers have an API for scroll restoration. We wait for the page to load first,
|
|
55
63
|
* in case the browser is able to restore scroll position automatically, and then
|
|
@@ -66,7 +74,7 @@ function useScrollRestoration(location, pending, serverProps) {
|
|
|
66
74
|
}, []));
|
|
67
75
|
useLayoutEffect(() => {
|
|
68
76
|
// The app has just loaded
|
|
69
|
-
if (isFirstLoad) {
|
|
77
|
+
if (isFirstLoad || !locationChanged) {
|
|
70
78
|
isFirstLoad = false;
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
@@ -87,15 +95,27 @@ function useScrollRestoration(location, pending, serverProps) {
|
|
|
87
95
|
const element = document.querySelector(location.hash);
|
|
88
96
|
if (element) {
|
|
89
97
|
element.scrollIntoView();
|
|
98
|
+
onFinishNavigating();
|
|
90
99
|
return;
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
// If we have a matching position, scroll to it
|
|
94
103
|
if (position) {
|
|
95
104
|
window.scrollTo(0, position);
|
|
105
|
+
onFinishNavigating();
|
|
96
106
|
return;
|
|
97
107
|
}
|
|
98
108
|
// Scroll to the top of new pages
|
|
99
109
|
window.scrollTo(0, 0);
|
|
100
|
-
|
|
110
|
+
onFinishNavigating();
|
|
111
|
+
}, [
|
|
112
|
+
location.pathname,
|
|
113
|
+
location.search,
|
|
114
|
+
location.hash,
|
|
115
|
+
pending,
|
|
116
|
+
serverProps.pathname,
|
|
117
|
+
serverProps.search,
|
|
118
|
+
locationChanged,
|
|
119
|
+
onFinishNavigating,
|
|
120
|
+
]);
|
|
101
121
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { createContext, useMemo, useCallback,
|
|
2
2
|
// @ts-ignore
|
|
3
|
-
useTransition, useState,
|
|
3
|
+
useTransition, useState, } from 'react';
|
|
4
4
|
const PRIVATE_PROPS = ['request', 'response'];
|
|
5
5
|
export const ServerPropsContext = createContext(null);
|
|
6
6
|
export function ServerPropsProvider({ initialServerProps, setServerPropsForRsc, children, }) {
|
|
@@ -8,12 +8,18 @@ export function ServerPropsProvider({ initialServerProps, setServerPropsForRsc,
|
|
|
8
8
|
const [serverProps, setServerProps] = useState({});
|
|
9
9
|
const [pending, startTransition] = useTransition();
|
|
10
10
|
const setServerPropsCallback = useCallback((input, propValue) => {
|
|
11
|
-
|
|
11
|
+
startTransition(() => {
|
|
12
|
+
setServerProps((prev) => getNewValue(prev, input, propValue));
|
|
13
|
+
setServerPropsForRsc((prev) => getNewValue(prev, input, propValue));
|
|
14
|
+
});
|
|
12
15
|
}, []);
|
|
13
16
|
const setLocationServerPropsCallback = useCallback((input, propValue) => {
|
|
14
17
|
// Flush the existing user server state when location changes, leaving only the persisted state
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
startTransition(() => {
|
|
19
|
+
setServerPropsForRsc((prev) => getNewValue(prev, input, propValue));
|
|
20
|
+
setServerProps({});
|
|
21
|
+
setLocationServerProps((prev) => getNewValue(prev, input, propValue));
|
|
22
|
+
});
|
|
17
23
|
}, []);
|
|
18
24
|
const getProposedLocationServerPropsCallback = useCallback((input, propValue) => {
|
|
19
25
|
return getNewValue(locationServerProps, input, propValue);
|
|
@@ -40,34 +46,17 @@ export function ServerPropsProvider({ initialServerProps, setServerPropsForRsc,
|
|
|
40
46
|
...newValue,
|
|
41
47
|
};
|
|
42
48
|
}
|
|
43
|
-
const resolvedServerPropsForRsc = useMemo(() => {
|
|
44
|
-
return {
|
|
45
|
-
...serverProps,
|
|
46
|
-
...locationServerProps,
|
|
47
|
-
};
|
|
48
|
-
}, [serverProps, locationServerProps]);
|
|
49
|
-
const resolvedServerProps = useMemo(() => {
|
|
50
|
-
return {
|
|
51
|
-
...serverProps,
|
|
52
|
-
};
|
|
53
|
-
}, [serverProps]);
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
startTransition(() => {
|
|
56
|
-
setServerPropsForRsc(resolvedServerPropsForRsc);
|
|
57
|
-
});
|
|
58
|
-
return () => { };
|
|
59
|
-
}, [resolvedServerPropsForRsc]);
|
|
60
49
|
const value = useMemo(() => ({
|
|
61
50
|
pending,
|
|
62
51
|
locationServerProps: locationServerProps,
|
|
63
|
-
serverProps
|
|
52
|
+
serverProps,
|
|
64
53
|
setServerProps: setServerPropsCallback,
|
|
65
54
|
setLocationServerProps: setLocationServerPropsCallback,
|
|
66
55
|
getProposedLocationServerProps: getProposedLocationServerPropsCallback,
|
|
67
56
|
}), [
|
|
68
57
|
pending,
|
|
69
58
|
locationServerProps,
|
|
70
|
-
|
|
59
|
+
serverProps,
|
|
71
60
|
setServerPropsCallback,
|
|
72
61
|
setLocationServerPropsCallback,
|
|
73
62
|
getProposedLocationServerPropsCallback,
|
|
@@ -19,7 +19,7 @@ export default () => {
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
resolveId(source, importer) {
|
|
22
|
-
if (normalizePath(source).includes('
|
|
22
|
+
if (normalizePath(source).includes('/hydrogen/platforms/')) {
|
|
23
23
|
const hydrogenPath = path.dirname(require.resolve('@shopify/hydrogen/package.json'));
|
|
24
24
|
const platformEntryName = source.split(path.sep).pop() || '';
|
|
25
25
|
const platformEntryPath = path.resolve(hydrogenPath, 'dist', 'esnext', 'platforms', platformEntryName);
|
|
@@ -30,7 +30,7 @@ export default () => {
|
|
|
30
30
|
return null;
|
|
31
31
|
},
|
|
32
32
|
transform(code, id) {
|
|
33
|
-
if (normalizePath(id).includes('
|
|
33
|
+
if (normalizePath(id).includes('/hydrogen/dist/esnext/platforms/')) {
|
|
34
34
|
code = code
|
|
35
35
|
.replace('__SERVER_ENTRY__', process.env.HYDROGEN_SERVER_ENTRY || HYDROGEN_DEFAULT_SERVER_ENTRY)
|
|
36
36
|
.replace('__INDEX_TEMPLATE__', normalizePath(path.resolve(config.root, config.build.outDir, '..', 'client', 'index.html')));
|
|
@@ -7,8 +7,6 @@ import { flattenConnection, parseMetafieldValue } from '../../utilities';
|
|
|
7
7
|
export function useParsedMetafields(
|
|
8
8
|
/** A [MetafieldConnection](https://shopify.dev/api/storefront/reference/common-objects/metafieldconnection). */
|
|
9
9
|
metafields) {
|
|
10
|
-
var _a, _b;
|
|
11
|
-
(_b = (_a = metafields === null || metafields === void 0 ? void 0 : metafields.edges) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.node;
|
|
12
10
|
return useMemo(() => {
|
|
13
11
|
if (!metafields) {
|
|
14
12
|
throw new Error(`'useParsedMetafields' needs metafields`);
|
package/dist/esnext/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { CartQuery } from './components/CartProvider/cart-queries';
|
|
|
15
15
|
export { generateCacheControlHeader, NoStore, CacheSeconds, CacheMinutes, CacheHours, CacheDays, CacheWeeks, CacheMonths, CacheCustom, } from './framework/CachingStrategy';
|
|
16
16
|
export { fetchSync } from './foundation/fetchSync/server/fetchSync';
|
|
17
17
|
export { useServerAnalytics } from './foundation/Analytics';
|
|
18
|
+
export * as PerformanceMetricsServerAnalyticsConnector from './foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server';
|
|
18
19
|
export { useSession } from './foundation/useSession/useSession';
|
|
19
20
|
export { CookieSessionStorage } from './foundation/CookieSessionStorage/CookieSessionStorage';
|
|
20
21
|
export { MemorySessionStorage } from './foundation/MemorySessionStorage/MemorySessionStorage';
|
package/dist/esnext/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { CartQuery } from './components/CartProvider/cart-queries';
|
|
|
19
19
|
export { generateCacheControlHeader, NoStore, CacheSeconds, CacheMinutes, CacheHours, CacheDays, CacheWeeks, CacheMonths, CacheCustom, } from './framework/CachingStrategy';
|
|
20
20
|
export { fetchSync } from './foundation/fetchSync/server/fetchSync';
|
|
21
21
|
export { useServerAnalytics } from './foundation/Analytics';
|
|
22
|
+
export * as PerformanceMetricsServerAnalyticsConnector from './foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.server';
|
|
22
23
|
export { useSession } from './foundation/useSession/useSession';
|
|
23
24
|
export { CookieSessionStorage } from './foundation/CookieSessionStorage/CookieSessionStorage';
|
|
24
25
|
export { MemorySessionStorage } from './foundation/MemorySessionStorage/MemorySessionStorage';
|
|
@@ -19,6 +19,7 @@ export declare type ApiRouteMatch = {
|
|
|
19
19
|
hasServerComponent: boolean;
|
|
20
20
|
params: RouteParams;
|
|
21
21
|
};
|
|
22
|
+
export declare function extractPathFromRoutesKey(routesKey: string, dirPrefix: string): string;
|
|
22
23
|
export declare function getApiRoutes(pages: ImportGlobEagerOutput | undefined, topLevelPath?: string): Array<HydrogenApiRoute>;
|
|
23
24
|
export declare function getApiRouteFromURL(url: URL, routes: Array<HydrogenApiRoute>): ApiRouteMatch | null;
|
|
24
25
|
/** The `queryShop` utility is a function that helps you query the Storefront API.
|
|
@@ -5,6 +5,28 @@ import { getStorefrontApiRequestHeaders } from './storefrontApi';
|
|
|
5
5
|
import { emptySessionImplementation, } from '../foundation/session/session';
|
|
6
6
|
let memoizedRoutes = [];
|
|
7
7
|
let memoizedPages = {};
|
|
8
|
+
export function extractPathFromRoutesKey(routesKey, dirPrefix) {
|
|
9
|
+
let path = routesKey
|
|
10
|
+
.replace(dirPrefix, '')
|
|
11
|
+
.replace(/\.server\.(t|j)sx?$/, '')
|
|
12
|
+
/**
|
|
13
|
+
* Replace /index with /
|
|
14
|
+
*/
|
|
15
|
+
.replace(/\/index$/i, '/')
|
|
16
|
+
/**
|
|
17
|
+
* Only lowercase the first letter. This allows the developer to use camelCase
|
|
18
|
+
* dynamic paths while ensuring their standard routes are normalized to lowercase.
|
|
19
|
+
*/
|
|
20
|
+
.replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase())
|
|
21
|
+
/**
|
|
22
|
+
* Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
|
|
23
|
+
*/
|
|
24
|
+
.replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
|
|
25
|
+
if (path.endsWith('/') && path !== '/') {
|
|
26
|
+
path = path.substring(0, path.length - 1);
|
|
27
|
+
}
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
8
30
|
export function getApiRoutes(pages, topLevelPath = '*') {
|
|
9
31
|
if (!pages || memoizedPages === pages)
|
|
10
32
|
return memoizedRoutes;
|
|
@@ -12,24 +34,7 @@ export function getApiRoutes(pages, topLevelPath = '*') {
|
|
|
12
34
|
const routes = Object.keys(pages)
|
|
13
35
|
.filter((key) => pages[key].api)
|
|
14
36
|
.map((key) => {
|
|
15
|
-
|
|
16
|
-
.replace('./routes', '')
|
|
17
|
-
.replace(/\.server\.(t|j)sx?$/, '')
|
|
18
|
-
/**
|
|
19
|
-
* Replace /index with /
|
|
20
|
-
*/
|
|
21
|
-
.replace(/\/index$/i, '/')
|
|
22
|
-
/**
|
|
23
|
-
* Only lowercase the first letter. This allows the developer to use camelCase
|
|
24
|
-
* dynamic paths while ensuring their standard routes are normalized to lowercase.
|
|
25
|
-
*/
|
|
26
|
-
.replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase())
|
|
27
|
-
/**
|
|
28
|
-
* Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
|
|
29
|
-
*/
|
|
30
|
-
.replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
|
|
31
|
-
if (path.endsWith('/') && path !== '/')
|
|
32
|
-
path = path.substring(0, path.length - 1);
|
|
37
|
+
const path = extractPathFromRoutesKey(key, './routes');
|
|
33
38
|
/**
|
|
34
39
|
* Catch-all routes [...handle].jsx don't need an exact match
|
|
35
40
|
* https://reactrouter.com/core/api/Route/exact-bool
|
package/dist/esnext/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const LIB_VERSION = "0.17.
|
|
1
|
+
export declare const LIB_VERSION = "0.17.3";
|
package/dist/esnext/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LIB_VERSION = '0.17.
|
|
1
|
+
export const LIB_VERSION = '0.17.3';
|
|
@@ -148,7 +148,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
148
148
|
* TODO: Also add `Vary` headers for `accept-language` and any other keys
|
|
149
149
|
* we want to shard our full-page cache for all Hydrogen storefronts.
|
|
150
150
|
*/
|
|
151
|
-
headers
|
|
151
|
+
headers.set('cache-control', componentResponse.cacheControlHeader);
|
|
152
152
|
if (componentResponse.customBody) {
|
|
153
153
|
// This can be used to return sitemap.xml or any other custom response.
|
|
154
154
|
postRequestTasks('ssr', status, request, componentResponse);
|
|
@@ -158,7 +158,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
158
158
|
headers,
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
-
headers
|
|
161
|
+
headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
|
|
162
162
|
html = (0, Html_1.applyHtmlHead)(html, request.ctx.head, template);
|
|
163
163
|
if (flight) {
|
|
164
164
|
html = html.replace('</body>', () => `${flightContainer({
|
|
@@ -247,8 +247,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
247
247
|
* queries which might be caught behind Suspense. Clarify this or add
|
|
248
248
|
* additional checks downstream?
|
|
249
249
|
*/
|
|
250
|
-
responseOptions.headers
|
|
251
|
-
componentResponse.cacheControlHeader;
|
|
250
|
+
responseOptions.headers.set('cache-control', componentResponse.cacheControlHeader);
|
|
252
251
|
if (isRedirect(responseOptions)) {
|
|
253
252
|
return false;
|
|
254
253
|
}
|
|
@@ -257,7 +256,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
257
256
|
writable.write(encoder.encode(await componentResponse.customBody));
|
|
258
257
|
return false;
|
|
259
258
|
}
|
|
260
|
-
responseOptions.headers
|
|
259
|
+
responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
|
|
261
260
|
writable.write(encoder.encode(DOCTYPE));
|
|
262
261
|
if (didError) {
|
|
263
262
|
// This error was delayed until the headers were properly sent.
|
|
@@ -514,8 +513,7 @@ function startWritingHtmlToServerResponse(response, error) {
|
|
|
514
513
|
function getResponseOptions({ headers, status, customStatus }, error) {
|
|
515
514
|
var _a, _b;
|
|
516
515
|
const responseInit = {};
|
|
517
|
-
|
|
518
|
-
responseInit.headers = Object.fromEntries(headers.entries());
|
|
516
|
+
responseInit.headers = headers;
|
|
519
517
|
if (error) {
|
|
520
518
|
responseInit.status = 500;
|
|
521
519
|
}
|
|
@@ -536,7 +534,7 @@ function writeHeadToServerResponse(response, serverComponentResponse, log, error
|
|
|
536
534
|
if (statusText) {
|
|
537
535
|
response.statusMessage = statusText;
|
|
538
536
|
}
|
|
539
|
-
Object.entries(headers).forEach(([key, value]) => response.setHeader(key, value));
|
|
537
|
+
Object.entries(headers.raw()).forEach(([key, value]) => response.setHeader(key, value));
|
|
540
538
|
}
|
|
541
539
|
function isRedirect(response) {
|
|
542
540
|
var _a, _b;
|
|
@@ -81,7 +81,13 @@ function subscribe(eventname, callbackFunction) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
function pushToServer(init, searchParam) {
|
|
84
|
-
return fetch(`${constants_1.EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`,
|
|
84
|
+
return fetch(`${constants_1.EVENT_PATHNAME}${searchParam ? `?${searchParam}` : ''}`, Object.assign({
|
|
85
|
+
method: 'post',
|
|
86
|
+
headers: {
|
|
87
|
+
'cache-control': 'no-cache',
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
},
|
|
90
|
+
}, init));
|
|
85
91
|
}
|
|
86
92
|
exports.ClientAnalytics = {
|
|
87
93
|
pushToPageAnalyticsData,
|
|
@@ -36,19 +36,27 @@ const BrowserRouter = ({ history: pHistory, children, }) => {
|
|
|
36
36
|
return react_1.default.createElement(react_1.default.Fragment, null, children);
|
|
37
37
|
const history = (0, react_1.useMemo)(() => pHistory || (0, history_1.createBrowserHistory)(), [pHistory]);
|
|
38
38
|
const [location, setLocation] = (0, react_1.useState)(history.location);
|
|
39
|
+
const [locationChanged, setLocationChanged] = (0, react_1.useState)(false);
|
|
39
40
|
const { pending, locationServerProps, setLocationServerProps } = (0, use_server_props_1.useInternalServerProps)();
|
|
40
|
-
useScrollRestoration(
|
|
41
|
+
useScrollRestoration({
|
|
42
|
+
location,
|
|
43
|
+
pending,
|
|
44
|
+
serverProps: locationServerProps,
|
|
45
|
+
locationChanged,
|
|
46
|
+
onFinishNavigating: () => setLocationChanged(false),
|
|
47
|
+
});
|
|
41
48
|
(0, react_1.useLayoutEffect)(() => {
|
|
42
49
|
const unlisten = history.listen(({ location: newLocation }) => {
|
|
43
50
|
positions[location.key] = window.scrollY;
|
|
44
51
|
setLocationServerProps({
|
|
45
52
|
pathname: newLocation.pathname,
|
|
46
|
-
search:
|
|
53
|
+
search: newLocation.search,
|
|
47
54
|
});
|
|
48
55
|
setLocation(newLocation);
|
|
56
|
+
setLocationChanged(true);
|
|
49
57
|
});
|
|
50
58
|
return () => unlisten();
|
|
51
|
-
}, [history, location]);
|
|
59
|
+
}, [history, location, setLocationChanged]);
|
|
52
60
|
return (react_1.default.createElement(exports.RouterContext.Provider, { value: {
|
|
53
61
|
history,
|
|
54
62
|
location,
|
|
@@ -78,7 +86,7 @@ function useBeforeUnload(callback) {
|
|
|
78
86
|
};
|
|
79
87
|
}, [callback]);
|
|
80
88
|
}
|
|
81
|
-
function useScrollRestoration(location, pending, serverProps) {
|
|
89
|
+
function useScrollRestoration({ location, pending, serverProps, locationChanged, onFinishNavigating, }) {
|
|
82
90
|
/**
|
|
83
91
|
* Browsers have an API for scroll restoration. We wait for the page to load first,
|
|
84
92
|
* in case the browser is able to restore scroll position automatically, and then
|
|
@@ -95,7 +103,7 @@ function useScrollRestoration(location, pending, serverProps) {
|
|
|
95
103
|
}, []));
|
|
96
104
|
(0, react_1.useLayoutEffect)(() => {
|
|
97
105
|
// The app has just loaded
|
|
98
|
-
if (isFirstLoad) {
|
|
106
|
+
if (isFirstLoad || !locationChanged) {
|
|
99
107
|
isFirstLoad = false;
|
|
100
108
|
return;
|
|
101
109
|
}
|
|
@@ -116,15 +124,27 @@ function useScrollRestoration(location, pending, serverProps) {
|
|
|
116
124
|
const element = document.querySelector(location.hash);
|
|
117
125
|
if (element) {
|
|
118
126
|
element.scrollIntoView();
|
|
127
|
+
onFinishNavigating();
|
|
119
128
|
return;
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
// If we have a matching position, scroll to it
|
|
123
132
|
if (position) {
|
|
124
133
|
window.scrollTo(0, position);
|
|
134
|
+
onFinishNavigating();
|
|
125
135
|
return;
|
|
126
136
|
}
|
|
127
137
|
// Scroll to the top of new pages
|
|
128
138
|
window.scrollTo(0, 0);
|
|
129
|
-
|
|
139
|
+
onFinishNavigating();
|
|
140
|
+
}, [
|
|
141
|
+
location.pathname,
|
|
142
|
+
location.search,
|
|
143
|
+
location.hash,
|
|
144
|
+
pending,
|
|
145
|
+
serverProps.pathname,
|
|
146
|
+
serverProps.search,
|
|
147
|
+
locationChanged,
|
|
148
|
+
onFinishNavigating,
|
|
149
|
+
]);
|
|
130
150
|
}
|
|
@@ -32,12 +32,18 @@ function ServerPropsProvider({ initialServerProps, setServerPropsForRsc, childre
|
|
|
32
32
|
const [serverProps, setServerProps] = (0, react_1.useState)({});
|
|
33
33
|
const [pending, startTransition] = (0, react_1.useTransition)();
|
|
34
34
|
const setServerPropsCallback = (0, react_1.useCallback)((input, propValue) => {
|
|
35
|
-
|
|
35
|
+
startTransition(() => {
|
|
36
|
+
setServerProps((prev) => getNewValue(prev, input, propValue));
|
|
37
|
+
setServerPropsForRsc((prev) => getNewValue(prev, input, propValue));
|
|
38
|
+
});
|
|
36
39
|
}, []);
|
|
37
40
|
const setLocationServerPropsCallback = (0, react_1.useCallback)((input, propValue) => {
|
|
38
41
|
// Flush the existing user server state when location changes, leaving only the persisted state
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
startTransition(() => {
|
|
43
|
+
setServerPropsForRsc((prev) => getNewValue(prev, input, propValue));
|
|
44
|
+
setServerProps({});
|
|
45
|
+
setLocationServerProps((prev) => getNewValue(prev, input, propValue));
|
|
46
|
+
});
|
|
41
47
|
}, []);
|
|
42
48
|
const getProposedLocationServerPropsCallback = (0, react_1.useCallback)((input, propValue) => {
|
|
43
49
|
return getNewValue(locationServerProps, input, propValue);
|
|
@@ -64,34 +70,17 @@ function ServerPropsProvider({ initialServerProps, setServerPropsForRsc, childre
|
|
|
64
70
|
...newValue,
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
|
-
const resolvedServerPropsForRsc = (0, react_1.useMemo)(() => {
|
|
68
|
-
return {
|
|
69
|
-
...serverProps,
|
|
70
|
-
...locationServerProps,
|
|
71
|
-
};
|
|
72
|
-
}, [serverProps, locationServerProps]);
|
|
73
|
-
const resolvedServerProps = (0, react_1.useMemo)(() => {
|
|
74
|
-
return {
|
|
75
|
-
...serverProps,
|
|
76
|
-
};
|
|
77
|
-
}, [serverProps]);
|
|
78
|
-
(0, react_1.useEffect)(() => {
|
|
79
|
-
startTransition(() => {
|
|
80
|
-
setServerPropsForRsc(resolvedServerPropsForRsc);
|
|
81
|
-
});
|
|
82
|
-
return () => { };
|
|
83
|
-
}, [resolvedServerPropsForRsc]);
|
|
84
73
|
const value = (0, react_1.useMemo)(() => ({
|
|
85
74
|
pending,
|
|
86
75
|
locationServerProps: locationServerProps,
|
|
87
|
-
serverProps
|
|
76
|
+
serverProps,
|
|
88
77
|
setServerProps: setServerPropsCallback,
|
|
89
78
|
setLocationServerProps: setLocationServerPropsCallback,
|
|
90
79
|
getProposedLocationServerProps: getProposedLocationServerPropsCallback,
|
|
91
80
|
}), [
|
|
92
81
|
pending,
|
|
93
82
|
locationServerProps,
|
|
94
|
-
|
|
83
|
+
serverProps,
|
|
95
84
|
setServerPropsCallback,
|
|
96
85
|
setLocationServerPropsCallback,
|
|
97
86
|
getProposedLocationServerPropsCallback,
|
|
@@ -24,7 +24,7 @@ exports.default = () => {
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
resolveId(source, importer) {
|
|
27
|
-
if ((0, vite_1.normalizePath)(source).includes('
|
|
27
|
+
if ((0, vite_1.normalizePath)(source).includes('/hydrogen/platforms/')) {
|
|
28
28
|
const hydrogenPath = path_1.default.dirname(require.resolve('@shopify/hydrogen/package.json'));
|
|
29
29
|
const platformEntryName = source.split(path_1.default.sep).pop() || '';
|
|
30
30
|
const platformEntryPath = path_1.default.resolve(hydrogenPath, 'dist', 'esnext', 'platforms', platformEntryName);
|
|
@@ -35,7 +35,7 @@ exports.default = () => {
|
|
|
35
35
|
return null;
|
|
36
36
|
},
|
|
37
37
|
transform(code, id) {
|
|
38
|
-
if ((0, vite_1.normalizePath)(id).includes('
|
|
38
|
+
if ((0, vite_1.normalizePath)(id).includes('/hydrogen/dist/esnext/platforms/')) {
|
|
39
39
|
code = code
|
|
40
40
|
.replace('__SERVER_ENTRY__', process.env.HYDROGEN_SERVER_ENTRY || vite_plugin_hydrogen_middleware_1.HYDROGEN_DEFAULT_SERVER_ENTRY)
|
|
41
41
|
.replace('__INDEX_TEMPLATE__', (0, vite_1.normalizePath)(path_1.default.resolve(config.root, config.build.outDir, '..', 'client', 'index.html')));
|
|
@@ -19,6 +19,7 @@ export declare type ApiRouteMatch = {
|
|
|
19
19
|
hasServerComponent: boolean;
|
|
20
20
|
params: RouteParams;
|
|
21
21
|
};
|
|
22
|
+
export declare function extractPathFromRoutesKey(routesKey: string, dirPrefix: string): string;
|
|
22
23
|
export declare function getApiRoutes(pages: ImportGlobEagerOutput | undefined, topLevelPath?: string): Array<HydrogenApiRoute>;
|
|
23
24
|
export declare function getApiRouteFromURL(url: URL, routes: Array<HydrogenApiRoute>): ApiRouteMatch | null;
|
|
24
25
|
/** The `queryShop` utility is a function that helps you query the Storefront API.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renderApiRoute = exports.getApiRouteFromURL = exports.getApiRoutes = void 0;
|
|
3
|
+
exports.renderApiRoute = exports.getApiRouteFromURL = exports.getApiRoutes = exports.extractPathFromRoutesKey = void 0;
|
|
4
4
|
const matchPath_1 = require("./matchPath");
|
|
5
5
|
const log_1 = require("../utilities/log/");
|
|
6
6
|
const fetch_1 = require("./fetch");
|
|
@@ -8,6 +8,29 @@ const storefrontApi_1 = require("./storefrontApi");
|
|
|
8
8
|
const session_1 = require("../foundation/session/session");
|
|
9
9
|
let memoizedRoutes = [];
|
|
10
10
|
let memoizedPages = {};
|
|
11
|
+
function extractPathFromRoutesKey(routesKey, dirPrefix) {
|
|
12
|
+
let path = routesKey
|
|
13
|
+
.replace(dirPrefix, '')
|
|
14
|
+
.replace(/\.server\.(t|j)sx?$/, '')
|
|
15
|
+
/**
|
|
16
|
+
* Replace /index with /
|
|
17
|
+
*/
|
|
18
|
+
.replace(/\/index$/i, '/')
|
|
19
|
+
/**
|
|
20
|
+
* Only lowercase the first letter. This allows the developer to use camelCase
|
|
21
|
+
* dynamic paths while ensuring their standard routes are normalized to lowercase.
|
|
22
|
+
*/
|
|
23
|
+
.replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase())
|
|
24
|
+
/**
|
|
25
|
+
* Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
|
|
26
|
+
*/
|
|
27
|
+
.replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
|
|
28
|
+
if (path.endsWith('/') && path !== '/') {
|
|
29
|
+
path = path.substring(0, path.length - 1);
|
|
30
|
+
}
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
exports.extractPathFromRoutesKey = extractPathFromRoutesKey;
|
|
11
34
|
function getApiRoutes(pages, topLevelPath = '*') {
|
|
12
35
|
if (!pages || memoizedPages === pages)
|
|
13
36
|
return memoizedRoutes;
|
|
@@ -15,24 +38,7 @@ function getApiRoutes(pages, topLevelPath = '*') {
|
|
|
15
38
|
const routes = Object.keys(pages)
|
|
16
39
|
.filter((key) => pages[key].api)
|
|
17
40
|
.map((key) => {
|
|
18
|
-
|
|
19
|
-
.replace('./routes', '')
|
|
20
|
-
.replace(/\.server\.(t|j)sx?$/, '')
|
|
21
|
-
/**
|
|
22
|
-
* Replace /index with /
|
|
23
|
-
*/
|
|
24
|
-
.replace(/\/index$/i, '/')
|
|
25
|
-
/**
|
|
26
|
-
* Only lowercase the first letter. This allows the developer to use camelCase
|
|
27
|
-
* dynamic paths while ensuring their standard routes are normalized to lowercase.
|
|
28
|
-
*/
|
|
29
|
-
.replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase())
|
|
30
|
-
/**
|
|
31
|
-
* Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
|
|
32
|
-
*/
|
|
33
|
-
.replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
|
|
34
|
-
if (path.endsWith('/') && path !== '/')
|
|
35
|
-
path = path.substring(0, path.length - 1);
|
|
41
|
+
const path = extractPathFromRoutesKey(key, './routes');
|
|
36
42
|
/**
|
|
37
43
|
* Catch-all routes [...handle].jsx don't need an exact match
|
|
38
44
|
* https://reactrouter.com/core/api/Route/exact-bool
|
package/dist/node/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const LIB_VERSION = "0.17.
|
|
1
|
+
export declare const LIB_VERSION = "0.17.3";
|
package/dist/node/version.js
CHANGED
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=14"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.17.
|
|
10
|
+
"version": "0.17.3",
|
|
11
11
|
"description": "Modern custom Shopify storefronts",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"main": "dist/esnext/index.js",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"rimraf": "^3.0.2"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
|
-
"body-parser": "^1.
|
|
84
|
+
"body-parser": "^1.20.0",
|
|
85
85
|
"compression": "^1.7.4",
|
|
86
86
|
"react": "0.0.0-experimental-2bf7c02f0-20220314",
|
|
87
87
|
"react-dom": "0.0.0-experimental-2bf7c02f0-20220314",
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
2
|
-
import { loadScript } from '../../utilities';
|
|
3
|
-
import { useShop } from '../useShop';
|
|
4
|
-
const URL = 'https://cdn.shopify.com/shopifycloud/boomerang/shopify-boomerang-hydrogen.min.js';
|
|
5
|
-
export function Boomerang({ pageTemplate }) {
|
|
6
|
-
const { storeDomain } = useShop();
|
|
7
|
-
const templateName = pageTemplate && pageTemplate !== null
|
|
8
|
-
? pageTemplate.toLowerCase()
|
|
9
|
-
: 'not-set';
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
(function () {
|
|
12
|
-
function boomerangAddVar() {
|
|
13
|
-
if (window.BOOMR && window.BOOMR.addVar) {
|
|
14
|
-
window.BOOMR.addVar('page_template', templateName);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
// Executes on every mount
|
|
18
|
-
boomerangAddVar();
|
|
19
|
-
if (window.BOOMR &&
|
|
20
|
-
(window.BOOMR.version || window.BOOMR.snippetExecuted)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
// Executes only on first mount
|
|
24
|
-
window.BOOMR = window.BOOMR || {};
|
|
25
|
-
window.BOOMR.storeDomain = storeDomain;
|
|
26
|
-
window.BOOMR.pageTemplate = templateName;
|
|
27
|
-
function boomerangSaveLoadTime(e) {
|
|
28
|
-
window.BOOMR_onload = (e && e.timeStamp) || Date.now();
|
|
29
|
-
}
|
|
30
|
-
// @ts-ignore
|
|
31
|
-
function boomerangInit(e) {
|
|
32
|
-
e.detail.BOOMR.init({
|
|
33
|
-
producer_url: 'https://monorail-edge.shopifysvc.com/v1/produce',
|
|
34
|
-
});
|
|
35
|
-
e.detail.BOOMR.t_end = Date.now();
|
|
36
|
-
boomerangAddVar();
|
|
37
|
-
}
|
|
38
|
-
if (window.addEventListener) {
|
|
39
|
-
window.addEventListener('load', boomerangSaveLoadTime, false);
|
|
40
|
-
// @ts-ignore
|
|
41
|
-
}
|
|
42
|
-
else if (window.attachEvent) {
|
|
43
|
-
// @ts-ignore
|
|
44
|
-
window.attachEvent('onload', boomerangSaveLoadTime);
|
|
45
|
-
}
|
|
46
|
-
if (document.addEventListener) {
|
|
47
|
-
document.addEventListener('onBoomerangLoaded', boomerangInit);
|
|
48
|
-
// @ts-ignore
|
|
49
|
-
}
|
|
50
|
-
else if (document.attachEvent) {
|
|
51
|
-
// @ts-ignore
|
|
52
|
-
document.attachEvent('onpropertychange', function (e) {
|
|
53
|
-
if (!e)
|
|
54
|
-
e = event;
|
|
55
|
-
if (e.propertyName === 'onBoomerangLoaded')
|
|
56
|
-
boomerangInit(e);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
})();
|
|
60
|
-
loadScript(URL).catch(() => {
|
|
61
|
-
// ignore if boomerang doesn't load
|
|
62
|
-
// most likely because of a ad blocker
|
|
63
|
-
});
|
|
64
|
-
}, [storeDomain, pageTemplate]);
|
|
65
|
-
return null;
|
|
66
|
-
}
|