@shopify/hydrogen 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +136 -1
- package/config.js +1 -0
- package/dist/esnext/client.d.ts +2 -0
- package/dist/esnext/client.js +2 -0
- package/dist/esnext/components/AddToCartButton/AddToCartButton.client.js +2 -2
- package/dist/esnext/components/CartProvider/CartProvider.client.js +15 -14
- package/dist/esnext/components/CartProvider/{hooks.d.ts → hooks.client.d.ts} +0 -0
- package/dist/esnext/components/CartProvider/{hooks.js → hooks.client.js} +0 -0
- package/dist/esnext/components/CartProvider/index.d.ts +1 -1
- package/dist/esnext/components/CartProvider/index.js +1 -1
- package/dist/esnext/components/{DevTools.d.ts → DevTools.client.d.ts} +0 -0
- package/dist/esnext/components/{DevTools.js → DevTools.client.js} +3 -2
- package/dist/esnext/components/Image/Image.js +2 -0
- package/dist/esnext/components/Link/Link.client.js +11 -2
- package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.js +1 -1
- package/dist/esnext/components/Metafield/Metafield.client.js +4 -5
- package/dist/esnext/components/ModelViewer/ModelViewer.client.d.ts +1 -1
- package/dist/esnext/components/ModelViewer/ModelViewer.client.js +2 -2
- package/dist/esnext/components/Money/Money.client.d.ts +5 -1
- package/dist/esnext/components/Money/Money.client.js +16 -3
- package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +1 -1
- package/dist/esnext/components/ProductProvider/ProductOptionsProvider.client.js +1 -1
- package/dist/esnext/components/ProductProvider/ProductProvider.client.d.ts +7 -3
- package/dist/esnext/components/ProductProvider/ProductProvider.client.js +1 -1
- package/dist/esnext/components/ShopPayButton/ShopPayButton.client.js +6 -2
- package/dist/esnext/components/Video/Video.js +3 -1
- package/dist/esnext/config.d.ts +3 -0
- package/dist/esnext/config.js +1 -0
- package/dist/esnext/constants.js +1 -1
- package/dist/esnext/entry-client.js +3 -1
- package/dist/esnext/entry-server.d.ts +2 -2
- package/dist/esnext/entry-server.js +59 -56
- 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.js +2 -1
- package/dist/esnext/foundation/FileRoutes/FileRoutes.server.d.ts +4 -4
- package/dist/esnext/foundation/FileRoutes/FileRoutes.server.js +18 -21
- package/dist/esnext/foundation/FileSessionStorage/FileSessionStorage.js +2 -1
- package/dist/esnext/foundation/Redirect/Redirect.client.js +1 -0
- package/dist/esnext/foundation/Route/Route.server.js +1 -10
- package/dist/esnext/foundation/Router/BrowserRouter.client.js +34 -5
- package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +5 -3
- package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.d.ts +2 -2
- package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +7 -2
- package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.js +6 -1
- package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.d.ts +8 -1
- package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.js +31 -5
- package/dist/esnext/foundation/ShopifyProvider/types.d.ts +3 -4
- package/dist/esnext/foundation/fetchSync/client/fetchSync.js +2 -1
- package/dist/esnext/foundation/fetchSync/server/fetchSync.js +4 -2
- package/dist/esnext/foundation/ssr-interop.js +1 -1
- package/dist/esnext/foundation/useQuery/hooks.d.ts +1 -1
- package/dist/esnext/foundation/useQuery/hooks.js +2 -2
- package/dist/esnext/foundation/useShop/use-shop.d.ts +3 -1
- package/dist/esnext/foundation/useShop/use-shop.js +3 -1
- package/dist/esnext/foundation/useUrl/useUrl.js +7 -4
- package/dist/esnext/framework/Hydration/Html.js +1 -1
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +3 -2
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +16 -7
- package/dist/esnext/framework/middleware.d.ts +3 -4
- package/dist/esnext/framework/middleware.js +4 -4
- package/dist/esnext/framework/plugin.d.ts +2 -2
- package/dist/esnext/framework/plugin.js +3 -3
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +1 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +2 -2
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +59 -4
- package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +2 -2
- package/dist/esnext/hooks/useLoadScript/index.d.ts +1 -1
- package/dist/esnext/hooks/useLoadScript/index.js +1 -1
- package/dist/esnext/hooks/useLoadScript/{useLoadScript.d.ts → useLoadScript.client.d.ts} +0 -0
- package/dist/esnext/hooks/useLoadScript/{useLoadScript.js → useLoadScript.client.js} +2 -1
- package/dist/esnext/hooks/useMoney/hooks.d.ts +13 -1
- package/dist/esnext/hooks/useMoney/hooks.js +25 -1
- package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.js +0 -2
- package/dist/esnext/hooks/useProduct/useProduct.js +1 -1
- package/dist/esnext/hooks/useProductOptions/index.d.ts +1 -1
- package/dist/esnext/hooks/useProductOptions/index.js +1 -1
- package/dist/esnext/hooks/useProductOptions/{useProductOptions.d.ts → useProductOptions.client.d.ts} +0 -0
- package/dist/esnext/hooks/useProductOptions/{useProductOptions.js → useProductOptions.client.js} +6 -23
- package/dist/esnext/hooks/useShopQuery/hooks.js +15 -4
- package/dist/esnext/index.d.ts +1 -0
- package/dist/esnext/index.js +1 -0
- package/dist/esnext/storefront-api-types.d.ts +60 -6
- package/dist/esnext/storefront-api-types.js +6 -2
- package/dist/esnext/types.d.ts +11 -4
- package/dist/esnext/utilities/apiRoutes.d.ts +4 -3
- package/dist/esnext/utilities/apiRoutes.js +51 -33
- package/dist/esnext/utilities/bot-ua.js +3 -0
- package/dist/esnext/utilities/empty-hydrogen-config.d.ts +2 -0
- package/dist/esnext/utilities/empty-hydrogen-config.js +2 -0
- package/dist/esnext/utilities/findRoutePrefix.d.ts +1 -0
- package/dist/esnext/utilities/findRoutePrefix.js +17 -0
- package/dist/esnext/utilities/log/utils.js +1 -1
- package/dist/esnext/utilities/parse.d.ts +1 -0
- package/dist/esnext/utilities/parse.js +9 -0
- package/dist/esnext/utilities/parseMetafieldValue/parseMetafieldValue.js +2 -1
- package/dist/esnext/utilities/storefrontApi.js +1 -0
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/constants.js +1 -1
- package/dist/node/entry-server.d.ts +2 -2
- package/dist/node/entry-server.js +59 -56
- 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/Redirect/Redirect.client.js +1 -0
- package/dist/node/foundation/Router/BrowserRouter.client.js +34 -5
- package/dist/node/foundation/ServerPropsProvider/ServerPropsProvider.js +5 -3
- package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.d.ts +2 -2
- package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +7 -2
- package/dist/node/foundation/ShopifyProvider/types.d.ts +3 -4
- package/dist/node/foundation/ssr-interop.js +1 -1
- package/dist/node/framework/Hydration/Html.js +1 -1
- package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +3 -2
- package/dist/node/framework/Hydration/ServerComponentRequest.server.js +16 -7
- package/dist/node/framework/middleware.d.ts +3 -4
- package/dist/node/framework/middleware.js +4 -4
- package/dist/node/framework/plugin.d.ts +2 -2
- package/dist/node/framework/plugin.js +3 -3
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +1 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +2 -2
- package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +58 -3
- package/dist/node/framework/plugins/vite-plugin-platform-entry.js +2 -2
- package/dist/node/storefront-api-types.d.ts +60 -6
- package/dist/node/storefront-api-types.js +6 -2
- package/dist/node/types.d.ts +11 -4
- package/dist/node/utilities/apiRoutes.d.ts +4 -3
- package/dist/node/utilities/apiRoutes.js +53 -34
- package/dist/node/utilities/bot-ua.js +3 -0
- package/dist/node/utilities/findRoutePrefix.d.ts +1 -0
- package/dist/node/utilities/findRoutePrefix.js +21 -0
- package/dist/node/utilities/log/utils.js +1 -1
- package/dist/node/utilities/parse.d.ts +1 -0
- package/dist/node/utilities/parse.js +13 -0
- package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +2 -1
- package/dist/node/utilities/storefrontApi.js +1 -0
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- package/package.json +9 -7
- package/dist/esnext/foundation/Boomerang/Boomerang.client.d.ts +0 -9
- package/dist/esnext/foundation/Boomerang/Boomerang.client.js +0 -66
|
@@ -17,17 +17,27 @@ import { stripScriptsFromTemplate } from './utilities/template';
|
|
|
17
17
|
import { Analytics } from './foundation/Analytics/Analytics.server';
|
|
18
18
|
import { ServerAnalyticsRoute } from './foundation/Analytics/ServerAnalyticsRoute.server';
|
|
19
19
|
import { getSyncSessionApi } from './foundation/session/session';
|
|
20
|
+
import { parseJSON } from './utilities/parse';
|
|
20
21
|
const DOCTYPE = '<!DOCTYPE html>';
|
|
21
22
|
const CONTENT_TYPE = 'Content-Type';
|
|
22
23
|
const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
|
|
23
|
-
export const renderHydrogen = (App,
|
|
24
|
+
export const renderHydrogen = (App, hydrogenConfig) => {
|
|
24
25
|
const handleRequest = async function (rawRequest, options) {
|
|
25
26
|
const { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, } = options;
|
|
26
27
|
const request = new ServerComponentRequest(rawRequest);
|
|
27
|
-
request.ctx.buyerIpHeader = buyerIpHeader;
|
|
28
28
|
const url = new URL(request.url);
|
|
29
|
+
if (!hydrogenConfig) {
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
// eslint-disable-next-line node/no-missing-import
|
|
32
|
+
const configFile = await import('virtual:hydrogen-config');
|
|
33
|
+
hydrogenConfig = configFile.default;
|
|
34
|
+
}
|
|
35
|
+
request.ctx.hydrogenConfig = hydrogenConfig;
|
|
36
|
+
request.ctx.buyerIpHeader = buyerIpHeader;
|
|
29
37
|
const log = getLoggerWithContext(request);
|
|
30
|
-
const sessionApi = session
|
|
38
|
+
const sessionApi = hydrogenConfig.session
|
|
39
|
+
? hydrogenConfig.session(log)
|
|
40
|
+
: undefined;
|
|
31
41
|
const componentResponse = new ServerComponentResponse();
|
|
32
42
|
request.ctx.session = getSyncSessionApi(request, componentResponse, log, sessionApi);
|
|
33
43
|
/**
|
|
@@ -38,16 +48,16 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
|
|
|
38
48
|
setConfig({ dev });
|
|
39
49
|
if (url.pathname === EVENT_PATHNAME ||
|
|
40
50
|
EVENT_PATHNAME_REGEX.test(url.pathname)) {
|
|
41
|
-
return ServerAnalyticsRoute(request, serverAnalyticsConnectors);
|
|
51
|
+
return ServerAnalyticsRoute(request, hydrogenConfig.serverAnalyticsConnectors);
|
|
42
52
|
}
|
|
43
53
|
const isReactHydrationRequest = url.pathname === RSC_PATHNAME;
|
|
44
|
-
if (!isReactHydrationRequest && routes) {
|
|
45
|
-
const apiRoute = getApiRoute(url,
|
|
54
|
+
if (!isReactHydrationRequest && hydrogenConfig.routes) {
|
|
55
|
+
const apiRoute = getApiRoute(url, hydrogenConfig.routes);
|
|
46
56
|
// The API Route might have a default export, making it also a server component
|
|
47
57
|
// If it does, only render the API route if the request method is GET
|
|
48
58
|
if (apiRoute &&
|
|
49
59
|
(!apiRoute.hasServerComponent || request.method !== 'GET')) {
|
|
50
|
-
const apiResponse = await renderApiRoute(request, apiRoute,
|
|
60
|
+
const apiResponse = await renderApiRoute(request, apiRoute, hydrogenConfig.shopify, sessionApi);
|
|
51
61
|
return apiResponse instanceof Request
|
|
52
62
|
? handleRequest(apiResponse, options)
|
|
53
63
|
: apiResponse;
|
|
@@ -65,7 +75,6 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
|
|
|
65
75
|
App,
|
|
66
76
|
log,
|
|
67
77
|
dev,
|
|
68
|
-
routes,
|
|
69
78
|
nonce,
|
|
70
79
|
request,
|
|
71
80
|
template,
|
|
@@ -88,7 +97,7 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
|
|
|
88
97
|
};
|
|
89
98
|
return handleRequest;
|
|
90
99
|
};
|
|
91
|
-
function getApiRoute(url,
|
|
100
|
+
function getApiRoute(url, routes) {
|
|
92
101
|
const apiRoutes = getApiRoutes(routes);
|
|
93
102
|
return getApiRouteFromURL(url, apiRoutes);
|
|
94
103
|
}
|
|
@@ -97,15 +106,14 @@ function getApiRoute(url, { routes }) {
|
|
|
97
106
|
* and returning any initial state that needs to be hydrated into the client version of the app.
|
|
98
107
|
* NOTE: This is currently only used for SEO bots or Worker runtime (where Stream is not yet supported).
|
|
99
108
|
*/
|
|
100
|
-
async function render(url, { App,
|
|
109
|
+
async function render(url, { App, request, template, componentResponse, nonce, log }) {
|
|
101
110
|
const state = { pathname: url.pathname, search: url.search };
|
|
102
111
|
const { AppSSR, rscReadable } = buildAppSSR({
|
|
103
112
|
App,
|
|
113
|
+
log,
|
|
104
114
|
state,
|
|
105
115
|
request,
|
|
106
116
|
response: componentResponse,
|
|
107
|
-
routes,
|
|
108
|
-
log,
|
|
109
117
|
}, { template });
|
|
110
118
|
function onErrorShell(error) {
|
|
111
119
|
log.error(error);
|
|
@@ -121,7 +129,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
121
129
|
* TODO: Also add `Vary` headers for `accept-language` and any other keys
|
|
122
130
|
* we want to shard our full-page cache for all Hydrogen storefronts.
|
|
123
131
|
*/
|
|
124
|
-
headers
|
|
132
|
+
headers.set('cache-control', componentResponse.cacheControlHeader);
|
|
125
133
|
if (componentResponse.customBody) {
|
|
126
134
|
// This can be used to return sitemap.xml or any other custom response.
|
|
127
135
|
postRequestTasks('ssr', status, request, componentResponse);
|
|
@@ -131,7 +139,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
131
139
|
headers,
|
|
132
140
|
});
|
|
133
141
|
}
|
|
134
|
-
headers
|
|
142
|
+
headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
|
|
135
143
|
html = applyHtmlHead(html, request.ctx.head, template);
|
|
136
144
|
if (flight) {
|
|
137
145
|
html = html.replace('</body>', () => `${flightContainer({
|
|
@@ -151,18 +159,17 @@ async function render(url, { App, routes, request, componentResponse, log, templ
|
|
|
151
159
|
* Stream a response to the client. NOTE: This omits custom `<head>`
|
|
152
160
|
* information, so this method should not be used by crawlers.
|
|
153
161
|
*/
|
|
154
|
-
async function stream(url, { App,
|
|
162
|
+
async function stream(url, { App, request, response, componentResponse, template, nonce, dev, log, }) {
|
|
155
163
|
var _a;
|
|
156
164
|
const state = { pathname: url.pathname, search: url.search };
|
|
157
165
|
log.trace('start stream');
|
|
158
166
|
const { noScriptTemplate, bootstrapScripts, bootstrapModules } = stripScriptsFromTemplate(template);
|
|
159
167
|
const { AppSSR, rscReadable } = buildAppSSR({
|
|
160
168
|
App,
|
|
169
|
+
log,
|
|
161
170
|
state,
|
|
162
171
|
request,
|
|
163
172
|
response: componentResponse,
|
|
164
|
-
log,
|
|
165
|
-
routes,
|
|
166
173
|
}, { template: noScriptTemplate });
|
|
167
174
|
const rscToScriptTagReadable = new ReadableStream({
|
|
168
175
|
start(controller) {
|
|
@@ -213,6 +220,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
213
220
|
log.trace('worker complete stream');
|
|
214
221
|
onCompleteAll.resolve(true);
|
|
215
222
|
});
|
|
223
|
+
/* eslint-disable no-inner-declarations */
|
|
216
224
|
async function prepareForStreaming(flush) {
|
|
217
225
|
Object.assign(responseOptions, getResponseOptions(componentResponse, didError));
|
|
218
226
|
/**
|
|
@@ -220,8 +228,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
220
228
|
* queries which might be caught behind Suspense. Clarify this or add
|
|
221
229
|
* additional checks downstream?
|
|
222
230
|
*/
|
|
223
|
-
responseOptions.headers
|
|
224
|
-
componentResponse.cacheControlHeader;
|
|
231
|
+
responseOptions.headers.set('cache-control', componentResponse.cacheControlHeader);
|
|
225
232
|
if (isRedirect(responseOptions)) {
|
|
226
233
|
return false;
|
|
227
234
|
}
|
|
@@ -230,7 +237,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
230
237
|
writable.write(encoder.encode(await componentResponse.customBody));
|
|
231
238
|
return false;
|
|
232
239
|
}
|
|
233
|
-
responseOptions.headers
|
|
240
|
+
responseOptions.headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
|
|
234
241
|
writable.write(encoder.encode(DOCTYPE));
|
|
235
242
|
if (didError) {
|
|
236
243
|
// This error was delayed until the headers were properly sent.
|
|
@@ -239,6 +246,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
239
246
|
return true;
|
|
240
247
|
}
|
|
241
248
|
}
|
|
249
|
+
/* eslint-enable no-inner-declarations */
|
|
242
250
|
const shouldReturnApp = (_a = (await prepareForStreaming(componentResponse.canStream()))) !== null && _a !== void 0 ? _a : (await onCompleteAll.promise.then(prepareForStreaming));
|
|
243
251
|
if (shouldReturnApp) {
|
|
244
252
|
let bufferedSsr = '';
|
|
@@ -353,15 +361,14 @@ async function stream(url, { App, routes, request, response, componentResponse,
|
|
|
353
361
|
/**
|
|
354
362
|
* Stream a hydration response to the client.
|
|
355
363
|
*/
|
|
356
|
-
async function hydrate(url, { App,
|
|
357
|
-
const state =
|
|
364
|
+
async function hydrate(url, { App, log, request, response, isStreamable, componentResponse, }) {
|
|
365
|
+
const state = parseJSON(url.searchParams.get('state') || '{}');
|
|
358
366
|
const { AppRSC } = buildAppRSC({
|
|
359
367
|
App,
|
|
368
|
+
log,
|
|
360
369
|
state,
|
|
361
370
|
request,
|
|
362
371
|
response: componentResponse,
|
|
363
|
-
log,
|
|
364
|
-
routes,
|
|
365
372
|
});
|
|
366
373
|
if (__WORKER__) {
|
|
367
374
|
const rscReadable = rscRenderToReadableStream(AppRSC);
|
|
@@ -392,9 +399,12 @@ async function hydrate(url, { App, routes, request, response, componentResponse,
|
|
|
392
399
|
});
|
|
393
400
|
}
|
|
394
401
|
}
|
|
395
|
-
function buildAppRSC({ App, state, request, response
|
|
402
|
+
function buildAppRSC({ App, log, state, request, response }) {
|
|
396
403
|
const hydrogenServerProps = { request, response, log };
|
|
397
|
-
const serverProps = {
|
|
404
|
+
const serverProps = {
|
|
405
|
+
...state,
|
|
406
|
+
...hydrogenServerProps,
|
|
407
|
+
};
|
|
398
408
|
request.ctx.router.serverProps = serverProps;
|
|
399
409
|
const AppRSC = (React.createElement(ServerRequestProvider, { request: request, isRSC: true },
|
|
400
410
|
React.createElement(PreloadQueries, { request: request },
|
|
@@ -403,14 +413,13 @@ function buildAppRSC({ App, state, request, response, log, routes, }) {
|
|
|
403
413
|
React.createElement(Analytics, null)))));
|
|
404
414
|
return { AppRSC };
|
|
405
415
|
}
|
|
406
|
-
function buildAppSSR({ App, state, request, response, log
|
|
416
|
+
function buildAppSSR({ App, state, request, response, log }, htmlOptions) {
|
|
407
417
|
const { AppRSC } = buildAppRSC({
|
|
408
418
|
App,
|
|
419
|
+
log,
|
|
409
420
|
state,
|
|
410
421
|
request,
|
|
411
422
|
response,
|
|
412
|
-
log,
|
|
413
|
-
routes,
|
|
414
423
|
});
|
|
415
424
|
const [rscReadableForFizz, rscReadableForFlight] = rscRenderToReadableStream(AppRSC).tee();
|
|
416
425
|
const rscResponse = createFromReadableStream(rscReadableForFizz);
|
|
@@ -431,28 +440,23 @@ function PreloadQueries({ request, children, }) {
|
|
|
431
440
|
return React.createElement(React.Fragment, null, children);
|
|
432
441
|
}
|
|
433
442
|
async function renderToBufferedString(ReactApp, { log, nonce }) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
reject(error);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
const writer = await createNodeWriter();
|
|
443
|
+
if (__WORKER__) {
|
|
444
|
+
const ssrReadable = await ssrRenderToReadableStream(ReactApp, {
|
|
445
|
+
nonce,
|
|
446
|
+
onError: (error) => log.error(error),
|
|
447
|
+
});
|
|
448
|
+
/**
|
|
449
|
+
* We want to wait until `allReady` resolves before fetching the
|
|
450
|
+
* stream body. Otherwise, React 18's streaming JS script/template tags
|
|
451
|
+
* will be included in the output and cause issues when loading
|
|
452
|
+
* the Client Components in the browser.
|
|
453
|
+
*/
|
|
454
|
+
await ssrReadable.allReady;
|
|
455
|
+
return bufferReadableStream(ssrReadable.getReader());
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const writer = await createNodeWriter();
|
|
459
|
+
return new Promise((resolve, reject) => {
|
|
456
460
|
const { pipe } = ssrRenderToPipeableStream(ReactApp, {
|
|
457
461
|
nonce,
|
|
458
462
|
/**
|
|
@@ -470,8 +474,8 @@ async function renderToBufferedString(ReactApp, { log, nonce }) {
|
|
|
470
474
|
onShellError: reject,
|
|
471
475
|
onError: (error) => log.error(error),
|
|
472
476
|
});
|
|
473
|
-
}
|
|
474
|
-
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
475
479
|
}
|
|
476
480
|
export default renderHydrogen;
|
|
477
481
|
function startWritingHtmlToServerResponse(response, error) {
|
|
@@ -487,8 +491,7 @@ function startWritingHtmlToServerResponse(response, error) {
|
|
|
487
491
|
function getResponseOptions({ headers, status, customStatus }, error) {
|
|
488
492
|
var _a, _b;
|
|
489
493
|
const responseInit = {};
|
|
490
|
-
|
|
491
|
-
responseInit.headers = Object.fromEntries(headers.entries());
|
|
494
|
+
responseInit.headers = headers;
|
|
492
495
|
if (error) {
|
|
493
496
|
responseInit.status = 500;
|
|
494
497
|
}
|
|
@@ -509,7 +512,7 @@ function writeHeadToServerResponse(response, serverComponentResponse, log, error
|
|
|
509
512
|
if (statusText) {
|
|
510
513
|
response.statusMessage = statusText;
|
|
511
514
|
}
|
|
512
|
-
Object.entries(headers).forEach(([key, value]) => response.setHeader(key, value));
|
|
515
|
+
Object.entries(headers.raw()).forEach(([key, value]) => response.setHeader(key, value));
|
|
513
516
|
}
|
|
514
517
|
function isRedirect(response) {
|
|
515
518
|
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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse, stringify as stringifyCookie } from 'worktop/cookie';
|
|
2
2
|
import { log } from '../../utilities/log';
|
|
3
|
+
import { parseJSON } from '../../utilities/parse';
|
|
3
4
|
const reservedCookieNames = ['mac', 'user_session_id'];
|
|
4
5
|
/** The `Cookie` component helps you build your own custom cookie and session implementations. All
|
|
5
6
|
* [Hydrogen session storage mechanisms](https://shopify.dev/custom-storefronts/hydrogen/framework/sessions#types-of-session-storage) use the
|
|
@@ -26,7 +27,7 @@ export class Cookie {
|
|
|
26
27
|
}
|
|
27
28
|
parse(cookie) {
|
|
28
29
|
try {
|
|
29
|
-
const data =
|
|
30
|
+
const data = parseJSON(parse(cookie)[this.name]);
|
|
30
31
|
this.data = data;
|
|
31
32
|
}
|
|
32
33
|
catch (e) {
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import type { ImportGlobEagerOutput } from '../../types';
|
|
2
2
|
interface FileRoutesProps {
|
|
3
3
|
/** The routes defined by Vite's [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. */
|
|
4
|
-
routes
|
|
4
|
+
routes?: ImportGlobEagerOutput;
|
|
5
5
|
/** A path that's prepended to all file routes. You can modify `basePath` if you want to prefix all file routes. For example, you can prefix all file routes with a locale. */
|
|
6
6
|
basePath?: string;
|
|
7
7
|
/** The portion of the file route path that shouldn't be a part of the URL. You need to modify this if you want to import routes from a location other than the default `src/routes`. */
|
|
8
|
-
dirPrefix?: string;
|
|
8
|
+
dirPrefix?: string | RegExp;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* The `FileRoutes` component builds a set of default Hydrogen routes based on the output provided by Vite's
|
|
12
12
|
* [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. You can have multiple
|
|
13
13
|
* instances of this component to source file routes from multiple locations.
|
|
14
14
|
*/
|
|
15
|
-
export declare function FileRoutes({ routes, basePath, dirPrefix
|
|
15
|
+
export declare function FileRoutes({ routes, basePath, dirPrefix }: FileRoutesProps): JSX.Element | null;
|
|
16
16
|
interface HydrogenRoute {
|
|
17
17
|
component: any;
|
|
18
18
|
path: string;
|
|
19
19
|
exact: boolean;
|
|
20
20
|
}
|
|
21
|
-
export declare function createPageRoutes(pages: ImportGlobEagerOutput, topLevelPath
|
|
21
|
+
export declare function createPageRoutes(pages: ImportGlobEagerOutput, topLevelPath?: string, dirPrefix?: string | RegExp): HydrogenRoute[];
|
|
22
22
|
export {};
|
|
@@ -1,19 +1,31 @@
|
|
|
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';
|
|
7
|
+
import { findRoutePrefix } from '../../utilities/findRoutePrefix';
|
|
6
8
|
/**
|
|
7
9
|
* The `FileRoutes` component builds a set of default Hydrogen routes based on the output provided by Vite's
|
|
8
10
|
* [import.meta.globEager](https://vitejs.dev/guide/features.html#glob-import) method. You can have multiple
|
|
9
11
|
* instances of this component to source file routes from multiple locations.
|
|
10
12
|
*/
|
|
11
|
-
export function FileRoutes({ routes, basePath
|
|
13
|
+
export function FileRoutes({ routes, basePath, dirPrefix }) {
|
|
14
|
+
var _a;
|
|
12
15
|
const request = useServerRequest();
|
|
13
16
|
const { routeRendered, serverProps } = request.ctx.router;
|
|
14
17
|
if (routeRendered)
|
|
15
18
|
return null;
|
|
16
|
-
|
|
19
|
+
if (!routes) {
|
|
20
|
+
const fileRoutes = request.ctx.hydrogenConfig.routes;
|
|
21
|
+
routes = (_a = fileRoutes === null || fileRoutes === void 0 ? void 0 : fileRoutes.files) !== null && _a !== void 0 ? _a : fileRoutes;
|
|
22
|
+
dirPrefix !== null && dirPrefix !== void 0 ? dirPrefix : (dirPrefix = fileRoutes === null || fileRoutes === void 0 ? void 0 : fileRoutes.dirPrefix);
|
|
23
|
+
basePath !== null && basePath !== void 0 ? basePath : (basePath = fileRoutes === null || fileRoutes === void 0 ? void 0 : fileRoutes.basePath);
|
|
24
|
+
}
|
|
25
|
+
basePath !== null && basePath !== void 0 ? basePath : (basePath = '/');
|
|
26
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
27
|
+
const pageRoutes = useMemo(() => createPageRoutes(routes, basePath, dirPrefix), [routes, basePath, dirPrefix]);
|
|
28
|
+
/* eslint-enable react-hooks/rules-of-hooks */
|
|
17
29
|
let foundRoute, foundRouteDetails;
|
|
18
30
|
for (let i = 0; i < pageRoutes.length; i++) {
|
|
19
31
|
foundRouteDetails = matchPath(serverProps.pathname, pageRoutes[i]);
|
|
@@ -32,26 +44,11 @@ export function FileRoutes({ routes, basePath = '/', dirPrefix = './routes', })
|
|
|
32
44
|
}
|
|
33
45
|
export function createPageRoutes(pages, topLevelPath = '*', dirPrefix) {
|
|
34
46
|
const topLevelPrefix = topLevelPath.replace('*', '').replace(/\/$/, '');
|
|
35
|
-
const
|
|
47
|
+
const keys = Object.keys(pages);
|
|
48
|
+
const commonRoutePrefix = dirPrefix !== null && dirPrefix !== void 0 ? dirPrefix : findRoutePrefix(keys);
|
|
49
|
+
const routes = keys
|
|
36
50
|
.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);
|
|
51
|
+
const path = extractPathFromRoutesKey(key, commonRoutePrefix);
|
|
55
52
|
/**
|
|
56
53
|
* Catch-all routes [...handle].jsx don't need an exact match
|
|
57
54
|
* https://reactrouter.com/core/api/Route/exact-bool
|
|
@@ -2,6 +2,7 @@ import { Cookie } from '../Cookie/Cookie';
|
|
|
2
2
|
import { v4 as uid } from 'uuid';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { promises as fsp } from 'fs';
|
|
5
|
+
import { parseJSON } from '../../utilities/parse';
|
|
5
6
|
async function wait() {
|
|
6
7
|
return new Promise((resolve) => setTimeout(resolve));
|
|
7
8
|
}
|
|
@@ -77,7 +78,7 @@ async function getFile(file, expires, log) {
|
|
|
77
78
|
try {
|
|
78
79
|
const textContent = await fsp.readFile(file, { encoding: 'utf-8' });
|
|
79
80
|
try {
|
|
80
|
-
content =
|
|
81
|
+
content = parseJSON(textContent);
|
|
81
82
|
}
|
|
82
83
|
catch (error) {
|
|
83
84
|
log.warn(`Cannot parse existing session file: ${file}`);
|
|
@@ -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
|
}
|