@shopify/hydrogen 2025.4.0 → 2025.4.2
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/dist/development/index.cjs +383 -68
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +382 -72
- package/dist/development/index.js.map +1 -1
- package/dist/production/index.cjs +71 -71
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.d.cts +71 -10
- package/dist/production/index.d.ts +71 -10
- package/dist/production/index.js +74 -74
- package/dist/production/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ var react$1 = require('@remix-run/react');
|
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
var hydrogenReact = require('@shopify/hydrogen-react');
|
|
7
7
|
var cookie = require('worktop/cookie');
|
|
8
|
+
var serverRuntime = require('@remix-run/server-runtime');
|
|
8
9
|
var cspBuilder = require('content-security-policy-builder');
|
|
9
10
|
require('url');
|
|
10
11
|
require('path');
|
|
@@ -170,7 +171,62 @@ var AnalyticsEvent = {
|
|
|
170
171
|
// Custom
|
|
171
172
|
CUSTOM_EVENT: `custom_`
|
|
172
173
|
};
|
|
173
|
-
|
|
174
|
+
|
|
175
|
+
// src/constants.ts
|
|
176
|
+
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
177
|
+
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
178
|
+
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
179
|
+
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
180
|
+
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
181
|
+
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
182
|
+
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
183
|
+
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
184
|
+
var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
|
|
185
|
+
|
|
186
|
+
// src/utils/server-timing.ts
|
|
187
|
+
function buildServerTimingHeader(values) {
|
|
188
|
+
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
189
|
+
}
|
|
190
|
+
function appendServerTimingHeader(response, values) {
|
|
191
|
+
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
192
|
+
if (header) {
|
|
193
|
+
response.headers.append("Server-Timing", header);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var trackedTimings = ["_y", "_s", "_cmp"];
|
|
197
|
+
function extractServerTimingHeader(serverTimingHeader) {
|
|
198
|
+
const values = {};
|
|
199
|
+
if (!serverTimingHeader) return values;
|
|
200
|
+
const re = new RegExp(
|
|
201
|
+
`\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
|
|
202
|
+
"g"
|
|
203
|
+
);
|
|
204
|
+
let match;
|
|
205
|
+
while ((match = re.exec(serverTimingHeader)) !== null) {
|
|
206
|
+
values[match[1]] = match[2];
|
|
207
|
+
}
|
|
208
|
+
return values;
|
|
209
|
+
}
|
|
210
|
+
function hasServerTimingInNavigationEntry(key) {
|
|
211
|
+
if (typeof window === "undefined") return false;
|
|
212
|
+
try {
|
|
213
|
+
const navigationEntry = window.performance.getEntriesByType(
|
|
214
|
+
"navigation"
|
|
215
|
+
)[0];
|
|
216
|
+
return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function isSfapiProxyEnabled() {
|
|
222
|
+
return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
|
|
223
|
+
}
|
|
224
|
+
function hasServerReturnedTrackingValues() {
|
|
225
|
+
return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/customer-privacy/ShopifyCustomerPrivacy.tsx
|
|
229
|
+
var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
|
|
174
230
|
var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
|
|
175
231
|
function logMissingConfig(fieldName) {
|
|
176
232
|
console.error(
|
|
@@ -182,19 +238,34 @@ function useCustomerPrivacy(props) {
|
|
|
182
238
|
withPrivacyBanner = false,
|
|
183
239
|
onVisitorConsentCollected,
|
|
184
240
|
onReady,
|
|
185
|
-
|
|
241
|
+
checkoutDomain,
|
|
242
|
+
storefrontAccessToken,
|
|
243
|
+
country,
|
|
244
|
+
locale,
|
|
245
|
+
sameDomainForStorefrontApi
|
|
186
246
|
} = props;
|
|
247
|
+
const hasSfapiProxy = react.useMemo(
|
|
248
|
+
() => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
|
|
249
|
+
[sameDomainForStorefrontApi]
|
|
250
|
+
);
|
|
251
|
+
const fetchTrackingValuesFromBrowser = react.useMemo(
|
|
252
|
+
() => hasSfapiProxy && !hasServerReturnedTrackingValues(),
|
|
253
|
+
[hasSfapiProxy]
|
|
254
|
+
);
|
|
255
|
+
const cookiesReady = hydrogenReact.useShopifyCookies({
|
|
256
|
+
fetchTrackingValues: fetchTrackingValuesFromBrowser,
|
|
257
|
+
storefrontAccessToken,
|
|
258
|
+
ignoreDeprecatedCookies: true
|
|
259
|
+
});
|
|
260
|
+
const initialTrackingValues = react.useMemo(hydrogenReact.getTrackingValues, [cookiesReady]);
|
|
261
|
+
const { revalidate } = react$1.useRevalidator();
|
|
187
262
|
hydrogenReact.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
|
|
188
263
|
attributes: {
|
|
189
264
|
id: "customer-privacy-api"
|
|
190
265
|
}
|
|
191
266
|
});
|
|
192
|
-
const { observing, setLoaded } = useApisLoaded({
|
|
193
|
-
withPrivacyBanner,
|
|
194
|
-
onLoaded: onReady
|
|
195
|
-
});
|
|
267
|
+
const { observing, setLoaded, apisLoaded } = useApisLoaded({ withPrivacyBanner });
|
|
196
268
|
const config = react.useMemo(() => {
|
|
197
|
-
const { checkoutDomain, storefrontAccessToken } = consentConfig;
|
|
198
269
|
if (!checkoutDomain) logMissingConfig("checkoutDomain");
|
|
199
270
|
if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
|
|
200
271
|
if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) {
|
|
@@ -202,18 +273,50 @@ function useCustomerPrivacy(props) {
|
|
|
202
273
|
`[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
|
|
203
274
|
);
|
|
204
275
|
}
|
|
276
|
+
const commonAncestorDomain = parseStoreDomain(checkoutDomain);
|
|
277
|
+
const sfapiDomain = (
|
|
278
|
+
// Check if standard route proxy is enabled in Hydrogen server
|
|
279
|
+
// to use it instead of doing a cross-origin request to checkout.
|
|
280
|
+
hasSfapiProxy && typeof window !== "undefined" ? window.location.host : checkoutDomain
|
|
281
|
+
);
|
|
205
282
|
const config2 = {
|
|
206
|
-
|
|
283
|
+
// This domain is used to send requests to SFAPI for setting and getting consent.
|
|
284
|
+
checkoutRootDomain: sfapiDomain,
|
|
285
|
+
// Prefix with a dot to ensure this domain is different from checkoutRootDomain.
|
|
286
|
+
// This will ensure old cookies are set for a cross-subdomain checkout setup
|
|
287
|
+
// so that we keep backward compatibility until new cookies are rolled out.
|
|
288
|
+
// Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
|
|
289
|
+
storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
|
|
207
290
|
storefrontAccessToken,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
locale: consentConfig.locale
|
|
291
|
+
country,
|
|
292
|
+
locale
|
|
211
293
|
};
|
|
212
294
|
return config2;
|
|
213
|
-
}, [
|
|
295
|
+
}, [
|
|
296
|
+
logMissingConfig,
|
|
297
|
+
checkoutDomain,
|
|
298
|
+
storefrontAccessToken,
|
|
299
|
+
country,
|
|
300
|
+
locale
|
|
301
|
+
]);
|
|
214
302
|
react.useEffect(() => {
|
|
215
303
|
const consentCollectedHandler = (event) => {
|
|
304
|
+
const latestTrackingValues = hydrogenReact.getTrackingValues();
|
|
305
|
+
if (initialTrackingValues.visitToken !== latestTrackingValues.visitToken || initialTrackingValues.uniqueToken !== latestTrackingValues.uniqueToken) {
|
|
306
|
+
revalidate();
|
|
307
|
+
}
|
|
216
308
|
if (onVisitorConsentCollected) {
|
|
309
|
+
const customerPrivacy = getCustomerPrivacy();
|
|
310
|
+
if (customerPrivacy?.shouldShowBanner()) {
|
|
311
|
+
const consentValues = customerPrivacy.currentVisitorConsent();
|
|
312
|
+
if (consentValues) {
|
|
313
|
+
const NO_VALUE = "";
|
|
314
|
+
const noInteraction = consentValues.marketing === NO_VALUE && consentValues.analytics === NO_VALUE && consentValues.preferences === NO_VALUE;
|
|
315
|
+
if (noInteraction) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
217
320
|
onVisitorConsentCollected(event.detail);
|
|
218
321
|
}
|
|
219
322
|
};
|
|
@@ -239,14 +342,11 @@ function useCustomerPrivacy(props) {
|
|
|
239
342
|
},
|
|
240
343
|
set(value) {
|
|
241
344
|
if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
|
|
242
|
-
const privacyBanner = value;
|
|
243
|
-
privacyBanner.loadBanner(config);
|
|
244
345
|
customPrivacyBanner = overridePrivacyBannerMethods({
|
|
245
|
-
privacyBanner,
|
|
346
|
+
privacyBanner: value,
|
|
246
347
|
config
|
|
247
348
|
});
|
|
248
349
|
setLoaded.privacyBanner();
|
|
249
|
-
emitCustomerPrivacyApiLoaded();
|
|
250
350
|
}
|
|
251
351
|
}
|
|
252
352
|
};
|
|
@@ -280,6 +380,8 @@ function useCustomerPrivacy(props) {
|
|
|
280
380
|
const customerPrivacy = value2;
|
|
281
381
|
customCustomerPrivacy = {
|
|
282
382
|
...customerPrivacy,
|
|
383
|
+
// Note: this method is not used by the privacy-banner,
|
|
384
|
+
// it bundles its own setTrackingConsent.
|
|
283
385
|
setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
|
|
284
386
|
{ customerPrivacy, config }
|
|
285
387
|
)
|
|
@@ -289,7 +391,6 @@ function useCustomerPrivacy(props) {
|
|
|
289
391
|
customerPrivacy: customCustomerPrivacy
|
|
290
392
|
};
|
|
291
393
|
setLoaded.customerPrivacy();
|
|
292
|
-
emitCustomerPrivacyApiLoaded();
|
|
293
394
|
}
|
|
294
395
|
}
|
|
295
396
|
});
|
|
@@ -301,6 +402,24 @@ function useCustomerPrivacy(props) {
|
|
|
301
402
|
overrideCustomerPrivacySetTrackingConsent,
|
|
302
403
|
setLoaded.customerPrivacy
|
|
303
404
|
]);
|
|
405
|
+
react.useEffect(() => {
|
|
406
|
+
if (!apisLoaded || !cookiesReady) return;
|
|
407
|
+
const customerPrivacy = getCustomerPrivacy();
|
|
408
|
+
if (customerPrivacy && !customerPrivacy.cachedConsent) {
|
|
409
|
+
const trackingValues = hydrogenReact.getTrackingValues();
|
|
410
|
+
if (trackingValues.consent) {
|
|
411
|
+
customerPrivacy.cachedConsent = trackingValues.consent;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (withPrivacyBanner) {
|
|
415
|
+
const privacyBanner = getPrivacyBanner();
|
|
416
|
+
if (privacyBanner) {
|
|
417
|
+
privacyBanner.loadBanner(config);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
emitCustomerPrivacyApiLoaded();
|
|
421
|
+
onReady?.();
|
|
422
|
+
}, [apisLoaded, cookiesReady]);
|
|
304
423
|
const result = {
|
|
305
424
|
customerPrivacy: getCustomerPrivacy()
|
|
306
425
|
};
|
|
@@ -316,15 +435,12 @@ function emitCustomerPrivacyApiLoaded() {
|
|
|
316
435
|
const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded");
|
|
317
436
|
document.dispatchEvent(event);
|
|
318
437
|
}
|
|
319
|
-
function useApisLoaded({
|
|
320
|
-
withPrivacyBanner,
|
|
321
|
-
onLoaded
|
|
322
|
-
}) {
|
|
438
|
+
function useApisLoaded({ withPrivacyBanner }) {
|
|
323
439
|
const observing = react.useRef({ customerPrivacy: false, privacyBanner: false });
|
|
324
|
-
const [
|
|
440
|
+
const [apisLoadedArray, setApisLoaded] = react.useState(
|
|
325
441
|
withPrivacyBanner ? [false, false] : [false]
|
|
326
442
|
);
|
|
327
|
-
const
|
|
443
|
+
const apisLoaded = apisLoadedArray.every(Boolean);
|
|
328
444
|
const setLoaded = {
|
|
329
445
|
customerPrivacy: () => {
|
|
330
446
|
if (withPrivacyBanner) {
|
|
@@ -340,16 +456,11 @@ function useApisLoaded({
|
|
|
340
456
|
setApisLoaded((prev) => [prev[0], true]);
|
|
341
457
|
}
|
|
342
458
|
};
|
|
343
|
-
|
|
344
|
-
if (loaded && onLoaded) {
|
|
345
|
-
onLoaded();
|
|
346
|
-
}
|
|
347
|
-
}, [loaded, onLoaded]);
|
|
348
|
-
return { observing, setLoaded };
|
|
459
|
+
return { observing, setLoaded, apisLoaded };
|
|
349
460
|
}
|
|
350
461
|
function parseStoreDomain(checkoutDomain) {
|
|
351
462
|
if (typeof window === "undefined") return;
|
|
352
|
-
const host = window.
|
|
463
|
+
const host = window.location.host;
|
|
353
464
|
const checkoutDomainParts = checkoutDomain.split(".").reverse();
|
|
354
465
|
const currentDomainParts = host.split(".").reverse();
|
|
355
466
|
const sameDomainParts = [];
|
|
@@ -358,7 +469,7 @@ function parseStoreDomain(checkoutDomain) {
|
|
|
358
469
|
sameDomainParts.push(part);
|
|
359
470
|
}
|
|
360
471
|
});
|
|
361
|
-
return sameDomainParts.reverse().join(".");
|
|
472
|
+
return sameDomainParts.reverse().join(".") || void 0;
|
|
362
473
|
}
|
|
363
474
|
function overrideCustomerPrivacySetTrackingConsent({
|
|
364
475
|
customerPrivacy,
|
|
@@ -416,7 +527,7 @@ function getPrivacyBanner() {
|
|
|
416
527
|
}
|
|
417
528
|
|
|
418
529
|
// package.json
|
|
419
|
-
var version = "2025.4.
|
|
530
|
+
var version = "2025.4.2";
|
|
420
531
|
|
|
421
532
|
// src/analytics-manager/ShopifyAnalytics.tsx
|
|
422
533
|
function getCustomerPrivacyRequired() {
|
|
@@ -436,6 +547,7 @@ function ShopifyAnalytics({
|
|
|
436
547
|
const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics();
|
|
437
548
|
const [shopifyReady, setShopifyReady] = react.useState(false);
|
|
438
549
|
const [privacyReady, setPrivacyReady] = react.useState(false);
|
|
550
|
+
const [collectedConsent, setCollectedConsent] = react.useState("");
|
|
439
551
|
const init = react.useRef(false);
|
|
440
552
|
const { checkoutDomain, storefrontAccessToken, language } = consent;
|
|
441
553
|
const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
|
|
@@ -444,14 +556,31 @@ function ShopifyAnalytics({
|
|
|
444
556
|
locale: language,
|
|
445
557
|
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
446
558
|
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
447
|
-
|
|
448
|
-
|
|
559
|
+
// If we use privacy banner, we should wait until consent is collected.
|
|
560
|
+
// Otherwise, we can consider privacy ready immediately:
|
|
561
|
+
onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
|
|
562
|
+
onVisitorConsentCollected: (consent2) => {
|
|
563
|
+
try {
|
|
564
|
+
setCollectedConsent(JSON.stringify(consent2));
|
|
565
|
+
} catch (e) {
|
|
566
|
+
}
|
|
567
|
+
setPrivacyReady(true);
|
|
568
|
+
}
|
|
449
569
|
});
|
|
570
|
+
const hasUserConsent = react.useMemo(
|
|
571
|
+
// must be initialized with true to avoid removing cookies too early
|
|
572
|
+
() => privacyReady ? canTrack() : true,
|
|
573
|
+
// Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
|
|
574
|
+
[privacyReady, canTrack, collectedConsent]
|
|
575
|
+
);
|
|
450
576
|
hydrogenReact.useShopifyCookies({
|
|
451
|
-
hasUserConsent
|
|
452
|
-
// must be initialized with true
|
|
577
|
+
hasUserConsent,
|
|
453
578
|
domain,
|
|
454
|
-
checkoutDomain
|
|
579
|
+
checkoutDomain,
|
|
580
|
+
// Already done inside useCustomerPrivacy
|
|
581
|
+
fetchTrackingValues: false,
|
|
582
|
+
// Avoid creating local cookies too early
|
|
583
|
+
ignoreDeprecatedCookies: !privacyReady
|
|
455
584
|
});
|
|
456
585
|
react.useEffect(() => {
|
|
457
586
|
if (init.current) return;
|
|
@@ -501,11 +630,11 @@ function prepareBasePageViewPayload(payload) {
|
|
|
501
630
|
...payload.shop,
|
|
502
631
|
hasUserConsent,
|
|
503
632
|
...hydrogenReact.getClientBrowserParameters(),
|
|
504
|
-
ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
|
|
505
|
-
gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()),
|
|
506
633
|
analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(),
|
|
507
634
|
marketingAllowed: customerPrivacy.marketingAllowed(),
|
|
508
|
-
saleOfDataAllowed: customerPrivacy.saleOfDataAllowed()
|
|
635
|
+
saleOfDataAllowed: customerPrivacy.saleOfDataAllowed(),
|
|
636
|
+
ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
|
|
637
|
+
gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed())
|
|
509
638
|
};
|
|
510
639
|
return eventPayload;
|
|
511
640
|
}
|
|
@@ -938,11 +1067,11 @@ function AnalyticsProvider({
|
|
|
938
1067
|
shop: shopProp = null,
|
|
939
1068
|
cookieDomain
|
|
940
1069
|
}) {
|
|
941
|
-
const listenerSet = react.useRef(false);
|
|
942
1070
|
const { shop } = useShopAnalytics(shopProp);
|
|
943
1071
|
const [analyticsLoaded, setAnalyticsLoaded] = react.useState(
|
|
944
1072
|
customCanTrack ? true : false
|
|
945
1073
|
);
|
|
1074
|
+
const [consentCollected, setConsentCollected] = react.useState(false);
|
|
946
1075
|
const [carts, setCarts] = react.useState({ cart: null, prevCart: null });
|
|
947
1076
|
const [canTrack, setCanTrack] = react.useState(
|
|
948
1077
|
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
|
|
@@ -1010,21 +1139,21 @@ function AnalyticsProvider({
|
|
|
1010
1139
|
children,
|
|
1011
1140
|
!!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
|
|
1012
1141
|
!!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }),
|
|
1013
|
-
!!shop &&
|
|
1142
|
+
!!shop && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1014
1143
|
ShopifyAnalytics,
|
|
1015
1144
|
{
|
|
1016
1145
|
consent,
|
|
1017
1146
|
onReady: () => {
|
|
1018
|
-
listenerSet.current = true;
|
|
1019
1147
|
setAnalyticsLoaded(true);
|
|
1020
1148
|
setCanTrack(
|
|
1021
1149
|
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
|
|
1022
1150
|
);
|
|
1151
|
+
setConsentCollected(true);
|
|
1023
1152
|
},
|
|
1024
1153
|
domain: cookieDomain
|
|
1025
1154
|
}
|
|
1026
1155
|
),
|
|
1027
|
-
!!shop && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
|
|
1156
|
+
!!shop && consentCollected && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
|
|
1028
1157
|
] });
|
|
1029
1158
|
}
|
|
1030
1159
|
function useAnalytics() {
|
|
@@ -1103,6 +1232,21 @@ function getDebugHeaders(request) {
|
|
|
1103
1232
|
purpose: request ? getHeader(request, "purpose") : void 0
|
|
1104
1233
|
};
|
|
1105
1234
|
}
|
|
1235
|
+
var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
|
|
1236
|
+
var getSafePathname = (url) => {
|
|
1237
|
+
try {
|
|
1238
|
+
return new URL(url, "http://e.c").pathname;
|
|
1239
|
+
} catch {
|
|
1240
|
+
return "/";
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
function extractHeaders(extract, keys) {
|
|
1244
|
+
return keys.reduce((acc, key) => {
|
|
1245
|
+
const forwardedValue = extract(key);
|
|
1246
|
+
if (forwardedValue) acc.push([key, forwardedValue]);
|
|
1247
|
+
return acc;
|
|
1248
|
+
}, []);
|
|
1249
|
+
}
|
|
1106
1250
|
|
|
1107
1251
|
// src/utils/callsites.ts
|
|
1108
1252
|
function withSyncStack(promise, options = {}) {
|
|
@@ -1472,13 +1616,16 @@ async function runWithCache(cacheKey, actionFn, {
|
|
|
1472
1616
|
}
|
|
1473
1617
|
|
|
1474
1618
|
// src/cache/server-fetch.ts
|
|
1619
|
+
var excludedHeaders = ["set-cookie", "server-timing"];
|
|
1475
1620
|
function toSerializableResponse(body, response) {
|
|
1476
1621
|
return [
|
|
1477
1622
|
body,
|
|
1478
1623
|
{
|
|
1479
1624
|
status: response.status,
|
|
1480
1625
|
statusText: response.statusText,
|
|
1481
|
-
headers:
|
|
1626
|
+
headers: [...response.headers].filter(
|
|
1627
|
+
([key]) => !excludedHeaders.includes(key.toLowerCase())
|
|
1628
|
+
)
|
|
1482
1629
|
}
|
|
1483
1630
|
];
|
|
1484
1631
|
}
|
|
@@ -1491,7 +1638,8 @@ async function fetchWithServerCache(url, requestInit, {
|
|
|
1491
1638
|
cacheKey = [url, requestInit],
|
|
1492
1639
|
shouldCacheResponse,
|
|
1493
1640
|
waitUntil,
|
|
1494
|
-
debugInfo
|
|
1641
|
+
debugInfo,
|
|
1642
|
+
onRawHeaders
|
|
1495
1643
|
}) {
|
|
1496
1644
|
if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) {
|
|
1497
1645
|
cacheOptions = CacheShort();
|
|
@@ -1500,6 +1648,7 @@ async function fetchWithServerCache(url, requestInit, {
|
|
|
1500
1648
|
cacheKey,
|
|
1501
1649
|
async () => {
|
|
1502
1650
|
const response = await fetch(url, requestInit);
|
|
1651
|
+
onRawHeaders?.(response.headers);
|
|
1503
1652
|
if (!response.ok) {
|
|
1504
1653
|
return response;
|
|
1505
1654
|
}
|
|
@@ -1722,13 +1871,6 @@ var cartSetIdDefault = (cookieOptions) => {
|
|
|
1722
1871
|
};
|
|
1723
1872
|
};
|
|
1724
1873
|
|
|
1725
|
-
// src/constants.ts
|
|
1726
|
-
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
1727
|
-
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
1728
|
-
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
1729
|
-
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
1730
|
-
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
1731
|
-
|
|
1732
1874
|
// src/utils/uuid.ts
|
|
1733
1875
|
function generateUUID() {
|
|
1734
1876
|
if (typeof crypto !== "undefined" && !!crypto.randomUUID) {
|
|
@@ -1739,7 +1881,7 @@ function generateUUID() {
|
|
|
1739
1881
|
}
|
|
1740
1882
|
|
|
1741
1883
|
// src/version.ts
|
|
1742
|
-
var LIB_VERSION = "2025.4.
|
|
1884
|
+
var LIB_VERSION = "2025.4.2";
|
|
1743
1885
|
|
|
1744
1886
|
// src/utils/graphql.ts
|
|
1745
1887
|
function minifyQuery(string) {
|
|
@@ -1900,16 +2042,34 @@ function createStorefrontClient(options) {
|
|
|
1900
2042
|
contentType: "json",
|
|
1901
2043
|
buyerIp: storefrontHeaders?.buyerIp || ""
|
|
1902
2044
|
});
|
|
2045
|
+
if (storefrontHeaders?.buyerIp) {
|
|
2046
|
+
defaultHeaders[SHOPIFY_CLIENT_IP_HEADER] = storefrontHeaders.buyerIp;
|
|
2047
|
+
}
|
|
2048
|
+
if (storefrontHeaders?.buyerIpSig) {
|
|
2049
|
+
defaultHeaders[SHOPIFY_CLIENT_IP_SIG_HEADER] = storefrontHeaders.buyerIpSig;
|
|
2050
|
+
}
|
|
1903
2051
|
defaultHeaders[STOREFRONT_REQUEST_GROUP_ID_HEADER] = storefrontHeaders?.requestGroupId || generateUUID();
|
|
1904
2052
|
if (storefrontId) defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER] = storefrontId;
|
|
1905
2053
|
defaultHeaders["user-agent"] = `Hydrogen ${LIB_VERSION}`;
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2054
|
+
const requestCookie = storefrontHeaders?.cookie ?? "";
|
|
2055
|
+
if (requestCookie) defaultHeaders["cookie"] = requestCookie;
|
|
2056
|
+
let uniqueToken;
|
|
2057
|
+
let visitToken;
|
|
2058
|
+
if (!/\b_shopify_(analytics|marketing)=/.test(requestCookie)) {
|
|
2059
|
+
const legacyUniqueToken = requestCookie.match(/\b_shopify_y=([^;]+)/)?.[1];
|
|
2060
|
+
const legacyVisitToken = requestCookie.match(/\b_shopify_s=([^;]+)/)?.[1];
|
|
2061
|
+
if (legacyUniqueToken) {
|
|
2062
|
+
defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = legacyUniqueToken;
|
|
2063
|
+
}
|
|
2064
|
+
if (legacyVisitToken) {
|
|
2065
|
+
defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = legacyVisitToken;
|
|
2066
|
+
}
|
|
2067
|
+
uniqueToken = legacyUniqueToken ?? generateUUID();
|
|
2068
|
+
visitToken = legacyVisitToken ?? generateUUID();
|
|
2069
|
+
defaultHeaders[hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER] = uniqueToken;
|
|
2070
|
+
defaultHeaders[hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER] = visitToken;
|
|
1912
2071
|
}
|
|
2072
|
+
let collectedSubrequestHeaders;
|
|
1913
2073
|
const cacheKeyHeader = JSON.stringify({
|
|
1914
2074
|
"content-type": defaultHeaders["content-type"],
|
|
1915
2075
|
"user-agent": defaultHeaders["user-agent"],
|
|
@@ -1971,6 +2131,13 @@ function createStorefrontClient(options) {
|
|
|
1971
2131
|
stackInfo,
|
|
1972
2132
|
graphql: graphqlData,
|
|
1973
2133
|
purpose: storefrontHeaders?.purpose
|
|
2134
|
+
},
|
|
2135
|
+
onRawHeaders: (headers2) => {
|
|
2136
|
+
collectedSubrequestHeaders ??= {
|
|
2137
|
+
// `getSetCookie` may not be available in all environments (e.g., classic Remix compiler)
|
|
2138
|
+
setCookie: typeof headers2.getSetCookie === "function" ? headers2.getSetCookie() : [],
|
|
2139
|
+
serverTiming: headers2.get("server-timing") ?? ""
|
|
2140
|
+
};
|
|
1974
2141
|
}
|
|
1975
2142
|
});
|
|
1976
2143
|
const errorOptions = {
|
|
@@ -2069,9 +2236,90 @@ function createStorefrontClient(options) {
|
|
|
2069
2236
|
generateCacheControlHeader,
|
|
2070
2237
|
getPublicTokenHeaders,
|
|
2071
2238
|
getPrivateTokenHeaders,
|
|
2239
|
+
getHeaders: () => ({ ...defaultHeaders }),
|
|
2072
2240
|
getShopifyDomain,
|
|
2073
2241
|
getApiUrl: getStorefrontApiUrl,
|
|
2074
|
-
i18n: i18n ?? defaultI18n
|
|
2242
|
+
i18n: i18n ?? defaultI18n,
|
|
2243
|
+
/**
|
|
2244
|
+
* Checks if the request is targeting the Storefront API endpoint.
|
|
2245
|
+
*/
|
|
2246
|
+
isStorefrontApiUrl(request) {
|
|
2247
|
+
return SFAPI_RE.test(getSafePathname(request.url ?? ""));
|
|
2248
|
+
},
|
|
2249
|
+
/**
|
|
2250
|
+
* Forwards the request to the Storefront API.
|
|
2251
|
+
*/
|
|
2252
|
+
async forward(request, options2) {
|
|
2253
|
+
const forwardedHeaders = new Headers([
|
|
2254
|
+
// Forward only a selected set of headers to the Storefront API
|
|
2255
|
+
// to avoid getting 403 errors due to unexpected headers.
|
|
2256
|
+
...extractHeaders(
|
|
2257
|
+
(key) => request.headers.get(key),
|
|
2258
|
+
[
|
|
2259
|
+
"accept",
|
|
2260
|
+
"accept-encoding",
|
|
2261
|
+
"accept-language",
|
|
2262
|
+
// Access-Control headers are used for CORS preflight requests.
|
|
2263
|
+
"access-control-request-headers",
|
|
2264
|
+
"access-control-request-method",
|
|
2265
|
+
"content-type",
|
|
2266
|
+
"content-length",
|
|
2267
|
+
"cookie",
|
|
2268
|
+
"origin",
|
|
2269
|
+
"referer",
|
|
2270
|
+
"user-agent",
|
|
2271
|
+
STOREFRONT_ACCESS_TOKEN_HEADER,
|
|
2272
|
+
hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER,
|
|
2273
|
+
hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER
|
|
2274
|
+
]
|
|
2275
|
+
),
|
|
2276
|
+
// Add some headers to help with geolocalization and debugging
|
|
2277
|
+
...extractHeaders(
|
|
2278
|
+
(key) => defaultHeaders[key],
|
|
2279
|
+
[
|
|
2280
|
+
SHOPIFY_CLIENT_IP_HEADER,
|
|
2281
|
+
SHOPIFY_CLIENT_IP_SIG_HEADER,
|
|
2282
|
+
hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER,
|
|
2283
|
+
STOREFRONT_REQUEST_GROUP_ID_HEADER
|
|
2284
|
+
]
|
|
2285
|
+
)
|
|
2286
|
+
]);
|
|
2287
|
+
if (storefrontHeaders?.buyerIp) {
|
|
2288
|
+
forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
|
|
2289
|
+
}
|
|
2290
|
+
const storefrontApiVersion = options2?.storefrontApiVersion ?? getSafePathname(request.url).match(SFAPI_RE)?.[1];
|
|
2291
|
+
const sfapiResponse = await fetch(
|
|
2292
|
+
getStorefrontApiUrl({ storefrontApiVersion }),
|
|
2293
|
+
{
|
|
2294
|
+
method: request.method,
|
|
2295
|
+
body: request.body,
|
|
2296
|
+
headers: forwardedHeaders
|
|
2297
|
+
}
|
|
2298
|
+
);
|
|
2299
|
+
return new Response(sfapiResponse.body, sfapiResponse);
|
|
2300
|
+
},
|
|
2301
|
+
setCollectedSubrequestHeaders: (response) => {
|
|
2302
|
+
if (collectedSubrequestHeaders) {
|
|
2303
|
+
for (const value of collectedSubrequestHeaders.setCookie) {
|
|
2304
|
+
response.headers.append("Set-Cookie", value);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
const serverTiming = extractServerTimingHeader(
|
|
2308
|
+
collectedSubrequestHeaders?.serverTiming
|
|
2309
|
+
);
|
|
2310
|
+
const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
|
|
2311
|
+
const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
|
|
2312
|
+
appendServerTimingHeader(response, {
|
|
2313
|
+
...fallbackValues,
|
|
2314
|
+
...serverTiming
|
|
2315
|
+
});
|
|
2316
|
+
if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
|
|
2317
|
+
collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
|
|
2318
|
+
appendServerTimingHeader(response, {
|
|
2319
|
+
[HYDROGEN_SERVER_TRACKING_KEY]: "1"
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2075
2323
|
}
|
|
2076
2324
|
};
|
|
2077
2325
|
}
|
|
@@ -2868,7 +3116,8 @@ function createCartHandler(options) {
|
|
|
2868
3116
|
storefront,
|
|
2869
3117
|
customerAccount,
|
|
2870
3118
|
cartQueryFragment,
|
|
2871
|
-
cartMutateFragment
|
|
3119
|
+
cartMutateFragment,
|
|
3120
|
+
buyerIdentity
|
|
2872
3121
|
} = options;
|
|
2873
3122
|
let cartId = _getCartId();
|
|
2874
3123
|
const getCartId = () => cartId || _getCartId();
|
|
@@ -2880,6 +3129,10 @@ function createCartHandler(options) {
|
|
|
2880
3129
|
};
|
|
2881
3130
|
const _cartCreate = cartCreateDefault(mutateOptions);
|
|
2882
3131
|
const cartCreate = async function(...args) {
|
|
3132
|
+
args[0].buyerIdentity = {
|
|
3133
|
+
...buyerIdentity,
|
|
3134
|
+
...args[0].buyerIdentity
|
|
3135
|
+
};
|
|
2883
3136
|
const result = await _cartCreate(...args);
|
|
2884
3137
|
cartId = result?.cart?.id;
|
|
2885
3138
|
return result;
|
|
@@ -2903,7 +3156,7 @@ function createCartHandler(options) {
|
|
|
2903
3156
|
sellingPlanId: line.sellingPlanId
|
|
2904
3157
|
};
|
|
2905
3158
|
});
|
|
2906
|
-
return cartId || optionalParams?.cartId ? await cartLinesAddDefault(mutateOptions)(lines, optionalParams) : await cartCreate({ lines }, optionalParams);
|
|
3159
|
+
return cartId || optionalParams?.cartId ? await cartLinesAddDefault(mutateOptions)(lines, optionalParams) : await cartCreate({ lines, buyerIdentity }, optionalParams);
|
|
2907
3160
|
},
|
|
2908
3161
|
updateLines: cartLinesUpdateDefault(mutateOptions),
|
|
2909
3162
|
removeLines: cartLinesRemoveDefault(mutateOptions),
|
|
@@ -2919,11 +3172,11 @@ function createCartHandler(options) {
|
|
|
2919
3172
|
optionalParams
|
|
2920
3173
|
) : await cartCreate({ giftCardCodes }, optionalParams);
|
|
2921
3174
|
},
|
|
2922
|
-
updateBuyerIdentity: async (
|
|
3175
|
+
updateBuyerIdentity: async (buyerIdentity2, optionalParams) => {
|
|
2923
3176
|
return cartId || optionalParams?.cartId ? await cartBuyerIdentityUpdateDefault(mutateOptions)(
|
|
2924
|
-
|
|
3177
|
+
buyerIdentity2,
|
|
2925
3178
|
optionalParams
|
|
2926
|
-
) : await cartCreate({ buyerIdentity }, optionalParams);
|
|
3179
|
+
) : await cartCreate({ buyerIdentity: buyerIdentity2 }, optionalParams);
|
|
2927
3180
|
},
|
|
2928
3181
|
updateNote: async (note, optionalParams) => {
|
|
2929
3182
|
return cartId || optionalParams?.cartId ? await cartNoteUpdateDefault(mutateOptions)(note, optionalParams) : await cartCreate({ note }, optionalParams);
|
|
@@ -3818,7 +4071,8 @@ function createHydrogenContext(options) {
|
|
|
3818
4071
|
logErrors,
|
|
3819
4072
|
storefront: storefrontOptions = {},
|
|
3820
4073
|
customerAccount: customerAccountOptions,
|
|
3821
|
-
cart: cartOptions = {}
|
|
4074
|
+
cart: cartOptions = {},
|
|
4075
|
+
buyerIdentity
|
|
3822
4076
|
} = options;
|
|
3823
4077
|
if (!session) {
|
|
3824
4078
|
console.warn(
|
|
@@ -3868,6 +4122,7 @@ function createHydrogenContext(options) {
|
|
|
3868
4122
|
cartQueryFragment: cartOptions.queryFragment,
|
|
3869
4123
|
cartMutateFragment: cartOptions.mutateFragment,
|
|
3870
4124
|
customMethods: cartOptions.customMethods,
|
|
4125
|
+
buyerIdentity,
|
|
3871
4126
|
// defaults
|
|
3872
4127
|
storefront,
|
|
3873
4128
|
customerAccount
|
|
@@ -3885,8 +4140,63 @@ function getStorefrontHeaders(request) {
|
|
|
3885
4140
|
return {
|
|
3886
4141
|
requestGroupId: getHeader(request, "request-id"),
|
|
3887
4142
|
buyerIp: getHeader(request, "oxygen-buyer-ip"),
|
|
4143
|
+
buyerIpSig: getHeader(request, "X-Shopify-Client-IP-Sig"),
|
|
3888
4144
|
cookie: getHeader(request, "cookie"),
|
|
3889
|
-
purpose: getHeader(request, "purpose")
|
|
4145
|
+
purpose: getHeader(request, "sec-purpose") || getHeader(request, "purpose")
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
function createRequestHandler({
|
|
4149
|
+
build,
|
|
4150
|
+
mode,
|
|
4151
|
+
poweredByHeader = true,
|
|
4152
|
+
getLoadContext,
|
|
4153
|
+
collectTrackingInformation = true,
|
|
4154
|
+
proxyStandardRoutes = true
|
|
4155
|
+
}) {
|
|
4156
|
+
const handleRequest = serverRuntime.createRequestHandler(build, mode);
|
|
4157
|
+
const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
|
|
4158
|
+
return async (request) => {
|
|
4159
|
+
const method = request.method;
|
|
4160
|
+
if ((method === "GET" || method === "HEAD") && request.body) {
|
|
4161
|
+
return new Response(`${method} requests cannot have a body`, {
|
|
4162
|
+
status: 400
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
const url = new URL(request.url);
|
|
4166
|
+
if (url.pathname.includes("//")) {
|
|
4167
|
+
return new Response(null, {
|
|
4168
|
+
status: 301,
|
|
4169
|
+
headers: {
|
|
4170
|
+
location: url.pathname.replace(/\/+/g, "/")
|
|
4171
|
+
}
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
const context = getLoadContext ? await getLoadContext(request) : void 0;
|
|
4175
|
+
const storefront = context?.storefront;
|
|
4176
|
+
if (proxyStandardRoutes) {
|
|
4177
|
+
if (!storefront) {
|
|
4178
|
+
warnOnce(
|
|
4179
|
+
"[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
if (storefront?.isStorefrontApiUrl(request)) {
|
|
4183
|
+
const response2 = await storefront.forward(request);
|
|
4184
|
+
appendPoweredByHeader?.(response2);
|
|
4185
|
+
return response2;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
const response = await handleRequest(request, context);
|
|
4189
|
+
if (storefront && proxyStandardRoutes) {
|
|
4190
|
+
if (collectTrackingInformation) {
|
|
4191
|
+
storefront.setCollectedSubrequestHeaders(response);
|
|
4192
|
+
}
|
|
4193
|
+
const fetchDest = request.headers.get("sec-fetch-dest");
|
|
4194
|
+
if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
|
|
4195
|
+
appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
appendPoweredByHeader?.(response);
|
|
4199
|
+
return response;
|
|
3890
4200
|
};
|
|
3891
4201
|
}
|
|
3892
4202
|
var NonceContext = react.createContext(void 0);
|
|
@@ -5946,6 +6256,10 @@ Object.defineProperty(exports, "getShopifyCookies", {
|
|
|
5946
6256
|
enumerable: true,
|
|
5947
6257
|
get: function () { return hydrogenReact.getShopifyCookies; }
|
|
5948
6258
|
});
|
|
6259
|
+
Object.defineProperty(exports, "getTrackingValues", {
|
|
6260
|
+
enumerable: true,
|
|
6261
|
+
get: function () { return hydrogenReact.getTrackingValues; }
|
|
6262
|
+
});
|
|
5949
6263
|
Object.defineProperty(exports, "isOptionValueCombinationInEncodedVariant", {
|
|
5950
6264
|
enumerable: true,
|
|
5951
6265
|
get: function () { return hydrogenReact.isOptionValueCombinationInEncodedVariant; }
|
|
@@ -6021,6 +6335,7 @@ exports.createCartHandler = createCartHandler;
|
|
|
6021
6335
|
exports.createContentSecurityPolicy = createContentSecurityPolicy;
|
|
6022
6336
|
exports.createCustomerAccountClient = createCustomerAccountClient;
|
|
6023
6337
|
exports.createHydrogenContext = createHydrogenContext;
|
|
6338
|
+
exports.createRequestHandler = createRequestHandler;
|
|
6024
6339
|
exports.createStorefrontClient = createStorefrontClient;
|
|
6025
6340
|
exports.createWithCache = createWithCache;
|
|
6026
6341
|
exports.formatAPIResult = formatAPIResult;
|