@shopify/hydrogen 2026.10.0-preview.2 → 2026.10.0-preview.4

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.
Files changed (404) hide show
  1. package/README.md +34 -3
  2. package/dist/cli/index.mjs +578 -85
  3. package/dist/client/client.mjs +2 -2
  4. package/dist/client/errors.d.mts +19 -19
  5. package/dist/client/errors.mjs +19 -16
  6. package/dist/client/errors.mjs.map +1 -1
  7. package/dist/client/types.d.mts +91 -10
  8. package/dist/core/analytics/cart-tracker.d.mts +14 -0
  9. package/dist/core/analytics/cart-tracker.mjs +15 -1
  10. package/dist/core/analytics/cart-tracker.mjs.map +1 -1
  11. package/dist/core/analytics/custom-consent.mjs +12 -0
  12. package/dist/core/analytics/custom-consent.mjs.map +1 -0
  13. package/dist/core/analytics/events.d.mts +7 -0
  14. package/dist/core/analytics/events.mjs +7 -0
  15. package/dist/core/analytics/events.mjs.map +1 -1
  16. package/dist/core/analytics/index.d.mts +1 -1
  17. package/dist/core/analytics/types.d.mts +152 -16
  18. package/dist/core/cart/actions.d.mts +47 -3
  19. package/dist/core/cart/actions.mjs +29 -0
  20. package/dist/core/cart/actions.mjs.map +1 -1
  21. package/dist/core/cart/attach-quantity-input.d.mts +26 -0
  22. package/dist/core/cart/attach-quantity-input.mjs +26 -0
  23. package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
  24. package/dist/core/cart/cart.d.mts +106 -6
  25. package/dist/core/cart/cart.mjs +57 -6
  26. package/dist/core/cart/cart.mjs.map +1 -1
  27. package/dist/core/cart/cookie.d.mts +15 -0
  28. package/dist/core/cart/cookie.mjs +15 -0
  29. package/dist/core/cart/cookie.mjs.map +1 -1
  30. package/dist/core/cart/form.d.mts +61 -0
  31. package/dist/core/cart/form.mjs +18 -0
  32. package/dist/core/cart/form.mjs.map +1 -1
  33. package/dist/core/cart/get-cart.d.mts +43 -2
  34. package/dist/core/cart/get-cart.mjs +33 -0
  35. package/dist/core/cart/get-cart.mjs.map +1 -1
  36. package/dist/core/cart/index.d.mts +1 -1
  37. package/dist/core/cart/quantity.d.mts +17 -0
  38. package/dist/core/cart/quantity.mjs +17 -0
  39. package/dist/core/cart/quantity.mjs.map +1 -1
  40. package/dist/core/cart/queries.d.mts +402 -369
  41. package/dist/core/cart/queries.mjs +2 -1
  42. package/dist/core/cart/queries.mjs.map +1 -1
  43. package/dist/core/cart/server-handlers.d.mts +69 -0
  44. package/dist/core/cart/server-handlers.mjs +2 -10
  45. package/dist/core/cart/server-handlers.mjs.map +1 -1
  46. package/dist/core/cart/state.d.mts +133 -0
  47. package/dist/core/cart/state.mjs +8 -0
  48. package/dist/core/cart/state.mjs.map +1 -1
  49. package/dist/core/collection/collection.d.mts +18 -2
  50. package/dist/core/collection/collection.mjs +11 -0
  51. package/dist/core/collection/collection.mjs.map +1 -1
  52. package/dist/core/collection/index.d.mts +1 -1
  53. package/dist/core/collection/reconciler.d.mts +13 -4
  54. package/dist/core/collection/reconciler.mjs.map +1 -1
  55. package/dist/core/collection/state.d.mts +7 -2
  56. package/dist/core/collection/state.mjs.map +1 -1
  57. package/dist/core/collection/url.d.mts +3 -0
  58. package/dist/core/collection/url.mjs.map +1 -1
  59. package/dist/core/constants.d.mts +1 -1
  60. package/dist/core/constants.mjs +2 -2
  61. package/dist/core/constants.mjs.map +1 -1
  62. package/dist/core/headers.mjs +2 -2
  63. package/dist/core/headers.mjs.map +1 -1
  64. package/dist/core/index.d.mts +5 -5
  65. package/dist/core/logging/logging.mjs.map +1 -1
  66. package/dist/core/money/format.mjs +6 -3
  67. package/dist/core/money/format.mjs.map +1 -1
  68. package/dist/core/observable.mjs +0 -3
  69. package/dist/core/observable.mjs.map +1 -1
  70. package/dist/core/predictive-search/form.d.mts +1 -0
  71. package/dist/core/predictive-search/form.mjs +1 -0
  72. package/dist/core/predictive-search/form.mjs.map +1 -1
  73. package/dist/core/predictive-search/index.d.mts +1 -1
  74. package/dist/core/predictive-search/queries.d.mts +40 -4
  75. package/dist/core/predictive-search/queries.mjs +7 -1
  76. package/dist/core/predictive-search/queries.mjs.map +1 -1
  77. package/dist/core/predictive-search/search.d.mts +43 -10
  78. package/dist/core/predictive-search/search.mjs +34 -0
  79. package/dist/core/predictive-search/search.mjs.map +1 -1
  80. package/dist/core/predictive-search/server-handlers.d.mts +31 -5
  81. package/dist/core/predictive-search/server-handlers.mjs +5 -3
  82. package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
  83. package/dist/core/predictive-search/store.d.mts +3 -1
  84. package/dist/core/predictive-search/store.mjs.map +1 -1
  85. package/dist/core/predictive-search/url.d.mts +50 -8
  86. package/dist/core/predictive-search/url.mjs +9 -0
  87. package/dist/core/predictive-search/url.mjs.map +1 -1
  88. package/dist/core/product/form.d.mts +21 -2
  89. package/dist/core/product/form.mjs +13 -0
  90. package/dist/core/product/form.mjs.map +1 -1
  91. package/dist/core/product/index.d.mts +1 -1
  92. package/dist/core/product/options.d.mts +2 -2
  93. package/dist/core/product/options.mjs +2 -2
  94. package/dist/core/product/options.mjs.map +1 -1
  95. package/dist/core/product/product-form.d.mts +33 -1
  96. package/dist/core/product/product-form.mjs +30 -0
  97. package/dist/core/product/product-form.mjs.map +1 -1
  98. package/dist/core/product/state.d.mts +41 -0
  99. package/dist/core/product/url.d.mts +6 -0
  100. package/dist/core/product/url.mjs.map +1 -1
  101. package/dist/core/request-context.d.mts +8 -5
  102. package/dist/core/request-context.mjs +17 -39
  103. package/dist/core/request-context.mjs.map +1 -1
  104. package/dist/core/request-routing/handle-shopify-routes.mjs +4 -1
  105. package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  106. package/dist/core/request-routing/interceptors/ajax-api.mjs +1 -1
  107. package/dist/core/request-routing/interceptors/api-proxy.mjs +1 -1
  108. package/dist/core/request-routing/interceptors/checkout.mjs +22 -6
  109. package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
  110. package/dist/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
  111. package/dist/core/request-routing/interceptors/proxy.mjs +36 -10
  112. package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
  113. package/dist/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
  114. package/dist/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
  115. package/dist/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
  116. package/dist/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
  117. package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
  118. package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
  119. package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
  120. package/dist/core/shopify-scripts/analytics.mjs +2 -2
  121. package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
  122. package/dist/core/shopify-scripts/constants.mjs +4 -2
  123. package/dist/core/shopify-scripts/constants.mjs.map +1 -1
  124. package/dist/core/shopify-scripts/global.mjs +9 -4
  125. package/dist/core/shopify-scripts/global.mjs.map +1 -1
  126. package/dist/core/shopify-scripts/index.d.mts +1 -0
  127. package/dist/core/shopify-scripts/index.mjs +14 -12
  128. package/dist/core/shopify-scripts/index.mjs.map +1 -1
  129. package/dist/core/shopify-scripts/initialize.d.mts +1 -0
  130. package/dist/core/shopify-scripts/initialize.mjs +10 -3
  131. package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
  132. package/dist/core/shopify-scripts/types.d.mts +6 -0
  133. package/dist/core/url.mjs +21 -2
  134. package/dist/core/url.mjs.map +1 -1
  135. package/dist/customer-account/client.d.mts +128 -4
  136. package/dist/customer-account/client.mjs +27 -1
  137. package/dist/customer-account/client.mjs.map +1 -1
  138. package/dist/customer-account/errors.d.mts +46 -0
  139. package/dist/customer-account/errors.mjs +46 -0
  140. package/dist/customer-account/errors.mjs.map +1 -1
  141. package/dist/customer-account/graphql.d.mts +68 -0
  142. package/dist/customer-account/graphql.mjs +47 -0
  143. package/dist/customer-account/graphql.mjs.map +1 -1
  144. package/dist/customer-account/session.d.mts +114 -23
  145. package/dist/customer-account/session.mjs +27 -12
  146. package/dist/customer-account/session.mjs.map +1 -1
  147. package/dist/customer-account/type-resolver.d.mts +23 -0
  148. package/dist/customer-account-api-types.d.ts +570 -374
  149. package/dist/customer-account.schema.json +2981 -1527
  150. package/dist/development/client/client.mjs +2 -2
  151. package/dist/development/client/errors.d.mts +19 -19
  152. package/dist/development/client/errors.mjs +20 -20
  153. package/dist/development/client/errors.mjs.map +1 -1
  154. package/dist/development/client/types.d.mts +91 -10
  155. package/dist/development/core/analytics/cart-tracker.d.mts +14 -0
  156. package/dist/development/core/analytics/cart-tracker.mjs +15 -1
  157. package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
  158. package/dist/development/core/analytics/custom-consent.mjs +12 -0
  159. package/dist/development/core/analytics/custom-consent.mjs.map +1 -0
  160. package/dist/development/core/analytics/events.d.mts +7 -0
  161. package/dist/development/core/analytics/events.mjs +7 -0
  162. package/dist/development/core/analytics/events.mjs.map +1 -1
  163. package/dist/development/core/analytics/index.d.mts +1 -1
  164. package/dist/development/core/analytics/types.d.mts +152 -16
  165. package/dist/development/core/cart/actions.d.mts +47 -3
  166. package/dist/development/core/cart/actions.mjs +29 -0
  167. package/dist/development/core/cart/actions.mjs.map +1 -1
  168. package/dist/development/core/cart/attach-quantity-input.d.mts +26 -0
  169. package/dist/development/core/cart/attach-quantity-input.mjs +26 -0
  170. package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
  171. package/dist/development/core/cart/cart.d.mts +106 -6
  172. package/dist/development/core/cart/cart.mjs +57 -6
  173. package/dist/development/core/cart/cart.mjs.map +1 -1
  174. package/dist/development/core/cart/cookie.d.mts +15 -0
  175. package/dist/development/core/cart/cookie.mjs +15 -0
  176. package/dist/development/core/cart/cookie.mjs.map +1 -1
  177. package/dist/development/core/cart/form.d.mts +61 -0
  178. package/dist/development/core/cart/form.mjs +18 -0
  179. package/dist/development/core/cart/form.mjs.map +1 -1
  180. package/dist/development/core/cart/get-cart.d.mts +43 -2
  181. package/dist/development/core/cart/get-cart.mjs +33 -0
  182. package/dist/development/core/cart/get-cart.mjs.map +1 -1
  183. package/dist/development/core/cart/index.d.mts +1 -1
  184. package/dist/development/core/cart/quantity.d.mts +17 -0
  185. package/dist/development/core/cart/quantity.mjs +17 -0
  186. package/dist/development/core/cart/quantity.mjs.map +1 -1
  187. package/dist/development/core/cart/queries.d.mts +345 -312
  188. package/dist/development/core/cart/queries.mjs +2 -1
  189. package/dist/development/core/cart/queries.mjs.map +1 -1
  190. package/dist/development/core/cart/server-handlers.d.mts +69 -0
  191. package/dist/development/core/cart/server-handlers.mjs +2 -10
  192. package/dist/development/core/cart/server-handlers.mjs.map +1 -1
  193. package/dist/development/core/cart/state.d.mts +133 -0
  194. package/dist/development/core/cart/state.mjs +8 -0
  195. package/dist/development/core/cart/state.mjs.map +1 -1
  196. package/dist/development/core/collection/collection.d.mts +18 -2
  197. package/dist/development/core/collection/collection.mjs +11 -0
  198. package/dist/development/core/collection/collection.mjs.map +1 -1
  199. package/dist/development/core/collection/index.d.mts +1 -1
  200. package/dist/development/core/collection/reconciler.d.mts +13 -4
  201. package/dist/development/core/collection/reconciler.mjs.map +1 -1
  202. package/dist/development/core/collection/state.d.mts +7 -2
  203. package/dist/development/core/collection/state.mjs.map +1 -1
  204. package/dist/development/core/collection/url.d.mts +3 -0
  205. package/dist/development/core/collection/url.mjs.map +1 -1
  206. package/dist/development/core/constants.mjs +1 -1
  207. package/dist/development/core/constants.mjs.map +1 -1
  208. package/dist/development/core/development.d.mts +5 -5
  209. package/dist/development/core/headers.mjs +2 -2
  210. package/dist/development/core/headers.mjs.map +1 -1
  211. package/dist/development/core/index.d.mts +4 -4
  212. package/dist/development/core/logging/logging.mjs.map +1 -1
  213. package/dist/development/core/money/format.mjs +6 -3
  214. package/dist/development/core/money/format.mjs.map +1 -1
  215. package/dist/development/core/observable.mjs +0 -3
  216. package/dist/development/core/observable.mjs.map +1 -1
  217. package/dist/development/core/predictive-search/form.d.mts +1 -0
  218. package/dist/development/core/predictive-search/form.mjs +1 -0
  219. package/dist/development/core/predictive-search/form.mjs.map +1 -1
  220. package/dist/development/core/predictive-search/index.d.mts +1 -1
  221. package/dist/development/core/predictive-search/queries.d.mts +38 -2
  222. package/dist/development/core/predictive-search/queries.mjs +7 -1
  223. package/dist/development/core/predictive-search/queries.mjs.map +1 -1
  224. package/dist/development/core/predictive-search/search.d.mts +43 -10
  225. package/dist/development/core/predictive-search/search.mjs +34 -0
  226. package/dist/development/core/predictive-search/search.mjs.map +1 -1
  227. package/dist/development/core/predictive-search/server-handlers.d.mts +31 -5
  228. package/dist/development/core/predictive-search/server-handlers.mjs +5 -3
  229. package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
  230. package/dist/development/core/predictive-search/store.d.mts +3 -1
  231. package/dist/development/core/predictive-search/store.mjs.map +1 -1
  232. package/dist/development/core/predictive-search/url.d.mts +50 -8
  233. package/dist/development/core/predictive-search/url.mjs +9 -0
  234. package/dist/development/core/predictive-search/url.mjs.map +1 -1
  235. package/dist/development/core/product/form.d.mts +21 -2
  236. package/dist/development/core/product/form.mjs +13 -0
  237. package/dist/development/core/product/form.mjs.map +1 -1
  238. package/dist/development/core/product/index.d.mts +1 -1
  239. package/dist/development/core/product/options.d.mts +2 -2
  240. package/dist/development/core/product/options.mjs +2 -2
  241. package/dist/development/core/product/options.mjs.map +1 -1
  242. package/dist/development/core/product/product-form.d.mts +33 -1
  243. package/dist/development/core/product/product-form.mjs +30 -0
  244. package/dist/development/core/product/product-form.mjs.map +1 -1
  245. package/dist/development/core/product/state.d.mts +41 -0
  246. package/dist/development/core/product/url.d.mts +6 -0
  247. package/dist/development/core/product/url.mjs.map +1 -1
  248. package/dist/development/core/request-context.d.mts +8 -5
  249. package/dist/development/core/request-context.mjs +17 -39
  250. package/dist/development/core/request-context.mjs.map +1 -1
  251. package/dist/development/core/request-routing/handle-shopify-routes.mjs +4 -1
  252. package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  253. package/dist/development/core/request-routing/interceptors/ajax-api.mjs +1 -1
  254. package/dist/development/core/request-routing/interceptors/api-proxy.mjs +1 -1
  255. package/dist/development/core/request-routing/interceptors/checkout.mjs +22 -6
  256. package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
  257. package/dist/development/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
  258. package/dist/development/core/request-routing/interceptors/proxy.mjs +36 -10
  259. package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
  260. package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
  261. package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
  262. package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
  263. package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
  264. package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
  265. package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
  266. package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
  267. package/dist/development/core/shopify-scripts/analytics.mjs +2 -2
  268. package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
  269. package/dist/development/core/shopify-scripts/constants.mjs +4 -2
  270. package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
  271. package/dist/development/core/shopify-scripts/global.mjs +9 -4
  272. package/dist/development/core/shopify-scripts/global.mjs.map +1 -1
  273. package/dist/development/core/shopify-scripts/index.d.mts +1 -0
  274. package/dist/development/core/shopify-scripts/index.mjs +14 -12
  275. package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
  276. package/dist/development/core/shopify-scripts/initialize.d.mts +1 -0
  277. package/dist/development/core/shopify-scripts/initialize.mjs +10 -3
  278. package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
  279. package/dist/development/core/shopify-scripts/types.d.mts +6 -0
  280. package/dist/development/core/url.mjs +21 -2
  281. package/dist/development/core/url.mjs.map +1 -1
  282. package/dist/development/customer-account/session.d.mts +69 -13
  283. package/dist/development/customer-account/session.mjs +4 -0
  284. package/dist/development/customer-account/session.mjs.map +1 -1
  285. package/dist/development/globals.d.mts +6 -5
  286. package/dist/development/graphql/generated/graphql-env.d.mts +459 -16
  287. package/dist/development/graphql/generated/storefront-api-types.d.mts +1 -1
  288. package/dist/development/graphql/graphql.d.mts +33 -0
  289. package/dist/development/graphql/graphql.mjs +30 -0
  290. package/dist/development/graphql/graphql.mjs.map +1 -1
  291. package/dist/development/graphql/type-resolver.d.mts +14 -0
  292. package/dist/development/react/cart.d.mts +85 -14
  293. package/dist/development/react/cart.mjs +123 -3
  294. package/dist/development/react/cart.mjs.map +1 -1
  295. package/dist/development/react/collection.d.mts +2 -4
  296. package/dist/development/react/collection.mjs.map +1 -1
  297. package/dist/development/react/index.d.mts +8 -6
  298. package/dist/development/react/index.mjs +2 -2
  299. package/dist/development/react/predictive-search.d.mts +68 -7
  300. package/dist/development/react/predictive-search.mjs +54 -12
  301. package/dist/development/react/predictive-search.mjs.map +1 -1
  302. package/dist/development/react/product.d.mts +1 -0
  303. package/dist/development/react/product.mjs +5 -2
  304. package/dist/development/react/product.mjs.map +1 -1
  305. package/dist/development/react/shopify-scripts.mjs +1 -0
  306. package/dist/development/react/shopify-scripts.mjs.map +1 -1
  307. package/dist/development/vue/cart.d.mts +85 -6
  308. package/dist/development/vue/cart.mjs +95 -0
  309. package/dist/development/vue/cart.mjs.map +1 -1
  310. package/dist/development/vue/collection.d.mts +83 -5
  311. package/dist/development/vue/collection.mjs +52 -0
  312. package/dist/development/vue/collection.mjs.map +1 -1
  313. package/dist/development/vue/index.d.mts +6 -4
  314. package/dist/development/vue/predictive-search.d.mts +53 -6
  315. package/dist/development/vue/predictive-search.mjs +33 -0
  316. package/dist/development/vue/predictive-search.mjs.map +1 -1
  317. package/dist/development/vue/product.d.mts +6 -2
  318. package/dist/development/vue/product.mjs.map +1 -1
  319. package/dist/development/vue/shopify-scripts.d.mts +30 -3
  320. package/dist/development/vue/shopify-scripts.mjs +6 -0
  321. package/dist/development/vue/shopify-scripts.mjs.map +1 -1
  322. package/dist/globals.d.mts +6 -5
  323. package/dist/graphql/generated/customer-account-graphql-env.d.mts +1717 -1090
  324. package/dist/graphql/generated/graphql-env.d.mts +459 -16
  325. package/dist/graphql/generated/storefront-api-types.d.mts +1 -1
  326. package/dist/graphql/graphql.d.mts +33 -0
  327. package/dist/graphql/graphql.mjs +30 -0
  328. package/dist/graphql/graphql.mjs.map +1 -1
  329. package/dist/graphql/type-resolver.d.mts +14 -0
  330. package/dist/react/cart.d.mts +85 -14
  331. package/dist/react/cart.mjs +123 -3
  332. package/dist/react/cart.mjs.map +1 -1
  333. package/dist/react/collection.d.mts +2 -4
  334. package/dist/react/collection.mjs.map +1 -1
  335. package/dist/react/index.d.mts +8 -6
  336. package/dist/react/index.mjs +2 -2
  337. package/dist/react/predictive-search.d.mts +68 -7
  338. package/dist/react/predictive-search.mjs +54 -12
  339. package/dist/react/predictive-search.mjs.map +1 -1
  340. package/dist/react/product.d.mts +1 -0
  341. package/dist/react/product.mjs +5 -2
  342. package/dist/react/product.mjs.map +1 -1
  343. package/dist/react/shopify-scripts.mjs +1 -0
  344. package/dist/react/shopify-scripts.mjs.map +1 -1
  345. package/dist/storefront-api-types.d.ts +165 -20
  346. package/dist/storefront.schema.json +782 -57
  347. package/dist/vite/customer-account.mjs +191 -0
  348. package/dist/vite/customer-account.mjs.map +1 -0
  349. package/dist/vite/local-https.mjs +13 -29
  350. package/dist/vite/local-https.mjs.map +1 -1
  351. package/dist/vite/mkcert.mjs +1 -1
  352. package/dist/vue/cart.d.mts +85 -6
  353. package/dist/vue/cart.mjs +95 -0
  354. package/dist/vue/cart.mjs.map +1 -1
  355. package/dist/vue/collection.d.mts +83 -5
  356. package/dist/vue/collection.mjs +52 -0
  357. package/dist/vue/collection.mjs.map +1 -1
  358. package/dist/vue/index.d.mts +6 -4
  359. package/dist/vue/predictive-search.d.mts +53 -6
  360. package/dist/vue/predictive-search.mjs +33 -0
  361. package/dist/vue/predictive-search.mjs.map +1 -1
  362. package/dist/vue/product.d.mts +6 -2
  363. package/dist/vue/product.mjs.map +1 -1
  364. package/dist/vue/shopify-scripts.d.mts +31 -4
  365. package/dist/vue/shopify-scripts.mjs +6 -0
  366. package/dist/vue/shopify-scripts.mjs.map +1 -1
  367. package/package.json +5 -2
  368. package/skills/hydrogen-analytics/SKILL.md +6 -5
  369. package/skills/hydrogen-analytics/references/custom-consent.md +52 -0
  370. package/skills/hydrogen-analytics/references/react.md +1 -1
  371. package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
  372. package/skills/hydrogen-cart-ui/SKILL.md +2 -1
  373. package/skills/hydrogen-customer-account/SKILL.md +3 -3
  374. package/skills/hydrogen-local-https/SKILL.md +22 -7
  375. package/skills/hydrogen-request-handlers/SKILL.md +5 -5
  376. package/skills/hydrogen-request-handlers/references/nuxt.md +0 -6
  377. package/skills/hydrogen-setup/references/analytics.md +24 -23
  378. package/skills/hydrogen-setup/steps/1-inspect-app.md +1 -1
  379. package/skills/hydrogen-setup/steps/11-verify.md +1 -0
  380. package/skills/hydrogen-setup/steps/2-scaffold.md +22 -1
  381. package/skills/hydrogen-smoke-test/SKILL.md +2 -2
  382. package/skills/hydrogen-storefront-client/SKILL.md +4 -2
  383. package/skills/hydrogen-variant-form/SKILL.md +10 -4
  384. package/ts-plugin/package.json +5 -0
  385. package/dist/core/shopify-scripts/consent.mjs +0 -17
  386. package/dist/core/shopify-scripts/consent.mjs.map +0 -1
  387. package/dist/core/shopify-scripts/deprecated-cookies.mjs +0 -97
  388. package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
  389. package/dist/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
  390. package/dist/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
  391. package/dist/core/shopify-scripts/utils/tracking-values.mjs +0 -65
  392. package/dist/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
  393. package/dist/core/shopify-scripts/utils/uuid.mjs +0 -17
  394. package/dist/core/shopify-scripts/utils/uuid.mjs.map +0 -1
  395. package/dist/development/core/shopify-scripts/consent.mjs +0 -17
  396. package/dist/development/core/shopify-scripts/consent.mjs.map +0 -1
  397. package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +0 -97
  398. package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
  399. package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
  400. package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
  401. package/dist/development/core/shopify-scripts/utils/tracking-values.mjs +0 -65
  402. package/dist/development/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
  403. package/dist/development/core/shopify-scripts/utils/uuid.mjs +0 -17
  404. package/dist/development/core/shopify-scripts/utils/uuid.mjs.map +0 -1
