@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
@@ -3,23 +3,81 @@ import { ShopifyScriptsShop } from "../shopify-scripts/types.mjs";
3
3
 
4
4
  //#region src/core/analytics/types.d.ts
5
5
  type ShopAnalyticsBase = Pick<ShopifyScriptsShop, "shopId">;
6
+ /** Discriminant for the analytics channel a storefront is running under. */
6
7
  type ShopAnalyticsChannel = "hydrogen" | "headless";
8
+ /**
9
+ * Identifies the shop and channel for every analytics payload.
10
+ *
11
+ * The `"hydrogen"` variant carries the Hydrogen sales channel's `storefrontId`;
12
+ * the `"headless"` variant omits it, because analytics for the Headless channel
13
+ * isn't attributed to a specific storefront.
14
+ */
7
15
  type ShopAnalytics = (ShopAnalyticsBase & {
8
- channel: "hydrogen";
16
+ /** The storefront is served by the Hydrogen sales channel. */channel: "hydrogen"; /** ID of the Hydrogen storefront that analytics are attributed to. */
9
17
  storefrontId: ShopifyScriptsShop["storefrontId"];
10
18
  }) | (ShopAnalyticsBase & {
11
- channel: "headless";
19
+ /** The storefront uses the generic Headless sales channel. */channel: "headless";
12
20
  storefrontId?: never;
13
21
  });
22
+ type ConsentPreferences = {
23
+ analytics: boolean;
24
+ marketing: boolean;
25
+ preferences: boolean;
26
+ sale_of_data: boolean;
27
+ };
28
+ /**
29
+ * Connects a consent provider once per analytics bus, after Shopify's consent API has loaded.
30
+ * Use window.Shopify.customerPrivacy to synchronize consent.
31
+ * Resolve only after the provider has a saved choice or the shopper interacts, and that
32
+ * consent has been synchronized with Shopify. Hydrogen then checks consent and replays
33
+ * allowed events. Rejection keeps delivery blocked. The integration stays active
34
+ * across component unmounts.
35
+ */
36
+ type ConsentSetup = () => Promise<void>;
37
+ /**
38
+ * Chooses which Shopify consent script `ShopifyScripts` loads and when the
39
+ * analytics bus releases events to destinations.
40
+ *
41
+ * - `"default-banner"` loads Shopify's hosted privacy banner. If the visitor
42
+ * must see the banner, destinations wait until they accept or decline;
43
+ * otherwise events are released as soon as the consent API loads.
44
+ * - `"custom-banner"` loads only the Customer Privacy API and requires a
45
+ * `setup` callback that integrates a third-party consent provider.
46
+ * Events release once the `setup` callback resolves.
47
+ * - `"no-banner"` (or omitted) loads only the Customer Privacy API and
48
+ * releases events once it loads.
49
+ *
50
+ * In every mode, destinations only receive events while analytics processing is
51
+ * allowed.
52
+ */
14
53
  type ConsentConfig = {
15
- mode?: "default-banner" | "custom-banner" | "no-banner";
54
+ mode?: "no-banner";
55
+ setup?: never;
56
+ } | {
57
+ mode: "default-banner";
58
+ setup?: never;
59
+ } | {
60
+ mode: "custom-banner";
61
+ setup: ConsentSetup;
16
62
  };
