@shopify/hydrogen 2026.4.5 → 2026.4.7
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/README.md +1 -1
- package/dist/development/index.cjs +242 -372
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +245 -375
- package/dist/development/index.js.map +1 -1
- package/dist/oxygen/index.d.ts +39 -7
- package/dist/production/index.cjs +42 -42
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.d.cts +46 -20
- package/dist/production/index.d.ts +46 -20
- package/dist/production/index.js +42 -42
- package/dist/production/index.js.map +1 -1
- package/dist/vite/request-events.d.ts +39 -7
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createContext, forwardRef, lazy, useContext, useMemo, useEffect,
|
|
1
|
+
import { createContext, forwardRef, lazy, useContext, useRef, useState, useMemo, useEffect, useCallback, startTransition, createElement, Fragment as Fragment$1, Suspense } from 'react';
|
|
2
2
|
import { createContext as createContext$1, useRevalidator, useFetcher, useFetchers, RouterContextProvider, createRequestHandler as createRequestHandler$1, useNavigation, useLocation, useNavigate, Link, useMatches } from 'react-router';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
-
import { useLoadScript,
|
|
4
|
+
import { useLoadScript, createStorefrontClient as createStorefrontClient$1, SHOPIFY_STOREFRONT_ID_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, flattenConnection, RichText as RichText$1, ShopPayButton as ShopPayButton$1, parseGid, getTrackingValues, sendShopifyAnalytics, AnalyticsEventName, AnalyticsPageType, getClientBrowserParameters } from '@shopify/hydrogen-react';
|
|
5
5
|
export { AnalyticsEventName, AnalyticsPageType, ExternalVideo, IMAGE_FRAGMENT, Image, MediaFile, ModelViewer, Money, ShopifySalesChannel, Video, customerAccountApiCustomScalars, decodeEncodedVariant, flattenConnection, getAdjacentAndFirstAvailableVariants, getClientBrowserParameters, getProductOptions, getShopifyCookies, getTrackingValues, isOptionValueCombinationInEncodedVariant, mapSelectedProductOptionToObject, parseGid, parseMetafield, sendShopifyAnalytics, storefrontApiCustomScalars, useLoadScript, useMoney, useSelectedOptionInUrlParam, useShopifyCookies } from '@shopify/hydrogen-react';
|
|
6
|
-
import {
|
|
6
|
+
import { loadScript } from '@shopify/hydrogen-react/load-script';
|
|
7
7
|
import { createGraphQLClient } from '@shopify/graphql-client';
|
|
8
8
|
import { parse, stringify } from 'worktop/cookie';
|
|
9
9
|
import cspBuilder from 'content-security-policy-builder';
|
|
@@ -59,6 +59,25 @@ function AnalyticsCustomView(props) {
|
|
|
59
59
|
return /* @__PURE__ */ jsx(AnalyticsView, { ...props });
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// src/analytics-manager/consent.ts
|
|
63
|
+
function hasAnalyticsConsent() {
|
|
64
|
+
try {
|
|
65
|
+
const privacy = window.Shopify?.customerPrivacy;
|
|
66
|
+
if (privacy?.consentStatus !== "loaded") return false;
|
|
67
|
+
if (privacy.currentVisitorConsent?.().analytics === "no") return false;
|
|
68
|
+
return privacy.analyticsProcessingAllowed?.() ?? false;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function shouldWaitForPrivacyBanner(privacy) {
|
|
74
|
+
if (!privacy.shouldShowBanner()) return false;
|
|
75
|
+
const consent = privacy.currentVisitorConsent();
|
|
76
|
+
return ![consent.marketing, consent.analytics, consent.preferences].some(
|
|
77
|
+
(value) => value === "yes" || value === "no"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
// src/analytics-manager/events.ts
|
|
63
82
|
var AnalyticsEvent = {
|
|
64
83
|
// Views
|
|
@@ -74,61 +93,6 @@ var AnalyticsEvent = {
|
|
|
74
93
|
// Custom
|
|
75
94
|
CUSTOM_EVENT: `custom_`
|
|
76
95
|
};
|
|
77
|
-
|
|
78
|
-
// src/constants.ts
|
|
79
|
-
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
80
|
-
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
81
|
-
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
82
|
-
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
83
|
-
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
84
|
-
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
85
|
-
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
86
|
-
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
87
|
-
var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
|
|
88
|
-
|
|
89
|
-
// src/utils/server-timing.ts
|
|
90
|
-
function buildServerTimingHeader(values) {
|
|
91
|
-
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
92
|
-
}
|
|
93
|
-
function appendServerTimingHeader(response, values) {
|
|
94
|
-
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
95
|
-
if (header) {
|
|
96
|
-
response.headers.append("Server-Timing", header);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
var trackedTimings = ["_y", "_s", "_cmp"];
|
|
100
|
-
function extractServerTimingHeader(serverTimingHeader) {
|
|
101
|
-
const values = {};
|
|
102
|
-
if (!serverTimingHeader) return values;
|
|
103
|
-
const re = new RegExp(
|
|
104
|
-
`\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
|
|
105
|
-
"g"
|
|
106
|
-
);
|
|
107
|
-
let match;
|
|
108
|
-
while ((match = re.exec(serverTimingHeader)) !== null) {
|
|
109
|
-
values[match[1]] = match[2];
|
|
110
|
-
}
|
|
111
|
-
return values;
|
|
112
|
-
}
|
|
113
|
-
function hasServerTimingInNavigationEntry(key) {
|
|
114
|
-
if (typeof window === "undefined") return false;
|
|
115
|
-
try {
|
|
116
|
-
const navigationEntry = window.performance.getEntriesByType(
|
|
117
|
-
"navigation"
|
|
118
|
-
)[0];
|
|
119
|
-
return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
|
|
120
|
-
} catch (e) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function isSfapiProxyEnabled() {
|
|
125
|
-
return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
|
|
126
|
-
}
|
|
127
|
-
function hasServerReturnedTrackingValues() {
|
|
128
|
-
return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/customer-privacy/ShopifyCustomerPrivacy.tsx
|
|
132
96
|
var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
|
|
133
97
|
var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
|
|
134
98
|
function logMissingConfig(fieldName) {
|
|
@@ -136,6 +100,11 @@ function logMissingConfig(fieldName) {
|
|
|
136
100
|
`[h2:error:useCustomerPrivacy] Unable to setup Customer Privacy API: Missing consent.${fieldName} configuration.`
|
|
137
101
|
);
|
|
138
102
|
}
|
|
103
|
+
function logIgnoredCrossDomainConsent() {
|
|
104
|
+
console.warn(
|
|
105
|
+
"[h2:warn:useCustomerPrivacy] `sameDomainForStorefrontApi: false` is ignored. Consent and analytics require the same-origin Storefront API proxy included in Hydrogen's `createRequestHandler`, so consent requests always use it. Remove this option. See https://shopify.dev/changelog/posts/tracking-cookie-deprecation-hydrogen"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
139
108
|
function useCustomerPrivacy(props) {
|
|
140
109
|
const {
|
|
141
110
|
withPrivacyBanner = false,
|
|
@@ -147,27 +116,15 @@ function useCustomerPrivacy(props) {
|
|
|
147
116
|
locale,
|
|
148
117
|
sameDomainForStorefrontApi
|
|
149
118
|
} = props;
|
|
150
|
-
const hasSfapiProxy = useMemo(
|
|
151
|
-
() => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
|
|
152
|
-
[sameDomainForStorefrontApi]
|
|
153
|
-
);
|
|
154
|
-
const fetchTrackingValuesFromBrowser = useMemo(
|
|
155
|
-
() => hasSfapiProxy && !hasServerReturnedTrackingValues(),
|
|
156
|
-
[hasSfapiProxy]
|
|
157
|
-
);
|
|
158
|
-
const cookiesReady = useShopifyCookies({
|
|
159
|
-
fetchTrackingValues: fetchTrackingValuesFromBrowser,
|
|
160
|
-
storefrontAccessToken,
|
|
161
|
-
ignoreDeprecatedCookies: true
|
|
162
|
-
});
|
|
163
|
-
const initialTrackingValues = useMemo(getTrackingValues, [cookiesReady]);
|
|
164
119
|
const { revalidate } = useRevalidator();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
120
|
+
const callbacks = useRef({ onReady, onVisitorConsentCollected, revalidate });
|
|
121
|
+
callbacks.current = { onReady, onVisitorConsentCollected, revalidate };
|
|
122
|
+
const notifiedReady = useRef(false);
|
|
123
|
+
const previousTrackingValues = useRef();
|
|
124
|
+
const [apis, setApis] = useState(() => ({
|
|
125
|
+
customerPrivacy: getCustomerPrivacy(),
|
|
126
|
+
privacyBanner: getPrivacyBanner()
|
|
127
|
+
}));
|
|
171
128
|
const config = useMemo(() => {
|
|
172
129
|
if (!checkoutDomain) logMissingConfig("checkoutDomain");
|
|
173
130
|
if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
|
|
@@ -176,196 +133,155 @@ function useCustomerPrivacy(props) {
|
|
|
176
133
|
`[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
|
|
177
134
|
);
|
|
178
135
|
}
|
|
136
|
+
if (sameDomainForStorefrontApi === false) logIgnoredCrossDomainConsent();
|
|
179
137
|
const commonAncestorDomain = parseStoreDomain(checkoutDomain);
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// This domain is used to send requests to SFAPI for setting and getting consent.
|
|
187
|
-
checkoutRootDomain: sfapiDomain,
|
|
188
|
-
// Prefix with a dot to ensure this domain is different from checkoutRootDomain.
|
|
189
|
-
// This will ensure old cookies are set for a cross-subdomain checkout setup
|
|
190
|
-
// so that we keep backward compatibility until new cookies are rolled out.
|
|
191
|
-
// Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
|
|
138
|
+
return {
|
|
139
|
+
// Always the same-origin proxy: cross-origin consent requests carry no
|
|
140
|
+
// cookies, so consent and visitor tokens would reset on every page load
|
|
141
|
+
// (and an opt-out would be forgotten). The server render never sends
|
|
142
|
+
// consent requests, so it keeps the checkout domain.
|
|
143
|
+
checkoutRootDomain: typeof window !== "undefined" ? window.location.host : checkoutDomain,
|
|
192
144
|
storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
|
|
193
145
|
storefrontAccessToken,
|
|
194
146
|
country,
|
|
195
147
|
locale
|
|
196
148
|
};
|
|
197
|
-
return config2;
|
|
198
149
|
}, [
|
|
199
|
-
|
|
150
|
+
sameDomainForStorefrontApi,
|
|
200
151
|
checkoutDomain,
|
|
201
152
|
storefrontAccessToken,
|
|
202
153
|
country,
|
|
203
154
|
locale
|
|
204
155
|
]);
|
|
205
156
|
useEffect(() => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
157
|
+
let active = true;
|
|
158
|
+
const shopify = window.Shopify ??= {};
|
|
159
|
+
const privacy = shopify.customerPrivacy ??= {};
|
|
160
|
+
privacy.config = {
|
|
161
|
+
...privacy.config,
|
|
162
|
+
isHeadless: true,
|
|
163
|
+
asyncConsent: true,
|
|
164
|
+
asyncVisitorState: true,
|
|
165
|
+
consentDomain: config.checkoutRootDomain,
|
|
166
|
+
storefrontAccessToken: config.storefrontAccessToken,
|
|
167
|
+
debug: {
|
|
168
|
+
...privacy.config?.debug,
|
|
169
|
+
hydrogen: HYDROGEN_DEBUG_METADATA
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
if (config.country) shopify.country = config.country;
|
|
173
|
+
if (config.locale) shopify.locale = config.locale.toLowerCase();
|
|
174
|
+
const updateTrackingValues = () => {
|
|
175
|
+
const latest = getTrackingValues();
|
|
176
|
+
const previous = previousTrackingValues.current;
|
|
177
|
+
previousTrackingValues.current = latest;
|
|
178
|
+
if (previous && (previous.visitToken !== latest.visitToken || previous.uniqueToken !== latest.uniqueToken)) {
|
|
179
|
+
callbacks.current.revalidate().catch(() => {
|
|
210
180
|
console.warn(
|
|
211
181
|
"[h2:warn:useCustomerPrivacy] Revalidation failed after consent change."
|
|
212
182
|
);
|
|
213
183
|
});
|
|
214
184
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
185
|
+
};
|
|
186
|
+
const checkReady = () => {
|
|
187
|
+
if (!active) return false;
|
|
188
|
+
const customerPrivacy = getCustomerPrivacy();
|
|
189
|
+
const privacyBanner = getPrivacyBanner();
|
|
190
|
+
if (customerPrivacy) configureCustomerPrivacy(customerPrivacy, config);
|
|
191
|
+
if (withPrivacyBanner && privacyBanner) {
|
|
192
|
+
configurePrivacyBanner(privacyBanner, config);
|
|
193
|
+
}
|
|
194
|
+
setApis(
|
|
195
|
+
(previous) => previous.customerPrivacy === customerPrivacy && previous.privacyBanner === privacyBanner ? previous : { customerPrivacy, privacyBanner }
|
|
196
|
+
);
|
|
197
|
+
if (customerPrivacy?.consentStatus !== "loaded" || withPrivacyBanner && !privacyBanner) {
|
|
198
|
+
return false;
|
|
228
199
|
}
|
|
200
|
+
if (!notifiedReady.current) {
|
|
201
|
+
notifiedReady.current = true;
|
|
202
|
+
updateTrackingValues();
|
|
203
|
+
emitCustomerPrivacyApiLoaded(customerPrivacy);
|
|
204
|
+
callbacks.current.onReady?.();
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
};
|
|
208
|
+
const consentCollectedHandler = (event) => {
|
|
209
|
+
if (!checkReady()) return;
|
|
210
|
+
updateTrackingValues();
|
|
211
|
+
callbacks.current.onVisitorConsentCollected?.(event.detail);
|
|
229
212
|
};
|
|
213
|
+
document.addEventListener("consentTrackingApiLoaded", checkReady);
|
|
230
214
|
document.addEventListener(
|
|
231
215
|
"visitorConsentCollected",
|
|
232
216
|
consentCollectedHandler
|
|
233
217
|
);
|
|
218
|
+
if (!checkReady()) {
|
|
219
|
+
void loadScript(
|
|
220
|
+
withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API,
|
|
221
|
+
{
|
|
222
|
+
attributes: { id: "customer-privacy-api" }
|
|
223
|
+
}
|
|
224
|
+
).then(checkReady, () => {
|
|
225
|
+
if (active) {
|
|
226
|
+
console.error(
|
|
227
|
+
"[h2:error:useCustomerPrivacy] Unable to load the Customer Privacy API."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
234
232
|
return () => {
|
|
233
|
+
active = false;
|
|
234
|
+
document.removeEventListener("consentTrackingApiLoaded", checkReady);
|
|
235
235
|
document.removeEventListener(
|
|
236
236
|
"visitorConsentCollected",
|
|
237
237
|
consentCollectedHandler
|
|
238
238
|
);
|
|
239
239
|
};
|
|
240
|
-
}, [
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let customPrivacyBanner = window.privacyBanner || void 0;
|
|
245
|
-
const privacyBannerWatcher = {
|
|
246
|
-
configurable: true,
|
|
247
|
-
get() {
|
|
248
|
-
return customPrivacyBanner;
|
|
249
|
-
},
|
|
250
|
-
set(value) {
|
|
251
|
-
if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
|
|
252
|
-
customPrivacyBanner = overridePrivacyBannerMethods({
|
|
253
|
-
privacyBanner: value,
|
|
254
|
-
config
|
|
255
|
-
});
|
|
256
|
-
setLoaded.privacyBanner();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
Object.defineProperty(window, "privacyBanner", privacyBannerWatcher);
|
|
261
|
-
}, [
|
|
262
|
-
withPrivacyBanner,
|
|
263
|
-
config,
|
|
264
|
-
overridePrivacyBannerMethods,
|
|
265
|
-
setLoaded.privacyBanner
|
|
266
|
-
]);
|
|
267
|
-
useEffect(() => {
|
|
268
|
-
if (observing.current.customerPrivacy) return;
|
|
269
|
-
observing.current.customerPrivacy = true;
|
|
270
|
-
let backendConsentStub = null;
|
|
271
|
-
let fullCustomerPrivacy = null;
|
|
272
|
-
let customShopify = window.Shopify || void 0;
|
|
273
|
-
Object.defineProperty(window, "Shopify", {
|
|
274
|
-
configurable: true,
|
|
275
|
-
get() {
|
|
276
|
-
return customShopify;
|
|
277
|
-
},
|
|
278
|
-
set(value) {
|
|
279
|
-
if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
|
|
280
|
-
customShopify = value;
|
|
281
|
-
backendConsentStub = { backendConsentEnabled: true };
|
|
282
|
-
Object.defineProperty(window.Shopify, "customerPrivacy", {
|
|
283
|
-
configurable: true,
|
|
284
|
-
get() {
|
|
285
|
-
return fullCustomerPrivacy ?? backendConsentStub;
|
|
286
|
-
},
|
|
287
|
-
set(value2) {
|
|
288
|
-
if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) {
|
|
289
|
-
const customerPrivacy = value2;
|
|
290
|
-
fullCustomerPrivacy = {
|
|
291
|
-
...customerPrivacy,
|
|
292
|
-
// Note: this method is not used by the privacy-banner,
|
|
293
|
-
// it bundles its own setTrackingConsent.
|
|
294
|
-
setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
|
|
295
|
-
{ customerPrivacy, config }
|
|
296
|
-
)
|
|
297
|
-
};
|
|
298
|
-
customShopify = {
|
|
299
|
-
...customShopify,
|
|
300
|
-
customerPrivacy: fullCustomerPrivacy
|
|
301
|
-
};
|
|
302
|
-
setLoaded.customerPrivacy();
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}, [
|
|
310
|
-
config,
|
|
311
|
-
overrideCustomerPrivacySetTrackingConsent,
|
|
312
|
-
setLoaded.customerPrivacy
|
|
313
|
-
]);
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
if (!apisLoaded || !cookiesReady) return;
|
|
316
|
-
const customerPrivacy = getCustomerPrivacy();
|
|
317
|
-
if (customerPrivacy && !customerPrivacy.cachedConsent) {
|
|
318
|
-
const trackingValues = getTrackingValues();
|
|
319
|
-
if (trackingValues.consent) {
|
|
320
|
-
customerPrivacy.cachedConsent = trackingValues.consent;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (withPrivacyBanner) {
|
|
324
|
-
const privacyBanner = getPrivacyBanner();
|
|
325
|
-
if (privacyBanner) {
|
|
326
|
-
privacyBanner.loadBanner(config);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
emitCustomerPrivacyApiLoaded();
|
|
330
|
-
onReady?.();
|
|
331
|
-
}, [apisLoaded, cookiesReady]);
|
|
332
|
-
const result = {
|
|
333
|
-
customerPrivacy: getCustomerPrivacy()
|
|
240
|
+
}, [config, withPrivacyBanner]);
|
|
241
|
+
return {
|
|
242
|
+
customerPrivacy: apis.customerPrivacy,
|
|
243
|
+
...withPrivacyBanner ? { privacyBanner: apis.privacyBanner } : {}
|
|
334
244
|
};
|
|
335
|
-
if (withPrivacyBanner) {
|
|
336
|
-
result.privacyBanner = getPrivacyBanner();
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
339
245
|
}
|
|
340
|
-
var
|
|
341
|
-
function emitCustomerPrivacyApiLoaded() {
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
setApisLoaded((prev) => [prev[0], true]);
|
|
366
|
-
}
|
|
246
|
+
var readyApis = /* @__PURE__ */ new WeakSet();
|
|
247
|
+
function emitCustomerPrivacyApiLoaded(customerPrivacy) {
|
|
248
|
+
if (readyApis.has(customerPrivacy)) return;
|
|
249
|
+
readyApis.add(customerPrivacy);
|
|
250
|
+
document.dispatchEvent(new CustomEvent("shopifyCustomerPrivacyApiLoaded"));
|
|
251
|
+
}
|
|
252
|
+
var HYDROGEN_DEBUG_METADATA = { generation: 2, serverTiming: false };
|
|
253
|
+
var configuredCustomerPrivacy = /* @__PURE__ */ new WeakMap();
|
|
254
|
+
function configureCustomerPrivacy(customerPrivacy, config) {
|
|
255
|
+
const existing = configuredCustomerPrivacy.get(customerPrivacy);
|
|
256
|
+
if (existing) {
|
|
257
|
+
existing.config = config;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const state = { config };
|
|
261
|
+
configuredCustomerPrivacy.set(customerPrivacy, state);
|
|
262
|
+
const original = customerPrivacy.setTrackingConsent;
|
|
263
|
+
customerPrivacy.setTrackingConsent = (consent, callback) => {
|
|
264
|
+
const { locale, country, ...headlessConfig } = state.config;
|
|
265
|
+
const params = {
|
|
266
|
+
...headlessConfig,
|
|
267
|
+
headlessStorefront: true,
|
|
268
|
+
...consent
|
|
269
|
+
};
|
|
270
|
+
original.call(customerPrivacy, params, callback);
|
|
367
271
|
};
|
|
368
|
-
|
|
272
|
+
}
|
|
273
|
+
var configuredPrivacyBanners = /* @__PURE__ */ new WeakMap();
|
|
274
|
+
function configurePrivacyBanner(privacyBanner, config) {
|
|
275
|
+
const existing = configuredPrivacyBanners.get(privacyBanner);
|
|
276
|
+
if (existing) {
|
|
277
|
+
existing.config = config;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const state = { config };
|
|
281
|
+
configuredPrivacyBanners.set(privacyBanner, state);
|
|
282
|
+
const { loadBanner, showPreferences } = privacyBanner;
|
|
283
|
+
privacyBanner.loadBanner = (options) => loadBanner.call(privacyBanner, { ...state.config, ...options });
|
|
284
|
+
privacyBanner.showPreferences = (options) => showPreferences.call(privacyBanner, { ...state.config, ...options });
|
|
369
285
|
}
|
|
370
286
|
function parseStoreDomain(checkoutDomain) {
|
|
371
287
|
if (typeof window === "undefined") return;
|
|
@@ -380,64 +296,25 @@ function parseStoreDomain(checkoutDomain) {
|
|
|
380
296
|
});
|
|
381
297
|
return sameDomainParts.reverse().join(".") || void 0;
|
|
382
298
|
}
|
|
383
|
-
function overrideCustomerPrivacySetTrackingConsent({
|
|
384
|
-
customerPrivacy,
|
|
385
|
-
config
|
|
386
|
-
}) {
|
|
387
|
-
const original = customerPrivacy.setTrackingConsent;
|
|
388
|
-
const { locale, country, ...rest } = config;
|
|
389
|
-
function updatedSetTrackingConsent(consent, callback) {
|
|
390
|
-
original(
|
|
391
|
-
{
|
|
392
|
-
...rest,
|
|
393
|
-
headlessStorefront: true,
|
|
394
|
-
...consent
|
|
395
|
-
},
|
|
396
|
-
callback
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
return updatedSetTrackingConsent;
|
|
400
|
-
}
|
|
401
|
-
function overridePrivacyBannerMethods({
|
|
402
|
-
privacyBanner,
|
|
403
|
-
config
|
|
404
|
-
}) {
|
|
405
|
-
const originalLoadBanner = privacyBanner.loadBanner;
|
|
406
|
-
const originalShowPreferences = privacyBanner.showPreferences;
|
|
407
|
-
function loadBanner(userConfig) {
|
|
408
|
-
if (typeof userConfig === "object") {
|
|
409
|
-
originalLoadBanner({ ...config, ...userConfig });
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
originalLoadBanner(config);
|
|
413
|
-
}
|
|
414
|
-
function showPreferences(userConfig) {
|
|
415
|
-
if (typeof userConfig === "object") {
|
|
416
|
-
originalShowPreferences({ ...config, ...userConfig });
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
originalShowPreferences(config);
|
|
420
|
-
}
|
|
421
|
-
return { loadBanner, showPreferences };
|
|
422
|
-
}
|
|
423
299
|
function getCustomerPrivacy() {
|
|
424
300
|
try {
|
|
425
301
|
const cp = window.Shopify?.customerPrivacy;
|
|
426
|
-
return cp
|
|
302
|
+
return typeof cp?.setTrackingConsent === "function" ? cp : null;
|
|
427
303
|
} catch (e) {
|
|
428
304
|
return null;
|
|
429
305
|
}
|
|
430
306
|
}
|
|
431
307
|
function getPrivacyBanner() {
|
|
432
308
|
try {
|
|
433
|
-
|
|
309
|
+
const banner = window.privacyBanner;
|
|
310
|
+
return typeof banner?.loadBanner === "function" && typeof banner.showPreferences === "function" ? banner : null;
|
|
434
311
|
} catch (e) {
|
|
435
312
|
return null;
|
|
436
313
|
}
|
|
437
314
|
}
|
|
438
315
|
|
|
439
316
|
// package.json
|
|
440
|
-
var version = "2026.4.
|
|
317
|
+
var version = "2026.4.7";
|
|
441
318
|
|
|
442
319
|
// src/analytics-manager/ShopifyAnalytics.tsx
|
|
443
320
|
function getCustomerPrivacyRequired() {
|
|
@@ -451,47 +328,13 @@ function getCustomerPrivacyRequired() {
|
|
|
451
328
|
}
|
|
452
329
|
function ShopifyAnalytics({
|
|
453
330
|
consent,
|
|
454
|
-
|
|
455
|
-
domain
|
|
331
|
+
onConsentChange
|
|
456
332
|
}) {
|
|
457
|
-
const { subscribe: subscribe2, register: register2
|
|
458
|
-
const [shopifyReady, setShopifyReady] = useState(false);
|
|
333
|
+
const { subscribe: subscribe2, register: register2 } = useAnalytics();
|
|
459
334
|
const [privacyReady, setPrivacyReady] = useState(false);
|
|
460
|
-
const [collectedConsent, setCollectedConsent] = useState("");
|
|
461
335
|
const init = useRef(false);
|
|
462
336
|
const { checkoutDomain, storefrontAccessToken, language } = consent;
|
|
463
337
|
const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
|
|
464
|
-
useCustomerPrivacy({
|
|
465
|
-
...consent,
|
|
466
|
-
locale: language,
|
|
467
|
-
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
468
|
-
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
469
|
-
// If we use privacy banner, we should wait until consent is collected.
|
|
470
|
-
// Otherwise, we can consider privacy ready immediately:
|
|
471
|
-
onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
|
|
472
|
-
onVisitorConsentCollected: (consent2) => {
|
|
473
|
-
try {
|
|
474
|
-
setCollectedConsent(JSON.stringify(consent2));
|
|
475
|
-
} catch (e) {
|
|
476
|
-
}
|
|
477
|
-
setPrivacyReady(true);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
const hasUserConsent = useMemo(
|
|
481
|
-
// must be initialized with true to avoid removing cookies too early
|
|
482
|
-
() => privacyReady ? canTrack() : true,
|
|
483
|
-
// Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
|
|
484
|
-
[privacyReady, canTrack, collectedConsent]
|
|
485
|
-
);
|
|
486
|
-
useShopifyCookies({
|
|
487
|
-
hasUserConsent,
|
|
488
|
-
domain,
|
|
489
|
-
checkoutDomain,
|
|
490
|
-
// Already done inside useCustomerPrivacy
|
|
491
|
-
fetchTrackingValues: false,
|
|
492
|
-
// Avoid creating local cookies too early
|
|
493
|
-
ignoreDeprecatedCookies: !privacyReady
|
|
494
|
-
});
|
|
495
338
|
useEffect(() => {
|
|
496
339
|
if (init.current) return;
|
|
497
340
|
init.current = true;
|
|
@@ -500,14 +343,28 @@ function ShopifyAnalytics({
|
|
|
500
343
|
subscribe2(AnalyticsEvent.COLLECTION_VIEWED, collectionViewHandler);
|
|
501
344
|
subscribe2(AnalyticsEvent.SEARCH_VIEWED, searchViewHandler);
|
|
502
345
|
subscribe2(AnalyticsEvent.PRODUCT_ADD_TO_CART, productAddedToCartHandler);
|
|
503
|
-
setShopifyReady(true);
|
|
504
346
|
}, [subscribe2]);
|
|
347
|
+
const updatePrivacyReadiness = () => {
|
|
348
|
+
if (privacyReady) {
|
|
349
|
+
onConsentChange();
|
|
350
|
+
} else if (!consent.withPrivacyBanner || !shouldWaitForPrivacyBanner(getCustomerPrivacyRequired())) {
|
|
351
|
+
setPrivacyReady(true);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
useCustomerPrivacy({
|
|
355
|
+
...consent,
|
|
356
|
+
locale: language,
|
|
357
|
+
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
|
|
358
|
+
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
|
|
359
|
+
onReady: updatePrivacyReadiness,
|
|
360
|
+
onVisitorConsentCollected: updatePrivacyReadiness
|
|
361
|
+
});
|
|
505
362
|
useEffect(() => {
|
|
506
|
-
if (
|
|
363
|
+
if (privacyReady) {
|
|
507
364
|
shopifyAnalyticsReady();
|
|
508
|
-
|
|
365
|
+
onConsentChange();
|
|
509
366
|
}
|
|
510
|
-
}, [
|
|
367
|
+
}, [privacyReady, onConsentChange]);
|
|
511
368
|
return null;
|
|
512
369
|
}
|
|
513
370
|
function logMissingConfig2(fieldName) {
|
|
@@ -517,7 +374,8 @@ function logMissingConfig2(fieldName) {
|
|
|
517
374
|
}
|
|
518
375
|
function prepareBasePageViewPayload(payload) {
|
|
519
376
|
const customerPrivacy = getCustomerPrivacyRequired();
|
|
520
|
-
const hasUserConsent =
|
|
377
|
+
const hasUserConsent = hasAnalyticsConsent();
|
|
378
|
+
if (!hasUserConsent) return;
|
|
521
379
|
if (!payload?.shop?.shopId) {
|
|
522
380
|
logMissingConfig2("shopId");
|
|
523
381
|
return;
|
|
@@ -958,13 +816,8 @@ function register(key) {
|
|
|
958
816
|
}
|
|
959
817
|
};
|
|
960
818
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return window.Shopify.customerPrivacy?.analyticsProcessingAllowed?.() ?? false;
|
|
964
|
-
} catch (e) {
|
|
965
|
-
}
|
|
966
|
-
return false;
|
|
967
|
-
}
|
|
819
|
+
var noopPublish = () => {
|
|
820
|
+
};
|
|
968
821
|
function messageOnError(field, envVar) {
|
|
969
822
|
return `[h2:error:Analytics.Provider] - ${field} is required. Make sure ${envVar} is defined in your environment variables. See https://h2o.fyi/analytics/consent to learn how to setup environment variables in the Shopify admin.`;
|
|
970
823
|
}
|
|
@@ -974,18 +827,31 @@ function AnalyticsProvider({
|
|
|
974
827
|
children,
|
|
975
828
|
consent,
|
|
976
829
|
customData = {},
|
|
977
|
-
shop: shopProp = null
|
|
978
|
-
cookieDomain
|
|
830
|
+
shop: shopProp = null
|
|
979
831
|
}) {
|
|
980
832
|
const { shop } = useShopAnalytics(shopProp);
|
|
981
|
-
const [
|
|
982
|
-
|
|
833
|
+
const [consentVersion, setConsentVersion] = useState(0);
|
|
834
|
+
const [privacyReady, setPrivacyReady] = useState(false);
|
|
835
|
+
const [carts, setCartsState] = useState({ cart: null, prevCart: null });
|
|
836
|
+
const setCarts = useCallback(
|
|
837
|
+
(update) => startTransition(() => setCartsState(update)),
|
|
838
|
+
[]
|
|
983
839
|
);
|
|
984
|
-
const
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
840
|
+
const canTrack = customCanTrack ?? hasAnalyticsConsent;
|
|
841
|
+
const canTrackRef = useRef(canTrack);
|
|
842
|
+
canTrackRef.current = canTrack;
|
|
843
|
+
const guardedPublish = useCallback(
|
|
844
|
+
(event, payload) => {
|
|
845
|
+
if (canTrackRef.current()) publish(event, payload);
|
|
846
|
+
},
|
|
847
|
+
[]
|
|
988
848
|
);
|
|
849
|
+
const onConsentChange = useCallback(() => {
|
|
850
|
+
startTransition(() => {
|
|
851
|
+
setPrivacyReady(true);
|
|
852
|
+
setConsentVersion((version2) => version2 + 1);
|
|
853
|
+
});
|
|
854
|
+
}, []);
|
|
989
855
|
if (!!shop) {
|
|
990
856
|
if (/\/68817551382$/.test(shop.shopId)) {
|
|
991
857
|
warnOnce(
|
|
@@ -1022,8 +888,7 @@ function AnalyticsProvider({
|
|
|
1022
888
|
canTrack,
|
|
1023
889
|
...carts,
|
|
1024
890
|
customData,
|
|
1025
|
-
publish: canTrack() ?
|
|
1026
|
-
},
|
|
891
|
+
publish: canTrack() ? guardedPublish : noopPublish,
|
|
1027
892
|
shop,
|
|
1028
893
|
subscribe,
|
|
1029
894
|
register,
|
|
@@ -1031,12 +896,12 @@ function AnalyticsProvider({
|
|
|
1031
896
|
privacyBanner: getPrivacyBanner()
|
|
1032
897
|
};
|
|
1033
898
|
}, [
|
|
1034
|
-
|
|
899
|
+
consentVersion,
|
|
1035
900
|
canTrack,
|
|
1036
901
|
carts,
|
|
1037
902
|
carts.cart?.updatedAt,
|
|
1038
903
|
carts.prevCart,
|
|
1039
|
-
|
|
904
|
+
guardedPublish,
|
|
1040
905
|
subscribe,
|
|
1041
906
|
customData,
|
|
1042
907
|
shop,
|
|
@@ -1049,21 +914,8 @@ function AnalyticsProvider({
|
|
|
1049
914
|
children,
|
|
1050
915
|
!!shop && /* @__PURE__ */ jsx(AnalyticsPageView, {}),
|
|
1051
916
|
!!shop && !!currentCart && /* @__PURE__ */ jsx(CartAnalytics, { cart: currentCart, setCarts }),
|
|
1052
|
-
!!shop && /* @__PURE__ */ jsx(
|
|
1053
|
-
|
|
1054
|
-
{
|
|
1055
|
-
consent,
|
|
1056
|
-
onReady: () => {
|
|
1057
|
-
setAnalyticsLoaded(true);
|
|
1058
|
-
setCanTrack(
|
|
1059
|
-
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
|
|
1060
|
-
);
|
|
1061
|
-
setConsentCollected(true);
|
|
1062
|
-
},
|
|
1063
|
-
domain: cookieDomain
|
|
1064
|
-
}
|
|
1065
|
-
),
|
|
1066
|
-
!!shop && consentCollected && /* @__PURE__ */ jsx(PerfKit, { shop })
|
|
917
|
+
!!shop && /* @__PURE__ */ jsx(ShopifyAnalytics, { consent, onConsentChange }),
|
|
918
|
+
!!shop && privacyReady && /* @__PURE__ */ jsx(PerfKit, { shop })
|
|
1067
919
|
] });
|
|
1068
920
|
}
|
|
1069
921
|
function useAnalytics() {
|
|
@@ -1078,7 +930,9 @@ function useAnalytics() {
|
|
|
1078
930
|
function useShopAnalytics(shopProp) {
|
|
1079
931
|
const [shop, setShop] = useState(null);
|
|
1080
932
|
useEffect(() => {
|
|
1081
|
-
Promise.resolve(shopProp).then(
|
|
933
|
+
Promise.resolve(shopProp).then((resolvedShop) => {
|
|
934
|
+
startTransition(() => setShop(resolvedShop));
|
|
935
|
+
});
|
|
1082
936
|
return () => {
|
|
1083
937
|
};
|
|
1084
938
|
}, [setShop, shopProp]);
|
|
@@ -1128,6 +982,17 @@ var Analytics = {
|
|
|
1128
982
|
SearchView: AnalyticsSearchView
|
|
1129
983
|
};
|
|
1130
984
|
|
|
985
|
+
// src/constants.ts
|
|
986
|
+
var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
|
|
987
|
+
var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
|
|
988
|
+
var SDK_VARIANT_HEADER = "X-SDK-Variant";
|
|
989
|
+
var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
|
|
990
|
+
var SDK_VERSION_HEADER = "X-SDK-Version";
|
|
991
|
+
var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
|
|
992
|
+
var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
|
|
993
|
+
var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
|
|
994
|
+
var STOREFRONT_CONSENT_MANAGEMENT_HEADER = "Shopify-Storefront-Consent-Management";
|
|
995
|
+
|
|
1131
996
|
// src/utils/request.ts
|
|
1132
997
|
function getHeader(request, key) {
|
|
1133
998
|
return getHeaderValue(request.headers, key);
|
|
@@ -1833,7 +1698,7 @@ function generateUUID() {
|
|
|
1833
1698
|
}
|
|
1834
1699
|
|
|
1835
1700
|
// src/version.ts
|
|
1836
|
-
var LIB_VERSION = "2026.4.
|
|
1701
|
+
var LIB_VERSION = "2026.4.7";
|
|
1837
1702
|
|
|
1838
1703
|
// src/utils/graphql.ts
|
|
1839
1704
|
function minifyQuery(string) {
|
|
@@ -2094,8 +1959,7 @@ function createStorefrontClient(options) {
|
|
|
2094
1959
|
streamConfig,
|
|
2095
1960
|
onRawHeaders: (headers2) => {
|
|
2096
1961
|
collectedSubrequestHeaders ??= {
|
|
2097
|
-
setCookie: headers2.getSetCookie()
|
|
2098
|
-
serverTiming: headers2.get("server-timing") ?? ""
|
|
1962
|
+
setCookie: headers2.getSetCookie()
|
|
2099
1963
|
};
|
|
2100
1964
|
}
|
|
2101
1965
|
});
|
|
@@ -2228,6 +2092,10 @@ function createStorefrontClient(options) {
|
|
|
2228
2092
|
"origin",
|
|
2229
2093
|
"referer",
|
|
2230
2094
|
"user-agent",
|
|
2095
|
+
// The marker identifies headless consent-management traffic
|
|
2096
|
+
// to the backend, so the proxy forwards it upstream with the
|
|
2097
|
+
// rest of the allowlisted set.
|
|
2098
|
+
STOREFRONT_CONSENT_MANAGEMENT_HEADER,
|
|
2231
2099
|
STOREFRONT_ACCESS_TOKEN_HEADER,
|
|
2232
2100
|
SHOPIFY_UNIQUE_TOKEN_HEADER,
|
|
2233
2101
|
SHOPIFY_VISIT_TOKEN_HEADER
|
|
@@ -2256,7 +2124,9 @@ function createStorefrontClient(options) {
|
|
|
2256
2124
|
headers: forwardedHeaders
|
|
2257
2125
|
}
|
|
2258
2126
|
);
|
|
2259
|
-
|
|
2127
|
+
const proxyResponse = new Response(sfapiResponse.body, sfapiResponse);
|
|
2128
|
+
proxyResponse.headers.delete("server-timing");
|
|
2129
|
+
return proxyResponse;
|
|
2260
2130
|
},
|
|
2261
2131
|
/**
|
|
2262
2132
|
* Checks if the request is targeting the Storefront MCP endpoint.
|
|
@@ -2305,7 +2175,9 @@ function createStorefrontClient(options) {
|
|
|
2305
2175
|
body: request.body,
|
|
2306
2176
|
headers: forwardedHeaders
|
|
2307
2177
|
});
|
|
2308
|
-
|
|
2178
|
+
const proxyResponse = new Response(mcpResponse.body, mcpResponse);
|
|
2179
|
+
proxyResponse.headers.delete("server-timing");
|
|
2180
|
+
return proxyResponse;
|
|
2309
2181
|
} catch (error) {
|
|
2310
2182
|
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
2311
2183
|
const message = error instanceof Error ? error.message : "Internal proxy error";
|
|
@@ -2328,21 +2200,6 @@ function createStorefrontClient(options) {
|
|
|
2328
2200
|
response.headers.append("Set-Cookie", value);
|
|
2329
2201
|
}
|
|
2330
2202
|
}
|
|
2331
|
-
const serverTiming = extractServerTimingHeader(
|
|
2332
|
-
collectedSubrequestHeaders?.serverTiming
|
|
2333
|
-
);
|
|
2334
|
-
const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
|
|
2335
|
-
const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
|
|
2336
|
-
appendServerTimingHeader(response, {
|
|
2337
|
-
...fallbackValues,
|
|
2338
|
-
...serverTiming
|
|
2339
|
-
});
|
|
2340
|
-
if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
|
|
2341
|
-
collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
|
|
2342
|
-
appendServerTimingHeader(response, {
|
|
2343
|
-
[HYDROGEN_SERVER_TRACKING_KEY]: "1"
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
2203
|
}
|
|
2347
2204
|
}
|
|
2348
2205
|
};
|
|
@@ -4504,6 +4361,19 @@ function createHydrogenContext(options, additionalContext) {
|
|
|
4504
4361
|
});
|
|
4505
4362
|
return hybridProvider;
|
|
4506
4363
|
}
|
|
4364
|
+
|
|
4365
|
+
// src/utils/server-timing.ts
|
|
4366
|
+
function buildServerTimingHeader(values) {
|
|
4367
|
+
return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
|
|
4368
|
+
}
|
|
4369
|
+
function appendServerTimingHeader(response, values) {
|
|
4370
|
+
const header = typeof values === "string" ? values : buildServerTimingHeader(values);
|
|
4371
|
+
if (header) {
|
|
4372
|
+
response.headers.append("Server-Timing", header);
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
// src/createRequestHandler.ts
|
|
4507
4377
|
function createRequestHandler({
|
|
4508
4378
|
build,
|
|
4509
4379
|
mode,
|
|
@@ -6018,7 +5888,7 @@ function renderTitle(template, title) {
|
|
|
6018
5888
|
return template.replace("%s", title ?? "");
|
|
6019
5889
|
}
|
|
6020
5890
|
function inferMimeType(url) {
|
|
6021
|
-
const ext = url && url.split(".").pop();
|
|
5891
|
+
const ext = url && url.split(/[?#]/)[0].split(".").pop()?.toLowerCase();
|
|
6022
5892
|
switch (ext) {
|
|
6023
5893
|
case "svg":
|
|
6024
5894
|
return "image/svg+xml";
|