@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.mjs","names":[],"sources":["../../../src/core/predictive-search/store.ts"],"sourcesContent":["import type {\n PredictiveSearchLimitScope,\n PredictiveSearchType,\n SearchUnavailableProductsType,\n SearchableField,\n} from \"../../graphql/generated/storefront-api-types\";\nimport { createObservable } from \"../observable\";\nimport { PREDICTIVE_SEARCH_API_PATH, PREDICTIVE_SEARCH_QUERY_PARAM } from \"./constants\";\nimport { getEmptyPredictiveSearchResult, type PredictiveSearchData } from \"./search\";\n\n/** Default delay before predictive search sends a request after a term changes. */\nexport const DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS = 150;\n\n/** Default minimum trimmed term length required before predictive search sends a request. */\nconst DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH = 1;\n\n/** Lifecycle state for the predictive search client store. */\nexport type PredictiveSearchStatus = \"idle\" | \"loading\" | \"success\" | \"error\";\n\n/** Current predictive search snapshot exposed to UI bindings. */\nexport type PredictiveSearchState<TData extends PredictiveSearchData = PredictiveSearchData> = {\n /** Latest trimmed search term known to the store. */\n term: string;\n /** Request lifecycle state for the current term. */\n status: PredictiveSearchStatus;\n /** Predictive search data for the current term, or an empty result while idle. */\n result: TData;\n /** Error message for the latest failed request, or null when there is no active error. */\n error: string | null;\n};\n\n/** Options for creating a framework-neutral predictive search client store. */\nexport type CreatePredictiveSearchStoreOptions = {\n /**\n * Same-origin JSON endpoint used for browser predictive-search requests.\n *\n * The endpoint must return JSON matching the data payload from\n * `await predictiveSearchHandlers.get(...)`.\n */\n predictiveSearchEndpoint?: string;\n /** Delay before sending a request after the term changes. */\n debounceInMs?: number;\n /** Minimum trimmed term length required before searching. */\n minTermLength?: number;\n /** Fetch implementation, injectable for tests and non-browser runtimes. */\n fetch?: typeof globalThis.fetch;\n /** Maximum result count requested from the Storefront API. */\n limit?: number;\n /** Whether `limit` applies to each result type or all result types combined. */\n limitScope?: PredictiveSearchLimitScope;\n /** Storefront API result types to include in predictive search. */\n types?: PredictiveSearchType[];\n /** Storefront API fields to search. */\n searchableFields?: SearchableField[];\n /** Storefront API unavailable-product behavior. */\n unavailableProducts?: SearchUnavailableProductsType;\n};\n\n/** Framework-neutral predictive search store used by UI bindings. */\nexport type PredictiveSearchStore<TData extends PredictiveSearchData = PredictiveSearchData> = {\n /** Reactivates store updates after destroy(). */\n connect(): void;\n /** Returns the current predictive search state snapshot. */\n getState(): PredictiveSearchState<TData>;\n /** Subscribes to state changes and returns an unsubscribe callback. */\n subscribe(listener: (state: PredictiveSearchState<TData>) => void): () => void;\n /** Searches for a term using the configured debounce and request lifecycle. */\n search(term: string): Promise<void>;\n /** Cancels pending work and resets the store to an empty state. */\n clear(): void;\n /** Cancels pending work and pauses store updates until reconnect. */\n destroy(): void;\n};\n\ntype PredictiveSearchStoreContext<TData extends PredictiveSearchData> = {\n observable: ReturnType<typeof createObservable<PredictiveSearchState<TData>>>;\n // API route used to fetch predictive search results.\n predictiveSearchEndpoint: string;\n // Delay before firing a search request after the term changes.\n debounceInMs: number;\n // Minimum term length required before searching.\n minTermLength: number;\n // Fetch implementation, injectable for tests and non-browser runtimes.\n fetch: typeof globalThis.fetch;\n // Storefront API search options forwarded to the server handler.\n searchOptions: StoreSearchOptions;\n abortController: AbortController | null;\n // Active debounce timer waiting to start the next search.\n debounceTimer: ReturnType<typeof setTimeout> | null;\n // Monotonic counter used to ignore responses from older requests.\n requestId: number;\n // Resolver for the current debounced search promise.\n pendingResolve: (() => void) | null;\n // Prevents future work after the store has been torn down.\n destroyed: boolean;\n};\n\ntype StoreSearchOptions = Pick<\n CreatePredictiveSearchStoreOptions,\n \"limit\" | \"limitScope\" | \"types\" | \"searchableFields\" | \"unavailableProducts\"\n>;\n\nexport function createPredictiveSearchStore<\n TData extends PredictiveSearchData = PredictiveSearchData,\n>(options: CreatePredictiveSearchStoreOptions = {}): PredictiveSearchStore<TData> {\n // Prefer an injected fetch for tests and non-browser runtimes, then fall back to the ambient runtime fetch.\n const resolvedFetch = options.fetch ?? resolveGlobalFetch();\n if (typeof resolvedFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n\n const context: PredictiveSearchStoreContext<TData> = {\n observable: createObservable(createInitialState<TData>()),\n predictiveSearchEndpoint: options.predictiveSearchEndpoint ?? PREDICTIVE_SEARCH_API_PATH,\n debounceInMs: sanitizeDebounceInMs(options.debounceInMs),\n minTermLength: sanitizeMinTermLength(options.minTermLength),\n fetch: resolvedFetch,\n searchOptions: {\n limit: options.limit,\n limitScope: options.limitScope,\n types: options.types,\n searchableFields: options.searchableFields,\n unavailableProducts: options.unavailableProducts,\n },\n abortController: null,\n debounceTimer: null,\n requestId: 0,\n pendingResolve: null,\n destroyed: false,\n };\n\n return {\n connect: () => connect(context),\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n search: (term) => search(context, term),\n clear: () => clear(context),\n destroy: () => destroy(context),\n };\n}\n\nfunction connect<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.destroyed = false;\n}\n\nfunction resolveGlobalFetch(): typeof globalThis.fetch | undefined {\n if (typeof globalThis.fetch !== \"function\") return undefined;\n return globalThis.fetch.bind(globalThis);\n}\n\nfunction sanitizeDebounceInMs(debounceInMs: number | undefined): number {\n if (typeof debounceInMs !== \"number\") return DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS;\n if (!Number.isFinite(debounceInMs)) return DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS;\n if (debounceInMs < 0) return DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS;\n return Math.trunc(debounceInMs);\n}\n\nfunction sanitizeMinTermLength(minTermLength: number | undefined): number {\n if (typeof minTermLength !== \"number\") return DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH;\n if (!Number.isFinite(minTermLength)) return DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH;\n if (minTermLength < 0) return DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH;\n return Math.trunc(minTermLength);\n}\n\nfunction createInitialState<TData extends PredictiveSearchData>(): PredictiveSearchState<TData> {\n return {\n term: \"\",\n status: \"idle\",\n result: getEmptyPredictiveSearchResult(\"\") as TData,\n error: null,\n };\n}\n\nfunction search<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n): Promise<void> {\n resolvePending(context);\n cancelScheduledWork(context);\n\n if (context.destroyed) return Promise.resolve();\n\n const trimmedTerm = term.trim();\n if (trimmedTerm.length < context.minTermLength) {\n resetState(context, trimmedTerm);\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n context.pendingResolve = resolve;\n context.requestId += 1;\n context.observable.setState({\n ...context.observable.state,\n term: trimmedTerm,\n status: \"loading\",\n error: null,\n });\n scheduleSearch(context, trimmedTerm, context.requestId);\n });\n}\n\nfunction clear<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.requestId += 1;\n resolvePending(context);\n cancelScheduledWork(context);\n resetState(context, \"\");\n}\n\nfunction destroy<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.destroyed = true;\n context.requestId += 1;\n resolvePending(context);\n cancelScheduledWork(context);\n}\n\nfunction scheduleSearch<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n requestId: number,\n): void {\n if (context.debounceInMs === 0) {\n void executeSearch(context, term, requestId);\n return;\n }\n\n context.debounceTimer = setTimeout(() => {\n context.debounceTimer = null;\n void executeSearch(context, term, requestId);\n }, context.debounceInMs);\n}\n\nasync function executeSearch<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n requestId: number,\n): Promise<void> {\n const abortController = new AbortController();\n context.abortController = abortController;\n\n try {\n const response = await context.fetch(buildRequestUrl(context, term), {\n signal: abortController.signal,\n });\n const result = await readResponse<TData>(response);\n settleSuccess(context, requestId, result);\n } catch (error) {\n if (isAbortError(error)) return;\n settleError(context, requestId, getErrorMessage(error));\n }\n}\n\nasync function readResponse<TData extends PredictiveSearchData>(\n response: Response,\n): Promise<TData> {\n const body = await readJson(response);\n if (response.ok) return body as TData;\n throw new Error(getResponseErrorMessage(body, response));\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction settleSuccess<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n requestId: number,\n result: TData,\n): void {\n if (!isActiveRequest(context, requestId)) return;\n context.observable.setState({\n term: result.term,\n status: \"success\",\n result,\n error: null,\n });\n finishActiveRequest(context);\n}\n\nfunction settleError<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n requestId: number,\n message: string,\n): void {\n if (!isActiveRequest(context, requestId)) return;\n context.observable.setState({\n ...context.observable.state,\n status: \"error\",\n error: message,\n });\n finishActiveRequest(context);\n}\n\nfunction finishActiveRequest<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.abortController = null;\n resolvePending(context);\n}\n\nfunction resetState<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n): void {\n context.observable.setState({\n term,\n status: \"idle\",\n result: getEmptyPredictiveSearchResult(term) as TData,\n error: null,\n });\n}\n\nfunction cancelScheduledWork<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n clearDebounceTimer(context);\n abortRequest(context);\n}\n\nfunction clearDebounceTimer<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n if (context.debounceTimer === null) return;\n clearTimeout(context.debounceTimer);\n context.debounceTimer = null;\n}\n\nfunction abortRequest<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.abortController?.abort();\n context.abortController = null;\n}\n\nfunction resolvePending<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.pendingResolve?.();\n context.pendingResolve = null;\n}\n\nfunction isActiveRequest<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n requestId: number,\n): boolean {\n return !context.destroyed && context.requestId === requestId;\n}\n\nfunction buildRequestUrl<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n): string {\n const url = new URL(context.predictiveSearchEndpoint, \"https://hydrogen.local\");\n url.searchParams.set(PREDICTIVE_SEARCH_QUERY_PARAM, term);\n appendSearchOptions(url.searchParams, context.searchOptions);\n if (URL.canParse(context.predictiveSearchEndpoint)) return url.href;\n return `${url.pathname}${url.search}`;\n}\n\nfunction appendSearchOptions(searchParams: URLSearchParams, options: StoreSearchOptions): void {\n setOptionalParam(searchParams, \"limit\", options.limit);\n setOptionalParam(searchParams, \"limitScope\", options.limitScope);\n setOptionalParam(searchParams, \"types\", options.types?.join(\",\"));\n setOptionalParam(searchParams, \"searchableFields\", options.searchableFields?.join(\",\"));\n setOptionalParam(searchParams, \"unavailableProducts\", options.unavailableProducts);\n}\n\nfunction setOptionalParam(\n searchParams: URLSearchParams,\n name: string,\n value: number | string | undefined,\n): void {\n if (value === undefined) return;\n searchParams.set(name, String(value));\n}\n\nfunction getResponseErrorMessage(body: unknown, response: Response): string {\n const message = getNestedErrorMessage(body);\n if (message) return message;\n return `Predictive search request failed with status ${response.status}`;\n}\n\nfunction getNestedErrorMessage(body: unknown): string | null {\n if (!body || typeof body !== \"object\") return null;\n if (!(\"error\" in body)) return null;\n\n const error = body.error;\n if (!error || typeof error !== \"object\") return null;\n if (!(\"message\" in error)) return null;\n\n return typeof error.message === \"string\" ? error.message : null;\n}\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : \"Predictive search request failed\";\n}\n\nfunction isAbortError(error: unknown): boolean {\n return error instanceof DOMException && error.name === \"AbortError\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.mjs","names":[],"sources":["../../../src/core/predictive-search/store.ts"],"sourcesContent":["import type {\n PredictiveSearchLimitScope,\n PredictiveSearchType,\n SearchUnavailableProductsType,\n SearchableField,\n} from \"../../graphql/generated/storefront-api-types\";\nimport { createObservable } from \"../observable\";\nimport { PREDICTIVE_SEARCH_API_PATH, PREDICTIVE_SEARCH_QUERY_PARAM } from \"./constants\";\nimport { getEmptyPredictiveSearchResult, type PredictiveSearchData } from \"./search\";\n\n/** Default delay before predictive search sends a request after a term changes. */\nexport const DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS = 150;\n\n/** Default minimum trimmed term length required before predictive search sends a request. */\nconst DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH = 1;\n\n/** Lifecycle state for the predictive search client store. */\nexport type PredictiveSearchStatus = \"idle\" | \"loading\" | \"success\" | \"error\";\n\n/** Current predictive search snapshot exposed to UI bindings. */\nexport type PredictiveSearchState<TData extends PredictiveSearchData = PredictiveSearchData> = {\n /** Latest trimmed search term known to the store. */\n term: string;\n /** Request lifecycle state for the current term. */\n status: PredictiveSearchStatus;\n /** Predictive search data for the current term, or an empty result while idle. */\n result: TData;\n /** Error message for the latest failed request, or null when there is no active error. */\n error: string | null;\n};\n\n/** Options for creating a framework-neutral predictive search client store. */\nexport type CreatePredictiveSearchStoreOptions = {\n /**\n * Same-origin JSON endpoint used for browser predictive-search requests.\n *\n * The endpoint must return JSON matching the data payload from\n * `await predictiveSearchHandlers.get(...)`.\n */\n predictiveSearchEndpoint?: string;\n /** Delay before sending a request after the term changes. */\n debounceInMs?: number;\n /** Minimum trimmed term length required before searching. */\n minTermLength?: number;\n /** Fetch implementation, injectable for tests and non-browser runtimes. */\n fetch?: typeof globalThis.fetch;\n /** Maximum result count requested from the Storefront API. */\n limit?: number;\n /** Whether `limit` applies to each result type or all result types combined. */\n limitScope?: PredictiveSearchLimitScope;\n /** Storefront API result types to include in predictive search. */\n types?: PredictiveSearchType[];\n /** Storefront API fields to search. */\n searchableFields?: SearchableField[];\n /** Storefront API unavailable-product behavior. */\n unavailableProducts?: SearchUnavailableProductsType;\n};\n\n/** Framework-neutral predictive search store used by UI bindings. */\nexport type PredictiveSearchStore<TData extends PredictiveSearchData = PredictiveSearchData> = {\n /** Reactivates store updates after destroy(). */\n connect(): void;\n /** Returns the current predictive search state snapshot. */\n getState(): PredictiveSearchState<TData>;\n /** Subscribes to state changes and returns an unsubscribe callback. */\n subscribe(listener: (state: PredictiveSearchState<TData>) => void): () => void;\n /** Searches for a term using the configured debounce and request lifecycle. */\n search(term: string): Promise<void>;\n /** Cancels pending work and resets the store to an empty state. */\n clear(): void;\n /** Cancels pending work and pauses store updates until reconnect. */\n destroy(): void;\n};\n\n/** Search methods exposed by the predictive search store. */\nexport type PredictiveSearchActions = Pick<PredictiveSearchStore, \"search\" | \"clear\">;\n\ntype PredictiveSearchStoreContext<TData extends PredictiveSearchData> = {\n observable: ReturnType<typeof createObservable<PredictiveSearchState<TData>>>;\n // API route used to fetch predictive search results.\n predictiveSearchEndpoint: string;\n // Delay before firing a search request after the term changes.\n debounceInMs: number;\n // Minimum term length required before searching.\n minTermLength: number;\n // Fetch implementation, injectable for tests and non-browser runtimes.\n fetch: typeof globalThis.fetch;\n // Storefront API search options forwarded to the server handler.\n searchOptions: StoreSearchOptions;\n abortController: AbortController | null;\n // Active debounce timer waiting to start the next search.\n debounceTimer: ReturnType<typeof setTimeout> | null;\n // Monotonic counter used to ignore responses from older requests.\n requestId: number;\n // Resolver for the current debounced search promise.\n pendingResolve: (() => void) | null;\n // Prevents future work after the store has been torn down.\n destroyed: boolean;\n};\n\ntype StoreSearchOptions = Pick<\n CreatePredictiveSearchStoreOptions,\n \"limit\" | \"limitScope\" | \"types\" | \"searchableFields\" | \"unavailableProducts\"\n>;\n\nexport function createPredictiveSearchStore<\n TData extends PredictiveSearchData = PredictiveSearchData,\n>(options: CreatePredictiveSearchStoreOptions = {}): PredictiveSearchStore<TData> {\n // Prefer an injected fetch for tests and non-browser runtimes, then fall back to the ambient runtime fetch.\n const resolvedFetch = options.fetch ?? resolveGlobalFetch();\n if (typeof resolvedFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n\n const context: PredictiveSearchStoreContext<TData> = {\n observable: createObservable(createInitialState<TData>()),\n predictiveSearchEndpoint: options.predictiveSearchEndpoint ?? PREDICTIVE_SEARCH_API_PATH,\n debounceInMs: sanitizeDebounceInMs(options.debounceInMs),\n minTermLength: sanitizeMinTermLength(options.minTermLength),\n fetch: resolvedFetch,\n searchOptions: {\n limit: options.limit,\n limitScope: options.limitScope,\n types: options.types,\n searchableFields: options.searchableFields,\n unavailableProducts: options.unavailableProducts,\n },\n abortController: null,\n debounceTimer: null,\n requestId: 0,\n pendingResolve: null,\n destroyed: false,\n };\n\n return {\n connect: () => connect(context),\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n search: (term) => search(context, term),\n clear: () => clear(context),\n destroy: () => destroy(context),\n };\n}\n\nfunction connect<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.destroyed = false;\n}\n\nfunction resolveGlobalFetch(): typeof globalThis.fetch | undefined {\n if (typeof globalThis.fetch !== \"function\") return undefined;\n return globalThis.fetch.bind(globalThis);\n}\n\nfunction sanitizeDebounceInMs(debounceInMs: number | undefined): number {\n if (typeof debounceInMs !== \"number\") return DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS;\n if (!Number.isFinite(debounceInMs)) return DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS;\n if (debounceInMs < 0) return DEFAULT_PREDICTIVE_SEARCH_DEBOUNCE_IN_MS;\n return Math.trunc(debounceInMs);\n}\n\nfunction sanitizeMinTermLength(minTermLength: number | undefined): number {\n if (typeof minTermLength !== \"number\") return DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH;\n if (!Number.isFinite(minTermLength)) return DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH;\n if (minTermLength < 0) return DEFAULT_PREDICTIVE_SEARCH_MIN_TERM_LENGTH;\n return Math.trunc(minTermLength);\n}\n\nfunction createInitialState<TData extends PredictiveSearchData>(): PredictiveSearchState<TData> {\n return {\n term: \"\",\n status: \"idle\",\n result: getEmptyPredictiveSearchResult(\"\") as TData,\n error: null,\n };\n}\n\nfunction search<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n): Promise<void> {\n resolvePending(context);\n cancelScheduledWork(context);\n\n if (context.destroyed) return Promise.resolve();\n\n const trimmedTerm = term.trim();\n if (trimmedTerm.length < context.minTermLength) {\n resetState(context, trimmedTerm);\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n context.pendingResolve = resolve;\n context.requestId += 1;\n context.observable.setState({\n ...context.observable.state,\n term: trimmedTerm,\n status: \"loading\",\n error: null,\n });\n scheduleSearch(context, trimmedTerm, context.requestId);\n });\n}\n\nfunction clear<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.requestId += 1;\n resolvePending(context);\n cancelScheduledWork(context);\n resetState(context, \"\");\n}\n\nfunction destroy<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.destroyed = true;\n context.requestId += 1;\n resolvePending(context);\n cancelScheduledWork(context);\n}\n\nfunction scheduleSearch<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n requestId: number,\n): void {\n if (context.debounceInMs === 0) {\n void executeSearch(context, term, requestId);\n return;\n }\n\n context.debounceTimer = setTimeout(() => {\n context.debounceTimer = null;\n void executeSearch(context, term, requestId);\n }, context.debounceInMs);\n}\n\nasync function executeSearch<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n requestId: number,\n): Promise<void> {\n const abortController = new AbortController();\n context.abortController = abortController;\n\n try {\n const response = await context.fetch(buildRequestUrl(context, term), {\n signal: abortController.signal,\n });\n const result = await readResponse<TData>(response);\n settleSuccess(context, requestId, result);\n } catch (error) {\n if (isAbortError(error)) return;\n settleError(context, requestId, getErrorMessage(error));\n }\n}\n\nasync function readResponse<TData extends PredictiveSearchData>(\n response: Response,\n): Promise<TData> {\n const body = await readJson(response);\n if (response.ok) return body as TData;\n throw new Error(getResponseErrorMessage(body, response));\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return null;\n }\n}\n\nfunction settleSuccess<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n requestId: number,\n result: TData,\n): void {\n if (!isActiveRequest(context, requestId)) return;\n context.observable.setState({\n term: result.term,\n status: \"success\",\n result,\n error: null,\n });\n finishActiveRequest(context);\n}\n\nfunction settleError<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n requestId: number,\n message: string,\n): void {\n if (!isActiveRequest(context, requestId)) return;\n context.observable.setState({\n ...context.observable.state,\n status: \"error\",\n error: message,\n });\n finishActiveRequest(context);\n}\n\nfunction finishActiveRequest<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.abortController = null;\n resolvePending(context);\n}\n\nfunction resetState<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n): void {\n context.observable.setState({\n term,\n status: \"idle\",\n result: getEmptyPredictiveSearchResult(term) as TData,\n error: null,\n });\n}\n\nfunction cancelScheduledWork<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n clearDebounceTimer(context);\n abortRequest(context);\n}\n\nfunction clearDebounceTimer<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n if (context.debounceTimer === null) return;\n clearTimeout(context.debounceTimer);\n context.debounceTimer = null;\n}\n\nfunction abortRequest<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.abortController?.abort();\n context.abortController = null;\n}\n\nfunction resolvePending<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n): void {\n context.pendingResolve?.();\n context.pendingResolve = null;\n}\n\nfunction isActiveRequest<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n requestId: number,\n): boolean {\n return !context.destroyed && context.requestId === requestId;\n}\n\nfunction buildRequestUrl<TData extends PredictiveSearchData>(\n context: PredictiveSearchStoreContext<TData>,\n term: string,\n): string {\n const url = new URL(context.predictiveSearchEndpoint, \"https://hydrogen.local\");\n url.searchParams.set(PREDICTIVE_SEARCH_QUERY_PARAM, term);\n appendSearchOptions(url.searchParams, context.searchOptions);\n if (URL.canParse(context.predictiveSearchEndpoint)) return url.href;\n return `${url.pathname}${url.search}`;\n}\n\nfunction appendSearchOptions(searchParams: URLSearchParams, options: StoreSearchOptions): void {\n setOptionalParam(searchParams, \"limit\", options.limit);\n setOptionalParam(searchParams, \"limitScope\", options.limitScope);\n setOptionalParam(searchParams, \"types\", options.types?.join(\",\"));\n setOptionalParam(searchParams, \"searchableFields\", options.searchableFields?.join(\",\"));\n setOptionalParam(searchParams, \"unavailableProducts\", options.unavailableProducts);\n}\n\nfunction setOptionalParam(\n searchParams: URLSearchParams,\n name: string,\n value: number | string | undefined,\n): void {\n if (value === undefined) return;\n searchParams.set(name, String(value));\n}\n\nfunction getResponseErrorMessage(body: unknown, response: Response): string {\n const message = getNestedErrorMessage(body);\n if (message) return message;\n return `Predictive search request failed with status ${response.status}`;\n}\n\nfunction getNestedErrorMessage(body: unknown): string | null {\n if (!body || typeof body !== \"object\") return null;\n if (!(\"error\" in body)) return null;\n\n const error = body.error;\n if (!error || typeof error !== \"object\") return null;\n if (!(\"message\" in error)) return null;\n\n return typeof error.message === \"string\" ? error.message : null;\n}\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : \"Predictive search request failed\";\n}\n\nfunction isAbortError(error: unknown): boolean {\n return error instanceof DOMException && error.name === \"AbortError\";\n}\n"],"mappings":"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"}
|
|
@@ -3,32 +3,74 @@ import { PredictiveSearchData } from "./search.mjs";
|
|
|
3
3
|
|
|
4
4
|
//#region src/core/predictive-search/url.d.ts
|
|
5
5
|
type PredictiveSearchItems = PredictiveSearchData["items"];
|
|
6
|
+
/** A product returned by predictive search. */
|
|
6
7
|
type PredictiveSearchProductItem = PredictiveSearchItems["products"][number];
|
|
8
|
+
/** A collection returned by predictive search. */
|
|
7
9
|
type PredictiveSearchCollectionItem = PredictiveSearchItems["collections"][number];
|
|
10
|
+
/** A page returned by predictive search. */
|
|
8
11
|
type PredictiveSearchPageItem = PredictiveSearchItems["pages"][number];
|
|
12
|
+
/** An article returned by predictive search. Includes a nested `blog.handle` used for URL generation via {@link getPredictiveSearchItemUrl}. */
|
|
9
13
|
type PredictiveSearchArticleItem = PredictiveSearchItems["articles"][number];
|
|
14
|
+
/** A search query suggestion returned by predictive search. Uses `text` as the search term when generating its URL via {@link getPredictiveSearchItemUrl}. */
|
|
10
15
|
type PredictiveSearchQueryItem = PredictiveSearchItems["queries"][number];
|
|
16
|
+
/** Union of product, collection, page, and article items. Excludes query suggestions. */
|
|
11
17
|
type PredictiveSearchResourceItem = PredictiveSearchProductItem | PredictiveSearchCollectionItem | PredictiveSearchPageItem | PredictiveSearchArticleItem;
|
|
18
|
+
/** Any item returned by predictive search, including query suggestions. */
|
|
12
19
|
type PredictiveSearchItem = PredictiveSearchResourceItem | PredictiveSearchQueryItem;
|
|
20
|
+
/**
|
|
21
|
+
* Options for generating URLs for resource items (products, collections,
|
|
22
|
+
* pages, articles) via {@link getPredictiveSearchItemUrl}.
|
|
23
|
+
*
|
|
24
|
+
* Required for resource items because their URLs depend on route templates
|
|
25
|
+
* and the current search term.
|
|
26
|
+
*/
|
|
13
27
|
type PredictiveSearchItemUrlOptions = {
|
|
14
|
-
pathPrefix?: string;
|
|
15
|
-
routes: ShopifyRouteTemplates;
|
|
28
|
+
/** Optional path prefix prepended to the generated route (e.g., a locale prefix). */pathPrefix?: string; /** Route templates used to resolve the resource's URL pattern. */
|
|
29
|
+
routes: ShopifyRouteTemplates; /** Search term appended as a query parameter to the generated URL. */
|
|
16
30
|
term: string;
|
|
17
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* Options for generating URLs for query suggestion items via
|
|
34
|
+
* {@link getPredictiveSearchItemUrl}.
|
|
35
|
+
*
|
|
36
|
+
* Optional because query suggestions use their own `text` as the search
|
|
37
|
+
* term and default to the standard search route.
|
|
38
|
+
*/
|
|
18
39
|
type PredictiveSearchQueryItemUrlOptions = {
|
|
19
|
-
pathPrefix?: string;
|
|
20
|
-
routes?: ShopifyRouteTemplates;
|
|
40
|
+
/** Optional path prefix prepended to the search route (e.g., a locale prefix). Ignored when `searchPath` is set. */pathPrefix?: string; /** Route templates. When omitted, the standard search route is used. */
|
|
41
|
+
routes?: ShopifyRouteTemplates; /** Custom search page path, used as-is (no `pathPrefix` applied). Takes precedence over `routes` and `pathPrefix`. */
|
|
21
42
|
searchPath?: string;
|
|
22
43
|
};
|
|
23
44
|
type SearchResultUrlOptions = {
|
|
24
|
-
baseUrl: string;
|
|
25
|
-
term: string;
|
|
26
|
-
trackingParameters?: string | null;
|
|
27
|
-
params?: Record<string, string>;
|
|
45
|
+
/** Search page path or absolute URL. */baseUrl: string; /** Search term, set under `searchParamName`. */
|
|
46
|
+
term: string; /** Storefront API `trackingParameters` query string, appended after the other parameters. */
|
|
47
|
+
trackingParameters?: string | null; /** Extra query parameters, set before the term, so a key matching `searchParamName` is overwritten. */
|
|
48
|
+
params?: Record<string, string>; /** Query parameter for the term. Defaults to `"q"`. */
|
|
28
49
|
searchParamName?: string;
|
|
29
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Generates a URL for a predictive search result item.
|
|
53
|
+
*
|
|
54
|
+
* For query suggestions, builds a search page URL using the suggestion's
|
|
55
|
+
* `text` as the search term. For resource items (products, collections,
|
|
56
|
+
* pages, articles), builds the resource URL using the provided route
|
|
57
|
+
* templates and appends the given search term as a query parameter.
|
|
58
|
+
*
|
|
59
|
+
* Always appends the item's `trackingParameters` when present.
|
|
60
|
+
*
|
|
61
|
+
* @throws {Error} When a resource item is passed without `routes` and `term` in options.
|
|
62
|
+
*/
|
|
30
63
|
declare function getPredictiveSearchItemUrl(item: PredictiveSearchQueryItem, options?: PredictiveSearchQueryItemUrlOptions): string;
|
|
31
64
|
declare function getPredictiveSearchItemUrl(item: PredictiveSearchResourceItem, options: PredictiveSearchItemUrlOptions): string;
|
|
65
|
+
/**
|
|
66
|
+
* Builds a search result URL from a base URL, search term, and optional
|
|
67
|
+
* extra parameters.
|
|
68
|
+
*
|
|
69
|
+
* Sets the term under `searchParamName` (defaults to `"q"`), replacing any
|
|
70
|
+
* existing value. `params` are set the same way, and `trackingParameters`
|
|
71
|
+
* are appended. Returns a relative URL unless the base
|
|
72
|
+
* URL is absolute.
|
|
73
|
+
*/
|
|
32
74
|
declare function getSearchResultUrl({
|
|
33
75
|
baseUrl,
|
|
34
76
|
term,
|
|
@@ -9,6 +9,15 @@ function getPredictiveSearchItemUrl(item, options) {
|
|
|
9
9
|
trackingParameters: item.trackingParameters
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds a search result URL from a base URL, search term, and optional
|
|
14
|
+
* extra parameters.
|
|
15
|
+
*
|
|
16
|
+
* Sets the term under `searchParamName` (defaults to `"q"`), replacing any
|
|
17
|
+
* existing value. `params` are set the same way, and `trackingParameters`
|
|
18
|
+
* are appended. Returns a relative URL unless the base
|
|
19
|
+
* URL is absolute.
|
|
20
|
+
*/
|
|
12
21
|
function getSearchResultUrl({ baseUrl, term, trackingParameters, params, searchParamName = "q" }) {
|
|
13
22
|
const url = new URL(baseUrl, RELATIVE_URL_BASE);
|
|
14
23
|
for (const [name, value] of Object.entries(params ?? {})) url.searchParams.set(name, value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.mjs","names":[],"sources":["../../../src/core/predictive-search/url.ts"],"sourcesContent":["import { getStandardRoute, type ShopifyRouteTemplates } from \"../standard-routes/index\";\nimport type { PredictiveSearchData } from \"./search\";\n\nconst RELATIVE_URL_BASE = \"https://hydrogen.local\";\n\ntype PredictiveSearchItems = PredictiveSearchData[\"items\"];\n\nexport type PredictiveSearchProductItem = PredictiveSearchItems[\"products\"][number];\nexport type PredictiveSearchCollectionItem = PredictiveSearchItems[\"collections\"][number];\nexport type PredictiveSearchPageItem = PredictiveSearchItems[\"pages\"][number];\nexport type PredictiveSearchArticleItem = PredictiveSearchItems[\"articles\"][number];\nexport type PredictiveSearchQueryItem = PredictiveSearchItems[\"queries\"][number];\n\nexport type PredictiveSearchResourceItem =\n | PredictiveSearchProductItem\n | PredictiveSearchCollectionItem\n | PredictiveSearchPageItem\n | PredictiveSearchArticleItem;\n\nexport type PredictiveSearchItem = PredictiveSearchResourceItem | PredictiveSearchQueryItem;\n\nexport type PredictiveSearchItemUrlOptions = {\n pathPrefix?: string;\n routes: ShopifyRouteTemplates;\n term: string;\n};\n\nexport type PredictiveSearchQueryItemUrlOptions = {\n pathPrefix?: string;\n routes?: ShopifyRouteTemplates;\n searchPath?: string;\n};\n\ntype AnyPredictiveSearchItemUrlOptions =\n | PredictiveSearchItemUrlOptions\n | PredictiveSearchQueryItemUrlOptions;\n\ntype SearchResultUrlOptions = {\n baseUrl: string;\n term: string;\n trackingParameters?: string | null;\n params?: Record<string, string>;\n searchParamName?: string;\n};\n\nexport function getPredictiveSearchItemUrl(\n item: PredictiveSearchQueryItem,\n options?: PredictiveSearchQueryItemUrlOptions,\n): string;\nexport function getPredictiveSearchItemUrl(\n item: PredictiveSearchResourceItem,\n options: PredictiveSearchItemUrlOptions,\n): string;\nexport function getPredictiveSearchItemUrl(\n item: PredictiveSearchItem,\n options?: AnyPredictiveSearchItemUrlOptions,\n): string {\n return getSearchResultUrl({\n baseUrl: getPredictiveSearchItemBaseUrl(item, options),\n term: getPredictiveSearchItemSearchTerm(item, options),\n trackingParameters: item.trackingParameters,\n });\n}\n\nexport function getSearchResultUrl({\n baseUrl,\n term,\n trackingParameters,\n params,\n searchParamName = \"q\",\n}: SearchResultUrlOptions): string {\n const url = new URL(baseUrl, RELATIVE_URL_BASE);\n\n for (const [name, value] of Object.entries(params ?? {})) {\n url.searchParams.set(name, value);\n }\n\n url.searchParams.set(searchParamName, term);\n appendTrackingParameters(url.searchParams, trackingParameters);\n\n if (isAbsoluteUrl(baseUrl)) return url.href;\n return `${url.pathname}${url.search}${url.hash}`;\n}\n\nfunction getPredictiveSearchItemBaseUrl(\n item: PredictiveSearchItem,\n options: AnyPredictiveSearchItemUrlOptions | undefined,\n): string {\n if (item.__typename === \"SearchQuerySuggestion\") return getQueryBaseUrl(options);\n\n const { pathPrefix, routes } = requirePredictiveSearchItemUrlOptions(options);\n return getResourceBaseUrl(item, routes, pathPrefix);\n}\n\nfunction getQueryBaseUrl(options: AnyPredictiveSearchItemUrlOptions | undefined): string {\n if (options && \"searchPath\" in options && options.searchPath) return options.searchPath;\n\n return getStandardRoute(\n options?.routes ?? {},\n \"search\",\n {},\n {\n pathPrefix: options?.pathPrefix,\n },\n );\n}\n\nfunction getResourceBaseUrl(\n item: PredictiveSearchResourceItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n switch (item.__typename) {\n case \"Product\":\n return getProductBaseUrl(item, routes, pathPrefix);\n case \"Collection\":\n return getCollectionBaseUrl(item, routes, pathPrefix);\n case \"Page\":\n return getPageBaseUrl(item, routes, pathPrefix);\n case \"Article\":\n return getArticleBaseUrl(item, routes, pathPrefix);\n }\n}\n\nfunction getProductBaseUrl(\n product: PredictiveSearchProductItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(routes, \"product\", { productHandle: product.handle }, { pathPrefix });\n}\n\nfunction getCollectionBaseUrl(\n collection: PredictiveSearchCollectionItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(\n routes,\n \"collection\",\n { collectionHandle: collection.handle },\n { pathPrefix },\n );\n}\n\nfunction getPageBaseUrl(\n page: PredictiveSearchPageItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(routes, \"page\", { pageHandle: page.handle }, { pathPrefix });\n}\n\nfunction getArticleBaseUrl(\n article: PredictiveSearchArticleItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(\n routes,\n \"article\",\n { articleHandle: article.handle, blogHandle: article.blog.handle },\n { pathPrefix },\n );\n}\n\nfunction getPredictiveSearchItemSearchTerm(\n item: PredictiveSearchItem,\n options: AnyPredictiveSearchItemUrlOptions | undefined,\n): string {\n if (item.__typename === \"SearchQuerySuggestion\") return item.text;\n return requirePredictiveSearchItemUrlOptions(options).term;\n}\n\nfunction requirePredictiveSearchItemUrlOptions(\n options: AnyPredictiveSearchItemUrlOptions | undefined,\n): PredictiveSearchItemUrlOptions {\n if (options && \"term\" in options && \"routes\" in options) return options;\n\n throw new Error(\n \"Predictive search resource URLs require route templates and the typed search term.\",\n );\n}\n\nfunction appendTrackingParameters(params: URLSearchParams, trackingParameters?: string | null) {\n if (!trackingParameters) return;\n\n const tracking = new URLSearchParams(trackingParameters);\n for (const [name, value] of tracking) {\n params.append(name, value);\n }\n}\n\nfunction isAbsoluteUrl(url: string): boolean {\n return URL.canParse(url);\n}\n"],"mappings":";;;AAGA,MAAM,oBAAoB;AAkD1B,SAAgB,2BACd,MACA,SACQ;AACR,QAAO,mBAAmB;EACxB,SAAS,+BAA+B,MAAM,QAAQ;EACtD,MAAM,kCAAkC,MAAM,QAAQ;EACtD,oBAAoB,KAAK;EAC1B,CAAC;;AAGJ,SAAgB,mBAAmB,EACjC,SACA,MACA,oBACA,QACA,kBAAkB,OACe;CACjC,MAAM,MAAM,IAAI,IAAI,SAAS,kBAAkB;AAE/C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,UAAU,EAAE,CAAC,CACtD,KAAI,aAAa,IAAI,MAAM,MAAM;AAGnC,KAAI,aAAa,IAAI,iBAAiB,KAAK;AAC3C,0BAAyB,IAAI,cAAc,mBAAmB;AAE9D,KAAI,cAAc,QAAQ,CAAE,QAAO,IAAI;AACvC,QAAO,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI;;AAG5C,SAAS,+BACP,MACA,SACQ;AACR,KAAI,KAAK,eAAe,wBAAyB,QAAO,gBAAgB,QAAQ;CAEhF,MAAM,EAAE,YAAY,WAAW,sCAAsC,QAAQ;AAC7E,QAAO,mBAAmB,MAAM,QAAQ,WAAW;;AAGrD,SAAS,gBAAgB,SAAgE;AACvF,KAAI,WAAW,gBAAgB,WAAW,QAAQ,WAAY,QAAO,QAAQ;AAE7E,QAAO,iBACL,SAAS,UAAU,EAAE,EACrB,UACA,EAAE,EACF,EACE,YAAY,SAAS,YACtB,CACF;;AAGH,SAAS,mBACP,MACA,QACA,YACQ;AACR,SAAQ,KAAK,YAAb;EACE,KAAK,UACH,QAAO,kBAAkB,MAAM,QAAQ,WAAW;EACpD,KAAK,aACH,QAAO,qBAAqB,MAAM,QAAQ,WAAW;EACvD,KAAK,OACH,QAAO,eAAe,MAAM,QAAQ,WAAW;EACjD,KAAK,UACH,QAAO,kBAAkB,MAAM,QAAQ,WAAW;;;AAIxD,SAAS,kBACP,SACA,QACA,YACQ;AACR,QAAO,iBAAiB,QAAQ,WAAW,EAAE,eAAe,QAAQ,QAAQ,EAAE,EAAE,YAAY,CAAC;;AAG/F,SAAS,qBACP,YACA,QACA,YACQ;AACR,QAAO,iBACL,QACA,cACA,EAAE,kBAAkB,WAAW,QAAQ,EACvC,EAAE,YAAY,CACf;;AAGH,SAAS,eACP,MACA,QACA,YACQ;AACR,QAAO,iBAAiB,QAAQ,QAAQ,EAAE,YAAY,KAAK,QAAQ,EAAE,EAAE,YAAY,CAAC;;AAGtF,SAAS,kBACP,SACA,QACA,YACQ;AACR,QAAO,iBACL,QACA,WACA;EAAE,eAAe,QAAQ;EAAQ,YAAY,QAAQ,KAAK;EAAQ,EAClE,EAAE,YAAY,CACf;;AAGH,SAAS,kCACP,MACA,SACQ;AACR,KAAI,KAAK,eAAe,wBAAyB,QAAO,KAAK;AAC7D,QAAO,sCAAsC,QAAQ,CAAC;;AAGxD,SAAS,sCACP,SACgC;AAChC,KAAI,WAAW,UAAU,WAAW,YAAY,QAAS,QAAO;AAEhE,OAAM,IAAI,MACR,qFACD;;AAGH,SAAS,yBAAyB,QAAyB,oBAAoC;AAC7F,KAAI,CAAC,mBAAoB;CAEzB,MAAM,WAAW,IAAI,gBAAgB,mBAAmB;AACxD,MAAK,MAAM,CAAC,MAAM,UAAU,SAC1B,QAAO,OAAO,MAAM,MAAM;;AAI9B,SAAS,cAAc,KAAsB;AAC3C,QAAO,IAAI,SAAS,IAAI"}
|
|
1
|
+
{"version":3,"file":"url.mjs","names":[],"sources":["../../../src/core/predictive-search/url.ts"],"sourcesContent":["import { getStandardRoute, type ShopifyRouteTemplates } from \"../standard-routes/index\";\nimport type { PredictiveSearchData } from \"./search\";\n\nconst RELATIVE_URL_BASE = \"https://hydrogen.local\";\n\ntype PredictiveSearchItems = PredictiveSearchData[\"items\"];\n\n/** A product returned by predictive search. */\nexport type PredictiveSearchProductItem = PredictiveSearchItems[\"products\"][number];\n/** A collection returned by predictive search. */\nexport type PredictiveSearchCollectionItem = PredictiveSearchItems[\"collections\"][number];\n/** A page returned by predictive search. */\nexport type PredictiveSearchPageItem = PredictiveSearchItems[\"pages\"][number];\n/** An article returned by predictive search. Includes a nested `blog.handle` used for URL generation via {@link getPredictiveSearchItemUrl}. */\nexport type PredictiveSearchArticleItem = PredictiveSearchItems[\"articles\"][number];\n/** A search query suggestion returned by predictive search. Uses `text` as the search term when generating its URL via {@link getPredictiveSearchItemUrl}. */\nexport type PredictiveSearchQueryItem = PredictiveSearchItems[\"queries\"][number];\n\n/** Union of product, collection, page, and article items. Excludes query suggestions. */\nexport type PredictiveSearchResourceItem =\n | PredictiveSearchProductItem\n | PredictiveSearchCollectionItem\n | PredictiveSearchPageItem\n | PredictiveSearchArticleItem;\n\n/** Any item returned by predictive search, including query suggestions. */\nexport type PredictiveSearchItem = PredictiveSearchResourceItem | PredictiveSearchQueryItem;\n\n/**\n * Options for generating URLs for resource items (products, collections,\n * pages, articles) via {@link getPredictiveSearchItemUrl}.\n *\n * Required for resource items because their URLs depend on route templates\n * and the current search term.\n */\nexport type PredictiveSearchItemUrlOptions = {\n /** Optional path prefix prepended to the generated route (e.g., a locale prefix). */\n pathPrefix?: string;\n /** Route templates used to resolve the resource's URL pattern. */\n routes: ShopifyRouteTemplates;\n /** Search term appended as a query parameter to the generated URL. */\n term: string;\n};\n\n/**\n * Options for generating URLs for query suggestion items via\n * {@link getPredictiveSearchItemUrl}.\n *\n * Optional because query suggestions use their own `text` as the search\n * term and default to the standard search route.\n */\nexport type PredictiveSearchQueryItemUrlOptions = {\n /** Optional path prefix prepended to the search route (e.g., a locale prefix). Ignored when `searchPath` is set. */\n pathPrefix?: string;\n /** Route templates. When omitted, the standard search route is used. */\n routes?: ShopifyRouteTemplates;\n /** Custom search page path, used as-is (no `pathPrefix` applied). Takes precedence over `routes` and `pathPrefix`. */\n searchPath?: string;\n};\n\ntype AnyPredictiveSearchItemUrlOptions =\n | PredictiveSearchItemUrlOptions\n | PredictiveSearchQueryItemUrlOptions;\n\ntype SearchResultUrlOptions = {\n /** Search page path or absolute URL. */\n baseUrl: string;\n /** Search term, set under `searchParamName`. */\n term: string;\n /** Storefront API `trackingParameters` query string, appended after the other parameters. */\n trackingParameters?: string | null;\n /** Extra query parameters, set before the term, so a key matching `searchParamName` is overwritten. */\n params?: Record<string, string>;\n /** Query parameter for the term. Defaults to `\"q\"`. */\n searchParamName?: string;\n};\n\n/**\n * Generates a URL for a predictive search result item.\n *\n * For query suggestions, builds a search page URL using the suggestion's\n * `text` as the search term. For resource items (products, collections,\n * pages, articles), builds the resource URL using the provided route\n * templates and appends the given search term as a query parameter.\n *\n * Always appends the item's `trackingParameters` when present.\n *\n * @throws {Error} When a resource item is passed without `routes` and `term` in options.\n */\nexport function getPredictiveSearchItemUrl(\n item: PredictiveSearchQueryItem,\n options?: PredictiveSearchQueryItemUrlOptions,\n): string;\nexport function getPredictiveSearchItemUrl(\n item: PredictiveSearchResourceItem,\n options: PredictiveSearchItemUrlOptions,\n): string;\nexport function getPredictiveSearchItemUrl(\n item: PredictiveSearchItem,\n options?: AnyPredictiveSearchItemUrlOptions,\n): string {\n return getSearchResultUrl({\n baseUrl: getPredictiveSearchItemBaseUrl(item, options),\n term: getPredictiveSearchItemSearchTerm(item, options),\n trackingParameters: item.trackingParameters,\n });\n}\n\n/**\n * Builds a search result URL from a base URL, search term, and optional\n * extra parameters.\n *\n * Sets the term under `searchParamName` (defaults to `\"q\"`), replacing any\n * existing value. `params` are set the same way, and `trackingParameters`\n * are appended. Returns a relative URL unless the base\n * URL is absolute.\n */\nexport function getSearchResultUrl({\n baseUrl,\n term,\n trackingParameters,\n params,\n searchParamName = \"q\",\n}: SearchResultUrlOptions): string {\n const url = new URL(baseUrl, RELATIVE_URL_BASE);\n\n for (const [name, value] of Object.entries(params ?? {})) {\n url.searchParams.set(name, value);\n }\n\n url.searchParams.set(searchParamName, term);\n appendTrackingParameters(url.searchParams, trackingParameters);\n\n if (isAbsoluteUrl(baseUrl)) return url.href;\n return `${url.pathname}${url.search}${url.hash}`;\n}\n\nfunction getPredictiveSearchItemBaseUrl(\n item: PredictiveSearchItem,\n options: AnyPredictiveSearchItemUrlOptions | undefined,\n): string {\n if (item.__typename === \"SearchQuerySuggestion\") return getQueryBaseUrl(options);\n\n const { pathPrefix, routes } = requirePredictiveSearchItemUrlOptions(options);\n return getResourceBaseUrl(item, routes, pathPrefix);\n}\n\nfunction getQueryBaseUrl(options: AnyPredictiveSearchItemUrlOptions | undefined): string {\n if (options && \"searchPath\" in options && options.searchPath) return options.searchPath;\n\n return getStandardRoute(\n options?.routes ?? {},\n \"search\",\n {},\n {\n pathPrefix: options?.pathPrefix,\n },\n );\n}\n\nfunction getResourceBaseUrl(\n item: PredictiveSearchResourceItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n switch (item.__typename) {\n case \"Product\":\n return getProductBaseUrl(item, routes, pathPrefix);\n case \"Collection\":\n return getCollectionBaseUrl(item, routes, pathPrefix);\n case \"Page\":\n return getPageBaseUrl(item, routes, pathPrefix);\n case \"Article\":\n return getArticleBaseUrl(item, routes, pathPrefix);\n }\n}\n\nfunction getProductBaseUrl(\n product: PredictiveSearchProductItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(routes, \"product\", { productHandle: product.handle }, { pathPrefix });\n}\n\nfunction getCollectionBaseUrl(\n collection: PredictiveSearchCollectionItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(\n routes,\n \"collection\",\n { collectionHandle: collection.handle },\n { pathPrefix },\n );\n}\n\nfunction getPageBaseUrl(\n page: PredictiveSearchPageItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(routes, \"page\", { pageHandle: page.handle }, { pathPrefix });\n}\n\nfunction getArticleBaseUrl(\n article: PredictiveSearchArticleItem,\n routes: ShopifyRouteTemplates,\n pathPrefix: string | undefined,\n): string {\n return getStandardRoute(\n routes,\n \"article\",\n { articleHandle: article.handle, blogHandle: article.blog.handle },\n { pathPrefix },\n );\n}\n\nfunction getPredictiveSearchItemSearchTerm(\n item: PredictiveSearchItem,\n options: AnyPredictiveSearchItemUrlOptions | undefined,\n): string {\n if (item.__typename === \"SearchQuerySuggestion\") return item.text;\n return requirePredictiveSearchItemUrlOptions(options).term;\n}\n\nfunction requirePredictiveSearchItemUrlOptions(\n options: AnyPredictiveSearchItemUrlOptions | undefined,\n): PredictiveSearchItemUrlOptions {\n if (options && \"term\" in options && \"routes\" in options) return options;\n\n throw new Error(\n \"Predictive search resource URLs require route templates and the typed search term.\",\n );\n}\n\nfunction appendTrackingParameters(params: URLSearchParams, trackingParameters?: string | null) {\n if (!trackingParameters) return;\n\n const tracking = new URLSearchParams(trackingParameters);\n for (const [name, value] of tracking) {\n params.append(name, value);\n }\n}\n\nfunction isAbsoluteUrl(url: string): boolean {\n return URL.canParse(url);\n}\n"],"mappings":";;;AAGA,MAAM,oBAAoB;AA8F1B,SAAgB,2BACd,MACA,SACQ;AACR,QAAO,mBAAmB;EACxB,SAAS,+BAA+B,MAAM,QAAQ;EACtD,MAAM,kCAAkC,MAAM,QAAQ;EACtD,oBAAoB,KAAK;EAC1B,CAAC;;;;;;;;;;;AAYJ,SAAgB,mBAAmB,EACjC,SACA,MACA,oBACA,QACA,kBAAkB,OACe;CACjC,MAAM,MAAM,IAAI,IAAI,SAAS,kBAAkB;AAE/C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,UAAU,EAAE,CAAC,CACtD,KAAI,aAAa,IAAI,MAAM,MAAM;AAGnC,KAAI,aAAa,IAAI,iBAAiB,KAAK;AAC3C,0BAAyB,IAAI,cAAc,mBAAmB;AAE9D,KAAI,cAAc,QAAQ,CAAE,QAAO,IAAI;AACvC,QAAO,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI;;AAG5C,SAAS,+BACP,MACA,SACQ;AACR,KAAI,KAAK,eAAe,wBAAyB,QAAO,gBAAgB,QAAQ;CAEhF,MAAM,EAAE,YAAY,WAAW,sCAAsC,QAAQ;AAC7E,QAAO,mBAAmB,MAAM,QAAQ,WAAW;;AAGrD,SAAS,gBAAgB,SAAgE;AACvF,KAAI,WAAW,gBAAgB,WAAW,QAAQ,WAAY,QAAO,QAAQ;AAE7E,QAAO,iBACL,SAAS,UAAU,EAAE,EACrB,UACA,EAAE,EACF,EACE,YAAY,SAAS,YACtB,CACF;;AAGH,SAAS,mBACP,MACA,QACA,YACQ;AACR,SAAQ,KAAK,YAAb;EACE,KAAK,UACH,QAAO,kBAAkB,MAAM,QAAQ,WAAW;EACpD,KAAK,aACH,QAAO,qBAAqB,MAAM,QAAQ,WAAW;EACvD,KAAK,OACH,QAAO,eAAe,MAAM,QAAQ,WAAW;EACjD,KAAK,UACH,QAAO,kBAAkB,MAAM,QAAQ,WAAW;;;AAIxD,SAAS,kBACP,SACA,QACA,YACQ;AACR,QAAO,iBAAiB,QAAQ,WAAW,EAAE,eAAe,QAAQ,QAAQ,EAAE,EAAE,YAAY,CAAC;;AAG/F,SAAS,qBACP,YACA,QACA,YACQ;AACR,QAAO,iBACL,QACA,cACA,EAAE,kBAAkB,WAAW,QAAQ,EACvC,EAAE,YAAY,CACf;;AAGH,SAAS,eACP,MACA,QACA,YACQ;AACR,QAAO,iBAAiB,QAAQ,QAAQ,EAAE,YAAY,KAAK,QAAQ,EAAE,EAAE,YAAY,CAAC;;AAGtF,SAAS,kBACP,SACA,QACA,YACQ;AACR,QAAO,iBACL,QACA,WACA;EAAE,eAAe,QAAQ;EAAQ,YAAY,QAAQ,KAAK;EAAQ,EAClE,EAAE,YAAY,CACf;;AAGH,SAAS,kCACP,MACA,SACQ;AACR,KAAI,KAAK,eAAe,wBAAyB,QAAO,KAAK;AAC7D,QAAO,sCAAsC,QAAQ,CAAC;;AAGxD,SAAS,sCACP,SACgC;AAChC,KAAI,WAAW,UAAU,WAAW,YAAY,QAAS,QAAO;AAEhE,OAAM,IAAI,MACR,qFACD;;AAGH,SAAS,yBAAyB,QAAyB,oBAAoC;AAC7F,KAAI,CAAC,mBAAoB;CAEzB,MAAM,WAAW,IAAI,gBAAgB,mBAAmB;AACxD,MAAK,MAAM,CAAC,MAAM,UAAU,SAC1B,QAAO,OAAO,MAAM,MAAM;;AAI9B,SAAS,cAAc,KAAsB;AAC3C,QAAO,IAAI,SAAS,IAAI"}
|
|
@@ -29,11 +29,22 @@ interface ProductAddToCartProps {
|
|
|
29
29
|
name: "add-to-cart";
|
|
30
30
|
type: "submit";
|
|
31
31
|
}
|
|
32
|
+
type AttributeValueName = `attributes.${string}`;
|
|
33
|
+
/** Props returned by {@link ProductFormRegister} for a line-item attribute input. */
|
|
34
|
+
interface ProductAttributeValueProps {
|
|
35
|
+
name: AttributeValueName;
|
|
36
|
+
value: string;
|
|
37
|
+
}
|
|
38
|
+
/** Props returned by {@link ProductFormRegister} for an uncontrolled line-item attribute input. */
|
|
39
|
+
interface ProductAttributeDefaultValueProps {
|
|
40
|
+
name: AttributeValueName;
|
|
41
|
+
defaultValue: string;
|
|
42
|
+
}
|
|
32
43
|
/**
|
|
33
44
|
* Register function returned by `useProductForm`.
|
|
34
45
|
*
|
|
35
46
|
* Product forms only register product-relevant fields:
|
|
36
|
-
* `merchandiseId`, `quantity`, `optionValue`, and `addToCart`.
|
|
47
|
+
* `merchandiseId`, `quantity`, `optionValue`, `attributeValue`, and `addToCart`.
|
|
37
48
|
*
|
|
38
49
|
* Option values return form identity and activation handlers. UI props such as
|
|
39
50
|
* `type`, `checked`, `disabled`, and `aria-pressed` belong to the caller — use
|
|
@@ -51,6 +62,14 @@ type ProductFormRegister = {
|
|
|
51
62
|
optionName: string;
|
|
52
63
|
value: string;
|
|
53
64
|
}): ProductOptionValueProps;
|
|
65
|
+
(field: "attributeValue", opts: {
|
|
66
|
+
key: string;
|
|
67
|
+
value: string;
|
|
68
|
+
}): ProductAttributeValueProps;
|
|
69
|
+
(field: "attributeValue", opts: {
|
|
70
|
+
key: string;
|
|
71
|
+
defaultValue: string;
|
|
72
|
+
}): ProductAttributeDefaultValueProps;
|
|
54
73
|
(field: "addToCart", opts: {}): ProductAddToCartProps;
|
|
55
74
|
};
|
|
56
75
|
/**
|
|
@@ -59,5 +78,5 @@ type ProductFormRegister = {
|
|
|
59
78
|
*/
|
|
60
79
|
declare function createProductFormRegister(selectedVariant: ProductVariantInput | null, selectOption: (name: string, value: string) => void): ProductFormRegister;
|
|
61
80
|
//#endregion
|
|
62
|
-
export { ProductAddToCartProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister };
|
|
81
|
+
export { ProductAddToCartProps, ProductAttributeDefaultValueProps, ProductAttributeValueProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister };
|
|
63
82
|
//# sourceMappingURL=form.d.mts.map
|
|
@@ -31,6 +31,19 @@ function createProductFormRegister(selectedVariant, selectOption) {
|
|
|
31
31
|
onClick: handleSelect
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
if (field === "attributeValue") {
|
|
35
|
+
const key = String(opts?.key ?? "");
|
|
36
|
+
if (!key) throw new TypeError("Product attribute values require a non-empty \"key\".");
|
|
37
|
+
const name = `attributes.${key}`;
|
|
38
|
+
if (opts && "defaultValue" in opts) return {
|
|
39
|
+
name,
|
|
40
|
+
defaultValue: String(opts.defaultValue)
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
value: String(opts?.value ?? "")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
34
47
|
if (field === "addToCart") return {
|
|
35
48
|
name: "add-to-cart",
|
|
36
49
|
type: "submit"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.mjs","names":[],"sources":["../../../src/core/product/form.ts"],"sourcesContent":["import type { ProductVariantInput } from \"./state\";\n\n/** Props returned by {@link ProductFormRegister} for the `merchandiseId` input. */\nexport interface ProductMerchandiseIdProps {\n name: \"merchandiseId\";\n /** The currently selected variant ID, or an empty string when no variant is resolved. */\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a quantity input. */\nexport interface ProductQuantityProps {\n name: \"quantity\";\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for an uncontrolled quantity input. */\nexport interface ProductQuantityDefaultProps {\n name: \"quantity\";\n defaultValue: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a variant option value control. */\nexport interface ProductOptionValueProps {\n name: string;\n value: string;\n onChange: () => void;\n onClick: () => void;\n}\n\n/** Props returned by {@link ProductFormRegister} for the add-to-cart submit button. */\nexport interface ProductAddToCartProps {\n name: \"add-to-cart\";\n type: \"submit\";\n}\n\n/**\n * Register function returned by `useProductForm`.\n *\n * Product forms only register product-relevant fields:\n * `merchandiseId`, `quantity`, `optionValue`, and `addToCart`.\n *\n * Option values return form identity and activation handlers. UI props such as\n * `type`, `checked`, `disabled`, and `aria-pressed` belong to the caller — use\n * the state available on `options` (e.g. `value.selected`, `value.available`).\n */\nexport type ProductFormRegister = {\n (field: \"merchandiseId\", opts: {}): ProductMerchandiseIdProps;\n (field: \"quantity\", opts: { value: number }): ProductQuantityProps;\n (field: \"quantity\", opts: { defaultValue: number }): ProductQuantityDefaultProps;\n (field: \"optionValue\", opts: { optionName: string; value: string }): ProductOptionValueProps;\n (field: \"addToCart\", opts: {}): ProductAddToCartProps;\n};\n\n/**\n * Creates a {@link ProductFormRegister} function bound to the current variant\n * state and `selectOption` dispatcher.\n */\nexport function createProductFormRegister(\n selectedVariant: ProductVariantInput | null,\n selectOption: (name: string, value: string) => void,\n): ProductFormRegister {\n return ((field: string, opts?: Record<string, unknown>) => {\n if (field === \"merchandiseId\") {\n return { name: \"merchandiseId\", value: selectedVariant?.id ?? \"\" };\n }\n\n if (field === \"quantity\") {\n if (typeof opts?.defaultValue === \"number\") {\n return { name: \"quantity\" as const, defaultValue: String(opts.defaultValue) };\n }\n const value = typeof opts?.value === \"number\" ? opts.value : 1;\n return { name: \"quantity\" as const, value: String(value) };\n }\n\n if (field === \"optionValue\") {\n const optionName = String(opts?.optionName ?? \"\");\n const value = String(opts?.value ?? \"\");\n const handleSelect = () => selectOption(optionName, value);\n return { name: optionName, value, onChange: handleSelect, onClick: handleSelect };\n }\n\n if (field === \"addToCart\") {\n return { name: \"add-to-cart\", type: \"submit\" } satisfies ProductAddToCartProps;\n }\n\n throw new Error(`Unknown product form field: \"${field}\".`);\n }) as ProductFormRegister;\n}\n"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"form.mjs","names":[],"sources":["../../../src/core/product/form.ts"],"sourcesContent":["import type { ProductVariantInput } from \"./state\";\n\n/** Props returned by {@link ProductFormRegister} for the `merchandiseId` input. */\nexport interface ProductMerchandiseIdProps {\n name: \"merchandiseId\";\n /** The currently selected variant ID, or an empty string when no variant is resolved. */\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a quantity input. */\nexport interface ProductQuantityProps {\n name: \"quantity\";\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for an uncontrolled quantity input. */\nexport interface ProductQuantityDefaultProps {\n name: \"quantity\";\n defaultValue: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for a variant option value control. */\nexport interface ProductOptionValueProps {\n name: string;\n value: string;\n onChange: () => void;\n onClick: () => void;\n}\n\n/** Props returned by {@link ProductFormRegister} for the add-to-cart submit button. */\nexport interface ProductAddToCartProps {\n name: \"add-to-cart\";\n type: \"submit\";\n}\n\ntype AttributeValueName = `attributes.${string}`;\n\n/** Props returned by {@link ProductFormRegister} for a line-item attribute input. */\nexport interface ProductAttributeValueProps {\n name: AttributeValueName;\n value: string;\n}\n\n/** Props returned by {@link ProductFormRegister} for an uncontrolled line-item attribute input. */\nexport interface ProductAttributeDefaultValueProps {\n name: AttributeValueName;\n defaultValue: string;\n}\n\n/**\n * Register function returned by `useProductForm`.\n *\n * Product forms only register product-relevant fields:\n * `merchandiseId`, `quantity`, `optionValue`, `attributeValue`, and `addToCart`.\n *\n * Option values return form identity and activation handlers. UI props such as\n * `type`, `checked`, `disabled`, and `aria-pressed` belong to the caller — use\n * the state available on `options` (e.g. `value.selected`, `value.available`).\n */\nexport type ProductFormRegister = {\n (field: \"merchandiseId\", opts: {}): ProductMerchandiseIdProps;\n (field: \"quantity\", opts: { value: number }): ProductQuantityProps;\n (field: \"quantity\", opts: { defaultValue: number }): ProductQuantityDefaultProps;\n (field: \"optionValue\", opts: { optionName: string; value: string }): ProductOptionValueProps;\n (field: \"attributeValue\", opts: { key: string; value: string }): ProductAttributeValueProps;\n (\n field: \"attributeValue\",\n opts: { key: string; defaultValue: string },\n ): ProductAttributeDefaultValueProps;\n (field: \"addToCart\", opts: {}): ProductAddToCartProps;\n};\n\n/**\n * Creates a {@link ProductFormRegister} function bound to the current variant\n * state and `selectOption` dispatcher.\n */\nexport function createProductFormRegister(\n selectedVariant: ProductVariantInput | null,\n selectOption: (name: string, value: string) => void,\n): ProductFormRegister {\n return ((field: string, opts?: Record<string, unknown>) => {\n if (field === \"merchandiseId\") {\n return { name: \"merchandiseId\", value: selectedVariant?.id ?? \"\" };\n }\n\n if (field === \"quantity\") {\n if (typeof opts?.defaultValue === \"number\") {\n return { name: \"quantity\" as const, defaultValue: String(opts.defaultValue) };\n }\n const value = typeof opts?.value === \"number\" ? opts.value : 1;\n return { name: \"quantity\" as const, value: String(value) };\n }\n\n if (field === \"optionValue\") {\n const optionName = String(opts?.optionName ?? \"\");\n const value = String(opts?.value ?? \"\");\n const handleSelect = () => selectOption(optionName, value);\n return { name: optionName, value, onChange: handleSelect, onClick: handleSelect };\n }\n\n if (field === \"attributeValue\") {\n const key = String(opts?.key ?? \"\");\n if (!key) throw new TypeError('Product attribute values require a non-empty \"key\".');\n const name = `attributes.${key}` as AttributeValueName;\n if (opts && \"defaultValue\" in opts) {\n return { name, defaultValue: String(opts.defaultValue) };\n }\n return { name, value: String(opts?.value ?? \"\") };\n }\n\n if (field === \"addToCart\") {\n return { name: \"add-to-cart\", type: \"submit\" } satisfies ProductAddToCartProps;\n }\n\n throw new Error(`Unknown product form field: \"${field}\".`);\n }) as ProductFormRegister;\n}\n"],"mappings":";;;;;AA4EA,SAAgB,0BACd,iBACA,cACqB;AACrB,UAAS,OAAe,SAAmC;AACzD,MAAI,UAAU,gBACZ,QAAO;GAAE,MAAM;GAAiB,OAAO,iBAAiB,MAAM;GAAI;AAGpE,MAAI,UAAU,YAAY;AACxB,OAAI,OAAO,MAAM,iBAAiB,SAChC,QAAO;IAAE,MAAM;IAAqB,cAAc,OAAO,KAAK,aAAa;IAAE;GAE/E,MAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAC7D,UAAO;IAAE,MAAM;IAAqB,OAAO,OAAO,MAAM;IAAE;;AAG5D,MAAI,UAAU,eAAe;GAC3B,MAAM,aAAa,OAAO,MAAM,cAAc,GAAG;GACjD,MAAM,QAAQ,OAAO,MAAM,SAAS,GAAG;GACvC,MAAM,qBAAqB,aAAa,YAAY,MAAM;AAC1D,UAAO;IAAE,MAAM;IAAY;IAAO,UAAU;IAAc,SAAS;IAAc;;AAGnF,MAAI,UAAU,kBAAkB;GAC9B,MAAM,MAAM,OAAO,MAAM,OAAO,GAAG;AACnC,OAAI,CAAC,IAAK,OAAM,IAAI,UAAU,wDAAsD;GACpF,MAAM,OAAO,cAAc;AAC3B,OAAI,QAAQ,kBAAkB,KAC5B,QAAO;IAAE;IAAM,cAAc,OAAO,KAAK,aAAa;IAAE;AAE1D,UAAO;IAAE;IAAM,OAAO,OAAO,MAAM,SAAS,GAAG;IAAE;;AAGnD,MAAI,UAAU,YACZ,QAAO;GAAE,MAAM;GAAe,MAAM;GAAU;AAGhD,QAAM,IAAI,MAAM,gCAAgC,MAAM,IAAI"}
|
|
@@ -2,4 +2,4 @@ import { ProductInput, ProductOptionInput, ProductOptionValueInput, ProductVaria
|
|
|
2
2
|
import { CreateProductFormStoreOptions, ProductFormErrors, ProductFormOptions, ProductFormStore, ProductFormStoreState, ValidProductSelectionResult, VariantSelectionResult, canAddToCart, createProductFormStore, findCartLineByMerchandiseId, getSelectedVariant } from "./product-form.mjs";
|
|
3
3
|
import { getSelectedProductOptions } from "./options.mjs";
|
|
4
4
|
import { ProductSelectionLinkStyle, buildProductSelectionSearchParams } from "./url.mjs";
|
|
5
|
-
import { ProductAddToCartProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister } from "./form.mjs";
|
|
5
|
+
import { ProductAddToCartProps, ProductAttributeDefaultValueProps, ProductAttributeValueProps, ProductFormRegister, ProductMerchandiseIdProps, ProductOptionValueProps, ProductQuantityDefaultProps, ProductQuantityProps, createProductFormRegister } from "./form.mjs";
|
|
@@ -14,8 +14,8 @@ import { SelectedOption } from "./state.mjs";
|
|
|
14
14
|
* entries whose decoded param name exactly matches a product option name.
|
|
15
15
|
* Passing an empty array filters out every option.
|
|
16
16
|
*
|
|
17
|
-
* Pass the result to `createProductFormStore`
|
|
18
|
-
*
|
|
17
|
+
* Pass the result to `createProductFormStore` as `selectedOptions`. Used as a
|
|
18
|
+
* fallback when the product has no `selectedOrFirstAvailableVariant`.
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
21
|
* ```ts
|
|
@@ -17,8 +17,8 @@ const INCLUSIVE_RANGE_END_OFFSET = 1;
|
|
|
17
17
|
* entries whose decoded param name exactly matches a product option name.
|
|
18
18
|
* Passing an empty array filters out every option.
|
|
19
19
|
*
|
|
20
|
-
* Pass the result to `createProductFormStore`
|
|
21
|
-
*
|
|
20
|
+
* Pass the result to `createProductFormStore` as `selectedOptions`. Used as a
|
|
21
|
+
* fallback when the product has no `selectedOrFirstAvailableVariant`.
|
|
22
22
|
*
|
|
23
23
|
* @example
|
|
24
24
|
* ```ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.mjs","names":[],"sources":["../../../src/core/product/options.ts"],"sourcesContent":["import { getLogger } from \"../logging\";\nimport type {\n ProductInput,\n ProductOptionValueFrom,\n ProductVariantFrom,\n ProductVariantInput,\n SelectedOption,\n VariantOptionState,\n VariantOptionValueState,\n} from \"./state\";\nimport { VARIANT_SEARCH_PARAM } from \"./url\";\n\nconst log = getLogger(\"product\");\n\nconst INCLUSIVE_RANGE_END_OFFSET = 1;\n\nexport type DecodedVariantCache = Map<string, number[][]>;\n\ntype EncodedVariantConstraint = {\n optionIndex: number;\n valueIndex: number;\n};\n\n/**\n * Extracts selected product options from search params.\n *\n * Each query parameter is treated as an option name/value pair\n * (e.g. `?Color=Red&Size=M` → `[{name:\"Color\",value:\"Red\"},{name:\"Size\",value:\"M\"}]`).\n *\n * The `variant` param is reserved for numeric variant ids (Liquid parity) and is\n * never treated as an option name, even when listed in `allowedOptionNames`.\n *\n * When `allowedOptionNames` is provided, the search params are filtered to only\n * entries whose decoded param name exactly matches a product option name.\n * Passing an empty array filters out every option.\n *\n * Pass the result to `createProductFormStore` or `useProductForm` to pre-select\n * the variant that matches the current URL.\n *\n * @example\n * ```ts\n * // Loader (React Router / Remix)\n * const selectedOptions = getSelectedProductOptions({\n * searchParams: new URL(request.url).searchParams,\n * });\n *\n * // Filter to known product option names when product data is available\n * const selectedOptions = getSelectedProductOptions({\n * searchParams,\n * allowedOptionNames: product.options.map((option) => option.name),\n * });\n * ```\n */\nexport function getSelectedProductOptions({\n searchParams,\n allowedOptionNames,\n}: {\n searchParams: URLSearchParams;\n allowedOptionNames?: readonly string[];\n}): SelectedOption[] {\n const allowedOptionNameSet = allowedOptionNames ? new Set(allowedOptionNames) : null;\n const selectedOptions: SelectedOption[] = [];\n\n for (const [name, value] of searchParams.entries()) {\n if (name === VARIANT_SEARCH_PARAM) continue;\n if (allowedOptionNameSet && !allowedOptionNameSet.has(name)) continue;\n selectedOptions.push({ name, value });\n }\n\n return selectedOptions;\n}\n\nfunction getAdjacentAndFirstSelectableVariants<TProduct extends ProductInput>(\n product: TProduct,\n): ProductVariantFrom<TProduct>[] {\n // Shopify returns a bounded set, not the whole matrix. Treat it as a concrete-variant cache.\n const variants = new Map<string, ProductVariantFrom<TProduct>>();\n\n for (const option of product.options) {\n for (const value of option.optionValues) {\n if (isConcreteProductVariant<TProduct>(value.firstSelectableVariant)) {\n addVariant(variants, value.firstSelectableVariant, product.options);\n }\n }\n }\n\n for (const variant of product.adjacentVariants) {\n if (isConcreteProductVariant<TProduct>(variant)) addVariant(variants, variant, product.options);\n }\n\n if (isConcreteProductVariant<TProduct>(product.selectedOrFirstAvailableVariant)) {\n addVariant(variants, product.selectedOrFirstAvailableVariant, product.options);\n }\n\n return [...variants.values()];\n}\n\nexport function buildProductOptions<TProduct extends ProductInput>(\n product: TProduct,\n selectedOptions: SelectedOption[],\n cache?: DecodedVariantCache,\n): VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[] {\n const selectedOptionMap = selectedOptionsToMap(selectedOptions);\n const optionValueIndex = buildOptionValueIndex(product);\n const selectableVariants = getAdjacentAndFirstSelectableVariants(product);\n const encodedOptionIndexes = buildEncodedOptionIndexes(product, selectableVariants);\n const variants = mapVariants(product, selectableVariants);\n const currentProductOptionValues = buildCurrentProductOptionValues(product, selectableVariants);\n\n return product.options.map((option) => ({\n name: option.name,\n values: option.optionValues.map((value) =>\n buildProductOptionValue({\n cache,\n currentProductOptionValues,\n encodedOptionIndexes,\n option,\n optionValueIndex,\n product,\n selectedOptionMap,\n value,\n variants,\n }),\n ),\n }));\n}\n\nexport function selectedOptionsToMap(selectedOptions: SelectedOption[]): Record<string, string> {\n const map: Record<string, string> = Object.create(null);\n for (const option of selectedOptions) {\n map[option.name] = option.value;\n }\n return map;\n}\n\nfunction selectedOptionsKey(\n selectedOptions: SelectedOption[],\n productOptions: Array<{ name: string }>,\n): string {\n return JSON.stringify(\n selectedOptionsFromMap({ options: productOptions }, selectedOptionsToMap(selectedOptions)),\n );\n}\n\nexport function selectedOptionsFromMap(\n product: { options: Array<{ name: string }> },\n selectedOptionMap: Record<string, string>,\n): SelectedOption[] {\n const selectedOptions: SelectedOption[] = [];\n for (const option of product.options) {\n const value = selectedOptionMap[option.name];\n if (value !== undefined) selectedOptions.push({ name: option.name, value });\n }\n return selectedOptions;\n}\n\nfunction mapVariants<TProduct extends ProductInput>(\n product: TProduct,\n selectableVariants: ProductVariantFrom<TProduct>[],\n): Map<string, ProductVariantFrom<TProduct>> {\n const variants = new Map<string, ProductVariantFrom<TProduct>>();\n for (const variant of selectableVariants) {\n variants.set(selectedOptionsKey(variant.selectedOptions, product.options), variant);\n }\n return variants;\n}\n\nfunction addVariant<TVariant extends ProductVariantInput>(\n variants: Map<string, TVariant>,\n variant: TVariant,\n productOptions: Array<{ name: string }>,\n): void {\n variants.set(selectedOptionsKey(variant.selectedOptions, productOptions), variant);\n}\n\nfunction buildProductOptionValue<TProduct extends ProductInput>({\n cache,\n currentProductOptionValues,\n encodedOptionIndexes,\n option,\n optionValueIndex,\n product,\n selectedOptionMap,\n value,\n variants,\n}: {\n cache?: DecodedVariantCache;\n currentProductOptionValues: Map<string, Set<string>>;\n encodedOptionIndexes: Map<string, Map<string, number>>;\n option: TProduct[\"options\"][number];\n optionValueIndex: Map<string, Map<string, number>>;\n product: TProduct;\n selectedOptionMap: Record<string, string>;\n value: ProductOptionValueFrom<TProduct>;\n variants: Map<string, ProductVariantFrom<TProduct>>;\n}): VariantOptionValueState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>> {\n const firstSelectableVariant = isConcreteProductVariant<TProduct>(value.firstSelectableVariant)\n ? value.firstSelectableVariant\n : null;\n const selected = selectedOptionMap[option.name] === value.name;\n const crossProduct = isCrossProductOptionValue(\n product,\n option.name,\n value.name,\n firstSelectableVariant,\n currentProductOptionValues,\n );\n const targetOptionMap = buildTargetOptionMap({\n currentProductOptionValues,\n crossProduct,\n firstSelectableVariant,\n optionName: option.name,\n selectedOptionMap,\n valueName: value.name,\n });\n const targetSelectedOptions = selectedOptionsFromMap(product, targetOptionMap);\n const key = selectedOptionsKey(targetSelectedOptions, product.options);\n const variant = variants.get(key) ?? null;\n const targetProductHandle = getTargetProductHandle(product, crossProduct, firstSelectableVariant);\n const encodingConstraints = buildEncodingConstraints(\n targetOptionMap,\n product,\n optionValueIndex,\n encodedOptionIndexes.get(targetProductHandle),\n );\n\n return {\n name: value.name,\n swatch: value.swatch,\n selected,\n exists: resolveEncodedStatus(product.encodedVariantExistence, encodingConstraints, true, cache),\n available: resolveEncodedStatus(\n product.encodedVariantAvailability,\n encodingConstraints,\n variant?.availableForSale ?? false,\n cache,\n ),\n variant,\n selectedOptions: variant?.selectedOptions ?? targetSelectedOptions,\n handle: getOptionValueHandle(selected, product.handle, variant, firstSelectableVariant),\n };\n}\n\nfunction getOptionValueHandle<TVariant extends ProductVariantInput>(\n selected: boolean,\n productHandle: string,\n variant: TVariant | null,\n firstSelectableVariant: TVariant | null,\n): string {\n if (selected) return productHandle;\n return variant?.product?.handle ?? firstSelectableVariant?.product?.handle ?? productHandle;\n}\n\nfunction buildOptionValueIndex<TProduct extends ProductInput>(\n product: TProduct,\n): Map<string, Map<string, number>> {\n return new Map(\n product.options.map((option) => [\n option.name,\n new Map(option.optionValues.map((value, index) => [value.name, index])),\n ]),\n );\n}\n\nfunction getTargetProductHandle<TVariant extends ProductVariantInput>(\n product: ProductInput,\n crossProduct: boolean,\n firstSelectableVariant: TVariant | null,\n): string {\n if (crossProduct && firstSelectableVariant?.product?.handle) {\n return firstSelectableVariant.product.handle;\n }\n return product.handle;\n}\n\nfunction buildEncodedOptionIndexes<TProduct extends ProductInput>(\n product: TProduct,\n selectableVariants: ProductVariantFrom<TProduct>[],\n): Map<string, Map<string, number>> {\n const indexes = new Map<string, Map<string, number>>();\n for (const variant of selectableVariants) {\n const productHandle = variant.product?.handle ?? product.handle;\n addEncodedOptionIndex(indexes, productHandle, product.options, variant.selectedOptions);\n }\n return indexes;\n}\n\nfunction addEncodedOptionIndex(\n indexes: Map<string, Map<string, number>>,\n productHandle: string,\n productOptions: Array<{ name: string }>,\n selectedOptions: SelectedOption[],\n): void {\n const index = indexes.get(productHandle) ?? new Map<string, number>();\n const selectedOptionNames = new Set(selectedOptions.map((option) => option.name));\n\n for (const option of productOptions) {\n if (!selectedOptionNames.has(option.name) || index.has(option.name)) continue;\n index.set(option.name, index.size);\n }\n\n indexes.set(productHandle, index);\n}\n\nfunction buildCurrentProductOptionValues<TProduct extends ProductInput>(\n product: TProduct,\n selectableVariants: ProductVariantFrom<TProduct>[],\n): Map<string, Set<string>> {\n const optionValues = new Map<string, Set<string>>();\n for (const variant of selectableVariants) {\n if (!variantBelongsToProduct(product, variant)) continue;\n for (const option of variant.selectedOptions) {\n addCurrentProductOptionValue(optionValues, option.name, option.value);\n }\n }\n return optionValues;\n}\n\nfunction addCurrentProductOptionValue(\n optionValues: Map<string, Set<string>>,\n optionName: string,\n valueName: string,\n): void {\n const values = optionValues.get(optionName) ?? new Set<string>();\n values.add(valueName);\n optionValues.set(optionName, values);\n}\n\nfunction isCrossProductOptionValue<TVariant extends ProductVariantInput>(\n product: ProductInput,\n optionName: string,\n valueName: string,\n firstSelectableVariant: TVariant | null,\n currentProductOptionValues: Map<string, Set<string>>,\n): boolean {\n if (!firstSelectableVariant || variantBelongsToProduct(product, firstSelectableVariant)) {\n return false;\n }\n return !currentProductOptionValues.get(optionName)?.has(valueName);\n}\n\nfunction variantBelongsToProduct<TVariant extends ProductVariantInput>(\n product: ProductInput,\n variant: TVariant,\n): boolean {\n return variant.product?.handle === undefined || variant.product.handle === product.handle;\n}\n\ntype TargetOptionMapInput<TVariant extends ProductVariantInput> = {\n currentProductOptionValues: Map<string, Set<string>>;\n crossProduct: boolean;\n firstSelectableVariant: TVariant | null;\n optionName: string;\n selectedOptionMap: Record<string, string>;\n valueName: string;\n};\n\nfunction buildTargetOptionMap<TVariant extends ProductVariantInput>({\n currentProductOptionValues,\n crossProduct,\n firstSelectableVariant,\n optionName,\n selectedOptionMap,\n valueName,\n}: TargetOptionMapInput<TVariant>): Record<string, string> {\n if (crossProduct && firstSelectableVariant) {\n return selectedOptionsToMap(firstSelectableVariant.selectedOptions);\n }\n\n const nextOptionMap = { ...selectedOptionMap, [optionName]: valueName };\n if (currentProductOptionValues.size === 0) return nextOptionMap;\n return filterOptionMap(nextOptionMap, currentProductOptionValues);\n}\n\nfunction filterOptionMap(\n selectedOptionMap: Record<string, string>,\n currentProductOptionValues: Map<string, Set<string>>,\n): Record<string, string> {\n const filtered: Record<string, string> = Object.create(null);\n for (const [optionName, values] of currentProductOptionValues) {\n const value = selectedOptionMap[optionName];\n if (value !== undefined && values.has(value)) filtered[optionName] = value;\n }\n return filtered;\n}\n\nfunction buildEncodingConstraints<TProduct extends ProductInput>(\n selectedOptionMap: Record<string, string>,\n product: TProduct,\n optionValueIndex = buildOptionValueIndex(product),\n encodedOptionIndex?: Map<string, number>,\n): EncodedVariantConstraint[] {\n const constraints: EncodedVariantConstraint[] = [];\n\n for (const [productOptionIndex, option] of product.options.entries()) {\n const selectedValue = selectedOptionMap[option.name];\n if (selectedValue === undefined) continue;\n const optionIndex = encodedOptionIndex?.get(option.name) ?? productOptionIndex;\n const valueIndex = optionValueIndex.get(option.name)?.get(selectedValue);\n if (valueIndex === undefined) continue;\n constraints.push({ optionIndex, valueIndex });\n }\n\n return constraints;\n}\n\nfunction resolveEncodedStatus(\n encodedField: string | null | undefined,\n constraints: EncodedVariantConstraint[],\n fallback: boolean,\n cache?: DecodedVariantCache,\n): boolean {\n if (!encodedField) return fallback;\n return isAnyEncodedVariantMatchingConstraints(constraints, encodedField, cache);\n}\n\nfunction isAnyEncodedVariantMatchingConstraints(\n constraints: EncodedVariantConstraint[],\n encodedVariantField: string,\n cache?: DecodedVariantCache,\n): boolean {\n if (constraints.length === 0) return false;\n\n return getDecodedVariantCombinations(encodedVariantField, cache).some((combination) =>\n combinationMatchesConstraints(combination, constraints),\n );\n}\n\nfunction getDecodedVariantCombinations(\n encodedVariantField: string,\n cache?: DecodedVariantCache,\n): number[][] {\n const cached = cache?.get(encodedVariantField);\n if (cached) return cached;\n\n const decoded = decodeEncodedVariant(encodedVariantField);\n cache?.set(encodedVariantField, decoded);\n return decoded;\n}\n\nfunction combinationMatchesConstraints(\n combination: number[],\n constraints: EncodedVariantConstraint[],\n): boolean {\n return constraints.every(\n ({ optionIndex, valueIndex }) => combination[optionIndex] === valueIndex,\n );\n}\n\nfunction decodeEncodedVariant(encodedVariantField: string | null | undefined): number[][] {\n if (!encodedVariantField) return [];\n if (!encodedVariantField.startsWith(\"v1_\")) {\n log.warn(`unsupported variant encoding: \"${encodedVariantField}\"`);\n return [];\n }\n return decodeV1EncodedVariant(encodedVariantField.replace(/^v1_/, \"\"));\n}\n\nfunction decodeV1EncodedVariant(encodedVariantField: string): number[][] {\n const tokenizer = /[ :,-]/g;\n const state: DecodeV1State = {\n currentOptionValue: [],\n decodedOptions: [],\n depth: 0,\n index: 0,\n rangeStart: null,\n };\n let token: RegExpExecArray | null;\n\n while ((token = tokenizer.exec(encodedVariantField))) {\n processDecodeV1Token(encodedVariantField, state, token, tokenizer);\n }\n\n pushFinalDecodeV1Value(encodedVariantField, state);\n\n return state.decodedOptions;\n}\n\ntype DecodeV1State = {\n currentOptionValue: number[];\n decodedOptions: number[][];\n depth: number;\n index: number;\n rangeStart: number | null;\n};\n\nfunction isConcreteProductVariant<TProduct extends ProductInput>(\n variant: ProductVariantInput | null | undefined,\n): variant is ProductVariantFrom<TProduct> {\n return variant !== null && variant !== undefined;\n}\n\nfunction processDecodeV1Token(\n encodedVariantField: string,\n state: DecodeV1State,\n token: RegExpExecArray,\n tokenizer: RegExp,\n): void {\n const operation = token[0];\n const optionValueIndex =\n Number.parseInt(encodedVariantField.slice(state.index, token.index), 10) || 0;\n\n pushDecodeV1Range(state, optionValueIndex);\n state.currentOptionValue[state.depth] = optionValueIndex;\n\n if (operation === \"-\") {\n state.rangeStart = optionValueIndex;\n } else if (operation === \":\") {\n state.depth++;\n } else {\n processDecodeV1Separator(encodedVariantField, state, token, operation);\n }\n\n state.index = tokenizer.lastIndex;\n}\n\nfunction processDecodeV1Separator(\n encodedVariantField: string,\n state: DecodeV1State,\n token: RegExpExecArray,\n operation: string,\n): void {\n if (shouldPushDecodeV1Option(encodedVariantField, token, operation)) {\n state.decodedOptions.push([...state.currentOptionValue]);\n }\n\n if (operation !== \",\") return;\n\n state.currentOptionValue.pop();\n state.depth--;\n}\n\nfunction shouldPushDecodeV1Option(\n encodedVariantField: string,\n token: RegExpExecArray,\n operation: string,\n): boolean {\n return operation === \" \" || (operation === \",\" && encodedVariantField[token.index - 1] !== \",\");\n}\n\nfunction pushFinalDecodeV1Value(encodedVariantField: string, state: DecodeV1State): void {\n const finalIndex = encodedVariantField.match(/\\d+$/g)?.[0];\n if (finalIndex === undefined) return;\n\n const finalValueIndex = Number.parseInt(finalIndex, 10);\n if (state.rangeStart !== null) {\n pushDecodeV1Range(state, finalValueIndex + INCLUSIVE_RANGE_END_OFFSET);\n return;\n }\n\n state.currentOptionValue[state.depth] = finalValueIndex;\n state.decodedOptions.push([...state.currentOptionValue]);\n}\n\nfunction pushDecodeV1Range(state: DecodeV1State, rangeEndExclusive: number): void {\n const rangeStart = state.rangeStart;\n if (rangeStart === null) return;\n\n for (let value = rangeStart; value < rangeEndExclusive; value++) {\n state.currentOptionValue[state.depth] = value;\n state.decodedOptions.push([...state.currentOptionValue]);\n }\n\n state.rangeStart = null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"options.mjs","names":[],"sources":["../../../src/core/product/options.ts"],"sourcesContent":["import { getLogger } from \"../logging\";\nimport type {\n ProductInput,\n ProductOptionValueFrom,\n ProductVariantFrom,\n ProductVariantInput,\n SelectedOption,\n VariantOptionState,\n VariantOptionValueState,\n} from \"./state\";\nimport { VARIANT_SEARCH_PARAM } from \"./url\";\n\nconst log = getLogger(\"product\");\n\nconst INCLUSIVE_RANGE_END_OFFSET = 1;\n\nexport type DecodedVariantCache = Map<string, number[][]>;\n\ntype EncodedVariantConstraint = {\n optionIndex: number;\n valueIndex: number;\n};\n\n/**\n * Extracts selected product options from search params.\n *\n * Each query parameter is treated as an option name/value pair\n * (e.g. `?Color=Red&Size=M` → `[{name:\"Color\",value:\"Red\"},{name:\"Size\",value:\"M\"}]`).\n *\n * The `variant` param is reserved for numeric variant ids (Liquid parity) and is\n * never treated as an option name, even when listed in `allowedOptionNames`.\n *\n * When `allowedOptionNames` is provided, the search params are filtered to only\n * entries whose decoded param name exactly matches a product option name.\n * Passing an empty array filters out every option.\n *\n * Pass the result to `createProductFormStore` as `selectedOptions`. Used as a\n * fallback when the product has no `selectedOrFirstAvailableVariant`.\n *\n * @example\n * ```ts\n * // Loader (React Router / Remix)\n * const selectedOptions = getSelectedProductOptions({\n * searchParams: new URL(request.url).searchParams,\n * });\n *\n * // Filter to known product option names when product data is available\n * const selectedOptions = getSelectedProductOptions({\n * searchParams,\n * allowedOptionNames: product.options.map((option) => option.name),\n * });\n * ```\n */\nexport function getSelectedProductOptions({\n searchParams,\n allowedOptionNames,\n}: {\n searchParams: URLSearchParams;\n allowedOptionNames?: readonly string[];\n}): SelectedOption[] {\n const allowedOptionNameSet = allowedOptionNames ? new Set(allowedOptionNames) : null;\n const selectedOptions: SelectedOption[] = [];\n\n for (const [name, value] of searchParams.entries()) {\n if (name === VARIANT_SEARCH_PARAM) continue;\n if (allowedOptionNameSet && !allowedOptionNameSet.has(name)) continue;\n selectedOptions.push({ name, value });\n }\n\n return selectedOptions;\n}\n\nfunction getAdjacentAndFirstSelectableVariants<TProduct extends ProductInput>(\n product: TProduct,\n): ProductVariantFrom<TProduct>[] {\n // Shopify returns a bounded set, not the whole matrix. Treat it as a concrete-variant cache.\n const variants = new Map<string, ProductVariantFrom<TProduct>>();\n\n for (const option of product.options) {\n for (const value of option.optionValues) {\n if (isConcreteProductVariant<TProduct>(value.firstSelectableVariant)) {\n addVariant(variants, value.firstSelectableVariant, product.options);\n }\n }\n }\n\n for (const variant of product.adjacentVariants) {\n if (isConcreteProductVariant<TProduct>(variant)) addVariant(variants, variant, product.options);\n }\n\n if (isConcreteProductVariant<TProduct>(product.selectedOrFirstAvailableVariant)) {\n addVariant(variants, product.selectedOrFirstAvailableVariant, product.options);\n }\n\n return [...variants.values()];\n}\n\nexport function buildProductOptions<TProduct extends ProductInput>(\n product: TProduct,\n selectedOptions: SelectedOption[],\n cache?: DecodedVariantCache,\n): VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[] {\n const selectedOptionMap = selectedOptionsToMap(selectedOptions);\n const optionValueIndex = buildOptionValueIndex(product);\n const selectableVariants = getAdjacentAndFirstSelectableVariants(product);\n const encodedOptionIndexes = buildEncodedOptionIndexes(product, selectableVariants);\n const variants = mapVariants(product, selectableVariants);\n const currentProductOptionValues = buildCurrentProductOptionValues(product, selectableVariants);\n\n return product.options.map((option) => ({\n name: option.name,\n values: option.optionValues.map((value) =>\n buildProductOptionValue({\n cache,\n currentProductOptionValues,\n encodedOptionIndexes,\n option,\n optionValueIndex,\n product,\n selectedOptionMap,\n value,\n variants,\n }),\n ),\n }));\n}\n\nexport function selectedOptionsToMap(selectedOptions: SelectedOption[]): Record<string, string> {\n const map: Record<string, string> = Object.create(null);\n for (const option of selectedOptions) {\n map[option.name] = option.value;\n }\n return map;\n}\n\nfunction selectedOptionsKey(\n selectedOptions: SelectedOption[],\n productOptions: Array<{ name: string }>,\n): string {\n return JSON.stringify(\n selectedOptionsFromMap({ options: productOptions }, selectedOptionsToMap(selectedOptions)),\n );\n}\n\nexport function selectedOptionsFromMap(\n product: { options: Array<{ name: string }> },\n selectedOptionMap: Record<string, string>,\n): SelectedOption[] {\n const selectedOptions: SelectedOption[] = [];\n for (const option of product.options) {\n const value = selectedOptionMap[option.name];\n if (value !== undefined) selectedOptions.push({ name: option.name, value });\n }\n return selectedOptions;\n}\n\nfunction mapVariants<TProduct extends ProductInput>(\n product: TProduct,\n selectableVariants: ProductVariantFrom<TProduct>[],\n): Map<string, ProductVariantFrom<TProduct>> {\n const variants = new Map<string, ProductVariantFrom<TProduct>>();\n for (const variant of selectableVariants) {\n variants.set(selectedOptionsKey(variant.selectedOptions, product.options), variant);\n }\n return variants;\n}\n\nfunction addVariant<TVariant extends ProductVariantInput>(\n variants: Map<string, TVariant>,\n variant: TVariant,\n productOptions: Array<{ name: string }>,\n): void {\n variants.set(selectedOptionsKey(variant.selectedOptions, productOptions), variant);\n}\n\nfunction buildProductOptionValue<TProduct extends ProductInput>({\n cache,\n currentProductOptionValues,\n encodedOptionIndexes,\n option,\n optionValueIndex,\n product,\n selectedOptionMap,\n value,\n variants,\n}: {\n cache?: DecodedVariantCache;\n currentProductOptionValues: Map<string, Set<string>>;\n encodedOptionIndexes: Map<string, Map<string, number>>;\n option: TProduct[\"options\"][number];\n optionValueIndex: Map<string, Map<string, number>>;\n product: TProduct;\n selectedOptionMap: Record<string, string>;\n value: ProductOptionValueFrom<TProduct>;\n variants: Map<string, ProductVariantFrom<TProduct>>;\n}): VariantOptionValueState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>> {\n const firstSelectableVariant = isConcreteProductVariant<TProduct>(value.firstSelectableVariant)\n ? value.firstSelectableVariant\n : null;\n const selected = selectedOptionMap[option.name] === value.name;\n const crossProduct = isCrossProductOptionValue(\n product,\n option.name,\n value.name,\n firstSelectableVariant,\n currentProductOptionValues,\n );\n const targetOptionMap = buildTargetOptionMap({\n currentProductOptionValues,\n crossProduct,\n firstSelectableVariant,\n optionName: option.name,\n selectedOptionMap,\n valueName: value.name,\n });\n const targetSelectedOptions = selectedOptionsFromMap(product, targetOptionMap);\n const key = selectedOptionsKey(targetSelectedOptions, product.options);\n const variant = variants.get(key) ?? null;\n const targetProductHandle = getTargetProductHandle(product, crossProduct, firstSelectableVariant);\n const encodingConstraints = buildEncodingConstraints(\n targetOptionMap,\n product,\n optionValueIndex,\n encodedOptionIndexes.get(targetProductHandle),\n );\n\n return {\n name: value.name,\n swatch: value.swatch,\n selected,\n exists: resolveEncodedStatus(product.encodedVariantExistence, encodingConstraints, true, cache),\n available: resolveEncodedStatus(\n product.encodedVariantAvailability,\n encodingConstraints,\n variant?.availableForSale ?? false,\n cache,\n ),\n variant,\n selectedOptions: variant?.selectedOptions ?? targetSelectedOptions,\n handle: getOptionValueHandle(selected, product.handle, variant, firstSelectableVariant),\n };\n}\n\nfunction getOptionValueHandle<TVariant extends ProductVariantInput>(\n selected: boolean,\n productHandle: string,\n variant: TVariant | null,\n firstSelectableVariant: TVariant | null,\n): string {\n if (selected) return productHandle;\n return variant?.product?.handle ?? firstSelectableVariant?.product?.handle ?? productHandle;\n}\n\nfunction buildOptionValueIndex<TProduct extends ProductInput>(\n product: TProduct,\n): Map<string, Map<string, number>> {\n return new Map(\n product.options.map((option) => [\n option.name,\n new Map(option.optionValues.map((value, index) => [value.name, index])),\n ]),\n );\n}\n\nfunction getTargetProductHandle<TVariant extends ProductVariantInput>(\n product: ProductInput,\n crossProduct: boolean,\n firstSelectableVariant: TVariant | null,\n): string {\n if (crossProduct && firstSelectableVariant?.product?.handle) {\n return firstSelectableVariant.product.handle;\n }\n return product.handle;\n}\n\nfunction buildEncodedOptionIndexes<TProduct extends ProductInput>(\n product: TProduct,\n selectableVariants: ProductVariantFrom<TProduct>[],\n): Map<string, Map<string, number>> {\n const indexes = new Map<string, Map<string, number>>();\n for (const variant of selectableVariants) {\n const productHandle = variant.product?.handle ?? product.handle;\n addEncodedOptionIndex(indexes, productHandle, product.options, variant.selectedOptions);\n }\n return indexes;\n}\n\nfunction addEncodedOptionIndex(\n indexes: Map<string, Map<string, number>>,\n productHandle: string,\n productOptions: Array<{ name: string }>,\n selectedOptions: SelectedOption[],\n): void {\n const index = indexes.get(productHandle) ?? new Map<string, number>();\n const selectedOptionNames = new Set(selectedOptions.map((option) => option.name));\n\n for (const option of productOptions) {\n if (!selectedOptionNames.has(option.name) || index.has(option.name)) continue;\n index.set(option.name, index.size);\n }\n\n indexes.set(productHandle, index);\n}\n\nfunction buildCurrentProductOptionValues<TProduct extends ProductInput>(\n product: TProduct,\n selectableVariants: ProductVariantFrom<TProduct>[],\n): Map<string, Set<string>> {\n const optionValues = new Map<string, Set<string>>();\n for (const variant of selectableVariants) {\n if (!variantBelongsToProduct(product, variant)) continue;\n for (const option of variant.selectedOptions) {\n addCurrentProductOptionValue(optionValues, option.name, option.value);\n }\n }\n return optionValues;\n}\n\nfunction addCurrentProductOptionValue(\n optionValues: Map<string, Set<string>>,\n optionName: string,\n valueName: string,\n): void {\n const values = optionValues.get(optionName) ?? new Set<string>();\n values.add(valueName);\n optionValues.set(optionName, values);\n}\n\nfunction isCrossProductOptionValue<TVariant extends ProductVariantInput>(\n product: ProductInput,\n optionName: string,\n valueName: string,\n firstSelectableVariant: TVariant | null,\n currentProductOptionValues: Map<string, Set<string>>,\n): boolean {\n if (!firstSelectableVariant || variantBelongsToProduct(product, firstSelectableVariant)) {\n return false;\n }\n return !currentProductOptionValues.get(optionName)?.has(valueName);\n}\n\nfunction variantBelongsToProduct<TVariant extends ProductVariantInput>(\n product: ProductInput,\n variant: TVariant,\n): boolean {\n return variant.product?.handle === undefined || variant.product.handle === product.handle;\n}\n\ntype TargetOptionMapInput<TVariant extends ProductVariantInput> = {\n currentProductOptionValues: Map<string, Set<string>>;\n crossProduct: boolean;\n firstSelectableVariant: TVariant | null;\n optionName: string;\n selectedOptionMap: Record<string, string>;\n valueName: string;\n};\n\nfunction buildTargetOptionMap<TVariant extends ProductVariantInput>({\n currentProductOptionValues,\n crossProduct,\n firstSelectableVariant,\n optionName,\n selectedOptionMap,\n valueName,\n}: TargetOptionMapInput<TVariant>): Record<string, string> {\n if (crossProduct && firstSelectableVariant) {\n return selectedOptionsToMap(firstSelectableVariant.selectedOptions);\n }\n\n const nextOptionMap = { ...selectedOptionMap, [optionName]: valueName };\n if (currentProductOptionValues.size === 0) return nextOptionMap;\n return filterOptionMap(nextOptionMap, currentProductOptionValues);\n}\n\nfunction filterOptionMap(\n selectedOptionMap: Record<string, string>,\n currentProductOptionValues: Map<string, Set<string>>,\n): Record<string, string> {\n const filtered: Record<string, string> = Object.create(null);\n for (const [optionName, values] of currentProductOptionValues) {\n const value = selectedOptionMap[optionName];\n if (value !== undefined && values.has(value)) filtered[optionName] = value;\n }\n return filtered;\n}\n\nfunction buildEncodingConstraints<TProduct extends ProductInput>(\n selectedOptionMap: Record<string, string>,\n product: TProduct,\n optionValueIndex = buildOptionValueIndex(product),\n encodedOptionIndex?: Map<string, number>,\n): EncodedVariantConstraint[] {\n const constraints: EncodedVariantConstraint[] = [];\n\n for (const [productOptionIndex, option] of product.options.entries()) {\n const selectedValue = selectedOptionMap[option.name];\n if (selectedValue === undefined) continue;\n const optionIndex = encodedOptionIndex?.get(option.name) ?? productOptionIndex;\n const valueIndex = optionValueIndex.get(option.name)?.get(selectedValue);\n if (valueIndex === undefined) continue;\n constraints.push({ optionIndex, valueIndex });\n }\n\n return constraints;\n}\n\nfunction resolveEncodedStatus(\n encodedField: string | null | undefined,\n constraints: EncodedVariantConstraint[],\n fallback: boolean,\n cache?: DecodedVariantCache,\n): boolean {\n if (!encodedField) return fallback;\n return isAnyEncodedVariantMatchingConstraints(constraints, encodedField, cache);\n}\n\nfunction isAnyEncodedVariantMatchingConstraints(\n constraints: EncodedVariantConstraint[],\n encodedVariantField: string,\n cache?: DecodedVariantCache,\n): boolean {\n if (constraints.length === 0) return false;\n\n return getDecodedVariantCombinations(encodedVariantField, cache).some((combination) =>\n combinationMatchesConstraints(combination, constraints),\n );\n}\n\nfunction getDecodedVariantCombinations(\n encodedVariantField: string,\n cache?: DecodedVariantCache,\n): number[][] {\n const cached = cache?.get(encodedVariantField);\n if (cached) return cached;\n\n const decoded = decodeEncodedVariant(encodedVariantField);\n cache?.set(encodedVariantField, decoded);\n return decoded;\n}\n\nfunction combinationMatchesConstraints(\n combination: number[],\n constraints: EncodedVariantConstraint[],\n): boolean {\n return constraints.every(\n ({ optionIndex, valueIndex }) => combination[optionIndex] === valueIndex,\n );\n}\n\nfunction decodeEncodedVariant(encodedVariantField: string | null | undefined): number[][] {\n if (!encodedVariantField) return [];\n if (!encodedVariantField.startsWith(\"v1_\")) {\n log.warn(`unsupported variant encoding: \"${encodedVariantField}\"`);\n return [];\n }\n return decodeV1EncodedVariant(encodedVariantField.replace(/^v1_/, \"\"));\n}\n\nfunction decodeV1EncodedVariant(encodedVariantField: string): number[][] {\n const tokenizer = /[ :,-]/g;\n const state: DecodeV1State = {\n currentOptionValue: [],\n decodedOptions: [],\n depth: 0,\n index: 0,\n rangeStart: null,\n };\n let token: RegExpExecArray | null;\n\n while ((token = tokenizer.exec(encodedVariantField))) {\n processDecodeV1Token(encodedVariantField, state, token, tokenizer);\n }\n\n pushFinalDecodeV1Value(encodedVariantField, state);\n\n return state.decodedOptions;\n}\n\ntype DecodeV1State = {\n currentOptionValue: number[];\n decodedOptions: number[][];\n depth: number;\n index: number;\n rangeStart: number | null;\n};\n\nfunction isConcreteProductVariant<TProduct extends ProductInput>(\n variant: ProductVariantInput | null | undefined,\n): variant is ProductVariantFrom<TProduct> {\n return variant !== null && variant !== undefined;\n}\n\nfunction processDecodeV1Token(\n encodedVariantField: string,\n state: DecodeV1State,\n token: RegExpExecArray,\n tokenizer: RegExp,\n): void {\n const operation = token[0];\n const optionValueIndex =\n Number.parseInt(encodedVariantField.slice(state.index, token.index), 10) || 0;\n\n pushDecodeV1Range(state, optionValueIndex);\n state.currentOptionValue[state.depth] = optionValueIndex;\n\n if (operation === \"-\") {\n state.rangeStart = optionValueIndex;\n } else if (operation === \":\") {\n state.depth++;\n } else {\n processDecodeV1Separator(encodedVariantField, state, token, operation);\n }\n\n state.index = tokenizer.lastIndex;\n}\n\nfunction processDecodeV1Separator(\n encodedVariantField: string,\n state: DecodeV1State,\n token: RegExpExecArray,\n operation: string,\n): void {\n if (shouldPushDecodeV1Option(encodedVariantField, token, operation)) {\n state.decodedOptions.push([...state.currentOptionValue]);\n }\n\n if (operation !== \",\") return;\n\n state.currentOptionValue.pop();\n state.depth--;\n}\n\nfunction shouldPushDecodeV1Option(\n encodedVariantField: string,\n token: RegExpExecArray,\n operation: string,\n): boolean {\n return operation === \" \" || (operation === \",\" && encodedVariantField[token.index - 1] !== \",\");\n}\n\nfunction pushFinalDecodeV1Value(encodedVariantField: string, state: DecodeV1State): void {\n const finalIndex = encodedVariantField.match(/\\d+$/g)?.[0];\n if (finalIndex === undefined) return;\n\n const finalValueIndex = Number.parseInt(finalIndex, 10);\n if (state.rangeStart !== null) {\n pushDecodeV1Range(state, finalValueIndex + INCLUSIVE_RANGE_END_OFFSET);\n return;\n }\n\n state.currentOptionValue[state.depth] = finalValueIndex;\n state.decodedOptions.push([...state.currentOptionValue]);\n}\n\nfunction pushDecodeV1Range(state: DecodeV1State, rangeEndExclusive: number): void {\n const rangeStart = state.rangeStart;\n if (rangeStart === null) return;\n\n for (let value = rangeStart; value < rangeEndExclusive; value++) {\n state.currentOptionValue[state.depth] = value;\n state.decodedOptions.push([...state.currentOptionValue]);\n }\n\n state.rangeStart = null;\n}\n"],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { CartStore } from "../cart/cart.mjs";
|
|
|
3
3
|
import { ProductInput, ProductOptionValueFrom, ProductOptionValueInput, ProductVariantFrom, ProductVariantInput, SelectedOption, VariantOptionState } from "./state.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/core/product/product-form.d.ts
|
|
6
|
+
/** Cart and line-item errors surfaced by a {@link ProductFormStore}. */
|
|
6
7
|
interface ProductFormErrors {
|
|
7
8
|
userErrors: CartUserError[];
|
|
8
9
|
warnings: CartWarning[];
|
|
@@ -16,6 +17,7 @@ interface ProductFormStoreState<TVariant extends ProductVariantInput = ProductVa
|
|
|
16
17
|
errors: ProductFormErrors;
|
|
17
18
|
matchedLineItem: CartLine | null;
|
|
18
19
|
}
|
|
20
|
+
/** The reactive options array from a {@link ProductFormStoreState}, typed to a specific product. */
|
|
19
21
|
type ProductFormOptions<TProduct extends ProductInput = ProductInput> = ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>["options"];
|
|
20
22
|
/**
|
|
21
23
|
* Result returned by {@link ProductFormStore.selectOption}.
|
|
@@ -42,22 +44,34 @@ type VariantSelectionResult<TVariant extends ProductVariantInput = ProductVarian
|
|
|
42
44
|
type ValidProductSelectionResult<TProduct extends ProductInput = ProductInput> = Exclude<VariantSelectionResult<ProductVariantFrom<TProduct>>, {
|
|
43
45
|
status: "invalid";
|
|
44
46
|
}>;
|
|
47
|
+
/** Options for {@link createProductFormStore}. */
|
|
45
48
|
type CreateProductFormStoreOptions = {
|
|
46
|
-
selectedOptions?: SelectedOption[];
|
|
49
|
+
/** Fallback selection used when the product has no `selectedOrFirstAvailableVariant`. */selectedOptions?: SelectedOption[];
|
|
47
50
|
};
|
|
48
51
|
/** Manages variant selection + cart integration for a product form. */
|
|
49
52
|
interface ProductFormStore<TProduct extends ProductInput = ProductInput, TVariant extends ProductVariantInput = ProductVariantFrom<TProduct>> {
|
|
53
|
+
/** Returns the current state snapshot. */
|
|
50
54
|
getState(): ProductFormStoreState<TVariant, ProductOptionValueFrom<TProduct>>;
|
|
55
|
+
/** Registers a listener invoked on every state change. Returns an unsubscribe function. */
|
|
51
56
|
subscribe(listener: (state: ProductFormStoreState<TVariant, ProductOptionValueFrom<TProduct>>) => void): () => void;
|
|
57
|
+
/** Selects an option value and resolves the new variant. */
|
|
52
58
|
selectOption(name: string, value: string): VariantSelectionResult<TVariant>;
|
|
59
|
+
/** Replaces the product data and re-derives state — use after a server-side product reload. */
|
|
53
60
|
hydrate(product: TProduct, opts?: {
|
|
54
61
|
selectedOptions?: SelectedOption[];
|
|
55
62
|
}): void;
|
|
63
|
+
/** Restores the store to its initial product and selection state. */
|
|
56
64
|
reset(): void;
|
|
65
|
+
/** Re-subscribes to the cart store and resyncs cart-derived state. Only needed to reuse the store after `destroy()` (e.g. React StrictMode effects). */
|
|
66
|
+
connect(): void;
|
|
67
|
+
/** Tears down the store — removes cart subscription and releases resources. */
|
|
57
68
|
destroy(): void;
|
|
69
|
+
/** Forwards a native `SubmitEvent` to the cart store's form handler (routed by the submitter's `value`, typically `add`), attaching selected variant data as event detail. */
|
|
58
70
|
handleFormSubmit(event: SubmitEvent): Promise<void>;
|
|
59
71
|
}
|
|
72
|
+
/** Returns the selected variant, or `null` when the selection is partial or the variant wasn't part of the query result. */
|
|
60
73
|
declare function getSelectedVariant<TVariant extends ProductVariantInput>(options: VariantOptionState<TVariant, ProductOptionValueInput>[]): TVariant | null;
|
|
74
|
+
/** Guards whether the current selection can be added to cart — a variant must be resolved and available, and the product must not require a selling plan. */
|
|
61
75
|
declare function canAddToCart<TProduct extends ProductInput>(product: TProduct, options: VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[]): boolean;
|
|
62
76
|
/**
|
|
63
77
|
* Finds the first cart line whose merchandise ID matches.
|
|
@@ -66,6 +80,24 @@ declare function canAddToCart<TProduct extends ProductInput>(product: TProduct,
|
|
|
66
80
|
* will narrow to the exact line.
|
|
67
81
|
*/
|
|
68
82
|
declare function findCartLineByMerchandiseId(lines: CartLine[], merchandiseId: string): CartLine | null;
|
|
83
|
+
/**
|
|
84
|
+
* Creates a reactive store that manages variant selection and cart integration for a product form.
|
|
85
|
+
*
|
|
86
|
+
* The store subscribes to the {@link CartStore} to keep error and line-item
|
|
87
|
+
* state in sync. Call `destroy()` when the form unmounts to clean up the
|
|
88
|
+
* subscription.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const store = createProductFormStore(product, cartStore);
|
|
93
|
+
*
|
|
94
|
+
* store.subscribe((state) => {
|
|
95
|
+
* console.log("selected variant:", state.selectedVariant);
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* store.selectOption("Color", "Red");
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
69
101
|
declare function createProductFormStore<TProduct extends ProductInput>(product: TProduct, cartStore: CartStore, options?: CreateProductFormStoreOptions): ProductFormStore<TProduct, ProductVariantFrom<TProduct>>;
|
|
70
102
|
//#endregion
|
|
71
103
|
export { CreateProductFormStoreOptions, ProductFormErrors, ProductFormOptions, ProductFormStore, ProductFormStoreState, ValidProductSelectionResult, VariantSelectionResult, canAddToCart, createProductFormStore, findCartLineByMerchandiseId, getSelectedVariant };
|