@shopify/hydrogen
Version:
<div align="center">
1,656 lines (1,637 loc) • 183 kB
JavaScript
'use strict';
var react = require('react');
var react$1 = require('@remix-run/react');
var jsxRuntime = require('react/jsx-runtime');
var hydrogenReact = require('@shopify/hydrogen-react');
var cookie = require('worktop/cookie');
var cspBuilder = require('content-security-policy-builder');
require('url');
require('path');
require('fs/promises');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var cspBuilder__default = /*#__PURE__*/_interopDefault(cspBuilder);
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/seo/log-seo-tags.ts
var log_seo_tags_exports = {};
__export(log_seo_tags_exports, {
default: () => Logger,
logSeoTags: () => logSeoTags
});
function Logger({ headTags }) {
logSeoTags(headTags);
return null;
}
function logSeoTags(headTags) {
console.log(" ");
console.log("%cSEO Meta Tags", `${titleStyle}`);
console.log(" ");
headTags.forEach((tag) => {
if (tag.tag === "script") {
console.log(`%c\u2022 JSON LD `, headingStyle);
if (tag.children) {
try {
console.table(JSON.parse(tag.children), ["name", "content"]);
} catch {
console.log(tag.children);
}
}
} else {
console.log(`%c\u2022 ${tag.tag} `, headingStyle);
if (tag.children) {
if (typeof tag.children === "string") {
console.log(`\u21B3 ${tag.children}`);
} else {
try {
Object.entries(JSON.parse(tag.children)).map(
([key, val]) => console.log(`\u21B3 ${val}`)
);
} catch {
console.log(tag.children);
}
}
}
if (tag.props.property === "og:image:url") {
const urlKey = tag.props.content;
fetchImage(urlKey).then((image) => {
const imageStyle = `font-size: 400px; padding: 10px; background: white url(${image}) no-repeat center; background-size: contain;`;
console.log(`%c\u2022 Share image preview`, headingStyle);
console.log("%c ", imageStyle);
console.log(`\u21B3 ${urlKey}`);
}).catch((err) => {
console.error(err);
});
}
Object.entries(tag.props).map(([key, val]) => {
console.log(`\u21B3 ${key} \u2192 ${val}`);
});
}
console.log(" ");
});
}
async function fetchImage(url) {
const result = await fetch(url);
const data = await result.blob();
const buff = await data.arrayBuffer();
const base64String = arrayBufferToBase64(buff);
return `data:image/png;base64,${base64String}`;
}
function arrayBufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let index = 0; index < len; index++) {
binary += String.fromCharCode(bytes[index]);
}
return btoa(binary);
}
var headingStyle, titleStyle;
var init_log_seo_tags = __esm({
"src/seo/log-seo-tags.ts"() {
headingStyle = "text-transform: uppercase;";
titleStyle = "text-transform: uppercase; font-weight: bold; text-transform: uppercase;font-weight: bold";
}
});
function AnalyticsView(props) {
const { type, data = {}, customData } = props;
const location = react$1.useLocation();
const {
publish: publish2,
cart,
prevCart,
shop,
customData: analyticProviderCustomData
} = useAnalytics();
const url = location.pathname + location.search;
let viewPayload2 = {
...data,
customData: {
...analyticProviderCustomData,
...customData
},
cart,
prevCart,
shop
};
react.useEffect(() => {
if (!shop?.shopId) return;
viewPayload2 = {
...viewPayload2,
url: window.location.href
};
publish2(type, viewPayload2);
}, [publish2, url, shop?.shopId]);
return null;
}
function AnalyticsPageView(props) {
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "page_viewed" });
}
function AnalyticsProductView(props) {
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "product_viewed" });
}
function AnalyticsCollectionView(props) {
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "collection_viewed" });
}
function AnalyticsCartView(props) {
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "cart_viewed" });
}
function AnalyticsSearchView(props) {
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props, type: "search_viewed" });
}
function AnalyticsCustomView(props) {
return /* @__PURE__ */ jsxRuntime.jsx(AnalyticsView, { ...props });
}
// src/analytics-manager/events.ts
var AnalyticsEvent = {
// Views
PAGE_VIEWED: "page_viewed",
PRODUCT_VIEWED: "product_viewed",
COLLECTION_VIEWED: "collection_viewed",
CART_VIEWED: "cart_viewed",
SEARCH_VIEWED: "search_viewed",
// Cart
CART_UPDATED: "cart_updated",
PRODUCT_ADD_TO_CART: "product_added_to_cart",
PRODUCT_REMOVED_FROM_CART: "product_removed_from_cart",
// Custom
CUSTOM_EVENT: `custom_`
};
var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js";
var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
function logMissingConfig(fieldName) {
console.error(
`[h2:error:useCustomerPrivacy] Unable to setup Customer Privacy API: Missing consent.${fieldName} configuration.`
);
}
function useCustomerPrivacy(props) {
const {
withPrivacyBanner = false,
onVisitorConsentCollected,
onReady,
...consentConfig
} = props;
hydrogenReact.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
attributes: {
id: "customer-privacy-api"
}
});
const { observing, setLoaded } = useApisLoaded({
withPrivacyBanner,
onLoaded: onReady
});
const config = react.useMemo(() => {
const { checkoutDomain, storefrontAccessToken } = consentConfig;
if (!checkoutDomain) logMissingConfig("checkoutDomain");
if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) {
console.error(
`[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
);
}
const config2 = {
checkoutRootDomain: checkoutDomain,
storefrontAccessToken,
storefrontRootDomain: parseStoreDomain(checkoutDomain),
country: consentConfig.country,
locale: consentConfig.locale
};
return config2;
}, [consentConfig, parseStoreDomain, logMissingConfig]);
react.useEffect(() => {
const consentCollectedHandler = (event) => {
if (onVisitorConsentCollected) {
onVisitorConsentCollected(event.detail);
}
};
document.addEventListener(
"visitorConsentCollected",
consentCollectedHandler
);
return () => {
document.removeEventListener(
"visitorConsentCollected",
consentCollectedHandler
);
};
}, [onVisitorConsentCollected]);
react.useEffect(() => {
if (!withPrivacyBanner || observing.current.privacyBanner) return;
observing.current.privacyBanner = true;
let customPrivacyBanner = window.privacyBanner || void 0;
const privacyBannerWatcher = {
configurable: true,
get() {
return customPrivacyBanner;
},
set(value) {
if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
const privacyBanner = value;
privacyBanner.loadBanner(config);
customPrivacyBanner = overridePrivacyBannerMethods({
privacyBanner,
config
});
setLoaded.privacyBanner();
emitCustomerPrivacyApiLoaded();
}
}
};
Object.defineProperty(window, "privacyBanner", privacyBannerWatcher);
}, [
withPrivacyBanner,
config,
overridePrivacyBannerMethods,
setLoaded.privacyBanner
]);
react.useEffect(() => {
if (observing.current.customerPrivacy) return;
observing.current.customerPrivacy = true;
let customCustomerPrivacy = null;
let customShopify = window.Shopify || void 0;
Object.defineProperty(window, "Shopify", {
configurable: true,
get() {
return customShopify;
},
set(value) {
if (typeof value === "object" && value !== null && Object.keys(value).length === 0) {
customShopify = value;
Object.defineProperty(window.Shopify, "customerPrivacy", {
configurable: true,
get() {
return customCustomerPrivacy;
},
set(value2) {
if (typeof value2 === "object" && value2 !== null && "setTrackingConsent" in value2) {
const customerPrivacy = value2;
customCustomerPrivacy = {
...customerPrivacy,
setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
{ customerPrivacy, config }
)
};
customShopify = {
...customShopify,
customerPrivacy: customCustomerPrivacy
};
setLoaded.customerPrivacy();
emitCustomerPrivacyApiLoaded();
}
}
});
}
}
});
}, [
config,
overrideCustomerPrivacySetTrackingConsent,
setLoaded.customerPrivacy
]);
const result = {
customerPrivacy: getCustomerPrivacy()
};
if (withPrivacyBanner) {
result.privacyBanner = getPrivacyBanner();
}
return result;
}
var hasEmitted = false;
function emitCustomerPrivacyApiLoaded() {
if (hasEmitted) return;
hasEmitted = true;
const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded");
document.dispatchEvent(event);
}
function useApisLoaded({
withPrivacyBanner,
onLoaded
}) {
const observing = react.useRef({ customerPrivacy: false, privacyBanner: false });
const [apisLoaded, setApisLoaded] = react.useState(
withPrivacyBanner ? [false, false] : [false]
);
const loaded = apisLoaded.every(Boolean);
const setLoaded = {
customerPrivacy: () => {
if (withPrivacyBanner) {
setApisLoaded((prev) => [true, prev[1]]);
} else {
setApisLoaded(() => [true]);
}
},
privacyBanner: () => {
if (!withPrivacyBanner) {
return;
}
setApisLoaded((prev) => [prev[0], true]);
}
};
react.useEffect(() => {
if (loaded && onLoaded) {
onLoaded();
}
}, [loaded, onLoaded]);
return { observing, setLoaded };
}
function parseStoreDomain(checkoutDomain) {
if (typeof window === "undefined") return;
const host = window.document.location.host;
const checkoutDomainParts = checkoutDomain.split(".").reverse();
const currentDomainParts = host.split(".").reverse();
const sameDomainParts = [];
checkoutDomainParts.forEach((part, index) => {
if (part === currentDomainParts[index]) {
sameDomainParts.push(part);
}
});
return sameDomainParts.reverse().join(".");
}
function overrideCustomerPrivacySetTrackingConsent({
customerPrivacy,
config
}) {
const original = customerPrivacy.setTrackingConsent;
const { locale, country, ...rest } = config;
function updatedSetTrackingConsent(consent, callback) {
original(
{
...rest,
headlessStorefront: true,
...consent
},
callback
);
}
return updatedSetTrackingConsent;
}
function overridePrivacyBannerMethods({
privacyBanner,
config
}) {
const originalLoadBanner = privacyBanner.loadBanner;
const originalShowPreferences = privacyBanner.showPreferences;
function loadBanner(userConfig) {
if (typeof userConfig === "object") {
originalLoadBanner({ ...config, ...userConfig });
return;
}
originalLoadBanner(config);
}
function showPreferences(userConfig) {
if (typeof userConfig === "object") {
originalShowPreferences({ ...config, ...userConfig });
return;
}
originalShowPreferences(config);
}
return { loadBanner, showPreferences };
}
function getCustomerPrivacy() {
try {
return window.Shopify && window.Shopify.customerPrivacy ? window.Shopify?.customerPrivacy : null;
} catch (e) {
return null;
}
}
function getPrivacyBanner() {
try {
return window && window?.privacyBanner ? window.privacyBanner : null;
} catch (e) {
return null;
}
}
// package.json
var version = "2025.1.3";
// src/analytics-manager/ShopifyAnalytics.tsx
function getCustomerPrivacyRequired() {
const customerPrivacy = getCustomerPrivacy();
if (!customerPrivacy) {
throw new Error(
"Shopify Customer Privacy API not available. Must be used within a useEffect. Make sure to load the Shopify Customer Privacy API with useCustomerPrivacy() or <AnalyticsProvider>."
);
}
return customerPrivacy;
}
function ShopifyAnalytics({
consent,
onReady,
domain
}) {
const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics();
const [shopifyReady, setShopifyReady] = react.useState(false);
const [privacyReady, setPrivacyReady] = react.useState(false);
const init = react.useRef(false);
const { checkoutDomain, storefrontAccessToken, language } = consent;
const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
useCustomerPrivacy({
...consent,
locale: language,
checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
onVisitorConsentCollected: () => setPrivacyReady(true),
onReady: () => setPrivacyReady(true)
});
hydrogenReact.useShopifyCookies({
hasUserConsent: privacyReady ? canTrack() : true,
// must be initialized with true
domain,
checkoutDomain
});
react.useEffect(() => {
if (init.current) return;
init.current = true;
subscribe2(AnalyticsEvent.PAGE_VIEWED, pageViewHandler);
subscribe2(AnalyticsEvent.PRODUCT_VIEWED, productViewHandler);
subscribe2(AnalyticsEvent.COLLECTION_VIEWED, collectionViewHandler);
subscribe2(AnalyticsEvent.SEARCH_VIEWED, searchViewHandler);
subscribe2(AnalyticsEvent.PRODUCT_ADD_TO_CART, productAddedToCartHandler);
setShopifyReady(true);
}, [subscribe2]);
react.useEffect(() => {
if (shopifyReady && privacyReady) {
shopifyAnalyticsReady();
onReady();
}
}, [shopifyReady, privacyReady, onReady]);
return null;
}
function logMissingConfig2(fieldName) {
console.error(
`[h2:error:ShopifyAnalytics] Unable to send Shopify analytics: Missing shop.${fieldName} configuration.`
);
}
function prepareBasePageViewPayload(payload) {
const customerPrivacy = getCustomerPrivacyRequired();
const hasUserConsent = customerPrivacy.analyticsProcessingAllowed();
if (!payload?.shop?.shopId) {
logMissingConfig2("shopId");
return;
}
if (!payload?.shop?.acceptedLanguage) {
logMissingConfig2("acceptedLanguage");
return;
}
if (!payload?.shop?.currency) {
logMissingConfig2("currency");
return;
}
if (!payload?.shop?.hydrogenSubchannelId) {
logMissingConfig2("hydrogenSubchannelId");
return;
}
const eventPayload = {
shopifySalesChannel: "hydrogen",
assetVersionId: version,
...payload.shop,
hasUserConsent,
...hydrogenReact.getClientBrowserParameters(),
ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()),
analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(),
marketingAllowed: customerPrivacy.marketingAllowed(),
saleOfDataAllowed: customerPrivacy.saleOfDataAllowed()
};
return eventPayload;
}
function prepareBaseCartPayload(payload, cart) {
if (cart === null) return;
const pageViewPayload = prepareBasePageViewPayload(payload);
if (!pageViewPayload) return;
const eventPayload = {
...pageViewPayload,
cartId: cart.id
};
return eventPayload;
}
var viewPayload = {};
function pageViewHandler(payload) {
const eventPayload = prepareBasePageViewPayload(payload);
if (!eventPayload) return;
hydrogenReact.sendShopifyAnalytics({
eventName: hydrogenReact.AnalyticsEventName.PAGE_VIEW_2,
payload: {
...eventPayload,
...viewPayload
}
});
viewPayload = {};
}
function productViewHandler(payload) {
let eventPayload = prepareBasePageViewPayload(payload);
if (eventPayload && validateProducts({
type: "product",
products: payload.products
})) {
const formattedProducts = formatProduct(payload.products);
viewPayload = {
pageType: hydrogenReact.AnalyticsPageType.product,
resourceId: formattedProducts[0].productGid
};
eventPayload = {
...eventPayload,
...viewPayload,
products: formatProduct(payload.products)
};
hydrogenReact.sendShopifyAnalytics({
eventName: hydrogenReact.AnalyticsEventName.PRODUCT_VIEW,
payload: eventPayload
});
}
}
function collectionViewHandler(payload) {
let eventPayload = prepareBasePageViewPayload(payload);
if (!eventPayload) return;
viewPayload = {
pageType: hydrogenReact.AnalyticsPageType.collection,
resourceId: payload.collection.id
};
eventPayload = {
...eventPayload,
...viewPayload,
collectionHandle: payload.collection.handle,
collectionId: payload.collection.id
};
hydrogenReact.sendShopifyAnalytics({
eventName: hydrogenReact.AnalyticsEventName.COLLECTION_VIEW,
payload: eventPayload
});
}
function searchViewHandler(payload) {
let eventPayload = prepareBasePageViewPayload(payload);
if (!eventPayload) return;
viewPayload = {
pageType: hydrogenReact.AnalyticsPageType.search
};
eventPayload = {
...eventPayload,
...viewPayload,
searchString: payload.searchTerm
};
hydrogenReact.sendShopifyAnalytics({
eventName: hydrogenReact.AnalyticsEventName.SEARCH_VIEW,
payload: eventPayload
});
}
function productAddedToCartHandler(payload) {
const { cart, currentLine } = payload;
const eventPayload = prepareBaseCartPayload(payload, cart);
if (!eventPayload || !currentLine?.id) return;
sendCartAnalytics({
matchedLine: currentLine,
eventPayload
});
}
function sendCartAnalytics({
matchedLine,
eventPayload
}) {
const product = {
id: matchedLine.merchandise.product.id,
variantId: matchedLine.merchandise.id,
title: matchedLine.merchandise.product.title,
variantTitle: matchedLine.merchandise.title,
vendor: matchedLine.merchandise.product.vendor,
price: matchedLine.merchandise.price.amount,
quantity: matchedLine.quantity,
productType: matchedLine.merchandise.product.productType,
sku: matchedLine.merchandise.sku
};
if (validateProducts({
type: "cart",
products: [product]
})) {
hydrogenReact.sendShopifyAnalytics({
eventName: hydrogenReact.AnalyticsEventName.ADD_TO_CART,
payload: {
...eventPayload,
products: formatProduct([product])
}
});
}
}
function missingErrorMessage(type, fieldName, isVariantField, viewKeyName) {
if (type === "cart") {
const name = `${isVariantField ? "merchandise" : "merchandise.product"}.${fieldName}`;
console.error(
`[h2:error:ShopifyAnalytics] Can't set up cart analytics events because the \`cart.lines[].${name}\` value is missing from your GraphQL cart query. In your project, search for where \`fragment CartLine on CartLine\` is defined and make sure \`${name}\` is part of your cart query. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L25-L56.`
);
} else {
const name = `${viewKeyName || fieldName}`;
console.error(
`[h2:error:ShopifyAnalytics] Can't set up product view analytics events because the \`${name}\` is missing from your \`<Analytics.ProductView>\`. Make sure \`${name}\` is part of your products data prop. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/routes/products.%24handle.tsx#L159-L165.`
);
}
}
function validateProducts({
type,
products
}) {
if (!products || products.length === 0) {
missingErrorMessage(type, "", false, "data.products");
return false;
}
products.forEach((product) => {
if (!product.id) {
missingErrorMessage(type, "id", false);
return false;
}
if (!product.title) {
missingErrorMessage(type, "title", false);
return false;
}
if (!product.price) {
missingErrorMessage(type, "price.amount", true, "price");
return false;
}
if (!product.vendor) {
missingErrorMessage(type, "vendor", false);
return false;
}
if (!product.variantId) {
missingErrorMessage(type, "id", true, "variantId");
return false;
}
if (!product.variantTitle) {
missingErrorMessage(type, "title", true, "variantTitle");
return false;
}
});
return true;
}
function formatProduct(products) {
return products.map((product) => {
const formattedProduct = {
productGid: product.id,
variantGid: product.variantId,
name: product.title,
variantName: product.variantTitle,
brand: product.vendor,
price: product.price,
quantity: product.quantity || 1,
category: product.productType
};
if (product.sku) formattedProduct.sku = product.sku;
if (product.productType) formattedProduct.category = product.productType;
return formattedProduct;
});
}
function logMissingField(fieldName) {
console.error(
`[h2:error:CartAnalytics] Can't set up cart analytics events because the \`cart.${fieldName}\` value is missing from your GraphQL cart query. In your project, search for where \`fragment CartApiQuery on Cart\` is defined and make sure \`${fieldName}\` is part of your cart query. Check the Hydrogen Skeleton template for reference: https://github.com/Shopify/hydrogen/blob/main/templates/skeleton/app/lib/fragments.ts#L59.`
);
}
function CartAnalytics({
cart: currentCart,
setCarts
}) {
const { publish: publish2, shop, customData, canTrack, cart, prevCart } = useAnalytics();
const lastEventId = react.useRef(null);
react.useEffect(() => {
if (!currentCart) return;
Promise.resolve(currentCart).then((updatedCart) => {
if (updatedCart && updatedCart.lines) {
if (!updatedCart.id) {
logMissingField("id");
return;
}
if (!updatedCart.updatedAt) {
logMissingField("updatedAt");
return;
}
}
setCarts(({ cart: cart2, prevCart: prevCart2 }) => {
return updatedCart?.updatedAt !== cart2?.updatedAt ? { cart: updatedCart, prevCart: cart2 } : { cart: cart2, prevCart: prevCart2 };
});
});
return () => {
};
}, [setCarts, currentCart]);
react.useEffect(() => {
if (!cart || !cart?.updatedAt) return;
if (cart?.updatedAt === prevCart?.updatedAt) return;
let cartLastUpdatedAt;
try {
cartLastUpdatedAt = JSON.parse(
localStorage.getItem("cartLastUpdatedAt") || ""
);
} catch (e) {
cartLastUpdatedAt = null;
}
if (cart.id === cartLastUpdatedAt?.id && cart.updatedAt === cartLastUpdatedAt?.updatedAt)
return;
const payload = {
eventTimestamp: Date.now(),
cart,
prevCart,
shop,
customData
};
if (cart.updatedAt === lastEventId.current) return;
lastEventId.current = cart.updatedAt;
publish2("cart_updated", payload);
localStorage.setItem(
"cartLastUpdatedAt",
JSON.stringify({
id: cart.id,
updatedAt: cart.updatedAt
})
);
const previousCartLines = prevCart?.lines ? hydrogenReact.flattenConnection(prevCart?.lines) : [];
const currentCartLines = cart.lines ? hydrogenReact.flattenConnection(cart.lines) : [];
previousCartLines?.forEach((prevLine) => {
const matchedLineId = currentCartLines.filter(
(line) => prevLine.id === line.id
);
if (matchedLineId?.length === 1) {
const matchedLine = matchedLineId[0];
if (prevLine.quantity < matchedLine.quantity) {
publish2("product_added_to_cart", {
...payload,
prevLine,
currentLine: matchedLine
});
} else if (prevLine.quantity > matchedLine.quantity) {
publish2("product_removed_from_cart", {
...payload,
prevLine,
currentLine: matchedLine
});
}
} else {
publish2("product_removed_from_cart", {
...payload,
prevLine
});
}
});
currentCartLines?.forEach((line) => {
const matchedLineId = previousCartLines.filter(
(previousLine) => line.id === previousLine.id
);
if (!matchedLineId || matchedLineId.length === 0) {
publish2("product_added_to_cart", {
...payload,
currentLine: line
});
}
});
}, [cart, prevCart, publish2, shop, customData, canTrack]);
return null;
}
var PERF_KIT_URL = "https://cdn.shopify.com/shopifycloud/perf-kit/shopify-perf-kit-1.0.1.min.js";
function PerfKit({ shop }) {
const loadedEvent = react.useRef(false);
const { subscribe: subscribe2, register: register2 } = useAnalytics();
const { ready } = register2("Internal_Shopify_Perf_Kit");
const scriptStatus = hydrogenReact.useLoadScript(PERF_KIT_URL, {
attributes: {
id: "perfkit",
"data-application": "hydrogen",
"data-shop-id": hydrogenReact.parseGid(shop.shopId).id.toString(),
"data-storefront-id": shop.hydrogenSubchannelId,
"data-monorail-region": "global",
"data-spa-mode": "true",
"data-resource-timing-sampling-rate": "100"
}
});
react.useEffect(() => {
if (scriptStatus !== "done" || loadedEvent.current) return;
loadedEvent.current = true;
subscribe2(AnalyticsEvent.PAGE_VIEWED, () => {
window.PerfKit?.navigate();
});
subscribe2(AnalyticsEvent.PRODUCT_VIEWED, () => {
window.PerfKit?.setPageType("product");
});
subscribe2(AnalyticsEvent.COLLECTION_VIEWED, () => {
window.PerfKit?.setPageType("collection");
});
subscribe2(AnalyticsEvent.SEARCH_VIEWED, () => {
window.PerfKit?.setPageType("search");
});
subscribe2(AnalyticsEvent.CART_VIEWED, () => {
window.PerfKit?.setPageType("cart");
});
ready();
}, [subscribe2, ready, scriptStatus]);
return null;
}
// src/utils/warning.ts
var warnings = /* @__PURE__ */ new Set();
var warnOnce = (string) => {
if (!warnings.has(string)) {
console.warn(string);
warnings.add(string);
}
};
var errors = /* @__PURE__ */ new Set();
var errorOnce = (string) => {
if (!errors.has(string)) {
console.error(new Error(string));
errors.add(string);
}
};
var defaultAnalyticsContext = {
canTrack: () => false,
cart: null,
customData: {},
prevCart: null,
publish: () => {
},
shop: null,
subscribe: () => {
},
register: () => ({ ready: () => {
} }),
customerPrivacy: null,
privacyBanner: null
};
var AnalyticsContext = react.createContext(
defaultAnalyticsContext
);
var subscribers = /* @__PURE__ */ new Map();
var registers = {};
function areRegistersReady() {
return Object.values(registers).every(Boolean);
}
function subscribe(event, callback) {
if (!subscribers.has(event)) {
subscribers.set(event, /* @__PURE__ */ new Map());
}
subscribers.get(event)?.set(callback.toString(), callback);
}
var waitForReadyQueue = /* @__PURE__ */ new Map();
function publish(event, payload) {
if (!areRegistersReady()) {
waitForReadyQueue.set(event, payload);
return;
}
publishEvent(event, payload);
}
function publishEvent(event, payload) {
(subscribers.get(event) ?? /* @__PURE__ */ new Map()).forEach((callback, subscriber) => {
try {
callback(payload);
} catch (error) {
if (typeof error === "object" && error instanceof Error) {
console.error(
"Analytics publish error",
error.message,
subscriber,
error.stack
);
} else {
console.error("Analytics publish error", error, subscriber);
}
}
});
}
function register(key) {
if (!registers.hasOwnProperty(key)) {
registers[key] = false;
}
return {
ready: () => {
registers[key] = true;
if (areRegistersReady() && waitForReadyQueue.size > 0) {
waitForReadyQueue.forEach((queuePayload, queueEvent) => {
publishEvent(queueEvent, queuePayload);
});
waitForReadyQueue.clear();
}
}
};
}
function shopifyCanTrack() {
try {
return window.Shopify.customerPrivacy.analyticsProcessingAllowed();
} catch (e) {
}
return false;
}
function messageOnError(field, envVar) {
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.`;
}
function AnalyticsProvider({
canTrack: customCanTrack,
cart: currentCart,
children,
consent,
customData = {},
shop: shopProp = null,
cookieDomain
}) {
const listenerSet = react.useRef(false);
const { shop } = useShopAnalytics(shopProp);
const [analyticsLoaded, setAnalyticsLoaded] = react.useState(
customCanTrack ? true : false
);
const [carts, setCarts] = react.useState({ cart: null, prevCart: null });
const [canTrack, setCanTrack] = react.useState(
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
);
if (!!shop) {
if (/\/68817551382$/.test(shop.shopId)) {
warnOnce(
"[h2:error:Analytics.Provider] - Mock shop is used. Analytics will not work properly."
);
} else {
if (!consent.checkoutDomain) {
const errorMsg = messageOnError(
"consent.checkoutDomain",
"PUBLIC_CHECKOUT_DOMAIN"
);
errorOnce(errorMsg);
}
if (!consent.storefrontAccessToken) {
const errorMsg = messageOnError(
"consent.storefrontAccessToken",
"PUBLIC_STOREFRONT_API_TOKEN"
);
errorOnce(errorMsg);
}
if (!consent?.country) {
consent.country = "US";
}
if (!consent?.language) {
consent.language = "EN";
}
if (consent.withPrivacyBanner === void 0) {
consent.withPrivacyBanner = false;
}
}
}
const value = react.useMemo(() => {
return {
canTrack,
...carts,
customData,
publish: canTrack() ? publish : () => {
},
shop,
subscribe,
register,
customerPrivacy: getCustomerPrivacy(),
privacyBanner: getPrivacyBanner()
};
}, [
analyticsLoaded,
canTrack,
carts,
carts.cart?.updatedAt,
carts.prevCart,
publish,
subscribe,
customData,
shop,
register,
JSON.stringify(registers),
getCustomerPrivacy,
getPrivacyBanner
]);
return /* @__PURE__ */ jsxRuntime.jsxs(AnalyticsContext.Provider, { value, children: [
children,
!!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
!!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }),
!!shop && consent.checkoutDomain && /* @__PURE__ */ jsxRuntime.jsx(
ShopifyAnalytics,
{
consent,
onReady: () => {
listenerSet.current = true;
setAnalyticsLoaded(true);
setCanTrack(
customCanTrack ? () => customCanTrack : () => shopifyCanTrack
);
},
domain: cookieDomain
}
),
!!shop && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
] });
}
function useAnalytics() {
const analyticsContext = react.useContext(AnalyticsContext);
if (!analyticsContext) {
throw new Error(
`[h2:error:useAnalytics] 'useAnalytics()' must be a descendent of <AnalyticsProvider/>`
);
}
return analyticsContext;
}
function useShopAnalytics(shopProp) {
const [shop, setShop] = react.useState(null);
react.useEffect(() => {
Promise.resolve(shopProp).then(setShop);
return () => {
};
}, [setShop, shopProp]);
return { shop };
}
async function getShopAnalytics({
storefront,
publicStorefrontId = "0"
}) {
return storefront.query(SHOP_QUERY, {
cache: storefront.CacheLong()
}).then(({ shop, localization }) => {
return {
shopId: shop.id,
acceptedLanguage: localization.language.isoCode,
currency: localization.country.currency.isoCode,
hydrogenSubchannelId: publicStorefrontId
};
});
}
var SHOP_QUERY = `#graphql
query ShopData(
$country: CountryCode
$language: LanguageCode
) @inContext(country: $country, language: $language) {
shop {
id
}
localization {
country {
currency {
isoCode
}
}
language {
isoCode
}
}
}
`;
var Analytics = {
CartView: AnalyticsCartView,
CollectionView: AnalyticsCollectionView,
CustomView: AnalyticsCustomView,
ProductView: AnalyticsProductView,
Provider: AnalyticsProvider,
SearchView: AnalyticsSearchView
};
// src/utils/request.ts
function getHeader(request, key) {
return getHeaderValue(request.headers, key);
}
function getHeaderValue(headers, key) {
const value = headers?.get?.(key) ?? headers?.[key];
return typeof value === "string" ? value : null;
}
function getDebugHeaders(request) {
return {
requestId: request ? getHeader(request, "request-id") : void 0,
purpose: request ? getHeader(request, "purpose") : void 0
};
}
// src/utils/callsites.ts
function withSyncStack(promise, options = {}) {
const syncError = new Error();
const getSyncStack = (message, name = "Error") => {
const syncStack = (syncError.stack ?? "").split("\n").slice(3 + (options.stackOffset ?? 0)).join("\n").replace(/ at loader(\d+) \(/, (all, m1) => all.replace(m1, ""));
return `${name}: ${message}
` + syncStack;
};
return promise.then((result) => {
if (result?.errors && Array.isArray(result.errors)) {
const logErrors = typeof options.logErrors === "function" ? options.logErrors : () => options.logErrors ?? false;
result.errors.forEach((error) => {
if (error) {
error.stack = getSyncStack(error.message, error.name);
if (logErrors(error)) console.error(error);
}
});
}
return result;
}).catch((error) => {
if (error) error.stack = getSyncStack(error.message, error.name);
throw error;
});
}
var getCallerStackLine = (stackOffset = 0) => {
let stackInfo = void 0;
const original = Error.prepareStackTrace;
Error.prepareStackTrace = (_, callsites) => {
const cs = callsites[2 + stackOffset];
stackInfo = cs && {
file: cs.getFileName() ?? void 0,
func: cs.getFunctionName() ?? void 0,
line: cs.getLineNumber() ?? void 0,
column: cs.getColumnNumber() ?? void 0
};
return "";
};
const err = { stack: "" };
Error.captureStackTrace(err);
err.stack;
Error.prepareStackTrace = original;
return stackInfo;
} ;
// src/cache/strategies.ts
var PUBLIC = "public";
var PRIVATE = "private";
var NO_STORE = "no-store";
var optionMapping = {
maxAge: "max-age",
staleWhileRevalidate: "stale-while-revalidate",
sMaxAge: "s-maxage",
staleIfError: "stale-if-error"
};
function generateCacheControlHeader(cacheOptions) {
const cacheControl = [];
Object.keys(cacheOptions).forEach((key) => {
if (key === "mode") {
cacheControl.push(cacheOptions[key]);
} else if (optionMapping[key]) {
cacheControl.push(
`${optionMapping[key]}=${cacheOptions[key]}`
);
}
});
return cacheControl.join(", ");
}
function CacheNone() {
return {
mode: NO_STORE
};
}
function guardExpirableModeType(overrideOptions) {
if (overrideOptions?.mode && overrideOptions?.mode !== PUBLIC && overrideOptions?.mode !== PRIVATE) {
throw Error("'mode' must be either 'public' or 'private'");
}
}
function CacheShort(overrideOptions) {
guardExpirableModeType(overrideOptions);
return {
mode: PUBLIC,
maxAge: 1,
staleWhileRevalidate: 9,
...overrideOptions
};
}
function CacheLong(overrideOptions) {
guardExpirableModeType(overrideOptions);
return {
mode: PUBLIC,
maxAge: 3600,
// 1 hour
staleWhileRevalidate: 82800,
// 23 Hours
...overrideOptions
};
}
function CacheDefault(overrideOptions) {
guardExpirableModeType(overrideOptions);
return {
mode: PUBLIC,
maxAge: 1,
staleWhileRevalidate: 86399,
// 1 second less than 24 hours
...overrideOptions
};
}
function CacheCustom(overrideOptions) {
return overrideOptions;
}
// src/utils/parse-json.ts
function parseJSON(json) {
if (String(json).includes("__proto__")) return JSON.parse(json, noproto);
return JSON.parse(json);
}
function noproto(k, v) {
if (k !== "__proto__") return v;
}
function getCacheControlSetting(userCacheOptions, options) {
if (userCacheOptions && options) {
return {
...userCacheOptions,
...options
};
} else {
return userCacheOptions || CacheDefault();
}
}
function generateDefaultCacheControlHeader(userCacheOptions) {
return generateCacheControlHeader(getCacheControlSetting(userCacheOptions));
}
async function getItem(cache, request) {
if (!cache) return;
const response = await cache.match(request);
if (!response) {
return;
}
return response;
}
async function setItem(cache, request, response, userCacheOptions) {
if (!cache) return;
const cacheControl = getCacheControlSetting(userCacheOptions);
const paddedCacheControlString = generateDefaultCacheControlHeader(
getCacheControlSetting(cacheControl, {
maxAge: (cacheControl.maxAge || 0) + (cacheControl.staleWhileRevalidate || 0)
})
);
const cacheControlString = generateDefaultCacheControlHeader(
getCacheControlSetting(cacheControl)
);
response.headers.set("cache-control", paddedCacheControlString);
response.headers.set("real-cache-control", cacheControlString);
response.headers.set("cache-put-date", String(Date.now()));
await cache.put(request, response);
}
async function deleteItem(cache, request) {
if (!cache) return;
await cache.delete(request);
}
function calculateAge(response, responseDate) {
const cacheControl = response.headers.get("real-cache-control");
let responseMaxAge = 0;
if (cacheControl) {
const maxAgeMatch = cacheControl.match(/max-age=(\d*)/);
if (maxAgeMatch && maxAgeMatch.length > 1) {
responseMaxAge = parseFloat(maxAgeMatch[1]);
}
}
const ageInMs = Date.now() - Number(responseDate);
return [ageInMs / 1e3, responseMaxAge];
}
function isStale(request, response) {
const responseDate = response.headers.get("cache-put-date");
if (!responseDate) {
return false;
}
const [age, responseMaxAge] = calculateAge(response, responseDate);
const result = age > responseMaxAge;
return result;
}
var CacheAPI = {
get: getItem,
set: setItem,
delete: deleteItem,
generateDefaultCacheControlHeader,
isStale
};
// src/cache/sub-request.ts
function getKeyUrl(key) {
return `https://shopify.dev/?${key}`;
}
function getCacheOption(userCacheOptions) {
return userCacheOptions || CacheDefault();
}
async function getItemFromCache(cache, key) {
if (!cache) return;
const url = getKeyUrl(key);
const request = new Request(url);
const response = await CacheAPI.get(cache, request);
if (!response) {
return;
}
const text = await response.text();
try {
return [parseJSON(text), response];
} catch {
return [text, response];
}
}
async function setItemInCache(cache, key, value, userCacheOptions) {
if (!cache) return;
const url = getKeyUrl(key);
const request = new Request(url);
const response = new Response(JSON.stringify(value));
await CacheAPI.set(
cache,
request,
response,
getCacheOption(userCacheOptions)
);
}
function isStale2(key, response) {
return CacheAPI.isStale(new Request(getKeyUrl(key)), response);
}
// src/utils/hash.ts
function hashKey(queryKey) {
const rawKeys = Array.isArray(queryKey) ? queryKey : [queryKey];
let hash = "";
for (const key of rawKeys) {
if (key != null) {
if (typeof key === "object") {
hash += JSON.stringify(key);
} else {
hash += key.toString();
}
}
}
return encodeURIComponent(hash);
}
// src/cache/run-with-cache.ts
var swrLock = /* @__PURE__ */ new Set();
async function runWithCache(cacheKey, actionFn, {
strategy = CacheShort(),
cacheInstance,
shouldCacheResult = () => true,
waitUntil,
debugInfo
}) {
const startTime = Date.now();
const key = hashKey([
// '__HYDROGEN_CACHE_ID__', // TODO purgeQueryCacheOnBuild
...typeof cacheKey === "string" ? [cacheKey] : cacheKey
]);
let cachedDebugInfo;
let userDebugInfo;
const addDebugData = (info) => {
userDebugInfo = {
displayName: info.displayName,
url: info.response?.url,
responseInit: {
status: info.response?.status || 0,
statusText: info.response?.statusText || "",
headers: Array.from(info.response?.headers.entries() || [])
}
};
};
const mergeDebugInfo = () => ({
...cachedDebugInfo,
...debugInfo,
url: userDebugInfo?.url || debugInfo?.url || cachedDebugInfo?.url || getKeyUrl(key),
displayName: debugInfo?.displayName || userDebugInfo?.displayName || cachedDebugInfo?.displayName
});
const logSubRequestEvent2 = ({
result: result2,
cacheStatus,
overrideStartTime
}) => {
globalThis.__H2O_LOG_EVENT?.({
...mergeDebugInfo(),
eventType: "subrequest",
startTime: overrideStartTime || startTime,
endTime: Date.now(),
cacheStatus,
responsePayload: result2 && result2[0] || result2,
responseInit: result2 && result2[1] || userDebugInfo?.responseInit,
cache: {
status: cacheStatus,
strategy: generateCacheControlHeader(strategy || {}),
key
},
waitUntil
});
} ;
if (!cacheInstance || !strategy || strategy.mode === NO_STORE) {
const result2 = await actionFn({ addDebugData });
logSubRequestEvent2?.({ result: result2 });
return result2;
}
const storeInCache = (value) => setItemInCache(
cacheInstance,
key,
{
value,
debugInfo: mergeDebugInfo()
},
strategy
);
const cachedItem = await getItemFromCache(cacheInstance, key);
if (cachedItem && typeof cachedItem[0] !== "string") {
const [{ value: cachedResult, debugInfo: debugInfo2 }, cacheInfo] = cachedItem;
cachedDebugInfo = debugInfo2;
const cacheStatus = isStale2(key, cacheInfo) ? "STALE" : "HIT";
if (!swrLock.has(key) && cacheStatus === "STALE") {
swrLock.add(key);
const revalidatingPromise = Promise.resolve().then(async () => {
const revalidateStartTime = Date.now();
try {
const result2 = await actionFn({ addDebugData });
if (shouldCacheResult(result2)) {
await storeInCache(result2);
logSubRequestEvent2?.({
result: result2,
cacheStatus: "PUT",
overrideStartTime: revalidateStartTime
});
}
} catch (error) {
if (error.message) {
error.message = "SWR in sub-request failed: " + error.message;
}
console.error(error);
} finally {
swrLock.delete(key);
}
});
waitUntil?.(revalidatingPromise);
}
logSubRequestEvent2?.({
result: cachedResult,
cacheStatus
});
return cachedResult;
}
const result = await actionFn({ addDebugData });
logSubRequestEvent2?.({
result,
cacheStatus: "MISS"
});
if (shouldCacheResult(result)) {
const cacheStoringPromise = Promise.resolve().then(async () => {
const putStartTime = Date.now();
await storeInCache(result);
logSubRequestEvent2?.({
result,
cacheStatus: "PUT",
overrideStartTime: putStartTime
});
});
waitUntil?.(cacheStoringPromise);
}
return result;
}
// src/cache/server-fetch.ts
function toSerializableResponse(body, response) {
return [
body,
{
status: response.status,
statusText: response.statusText,
headers: Array.from(response.headers.entries())
}
];
}
function fromSerializableResponse([body, init]) {
return [body, new Response(body, init)];
}
async function fetchWithServerCache(url, requestInit, {
cacheInstance,
cache: cacheOptions,
cacheKey = [url, requestInit],
shouldCacheResponse,
waitUntil,
debugInfo
}) {
if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) {
cacheOptions = CacheShort();
}
return runWithCache(
cacheKey,
async () => {
const response = await fetch(url, requestInit);
if (!response.ok) {
return response;
}
let data = await response.text().catch(() => "");
try {
if (data) data = parseJSON(data);
} catch {
}
return toSerializableResponse(data, response);
},
{
cacheInstance,
waitUntil,
strategy: cacheOptions ?? null,
debugInfo,
shouldCacheResult: (payload) => {
return "ok" in payload ? false : shouldCacheResponse(...fromSerializableResponse(payload));
}
}
).then((payload) => {
return "ok" in payload ? [null, payload] : fromSerializableResponse(payload);
});
}
// src/cache/create-with-cache.ts
function createWithCache(cacheOptions) {
const { cache, waitUntil, request } = cacheOptions;
return {
run: ({ cacheKey, cacheStrategy, shouldCacheResult }, fn) => {
return runWithCache(cacheKey, fn, {
shouldCacheResult,
strategy: cacheStrategy,
cacheInstance: cache,
waitUntil,
debugInfo: {
...getDebugHeaders(request),
stackInfo: getCallerStackLine?.()
}
});
},
fetch: (url, requestInit, options) => {
return fetchWithServerCache(url, requestInit ?? {}, {
waitUntil,
cacheKey: [url, requestInit],
cacheInstance: cache,
debugInfo: {
url,
...getDebugHeaders(request),
stackInfo: getCallerStackLine?.(),
displayName: options?.displayName
},
cache: options.cacheStrategy,
...options
}).then(([data, response]) => ({ data, response }));
}
};
}
// src/cache/in-memory.ts
var InMemoryCache = class {
#store;
constructor() {
this.#store = /* @__PURE__ */ new Map();
}
add(request) {
throw new Error("Method not implemented. Use `put` instead.");
}
addAll(requests) {
throw new Error("Method not implemented. Use `put` instead.");
}
matchAll(request, options) {
throw new Error("Method not implemented. Use `match` instead.");
}
async put(request, response) {
if (request.method !== "GET") {
throw new TypeError("Cannot cache response to non-GET request.");
}
if (response.status === 206) {
throw new TypeError(
"Cannot cache response to a range request (206 Partial Content)."
);
}
if (response.headers.get("vary")?.includes("*")) {
throw new TypeError("Cannot cache response with 'Vary: *' header.");
}
this.#store.set(request.url, {
body: new Uint8Array(await response.arrayBuffer()),
status: response.status,
headers: [...response.headers],
timestamp: Date.now()
});
}
async match(request) {
if (request.method !== "GET") return;
const match = this.#store.get(request.url);
if (!match) {
return;
}
const { body, timestamp, ...metadata } = match;
const headers = new Headers(metadata.headers);
const cacheControl = headers.get("cache-control") || headers.get("real-cache-control") || "";
const maxAge = parseInt(
cacheControl.match(/max-age=(\d+)/)?.[1] || "0",
10
);
const swr = parseInt(
cacheControl.match(/stale-while-revalidate=(\d+)/)?.[1] || "0",
10
);
const age = (Date.now() - timestamp) / 1e3;
const isMiss = age > maxAge + swr;
if (isMiss) {
this.#store.delete(request.url);
return;
}
const isStale3 = age > maxAge;
headers.set("cache", isStale3 ? "STALE" : "HIT");
headers.set("date", new Date(timestamp).toUTCString());
return new Response(body, {
status: metadata.status ?? 200,
headers
});
}
async delete(request) {
if (this.#store.has(request.url)) {
this.#store.delete(request.url);
return true;
}
return false;
}
keys(request) {
const cacheKeys = [];
for (const url of this.#store.keys()) {
if (!request || request.url === url) {
cacheKeys.push(new Request(url));
}
}
return Promise.resolve(cacheKeys);
}
};
var INPUT_NAME = "cartFormInput";
function CartForm({
children,
action,
inputs,
route,
fetcherKey
}) {
const fetcher = react$1.useFetcher({ key: fetcherKey });
return /* @__PURE__ */ jsxRuntime.jsxs(fetcher.Form, { action: route || "", method: "post", children: [
(action || inputs) && /* @__PURE__ */ jsxRuntime.jsx(
"input",
{
type: "