@@ -22,11 +22,11 @@
22
22
 
23
23
  **Prerequisites:**
24
24
 
25
- - A storefront built on `@shopify/hydrogen` with the request interceptors already wired (`handleShopifyRoutes` and `handleShopifyRedirects`). The analytics bus depends on the SFAPI proxy so the browser can observe same-origin Storefront API responses for session cookies. Without the proxy, analytics falls back to deprecated JavaScript-visible cookies and should be treated as incomplete. If you have not installed the interceptors yet, install them first with the local `hydrogen-request-handlers` skill.
25
+ - A storefront built on `@shopify/hydrogen` with the request interceptors already wired (`handleShopifyRoutes` and `handleShopifyRedirects`). The analytics bus depends on the SFAPI proxy so the browser can observe same-origin Storefront API responses for session cookies. Without the proxy, the backend session cookies cannot be established and analytics should be treated as incomplete. If you have not installed the interceptors yet, install them first with the local `hydrogen-request-handlers` skill.
26
26
  - Shopify runtime scripts rendered from the root/document head. Use `ShopifyScripts` from your framework binding if it exports one, or `getShopifyScriptTags()` / `renderShopifyScriptTags()` from core in other framework heads. Pass `{country, language, currency?}` as `i18n`; pass `{shopId: env.SHOP_ID, storefrontId: env.PUBLIC_STOREFRONT_ID ?? "0", myshopifyDomain: env.PUBLIC_STORE_DOMAIN}` as `shop`. Resolve both on the server, declare them as consts annotated with the `ShopifyScriptsShop` / `ShopifyScriptsI18n` types from `@shopify/hydrogen` (so wrong or missing fields fail typecheck where they are built), and serialize them into ShopifyScripts. ShopifyScripts creates `window.Shopify.analytics` by default and exposes the permanent domain as `window.Shopify.shop`. Analytics consent config does not accept `country` or `language`.