63
+ /**
64
+ * Lightweight cart line shape for analytics payloads.
65
+ *
66
+ * Mirrors the Storefront API `CartLine` fields needed for tracking without
67
+ * depending on the cart store's `CartData` type, so the analytics bus stays
68
+ * framework-agnostic.
69
+ */
17
70
  type AnalyticsCartLine = {
18
71
  id: string;
19
72
  quantity: number;
20
73
  merchandise: {
21
74
  id: string;
22
- title: string;
75
+ /**
76
+ * Variant title (`"Default Title"` for single-variant products).
77
+ * `trackCartAnalytics` falls back to the product title only when the cart
78
+ * fragment doesn't select it.
79
+ */
80
+ title: string; /** Per-unit price (`CartLine.cost.amountPerQuantity`). */
23
81
  price: {
24
82
  amount: string;
25
83
  currencyCode?: string;
@@ -34,8 +92,17 @@ type AnalyticsCartLine = {
34
92
  };
35
93
  };
36
94
  };
95
+ /**
96
+ * Lightweight cart snapshot included in cart analytics payloads.
97
+ *
98
+ * Accepts both `nodes` and `edges` connection shapes so the analytics layer
99
+ * works regardless of which Storefront API query pattern the storefront uses.
100
+ */
37
101
  type AnalyticsCart = {
38
102
  id: string;
103
+ /**
104
+ * `Cart.updatedAt` (ISO 8601), used to deduplicate cart events.
105
+ */
39
106
  updatedAt: string;
40
107
  cost?: {
41
108
  subtotalAmount?: {
@@ -44,7 +111,7 @@ type AnalyticsCart = {
44
111
  totalAmount?: {
45
112
  currencyCode?: string;
46
113
  };
47
- };
114
+ }; /** Cart lines in Storefront API connection format. */
48
115
  lines: {
49
116
  nodes?: AnalyticsCartLine[];
50
117
  edges?: Array<{
@@ -53,6 +120,7 @@ type AnalyticsCart = {
53
120
  };
54
121
  [key: string]: unknown;
55
122
  };
123
+ /** Index signature that allows arbitrary extra keys on a payload. */
56
124
  type OtherData = {
57
125
  [key: string]: unknown;
58
126
  };
@@ -63,9 +131,10 @@ type BasePayload = {
63
131
  type UrlPayload = {
64
132
  url?: string;
65
133
  };
134
+ /** Describes a single product for `product_viewed` payloads. */
66
135
  type ProductPayload = {
67
136
  id: string;
68
- title: string;
137
+ title: string; /** Decimal amount string for the selected variant (e.g. `"19.99"`). */
69
138
  price: string;
70
139
  vendor: string;
71
140
  variantId: string;
@@ -97,14 +166,30 @@ type CartLinePayload = {
97
166
  prevLine?: AnalyticsCartLine;
98
167
  currentLine?: AnalyticsCartLine;
99
168
  };
169
+ /** Payload for `page_viewed` events. */
100
170
  type PageViewPayload = UrlPayload & BasePayload;
171
+ /** Payload for `product_viewed` events. */
101
172
  type ProductViewPayload = ProductsPayload & UrlPayload & BasePayload;
173
+ /** Payload for `collection_viewed` events. */
102
174
  type CollectionViewPayload = CollectionPayload & UrlPayload & BasePayload;
175
+ /** Payload for `cart_viewed` events. */
103
176
  type CartViewPayload = CartPayload & UrlPayload & BasePayload;
177
+ /** Payload for `search_viewed` events. */
104
178
  type SearchViewPayload = SearchPayload & UrlPayload & BasePayload;
179
+ /** Payload for `cart_updated` events. `prevCart` is `null` when there was no earlier snapshot. */
105
180
  type CartUpdatePayload = CartChangePayload & BasePayload & OtherData;
181
+ /**
182
+ * Payload for `product_added_to_cart` and `product_removed_from_cart` events.
183
+ * New lines have only `currentLine`, removed lines have only `prevLine`, and
184
+ * quantity changes have both.
185
+ */
106
186
  type CartLineUpdatePayload = CartLinePayload & CartChangePayload & BasePayload & OtherData;
107
- type EventPayloads = PageViewPayload | ProductViewPayload | CollectionViewPayload | CartViewPayload | SearchViewPayload | CartUpdatePayload | CartLineUpdatePayload;
187
+ /**
188
+ * Maps each analytics event name to its payload type.
189
+ *
190
+ * TypeScript uses this to infer the correct payload when you call
191
+ * `publish()` or a destination's `subscribe()` with a specific event name.
192
+ */
108
193
  interface AnalyticsEventMap {
109
194
  page_viewed: PageViewPayload;
110
195
  product_viewed: ProductViewPayload;
@@ -118,26 +203,77 @@ interface AnalyticsEventMap {
118
203
  /** Resolves the payload type for a supported analytics event name. */
119
204
  type PayloadFor<E extends AnalyticsEventName> = AnalyticsEventMap[E];
120
205
  type PublishPayloadArgs<E extends AnalyticsEventName> = {} extends PayloadFor<E> ? [payload?: PayloadFor<E>] : [payload: PayloadFor<E>];
206
+ /**
207
+ * Analytics bus configuration, built by `ShopifyScripts` from its `shop`,
208
+ * `consent`, and `analytics` options.
209
+ */
121
210
  type StorefrontAnalyticsConfig = {
122
- shop: ShopAnalytics | null;
123
- consent: ConsentConfig;
211
+ /** Shop identity and channel for analytics payloads. */shop: ShopAnalytics | null; /** Serializable consent settings. The provider setup callback stays in the client bundle. */
212
+ consent: Pick<ConsentConfig, "mode">;
213
+ /**
214
+ * Extra key-value pairs accessible to destinations via `getConfig().customData`.
215
+ * Not added to events you publish.
216
+ */
124
217
  customData?: Record<string, unknown>;
125
218
  };
219
+ /**
220
+ * Visitor identifiers from Shopify's Customer Privacy API, used to correlate
221
+ * analytics events. See `getTrackingValues` for when they are empty.
222
+ */
223
+ type AnalyticsTrackingValues = {
224
+ /** Long-lived browser identifier; persists across visits. */uniqueToken: string; /** Session identifier (30-minute rolling expiry). */
225
+ visitToken: string;
226
+ };
227
+ /** Context provided to destination callbacks alongside each event payload. */
228
+ type StorefrontAnalyticsDestinationEventContext = {
229
+ /**
230
+ * Reads current tokens from the consent API. Returns empty strings when tokens are
231
+ * unavailable or analytics tracking is not currently allowed.
232
+ */
233
+ getTrackingValues: () => AnalyticsTrackingValues;
234
+ };
235
+ /** Context passed into a destination's `setup()` function. */
126
236
  type StorefrontAnalyticsDestinationSetupContext = {
127
- subscribe: <E extends AnalyticsEventName>(event: E, callback: (payload: PayloadFor<E>) => void) => () => void;
237
+ /** Subscribe to an analytics event. Returns an unsubscribe function. */subscribe: <E extends AnalyticsEventName>(event: E, callback: (payload: PayloadFor<E>, context: StorefrontAnalyticsDestinationEventContext) => void) => () => void; /** Returns the current analytics bus configuration. */
128
238
  getConfig: () => StorefrontAnalyticsConfig;
129
239
  };
240
+ /**
241
+ * A consent-gated analytics integration that receives events after consent is granted.
242
+ *
243
+ * Destinations subscribe to events during `setup()` and receive live delivery
244
+ * plus replayed buffered events once tracking is allowed. Return a cleanup
245
+ * function from `setup()` to tear down side effects when the destination is
246
+ * removed via the function returned by `addDestination()`. The bus itself lives
247
+ * for the page's lifetime.
248
+ */
130
249
  type StorefrontAnalyticsDestination = {
131
- name: string;
250
+ /** Unique name for this destination — duplicates are rejected with a warning. */name: string;
251
+ /**
252
+ * Runs immediately when the destination is added, before consent is known, so
253
+ * don't load third-party scripts or set cookies here. Subscribe via the context.
254
+ * May be async: buffered events replay after it resolves (once tracking is
255
+ * allowed). If it throws or rejects, the destination is removed.
256
+ */
132
257
  setup: (context: StorefrontAnalyticsDestinationSetupContext) => void | (() => void) | Promise<void | (() => void)>;
133
258
  };
259
+ /**
260
+ * The framework-agnostic analytics event bus that `ShopifyScripts` exposes on
261
+ * `window.Shopify.analytics`.
262
+ *
263
+ * Storefronts publish events (page views, product views, cart changes) via
264
+ * `publish()`. Consent-gated tracking integrations register through
265
+ * `addDestination()` and receive buffered events once consent is granted.
266
+ */
134
267
  type StorefrontAnalytics = {
135
- publish: <E extends AnalyticsEventName>(event: E, ...payload: PublishPayloadArgs<E>) => void; /** Listen for live analytics events. Destination integrations should use `addDestination()`. */
136
- subscribe: <E extends AnalyticsEventName>(event: E, callback: (payload: PayloadFor<E>) => void) => () => void;
137
- addDestination: (destination: StorefrontAnalyticsDestination) => () => void;
138
- destroy: () => void;
268
+ /**
269
+ * Emits an event to destinations while tracking is allowed. Otherwise the
270
+ * event goes into a bounded replay buffer. Fills in `shop` and (for view
271
+ * events) `url` when omitted.
272
+ */
273
+ publish: <E extends AnalyticsEventName>(event: E, ...payload: PublishPayloadArgs<E>) => void; /** Register a consent-gated destination integration. Returns a removal function. */
274
+ addDestination: (destination: StorefrontAnalyticsDestination) => () => void; /** Returns the bus configuration; `shop.shopId` is normalized to a Shop GID. */
139
275
  getConfig: () => StorefrontAnalyticsConfig;
140
276
  };
141
277
  //#endregion
142
- export { AnalyticsCart, AnalyticsCartLine, AnalyticsEventMap, CartLineUpdatePayload, CartUpdatePayload, CartViewPayload, CollectionViewPayload, ConsentConfig, EventPayloads, OtherData, PageViewPayload, PayloadFor, ProductPayload, ProductViewPayload, SearchViewPayload, ShopAnalytics, ShopAnalyticsChannel, StorefrontAnalytics, StorefrontAnalyticsConfig, StorefrontAnalyticsDestination, StorefrontAnalyticsDestinationSetupContext };
278
+ export { AnalyticsCart, AnalyticsCartLine, AnalyticsEventMap, AnalyticsTrackingValues, CartLineUpdatePayload, CartUpdatePayload, CartViewPayload, CollectionViewPayload, ConsentConfig, ConsentPreferences, ConsentSetup, OtherData, PageViewPayload, PayloadFor, ProductPayload, ProductViewPayload, SearchViewPayload, ShopAnalytics, ShopAnalyticsChannel, StorefrontAnalytics, StorefrontAnalyticsConfig, StorefrontAnalyticsDestination, StorefrontAnalyticsDestinationEventContext, StorefrontAnalyticsDestinationSetupContext };
143
279
  //# sourceMappingURL=types.d.mts.map
@@ -1,20 +1,37 @@
1
1
  //#region src/core/cart/actions.d.ts
2
+ /** A key-value pair attached to the cart or an individual cart line. */
2
3
  type CartAttributeInput = {
3
4
  key: string;
4
5
  value: string;
5
6
  };
6
7
  type CartLineAddInput = {
7
- merchandiseId: string;
8
+ /** Storefront API GID of the product variant to add. */merchandiseId: string;
8
9
  quantity: number;
9
- attributes?: CartAttributeInput[];
10
+ attributes?: CartAttributeInput[]; /** Selling plan GID for subscription line items. */
10
11
  sellingPlanId?: string;
11
12
  };
13
+ /** Input for updating an existing cart line. Setting `quantity` to `0` removes the line. */
12
14
  type CartLineUpdateInput = {
13
- id: string;
15
+ /** The `CartLine.id` of the line to update. */id: string;
14
16
  quantity: number;
15
17
  attributes?: CartAttributeInput[];
16
18
  sellingPlanId?: string;
17
19
  };
20
+ /**
21
+ * Discriminated union of all cart mutation intents.
22
+ *
23
+ * {@link parseCartRequest} normalizes both JSON and FormData requests into one
24
+ * of these variants. The `intent` field determines the Storefront API mutation:
25
+ *
26
+ * - `"add"` — add new lines (cartLinesAdd, or cartCreate when no cart exists)
27
+ * - `"update"` — change quantity or attributes on existing lines (cartLinesUpdate)
28
+ * - `"remove"` — remove lines by ID (cartLinesRemove)
29
+ * - `"discount-update"` — replace all discount codes (cartDiscountCodesUpdate)
30
+ * - `"discount-apply"` — add a single discount code (read-then-write via cartDiscountCodesUpdate)
31
+ * - `"discount-remove"` — remove a single discount code (read-then-write via cartDiscountCodesUpdate)
32
+ * - `"attributes-update"` — set cart-level attributes (cartAttributesUpdate)
33
+ * - `"note-update"` — set the cart note (cartNoteUpdate)
34
+ */
18
35
  type CartAction = {
19
36
  intent: "add";
20
37
  lines: CartLineAddInput[];
@@ -44,6 +61,33 @@ type ParsedCartRequest = {
44
61
  action: CartAction;
45
62
  cartId: string | null;
46
63
  };
64
+ /**
65
+ * Parses an incoming cart mutation request into a typed {@link CartAction}.
66
+ *
67
+ * Accepts `application/json` and `application/x-www-form-urlencoded` /
68
+ * `multipart/form-data` content types. JSON and FormData each support a
69
+ * different subset of intents — JSON produces `discount-update` while
70
+ * FormData produces `discount-apply` / `discount-remove`.
71
+ *
72
+ * FormData requests always return `cartId: null` — the server handler
73
+ * should fall back to the cart cookie.
74
+ *
75
+ * @throws If the content-type is unsupported, the intent is unrecognized,
76
+ * or required fields are missing.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const { action, cartId } = await parseCartRequest(request);
81
+ *
82
+ * switch (action.intent) {
83
+ * case "add":
84
+ * return cartLinesAdd(cartId ?? getCartId(request), action.lines);
85
+ * case "remove":
86
+ * return cartLinesRemove(cartId ?? getCartId(request), action.lineIds);
87
+ * // ...
88
+ * }
89
+ * ```
90
+ */
47
91
  declare function parseCartRequest(request: Request): Promise<ParsedCartRequest>;
48
92
  //#endregion
49
93
  export { CartAction, CartAttributeInput, CartLineAddInput, CartLineUpdateInput, parseCartRequest };
@@ -8,6 +8,33 @@ var CartActionError = class extends Error {
8
8
  this.name = "CartActionError";
9
9
  }
10
10
  };
11
+ /**
12
+ * Parses an incoming cart mutation request into a typed {@link CartAction}.
13
+ *
14
+ * Accepts `application/json` and `application/x-www-form-urlencoded` /
15
+ * `multipart/form-data` content types. JSON and FormData each support a
16
+ * different subset of intents — JSON produces `discount-update` while
17
+ * FormData produces `discount-apply` / `discount-remove`.
18
+ *
19
+ * FormData requests always return `cartId: null` — the server handler
20
+ * should fall back to the cart cookie.
21
+ *
22
+ * @throws If the content-type is unsupported, the intent is unrecognized,
23
+ * or required fields are missing.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const { action, cartId } = await parseCartRequest(request);
28
+ *
29
+ * switch (action.intent) {
30
+ * case "add":
31
+ * return cartLinesAdd(cartId ?? getCartId(request), action.lines);
32
+ * case "remove":
33
+ * return cartLinesRemove(cartId ?? getCartId(request), action.lineIds);
34
+ * // ...
35
+ * }
36
+ * ```
37
+ */
11
38
  async function parseCartRequest(request) {
12
39
  const contentType = request.headers.get("content-type") ?? "";
13
40
  if (contentType.includes("application/json")) return parseJsonBody(await request.json());
@@ -182,6 +209,8 @@ function parseAddIntent(form, merchandiseId) {
182
209
  quantity
183
210
  };
184
211
  if (sellingPlanId) line.sellingPlanId = sellingPlanId;
212
+ const rawAttributes = getCartAttributeFormEntries(form);
213
+ if (rawAttributes.length > 0) line.attributes = parseAttributes(rawAttributes);
185
214
  return {
186
215
  intent: "add",
187
216
  lines: [line]
@@ -1 +1 @@
1
- {"version":3,"file":"actions.mjs","names":[],"sources":["../../../src/core/cart/actions.ts"],"sourcesContent":["import { normalizeCartId } from \"./cookie\";\nimport { getCartAttributeFormEntries } from \"./form\";\n\nexport type CartAttributeInput = { key: string; value: string };\n\nexport type CartLineAddInput = {\n merchandiseId: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartLineUpdateInput = {\n id: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartAction =\n | { intent: \"add\"; lines: CartLineAddInput[] }\n | { intent: \"update\"; lines: CartLineUpdateInput[] }\n | { intent: \"remove\"; lineIds: string[] }\n | { intent: \"discount-update\"; discountCodes: string[] }\n | { intent: \"discount-apply\"; code: string }\n | { intent: \"discount-remove\"; code: string }\n | { intent: \"attributes-update\"; attributes: CartAttributeInput[] }\n | { intent: \"note-update\"; note: string };\n\ntype ParsedCartRequest = {\n action: CartAction;\n cartId: string | null;\n};\n\nclass CartActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CartActionError\";\n }\n}\n\nexport async function parseCartRequest(request: Request): Promise<ParsedCartRequest> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n\n if (contentType.includes(\"application/json\")) {\n return parseJsonBody(await request.json());\n }\n\n if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n return { action: parseFormData(await request.formData()), cartId: null };\n }\n\n throw new CartActionError(\n `Unsupported content-type: \"${contentType}\". Expected application/json or form data.`,\n );\n}\n\n// --- JSON parsing ---\n\nfunction parseJsonBody(body: unknown): ParsedCartRequest {\n assertObject(body);\n const cartId = typeof body.cartId === \"string\" ? normalizeCartId(body.cartId) : null;\n\n if (\"note\" in body && typeof body.note === \"string\") {\n return { action: { intent: \"note-update\", note: body.note }, cartId };\n }\n\n if (\"discountCodes\" in body && Array.isArray(body.discountCodes)) {\n for (const code of body.discountCodes) {\n assertString(code, \"discountCodes entries\");\n }\n return {\n action: { intent: \"discount-update\", discountCodes: body.discountCodes as string[] },\n cartId,\n };\n }\n\n if (\"attributes\" in body && Array.isArray(body.attributes)) {\n return {\n action: { intent: \"attributes-update\", attributes: parseAttributes(body.attributes) },\n cartId,\n };\n }\n\n if (!(\"lines\" in body) || !Array.isArray(body.lines)) {\n throw new CartActionError(\n 'Request body must contain \"lines\", \"discountCodes\", \"attributes\", or \"note\".',\n );\n }\n\n const lines = body.lines as unknown[];\n if (lines.length === 0) {\n throw new CartActionError(\"Lines array must not be empty.\");\n }\n\n return { action: partitionLines(lines), cartId };\n}\n\nfunction partitionLines(rawLines: unknown[]): CartAction {\n const adds: CartLineAddInput[] = [];\n const updates: CartLineUpdateInput[] = [];\n const removeIds: string[] = [];\n\n for (const raw of rawLines) {\n assertObject(raw);\n const line = raw as Record<string, unknown>;\n const hasId = \"id\" in line && typeof line.id === \"string\" && line.id !== \"\";\n const hasMerchandiseId =\n \"merchandiseId\" in line &&\n typeof line.merchandiseId === \"string\" &&\n line.merchandiseId !== \"\";\n\n assertNonNegativeInteger(line.quantity, \"quantity\");\n const quantity = line.quantity as number;\n\n if (!hasId && !hasMerchandiseId) {\n throw new CartActionError(\n 'Each line must have either \"id\" (for update/remove) or \"merchandiseId\" (for add).',\n );\n }\n\n if (hasId && (line.id as string) === \"\") {\n throw new CartActionError('Line \"id\" must not be empty.');\n }\n\n const optionalFields = extractOptionalLineFields(line);\n\n if (!hasId && hasMerchandiseId) {\n adds.push({\n merchandiseId: line.merchandiseId as string,\n quantity,\n ...optionalFields,\n });\n } else if (hasId && quantity === 0) {\n removeIds.push(line.id as string);\n } else if (hasId) {\n updates.push({\n id: line.id as string,\n quantity,\n ...optionalFields,\n });\n }\n }\n\n const populatedBuckets = [adds.length, updates.length, removeIds.length].filter(\n (n) => n > 0,\n ).length;\n\n if (populatedBuckets > 1) {\n throw new CartActionError(\n \"Mixed line operations are not allowed. Separate add, update, and remove into distinct requests.\",\n );\n }\n\n if (adds.length > 0) return { intent: \"add\", lines: adds };\n if (updates.length > 0) return { intent: \"update\", lines: updates };\n return { intent: \"remove\", lineIds: removeIds };\n}\n\nfunction extractOptionalLineFields(\n line: Record<string, unknown>,\n): Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> {\n const result: Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> = {};\n\n if (\"attributes\" in line && Array.isArray(line.attributes)) {\n result.attributes = line.attributes as CartAttributeInput[];\n }\n\n if (\"sellingPlanId\" in line && typeof line.sellingPlanId === \"string\") {\n result.sellingPlanId = line.sellingPlanId;\n }\n\n return result;\n}\n\n// --- FormData parsing ---\n//\n// HTML forms can't express the same structures as JSON. Key compromises:\n//\n// Add line: single item per submission (forms can't batch multiple adds)\n// Update qty: server does the math — form sends current qty, server increments/decrements\n// Remove line: explicit intent field (can't express \"quantity=0 means remove\" implicitly)\n// Discount bulk: no FormData equivalent — forms handle one code at a time\n// Cart attributes: attributes.<key> fields preserve each key/value relationship\n// Selling plan: add only — no way to change selling plan on existing line via form\n//\n// Design decisions:\n// - `intent` field disambiguates operations (forms need explicit routing; JSON infers from structure)\n// - Field names match where possible: merchandiseId, quantity, lineId, discountCode, note\n// - Form submissions get 303 redirect to referer; JSON gets JSON response\n// - `increase`/`decrease` intents exist because forms can't atomically read-modify-write quantity\n\nconst LINE_INTENTS = new Set([\"increase\", \"decrease\", \"remove\", \"set\"]);\nconst DISCOUNT_INTENTS = new Set([\"discount-apply\", \"discount-remove\"]);\n\nfunction parseFormData(form: FormData): CartAction {\n const intent = formString(form, \"intent\");\n const merchandiseId = formString(form, \"merchandiseId\");\n\n if (!intent && merchandiseId) {\n return parseAddIntent(form, merchandiseId);\n }\n\n if (intent === \"add\") {\n if (!merchandiseId) {\n throw new CartActionError('Intent \"add\" requires a \"merchandiseId\" field.');\n }\n return parseAddIntent(form, merchandiseId);\n }\n\n if (!intent) {\n throw new CartActionError(\n 'FormData must include an \"intent\" field or a \"merchandiseId\" field.',\n );\n }\n\n if (LINE_INTENTS.has(intent)) {\n return parseLineIntent(intent, form);\n }\n\n if (DISCOUNT_INTENTS.has(intent)) {\n return parseDiscountIntent(intent as \"discount-apply\" | \"discount-remove\", form);\n }\n\n if (intent === \"note-update\") {\n return parseNoteIntent(form);\n }\n\n if (intent === \"attributes-update\") {\n return parseAttributesIntent(form);\n }\n\n throw new CartActionError(\n `Unknown intent \"${intent}\". Expected one of: add, increase, decrease, remove, set, discount-apply, discount-remove, attributes-update, note-update.`,\n );\n}\n\nfunction parseLineIntent(intent: string, form: FormData): CartAction {\n const lineId = formString(form, \"lineId\");\n if (!lineId) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"lineId\" field.`);\n }\n\n if (intent === \"remove\") {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n\n const quantity = formInt(form, \"quantity\");\n if (quantity === null) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"quantity\" field.`);\n }\n\n if (intent === \"set\") {\n if (quantity <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity }] };\n }\n\n if (intent === \"increase\") {\n return { intent: \"update\", lines: [{ id: lineId, quantity: quantity + 1 }] };\n }\n\n const decremented = quantity - 1;\n if (decremented <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity: decremented }] };\n}\n\nfunction parseDiscountIntent(\n intent: \"discount-apply\" | \"discount-remove\",\n form: FormData,\n): CartAction {\n const code = formString(form, \"discountCode\");\n if (!code) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"discountCode\" field.`);\n }\n return { intent, code };\n}\n\nfunction parseAddIntent(form: FormData, merchandiseId: string): CartAction {\n const quantity = formInt(form, \"quantity\") ?? 1;\n const sellingPlanId = formString(form, \"sellingPlanId\");\n\n const line: CartLineAddInput = { merchandiseId, quantity };\n if (sellingPlanId) line.sellingPlanId = sellingPlanId;\n\n return { intent: \"add\", lines: [line] };\n}\n\nfunction parseNoteIntent(form: FormData): CartAction {\n const noteValue = form.get(\"note\");\n if (noteValue === null) {\n throw new CartActionError('Intent \"note-update\" requires a \"note\" field.');\n }\n return { intent: \"note-update\", note: String(noteValue) };\n}\n\nfunction parseAttributesIntent(form: FormData): CartAction {\n return {\n intent: \"attributes-update\",\n attributes: parseAttributes(getCartAttributeFormEntries(form)),\n };\n}\n\nfunction parseAttributes(values: unknown[]): CartAttributeInput[] {\n return values.map((value, index) => {\n assertObject(value);\n assertString(value.key, `attributes[${index}].key`);\n assertString(value.value, `attributes[${index}].value`);\n if (!value.key) {\n throw new CartActionError(`Expected \"attributes[${index}].key\" not to be empty.`);\n }\n return { key: value.key, value: value.value };\n });\n}\n\n// --- Assertion helpers ---\n\nfunction assertObject(value: unknown): asserts value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new CartActionError(\"Expected an object.\");\n }\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new CartActionError(`Expected \"${label}\" to be a string.`);\n }\n}\n\nfunction assertNonNegativeInteger(value: unknown, label: string): asserts value is number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n throw new CartActionError(\n `Expected \"${label}\" to be a non-negative integer, got ${JSON.stringify(value)}.`,\n );\n }\n}\n\n// --- FormData field helpers ---\n\nfunction formString(form: FormData, key: string): string | null {\n const value = form.get(key);\n if (value === null || typeof value !== \"string\") return null;\n return value || null;\n}\n\nfunction formInt(form: FormData, key: string): number | null {\n const raw = form.get(key);\n if (raw === null || typeof raw !== \"string\") return null;\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed)) return null;\n return parsed;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"actions.mjs","names":[],"sources":["../../../src/core/cart/actions.ts"],"sourcesContent":["import { normalizeCartId } from \"./cookie\";\nimport { getCartAttributeFormEntries } from \"./form\";\n\n/** A key-value pair attached to the cart or an individual cart line. */\nexport type CartAttributeInput = { key: string; value: string };\n\nexport type CartLineAddInput = {\n /** Storefront API GID of the product variant to add. */\n merchandiseId: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n /** Selling plan GID for subscription line items. */\n sellingPlanId?: string;\n};\n\n/** Input for updating an existing cart line. Setting `quantity` to `0` removes the line. */\nexport type CartLineUpdateInput = {\n /** The `CartLine.id` of the line to update. */\n id: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\n/**\n * Discriminated union of all cart mutation intents.\n *\n * {@link parseCartRequest} normalizes both JSON and FormData requests into one\n * of these variants. The `intent` field determines the Storefront API mutation:\n *\n * - `\"add\"` — add new lines (cartLinesAdd, or cartCreate when no cart exists)\n * - `\"update\"` — change quantity or attributes on existing lines (cartLinesUpdate)\n * - `\"remove\"` — remove lines by ID (cartLinesRemove)\n * - `\"discount-update\"` — replace all discount codes (cartDiscountCodesUpdate)\n * - `\"discount-apply\"` — add a single discount code (read-then-write via cartDiscountCodesUpdate)\n * - `\"discount-remove\"` — remove a single discount code (read-then-write via cartDiscountCodesUpdate)\n * - `\"attributes-update\"` — set cart-level attributes (cartAttributesUpdate)\n * - `\"note-update\"` — set the cart note (cartNoteUpdate)\n */\nexport type CartAction =\n | { intent: \"add\"; lines: CartLineAddInput[] }\n | { intent: \"update\"; lines: CartLineUpdateInput[] }\n | { intent: \"remove\"; lineIds: string[] }\n | { intent: \"discount-update\"; discountCodes: string[] }\n | { intent: \"discount-apply\"; code: string }\n | { intent: \"discount-remove\"; code: string }\n | { intent: \"attributes-update\"; attributes: CartAttributeInput[] }\n | { intent: \"note-update\"; note: string };\n\ntype ParsedCartRequest = {\n action: CartAction;\n cartId: string | null;\n};\n\nclass CartActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CartActionError\";\n }\n}\n\n/**\n * Parses an incoming cart mutation request into a typed {@link CartAction}.\n *\n * Accepts `application/json` and `application/x-www-form-urlencoded` /\n * `multipart/form-data` content types. JSON and FormData each support a\n * different subset of intents — JSON produces `discount-update` while\n * FormData produces `discount-apply` / `discount-remove`.\n *\n * FormData requests always return `cartId: null` — the server handler\n * should fall back to the cart cookie.\n *\n * @throws If the content-type is unsupported, the intent is unrecognized,\n * or required fields are missing.\n *\n * @example\n * ```ts\n * const { action, cartId } = await parseCartRequest(request);\n *\n * switch (action.intent) {\n * case \"add\":\n * return cartLinesAdd(cartId ?? getCartId(request), action.lines);\n * case \"remove\":\n * return cartLinesRemove(cartId ?? getCartId(request), action.lineIds);\n * // ...\n * }\n * ```\n */\nexport async function parseCartRequest(request: Request): Promise<ParsedCartRequest> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n\n if (contentType.includes(\"application/json\")) {\n return parseJsonBody(await request.json());\n }\n\n if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n return { action: parseFormData(await request.formData()), cartId: null };\n }\n\n throw new CartActionError(\n `Unsupported content-type: \"${contentType}\". Expected application/json or form data.`,\n );\n}\n\n// --- JSON parsing ---\n\nfunction parseJsonBody(body: unknown): ParsedCartRequest {\n assertObject(body);\n const cartId = typeof body.cartId === \"string\" ? normalizeCartId(body.cartId) : null;\n\n if (\"note\" in body && typeof body.note === \"string\") {\n return { action: { intent: \"note-update\", note: body.note }, cartId };\n }\n\n if (\"discountCodes\" in body && Array.isArray(body.discountCodes)) {\n for (const code of body.discountCodes) {\n assertString(code, \"discountCodes entries\");\n }\n return {\n action: { intent: \"discount-update\", discountCodes: body.discountCodes as string[] },\n cartId,\n };\n }\n\n if (\"attributes\" in body && Array.isArray(body.attributes)) {\n return {\n action: { intent: \"attributes-update\", attributes: parseAttributes(body.attributes) },\n cartId,\n };\n }\n\n if (!(\"lines\" in body) || !Array.isArray(body.lines)) {\n throw new CartActionError(\n 'Request body must contain \"lines\", \"discountCodes\", \"attributes\", or \"note\".',\n );\n }\n\n const lines = body.lines as unknown[];\n if (lines.length === 0) {\n throw new CartActionError(\"Lines array must not be empty.\");\n }\n\n return { action: partitionLines(lines), cartId };\n}\n\nfunction partitionLines(rawLines: unknown[]): CartAction {\n const adds: CartLineAddInput[] = [];\n const updates: CartLineUpdateInput[] = [];\n const removeIds: string[] = [];\n\n for (const raw of rawLines) {\n assertObject(raw);\n const line = raw as Record<string, unknown>;\n const hasId = \"id\" in line && typeof line.id === \"string\" && line.id !== \"\";\n const hasMerchandiseId =\n \"merchandiseId\" in line &&\n typeof line.merchandiseId === \"string\" &&\n line.merchandiseId !== \"\";\n\n assertNonNegativeInteger(line.quantity, \"quantity\");\n const quantity = line.quantity as number;\n\n if (!hasId && !hasMerchandiseId) {\n throw new CartActionError(\n 'Each line must have either \"id\" (for update/remove) or \"merchandiseId\" (for add).',\n );\n }\n\n if (hasId && (line.id as string) === \"\") {\n throw new CartActionError('Line \"id\" must not be empty.');\n }\n\n const optionalFields = extractOptionalLineFields(line);\n\n if (!hasId && hasMerchandiseId) {\n adds.push({\n merchandiseId: line.merchandiseId as string,\n quantity,\n ...optionalFields,\n });\n } else if (hasId && quantity === 0) {\n removeIds.push(line.id as string);\n } else if (hasId) {\n updates.push({\n id: line.id as string,\n quantity,\n ...optionalFields,\n });\n }\n }\n\n const populatedBuckets = [adds.length, updates.length, removeIds.length].filter(\n (n) => n > 0,\n ).length;\n\n if (populatedBuckets > 1) {\n throw new CartActionError(\n \"Mixed line operations are not allowed. Separate add, update, and remove into distinct requests.\",\n );\n }\n\n if (adds.length > 0) return { intent: \"add\", lines: adds };\n if (updates.length > 0) return { intent: \"update\", lines: updates };\n return { intent: \"remove\", lineIds: removeIds };\n}\n\nfunction extractOptionalLineFields(\n line: Record<string, unknown>,\n): Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> {\n const result: Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> = {};\n\n if (\"attributes\" in line && Array.isArray(line.attributes)) {\n result.attributes = line.attributes as CartAttributeInput[];\n }\n\n if (\"sellingPlanId\" in line && typeof line.sellingPlanId === \"string\") {\n result.sellingPlanId = line.sellingPlanId;\n }\n\n return result;\n}\n\n// --- FormData parsing ---\n//\n// HTML forms can't express the same structures as JSON. Key compromises:\n//\n// Add line: single item per submission (forms can't batch multiple adds)\n// Update qty: server does the math — form sends current qty, server increments/decrements\n// Remove line: explicit intent field (can't express \"quantity=0 means remove\" implicitly)\n// Discount bulk: no FormData equivalent — forms handle one code at a time\n// Cart attributes: attributes.<key> fields preserve each key/value relationship\n// Selling plan: add only — no way to change selling plan on existing line via form\n//\n// Design decisions:\n// - `intent` field disambiguates operations (forms need explicit routing; JSON infers from structure)\n// - Field names match where possible: merchandiseId, quantity, lineId, discountCode, note\n// - Form submissions get 303 redirect to referer; JSON gets JSON response\n// - `increase`/`decrease` intents exist because forms can't atomically read-modify-write quantity\n\nconst LINE_INTENTS = new Set([\"increase\", \"decrease\", \"remove\", \"set\"]);\nconst DISCOUNT_INTENTS = new Set([\"discount-apply\", \"discount-remove\"]);\n\nfunction parseFormData(form: FormData): CartAction {\n const intent = formString(form, \"intent\");\n const merchandiseId = formString(form, \"merchandiseId\");\n\n if (!intent && merchandiseId) {\n return parseAddIntent(form, merchandiseId);\n }\n\n if (intent === \"add\") {\n if (!merchandiseId) {\n throw new CartActionError('Intent \"add\" requires a \"merchandiseId\" field.');\n }\n return parseAddIntent(form, merchandiseId);\n }\n\n if (!intent) {\n throw new CartActionError(\n 'FormData must include an \"intent\" field or a \"merchandiseId\" field.',\n );\n }\n\n if (LINE_INTENTS.has(intent)) {\n return parseLineIntent(intent, form);\n }\n\n if (DISCOUNT_INTENTS.has(intent)) {\n return parseDiscountIntent(intent as \"discount-apply\" | \"discount-remove\", form);\n }\n\n if (intent === \"note-update\") {\n return parseNoteIntent(form);\n }\n\n if (intent === \"attributes-update\") {\n return parseAttributesIntent(form);\n }\n\n throw new CartActionError(\n `Unknown intent \"${intent}\". Expected one of: add, increase, decrease, remove, set, discount-apply, discount-remove, attributes-update, note-update.`,\n );\n}\n\nfunction parseLineIntent(intent: string, form: FormData): CartAction {\n const lineId = formString(form, \"lineId\");\n if (!lineId) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"lineId\" field.`);\n }\n\n if (intent === \"remove\") {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n\n const quantity = formInt(form, \"quantity\");\n if (quantity === null) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"quantity\" field.`);\n }\n\n if (intent === \"set\") {\n if (quantity <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity }] };\n }\n\n if (intent === \"increase\") {\n return { intent: \"update\", lines: [{ id: lineId, quantity: quantity + 1 }] };\n }\n\n const decremented = quantity - 1;\n if (decremented <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity: decremented }] };\n}\n\nfunction parseDiscountIntent(\n intent: \"discount-apply\" | \"discount-remove\",\n form: FormData,\n): CartAction {\n const code = formString(form, \"discountCode\");\n if (!code) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"discountCode\" field.`);\n }\n return { intent, code };\n}\n\nfunction parseAddIntent(form: FormData, merchandiseId: string): CartAction {\n const quantity = formInt(form, \"quantity\") ?? 1;\n const sellingPlanId = formString(form, \"sellingPlanId\");\n\n const line: CartLineAddInput = { merchandiseId, quantity };\n if (sellingPlanId) line.sellingPlanId = sellingPlanId;\n\n const rawAttributes = getCartAttributeFormEntries(form);\n if (rawAttributes.length > 0) {\n line.attributes = parseAttributes(rawAttributes);\n }\n\n return { intent: \"add\", lines: [line] };\n}\n\nfunction parseNoteIntent(form: FormData): CartAction {\n const noteValue = form.get(\"note\");\n if (noteValue === null) {\n throw new CartActionError('Intent \"note-update\" requires a \"note\" field.');\n }\n return { intent: \"note-update\", note: String(noteValue) };\n}\n\nfunction parseAttributesIntent(form: FormData): CartAction {\n return {\n intent: \"attributes-update\",\n attributes: parseAttributes(getCartAttributeFormEntries(form)),\n };\n}\n\nfunction parseAttributes(values: unknown[]): CartAttributeInput[] {\n return values.map((value, index) => {\n assertObject(value);\n assertString(value.key, `attributes[${index}].key`);\n assertString(value.value, `attributes[${index}].value`);\n if (!value.key) {\n throw new CartActionError(`Expected \"attributes[${index}].key\" not to be empty.`);\n }\n return { key: value.key, value: value.value };\n });\n}\n\n// --- Assertion helpers ---\n\nfunction assertObject(value: unknown): asserts value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new CartActionError(\"Expected an object.\");\n }\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new CartActionError(`Expected \"${label}\" to be a string.`);\n }\n}\n\nfunction assertNonNegativeInteger(value: unknown, label: string): asserts value is number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n throw new CartActionError(\n `Expected \"${label}\" to be a non-negative integer, got ${JSON.stringify(value)}.`,\n );\n }\n}\n\n// --- FormData field helpers ---\n\nfunction formString(form: FormData, key: string): string | null {\n const value = form.get(key);\n if (value === null || typeof value !== \"string\") return null;\n return value || null;\n}\n\nfunction formInt(form: FormData, key: string): number | null {\n const raw = form.get(key);\n if (raw === null || typeof raw !== \"string\") return null;\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed)) return null;\n return parsed;\n}\n"],"mappings":"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"}
@@ -1,4 +1,30 @@
1
1
  //#region src/core/cart/attach-quantity-input.d.ts
2
+ /**
3
+ * Attaches interactive quantity behavior to a DOM `<input>` inside a cart form.
4
+ *
5
+ * When the input's value changes, the form is automatically submitted via the
6
+ * first submit button (which must carry `name="intent" value="set"`). Returns
7
+ * a cleanup function that removes the event listener.
8
+ *
9
+ * The form must contain a `"set"` button as its first submit element — use
10
+ * `register("set")` from `createCartFormRegister` to produce it.
11
+ *
12
+ * @throws If the form's first submit button is not a `name="intent" value="set"` button.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const input = document.querySelector<HTMLInputElement>('input[name="quantity"]');
17
+ * const form = input?.closest('form');
18
+ *
19
+ * if (input && form) {
20
+ * const detach = attachQuantityInput(input, form);
21
+ * // Input changes now auto-submit the form
22
+ *
23
+ * // Clean up when unmounting
24
+ * detach();
25
+ * }
26
+ * ```
27
+ */
2
28
  declare function attachQuantityInput(inputEl: HTMLInputElement, formEl: HTMLFormElement): () => void;
3
29
  //#endregion
4
30
  export { attachQuantityInput };
@@ -1,5 +1,31 @@
1
1
  //#region src/core/cart/attach-quantity-input.ts
2
2
  const FIRST_SUBMIT_BUTTON_SELECTOR = "button:not([type=button])";
3
+ /**
4
+ * Attaches interactive quantity behavior to a DOM `<input>` inside a cart form.
5
+ *
6
+ * When the input's value changes, the form is automatically submitted via the
7
+ * first submit button (which must carry `name="intent" value="set"`). Returns
8
+ * a cleanup function that removes the event listener.
9
+ *
10
+ * The form must contain a `"set"` button as its first submit element — use
11
+ * `register("set")` from `createCartFormRegister` to produce it.
12
+ *
13
+ * @throws If the form's first submit button is not a `name="intent" value="set"` button.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const input = document.querySelector<HTMLInputElement>('input[name="quantity"]');
18
+ * const form = input?.closest('form');
19
+ *
20
+ * if (input && form) {
21
+ * const detach = attachQuantityInput(input, form);
22
+ * // Input changes now auto-submit the form
23
+ *
24
+ * // Clean up when unmounting
25
+ * detach();
26
+ * }
27
+ * ```
28
+ */
3
29
  function attachQuantityInput(inputEl, formEl) {
4
30
  const firstSubmitButton = formEl.querySelector(FIRST_SUBMIT_BUTTON_SELECTOR);
5
31
  if (!firstSubmitButton || firstSubmitButton.name !== "intent" || firstSubmitButton.value !== "set") throw new Error(`The first button in cart form must have a name of "intent" and value of "set". Your UI will not behave as expected.`, { cause: formEl });
@@ -1 +1 @@
1
- {"version":3,"file":"attach-quantity-input.mjs","names":[],"sources":["../../../src/core/cart/attach-quantity-input.ts"],"sourcesContent":["const FIRST_SUBMIT_BUTTON_SELECTOR = \"button:not([type=button])\";\n\nexport function attachQuantityInput(\n inputEl: HTMLInputElement,\n formEl: HTMLFormElement,\n): () => void {\n const firstSubmitButton = formEl.querySelector(\n FIRST_SUBMIT_BUTTON_SELECTOR,\n ) as HTMLButtonElement | null;\n\n if (\n !firstSubmitButton ||\n firstSubmitButton.name !== \"intent\" ||\n firstSubmitButton.value !== \"set\"\n ) {\n throw new Error(\n `The first button in cart form must have a name of \"intent\" and value of \"set\". Your UI will not behave as expected.`,\n { cause: formEl },\n );\n }\n\n const handleChange = (e: Event) => {\n if (formEl.isConnected && e.target === inputEl) {\n formEl.requestSubmit(firstSubmitButton);\n }\n };\n\n inputEl.addEventListener(\"change\", handleChange);\n\n return () => {\n inputEl.removeEventListener(\"change\", handleChange);\n };\n}\n"],"mappings":";AAAA,MAAM,+BAA+B;AAErC,SAAgB,oBACd,SACA,QACY;CACZ,MAAM,oBAAoB,OAAO,cAC/B,6BACD;AAED,KACE,CAAC,qBACD,kBAAkB,SAAS,YAC3B,kBAAkB,UAAU,MAE5B,OAAM,IAAI,MACR,uHACA,EAAE,OAAO,QAAQ,CAClB;CAGH,MAAM,gBAAgB,MAAa;AACjC,MAAI,OAAO,eAAe,EAAE,WAAW,QACrC,QAAO,cAAc,kBAAkB;;AAI3C,SAAQ,iBAAiB,UAAU,aAAa;AAEhD,cAAa;AACX,UAAQ,oBAAoB,UAAU,aAAa"}
1
+ {"version":3,"file":"attach-quantity-input.mjs","names":[],"sources":["../../../src/core/cart/attach-quantity-input.ts"],"sourcesContent":["const FIRST_SUBMIT_BUTTON_SELECTOR = \"button:not([type=button])\";\n\n/**\n * Attaches interactive quantity behavior to a DOM `<input>` inside a cart form.\n *\n * When the input's value changes, the form is automatically submitted via the\n * first submit button (which must carry `name=\"intent\" value=\"set\"`). Returns\n * a cleanup function that removes the event listener.\n *\n * The form must contain a `\"set\"` button as its first submit element — use\n * `register(\"set\")` from `createCartFormRegister` to produce it.\n *\n * @throws If the form's first submit button is not a `name=\"intent\" value=\"set\"` button.\n *\n * @example\n * ```ts\n * const input = document.querySelector<HTMLInputElement>('input[name=\"quantity\"]');\n * const form = input?.closest('form');\n *\n * if (input && form) {\n * const detach = attachQuantityInput(input, form);\n * // Input changes now auto-submit the form\n *\n * // Clean up when unmounting\n * detach();\n * }\n * ```\n */\nexport function attachQuantityInput(\n inputEl: HTMLInputElement,\n formEl: HTMLFormElement,\n): () => void {\n const firstSubmitButton = formEl.querySelector(\n FIRST_SUBMIT_BUTTON_SELECTOR,\n ) as HTMLButtonElement | null;\n\n if (\n !firstSubmitButton ||\n firstSubmitButton.name !== \"intent\" ||\n firstSubmitButton.value !== \"set\"\n ) {\n throw new Error(\n `The first button in cart form must have a name of \"intent\" and value of \"set\". Your UI will not behave as expected.`,\n { cause: formEl },\n );\n }\n\n const handleChange = (e: Event) => {\n if (formEl.isConnected && e.target === inputEl) {\n formEl.requestSubmit(firstSubmitButton);\n }\n };\n\n inputEl.addEventListener(\"change\", handleChange);\n\n return () => {\n inputEl.removeEventListener(\"change\", handleChange);\n };\n}\n"],"mappings":";AAAA,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BrC,SAAgB,oBACd,SACA,QACY;CACZ,MAAM,oBAAoB,OAAO,cAC/B,6BACD;AAED,KACE,CAAC,qBACD,kBAAkB,SAAS,YAC3B,kBAAkB,UAAU,MAE5B,OAAM,IAAI,MACR,uHACA,EAAE,OAAO,QAAQ,CAClB;CAGH,MAAM,gBAAgB,MAAa;AACjC,MAAI,OAAO,eAAe,EAAE,WAAW,QACrC,QAAO,cAAc,kBAAkB;;AAI3C,SAAQ,iBAAiB,UAAU,aAAa;AAEhD,cAAa;AACX,UAAQ,oBAAoB,UAAU,aAAa"}