@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
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
import { CartErrorCode, CartWarningCode } from "../../graphql/generated/storefront-api-types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/core/cart/state.d.ts
|
|
4
|
+
/** Storefront API monetary value — an amount string and ISO 4217 currency code. */
|
|
4
5
|
interface Money {
|
|
6
|
+
/** Decimal amount as a string (e.g. `"29.99"`). */
|
|
5
7
|
amount: string;
|
|
8
|
+
/** ISO 4217 currency code (e.g. `"USD"`, `"EUR"`). */
|
|
6
9
|
currencyCode: string;
|
|
7
10
|
}
|
|
11
|
+
/** Cart-level cost breakdown returned by the Storefront API. */
|
|
8
12
|
interface CartCost {
|
|
13
|
+
/** The amount, before taxes and cart-level discounts, for the customer to pay. */
|
|
9
14
|
subtotalAmount: Money;
|
|
15
|
+
/** Final amount the buyer pays at checkout. */
|
|
10
16
|
totalAmount: Money;
|
|
17
|
+
/** Estimated amount due at checkout, excluding deferred payments. Equals the subtotal when no deferred payments exist. */
|
|
11
18
|
checkoutChargeAmount: Money;
|
|
12
19
|
}
|
|
13
20
|
interface CartLineCost {
|
|
21
|
+
/** Line total after all applicable discounts. */
|
|
14
22
|
totalAmount: Money;
|
|
23
|
+
/** Line subtotal before line-level discounts. */
|
|
15
24
|
subtotalAmount: Money;
|
|
25
|
+
/** Unit price for a single quantity of this line. */
|
|
16
26
|
amountPerQuantity: Money;
|
|
27
|
+
/** Original compare-at unit price, or `null` when no comparison exists. */
|
|
17
28
|
compareAtAmountPerQuantity: Money | null;
|
|
18
29
|
}
|
|
30
|
+
/** Product variant information attached to a {@link CartLine}. */
|
|
19
31
|
interface CartLineMerchandise {
|
|
32
|
+
/** Storefront API GID of the product variant. */
|
|
20
33
|
id: string;
|
|
34
|
+
/** Variant title (e.g. `"Small / Black"`). */
|
|
21
35
|
title?: string;
|
|
36
|
+
/** Selected variant option name/value pairs (e.g. `{ name: "Size", value: "S" }`). */
|
|
22
37
|
selectedOptions?: Array<{
|
|
23
38
|
name: string;
|
|
24
39
|
value: string;
|
|
@@ -39,12 +54,24 @@ interface CartLineMerchandise {
|
|
|
39
54
|
quantityAvailable?: number | null;
|
|
40
55
|
[key: string]: unknown;
|
|
41
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A single line item in the cart.
|
|
59
|
+
*
|
|
60
|
+
* Each line represents a product variant at a given quantity. Lines may carry
|
|
61
|
+
* custom {@link Attribute | attributes}, a selling plan allocation (subscriptions),
|
|
62
|
+
* or nested {@link CartLine.lineComponents | line components} (bundles).
|
|
63
|
+
*/
|
|
42
64
|
interface CartLine {
|
|
65
|
+
/** Storefront API GID of this cart line — used as the key in {@link CartPending.lines}. */
|
|
43
66
|
id: string;
|
|
67
|
+
/** Number of units of this merchandise in the cart. */
|
|
44
68
|
quantity: number;
|
|
45
69
|
attributes?: Attribute[];
|
|
70
|
+
/** Per-line cost breakdown from the Storefront API. */
|
|
46
71
|
cost: CartLineCost;
|
|
72
|
+
/** The product variant this line represents. */
|
|
47
73
|
merchandise?: CartLineMerchandise;
|
|
74
|
+
/** Subscription selling plan allocation, or `null` for one-time purchases. */
|
|
48
75
|
sellingPlanAllocation?: {
|
|
49
76
|
sellingPlan: {
|
|
50
77
|
id: string;
|
|
@@ -55,49 +82,101 @@ interface CartLine {
|
|
|
55
82
|
id: string;
|
|
56
83
|
};
|
|
57
84
|
} | null;
|
|
85
|
+
/** Nested child lines when this line is a bundle parent. */
|
|
58
86
|
lineComponents?: CartLine[];
|
|
59
87
|
}
|
|
60
88
|
interface CartLineConnection {
|
|
61
89
|
nodes: CartLine[];
|
|
62
90
|
[key: string]: unknown;
|
|
63
91
|
}
|
|
92
|
+
/** A discount code applied to the cart. */
|
|
64
93
|
interface DiscountCode {
|
|
65
94
|
code: string;
|
|
95
|
+
/** Whether the code is applicable to the cart's current contents. Set optimistically to `false` for newly added codes until the server confirms. */
|
|
66
96
|
applicable: boolean;
|
|
67
97
|
}
|
|
98
|
+
/** A validation error returned by a Storefront API cart mutation. */
|
|
68
99
|
interface CartUserError {
|
|
100
|
+
/** Machine-readable error code, or `null` for unclassified errors. */
|
|
69
101
|
code: CartErrorCode | null;
|
|
70
102
|
message: string;
|
|
103
|
+
/** JSONPath to the input field that caused the error (e.g. `["lines", "0", "quantity"]`). */
|
|
71
104
|
field?: string[];
|
|
72
105
|
}
|
|
106
|
+
/** A non-blocking warning returned by a Storefront API cart mutation. */
|
|
73
107
|
interface CartWarning {
|
|
74
108
|
code: CartWarningCode;
|
|
75
109
|
message: string;
|
|
76
110
|
}
|
|
111
|
+
/** Errors and warnings from a single cart mutation response. */
|
|
77
112
|
interface CartErrorGroup {
|
|
78
113
|
userErrors: CartUserError[];
|
|
79
114
|
warnings: CartWarning[];
|
|
80
115
|
}
|
|
116
|
+
/** A network-level error encountered during a cart operation (e.g. non-2xx response, timeout). Aborted requests are not recorded. */
|
|
81
117
|
interface CartNetworkEntry {
|
|
82
118
|
message: string;
|
|
119
|
+
/** HTTP status code when available. */
|
|
83
120
|
status?: number;
|
|
84
121
|
}
|
|
122
|
+
/** A key-value pair attached to the cart or to an individual {@link CartLine}. */
|
|
85
123
|
interface Attribute {
|
|
86
124
|
key: string;
|
|
87
125
|
value: string | null;
|
|
88
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Tracks which parts of the cart have in-flight mutations.
|
|
129
|
+
*
|
|
130
|
+
* `lines` and `discountCodes` are `Set<string>` keyed by line ID / discount code,
|
|
131
|
+
* enabling per-item pending indicators rather than a single global spinner.
|
|
132
|
+
* `cost` is a boolean because *any* line or discount change affects the total —
|
|
133
|
+
* use it to show pending UI on price displays without computing optimistic amounts.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const { pending } = store.getState();
|
|
138
|
+
*
|
|
139
|
+
* // Per-line-item pending indicator
|
|
140
|
+
* if (pending.lines.has(lineId)) {
|
|
141
|
+
* showSpinner(lineId);
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* // Cart total is stale — show pending UI, don't compute a price
|
|
145
|
+
* if (pending.cost) {
|
|
146
|
+
* dimTotalDisplay();
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
89
150
|
interface CartPending {
|
|
151
|
+
/** Line IDs with in-flight add, quantity, or remove mutations. New lines use their `optimistic:` ID until the server confirms. */
|
|
90
152
|
lines: Set<string>;
|
|
91
153
|
note: boolean;
|
|
92
154
|
attributes: boolean;
|
|
155
|
+
/** Discount codes with in-flight apply/remove mutations. */
|
|
93
156
|
discountCodes: Set<string>;
|
|
157
|
+
/** `true` when any mutation may affect cart pricing — show pending UI on cost displays. */
|
|
94
158
|
cost?: boolean;
|
|
95
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Per-resource error tracking for the cart.
|
|
162
|
+
*
|
|
163
|
+
* Errors are bucketed by the resource they affect — `lines`, `note`, `attributes`,
|
|
164
|
+
* `discountCodes`, or `cart` (for cross-cutting errors). Each bucket carries a
|
|
165
|
+
* `*UpdatedAt` timestamp (ms since epoch) so the UI can show stale-error indicators
|
|
166
|
+
* or auto-dismiss after a timeout.
|
|
167
|
+
*
|
|
168
|
+
* `network` collects transport-level failures (timeouts, 5xx) separately from
|
|
169
|
+
* Storefront API user errors.
|
|
170
|
+
*/
|
|
96
171
|
interface CartErrorState {
|
|
172
|
+
/** Cross-cutting cart errors not attributable to a specific resource. */
|
|
97
173
|
cart: CartErrorGroup;
|
|
174
|
+
/** Errors keyed by {@link CartLine.id}. */
|
|
98
175
|
lines: Map<string, CartErrorGroup>;
|
|
99
176
|
note: CartErrorGroup;
|
|
177
|
+
/** Errors keyed by {@link Attribute.key}. */
|
|
100
178
|
attributes: Map<string, CartErrorGroup>;
|
|
179
|
+
/** Errors keyed by discount code string. */
|
|
101
180
|
discountCodes: Map<string, CartErrorGroup>;
|
|
102
181
|
network: CartNetworkEntry[];
|
|
103
182
|
lastUpdatedAt: number;
|
|
@@ -108,29 +187,83 @@ interface CartErrorState {
|
|
|
108
187
|
discountCodesUpdatedAt: number;
|
|
109
188
|
networkUpdatedAt: number;
|
|
110
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Cart data returned by the Storefront API.
|
|
192
|
+
*
|
|
193
|
+
* This is the server's authoritative snapshot — prices, discount applicability,
|
|
194
|
+
* and totals come exclusively from here, never from client-side computation.
|
|
195
|
+
* The index signature allows custom cart query fields to pass through.
|
|
196
|
+
*/
|
|
111
197
|
interface CartData {
|
|
198
|
+
/** Storefront API Cart GID, or `null` when no cart exists yet. */
|
|
112
199
|
id: string | null;
|
|
200
|
+
/** `Cart.updatedAt` (ISO 8601). Absent until the first server response. */
|
|
201
|
+
updatedAt?: string;
|
|
202
|
+
/** Shopify checkout URL. Changes when buyer identity or consent state changes. */
|
|
113
203
|
checkoutUrl?: string | null;
|
|
204
|
+
/** Sum of all line quantities. */
|
|
114
205
|
totalQuantity: number;
|
|
206
|
+
/** Cart-level cost breakdown (subtotal, total, checkout charge). */
|
|
115
207
|
cost: CartCost;
|
|
208
|
+
/** Buyer-supplied cart note, or `null`/empty when unset. */
|
|
116
209
|
note?: string | null;
|
|
210
|
+
/** Cart-level custom attributes. */
|
|
117
211
|
attributes: Attribute[];
|
|
212
|
+
/** The cart's line items. */
|
|
118
213
|
lines: CartLineConnection;
|
|
214
|
+
/** Discount codes applied to the cart, with their applicability. */
|
|
119
215
|
discountCodes: DiscountCode[];
|
|
120
216
|
[key: string]: unknown;
|
|
121
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Full observable state of the cart store.
|
|
220
|
+
*
|
|
221
|
+
* Framework adapters subscribe to this via `CartStore.subscribe` (or the
|
|
222
|
+
* framework-specific hooks like `useCart`). The generic `TData` parameter
|
|
223
|
+
* flows from your server handler configuration, so `data` is typed to match
|
|
224
|
+
* your custom cart query.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* const state = store.getState();
|
|
229
|
+
*
|
|
230
|
+
* if (state.loading) {
|
|
231
|
+
* // Initial cart load in progress — show skeleton
|
|
232
|
+
* }
|
|
233
|
+
*
|
|
234
|
+
* if (state.pending.lines.size > 0) {
|
|
235
|
+
* // One or more lines are mid-mutation — show per-line spinners
|
|
236
|
+
* }
|
|
237
|
+
*
|
|
238
|
+
* if (state.errors.network.length > 0) {
|
|
239
|
+
* // Network errors occurred — show retry prompt
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
122
243
|
interface CartState<TData extends CartData = CartData> {
|
|
244
|
+
/** The latest server-confirmed cart data, with optimistic projections applied on top. */
|
|
123
245
|
data: TData;
|
|
246
|
+
/** `true` during a full-cart fetch — initial load, after `reset()`, or before connect with no `initialData`. */
|
|
124
247
|
loading: boolean;
|
|
125
248
|
/** Non-rejecting signal that resolves when the current full-cart load settles or is invalidated. */
|
|
126
249
|
readonly readyPromise?: PromiseLike<void>;
|
|
250
|
+
/** Per-resource in-flight mutation tracking. */
|
|
127
251
|
pending: CartPending;
|
|
252
|
+
/** `true` when a background revalidation is in flight (triggered by overlapping mutations or `refresh()`). */
|
|
128
253
|
revalidating?: boolean;
|
|
254
|
+
/** Per-resource error and warning state. */
|
|
129
255
|
errors: CartErrorState;
|
|
130
256
|
}
|
|
257
|
+
/** Creates an empty {@link CartErrorGroup} — no user errors, no warnings. */
|
|
131
258
|
declare function createEmptyErrorGroup(): CartErrorGroup;
|
|
259
|
+
/** Creates an empty {@link CartErrorState} — all timestamps at `0`, all buckets empty. */
|
|
132
260
|
declare function createEmptyCartErrors(): CartErrorState;
|
|
261
|
+
/**
|
|
262
|
+
* Frozen {@link CartData} representing an empty cart — `id: null`, zero quantities,
|
|
263
|
+
* zero-amount costs. Used as the initial/fallback value before the first server response.
|
|
264
|
+
*/
|
|
133
265
|
declare const EMPTY_CART_DATA: CartData;
|
|
266
|
+
/** Shared empty {@link CartState} — backed by {@link EMPTY_CART_DATA}, defaults to `loading: true`. */
|
|
134
267
|
declare const EMPTY_CART_STATE: CartState;
|
|
135
268
|
//#endregion
|
|
136
269
|
export { Attribute, CartCost, CartData, CartErrorGroup, CartErrorState, CartLine, CartLineConnection, CartLineCost, CartLineMerchandise, CartNetworkEntry, CartPending, CartState, CartUserError, CartWarning, DiscountCode, EMPTY_CART_DATA, EMPTY_CART_STATE, createEmptyCartErrors, createEmptyErrorGroup };
|
|
@@ -8,12 +8,14 @@ function createEmptyPending() {
|
|
|
8
8
|
cost: false
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
+
/** Creates an empty {@link CartErrorGroup} — no user errors, no warnings. */
|
|
11
12
|
function createEmptyErrorGroup() {
|
|
12
13
|
return {
|
|
13
14
|
userErrors: [],
|
|
14
15
|
warnings: []
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
/** Creates an empty {@link CartErrorState} — all timestamps at `0`, all buckets empty. */
|
|
17
19
|
function createEmptyCartErrors() {
|
|
18
20
|
return {
|
|
19
21
|
cart: createEmptyErrorGroup(),
|
|
@@ -31,6 +33,10 @@ function createEmptyCartErrors() {
|
|
|
31
33
|
networkUpdatedAt: 0
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Frozen {@link CartData} representing an empty cart — `id: null`, zero quantities,
|
|
38
|
+
* zero-amount costs. Used as the initial/fallback value before the first server response.
|
|
39
|
+
*/
|
|
34
40
|
const EMPTY_CART_DATA = Object.freeze({
|
|
35
41
|
id: null,
|
|
36
42
|
checkoutUrl: null,
|
|
@@ -62,6 +68,7 @@ function createCartState(data, { loading = false } = {}) {
|
|
|
62
68
|
errors: createEmptyCartErrors()
|
|
63
69
|
};
|
|
64
70
|
}
|
|
71
|
+
/** Creates an empty {@link CartState} backed by {@link EMPTY_CART_DATA}. Defaults to `loading: true`. */
|
|
65
72
|
function createEmptyCartState({ loading = true } = {}) {
|
|
66
73
|
return createCartState({
|
|
67
74
|
...EMPTY_CART_DATA,
|
|
@@ -69,6 +76,7 @@ function createEmptyCartState({ loading = true } = {}) {
|
|
|
69
76
|
discountCodes: []
|
|
70
77
|
}, { loading });
|
|
71
78
|
}
|
|
79
|
+
/** Shared empty {@link CartState} — backed by {@link EMPTY_CART_DATA}, defaults to `loading: true`. */
|
|
72
80
|
const EMPTY_CART_STATE = createEmptyCartState();
|
|
73
81
|
|
|
74
82
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.mjs","names":[],"sources":["../../../../src/core/cart/state.ts"],"sourcesContent":["import type { CartErrorCode, CartWarningCode } from \"../../graphql/generated/storefront-api-types\";\n\nexport interface Money {\n amount: string;\n currencyCode: string;\n}\n\nexport interface CartCost {\n subtotalAmount: Money;\n totalAmount: Money;\n checkoutChargeAmount: Money;\n}\n\nexport interface CartLineCost {\n totalAmount: Money;\n subtotalAmount: Money;\n amountPerQuantity: Money;\n compareAtAmountPerQuantity: Money | null;\n}\n\nexport interface CartLineMerchandise {\n id: string;\n title?: string;\n selectedOptions?: Array<{ name: string; value: string }>;\n product: {\n title: string;\n handle?: string;\n [key: string]: unknown;\n };\n image?: {\n id?: string | null;\n url: string;\n altText?: string | null;\n width?: number | null;\n height?: number | null;\n [key: string]: unknown;\n } | null;\n quantityAvailable?: number | null;\n [key: string]: unknown;\n}\n\nexport interface CartLine {\n id: string;\n quantity: number;\n attributes?: Attribute[];\n cost: CartLineCost;\n merchandise?: CartLineMerchandise;\n sellingPlanAllocation?: { sellingPlan: { id: string } } | null;\n parentRelationship?: { parent: { id: string } } | null;\n lineComponents?: CartLine[];\n}\n\nexport interface CartLineConnection {\n nodes: CartLine[];\n [key: string]: unknown;\n}\n\nexport interface DiscountCode {\n code: string;\n applicable: boolean;\n}\n\nexport interface CartUserError {\n code: CartErrorCode | null;\n message: string;\n field?: string[];\n}\n\nexport interface CartWarning {\n code: CartWarningCode;\n message: string;\n}\n\nexport interface CartErrorGroup {\n userErrors: CartUserError[];\n warnings: CartWarning[];\n}\n\nexport interface CartNetworkEntry {\n message: string;\n status?: number;\n}\n\nexport interface Attribute {\n key: string;\n value: string | null;\n}\n\nexport interface CartPending {\n lines: Set<string>;\n note: boolean;\n attributes: boolean;\n discountCodes: Set<string>;\n cost?: boolean;\n}\n\nexport interface CartErrorState {\n cart: CartErrorGroup;\n lines: Map<string, CartErrorGroup>;\n note: CartErrorGroup;\n attributes: Map<string, CartErrorGroup>;\n discountCodes: Map<string, CartErrorGroup>;\n network: CartNetworkEntry[];\n lastUpdatedAt: number;\n cartUpdatedAt: number;\n linesUpdatedAt: number;\n noteUpdatedAt: number;\n attributesUpdatedAt: number;\n discountCodesUpdatedAt: number;\n networkUpdatedAt: number;\n}\n\nexport interface CartData {\n id: string | null;\n checkoutUrl?: string | null;\n totalQuantity: number;\n cost: CartCost;\n note?: string | null;\n attributes: Attribute[];\n lines: CartLineConnection;\n discountCodes: DiscountCode[];\n [key: string]: unknown;\n}\n\nexport interface CartState<TData extends CartData = CartData> {\n data: TData;\n loading: boolean;\n /** Non-rejecting signal that resolves when the current full-cart load settles or is invalidated. */\n readonly readyPromise?: PromiseLike<void>;\n pending: CartPending;\n revalidating?: boolean;\n errors: CartErrorState;\n}\n\nexport function createEmptyPending(): CartPending {\n return {\n lines: new Set(),\n note: false,\n attributes: false,\n discountCodes: new Set(),\n cost: false,\n };\n}\n\nexport function createEmptyErrorGroup(): CartErrorGroup {\n return { userErrors: [], warnings: [] };\n}\n\nexport function createEmptyCartErrors(): CartErrorState {\n return {\n cart: createEmptyErrorGroup(),\n lines: new Map(),\n note: createEmptyErrorGroup(),\n attributes: new Map(),\n discountCodes: new Map(),\n network: [],\n lastUpdatedAt: 0,\n cartUpdatedAt: 0,\n linesUpdatedAt: 0,\n noteUpdatedAt: 0,\n attributesUpdatedAt: 0,\n discountCodesUpdatedAt: 0,\n networkUpdatedAt: 0,\n };\n}\n\nexport const EMPTY_CART_DATA: CartData = Object.freeze({\n id: null,\n checkoutUrl: null,\n totalQuantity: 0,\n cost: Object.freeze({\n subtotalAmount: Object.freeze({ amount: \"0\", currencyCode: \"\" }),\n totalAmount: Object.freeze({ amount: \"0\", currencyCode: \"\" }),\n checkoutChargeAmount: Object.freeze({ amount: \"0\", currencyCode: \"\" }),\n }),\n note: \"\",\n attributes: [] as Attribute[],\n lines: Object.freeze({ nodes: [] as CartLine[] }),\n discountCodes: [] as DiscountCode[],\n});\n\nexport function createCartState<TData extends CartData>(\n data: TData,\n { loading = false }: { loading?: boolean } = {},\n): CartState<TData> {\n return {\n data,\n loading,\n pending: createEmptyPending(),\n errors: createEmptyCartErrors(),\n };\n}\n\nexport function createEmptyCartState({ loading = true }: { loading?: boolean } = {}): CartState {\n return createCartState(\n {\n ...EMPTY_CART_DATA,\n lines: { nodes: [] },\n discountCodes: [],\n },\n { loading },\n );\n}\n\nexport const EMPTY_CART_STATE: CartState = createEmptyCartState();\n"],"mappings":";AAsIA,SAAgB,qBAAkC;AAChD,QAAO;EACL,uBAAO,IAAI,KAAK;EAChB,MAAM;EACN,YAAY;EACZ,+BAAe,IAAI,KAAK;EACxB,MAAM;EACP;;AAGH,SAAgB,wBAAwC;AACtD,QAAO;EAAE,YAAY,EAAE;EAAE,UAAU,EAAE;EAAE;;AAGzC,SAAgB,wBAAwC;AACtD,QAAO;EACL,MAAM,uBAAuB;EAC7B,uBAAO,IAAI,KAAK;EAChB,MAAM,uBAAuB;EAC7B,4BAAY,IAAI,KAAK;EACrB,+BAAe,IAAI,KAAK;EACxB,SAAS,EAAE;EACX,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,eAAe;EACf,qBAAqB;EACrB,wBAAwB;EACxB,kBAAkB;EACnB;;AAGH,MAAa,kBAA4B,OAAO,OAAO;CACrD,IAAI;CACJ,aAAa;CACb,eAAe;CACf,MAAM,OAAO,OAAO;EAClB,gBAAgB,OAAO,OAAO;GAAE,QAAQ;GAAK,cAAc;GAAI,CAAC;EAChE,aAAa,OAAO,OAAO;GAAE,QAAQ;GAAK,cAAc;GAAI,CAAC;EAC7D,sBAAsB,OAAO,OAAO;GAAE,QAAQ;GAAK,cAAc;GAAI,CAAC;EACvE,CAAC;CACF,MAAM;CACN,YAAY,EAAE;CACd,OAAO,OAAO,OAAO,EAAE,OAAO,EAAE,EAAgB,CAAC;CACjD,eAAe,EAAE;CAClB,CAAC;AAEF,SAAgB,gBACd,MACA,EAAE,UAAU,UAAiC,EAAE,EAC7B;AAClB,QAAO;EACL;EACA;EACA,SAAS,oBAAoB;EAC7B,QAAQ,uBAAuB;EAChC;;AAGH,SAAgB,qBAAqB,EAAE,UAAU,SAAgC,EAAE,EAAa;AAC9F,QAAO,gBACL;EACE,GAAG;EACH,OAAO,EAAE,OAAO,EAAE,EAAE;EACpB,eAAe,EAAE;EAClB,EACD,EAAE,SAAS,CACZ;;AAGH,MAAa,mBAA8B,sBAAsB"}
|
|
1
|
+
{"version":3,"file":"state.mjs","names":[],"sources":["../../../../src/core/cart/state.ts"],"sourcesContent":["import type { CartErrorCode, CartWarningCode } from \"../../graphql/generated/storefront-api-types\";\n\n/** Storefront API monetary value — an amount string and ISO 4217 currency code. */\nexport interface Money {\n /** Decimal amount as a string (e.g. `\"29.99\"`). */\n amount: string;\n /** ISO 4217 currency code (e.g. `\"USD\"`, `\"EUR\"`). */\n currencyCode: string;\n}\n\n/** Cart-level cost breakdown returned by the Storefront API. */\nexport interface CartCost {\n /** The amount, before taxes and cart-level discounts, for the customer to pay. */\n subtotalAmount: Money;\n /** Final amount the buyer pays at checkout. */\n totalAmount: Money;\n /** Estimated amount due at checkout, excluding deferred payments. Equals the subtotal when no deferred payments exist. */\n checkoutChargeAmount: Money;\n}\n\nexport interface CartLineCost {\n /** Line total after all applicable discounts. */\n totalAmount: Money;\n /** Line subtotal before line-level discounts. */\n subtotalAmount: Money;\n /** Unit price for a single quantity of this line. */\n amountPerQuantity: Money;\n /** Original compare-at unit price, or `null` when no comparison exists. */\n compareAtAmountPerQuantity: Money | null;\n}\n\n/** Product variant information attached to a {@link CartLine}. */\nexport interface CartLineMerchandise {\n /** Storefront API GID of the product variant. */\n id: string;\n /** Variant title (e.g. `\"Small / Black\"`). */\n title?: string;\n /** Selected variant option name/value pairs (e.g. `{ name: \"Size\", value: \"S\" }`). */\n selectedOptions?: Array<{ name: string; value: string }>;\n product: {\n title: string;\n handle?: string;\n [key: string]: unknown;\n };\n image?: {\n id?: string | null;\n url: string;\n altText?: string | null;\n width?: number | null;\n height?: number | null;\n [key: string]: unknown;\n } | null;\n quantityAvailable?: number | null;\n [key: string]: unknown;\n}\n\n/**\n * A single line item in the cart.\n *\n * Each line represents a product variant at a given quantity. Lines may carry\n * custom {@link Attribute | attributes}, a selling plan allocation (subscriptions),\n * or nested {@link CartLine.lineComponents | line components} (bundles).\n */\nexport interface CartLine {\n /** Storefront API GID of this cart line — used as the key in {@link CartPending.lines}. */\n id: string;\n /** Number of units of this merchandise in the cart. */\n quantity: number;\n attributes?: Attribute[];\n /** Per-line cost breakdown from the Storefront API. */\n cost: CartLineCost;\n /** The product variant this line represents. */\n merchandise?: CartLineMerchandise;\n /** Subscription selling plan allocation, or `null` for one-time purchases. */\n sellingPlanAllocation?: { sellingPlan: { id: string } } | null;\n parentRelationship?: { parent: { id: string } } | null;\n /** Nested child lines when this line is a bundle parent. */\n lineComponents?: CartLine[];\n}\n\nexport interface CartLineConnection {\n nodes: CartLine[];\n [key: string]: unknown;\n}\n\n/** A discount code applied to the cart. */\nexport interface DiscountCode {\n code: string;\n /** Whether the code is applicable to the cart's current contents. Set optimistically to `false` for newly added codes until the server confirms. */\n applicable: boolean;\n}\n\n/** A validation error returned by a Storefront API cart mutation. */\nexport interface CartUserError {\n /** Machine-readable error code, or `null` for unclassified errors. */\n code: CartErrorCode | null;\n message: string;\n /** JSONPath to the input field that caused the error (e.g. `[\"lines\", \"0\", \"quantity\"]`). */\n field?: string[];\n}\n\n/** A non-blocking warning returned by a Storefront API cart mutation. */\nexport interface CartWarning {\n code: CartWarningCode;\n message: string;\n}\n\n/** Errors and warnings from a single cart mutation response. */\nexport interface CartErrorGroup {\n userErrors: CartUserError[];\n warnings: CartWarning[];\n}\n\n/** A network-level error encountered during a cart operation (e.g. non-2xx response, timeout). Aborted requests are not recorded. */\nexport interface CartNetworkEntry {\n message: string;\n /** HTTP status code when available. */\n status?: number;\n}\n\n/** A key-value pair attached to the cart or to an individual {@link CartLine}. */\nexport interface Attribute {\n key: string;\n value: string | null;\n}\n\n/**\n * Tracks which parts of the cart have in-flight mutations.\n *\n * `lines` and `discountCodes` are `Set<string>` keyed by line ID / discount code,\n * enabling per-item pending indicators rather than a single global spinner.\n * `cost` is a boolean because *any* line or discount change affects the total —\n * use it to show pending UI on price displays without computing optimistic amounts.\n *\n * @example\n * ```ts\n * const { pending } = store.getState();\n *\n * // Per-line-item pending indicator\n * if (pending.lines.has(lineId)) {\n * showSpinner(lineId);\n * }\n *\n * // Cart total is stale — show pending UI, don't compute a price\n * if (pending.cost) {\n * dimTotalDisplay();\n * }\n * ```\n */\nexport interface CartPending {\n /** Line IDs with in-flight add, quantity, or remove mutations. New lines use their `optimistic:` ID until the server confirms. */\n lines: Set<string>;\n note: boolean;\n attributes: boolean;\n /** Discount codes with in-flight apply/remove mutations. */\n discountCodes: Set<string>;\n /** `true` when any mutation may affect cart pricing — show pending UI on cost displays. */\n cost?: boolean;\n}\n\n/**\n * Per-resource error tracking for the cart.\n *\n * Errors are bucketed by the resource they affect — `lines`, `note`, `attributes`,\n * `discountCodes`, or `cart` (for cross-cutting errors). Each bucket carries a\n * `*UpdatedAt` timestamp (ms since epoch) so the UI can show stale-error indicators\n * or auto-dismiss after a timeout.\n *\n * `network` collects transport-level failures (timeouts, 5xx) separately from\n * Storefront API user errors.\n */\nexport interface CartErrorState {\n /** Cross-cutting cart errors not attributable to a specific resource. */\n cart: CartErrorGroup;\n /** Errors keyed by {@link CartLine.id}. */\n lines: Map<string, CartErrorGroup>;\n note: CartErrorGroup;\n /** Errors keyed by {@link Attribute.key}. */\n attributes: Map<string, CartErrorGroup>;\n /** Errors keyed by discount code string. */\n discountCodes: Map<string, CartErrorGroup>;\n network: CartNetworkEntry[];\n lastUpdatedAt: number;\n cartUpdatedAt: number;\n linesUpdatedAt: number;\n noteUpdatedAt: number;\n attributesUpdatedAt: number;\n discountCodesUpdatedAt: number;\n networkUpdatedAt: number;\n}\n\n/**\n * Cart data returned by the Storefront API.\n *\n * This is the server's authoritative snapshot — prices, discount applicability,\n * and totals come exclusively from here, never from client-side computation.\n * The index signature allows custom cart query fields to pass through.\n */\nexport interface CartData {\n /** Storefront API Cart GID, or `null` when no cart exists yet. */\n id: string | null;\n /** `Cart.updatedAt` (ISO 8601). Absent until the first server response. */\n updatedAt?: string;\n /** Shopify checkout URL. Changes when buyer identity or consent state changes. */\n checkoutUrl?: string | null;\n /** Sum of all line quantities. */\n totalQuantity: number;\n /** Cart-level cost breakdown (subtotal, total, checkout charge). */\n cost: CartCost;\n /** Buyer-supplied cart note, or `null`/empty when unset. */\n note?: string | null;\n /** Cart-level custom attributes. */\n attributes: Attribute[];\n /** The cart's line items. */\n lines: CartLineConnection;\n /** Discount codes applied to the cart, with their applicability. */\n discountCodes: DiscountCode[];\n [key: string]: unknown;\n}\n\n/**\n * Full observable state of the cart store.\n *\n * Framework adapters subscribe to this via `CartStore.subscribe` (or the\n * framework-specific hooks like `useCart`). The generic `TData` parameter\n * flows from your server handler configuration, so `data` is typed to match\n * your custom cart query.\n *\n * @example\n * ```ts\n * const state = store.getState();\n *\n * if (state.loading) {\n * // Initial cart load in progress — show skeleton\n * }\n *\n * if (state.pending.lines.size > 0) {\n * // One or more lines are mid-mutation — show per-line spinners\n * }\n *\n * if (state.errors.network.length > 0) {\n * // Network errors occurred — show retry prompt\n * }\n * ```\n */\nexport interface CartState<TData extends CartData = CartData> {\n /** The latest server-confirmed cart data, with optimistic projections applied on top. */\n data: TData;\n /** `true` during a full-cart fetch — initial load, after `reset()`, or before connect with no `initialData`. */\n loading: boolean;\n /** Non-rejecting signal that resolves when the current full-cart load settles or is invalidated. */\n readonly readyPromise?: PromiseLike<void>;\n /** Per-resource in-flight mutation tracking. */\n pending: CartPending;\n /** `true` when a background revalidation is in flight (triggered by overlapping mutations or `refresh()`). */\n revalidating?: boolean;\n /** Per-resource error and warning state. */\n errors: CartErrorState;\n}\n\nexport function createEmptyPending(): CartPending {\n return {\n lines: new Set(),\n note: false,\n attributes: false,\n discountCodes: new Set(),\n cost: false,\n };\n}\n\n/** Creates an empty {@link CartErrorGroup} — no user errors, no warnings. */\nexport function createEmptyErrorGroup(): CartErrorGroup {\n return { userErrors: [], warnings: [] };\n}\n\n/** Creates an empty {@link CartErrorState} — all timestamps at `0`, all buckets empty. */\nexport function createEmptyCartErrors(): CartErrorState {\n return {\n cart: createEmptyErrorGroup(),\n lines: new Map(),\n note: createEmptyErrorGroup(),\n attributes: new Map(),\n discountCodes: new Map(),\n network: [],\n lastUpdatedAt: 0,\n cartUpdatedAt: 0,\n linesUpdatedAt: 0,\n noteUpdatedAt: 0,\n attributesUpdatedAt: 0,\n discountCodesUpdatedAt: 0,\n networkUpdatedAt: 0,\n };\n}\n\n/**\n * Frozen {@link CartData} representing an empty cart — `id: null`, zero quantities,\n * zero-amount costs. Used as the initial/fallback value before the first server response.\n */\nexport const EMPTY_CART_DATA: CartData = Object.freeze({\n id: null,\n checkoutUrl: null,\n totalQuantity: 0,\n cost: Object.freeze({\n subtotalAmount: Object.freeze({ amount: \"0\", currencyCode: \"\" }),\n totalAmount: Object.freeze({ amount: \"0\", currencyCode: \"\" }),\n checkoutChargeAmount: Object.freeze({ amount: \"0\", currencyCode: \"\" }),\n }),\n note: \"\",\n attributes: [] as Attribute[],\n lines: Object.freeze({ nodes: [] as CartLine[] }),\n discountCodes: [] as DiscountCode[],\n});\n\nexport function createCartState<TData extends CartData>(\n data: TData,\n { loading = false }: { loading?: boolean } = {},\n): CartState<TData> {\n return {\n data,\n loading,\n pending: createEmptyPending(),\n errors: createEmptyCartErrors(),\n };\n}\n\n/** Creates an empty {@link CartState} backed by {@link EMPTY_CART_DATA}. Defaults to `loading: true`. */\nexport function createEmptyCartState({ loading = true }: { loading?: boolean } = {}): CartState {\n return createCartState(\n {\n ...EMPTY_CART_DATA,\n lines: { nodes: [] },\n discountCodes: [],\n },\n { loading },\n );\n}\n\n/** Shared empty {@link CartState} — backed by {@link EMPTY_CART_DATA}, defaults to `loading: true`. */\nexport const EMPTY_CART_STATE: CartState = createEmptyCartState();\n"],"mappings":";AAoQA,SAAgB,qBAAkC;AAChD,QAAO;EACL,uBAAO,IAAI,KAAK;EAChB,MAAM;EACN,YAAY;EACZ,+BAAe,IAAI,KAAK;EACxB,MAAM;EACP;;;AAIH,SAAgB,wBAAwC;AACtD,QAAO;EAAE,YAAY,EAAE;EAAE,UAAU,EAAE;EAAE;;;AAIzC,SAAgB,wBAAwC;AACtD,QAAO;EACL,MAAM,uBAAuB;EAC7B,uBAAO,IAAI,KAAK;EAChB,MAAM,uBAAuB;EAC7B,4BAAY,IAAI,KAAK;EACrB,+BAAe,IAAI,KAAK;EACxB,SAAS,EAAE;EACX,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,eAAe;EACf,qBAAqB;EACrB,wBAAwB;EACxB,kBAAkB;EACnB;;;;;;AAOH,MAAa,kBAA4B,OAAO,OAAO;CACrD,IAAI;CACJ,aAAa;CACb,eAAe;CACf,MAAM,OAAO,OAAO;EAClB,gBAAgB,OAAO,OAAO;GAAE,QAAQ;GAAK,cAAc;GAAI,CAAC;EAChE,aAAa,OAAO,OAAO;GAAE,QAAQ;GAAK,cAAc;GAAI,CAAC;EAC7D,sBAAsB,OAAO,OAAO;GAAE,QAAQ;GAAK,cAAc;GAAI,CAAC;EACvE,CAAC;CACF,MAAM;CACN,YAAY,EAAE;CACd,OAAO,OAAO,OAAO,EAAE,OAAO,EAAE,EAAgB,CAAC;CACjD,eAAe,EAAE;CAClB,CAAC;AAEF,SAAgB,gBACd,MACA,EAAE,UAAU,UAAiC,EAAE,EAC7B;AAClB,QAAO;EACL;EACA;EACA,SAAS,oBAAoB;EAC7B,QAAQ,uBAAuB;EAChC;;;AAIH,SAAgB,qBAAqB,EAAE,UAAU,SAAgC,EAAE,EAAa;AAC9F,QAAO,gBACL;EACE,GAAG;EACH,OAAO,EAAE,OAAO,EAAE,EAAE;EACpB,eAAe,EAAE;EAClB,EACD,EAAE,SAAS,CACZ;;;AAIH,MAAa,mBAA8B,sBAAsB"}
|
|
@@ -87,7 +87,10 @@ type CollectionStore = {
|
|
|
87
87
|
getFilterRemovalUrl(filter: ProductFilter): string;
|
|
88
88
|
/**
|
|
89
89
|
* Parses `FormData` from the submitted form into collection params and applies them.
|
|
90
|
-
*
|
|
90
|
+
* Doesn't cancel the event: call `event.preventDefault()` first, or the browser
|
|
91
|
+
* submits the form natively and navigates away.
|
|
92
|
+
*
|
|
93
|
+
* @throws {TypeError} If `event.target` is not an `HTMLFormElement`.
|
|
91
94
|
*/
|
|
92
95
|
handleFormSubmit(event: SubmitEvent): void;
|
|
93
96
|
/**
|
|
@@ -107,6 +110,8 @@ type CollectionStore = {
|
|
|
107
110
|
*/
|
|
108
111
|
setSortByValue(sortByValue: string): void;
|
|
109
112
|
};
|
|
113
|
+
/** Mutation methods exposed by the collection store. */
|
|
114
|
+
type CollectionActions = Pick<CollectionStore, "setFilters" | "toggleFilter" | "toggleFilterInput" | "setSortKey" | "setSortByValue" | "reset" | "handleFormSubmit">;
|
|
110
115
|
/** Options for creating a new {@link CollectionStore}. */
|
|
111
116
|
type CreateCollectionStoreOptions = {
|
|
112
117
|
/** Collection metadata from the framework loader. */data: CollectionData;
|
|
@@ -127,8 +132,19 @@ type CreateCollectionStoreOptions = {
|
|
|
127
132
|
*
|
|
128
133
|
* The store manages browse intent (filters, sort) only.
|
|
129
134
|
* Server response data lives in the framework's loader.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const store = createCollectionStore({
|
|
139
|
+
* data: loaderData.collection,
|
|
140
|
+
* urlSearch: window.location.search,
|
|
141
|
+
* onBrowseChange: () => navigate({ search: store.serializeToParams().toString() }),
|
|
142
|
+
* });
|
|
143
|
+
*
|
|
144
|
+
* store.subscribe((state) => renderFilters(state.filters));
|
|
145
|
+
* ```
|
|
130
146
|
*/
|
|
131
147
|
declare function createCollectionStore(options: CreateCollectionStoreOptions): CollectionStore;
|
|
132
148
|
//#endregion
|
|
133
|
-
export { CollectionData, CollectionStore, CreateCollectionStoreOptions, createCollectionStore };
|
|
149
|
+
export { CollectionActions, CollectionData, CollectionStore, CreateCollectionStoreOptions, createCollectionStore };
|
|
134
150
|
//# sourceMappingURL=collection.d.mts.map
|
|
@@ -8,6 +8,17 @@ import { collectionParamsMatchState, filterEquals, getFilterRemovalUrl, isDirect
|
|
|
8
8
|
*
|
|
9
9
|
* The store manages browse intent (filters, sort) only.
|
|
10
10
|
* Server response data lives in the framework's loader.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const store = createCollectionStore({
|
|
15
|
+
* data: loaderData.collection,
|
|
16
|
+
* urlSearch: window.location.search,
|
|
17
|
+
* onBrowseChange: () => navigate({ search: store.serializeToParams().toString() }),
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* store.subscribe((state) => renderFilters(state.filters));
|
|
21
|
+
* ```
|
|
11
22
|
*/
|
|
12
23
|
function createCollectionStore(options) {
|
|
13
24
|
const context = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.mjs","names":[],"sources":["../../../../src/core/collection/collection.ts"],"sourcesContent":["import type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\nimport { createObservable } from \"../observable\";\nimport type { CollectionState, ProductFilter } from \"./state\";\nimport { createInitialCollectionState } from \"./state\";\nimport {\n collectionParamsMatchState,\n filterEquals,\n getFilterRemovalUrl,\n isDirectionalSortKey,\n parseCollectionParams,\n parseSortByValue,\n serializeCollectionParams,\n} from \"./url\";\n\n/** Snapshot from the framework loader for a single collection fetch. */\nexport type CollectionData = {\n /** URL-safe collection slug (e.g. `\"shoes\"`). */\n handle: string;\n /**\n * Search string this data snapshot was fetched for (with or without leading `?`).\n * Compared against live {@link CreateCollectionStoreOptions.urlSearch} before settling.\n */\n dataSearch: string;\n};\n\n/**\n * A reactive, framework-agnostic store for collection browse state.\n *\n * Manages filters and sorting — the user's *intent*. Server response\n * data (products, productsCount, availableFilters) lives in the framework's\n * loader data, not here.\n *\n * Mutations are synchronous state changes that set `status: \"loading\"`.\n * The framework adapter navigates (URL change), the framework loader re-runs,\n * and the adapter calls {@link settle} when fresh data arrives.\n *\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: { handle: \"shoes\", dataSearch: \"\" },\n * });\n *\n * store.setFilters([{ tag: \"sale\" }]); // status → \"loading\"\n * // ...framework fetches...\n * store.settle(); // status → \"idle\"\n * ```\n */\nexport type CollectionStore = {\n /** Returns the current snapshot of collection browse state. */\n getState(): CollectionState;\n\n /**\n * Registers a listener invoked on every state change. Returns an unsubscribe function.\n * @param listener - Callback receiving the new state snapshot\n */\n subscribe(listener: (state: CollectionState) => void): () => void;\n\n /**\n * Replaces the active product filters.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n setFilters(filters: ProductFilter[]): void;\n\n /**\n * Adds the filter if not active, removes it if already active.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n toggleFilter(filter: ProductFilter): void;\n\n /**\n * Changes the sort order.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n setSortKey(sortKey: ProductCollectionSortKeys, reverse?: boolean): void;\n\n /**\n * Resets filters and sort to defaults.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n reset(): void;\n\n /**\n * Returns `true` when URL params and current browse state describe the same filters and sort.\n */\n matchesParams(searchParams: URLSearchParams): boolean;\n\n /**\n * Applies URL search params to the store when they differ from current state.\n * Used when the framework router reports an external URL change (back/forward).\n */\n syncFromParams(searchParams: URLSearchParams): void;\n\n /**\n * Signals that the framework fetch completed and fresh data is available.\n * Resets `status` to `\"idle\"`.\n */\n settle(): void;\n\n /**\n * Serializes the store's current filter/sort state into URL search params.\n * Only includes store-owned keys (`filter.*`, `sort_by`).\n */\n serializeToParams(): URLSearchParams;\n\n /**\n * Builds a URL string that removes the given filter from the current params.\n * Useful for rendering \"remove filter\" links/buttons.\n */\n getFilterRemovalUrl(filter: ProductFilter): string;\n\n /**\n * Parses `FormData` from the submitted form into collection params and applies them.\n * Callers must call `event.preventDefault()` before invoking this method.\n */\n handleFormSubmit(event: SubmitEvent): void;\n\n /**\n * Replaces the callback that runs after user filter/sort changes. Framework\n * adapters use this to trigger navigation. Pass `null` to remove.\n */\n setOnBrowseChange(callback: (() => void) | null): void;\n\n /**\n * Parses a Storefront API filter input JSON string (e.g. from\n * `FilterValue.input`) and toggles the resulting filter.\n * No-op if `input` is not valid JSON.\n */\n toggleFilterInput(input: string): void;\n\n /**\n * Parses a Liquid-compatible `sort_by` value (e.g. `\"price-ascending\"`) and\n * applies the corresponding sort key and direction.\n */\n setSortByValue(sortByValue: string): void;\n};\n\n/** Options for creating a new {@link CollectionStore}. */\nexport type CreateCollectionStoreOptions = {\n /** Collection metadata from the framework loader. */\n data: CollectionData;\n /**\n * Live URL search string from the framework router (with or without leading `?`).\n * Falls back to `data.dataSearch` when omitted.\n */\n urlSearch?: string;\n /**\n * Called after user-initiated filter/sort changes.\n * Framework adapters use this to trigger navigation. Not called by\n * {@link CollectionStore.syncFromParams} or {@link CollectionStore.settle}.\n */\n onBrowseChange?: () => void;\n};\n\ntype CollectionStoreContext = {\n observable: ReturnType<typeof createObservable<CollectionState>>;\n handle: string;\n onBrowseChange: (() => void) | null;\n};\n\n/**\n * Creates a new collection store for the given collection handle.\n *\n * The store manages browse intent (filters, sort) only.\n * Server response data lives in the framework's loader.\n */\nexport function createCollectionStore(options: CreateCollectionStoreOptions): CollectionStore {\n const initialState = buildInitialState(options);\n\n const context: CollectionStoreContext = {\n observable: createObservable<CollectionState>(initialState),\n handle: options.data.handle,\n onBrowseChange: options.onBrowseChange ?? null,\n };\n\n return {\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n setFilters: (filters) => applyBrowseChange(context, { filters: normalizeFilters(filters) }),\n toggleFilter: (filter) => toggleFilter(context, filter),\n setSortKey: (sortKey, reverse) =>\n applyBrowseChange(context, { sortKey, reverse: reverse ?? false }),\n reset: () => resetStore(context),\n matchesParams: (searchParams) => matchesParams(context, searchParams),\n syncFromParams: (searchParams) => syncFromParams(context, searchParams),\n settle: () => settleStore(context),\n serializeToParams: () => serializeCollectionParams(context.observable.state),\n getFilterRemovalUrl: (filter) =>\n getFilterRemovalUrl(serializeCollectionParams(context.observable.state), filter),\n handleFormSubmit: (event) => handleFormSubmit(context, event),\n setOnBrowseChange: (callback) => {\n context.onBrowseChange = callback;\n },\n toggleFilterInput: (input) => toggleFilterInput(context, input),\n setSortByValue: (sortByValue) => setSortByValue(context, sortByValue),\n };\n}\n\nfunction buildInitialState(options: CreateCollectionStoreOptions): CollectionState {\n const base = createInitialCollectionState(options.data.handle);\n\n const search = options.urlSearch ?? options.data.dataSearch;\n if (search) {\n const parsed = parseCollectionParams(new URLSearchParams(search));\n base.filters = parsed.filters;\n base.sortKey = parsed.sortKey;\n base.reverse = normalizeReverse(parsed.sortKey, parsed.reverse);\n }\n\n return base;\n}\n\ntype BrowseChange = Partial<Pick<CollectionState, \"filters\" | \"sortKey\" | \"reverse\">>;\n\n/**\n * Drops `reverse` for sort keys that do not support direction.\n * Only `PRICE`, `TITLE`, `CREATED`, and `ID` honor ascending/descending.\n *\n * @example normalizeReverse(\"BEST_SELLING\", true) // → false\n * @example normalizeReverse(\"PRICE\", true) // → true\n */\nfunction normalizeReverse(\n sortKey: ProductCollectionSortKeys | undefined,\n reverse: boolean,\n): boolean {\n if (!sortKey || !isDirectionalSortKey(sortKey)) return false;\n return reverse;\n}\n\nfunction applyBrowseChange(\n context: CollectionStoreContext,\n change: BrowseChange,\n notify = true,\n): void {\n context.observable.setState((prev) => {\n const merged = { ...prev, ...change, status: \"loading\" as const };\n merged.reverse = normalizeReverse(merged.sortKey, merged.reverse);\n return merged;\n });\n if (notify) {\n context.onBrowseChange?.();\n }\n}\n\nfunction toggleFilter(context: CollectionStoreContext, filter: ProductFilter): void {\n const current = context.observable.state.filters;\n const isActive = current.some((f) => filterEquals(f, filter));\n\n const next = isActive\n ? current.filter((f) => !filterEquals(f, filter))\n : normalizeFilters([...current, filter]);\n\n applyBrowseChange(context, { filters: next });\n}\n\nfunction normalizeFilters(filters: ProductFilter[]): ProductFilter[] {\n const normalized: ProductFilter[] = [];\n\n for (const filter of filters) {\n if (filter.available != null) {\n // Availability is a single boolean facet. Duplicate availability params\n // (filter.v.availability=1&filter.v.availability=0) mean no filter, so\n // keeping both values here prevents the provider from settling navigation.\n const existingAvailabilityIndex = normalized.findIndex((candidate) => {\n return candidate.available != null;\n });\n\n if (existingAvailabilityIndex !== -1) {\n normalized.splice(existingAvailabilityIndex, 1);\n continue;\n }\n }\n\n normalized.push(filter);\n }\n\n return normalized;\n}\n\nfunction resetStore(context: CollectionStoreContext): void {\n const initial = createInitialCollectionState(context.handle);\n applyBrowseChange(context, {\n filters: initial.filters,\n sortKey: initial.sortKey,\n reverse: initial.reverse,\n });\n}\n\nfunction matchesParams(context: CollectionStoreContext, searchParams: URLSearchParams): boolean {\n return collectionParamsMatchState(searchParams, context.observable.state);\n}\n\nfunction syncFromParams(context: CollectionStoreContext, searchParams: URLSearchParams): void {\n if (matchesParams(context, searchParams)) return;\n\n const parsed = parseCollectionParams(searchParams);\n\n // URL caught up to an in-flight browse change — align state without re-entering loading.\n if (context.observable.state.status === \"loading\") {\n context.observable.setState((prev) => ({\n ...prev,\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: normalizeReverse(parsed.sortKey, parsed.reverse),\n status: \"loading\",\n }));\n return;\n }\n\n /**\n * URL already changes, so we don't need to notify to call onBrowseChange\n * again which would cause a double navigate.\n */\n applyBrowseChange(\n context,\n {\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: parsed.reverse,\n },\n false,\n );\n}\n\nfunction settleStore(context: CollectionStoreContext): void {\n context.observable.setState((prev) => {\n if (prev.status === \"idle\") return prev;\n return { ...prev, status: \"idle\" };\n });\n}\n\nfunction handleFormSubmit(context: CollectionStoreContext, event: SubmitEvent): void {\n if (!(event.target instanceof HTMLFormElement)) {\n throw new TypeError(`Expected event.target to be an HTMLFormElement, got ${event.target}`);\n }\n\n const formData = new FormData(event.target);\n const formParams = new URLSearchParams();\n\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\") {\n formParams.append(key, value);\n }\n }\n\n const parsed = parseCollectionParams(formParams);\n\n const change: BrowseChange = {\n filters: normalizeFilters(parsed.filters),\n };\n\n if (formParams.has(\"sort_by\")) {\n change.sortKey = parsed.sortKey;\n change.reverse = parsed.reverse;\n }\n\n applyBrowseChange(context, change);\n}\n\nfunction toggleFilterInput(context: CollectionStoreContext, input: string): void {\n let parsed: ProductFilter;\n try {\n parsed = JSON.parse(input) as ProductFilter;\n } catch {\n return;\n }\n toggleFilter(context, parsed);\n}\n\nfunction setSortByValue(context: CollectionStoreContext, sortByValue: string): void {\n const { sortKey, reverse } = parseSortByValue(sortByValue);\n applyBrowseChange(context, {\n sortKey: sortKey ?? \"COLLECTION_DEFAULT\",\n reverse: reverse ?? false,\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection.mjs","names":[],"sources":["../../../../src/core/collection/collection.ts"],"sourcesContent":["import type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\nimport { createObservable } from \"../observable\";\nimport type { CollectionState, ProductFilter } from \"./state\";\nimport { createInitialCollectionState } from \"./state\";\nimport {\n collectionParamsMatchState,\n filterEquals,\n getFilterRemovalUrl,\n isDirectionalSortKey,\n parseCollectionParams,\n parseSortByValue,\n serializeCollectionParams,\n} from \"./url\";\n\n/** Snapshot from the framework loader for a single collection fetch. */\nexport type CollectionData = {\n /** URL-safe collection slug (e.g. `\"shoes\"`). */\n handle: string;\n /**\n * Search string this data snapshot was fetched for (with or without leading `?`).\n * Compared against live {@link CreateCollectionStoreOptions.urlSearch} before settling.\n */\n dataSearch: string;\n};\n\n/**\n * A reactive, framework-agnostic store for collection browse state.\n *\n * Manages filters and sorting — the user's *intent*. Server response\n * data (products, productsCount, availableFilters) lives in the framework's\n * loader data, not here.\n *\n * Mutations are synchronous state changes that set `status: \"loading\"`.\n * The framework adapter navigates (URL change), the framework loader re-runs,\n * and the adapter calls {@link settle} when fresh data arrives.\n *\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: { handle: \"shoes\", dataSearch: \"\" },\n * });\n *\n * store.setFilters([{ tag: \"sale\" }]); // status → \"loading\"\n * // ...framework fetches...\n * store.settle(); // status → \"idle\"\n * ```\n */\nexport type CollectionStore = {\n /** Returns the current snapshot of collection browse state. */\n getState(): CollectionState;\n\n /**\n * Registers a listener invoked on every state change. Returns an unsubscribe function.\n * @param listener - Callback receiving the new state snapshot\n */\n subscribe(listener: (state: CollectionState) => void): () => void;\n\n /**\n * Replaces the active product filters.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n setFilters(filters: ProductFilter[]): void;\n\n /**\n * Adds the filter if not active, removes it if already active.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n toggleFilter(filter: ProductFilter): void;\n\n /**\n * Changes the sort order.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n setSortKey(sortKey: ProductCollectionSortKeys, reverse?: boolean): void;\n\n /**\n * Resets filters and sort to defaults.\n * Sets `status: \"loading\"` — call {@link settle} when the framework fetch completes.\n */\n reset(): void;\n\n /**\n * Returns `true` when URL params and current browse state describe the same filters and sort.\n */\n matchesParams(searchParams: URLSearchParams): boolean;\n\n /**\n * Applies URL search params to the store when they differ from current state.\n * Used when the framework router reports an external URL change (back/forward).\n */\n syncFromParams(searchParams: URLSearchParams): void;\n\n /**\n * Signals that the framework fetch completed and fresh data is available.\n * Resets `status` to `\"idle\"`.\n */\n settle(): void;\n\n /**\n * Serializes the store's current filter/sort state into URL search params.\n * Only includes store-owned keys (`filter.*`, `sort_by`).\n */\n serializeToParams(): URLSearchParams;\n\n /**\n * Builds a URL string that removes the given filter from the current params.\n * Useful for rendering \"remove filter\" links/buttons.\n */\n getFilterRemovalUrl(filter: ProductFilter): string;\n\n /**\n * Parses `FormData` from the submitted form into collection params and applies them.\n * Doesn't cancel the event: call `event.preventDefault()` first, or the browser\n * submits the form natively and navigates away.\n *\n * @throws {TypeError} If `event.target` is not an `HTMLFormElement`.\n */\n handleFormSubmit(event: SubmitEvent): void;\n\n /**\n * Replaces the callback that runs after user filter/sort changes. Framework\n * adapters use this to trigger navigation. Pass `null` to remove.\n */\n setOnBrowseChange(callback: (() => void) | null): void;\n\n /**\n * Parses a Storefront API filter input JSON string (e.g. from\n * `FilterValue.input`) and toggles the resulting filter.\n * No-op if `input` is not valid JSON.\n */\n toggleFilterInput(input: string): void;\n\n /**\n * Parses a Liquid-compatible `sort_by` value (e.g. `\"price-ascending\"`) and\n * applies the corresponding sort key and direction.\n */\n setSortByValue(sortByValue: string): void;\n};\n\n/** Mutation methods exposed by the collection store. */\nexport type CollectionActions = Pick<\n CollectionStore,\n | \"setFilters\"\n | \"toggleFilter\"\n | \"toggleFilterInput\"\n | \"setSortKey\"\n | \"setSortByValue\"\n | \"reset\"\n | \"handleFormSubmit\"\n>;\n\n/** Options for creating a new {@link CollectionStore}. */\nexport type CreateCollectionStoreOptions = {\n /** Collection metadata from the framework loader. */\n data: CollectionData;\n /**\n * Live URL search string from the framework router (with or without leading `?`).\n * Falls back to `data.dataSearch` when omitted.\n */\n urlSearch?: string;\n /**\n * Called after user-initiated filter/sort changes.\n * Framework adapters use this to trigger navigation. Not called by\n * {@link CollectionStore.syncFromParams} or {@link CollectionStore.settle}.\n */\n onBrowseChange?: () => void;\n};\n\ntype CollectionStoreContext = {\n observable: ReturnType<typeof createObservable<CollectionState>>;\n handle: string;\n onBrowseChange: (() => void) | null;\n};\n\n/**\n * Creates a new collection store for the given collection handle.\n *\n * The store manages browse intent (filters, sort) only.\n * Server response data lives in the framework's loader.\n *\n * @example\n * ```ts\n * const store = createCollectionStore({\n * data: loaderData.collection,\n * urlSearch: window.location.search,\n * onBrowseChange: () => navigate({ search: store.serializeToParams().toString() }),\n * });\n *\n * store.subscribe((state) => renderFilters(state.filters));\n * ```\n */\nexport function createCollectionStore(options: CreateCollectionStoreOptions): CollectionStore {\n const initialState = buildInitialState(options);\n\n const context: CollectionStoreContext = {\n observable: createObservable<CollectionState>(initialState),\n handle: options.data.handle,\n onBrowseChange: options.onBrowseChange ?? null,\n };\n\n return {\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n setFilters: (filters) => applyBrowseChange(context, { filters: normalizeFilters(filters) }),\n toggleFilter: (filter) => toggleFilter(context, filter),\n setSortKey: (sortKey, reverse) =>\n applyBrowseChange(context, { sortKey, reverse: reverse ?? false }),\n reset: () => resetStore(context),\n matchesParams: (searchParams) => matchesParams(context, searchParams),\n syncFromParams: (searchParams) => syncFromParams(context, searchParams),\n settle: () => settleStore(context),\n serializeToParams: () => serializeCollectionParams(context.observable.state),\n getFilterRemovalUrl: (filter) =>\n getFilterRemovalUrl(serializeCollectionParams(context.observable.state), filter),\n handleFormSubmit: (event) => handleFormSubmit(context, event),\n setOnBrowseChange: (callback) => {\n context.onBrowseChange = callback;\n },\n toggleFilterInput: (input) => toggleFilterInput(context, input),\n setSortByValue: (sortByValue) => setSortByValue(context, sortByValue),\n };\n}\n\nfunction buildInitialState(options: CreateCollectionStoreOptions): CollectionState {\n const base = createInitialCollectionState(options.data.handle);\n\n const search = options.urlSearch ?? options.data.dataSearch;\n if (search) {\n const parsed = parseCollectionParams(new URLSearchParams(search));\n base.filters = parsed.filters;\n base.sortKey = parsed.sortKey;\n base.reverse = normalizeReverse(parsed.sortKey, parsed.reverse);\n }\n\n return base;\n}\n\ntype BrowseChange = Partial<Pick<CollectionState, \"filters\" | \"sortKey\" | \"reverse\">>;\n\n/**\n * Drops `reverse` for sort keys that do not support direction.\n * Only `PRICE`, `TITLE`, `CREATED`, and `ID` honor ascending/descending.\n *\n * @example normalizeReverse(\"BEST_SELLING\", true) // → false\n * @example normalizeReverse(\"PRICE\", true) // → true\n */\nfunction normalizeReverse(\n sortKey: ProductCollectionSortKeys | undefined,\n reverse: boolean,\n): boolean {\n if (!sortKey || !isDirectionalSortKey(sortKey)) return false;\n return reverse;\n}\n\nfunction applyBrowseChange(\n context: CollectionStoreContext,\n change: BrowseChange,\n notify = true,\n): void {\n context.observable.setState((prev) => {\n const merged = { ...prev, ...change, status: \"loading\" as const };\n merged.reverse = normalizeReverse(merged.sortKey, merged.reverse);\n return merged;\n });\n if (notify) {\n context.onBrowseChange?.();\n }\n}\n\nfunction toggleFilter(context: CollectionStoreContext, filter: ProductFilter): void {\n const current = context.observable.state.filters;\n const isActive = current.some((f) => filterEquals(f, filter));\n\n const next = isActive\n ? current.filter((f) => !filterEquals(f, filter))\n : normalizeFilters([...current, filter]);\n\n applyBrowseChange(context, { filters: next });\n}\n\nfunction normalizeFilters(filters: ProductFilter[]): ProductFilter[] {\n const normalized: ProductFilter[] = [];\n\n for (const filter of filters) {\n if (filter.available != null) {\n // Availability is a single boolean facet. Duplicate availability params\n // (filter.v.availability=1&filter.v.availability=0) mean no filter, so\n // keeping both values here prevents the provider from settling navigation.\n const existingAvailabilityIndex = normalized.findIndex((candidate) => {\n return candidate.available != null;\n });\n\n if (existingAvailabilityIndex !== -1) {\n normalized.splice(existingAvailabilityIndex, 1);\n continue;\n }\n }\n\n normalized.push(filter);\n }\n\n return normalized;\n}\n\nfunction resetStore(context: CollectionStoreContext): void {\n const initial = createInitialCollectionState(context.handle);\n applyBrowseChange(context, {\n filters: initial.filters,\n sortKey: initial.sortKey,\n reverse: initial.reverse,\n });\n}\n\nfunction matchesParams(context: CollectionStoreContext, searchParams: URLSearchParams): boolean {\n return collectionParamsMatchState(searchParams, context.observable.state);\n}\n\nfunction syncFromParams(context: CollectionStoreContext, searchParams: URLSearchParams): void {\n if (matchesParams(context, searchParams)) return;\n\n const parsed = parseCollectionParams(searchParams);\n\n // URL caught up to an in-flight browse change — align state without re-entering loading.\n if (context.observable.state.status === \"loading\") {\n context.observable.setState((prev) => ({\n ...prev,\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: normalizeReverse(parsed.sortKey, parsed.reverse),\n status: \"loading\",\n }));\n return;\n }\n\n /**\n * URL already changes, so we don't need to notify to call onBrowseChange\n * again which would cause a double navigate.\n */\n applyBrowseChange(\n context,\n {\n filters: parsed.filters,\n sortKey: parsed.sortKey,\n reverse: parsed.reverse,\n },\n false,\n );\n}\n\nfunction settleStore(context: CollectionStoreContext): void {\n context.observable.setState((prev) => {\n if (prev.status === \"idle\") return prev;\n return { ...prev, status: \"idle\" };\n });\n}\n\nfunction handleFormSubmit(context: CollectionStoreContext, event: SubmitEvent): void {\n if (!(event.target instanceof HTMLFormElement)) {\n throw new TypeError(`Expected event.target to be an HTMLFormElement, got ${event.target}`);\n }\n\n const formData = new FormData(event.target);\n const formParams = new URLSearchParams();\n\n for (const [key, value] of formData.entries()) {\n if (typeof value === \"string\") {\n formParams.append(key, value);\n }\n }\n\n const parsed = parseCollectionParams(formParams);\n\n const change: BrowseChange = {\n filters: normalizeFilters(parsed.filters),\n };\n\n if (formParams.has(\"sort_by\")) {\n change.sortKey = parsed.sortKey;\n change.reverse = parsed.reverse;\n }\n\n applyBrowseChange(context, change);\n}\n\nfunction toggleFilterInput(context: CollectionStoreContext, input: string): void {\n let parsed: ProductFilter;\n try {\n parsed = JSON.parse(input) as ProductFilter;\n } catch {\n return;\n }\n toggleFilter(context, parsed);\n}\n\nfunction setSortByValue(context: CollectionStoreContext, sortByValue: string): void {\n const { sortKey, reverse } = parseSortByValue(sortByValue);\n applyBrowseChange(context, {\n sortKey: sortKey ?? \"COLLECTION_DEFAULT\",\n reverse: reverse ?? false,\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProductCollectionSortKeys } from "../../graphql/generated/storefront-api-types.mjs";
|
|
2
2
|
import { ProductFilter } from "../../vendor/standard-events.mjs";
|
|
3
3
|
import { AvailableFilter, AvailableFilterValue, CollectionState, FilterPresentation, FilterType, createInitialCollectionState } from "./state.mjs";
|
|
4
|
-
import { CollectionData, CollectionStore, CreateCollectionStoreOptions, createCollectionStore } from "./collection.mjs";
|
|
4
|
+
import { CollectionActions, CollectionData, CollectionStore, CreateCollectionStoreOptions, createCollectionStore } from "./collection.mjs";
|
|
5
5
|
import { CollectionReconciler, ReconcilerCallbacks, createCollectionReconciler } from "./reconciler.mjs";
|
|
6
6
|
import { CollectionParams, filterEquals, getFilterRemovalUrl, getSortByValue, isFilterInputActive, normalizeCollectionSearch, parseCollectionParams, parseSortByValue, serializeCollectionParams } from "./url.mjs";
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { CollectionStore } from "./collection.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/core/collection/reconciler.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Adapter-supplied hooks that let the reconciler read framework state and
|
|
6
|
+
* push navigation side-effects without depending on any specific framework.
|
|
7
|
+
*/
|
|
4
8
|
type ReconcilerCallbacks = {
|
|
5
|
-
getStore: () => CollectionStore;
|
|
6
|
-
readUrlSearch: () => string;
|
|
9
|
+
/** Returns the current collection store instance. */getStore: () => CollectionStore; /** Reads the live URL search string from the framework router. */
|
|
10
|
+
readUrlSearch: () => string; /** Delivers a serialized search string for the framework adapter to handle. */
|
|
7
11
|
emitChange: (searchString: string) => void;
|
|
8
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* State machine that keeps the URL, server data, and `CollectionStore` in
|
|
15
|
+
* sync during browse-change chains. Created by `createCollectionReconciler`.
|
|
16
|
+
*/
|
|
9
17
|
type CollectionReconciler = {
|
|
10
18
|
/**
|
|
11
|
-
* URL ↔ store reconciliation state machine. Call whenever urlSearch
|
|
12
|
-
* dataSearch
|
|
19
|
+
* URL ↔ store reconciliation state machine. Call whenever `urlSearch` or
|
|
20
|
+
* `dataSearch` changes. Store changes go through
|
|
21
|
+
* {@link CollectionReconciler.handleBrowseChange} instead.
|
|
13
22
|
*/
|
|
14
23
|
reconcile(urlSearch: string, dataSearch: string): void;
|
|
15
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.mjs","names":[],"sources":["../../../../src/core/collection/reconciler.ts"],"sourcesContent":["import type { CollectionStore } from \"./collection\";\nimport { collectionSearchEqual, mergeCollectionParams, normalizeCollectionSearch } from \"./url\";\n\nexport type ReconcilerCallbacks = {\n getStore: () => CollectionStore;\n readUrlSearch: () => string;\n emitChange: (searchString: string) => void;\n};\n\nexport type CollectionReconciler = {\n /**\n * URL ↔ store reconciliation state machine. Call whenever urlSearch
|
|
1
|
+
{"version":3,"file":"reconciler.mjs","names":[],"sources":["../../../../src/core/collection/reconciler.ts"],"sourcesContent":["import type { CollectionStore } from \"./collection\";\nimport { collectionSearchEqual, mergeCollectionParams, normalizeCollectionSearch } from \"./url\";\n\n/**\n * Adapter-supplied hooks that let the reconciler read framework state and\n * push navigation side-effects without depending on any specific framework.\n */\nexport type ReconcilerCallbacks = {\n /** Returns the current collection store instance. */\n getStore: () => CollectionStore;\n /** Reads the live URL search string from the framework router. */\n readUrlSearch: () => string;\n /** Delivers a serialized search string for the framework adapter to handle. */\n emitChange: (searchString: string) => void;\n};\n\n/**\n * State machine that keeps the URL, server data, and `CollectionStore` in\n * sync during browse-change chains. Created by `createCollectionReconciler`.\n */\nexport type CollectionReconciler = {\n /**\n * URL ↔ store reconciliation state machine. Call whenever `urlSearch` or\n * `dataSearch` changes. Store changes go through\n * {@link CollectionReconciler.handleBrowseChange} instead.\n */\n reconcile(urlSearch: string, dataSearch: string): void;\n /**\n * Callback for {@link CollectionStore.setOnBrowseChange}. Merges current\n * URL params with store state, tracks the pending navigation, and emits.\n */\n handleBrowseChange(): void;\n /** Clears pending navigation state (call when the store is recreated). */\n reset(newPrevUrlSearch: string): void;\n};\n\n/**\n * Creates a framework-agnostic reconciler that manages the state machine for\n * keeping the URL, server data, and store in sync during browse-change chains.\n *\n * Framework adapters delegate to this so the reconciliation logic\n * lives in one place.\n */\nexport function createCollectionReconciler(\n callbacks: ReconcilerCallbacks,\n initialPrevUrlSearch = \"\",\n): CollectionReconciler {\n let pendingFilter: string | null = null;\n let pendingUrls = new Set<string>();\n let prevUrlSearch = initialPrevUrlSearch;\n\n return {\n handleBrowseChange() {\n const store = callbacks.getStore();\n const merged = mergeCollectionParams(\n new URLSearchParams(callbacks.readUrlSearch()),\n store.getState(),\n );\n const searchString = merged.toString();\n pendingFilter = searchString;\n pendingUrls.add(searchString);\n callbacks.emitChange(searchString ? `?${searchString}` : \"\");\n },\n\n reset(newPrevUrlSearch: string) {\n pendingFilter = null;\n pendingUrls = new Set();\n prevUrlSearch = newPrevUrlSearch;\n },\n\n // State machine — four states:\n //\n // ┌─────────────────────────────────────────────────────────────────────┐\n // │ State A: No pending navigation (pendingFilter === null) │\n // │ • URL matches store → try settle if server data caught up │\n // │ • URL differs from store → syncFromParams, then try settle │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State B: Pending — URL matches target (pendingTargetMatchesUrl) │\n // │ • Server data matches URL → settle, clear pending state │\n // │ • Server data stale (intermediate) → re-dispatch to target │\n // │ • Server data not yet arrived → wait (keep pending set) │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State C: Pending — URL is intermediate (in pendingUrls) │\n // │ • Ignore URL change; if router settled here with server data │\n // │ confirming the intermediate, re-dispatch to the real target │\n // ├─────────────────────────────────────────────────────────────────────┤\n // │ State D: Pending — URL is unknown (not in pendingUrls) │\n // │ • External navigation (back/forward, <Link>) │\n // │ • Abandon pending state, fall through to State A │\n // └─────────────────────────────────────────────────────────────────────┘\n //\n // Rapid toggles (A on → B on → A off) produce multiple in-flight navigations.\n // pendingFilter tracks the latest target; pendingUrls tracks every URL\n // dispatched in the chain so intermediates can be distinguished from external navs.\n reconcile(urlSearch: string, dataSearch: string) {\n const store = callbacks.getStore();\n\n const pendingSearchEquals = (search: string): boolean =>\n [...pendingUrls].some((pending) => collectionSearchEqual(pending, search));\n\n const pendingTargetMatchesUrl =\n pendingFilter != null && collectionSearchEqual(pendingFilter, urlSearch);\n\n const trySettleFromServer = (): boolean => {\n if (store.getState().status !== \"loading\") return false;\n if (!collectionSearchEqual(dataSearch, urlSearch)) return false;\n\n const incomingParams = new URLSearchParams(urlSearch);\n if (!store.matchesParams(incomingParams)) {\n store.syncFromParams(incomingParams);\n }\n\n store.settle();\n return true;\n };\n\n const clearPendingIfSettled = (): boolean => {\n if (!trySettleFromServer()) return false;\n pendingFilter = null;\n pendingUrls.clear();\n return true;\n };\n\n if (clearPendingIfSettled()) return;\n\n const normalizedUrlSearch = normalizeCollectionSearch(urlSearch);\n const normalizedPrevUrlSearch = normalizeCollectionSearch(prevUrlSearch);\n const urlSearchChanged = normalizedUrlSearch !== normalizedPrevUrlSearch;\n prevUrlSearch = urlSearch;\n\n const reDispatchToPendingTarget = () => {\n if (pendingFilter == null) return;\n callbacks.emitChange(pendingFilter ? `?${pendingFilter}` : \"\");\n };\n\n const routerSettledOnIntermediateWithStaleTarget =\n pendingFilter != null &&\n pendingSearchEquals(urlSearch) &&\n !pendingTargetMatchesUrl &&\n collectionSearchEqual(dataSearch, urlSearch);\n\n const serverConfirmedIntermediateWhileTargeting =\n pendingFilter != null &&\n pendingSearchEquals(dataSearch) &&\n !collectionSearchEqual(dataSearch, pendingFilter) &&\n !collectionSearchEqual(dataSearch, urlSearch);\n\n if (pendingTargetMatchesUrl) {\n if (trySettleFromServer()) {\n pendingFilter = null;\n pendingUrls.clear();\n } else if (\n store.getState().status === \"loading\" &&\n serverConfirmedIntermediateWhileTargeting\n ) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n if (pendingFilter != null) {\n if (!urlSearchChanged) {\n if (clearPendingIfSettled()) return;\n if (routerSettledOnIntermediateWithStaleTarget) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n if (pendingSearchEquals(urlSearch)) {\n if (clearPendingIfSettled()) return;\n if (routerSettledOnIntermediateWithStaleTarget) {\n reDispatchToPendingTarget();\n }\n return;\n }\n\n pendingFilter = null;\n pendingUrls.clear();\n }\n\n const incomingParams = new URLSearchParams(urlSearch);\n\n if (store.matchesParams(incomingParams)) {\n trySettleFromServer();\n return;\n }\n\n store.syncFromParams(incomingParams);\n trySettleFromServer();\n },\n };\n}\n"],"mappings":";;;;;;;;;;AA2CA,SAAgB,2BACd,WACA,uBAAuB,IACD;CACtB,IAAI,gBAA+B;CACnC,IAAI,8BAAc,IAAI,KAAa;CACnC,IAAI,gBAAgB;AAEpB,QAAO;EACL,qBAAqB;GACnB,MAAM,QAAQ,UAAU,UAAU;GAKlC,MAAM,eAJS,sBACb,IAAI,gBAAgB,UAAU,eAAe,CAAC,EAC9C,MAAM,UAAU,CAES,CAAC,UAAU;AACtC,mBAAgB;AAChB,eAAY,IAAI,aAAa;AAC7B,aAAU,WAAW,eAAe,IAAI,iBAAiB,GAAG;;EAG9D,MAAM,kBAA0B;AAC9B,mBAAgB;AAChB,iCAAc,IAAI,KAAK;AACvB,mBAAgB;;EA2BlB,UAAU,WAAmB,YAAoB;GAC/C,MAAM,QAAQ,UAAU,UAAU;GAElC,MAAM,uBAAuB,WAC3B,CAAC,GAAG,YAAY,CAAC,MAAM,YAAY,sBAAsB,SAAS,OAAO,CAAC;GAE5E,MAAM,0BACJ,iBAAiB,QAAQ,sBAAsB,eAAe,UAAU;GAE1E,MAAM,4BAAqC;AACzC,QAAI,MAAM,UAAU,CAAC,WAAW,UAAW,QAAO;AAClD,QAAI,CAAC,sBAAsB,YAAY,UAAU,CAAE,QAAO;IAE1D,MAAM,iBAAiB,IAAI,gBAAgB,UAAU;AACrD,QAAI,CAAC,MAAM,cAAc,eAAe,CACtC,OAAM,eAAe,eAAe;AAGtC,UAAM,QAAQ;AACd,WAAO;;GAGT,MAAM,8BAAuC;AAC3C,QAAI,CAAC,qBAAqB,CAAE,QAAO;AACnC,oBAAgB;AAChB,gBAAY,OAAO;AACnB,WAAO;;AAGT,OAAI,uBAAuB,CAAE;GAI7B,MAAM,mBAFsB,0BAA0B,UAEV,KADZ,0BAA0B,cACc;AACxE,mBAAgB;GAEhB,MAAM,kCAAkC;AACtC,QAAI,iBAAiB,KAAM;AAC3B,cAAU,WAAW,gBAAgB,IAAI,kBAAkB,GAAG;;GAGhE,MAAM,6CACJ,iBAAiB,QACjB,oBAAoB,UAAU,IAC9B,CAAC,2BACD,sBAAsB,YAAY,UAAU;GAE9C,MAAM,4CACJ,iBAAiB,QACjB,oBAAoB,WAAW,IAC/B,CAAC,sBAAsB,YAAY,cAAc,IACjD,CAAC,sBAAsB,YAAY,UAAU;AAE/C,OAAI,yBAAyB;AAC3B,QAAI,qBAAqB,EAAE;AACzB,qBAAgB;AAChB,iBAAY,OAAO;eAEnB,MAAM,UAAU,CAAC,WAAW,aAC5B,0CAEA,4BAA2B;AAE7B;;AAGF,OAAI,iBAAiB,MAAM;AACzB,QAAI,CAAC,kBAAkB;AACrB,SAAI,uBAAuB,CAAE;AAC7B,SAAI,2CACF,4BAA2B;AAE7B;;AAGF,QAAI,oBAAoB,UAAU,EAAE;AAClC,SAAI,uBAAuB,CAAE;AAC7B,SAAI,2CACF,4BAA2B;AAE7B;;AAGF,oBAAgB;AAChB,gBAAY,OAAO;;GAGrB,MAAM,iBAAiB,IAAI,gBAAgB,UAAU;AAErD,OAAI,MAAM,cAAc,eAAe,EAAE;AACvC,yBAAqB;AACrB;;AAGF,SAAM,eAAe,eAAe;AACpC,wBAAqB;;EAExB"}
|
|
@@ -4,10 +4,9 @@ import { ProductFilter } from "../../vendor/standard-events.mjs";
|
|
|
4
4
|
//#region src/core/collection/state.d.ts
|
|
5
5
|
/** How a filter option is visually presented in the storefront UI. */
|
|
6
6
|
type FilterPresentation = "IMAGE" | "SWATCH" | "TEXT";
|
|
7
|
-
/** The input mechanism a filter uses — boolean toggle, multi-select list, or price
|
|
7
|
+
/** The input mechanism a filter uses — e.g. boolean toggle, multi-select list, or price range. */
|
|
8
8
|
type FilterType = "BOOLEAN" | "LIST" | "PRICE_RANGE" | (string & {});
|
|
9
9
|
interface BaseAvailableFilterValue {
|
|
10
|
-
/** Unique identifier for this filter value. */
|
|
11
10
|
id: string;
|
|
12
11
|
/** Human-readable label (e.g. "Red", "Nike", "$50–$100"). */
|
|
13
12
|
label: string;
|
|
@@ -30,8 +29,14 @@ type AvailableFilterValue<TValue extends BaseAvailableFilterValue = BaseAvailabl
|
|
|
30
29
|
*/
|
|
31
30
|
interface AvailableFilter<TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue> {
|
|
32
31
|
id: string;
|
|
32
|
+
/** Human-readable name (e.g. "Color", "Size", "Price"). */
|
|
33
33
|
label: string;
|
|
34
|
+
/** Input mechanism — e.g. boolean toggle, multi-select list, or price range. */
|
|
34
35
|
type: FilterType;
|
|
36
|
+
/**
|
|
37
|
+
* Visual presentation hint for the storefront UI. Populated only for `LIST`
|
|
38
|
+
* filters; `null` otherwise.
|
|
39
|
+
*/
|
|
35
40
|
presentation?: FilterPresentation | null;
|
|
36
41
|
values: AvailableFilterValue<TValue>[];
|
|
37
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.mjs","names":[],"sources":["../../../../src/core/collection/state.ts"],"sourcesContent":["import type { ProductFilter } from \"../../../vendor/standard-events\";\nimport type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\n\nexport type { ProductCollectionSortKeys, ProductFilter };\n\n/** How a filter option is visually presented in the storefront UI. */\nexport type FilterPresentation = \"IMAGE\" | \"SWATCH\" | \"TEXT\";\n\n/** The input mechanism a filter uses — boolean toggle, multi-select list, or price
|
|
1
|
+
{"version":3,"file":"state.mjs","names":[],"sources":["../../../../src/core/collection/state.ts"],"sourcesContent":["import type { ProductFilter } from \"../../../vendor/standard-events\";\nimport type { ProductCollectionSortKeys } from \"../../graphql/generated/storefront-api-types\";\n\nexport type { ProductCollectionSortKeys, ProductFilter };\n\n/** How a filter option is visually presented in the storefront UI. */\nexport type FilterPresentation = \"IMAGE\" | \"SWATCH\" | \"TEXT\";\n\n/** The input mechanism a filter uses — e.g. boolean toggle, multi-select list, or price range. */\nexport type FilterType = \"BOOLEAN\" | \"LIST\" | \"PRICE_RANGE\" | (string & {});\n\ninterface BaseAvailableFilterValue {\n id: string;\n /** Human-readable label (e.g. \"Red\", \"Nike\", \"$50–$100\"). */\n label: string;\n /** Number of products matching this value in the current result set. */\n count: number;\n /** JSON-encoded `ProductFilter` input to apply this value. */\n input: string;\n}\n\ntype PickIfPresent<TValue, TKey extends PropertyKey> = TKey extends keyof TValue\n ? Pick<TValue, TKey>\n : {};\n\n/**\n * A single selectable value within an {@link AvailableFilter}.\n *\n * Pass your Storefront API query value type as `TValue` to expose query-selected\n * visual fields such as `swatch`.\n */\nexport type AvailableFilterValue<\n TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue,\n> = BaseAvailableFilterValue & PickIfPresent<TValue, \"swatch\">;\n\n/**\n * A filter facet available for the current collection, returned by the\n * Storefront API. Used to render filter UI (checkboxes, swatches, sliders).\n */\nexport interface AvailableFilter<\n TValue extends BaseAvailableFilterValue = BaseAvailableFilterValue,\n> {\n id: string;\n /** Human-readable name (e.g. \"Color\", \"Size\", \"Price\"). */\n label: string;\n /** Input mechanism — e.g. boolean toggle, multi-select list, or price range. */\n type: FilterType;\n /**\n * Visual presentation hint for the storefront UI. Populated only for `LIST`\n * filters; `null` otherwise.\n */\n presentation?: FilterPresentation | null;\n values: AvailableFilterValue<TValue>[];\n}\n\n/**\n * Pure browse-intent state for a collection store. Represents what the user\n * wants to see (filters, sort), not what the server returned.\n *\n * Server response data (productsCount, availableFilters, collection id) lives\n * in the framework's loader data, not in the store.\n */\nexport interface CollectionState {\n /** URL-safe slug identifying the collection (e.g. `\"shoes\"`). */\n handle: string;\n\n /** Active product filters, synced bidirectionally with URL params (`filter.*`). */\n filters: ProductFilter[];\n /** Storefront API sort key. `undefined` means the collection's default sort order. */\n sortKey: ProductCollectionSortKeys | undefined;\n /** When `true`, the sort direction is descending (maps to `sort_by` suffix `-descending`). */\n reverse: boolean;\n /** `\"idle\"` when browse state matches the last-loaded data, `\"loading\"` after a browse change before the framework fetch settles. */\n status: \"idle\" | \"loading\";\n}\n\n/**\n * Creates a blank {@link CollectionState} for the given collection handle.\n * All browse fields start at their zero values.\n *\n * @param handle - URL-safe collection slug (e.g. `\"shoes\"`)\n */\nexport function createInitialCollectionState(handle: string): CollectionState {\n return {\n handle,\n filters: [],\n sortKey: undefined,\n reverse: false,\n status: \"idle\",\n };\n}\n"],"mappings":";;;;;;;AAkFA,SAAgB,6BAA6B,QAAiC;AAC5E,QAAO;EACL;EACA,SAAS,EAAE;EACX,SAAS;EACT,SAAS;EACT,QAAQ;EACT"}
|
|
@@ -8,8 +8,11 @@ import { CollectionState } from "./state.mjs";
|
|
|
8
8
|
* that is derived from the URL query string.
|
|
9
9
|
*/
|
|
10
10
|
interface CollectionParams {
|
|
11
|
+
/** Active product filters parsed from `filter.*` URL keys. */
|
|
11
12
|
filters: ProductFilter[];
|
|
13
|
+
/** Storefront API sort key parsed from the `sort_by` param. `undefined` when absent. */
|
|
12
14
|
sortKey: ProductCollectionSortKeys | undefined;
|
|
15
|
+
/** `true` when the `sort_by` value ends with `-descending`. */
|
|
13
16
|
reverse: boolean;
|
|
14
17
|
}
|
|
15
18
|
/**
|