27
27
  - A client-side lifecycle hook in your framework (route-change effect, navigation event, `<script>` tag, etc.) so view events can fire on the right URL transitions.
28
28
 
29
- `ShopifyScripts` creates the zero-dependency analytics bus, sets it on `window.Shopify.analytics`, and owns Shopify consent setup, analytics CDN loading, and deprecated-cookie compatibility. Framework adapters stay thin: they translate framework lifecycle events into bus calls and wire cart delta tracking with `trackCartAnalytics()`.
29
+ `ShopifyScripts` creates the zero-dependency analytics bus, sets it on `window.Shopify.analytics`, and owns Shopify consent setup and analytics CDN loading. Framework adapters stay thin: they translate framework lifecycle events into bus calls and wire cart delta tracking with `trackCartAnalytics()`.
30
30
 
31
31
  ## What you're installing, and what it does on its own
32
32
 
@@ -36,8 +36,7 @@ Your app code
36
36
  ▼
37
37
  window.Shopify.analytics
38
38
  │
39
- ├── publish / subscribe / addDestination / destroy (app-facing)
40
- ├── raw live subscribers
39
+ ├── publish / addDestination / getConfig (app-facing)
41
40
  └── consent-gated destinations with replay
42
41
  │
43
42
  └── Customer Privacy gates destination delivery and replay
@@ -46,8 +45,7 @@ ShopifyScripts
46
45
  │
47
46
  ├── loads Shopify Customer Privacy script (consent + region gating)
48
47
  ├── loads Privacy Banner script in default-banner mode
49
- ├── loads Shopify analytics destination by default
50
- └── writes deprecated _shopify_y / _shopify_s cookies
48
+ └── loads Shopify analytics destination by default
51
49
  ```
52
50
 
53
51
  The bus is **browser-only effective** and is created by ShopifyScripts in the browser. There is no server-side dispatch.
@@ -93,7 +91,7 @@ const consent: ConsentConfig = {
93
91
  };
94
92
 
95
93
  const analytics: NonNullable<ShopifyScriptTagsOptions["analytics"]> = {
96
- customData: { theme: "v2" }, // optional, attached to bus-generated payloads
94
+ customData: { theme: "v2" }, // optional
97
95
  };
98
96
  ```
99
97
 
@@ -115,19 +113,19 @@ Pass the app's resolved `country` and `language` market values. Optional `curren
115
113
 
116
114
  ### `analytics`
117
115
 
118
- The analytics bus is enabled by default. Pass `analytics` only when you need optional bus configuration such as `customData`, which is attached to bus-generated payloads. Shopify analytics reads currency from `window.Shopify.currency.active`, which is seeded by `i18n.currency` and updated from cart currency when available.
116
+ The analytics bus is enabled by default. Pass `analytics` only when you need optional bus configuration such as `customData`. Shopify analytics reads currency from `window.Shopify.currency.active`, which is seeded by `i18n.currency` and updated from cart currency when available.
119
117
 
120
118
  ### `consent`
121
119
 
122
120
  This is where the location/region nuance lives. Shopify's hosted Customer Privacy API decides per-visitor whether tracking requires consent based on the visitor's geography:
123
121
 
124
- - **Visitors in jurisdictions with consent requirements** (EU/EEA/UK GDPR, parts of Canada, California CCPA, etc.) — analytics must wait for consent. Use `mode: "default-banner"` for Shopify's hosted privacy banner, or `mode: "custom-banner"` if your app renders its own banner and calls `window.Shopify.customerPrivacy.setTrackingConsent()`.
125
- - **Visitors in jurisdictions without consent requirements** — the Customer Privacy SDK auto-allows tracking and the banner does not render. The bus dispatches normally.
122
+ - **Visitors in jurisdictions with consent requirements** (EU/EEA/UK GDPR, parts of Canada, California CCPA, etc.) — analytics must wait for consent. Use `mode: "default-banner"` for Shopify's hosted privacy banner, or `mode: "custom-banner"` with a `setup` callback that connects the app's consent provider.
123
+ - **Visitors in jurisdictions without consent requirements** — the Customer Privacy SDK may allow tracking without showing Shopify's banner. Custom-banner integrations still wait for the provider's resolved consent; the provider owns its regional policy.
126
124
 
127
125
  `mode` controls how consent is collected:
128
126
 
129
127
  - `"default-banner"` loads Shopify's hosted privacy banner and waits when the Customer Privacy API says banner interaction is required.
130
- - `"custom-banner"` loads only the Customer Privacy API and treats the initial consent event as actionable. Your banner must call `setTrackingConsent()` when the shopper accepts or declines.
128
+ - `"custom-banner"` loads only the Customer Privacy API and waits for the required asynchronous `setup` callback to resolve before checking consent. Read [Custom consent providers](../../hydrogen-analytics/references/custom-consent.md) when integrating a third-party banner.
131
129
  - `"no-banner"` loads only the Customer Privacy API and releases analytics after consent setup. Use this only when consent is already allowed or managed outside this storefront.
132
130
 
133
131
  ### Consent Gating
@@ -138,9 +136,9 @@ Default:
138
136
  () => window.Shopify?.customerPrivacy?.analyticsProcessingAllowed() ?? false
139
137
  ```
140
138
 
141
- This is conservative by design: if the Customer Privacy script is blocked, hasn't loaded, or is unavailable, **destination delivery is blocked**. Raw `subscribe()` listeners still see live events, but analytics destinations do not receive events until `analyticsProcessingAllowed()` returns true.
139
+ If the Customer Privacy script is blocked, hasn't loaded, or is unavailable, **destination delivery is blocked**. Destinations receive events only after consent is loaded and analytics processing is allowed.
142
140
 
143
- Events published before consent is ready are buffered for destinations and replayed only if analytics consent is granted. Destinations only receive supported event names they subscribe to. If the visitor explicitly denies analytics consent, the replay buffer is cleared.
141
+ The bus retains up to 500 events for destination replay, including events published while analytics consent was already allowed. New destinations receive that retained history only when analytics consent allows delivery, and only for supported event names they subscribe to. If the visitor explicitly denies analytics consent, the replay buffer is cleared.
144
142
 
145
143
  Custom event names such as `custom_*` are temporarily unsupported. Publishing or subscribing to an unsupported event name logs a small warning and the event is ignored.
146
144
 
@@ -179,7 +177,7 @@ Non-optional constraints:
179
177
  - **One instance per page.** The singleton means every route, every effect, every script tag shares the same bus. Multiple instances on the same page overwrite `window.Shopify.customerPrivacy.config`; the latest initialized config wins. Multi-store on one page is not supported.
180
178
  - **Lazy.** Reading the bus happens on first `getAnalytics()` call. ShopifyScripts owns bus construction.
181
179
 
182
- App code can add explicit destinations for development logging or third-party integrations, then publish from each route:
180
+ Register destinations once during browser app initialization, after ShopifyScripts has created the bus. Keep destination registration outside route component mount/unmount cycles; route components publish events through the shared bus:
183
181
 
184
182
  ```ts
