@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.
- package/README.md +34 -3
- package/dist/cli/index.mjs +578 -85
- package/dist/client/client.mjs +2 -2
- package/dist/client/errors.d.mts +19 -19
- package/dist/client/errors.mjs +19 -16
- package/dist/client/errors.mjs.map +1 -1
- package/dist/client/types.d.mts +91 -10
- package/dist/core/analytics/cart-tracker.d.mts +14 -0
- package/dist/core/analytics/cart-tracker.mjs +15 -1
- package/dist/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/core/analytics/custom-consent.mjs +12 -0
- package/dist/core/analytics/custom-consent.mjs.map +1 -0
- package/dist/core/analytics/events.d.mts +7 -0
- package/dist/core/analytics/events.mjs +7 -0
- package/dist/core/analytics/events.mjs.map +1 -1
- package/dist/core/analytics/index.d.mts +1 -1
- package/dist/core/analytics/types.d.mts +152 -16
- package/dist/core/cart/actions.d.mts +47 -3
- package/dist/core/cart/actions.mjs +29 -0
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/attach-quantity-input.d.mts +26 -0
- package/dist/core/cart/attach-quantity-input.mjs +26 -0
- package/dist/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/core/cart/cart.d.mts +106 -6
- package/dist/core/cart/cart.mjs +57 -6
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/cookie.d.mts +15 -0
- package/dist/core/cart/cookie.mjs +15 -0
- package/dist/core/cart/cookie.mjs.map +1 -1
- package/dist/core/cart/form.d.mts +61 -0
- package/dist/core/cart/form.mjs +18 -0
- package/dist/core/cart/form.mjs.map +1 -1
- package/dist/core/cart/get-cart.d.mts +43 -2
- package/dist/core/cart/get-cart.mjs +33 -0
- package/dist/core/cart/get-cart.mjs.map +1 -1
- package/dist/core/cart/index.d.mts +1 -1
- package/dist/core/cart/quantity.d.mts +17 -0
- package/dist/core/cart/quantity.mjs +17 -0
- package/dist/core/cart/quantity.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +402 -369
- package/dist/core/cart/queries.mjs +2 -1
- package/dist/core/cart/queries.mjs.map +1 -1
- package/dist/core/cart/server-handlers.d.mts +69 -0
- package/dist/core/cart/server-handlers.mjs +2 -10
- package/dist/core/cart/server-handlers.mjs.map +1 -1
- package/dist/core/cart/state.d.mts +133 -0
- package/dist/core/cart/state.mjs +8 -0
- package/dist/core/cart/state.mjs.map +1 -1
- package/dist/core/collection/collection.d.mts +18 -2
- package/dist/core/collection/collection.mjs +11 -0
- package/dist/core/collection/collection.mjs.map +1 -1
- package/dist/core/collection/index.d.mts +1 -1
- package/dist/core/collection/reconciler.d.mts +13 -4
- package/dist/core/collection/reconciler.mjs.map +1 -1
- package/dist/core/collection/state.d.mts +7 -2
- package/dist/core/collection/state.mjs.map +1 -1
- package/dist/core/collection/url.d.mts +3 -0
- package/dist/core/collection/url.mjs.map +1 -1
- package/dist/core/constants.d.mts +1 -1
- package/dist/core/constants.mjs +2 -2
- package/dist/core/constants.mjs.map +1 -1
- package/dist/core/headers.mjs +2 -2
- package/dist/core/headers.mjs.map +1 -1
- package/dist/core/index.d.mts +5 -5
- package/dist/core/logging/logging.mjs.map +1 -1
- package/dist/core/money/format.mjs +6 -3
- package/dist/core/money/format.mjs.map +1 -1
- package/dist/core/observable.mjs +0 -3
- package/dist/core/observable.mjs.map +1 -1
- package/dist/core/predictive-search/form.d.mts +1 -0
- package/dist/core/predictive-search/form.mjs +1 -0
- package/dist/core/predictive-search/form.mjs.map +1 -1
- package/dist/core/predictive-search/index.d.mts +1 -1
- package/dist/core/predictive-search/queries.d.mts +40 -4
- package/dist/core/predictive-search/queries.mjs +7 -1
- package/dist/core/predictive-search/queries.mjs.map +1 -1
- package/dist/core/predictive-search/search.d.mts +43 -10
- package/dist/core/predictive-search/search.mjs +34 -0
- package/dist/core/predictive-search/search.mjs.map +1 -1
- package/dist/core/predictive-search/server-handlers.d.mts +31 -5
- package/dist/core/predictive-search/server-handlers.mjs +5 -3
- package/dist/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/core/predictive-search/store.d.mts +3 -1
- package/dist/core/predictive-search/store.mjs.map +1 -1
- package/dist/core/predictive-search/url.d.mts +50 -8
- package/dist/core/predictive-search/url.mjs +9 -0
- package/dist/core/predictive-search/url.mjs.map +1 -1
- package/dist/core/product/form.d.mts +21 -2
- package/dist/core/product/form.mjs +13 -0
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/index.d.mts +1 -1
- package/dist/core/product/options.d.mts +2 -2
- package/dist/core/product/options.mjs +2 -2
- package/dist/core/product/options.mjs.map +1 -1
- package/dist/core/product/product-form.d.mts +33 -1
- package/dist/core/product/product-form.mjs +30 -0
- package/dist/core/product/product-form.mjs.map +1 -1
- package/dist/core/product/state.d.mts +41 -0
- package/dist/core/product/url.d.mts +6 -0
- package/dist/core/product/url.mjs.map +1 -1
- package/dist/core/request-context.d.mts +8 -5
- package/dist/core/request-context.mjs +17 -39
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs +4 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ajax-api.mjs +1 -1
- package/dist/core/request-routing/interceptors/api-proxy.mjs +1 -1
- package/dist/core/request-routing/interceptors/checkout.mjs +22 -6
- package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +36 -10
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
- package/dist/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
- package/dist/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/core/shopify-scripts/analytics.mjs +2 -2
- package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/core/shopify-scripts/constants.mjs +4 -2
- package/dist/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/core/shopify-scripts/global.mjs +9 -4
- package/dist/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +1 -0
- package/dist/core/shopify-scripts/index.mjs +14 -12
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/initialize.d.mts +1 -0
- package/dist/core/shopify-scripts/initialize.mjs +10 -3
- package/dist/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +6 -0
- package/dist/core/url.mjs +21 -2
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.d.mts +128 -4
- package/dist/customer-account/client.mjs +27 -1
- package/dist/customer-account/client.mjs.map +1 -1
- package/dist/customer-account/errors.d.mts +46 -0
- package/dist/customer-account/errors.mjs +46 -0
- package/dist/customer-account/errors.mjs.map +1 -1
- package/dist/customer-account/graphql.d.mts +68 -0
- package/dist/customer-account/graphql.mjs +47 -0
- package/dist/customer-account/graphql.mjs.map +1 -1
- package/dist/customer-account/session.d.mts +114 -23
- package/dist/customer-account/session.mjs +27 -12
- package/dist/customer-account/session.mjs.map +1 -1
- package/dist/customer-account/type-resolver.d.mts +23 -0
- package/dist/customer-account-api-types.d.ts +570 -374
- package/dist/customer-account.schema.json +2981 -1527
- package/dist/development/client/client.mjs +2 -2
- package/dist/development/client/errors.d.mts +19 -19
- package/dist/development/client/errors.mjs +20 -20
- package/dist/development/client/errors.mjs.map +1 -1
- package/dist/development/client/types.d.mts +91 -10
- package/dist/development/core/analytics/cart-tracker.d.mts +14 -0
- package/dist/development/core/analytics/cart-tracker.mjs +15 -1
- package/dist/development/core/analytics/cart-tracker.mjs.map +1 -1
- package/dist/development/core/analytics/custom-consent.mjs +12 -0
- package/dist/development/core/analytics/custom-consent.mjs.map +1 -0
- package/dist/development/core/analytics/events.d.mts +7 -0
- package/dist/development/core/analytics/events.mjs +7 -0
- package/dist/development/core/analytics/events.mjs.map +1 -1
- package/dist/development/core/analytics/index.d.mts +1 -1
- package/dist/development/core/analytics/types.d.mts +152 -16
- package/dist/development/core/cart/actions.d.mts +47 -3
- package/dist/development/core/cart/actions.mjs +29 -0
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/attach-quantity-input.d.mts +26 -0
- package/dist/development/core/cart/attach-quantity-input.mjs +26 -0
- package/dist/development/core/cart/attach-quantity-input.mjs.map +1 -1
- package/dist/development/core/cart/cart.d.mts +106 -6
- package/dist/development/core/cart/cart.mjs +57 -6
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/cookie.d.mts +15 -0
- package/dist/development/core/cart/cookie.mjs +15 -0
- package/dist/development/core/cart/cookie.mjs.map +1 -1
- package/dist/development/core/cart/form.d.mts +61 -0
- package/dist/development/core/cart/form.mjs +18 -0
- package/dist/development/core/cart/form.mjs.map +1 -1
- package/dist/development/core/cart/get-cart.d.mts +43 -2
- package/dist/development/core/cart/get-cart.mjs +33 -0
- package/dist/development/core/cart/get-cart.mjs.map +1 -1
- package/dist/development/core/cart/index.d.mts +1 -1
- package/dist/development/core/cart/quantity.d.mts +17 -0
- package/dist/development/core/cart/quantity.mjs +17 -0
- package/dist/development/core/cart/quantity.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +345 -312
- package/dist/development/core/cart/queries.mjs +2 -1
- package/dist/development/core/cart/queries.mjs.map +1 -1
- package/dist/development/core/cart/server-handlers.d.mts +69 -0
- package/dist/development/core/cart/server-handlers.mjs +2 -10
- package/dist/development/core/cart/server-handlers.mjs.map +1 -1
- package/dist/development/core/cart/state.d.mts +133 -0
- package/dist/development/core/cart/state.mjs +8 -0
- package/dist/development/core/cart/state.mjs.map +1 -1
- package/dist/development/core/collection/collection.d.mts +18 -2
- package/dist/development/core/collection/collection.mjs +11 -0
- package/dist/development/core/collection/collection.mjs.map +1 -1
- package/dist/development/core/collection/index.d.mts +1 -1
- package/dist/development/core/collection/reconciler.d.mts +13 -4
- package/dist/development/core/collection/reconciler.mjs.map +1 -1
- package/dist/development/core/collection/state.d.mts +7 -2
- package/dist/development/core/collection/state.mjs.map +1 -1
- package/dist/development/core/collection/url.d.mts +3 -0
- package/dist/development/core/collection/url.mjs.map +1 -1
- package/dist/development/core/constants.mjs +1 -1
- package/dist/development/core/constants.mjs.map +1 -1
- package/dist/development/core/development.d.mts +5 -5
- package/dist/development/core/headers.mjs +2 -2
- package/dist/development/core/headers.mjs.map +1 -1
- package/dist/development/core/index.d.mts +4 -4
- package/dist/development/core/logging/logging.mjs.map +1 -1
- package/dist/development/core/money/format.mjs +6 -3
- package/dist/development/core/money/format.mjs.map +1 -1
- package/dist/development/core/observable.mjs +0 -3
- package/dist/development/core/observable.mjs.map +1 -1
- package/dist/development/core/predictive-search/form.d.mts +1 -0
- package/dist/development/core/predictive-search/form.mjs +1 -0
- package/dist/development/core/predictive-search/form.mjs.map +1 -1
- package/dist/development/core/predictive-search/index.d.mts +1 -1
- package/dist/development/core/predictive-search/queries.d.mts +38 -2
- package/dist/development/core/predictive-search/queries.mjs +7 -1
- package/dist/development/core/predictive-search/queries.mjs.map +1 -1
- package/dist/development/core/predictive-search/search.d.mts +43 -10
- package/dist/development/core/predictive-search/search.mjs +34 -0
- package/dist/development/core/predictive-search/search.mjs.map +1 -1
- package/dist/development/core/predictive-search/server-handlers.d.mts +31 -5
- package/dist/development/core/predictive-search/server-handlers.mjs +5 -3
- package/dist/development/core/predictive-search/server-handlers.mjs.map +1 -1
- package/dist/development/core/predictive-search/store.d.mts +3 -1
- package/dist/development/core/predictive-search/store.mjs.map +1 -1
- package/dist/development/core/predictive-search/url.d.mts +50 -8
- package/dist/development/core/predictive-search/url.mjs +9 -0
- package/dist/development/core/predictive-search/url.mjs.map +1 -1
- package/dist/development/core/product/form.d.mts +21 -2
- package/dist/development/core/product/form.mjs +13 -0
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/index.d.mts +1 -1
- package/dist/development/core/product/options.d.mts +2 -2
- package/dist/development/core/product/options.mjs +2 -2
- package/dist/development/core/product/options.mjs.map +1 -1
- package/dist/development/core/product/product-form.d.mts +33 -1
- package/dist/development/core/product/product-form.mjs +30 -0
- package/dist/development/core/product/product-form.mjs.map +1 -1
- package/dist/development/core/product/state.d.mts +41 -0
- package/dist/development/core/product/url.d.mts +6 -0
- package/dist/development/core/product/url.mjs.map +1 -1
- package/dist/development/core/request-context.d.mts +8 -5
- package/dist/development/core/request-context.mjs +17 -39
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +4 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ajax-api.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/api-proxy.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs +22 -6
- package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +36 -10
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs +5 -11
- package/dist/development/core/request-routing/interceptors/query-param-redirect.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs +19 -1
- package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/development/core/request-routing/interceptors/well-known.mjs +1 -1
- package/dist/development/core/shopify-scripts/analytics.mjs +2 -2
- package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/constants.mjs +4 -2
- package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/global.mjs +9 -4
- package/dist/development/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +1 -0
- package/dist/development/core/shopify-scripts/index.mjs +14 -12
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/initialize.d.mts +1 -0
- package/dist/development/core/shopify-scripts/initialize.mjs +10 -3
- package/dist/development/core/shopify-scripts/initialize.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +6 -0
- package/dist/development/core/url.mjs +21 -2
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/customer-account/session.d.mts +69 -13
- package/dist/development/customer-account/session.mjs +4 -0
- package/dist/development/customer-account/session.mjs.map +1 -1
- package/dist/development/globals.d.mts +6 -5
- package/dist/development/graphql/generated/graphql-env.d.mts +459 -16
- package/dist/development/graphql/generated/storefront-api-types.d.mts +1 -1
- package/dist/development/graphql/graphql.d.mts +33 -0
- package/dist/development/graphql/graphql.mjs +30 -0
- package/dist/development/graphql/graphql.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +14 -0
- package/dist/development/react/cart.d.mts +85 -14
- package/dist/development/react/cart.mjs +123 -3
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/development/react/collection.d.mts +2 -4
- package/dist/development/react/collection.mjs.map +1 -1
- package/dist/development/react/index.d.mts +8 -6
- package/dist/development/react/index.mjs +2 -2
- package/dist/development/react/predictive-search.d.mts +68 -7
- package/dist/development/react/predictive-search.mjs +54 -12
- package/dist/development/react/predictive-search.mjs.map +1 -1
- package/dist/development/react/product.d.mts +1 -0
- package/dist/development/react/product.mjs +5 -2
- package/dist/development/react/product.mjs.map +1 -1
- package/dist/development/react/shopify-scripts.mjs +1 -0
- package/dist/development/react/shopify-scripts.mjs.map +1 -1
- package/dist/development/vue/cart.d.mts +85 -6
- package/dist/development/vue/cart.mjs +95 -0
- package/dist/development/vue/cart.mjs.map +1 -1
- package/dist/development/vue/collection.d.mts +83 -5
- package/dist/development/vue/collection.mjs +52 -0
- package/dist/development/vue/collection.mjs.map +1 -1
- package/dist/development/vue/index.d.mts +6 -4
- package/dist/development/vue/predictive-search.d.mts +53 -6
- package/dist/development/vue/predictive-search.mjs +33 -0
- package/dist/development/vue/predictive-search.mjs.map +1 -1
- package/dist/development/vue/product.d.mts +6 -2
- package/dist/development/vue/product.mjs.map +1 -1
- package/dist/development/vue/shopify-scripts.d.mts +30 -3
- package/dist/development/vue/shopify-scripts.mjs +6 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +6 -5
- package/dist/graphql/generated/customer-account-graphql-env.d.mts +1717 -1090
- package/dist/graphql/generated/graphql-env.d.mts +459 -16
- package/dist/graphql/generated/storefront-api-types.d.mts +1 -1
- package/dist/graphql/graphql.d.mts +33 -0
- package/dist/graphql/graphql.mjs +30 -0
- package/dist/graphql/graphql.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +14 -0
- package/dist/react/cart.d.mts +85 -14
- package/dist/react/cart.mjs +123 -3
- package/dist/react/cart.mjs.map +1 -1
- package/dist/react/collection.d.mts +2 -4
- package/dist/react/collection.mjs.map +1 -1
- package/dist/react/index.d.mts +8 -6
- package/dist/react/index.mjs +2 -2
- package/dist/react/predictive-search.d.mts +68 -7
- package/dist/react/predictive-search.mjs +54 -12
- package/dist/react/predictive-search.mjs.map +1 -1
- package/dist/react/product.d.mts +1 -0
- package/dist/react/product.mjs +5 -2
- package/dist/react/product.mjs.map +1 -1
- package/dist/react/shopify-scripts.mjs +1 -0
- package/dist/react/shopify-scripts.mjs.map +1 -1
- package/dist/storefront-api-types.d.ts +165 -20
- package/dist/storefront.schema.json +782 -57
- package/dist/vite/customer-account.mjs +191 -0
- package/dist/vite/customer-account.mjs.map +1 -0
- package/dist/vite/local-https.mjs +13 -29
- package/dist/vite/local-https.mjs.map +1 -1
- package/dist/vite/mkcert.mjs +1 -1
- package/dist/vue/cart.d.mts +85 -6
- package/dist/vue/cart.mjs +95 -0
- package/dist/vue/cart.mjs.map +1 -1
- package/dist/vue/collection.d.mts +83 -5
- package/dist/vue/collection.mjs +52 -0
- package/dist/vue/collection.mjs.map +1 -1
- package/dist/vue/index.d.mts +6 -4
- package/dist/vue/predictive-search.d.mts +53 -6
- package/dist/vue/predictive-search.mjs +33 -0
- package/dist/vue/predictive-search.mjs.map +1 -1
- package/dist/vue/product.d.mts +6 -2
- package/dist/vue/product.mjs.map +1 -1
- package/dist/vue/shopify-scripts.d.mts +31 -4
- package/dist/vue/shopify-scripts.mjs +6 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +5 -2
- package/skills/hydrogen-analytics/SKILL.md +6 -5
- package/skills/hydrogen-analytics/references/custom-consent.md +52 -0
- package/skills/hydrogen-analytics/references/react.md +1 -1
- package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
- package/skills/hydrogen-cart-ui/SKILL.md +2 -1
- package/skills/hydrogen-customer-account/SKILL.md +3 -3
- package/skills/hydrogen-local-https/SKILL.md +22 -7
- package/skills/hydrogen-request-handlers/SKILL.md +5 -5
- package/skills/hydrogen-request-handlers/references/nuxt.md +0 -6
- package/skills/hydrogen-setup/references/analytics.md +24 -23
- package/skills/hydrogen-setup/steps/1-inspect-app.md +1 -1
- package/skills/hydrogen-setup/steps/11-verify.md +1 -0
- package/skills/hydrogen-setup/steps/2-scaffold.md +22 -1
- package/skills/hydrogen-smoke-test/SKILL.md +2 -2
- package/skills/hydrogen-storefront-client/SKILL.md +4 -2
- package/skills/hydrogen-variant-form/SKILL.md +10 -4
- package/ts-plugin/package.json +5 -0
- package/dist/core/shopify-scripts/consent.mjs +0 -17
- package/dist/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs +0 -97
- package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
- package/dist/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/tracking-values.mjs +0 -65
- package/dist/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
- package/dist/core/shopify-scripts/utils/uuid.mjs +0 -17
- package/dist/core/shopify-scripts/utils/uuid.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/consent.mjs +0 -17
- package/dist/development/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +0 -97
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs +0 -43
- package/dist/development/core/shopify-scripts/utils/cookie-domain.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/tracking-values.mjs +0 -65
- package/dist/development/core/shopify-scripts/utils/tracking-values.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/utils/uuid.mjs +0 -17
- 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?: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,
|
|
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
|
|
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"}
|