185
183
  const cleanup = analytics.addDestination({
@@ -205,7 +203,7 @@ const cleanup = analytics.addDestination({
205
203
  });
206
204
  ```
207
205
 
208
- Destinations are consent-gated and receive replayed buffered events once tracking is allowed.
206
+ Destinations are consent-gated and receive retained history once tracking is allowed. The returned cleanup function removes the destination when the app intentionally stops that integration.
209
207
 
210
208
  ---
211
209
 
@@ -555,7 +553,7 @@ type AnalyticsCart = {
555
553
  };
556
554
  ```
557
555
 
558
- Manually published `AnalyticsCart` payloads (like `CART_VIEWED`) accept both `lines.nodes` and `lines.edges` (GraphQL connection) shapes at the type level; the bus forwards them unchanged, so subscribers that read lines should flatten them with the exported `flattenConnection()` helper. The cart store consumed by `trackCartAnalytics` is different: it reads `cart.lines.nodes` directly, so the app's cart query must select `lines.nodes`. The tracker falls back to the current time internally if store data is missing `updatedAt`, but include `updatedAt` in the cart query for stable dedupe.
556
+ Manually published `AnalyticsCart` payloads (like `CART_VIEWED`) accept both `lines.nodes` and `lines.edges` (GraphQL connection) shapes at the type level; the bus forwards them unchanged, so subscribers that read lines should flatten them with the exported `flattenConnection()` helper. The cart store consumed by `trackCartAnalytics` is different: it reads `cart.lines.nodes` directly, so the app's cart query must select `lines.nodes`.
559
557
 
560
558
  Application code should not manually publish `cart_updated`, `product_added_to_cart`, or `product_removed_from_cart`. Always go through `trackCartAnalytics`.
561
559
 
@@ -563,7 +561,7 @@ Application code should not manually publish `cart_updated`, `product_added_to_c
563
561
 
564
562
  ## Wiring third-party destinations
565
563
 
566
- The bus is the right integration point for GA4, Meta Pixel, Klaviyo, etc. Register third-party analytics with `addDestination()`. The bus gates destination callbacks with Shopify Customer Privacy and replays buffered events after analytics consent is granted:
564
+ The bus is the right integration point for GA4, Meta Pixel, Klaviyo, etc. Register each integration once with `addDestination()` during browser app initialization, after ShopifyScripts has created the bus. The bus gates destination callbacks with Shopify Customer Privacy and replays retained history when analytics consent allows:
567
565
 
568
566
  ```ts
569
567
  const analytics = getAnalytics();
@@ -578,7 +576,11 @@ analytics?.addDestination({
578
576
  });
579
577
  ```
580
578
 
581
- Consent gating happens at the bus level before destination callbacks see the payload. Raw `analytics.subscribe()` is live-only and consent-agnostic; use `addDestination()` for logging or analytics destinations that should respect consent and replay.
579
+ Register all event consumers with `addDestination()` and subscribe inside its setup callback. Consent gating happens at the bus level before destination callbacks see the payload.
580
+
581
+ Removing and re-adding a destination, even with the same name, creates a fresh registration and replays retained history again. This can duplicate deliveries on component remounts, including React Strict Mode's development effect replay. Keep destinations registered for the page's lifetime; use the returned cleanup when intentionally removing an integration.
582
+
583
+ Destinations that need Shopify visitor IDs can call `getTrackingValues()` from the callback's second argument: `subscribe(event, (payload, { getTrackingValues }) => { ... })`. It reads current `uniqueToken` and `visitToken` values from the consent API when called, including during replay, and requests fallback generation with the tag `hydrogen:<destination name>`. Unavailable tokens are empty strings, and the getter also returns empty strings whenever analytics tracking is not currently allowed, so a retained getter cannot read or generate tokens after consent is revoked. Call it inside the destination callback; registration itself does not read or generate tokens.
582
584
 
583
585
  ---
584
586
 
@@ -589,7 +591,7 @@ After wiring, smoke-test each event in the browser dev tools:
589
591
  1. **Destination log fires** — open the page with the dev console open. You should see `[analytics] page_viewed` (or whichever events you logged) after consent allows tracking. If nothing logs, either `getAnalytics()` is no-op'ing on the server, the bus is unavailable, or `analyticsProcessingAllowed()` is false.
590
592
  2. **Monorail request fires** — Network tab, filter for `monorail-edge.shopifysvc.com`. A `produce_batch` POST should land within ~1s of consent being granted (or immediately if the visitor is in a no-consent-required region). If it never fires, either consent has not been granted, the schemas are missing required fields (check console for warnings about missing `id`/`title`/`vendor`/etc.), or `hasUserConsent` is false on the payload.
591
593
  3. **Per-route navigation fires page_viewed** — click around. Each navigation should produce a fresh `page_viewed` event. If only the initial page load fires, the route-change hook is wired wrong (e.g. effect dependency missing in React, reactive read missing in Solid).
592
- 4. **Cart events fire** — add an item to the cart. You should see `cart_updated` followed by `product_added_to_cart`. If you see `cart_updated` repeating with the same payload, the dedupe key (`updatedAt`) is stale — confirm your cart query selects `updatedAt`.
594
+ 4. **Cart events fire** — add an item to the cart. You should see `cart_updated` followed by `product_added_to_cart`.
593
595
  5. **Privacy banner renders for EU/UK visitors** — if `mode: "default-banner"`, simulate a GDPR-protected region with browser dev-tools location override or VPN. The banner should render. If it does not, check that `cdn.shopify.com` is not blocked by your CSP.
594
596
 
595
597
  For production, re-verify against the production bundle. Several gotchas only appear once the SSR/CSR boundary stabilizes.
@@ -598,7 +600,7 @@ For production, re-verify against the production bundle. Several gotchas only ap
598
600
 
599
601
  ## Common gotchas
600
602
 
601
- - **Replay is destination-only.** Raw `analytics.subscribe()` listeners only receive live events. `analytics.addDestination()` callbacks receive consent-gated live events plus buffered replay after analytics consent is granted. If the visitor explicitly denies analytics consent, the buffer is cleared and those pre-denial events are never replayed.
603
+ - **Destinations receive live events and buffered replay after analytics consent is granted.** If the visitor explicitly denies analytics consent, the buffer is cleared and those pre-denial events are never replayed.
602
604
  - **The singleton must be lazy.** Reading the global bus at module top-level can run on the server during SSR and crash on `window` access. Always wrap in a `typeof window === 'undefined'` guard.
603
605
  - **Use the right shop shape for each API.** `ShopifyScripts` accepts a numeric Shop ID or Shopify Shop GID plus `storefrontId` and the permanent `myshopifyDomain`; the analytics bus normalizes `shop.shopId` to a Shopify Shop GID before dispatch, while the bootstrap exposes the domain as `window.Shopify.shop`.
604
606
  - **Customer Privacy script blocked by CSP.** If your CSP does not allow `cdn.shopify.com`, the consent script never loads, `analyticsProcessingAllowed()` stays `false`, and destination events never deliver. Check Network tab for blocked requests; add `cdn.shopify.com` to `script-src`.
@@ -607,8 +609,7 @@ For production, re-verify against the production bundle. Several gotchas only ap
607
609
  - **Astro inline scripts cannot reference component scope.** Astro hoists `<script>` tags at build time. Bridge SSR data through hidden DOM (`data-*` attributes) and read it from the script. Trying to interpolate `{product.id}` directly into a script body silently fails — the script ships as a static string.
608
610
  - **Astro page-view fires only on full loads.** Astro is MPA-by-default. If you adopt View Transitions, listen for `astro:after-swap` instead of relying on the inline-script-runs-on-load behavior — otherwise SPA-nav transitions skip `page_viewed`.
609
611
  - **Required product fields silently drop the Monorail leg.** Missing `id`/`title`/`vendor`/`variantId`/`variantTitle`/`price` causes the Shopify analytics subscriber to skip Monorail dispatch and log a field-specific error. The bus event still fires for your subscribers — the loss is only in Shopify analytics. Watch the console.
610
- - **`updatedAt` missing from cart query weakens dedupe.** The cart tracker prefers cart `updatedAt`, but falls back to the current time when it is absent. Include `updatedAt` in cart queries for stable dedupe across navigations and reloads.
611
- - **`destroy()` is not called by any of the framework adapter sketches.** During HMR or React Strict Mode double-mount, this means duplicate event subscribers and possibly duplicate Monorail events in dev. For production this is rarely visible (one bus per page lifetime). If duplicate dev events bother you, wire `analytics.destroy()` into your framework's teardown (React effect cleanup, Svelte `onDestroy`, Solid `onCleanup`, or equivalent).
612
+ - **Register destinations once per page lifetime.** Hydrogen owns the shared bus. Removing and re-adding a destination resets its replay position, so component remounts can deliver retained events again.
612
613
  - **Lighthouse skip is silent.** Monorail dispatch is skipped for Chrome Lighthouse user-agents. If your synthetic monitoring runs Lighthouse, you will see no Monorail requests in those runs — this is intentional.
613
614
 
614
615
  ---
@@ -621,4 +622,4 @@ For production, re-verify against the production bundle. Several gotchas only ap
621
622
  - **Don't reimplement Monorail dispatch.** If you need a third-party destination, register it with `addDestination()` and forward from there — do not parallel-publish to Monorail yourself.
622
623
  - **Don't put per-route view events in a global subscriber.** A single subscriber that watches `page_viewed` and synthesizes `product_viewed` from URL parsing is brittle and loses payload context. Publish each view event from the route that has the data.
623
624
  - **Don't construct multiple buses for "different consent contexts" on the same page.** Customer Privacy config is global; the latest initialized config takes effect. If you need conditional behavior, branch inside subscribers, not at construction.
624
- - **Don't skip the request-handler prerequisite.** Without the SFAPI proxy, modern same-origin Shopify cookies cannot be set. Analytics may appear to work via deprecated JS-visible cookies, but session continuity into checkout breaks. Treat analytics as incomplete until the proxy is live in production.
625
+ - **Don't skip the request-handler prerequisite.** Without the SFAPI proxy, modern same-origin Shopify cookies cannot be set. Session continuity into checkout depends on these backend cookies. Treat analytics as incomplete until the proxy is live in production.
@@ -10,7 +10,7 @@ If it does not contain `@shopify/hydrogen@preview` (or a more specific hash) in
10
10
 
11
11
  ### Continue when
12
12
 
13
- - [ ] Current directrory has `package.json`
13
+ - [ ] Current directory has `package.json`
14
14
  - [ ] Current project includes `@shopify/hydrogen@preview` or similar in its dependencies
15
15
 
16
16
  ## Styling
@@ -20,6 +20,7 @@ If Playwright is present, run it headless.
20
20
 
21
21
  - [ ] Every applicable script was executed in this session, after the last code change, and passed
22
22
  - [ ] `hydrogen gql check` ran as part of static checks (not skipped)
23
+ - [ ] `dev` still starts with `hydrogen skills check --mode=warn` and prints no skills warning
23
24
 
24
25
  ## Run Runtime Smoke Tests
25
26
 
@@ -10,7 +10,7 @@ Preserve the app's existing route shape when present. When there is no establish
10
10
  - `/products/{handle}` for product detail. Use plural `products`, not `/product`.
11
11
  - `/cart` for the full cart page.
12
12
 
13
- Hydrogen-owned handlers are not page routes: `/api/cart`, `/api/{api-version}/graphql.json`, the generic `/__shopify/*` API proxy, `/checkout`, cart permalinks like `/cart/{variantId}:{quantity}`, AJAX cart URLs like `/cart.js` and `/cart/add.js`, `/api/mcp`, `/graphiql` in development, Liquid-style `?variant=<numeric id>` product redirects, `/admin` redirects, and Storefront URL redirects belong in the `hydrogen-request-handlers` wiring.
13
+ Hydrogen-owned handlers are not page routes: `/api/cart`, `/api/{api-version}/graphql.json`, the generic `/__shopify/*` API proxy, `/checkout`, cart permalinks like `/cart/{variantId}:{quantity}`, UCP buy permalinks like `/buy/{itemId}:{quantity}`, AJAX cart URLs like `/cart.js` and `/cart/add.js`, `/api/mcp`, `/graphiql` in development, Liquid-style `?variant=<numeric id>` product redirects, `/admin` redirects, and Storefront URL redirects belong in the `hydrogen-request-handlers` wiring.
14
14
 
15
15
  Invoke the `hydrogen-routing` skill and create the shared route template manifest for Shopify resources such as products, collections, pages, blogs, or articles.
16
16
 
@@ -18,6 +18,25 @@ Invoke the `hydrogen-routing` skill and create the shared route template manifes
18
18
 
19
19
  - [ ] Route templates are set up
20
20
 
21
+ ## Keep Skills In Sync
22
+
23
+ The packaged skills describe the installed Hydrogen version. Chain `hydrogen skills check --mode=warn` in front of the app's `dev` script so a later `@shopify/hydrogen` bump prints the resync command the next time the dev server starts, without blocking it:
24
+
25
+ ```json
26
+ {
27
+ "scripts": {
28
+ "dev": "hydrogen skills check --mode=warn && <framework dev command>"
29
+ }
30
+ }
31
+ ```
32
+
33
+ Keep the framework's own dev command exactly as it was; only prefix it. Apply the same prefix to any sibling dev script such as `dev:https`. Do not add the plain `hydrogen skills check` (error mode) to `dev`; that belongs in CI, where a stale skill copy should fail the build.
34
+
35
+ ### Continue when
36
+
37
+ - [ ] `dev` runs `hydrogen skills check --mode=warn` before the framework dev command
38
+ - [ ] Running the `dev` script prints no skills warning (skills were synced by `hydrogen setup`)
39
+
21
40
  ## Use Standard Environment Names
22
41
 
23
42
  Use these canonical environment variable names throughout the app (kept in sync with `hydrogen-storefront-client`):
@@ -35,6 +54,8 @@ If the framework requires a prefix to expose client-side variables, preserve the
35
54
 
36
55
  List `PUBLIC_STOREFRONT_API_TOKEN` in the app's env example file as a commented-out entry (`# PUBLIC_STOREFRONT_API_TOKEN=`). The Storefront client accepts `undefined` as tokenless access, which is all mock.shop supports, so the scaffold works before the user has tokens and upgrades in place when they add one. Do not write an uncommented empty assignment (`PUBLIC_STOREFRONT_API_TOKEN=`): env loaders parse that as an empty string, and the client rejects empty tokens. Recommend filling it in (or switching to a private client) once the app targets a real store.
37
56
 
57
+ When the user has no store yet, point `PUBLIC_STORE_DOMAIN` at a mock.shop store. `mock.shop` is the default apparel catalog; https://mock.shop/llms.txt lists every other fictional store with its host (for example `pets.mock.shop`) and what it sells. Pick the store closest to what the user is building, and see the `hydrogen-storefront-client` skill for the details.
58
+
38
59
  ### Continue when
39
60
 
40
61
  - [ ] Env vars follow the canonical names (plus any required framework prefix)
@@ -68,7 +68,7 @@ Collection and search:
68
68
  Analytics:
69
69
  - [ ] Page view fires on initial load and client navigations
70
70
  - [ ] Product/collection/search/cart view events fire once per route data change
71
- - [ ] Cart tracking is wired once via trackCartAnalytics(cartStore) (React/Vue: useCartAnalytics()), delta events fire on confirmed cart data changes, and the cart query includes updatedAt
71
+ - [ ] Cart tracking is wired once via trackCartAnalytics(cartStore) (React/Vue: useCartAnalytics()), and delta events fire on confirmed cart data changes
72
72
  - [ ] No browser module reads private env variables
73
73
  - [ ] Production does not bypass Customer Privacy consent gating (no forced-always-true consent checks)
74
74
 
@@ -190,7 +190,7 @@ Expected: a redirect whose `location` header points at Shopify's hosted login, n
190
190
 
191
191
  - Page view fires on initial load and client navigations.
192
192
  - Product/collection/search/cart view events fire once per route data change.
193
- - Cart tracking is wired once per cart store lifecycle via `trackCartAnalytics(cartStore)` (React/Vue: `useCartAnalytics()`), cart delta events fire on confirmed cart data changes, and the cart query includes `updatedAt`.
193
+ - Cart tracking is wired once per cart store lifecycle via `trackCartAnalytics(cartStore)` (React/Vue: `useCartAnalytics()`), and cart delta events fire on confirmed cart data changes.
194
194
  - No browser module reads private env variables.
195
195
  - Analytics destinations only receive events after Shopify Customer Privacy allows analytics processing.
196
196
 
@@ -43,6 +43,10 @@ Environment variables are still a **server-side input boundary**. Do not read `p
43
43
 
44
44
  Default to a public client. `publicStorefrontToken` accepts `undefined`, which means tokenless access — there is no rate limit for tokenless clients, and it is all mock.shop supports. Always recommend a token-backed client (public or private) once the app targets a real store: token-based Storefront API access is required for product tags, metaobjects, metafields, menus, and customers. Public and private tokens can query the same token-required fields; the difference is whether the token is safe to expose and whether the request has trusted buyer context.
45
45
 
46
+ ### mock.shop is a catalog of stores
47
+
48
+ mock.shop is not one store. The default store at `mock.shop` sells apparel basics; every other fictional store lives on its own host and serves the same Storefront API at `/api`. When the user has no store yet, read the directory at https://mock.shop/llms.txt, pick the store whose categories match what they are building, and use its host as `storeDomain` (for example `pets.mock.shop`). Each store describes its own catalog at `https://<store>.mock.shop/llms.txt`. Every mock.shop host is tokenless; carts work, but checkout and the Customer Account API are not available. Moving to a real store changes only `storeDomain` and the token.
49
+
46
50
  ### Public client (default)
47
51
 
48
52
  Wire `publicStorefrontToken` from the canonical env variable without asserting it exists. When `PUBLIC_STOREFRONT_API_TOKEN` is unset the client runs tokenless, so the same scaffold works against mock.shop before the user has tokens and upgrades in place when they add one. In env example files list the variable commented out (`# PUBLIC_STOREFRONT_API_TOKEN=`) rather than as an empty assignment: env loaders parse `PUBLIC_STOREFRONT_API_TOKEN=` as an empty string, and the client rejects empty tokens.
@@ -280,8 +284,6 @@ Every successful result includes `headers` (the raw `Headers` object from the re
280
284
 
281
285
  A 200 response with GraphQL `errors` does NOT throw — partial success is valid in GraphQL. Non-200 responses, timeouts, network failures, and JSON parse errors all throw `StorefrontApiError` (or the `StorefrontTimeoutError` subclass).
282
286
 
283
- `StorefrontApiError` carries GraphQL error context (`locations`, `path`, `extensions`) when available. `extensions.code` enables programmatic branching (e.g. retry on `"THROTTLED"`). `toJSON()` strips dev-only fields (`queryText`, `variables`, `stack`) — safe for error reporters.
284
-
285
287
  ---
286
288
 
287
289
  ## Testing
@@ -86,6 +86,10 @@ Accepts `{ value: number }` for a controlled input or `{ defaultValue: number }`
86
86
 
87
87
  Requires `{ optionName, value }`. Returns `{ name, value, onChange, onClick }` — form identity plus activation handlers. Derive caller-owned UI props from the matching option value state, such as `value.exists`, `value.available`, and `value.selected`.
88
88
 
89
+ ### `register("attributeValue", opts)`
90
+
91
+ Requires `{ key: string; value: string }` (controlled) or `{ key: string; defaultValue: string }` (uncontrolled). Returns `{ name: "attributes.<key>", value }` — a hidden input that attaches a line-item attribute to the add-to-cart submission. Use for per-line metadata like engraving text, gift messages, or custom options. Multiple `attributeValue` fields can be registered for different keys. The `key` must be non-empty (throws `TypeError` otherwise). Attributes with keys starting with `_` are conventionally internal/private and should not be set from the storefront UI.
92
+
89
93
  ### `register("addToCart", opts)`
90
94
 
91
95
  Returns `{ name: "add-to-cart", type: "submit" }` for the add-to-cart submit button.
@@ -174,8 +178,8 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
174
178
  ### Add-to-cart
175
179
 
176
180
  - **ALWAYS use `canAddToCart(product, options)` to determine if the add-to-cart button should be enabled.** This checks three conditions: a variant is selected, it is available for sale, and the product does not require a selling plan. Checking only `selectedVariant !== null` misses the selling-plan and availability constraints.
177
- - **The add-to-cart form is separate from the variant selector.** Variant selection uses buttons and links — not form submissions. The add-to-cart form contains `merchandiseId` (the selected variant ID) and `quantity`. Do not put variant selection controls inside the cart form.
178
- - **Use `register` to bind form fields.** `register("merchandiseId", {})` returns the hidden input props with the current variant ID. `register("quantity", { value: 1 })` returns the quantity input props. `register("addToCart", {})` returns stable add-to-cart submit button props. These stay synchronized with store state automatically.
181
+ - **The add-to-cart form is separate from the variant selector.** Variant selection uses buttons and links — not form submissions. The add-to-cart form contains `merchandiseId` (the selected variant ID), `quantity`, and optionally `attributeValue` fields for line-item attributes. Do not put variant selection controls inside the cart form.
182
+ - **Use `register` to bind form fields.** `register("merchandiseId", {})` returns the hidden input props with the current variant ID. `register("quantity", { value: 1 })` returns the quantity input props. `register("attributeValue", { key: "Engraving", value })` returns a hidden input for a line-item attribute. `register("addToCart", {})` returns stable add-to-cart submit button props. These stay synchronized with store state automatically.
179
183
  - **Use the local `hydrogen-shop-pay` skill** when adding accelerated checkout near the add-to-cart form.
180
184
  - **Show contextual CTA text.** When `canAddToCart` is `true`: "Add to cart". When no variant is selected (`selectedVariant === null`): "Select options" unless a navigation or submission is actually pending. When a variant is selected but unavailable: "Unavailable" or "Sold out".
181
185
  - **Surface cart errors from `errors` state.** After form submission, user errors, warnings, and network errors relevant to the current product form are available on `state.errors`. Display these to the buyer.
@@ -246,14 +250,16 @@ Pass `style: "variant"` with a resolved `variant` to emit a shareable `?variant=
246
250
  28. **Caller-owned option attributes** — `register("optionValue", { optionName: "Color", value: "Red" })` returns only `{ name, value, onChange, onClick }`. Derive `disabled`, `aria-pressed`, and visual state from the matching `options` value.
247
251
  29. **Quantity registration** — `register("quantity", { value: 1 })` returns `{ name: "quantity", value: "1" }`. `register("quantity", { defaultValue: 1 })` returns `{ name: "quantity", defaultValue: "1" }`.
248
252
  30. **Add-to-cart registration** — `register("addToCart", {})` returns `{ name: "add-to-cart", type: "submit" }`.
253
+ 31. **Attribute value registration** — `register("attributeValue", { key: "Engraving", value: "Hello" })` returns `{ name: "attributes.Engraving", value: "Hello" }`. `register("attributeValue", { key: "Engraving", defaultValue: "" })` returns `{ name: "attributes.Engraving", defaultValue: "" }`. An empty `key` throws a `TypeError`.
254
+ 32. **Attribute in add-to-cart** — When `register("attributeValue", { key: "Engraving", value: "Hello" })` is included as a hidden input in the add-to-cart form, the submitted cart line carries the attribute. The attribute appears on the matched line item in the cart drawer (with `_`-prefixed internal attributes filtered out).
249
255
 
250
256
  ### Unresolved selection
251
257
 
252
- 31. **Transient unresolved** — In URL-routing apps, when a complete selection returns `unresolved` because the exact variant is absent from the local cache, the app navigates to the new URL and re-fetches product data. The subsequent hydration resolves the variant. Incomplete selections should remain in selection UI until the buyer chooses the remaining options.
258
+ 33. **Transient unresolved** — In URL-routing apps, when a complete selection returns `unresolved` because the exact variant is absent from the local cache, the app navigates to the new URL and re-fetches product data. The subsequent hydration resolves the variant. Incomplete selections should remain in selection UI until the buyer chooses the remaining options.
253
259
 
254
260
  ### Reset
255
261
 
256
- 32. **Reset to initial state** — Calling `reset()` restores the store to the product and selected options it was created with. All user selections are discarded.
262
+ 34. **Reset to initial state** — Calling `reset()` restores the store to the product and selected options it was created with. All user selections are discarded.
257
263
 
258
264
  ---
259
265
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "private": true,
3
+ "main": "../dist/ts-plugin/index.cjs",
4
+ "types": "../dist/ts-plugin/index.d.cts"
5
+ }
@@ -1,17 +0,0 @@
1
- import { asInlineScript } from "./utils/inline-script.mjs";
2
- import { SHOPIFY_CONSENT_SCRIPT_ID, VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
3
-
4
- //#region src/core/shopify-scripts/consent.ts
5
- const initializeShopifyConsentTracking = (scriptData) => decodeURIComponent("(function()%7Bfunction%20e(e)%7Breturn%20typeof%20e%3D%3D%60object%60%26%26!!e%7Dfunction%20t(e%2Ct)%7Blet%20n%3D%5B%60%5Bhydrogen%3Aerror%3Aconsent%5D%20%24%7Be%7D%60%5D%3Bt!%3D%3Dvoid%200%26%26n.push(t)%2Cconsole.error(...n)%7Dfunction%20n(n)%7Blet%20r%3Dwindow%2Ci%3Dn.mode%3D%3D%3D%60default-banner%60%2Ca%3D!1%2Co%3D!1%2Cs%3D%60initial%60%2Cc%3D()%3D%3Er.Shopify%3F.customerPrivacy%2Cl%3D()%3D%3E%7Blet%20e%3Dc()%3Be%26%26(e.consentStatus%3D%60loaded%60)%7D%2Cu%3D()%3D%3E%7Bo%3D!0%2Ca%26%26l()%7D%2Cd%3D()%3D%3E%7Blet%20e%3Dc()%3F.setTrackingConsent%3Bif(typeof%20e!%3D%60function%60)return%20t(%60unable%20to%20request%20initial%20consent%60)%2Cu()%3Be(%7BheadlessStorefront%3A!0%7D%2Ce%3D%3E%7Bif(e%3F.error)%7Bt(%60unable%20to%20request%20initial%20consent%60%2Ce.error)%3Breturn%7Du()%7D)%7D%2Cf%3D()%3D%3E%7Blet%20e%3Dc()%3Breturn%20typeof%20e%3F.setTrackingConsent%3D%3D%60function%60%7C%7Ctypeof%20e%3F.shouldShowBanner%3D%3D%60function%60%7C%7Ctypeof%20window.privacyBanner%3D%3D%60object%60%7D%2Cp%3D()%3D%3E%7Ba%7C%7C(a%3D!0%2Ci%3Fo%26%26u()%3Ad())%7D%3Bdocument.addEventListener(n.eventName%2Ct%3D%3E%7Blet%20n%3D%60detail%60in%20t%3Ft.detail%3Avoid%200%3Be(n)%26%26(n.source%3F%3F%3Ds)%2Cs%3D%60interaction%60%7D%2C%7Bcapture%3A!0%7D)%2Cdocument.addEventListener(n.eventName%2Cu%2C%7Bcapture%3A!0%7D)%3Blet%20m%3Ddocument.getElementById(n.scriptId)%3Bf()%3Fp()%3Am%3F.addEventListener(%60load%60%2Cp%2C%7Bonce%3A!0%7D)%7Dn(__SHOPIFY_SCRIPT_DATA__)%7D)()%3B").replace("__SHOPIFY_SCRIPT_DATA__", () => (JSON.stringify(scriptData) ?? "undefined").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"));
6
- function getShopifyConsentTrackingScript(consent = {}) {
7
- const config = {
8
- eventName: VISITOR_CONSENT_COLLECTED_EVENT,
9
- mode: consent.mode ?? "no-banner",
10
- scriptId: SHOPIFY_CONSENT_SCRIPT_ID
11
- };
12
- return asInlineScript(initializeShopifyConsentTracking)(config);
13
- }
14
-
15
- //#endregion
16
- export { getShopifyConsentTrackingScript };
17
- //# sourceMappingURL=consent.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"consent.mjs","names":[],"sources":["../../../src/core/shopify-scripts/consent.ts"],"sourcesContent":["import type { ConsentConfig } from \"../analytics/types\";\nimport initializeShopifyConsentTracking from \"./consent-script\" with { type: \"script\" };\nimport type { ShopifyConsentTrackingConfig } from \"./consent-script\";\nimport { SHOPIFY_CONSENT_SCRIPT_ID, VISITOR_CONSENT_COLLECTED_EVENT } from \"./constants\";\nimport { asInlineScript } from \"./utils/inline-script\";\n\nexport function getShopifyConsentTrackingScript(consent: ConsentConfig = {}): string {\n const config: ShopifyConsentTrackingConfig = {\n eventName: VISITOR_CONSENT_COLLECTED_EVENT,\n mode: consent.mode ?? \"no-banner\",\n scriptId: SHOPIFY_CONSENT_SCRIPT_ID,\n };\n\n return asInlineScript(initializeShopifyConsentTracking)(config);\n}\n"],"mappings":";;;;AACA,MAAM,oCAAkC,eAAgB,mBAAqB,q8CAAU,CAAA,QAAA,kCAAA,KAAA,UAAA,WAAA,IAAA,aAAA,QAAA,MAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA,QAAA,WAAA,UAAA,CAAA;AAKvF,SAAgB,gCAAgC,UAAyB,EAAE,EAAU;CACnF,MAAM,SAAuC;EAC3C,WAAW;EACX,MAAM,QAAQ,QAAQ;EACtB,UAAU;EACX;AAED,QAAO,eAAe,iCAAiC,CAAC,OAAO"}
@@ -1,97 +0,0 @@
1
- import { VISITOR_CONSENT_COLLECTED_EVENT } from "./constants.mjs";
2
- import { findWritableCookieDomain } from "./utils/cookie-domain.mjs";
3
- import { getTrackingValues } from "./utils/tracking-values.mjs";
4
- import { buildUUID } from "./utils/uuid.mjs";
5
-
6
- //#region src/core/shopify-scripts/deprecated-cookies.ts
7
- /**
8
- * @deprecated This module manages the legacy `_shopify_y` and `_shopify_s`
9
- * JavaScript-visible cookies. Modern Shopify storefronts use http-only cookies
10
- * set by the Storefront API via the SFAPI proxy. This module exists only for
11
- * backward compatibility with downstream systems that may still read these
12
- * JS cookies directly.
13
- *
14
- * To remove: delete this file and remove the ShopifyScripts startup call.
15
- */
16
- const SHOPIFY_Y = "_shopify_y";
17
- const SHOPIFY_S = "_shopify_s";
18
- const LONG_TERM_EXPIRY_IN_SECONDS = 3600 * 24 * 360;
19
- const SHORT_TERM_EXPIRY_IN_SECONDS = 1800;
20
- /**
21
- * Computes the cookie domain as the broadest wildcard domain the browser will
22
- * accept for the consent domain. Returns empty string for localhost or when
23
- * probing fails (cookies use current domain).
24
- *
25
- * @deprecated Part of legacy cookie management.
26
- */
27
- function computeCookieDomain(consentDomain) {
28
- if (typeof window === "undefined") return "";
29
- return findWritableCookieDomain(consentDomain || window.location.host);
30
- }
31
- function setCookie(name, value, maxAgeInSeconds, domain) {
32
- const parts = [`${name}=${encodeURIComponent(value)}`];
33
- parts.push(`max-age=${maxAgeInSeconds}`);
34
- parts.push("path=/");
35
- parts.push("SameSite=Lax");
36
- if (domain) parts.push(`domain=${domain}`);
37
- document.cookie = parts.join("; ");
38
- }
39
- /**
40
- * @deprecated Sets or removes the legacy `_shopify_y` and `_shopify_s` cookies
41
- * based on the current consent state and tracking values.
42
- */
43
- function updateCookies(hasConsent, domain) {
44
- if (hasConsent) {
45
- const trackingValues = getTrackingValues();
46
- if ((trackingValues.uniqueToken || trackingValues.visitToken || "").startsWith("00000000-")) return;
47
- setCookie(SHOPIFY_Y, trackingValues.uniqueToken || buildUUID(), LONG_TERM_EXPIRY_IN_SECONDS, domain);
48
- setCookie(SHOPIFY_S, trackingValues.visitToken || buildUUID(), SHORT_TERM_EXPIRY_IN_SECONDS, domain);
49
- } else {
50
- setCookie(SHOPIFY_Y, "", 0, domain);
51
- setCookie(SHOPIFY_S, "", 0, domain);
52
- }
53
- }
54
- function hasAnalyticsConsent() {
55
- try {
56
- return (window.Shopify?.customerPrivacy)?.analyticsProcessingAllowed?.() ?? false;
57
- } catch {
58
- return false;
59
- }
60
- }
61
- function isConsentLoaded() {
62
- return window.Shopify?.customerPrivacy?.consentStatus === "loaded";
63
- }
64
- function getConsentDomain() {
65
- return window.Shopify?.customerPrivacy?.config?.consentDomain;
66
- }
67
- let didInitializeDeprecatedCookies = false;
68
- const noOpCleanup = () => {};
69
- /**
70
- * @deprecated Starts page-lifetime legacy cookie management from ShopifyScripts.
71
- */
72
- function initializeDeprecatedCookies() {
73
- if (typeof window === "undefined" || typeof document === "undefined") return noOpCleanup;
74
- if (didInitializeDeprecatedCookies) return noOpCleanup;
75
- didInitializeDeprecatedCookies = true;
76
- const domain = computeCookieDomain(getConsentDomain());
77
- const syncConsent = () => {
78
- updateCookies(hasAnalyticsConsent(), domain);
79
- };
80
- document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);
81
- if (isConsentLoaded()) syncConsent();
82
- const syncPageView = () => {
83
- if (hasAnalyticsConsent()) updateCookies(true, domain);
84
- };
85
- const subscribeToAnalytics = () => {
86
- const subscribe = window.Shopify?.analytics?.subscribe;
87
- if (typeof subscribe !== "function") return false;
88
- subscribe("page_viewed", syncPageView);
89
- return true;
90
- };
91
- if (!subscribeToAnalytics() && document.readyState === "loading") document.addEventListener("DOMContentLoaded", subscribeToAnalytics, { once: true });
92
- return noOpCleanup;
93
- }
94
-
95
- //#endregion
96
- export { initializeDeprecatedCookies };
97
- //# sourceMappingURL=deprecated-cookies.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"deprecated-cookies.mjs","names":[],"sources":["../../../src/core/shopify-scripts/deprecated-cookies.ts"],"sourcesContent":["/**\n * @deprecated This module manages the legacy `_shopify_y` and `_shopify_s`\n * JavaScript-visible cookies. Modern Shopify storefronts use http-only cookies\n * set by the Storefront API via the SFAPI proxy. This module exists only for\n * backward compatibility with downstream systems that may still read these\n * JS cookies directly.\n *\n * To remove: delete this file and remove the ShopifyScripts startup call.\n */\n\nimport { VISITOR_CONSENT_COLLECTED_EVENT } from \"./constants\";\nimport { findWritableCookieDomain } from \"./utils/cookie-domain\";\nimport { getTrackingValues } from \"./utils/tracking-values\";\nimport { buildUUID } from \"./utils/uuid\";\n\nconst SHOPIFY_Y = \"_shopify_y\";\nconst SHOPIFY_S = \"_shopify_s\";\n\nconst LONG_TERM_EXPIRY_IN_SECONDS = 60 * 60 * 24 * 360;\nconst SHORT_TERM_EXPIRY_IN_SECONDS = 60 * 30;\n\n/**\n * Computes the cookie domain as the broadest wildcard domain the browser will\n * accept for the consent domain. Returns empty string for localhost or when\n * probing fails (cookies use current domain).\n *\n * @deprecated Part of legacy cookie management.\n */\nexport function computeCookieDomain(consentDomain?: string): string {\n if (typeof window === \"undefined\") return \"\";\n\n return findWritableCookieDomain(consentDomain || window.location.host);\n}\n\nfunction setCookie(name: string, value: string, maxAgeInSeconds: number, domain: string): void {\n const parts = [`${name}=${encodeURIComponent(value)}`];\n parts.push(`max-age=${maxAgeInSeconds}`);\n parts.push(\"path=/\");\n parts.push(\"SameSite=Lax\");\n if (domain) parts.push(`domain=${domain}`);\n document.cookie = parts.join(\"; \");\n}\n\n/**\n * @deprecated Sets or removes the legacy `_shopify_y` and `_shopify_s` cookies\n * based on the current consent state and tracking values.\n */\nfunction updateCookies(hasConsent: boolean, domain: string): void {\n if (hasConsent) {\n const trackingValues = getTrackingValues();\n const tokenValue = trackingValues.uniqueToken || trackingValues.visitToken || \"\";\n if (tokenValue.startsWith(\"00000000-\")) return;\n\n setCookie(\n SHOPIFY_Y,\n trackingValues.uniqueToken || buildUUID(),\n LONG_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n setCookie(\n SHOPIFY_S,\n trackingValues.visitToken || buildUUID(),\n SHORT_TERM_EXPIRY_IN_SECONDS,\n domain,\n );\n } else {\n setCookie(SHOPIFY_Y, \"\", 0, domain);\n setCookie(SHOPIFY_S, \"\", 0, domain);\n }\n}\n\nfunction hasAnalyticsConsent(): boolean {\n try {\n const privacy = window.Shopify?.customerPrivacy;\n return privacy?.analyticsProcessingAllowed?.() ?? false;\n } catch {\n return false;\n }\n}\n\nfunction isConsentLoaded(): boolean {\n return window.Shopify?.customerPrivacy?.consentStatus === \"loaded\";\n}\n\nfunction getConsentDomain(): string | undefined {\n return window.Shopify?.customerPrivacy?.config?.consentDomain;\n}\n\nlet didInitializeDeprecatedCookies = false;\nconst noOpCleanup = () => {};\n\n/**\n * @deprecated Starts page-lifetime legacy cookie management from ShopifyScripts.\n */\nexport function initializeDeprecatedCookies(): () => void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return noOpCleanup;\n if (didInitializeDeprecatedCookies) return noOpCleanup;\n\n didInitializeDeprecatedCookies = true;\n const domain = computeCookieDomain(getConsentDomain());\n\n const syncConsent = () => {\n updateCookies(hasAnalyticsConsent(), domain);\n };\n\n document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, syncConsent);\n if (isConsentLoaded()) syncConsent();\n\n const syncPageView = () => {\n if (hasAnalyticsConsent()) {\n updateCookies(true, domain);\n }\n };\n\n const subscribeToAnalytics = () => {\n const subscribe = window.Shopify?.analytics?.subscribe;\n if (typeof subscribe !== \"function\") return false;\n\n subscribe(\"page_viewed\", syncPageView);\n return true;\n };\n\n if (!subscribeToAnalytics() && document.readyState === \"loading\") {\n // The bus is inline today, but if it moves to a module/defer script it will\n // still be available before DOMContentLoaded.\n document.addEventListener(\"DOMContentLoaded\", subscribeToAnalytics, { once: true });\n }\n\n return noOpCleanup;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,YAAY;AAClB,MAAM,YAAY;AAElB,MAAM,8BAA8B,OAAU,KAAK;AACnD,MAAM,+BAA+B;;;;;;;;AASrC,SAAgB,oBAAoB,eAAgC;AAClE,KAAI,OAAO,WAAW,YAAa,QAAO;AAE1C,QAAO,yBAAyB,iBAAiB,OAAO,SAAS,KAAK;;AAGxE,SAAS,UAAU,MAAc,OAAe,iBAAyB,QAAsB;CAC7F,MAAM,QAAQ,CAAC,GAAG,KAAK,GAAG,mBAAmB,MAAM,GAAG;AACtD,OAAM,KAAK,WAAW,kBAAkB;AACxC,OAAM,KAAK,SAAS;AACpB,OAAM,KAAK,eAAe;AAC1B,KAAI,OAAQ,OAAM,KAAK,UAAU,SAAS;AAC1C,UAAS,SAAS,MAAM,KAAK,KAAK;;;;;;AAOpC,SAAS,cAAc,YAAqB,QAAsB;AAChE,KAAI,YAAY;EACd,MAAM,iBAAiB,mBAAmB;AAE1C,OADmB,eAAe,eAAe,eAAe,cAAc,IAC/D,WAAW,YAAY,CAAE;AAExC,YACE,WACA,eAAe,eAAe,WAAW,EACzC,6BACA,OACD;AACD,YACE,WACA,eAAe,cAAc,WAAW,EACxC,8BACA,OACD;QACI;AACL,YAAU,WAAW,IAAI,GAAG,OAAO;AACnC,YAAU,WAAW,IAAI,GAAG,OAAO;;;AAIvC,SAAS,sBAA+B;AACtC,KAAI;AAEF,UADgB,OAAO,SAAS,kBAChB,8BAA8B,IAAI;SAC5C;AACN,SAAO;;;AAIX,SAAS,kBAA2B;AAClC,QAAO,OAAO,SAAS,iBAAiB,kBAAkB;;AAG5D,SAAS,mBAAuC;AAC9C,QAAO,OAAO,SAAS,iBAAiB,QAAQ;;AAGlD,IAAI,iCAAiC;AACrC,MAAM,oBAAoB;;;;AAK1B,SAAgB,8BAA0C;AACxD,KAAI,OAAO,WAAW,eAAe,OAAO,aAAa,YAAa,QAAO;AAC7E,KAAI,+BAAgC,QAAO;AAE3C,kCAAiC;CACjC,MAAM,SAAS,oBAAoB,kBAAkB,CAAC;CAEtD,MAAM,oBAAoB;AACxB,gBAAc,qBAAqB,EAAE,OAAO;;AAG9C,UAAS,iBAAiB,iCAAiC,YAAY;AACvE,KAAI,iBAAiB,CAAE,cAAa;CAEpC,MAAM,qBAAqB;AACzB,MAAI,qBAAqB,CACvB,eAAc,MAAM,OAAO;;CAI/B,MAAM,6BAA6B;EACjC,MAAM,YAAY,OAAO,SAAS,WAAW;AAC7C,MAAI,OAAO,cAAc,WAAY,QAAO;AAE5C,YAAU,eAAe,aAAa;AACtC,SAAO;;AAGT,KAAI,CAAC,sBAAsB,IAAI,SAAS,eAAe,UAGrD,UAAS,iBAAiB,oBAAoB,sBAAsB,EAAE,MAAM,MAAM,CAAC;AAGrF,QAAO"}
@@ -1,43 +0,0 @@
1
- //#region src/core/shopify-scripts/utils/cookie-domain.ts
2
- function normalizeHost(host) {
3
- return host.replace(/^https?:\/\//, "").split("/")[0]?.split(":")[0]?.replace(/^\./, "").toLowerCase() ?? "";
4
- }
5
- function isIpAddress(host) {
6
- return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
7
- }
8
- function canUseDomainCookie(domain) {
9
- if (typeof document === "undefined") return false;
10
- const name = `hydrogen_domain_probe_${Math.random().toString(36).slice(2)}`;
11
- const nameValuePair = `${name}=1`;
12
- const cookie = `${nameValuePair}; domain=${domain}; path=/; SameSite=Lax`;
13
- const cleanup = `${name}=; domain=${domain}; path=/; max-age=0; SameSite=Lax`;
14
- try {
15
- document.cookie = cookie;
16
- const found = document.cookie.split(";").some((part) => part.trim() === nameValuePair);
17
- document.cookie = cleanup;
18
- return found;
19
- } catch {
20
- return false;
21
- }
22
- }
23
- /**
24
- * Finds the broadest wildcard cookie domain the current document can set for a
25
- * host. Browsers reject public suffixes like `.com` and `.co.uk`, so probing is
26
- * the only reliable way to find the earliest usable wildcard without shipping a
27
- * public suffix list.
28
- */
29
- function findWritableCookieDomain(host, probe = canUseDomainCookie) {
30
- const normalizedHost = normalizeHost(host);
31
- if (!normalizedHost || normalizedHost === "localhost" || isIpAddress(normalizedHost)) return "";
32
- const parts = normalizedHost.split(".").filter(Boolean);
33
- if (parts.length < 2) return "";
34
- for (let index = parts.length - 2; index >= 0; index--) {
35
- const domain = `.${parts.slice(index).join(".")}`;
36
- if (probe(domain)) return domain;
37
- }
38
- return "";
39
- }
40
-
41
- //#endregion
42
- export { findWritableCookieDomain };
43
- //# sourceMappingURL=cookie-domain.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cookie-domain.mjs","names":[],"sources":["../../../../src/core/shopify-scripts/utils/cookie-domain.ts"],"sourcesContent":["function normalizeHost(host: string): string {\n const withoutProtocol = host.replace(/^https?:\\/\\//, \"\");\n return withoutProtocol.split(\"/\")[0]?.split(\":\")[0]?.replace(/^\\./, \"\").toLowerCase() ?? \"\";\n}\n\nfunction isIpAddress(host: string): boolean {\n return /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host) || host.includes(\":\");\n}\n\nfunction canUseDomainCookie(domain: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const name = `hydrogen_domain_probe_${Math.random().toString(36).slice(2)}`;\n const value = \"1\";\n const nameValuePair = `${name}=${value}`;\n const cookie = `${nameValuePair}; domain=${domain}; path=/; SameSite=Lax`;\n const cleanup = `${name}=; domain=${domain}; path=/; max-age=0; SameSite=Lax`;\n\n try {\n document.cookie = cookie;\n const found = document.cookie.split(\";\").some((part) => part.trim() === nameValuePair);\n document.cookie = cleanup;\n return found;\n } catch {\n return false;\n }\n}\n\n/**\n * Finds the broadest wildcard cookie domain the current document can set for a\n * host. Browsers reject public suffixes like `.com` and `.co.uk`, so probing is\n * the only reliable way to find the earliest usable wildcard without shipping a\n * public suffix list.\n */\nexport function findWritableCookieDomain(\n host: string,\n probe: (domain: string) => boolean = canUseDomainCookie,\n): string {\n const normalizedHost = normalizeHost(host);\n if (!normalizedHost || normalizedHost === \"localhost\" || isIpAddress(normalizedHost)) return \"\";\n\n const parts = normalizedHost.split(\".\").filter(Boolean);\n if (parts.length < 2) return \"\";\n\n for (let index = parts.length - 2; index >= 0; index--) {\n const domain = `.${parts.slice(index).join(\".\")}`;\n if (probe(domain)) return domain;\n }\n\n return \"\";\n}\n"],"mappings":";AAAA,SAAS,cAAc,MAAsB;AAE3C,QADwB,KAAK,QAAQ,gBAAgB,GAC/B,CAAC,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,QAAQ,OAAO,GAAG,CAAC,aAAa,IAAI;;AAG3F,SAAS,YAAY,MAAuB;AAC1C,QAAO,4BAA4B,KAAK,KAAK,IAAI,KAAK,SAAS,IAAI;;AAGrE,SAAS,mBAAmB,QAAyB;AACnD,KAAI,OAAO,aAAa,YAAa,QAAO;CAE5C,MAAM,OAAO,yBAAyB,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,EAAE;CAEzE,MAAM,gBAAgB,GAAG,KAAK;CAC9B,MAAM,SAAS,GAAG,cAAc,WAAW,OAAO;CAClD,MAAM,UAAU,GAAG,KAAK,YAAY,OAAO;AAE3C,KAAI;AACF,WAAS,SAAS;EAClB,MAAM,QAAQ,SAAS,OAAO,MAAM,IAAI,CAAC,MAAM,SAAS,KAAK,MAAM,KAAK,cAAc;AACtF,WAAS,SAAS;AAClB,SAAO;SACD;AACN,SAAO;;;;;;;;;AAUX,SAAgB,yBACd,MACA,QAAqC,oBAC7B;CACR,MAAM,iBAAiB,cAAc,KAAK;AAC1C,KAAI,CAAC,kBAAkB,mBAAmB,eAAe,YAAY,eAAe,CAAE,QAAO;CAE7F,MAAM,QAAQ,eAAe,MAAM,IAAI,CAAC,OAAO,QAAQ;AACvD,KAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,MAAK,IAAI,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;EACtD,MAAM,SAAS,IAAI,MAAM,MAAM,MAAM,CAAC,KAAK,IAAI;AAC/C,MAAI,MAAM,OAAO,CAAE,QAAO;;AAG5B,QAAO"}
@@ -1,65 +0,0 @@
1
- //#region src/core/shopify-scripts/utils/tracking-values.ts
2
- const cachedTrackingValues = { current: null };
3
- function getTrackingValues() {
4
- let trackingValues;
5
- if (typeof window !== "undefined" && typeof window.performance !== "undefined") try {
6
- const resourceRE = /^https?:\/\/([^/]+)(\/api\/(?:unstable|2\d{3}-\d{2})\/graphql\.json(?=$|\?))?/;
7
- const entries = performance.getEntriesByType("resource");
8
- let matchedValues;
9
- for (let i = entries.length - 1; i >= 0; i--) {
10
- const entry = entries[i];
11
- if (entry.initiatorType !== "fetch") continue;
12
- const currentHost = window.location.host;
13
- const match = entry.name.match(resourceRE);
14
- if (!match) continue;
15
- const [, matchedHost, sfapiPath] = match;
16
- if (matchedHost === currentHost || sfapiPath && matchedHost?.endsWith(`.${currentHost}`)) {
17
- const values = extractFromPerformanceEntry(entry);
18
- if (values) {
19
- matchedValues = values;
20
- break;
21
- }
22
- }
23
- }
24
- if (matchedValues) trackingValues = matchedValues;
25
- if (trackingValues) cachedTrackingValues.current = trackingValues;
26
- else if (cachedTrackingValues.current) trackingValues = cachedTrackingValues.current;
27
- if (!trackingValues) {
28
- const navigationEntries = performance.getEntriesByType("navigation")[0];
29
- trackingValues = extractFromPerformanceEntry(navigationEntries, false);
30
- }
31
- } catch {}
32
- if (!trackingValues) {
33
- const cookie = typeof document !== "undefined" ? document.cookie : "";
34
- trackingValues = {
35
- uniqueToken: cookie.match(/\b_shopify_y=([^;]+)/)?.[1] || "",
36
- visitToken: cookie.match(/\b_shopify_s=([^;]+)/)?.[1] || "",
37
- consent: cookie.match(/\b_tracking_consent=([^;]+)/)?.[1] || ""
38
- };
39
- }
40
- return trackingValues;
41
- }
42
- function extractFromPerformanceEntry(entry, isConsentRequired = true) {
43
- let uniqueToken = "";
44
- let visitToken = "";
45
- let consent = "";
46
- const serverTiming = entry.serverTiming;
47
- const minimumEntries = isConsentRequired ? 3 : 2;
48
- if (serverTiming && serverTiming.length >= minimumEntries) for (let i = serverTiming.length - 1; i >= 0; i--) {
49
- const { name, description } = serverTiming[i];
50
- if (!name || !description) continue;
51
- if (name === "_y") uniqueToken = description;
52
- else if (name === "_s") visitToken = description;
53
- else if (name === "_cmp") consent = description;
54
- if (uniqueToken && visitToken && consent) break;
55
- }
56
- return uniqueToken && visitToken && (isConsentRequired ? consent : true) ? {
57
- uniqueToken,
58
- visitToken,
59
- consent
60
- } : void 0;
61
- }
62
-
63
- //#endregion
64
- export { getTrackingValues };
65
- //# sourceMappingURL=tracking-values.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tracking-values.mjs","names":[],"sources":["../../../../src/core/shopify-scripts/utils/tracking-values.ts"],"sourcesContent":["/**\n * Inlined from @shopify/hydrogen-react/tracking-utils.\n * Retrieves user session tracking values from Server-Timing headers\n * and the Performance API.\n */\n\ntype TrackingValues = {\n uniqueToken: string;\n visitToken: string;\n consent: string;\n};\n\nconst cachedTrackingValues: { current: null | TrackingValues } = { current: null };\n\nexport function getTrackingValues(): TrackingValues {\n // Tracking values are returned in Server-Timing headers from SFAPI responses.\n // Prefer recent fetch/resource entries, then cached values, then the initial\n // navigation entry, and finally the deprecated JS-visible cookies.\n let trackingValues: TrackingValues | undefined;\n\n if (typeof window !== \"undefined\" && typeof window.performance !== \"undefined\") {\n try {\n // Extract host and optionally match the SFAPI GraphQL pathname.\n const resourceRE =\n /^https?:\\/\\/([^/]+)(\\/api\\/(?:unstable|2\\d{3}-\\d{2})\\/graphql\\.json(?=$|\\?))?/;\n\n // Search backwards so the latest matching fetch with complete tracking\n // values wins. Same-origin fetches cover Hydrogen proxy/server responses;\n // subdomain SFAPI requests cover direct Storefront API calls.\n const entries = performance.getEntriesByType(\"resource\") as PerformanceResourceTiming[];\n\n let matchedValues: TrackingValues | undefined;\n\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n if (entry.initiatorType !== \"fetch\") continue;\n\n const currentHost = window.location.host;\n const match = entry.name.match(resourceRE);\n if (!match) continue;\n\n const [, matchedHost, sfapiPath] = match;\n const isMatch =\n matchedHost === currentHost || (sfapiPath && matchedHost?.endsWith(`.${currentHost}`));\n\n if (isMatch) {\n const values = extractFromPerformanceEntry(entry);\n if (values) {\n matchedValues = values;\n break;\n }\n }\n }\n\n if (matchedValues) {\n trackingValues = matchedValues;\n }\n\n // Resource entries have a limited buffer and can be removed over time.\n // A cached resource entry is newer than the navigation entry.\n if (trackingValues) {\n cachedTrackingValues.current = trackingValues;\n } else if (cachedTrackingValues.current) {\n trackingValues = cachedTrackingValues.current;\n }\n\n if (!trackingValues) {\n // The document navigation entry can omit _cmp when the server generated\n // tracking tokens, so only _y and _s are required for this fallback.\n const navigationEntries = performance.getEntriesByType(\n \"navigation\",\n )[0] as PerformanceNavigationTiming;\n trackingValues = extractFromPerformanceEntry(navigationEntries, false);\n }\n } catch {}\n }\n\n if (!trackingValues) {\n const cookie = typeof document !== \"undefined\" ? document.cookie : \"\";\n\n trackingValues = {\n uniqueToken: cookie.match(/\\b_shopify_y=([^;]+)/)?.[1] || \"\",\n visitToken: cookie.match(/\\b_shopify_s=([^;]+)/)?.[1] || \"\",\n consent: cookie.match(/\\b_tracking_consent=([^;]+)/)?.[1] || \"\",\n };\n }\n\n return trackingValues;\n}\n\nfunction extractFromPerformanceEntry(\n entry: PerformanceNavigationTiming | PerformanceResourceTiming,\n isConsentRequired = true,\n): TrackingValues | undefined {\n let uniqueToken = \"\";\n let visitToken = \"\";\n let consent = \"\";\n\n const serverTiming = entry.serverTiming;\n const minimumEntries = isConsentRequired ? 3 : 2;\n if (serverTiming && serverTiming.length >= minimumEntries) {\n // Iterate backwards since Shopify tracking metrics are usually appended\n // after framework/server metrics.\n for (let i = serverTiming.length - 1; i >= 0; i--) {\n const { name, description } = serverTiming[i];\n if (!name || !description) continue;\n\n if (name === \"_y\") uniqueToken = description;\n else if (name === \"_s\") visitToken = description;\n else if (name === \"_cmp\") {\n // Consent value used by consent-tracking-api and privacy-banner scripts.\n consent = description;\n }\n\n if (uniqueToken && visitToken && consent) break;\n }\n }\n\n return uniqueToken && visitToken && (isConsentRequired ? consent : true)\n ? { uniqueToken, visitToken, consent }\n : undefined;\n}\n"],"mappings":";AAYA,MAAM,uBAA2D,EAAE,SAAS,MAAM;AAElF,SAAgB,oBAAoC;CAIlD,IAAI;AAEJ,KAAI,OAAO,WAAW,eAAe,OAAO,OAAO,gBAAgB,YACjE,KAAI;EAEF,MAAM,aACJ;EAKF,MAAM,UAAU,YAAY,iBAAiB,WAAW;EAExD,IAAI;AAEJ,OAAK,IAAI,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;GAC5C,MAAM,QAAQ,QAAQ;AACtB,OAAI,MAAM,kBAAkB,QAAS;GAErC,MAAM,cAAc,OAAO,SAAS;GACpC,MAAM,QAAQ,MAAM,KAAK,MAAM,WAAW;AAC1C,OAAI,CAAC,MAAO;GAEZ,MAAM,GAAG,aAAa,aAAa;AAInC,OAFE,gBAAgB,eAAgB,aAAa,aAAa,SAAS,IAAI,cAAc,EAE1E;IACX,MAAM,SAAS,4BAA4B,MAAM;AACjD,QAAI,QAAQ;AACV,qBAAgB;AAChB;;;;AAKN,MAAI,cACF,kBAAiB;AAKnB,MAAI,eACF,sBAAqB,UAAU;WACtB,qBAAqB,QAC9B,kBAAiB,qBAAqB;AAGxC,MAAI,CAAC,gBAAgB;GAGnB,MAAM,oBAAoB,YAAY,iBACpC,aACD,CAAC;AACF,oBAAiB,4BAA4B,mBAAmB,MAAM;;SAElE;AAGV,KAAI,CAAC,gBAAgB;EACnB,MAAM,SAAS,OAAO,aAAa,cAAc,SAAS,SAAS;AAEnE,mBAAiB;GACf,aAAa,OAAO,MAAM,uBAAuB,GAAG,MAAM;GAC1D,YAAY,OAAO,MAAM,uBAAuB,GAAG,MAAM;GACzD,SAAS,OAAO,MAAM,8BAA8B,GAAG,MAAM;GAC9D;;AAGH,QAAO;;AAGT,SAAS,4BACP,OACA,oBAAoB,MACQ;CAC5B,IAAI,cAAc;CAClB,IAAI,aAAa;CACjB,IAAI,UAAU;CAEd,MAAM,eAAe,MAAM;CAC3B,MAAM,iBAAiB,oBAAoB,IAAI;AAC/C,KAAI,gBAAgB,aAAa,UAAU,eAGzC,MAAK,IAAI,IAAI,aAAa,SAAS,GAAG,KAAK,GAAG,KAAK;EACjD,MAAM,EAAE,MAAM,gBAAgB,aAAa;AAC3C,MAAI,CAAC,QAAQ,CAAC,YAAa;AAE3B,MAAI,SAAS,KAAM,eAAc;WACxB,SAAS,KAAM,cAAa;WAC5B,SAAS,OAEhB,WAAU;AAGZ,MAAI,eAAe,cAAc,QAAS;;AAI9C,QAAO,eAAe,eAAe,oBAAoB,UAAU,QAC/D;EAAE;EAAa;EAAY;EAAS,GACpC"}