@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":"product-form.mjs","names":[],"sources":["../../../../src/core/product/product-form.ts"],"sourcesContent":["import type { CartStore } from \"../cart/cart\";\nimport type {\n CartLine,\n CartNetworkEntry,\n CartState,\n CartUserError,\n CartWarning,\n} from \"../cart/state\";\nimport { createObservable } from \"../observable\";\nimport {\n buildProductOptions,\n type DecodedVariantCache,\n selectedOptionsFromMap,\n selectedOptionsToMap,\n} from \"./options\";\nimport type {\n ProductInput,\n ProductOptionValueFrom,\n ProductOptionValueInput,\n ProductVariantFrom,\n ProductVariantInput,\n SelectedOption,\n VariantOptionState,\n} from \"./state\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface ProductFormErrors {\n userErrors: CartUserError[];\n warnings: CartWarning[];\n networkErrors: CartNetworkEntry[];\n}\n\n/** Live state snapshot emitted by a {@link ProductFormStore}. */\nexport interface ProductFormStoreState<\n TVariant extends ProductVariantInput = ProductVariantInput,\n TOptionValue extends ProductOptionValueInput = ProductOptionValueInput,\n> {\n options: VariantOptionState<TVariant, TOptionValue>[];\n selectedOptions: SelectedOption[];\n selectedVariant: TVariant | null;\n errors: ProductFormErrors;\n matchedLineItem: CartLine | null;\n}\n\nexport type ProductFormOptions<TProduct extends ProductInput = ProductInput> =\n ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[\"options\"];\n\n/**\n * Result returned by {@link ProductFormStore.selectOption}.\n *\n * - `resolved` — a specific variant was matched for the full selection.\n * - `unresolved` — the selection is valid but not yet complete.\n * - `invalid` — the option name or value does not exist on the product.\n */\nexport type VariantSelectionResult<TVariant extends ProductVariantInput = ProductVariantInput> =\n | {\n status: \"resolved\";\n selectedOptions: SelectedOption[];\n selectedVariant: TVariant;\n }\n | {\n status: \"unresolved\";\n selectedOptions: SelectedOption[];\n selectedVariant: null;\n }\n | {\n status: \"invalid\";\n selectedOptions: SelectedOption[];\n selectedVariant: null;\n reason: string;\n };\n\n/** A selection result that is not invalid — either resolved or unresolved. */\nexport type ValidProductSelectionResult<TProduct extends ProductInput = ProductInput> = Exclude<\n VariantSelectionResult<ProductVariantFrom<TProduct>>,\n { status: \"invalid\" }\n>;\n\nexport type CreateProductFormStoreOptions = {\n selectedOptions?: SelectedOption[];\n};\n\n/** Manages variant selection + cart integration for a product form. */\nexport interface ProductFormStore<\n TProduct extends ProductInput = ProductInput,\n TVariant extends ProductVariantInput = ProductVariantFrom<TProduct>,\n> {\n getState(): ProductFormStoreState<TVariant, ProductOptionValueFrom<TProduct>>;\n subscribe(\n listener: (state: ProductFormStoreState<TVariant, ProductOptionValueFrom<TProduct>>) => void,\n ): () => void;\n selectOption(name: string, value: string): VariantSelectionResult<TVariant>;\n hydrate(product: TProduct, opts?: { selectedOptions?: SelectedOption[] }): void;\n reset(): void;\n destroy(): void;\n handleFormSubmit(event: SubmitEvent): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\nexport function getSelectedVariant<TVariant extends ProductVariantInput>(\n options: VariantOptionState<TVariant, ProductOptionValueInput>[],\n): TVariant | null {\n return options[0]?.values.find((v) => v.selected)?.variant ?? null;\n}\n\nexport function canAddToCart<TProduct extends ProductInput>(\n product: TProduct,\n options: VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[],\n): boolean {\n if (product.requiresSellingPlan) return false;\n const variant = getSelectedVariant(options);\n return variant !== null && variant.availableForSale;\n}\n\n/**\n * Finds the first cart line whose merchandise ID matches.\n * When selling plans are used, the same merchandise ID can appear on multiple\n * cart lines — this returns the first match. Future attribute-based matching\n * will narrow to the exact line.\n */\nexport function findCartLineByMerchandiseId(\n lines: CartLine[],\n merchandiseId: string,\n): CartLine | null {\n return lines.find((l) => l.merchandise?.id === merchandiseId) ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Internal context\n// ---------------------------------------------------------------------------\n\ntype ProductFormStoreContext<TProduct extends ProductInput> = {\n observable: ReturnType<\n typeof createObservable<\n ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>\n >\n >;\n cartStore: CartStore;\n currentProduct: TProduct;\n initialProduct: TProduct;\n initialSelectedOptions: SelectedOption[];\n decodedVariantCache: DecodedVariantCache;\n destroyed: boolean;\n unsubCart: () => void;\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createProductFormStore<TProduct extends ProductInput>(\n product: TProduct,\n cartStore: CartStore,\n options: CreateProductFormStoreOptions = {},\n): ProductFormStore<TProduct, ProductVariantFrom<TProduct>> {\n const decodedVariantCache: DecodedVariantCache = new Map();\n const initialSelectedOptions = resolveSelectedOptions(product, options.selectedOptions);\n\n const variantState = buildVariantState(product, initialSelectedOptions, decodedVariantCache);\n\n const context: ProductFormStoreContext<TProduct> = {\n observable: createObservable(deriveFullState(variantState, cartStore.getState())),\n cartStore,\n currentProduct: product,\n initialProduct: product,\n initialSelectedOptions,\n decodedVariantCache,\n destroyed: false,\n unsubCart: () => {},\n };\n\n context.unsubCart = cartStore.subscribe(() => {\n if (context.destroyed) return;\n syncFromCart(context);\n });\n\n return {\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n selectOption: (name, value) => selectOption(context, name, value),\n hydrate: (nextProduct, opts) => hydrate(context, nextProduct, opts),\n reset: () => reset(context),\n destroy: () => destroy(context),\n handleFormSubmit: (event) => handleFormSubmit(context, event),\n };\n}\n\n// ---------------------------------------------------------------------------\n// State builders\n// ---------------------------------------------------------------------------\n\ntype VariantOnlyState<TProduct extends ProductInput> = {\n options: VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[];\n selectedOptions: SelectedOption[];\n};\n\nfunction buildVariantState<TProduct extends ProductInput>(\n product: TProduct,\n selectedOptions: SelectedOption[],\n cache: DecodedVariantCache,\n): VariantOnlyState<TProduct> {\n return {\n options: buildProductOptions(product, selectedOptions, cache),\n selectedOptions,\n };\n}\n\nfunction deriveFullState<TProduct extends ProductInput>(\n variant: VariantOnlyState<TProduct>,\n cartState: CartState,\n): ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>> {\n const selectedVariant = getSelectedVariant(variant.options);\n const matchedLineItem = selectedVariant\n ? findCartLineByMerchandiseId(cartState.data.lines.nodes, selectedVariant.id)\n : null;\n\n const lineErrors = matchedLineItem ? cartState.errors.lines.get(matchedLineItem.id) : undefined;\n\n return {\n options: variant.options,\n selectedOptions: variant.selectedOptions,\n selectedVariant,\n errors: {\n userErrors: [...cartState.errors.cart.userErrors, ...(lineErrors?.userErrors ?? [])],\n warnings: [...cartState.errors.cart.warnings, ...(lineErrors?.warnings ?? [])],\n networkErrors: cartState.errors.network,\n },\n matchedLineItem,\n };\n}\n\nfunction resolveSelectedOptions<TProduct extends ProductInput>(\n product: TProduct,\n requested?: SelectedOption[],\n): SelectedOption[] {\n return product.selectedOrFirstAvailableVariant?.selectedOptions ?? requested ?? [];\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction selectOption<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n name: string,\n value: string,\n): VariantSelectionResult<ProductVariantFrom<TProduct>> {\n const { options, selectedOptions } = context.observable.state;\n const option = options.find((o) => o.name === name);\n const optionValue = option?.values.find((v) => v.name === value);\n\n if (!option || !optionValue) {\n return invalid(selectedOptions, `Unknown option \"${name}\" value \"${value}\".`);\n }\n\n if (!optionValue.exists) {\n return invalid(\n selectedOptions,\n `Option \"${name}\" value \"${value}\" does not resolve to a product variant.`,\n );\n }\n\n const nextSelectedOptionMap = {\n ...selectedOptionsToMap(selectedOptions),\n [name]: value,\n };\n const nextSelectedOptions = selectedOptionsFromMap(context.currentProduct, nextSelectedOptionMap);\n\n return applySelection(\n context,\n optionValue.variant\n ? {\n status: \"resolved\",\n selectedOptions: optionValue.variant.selectedOptions,\n selectedVariant: optionValue.variant as ProductVariantFrom<TProduct>,\n }\n : {\n status: \"unresolved\",\n selectedOptions: nextSelectedOptions,\n selectedVariant: null,\n },\n );\n}\n\nfunction hydrate<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n product: TProduct,\n opts: { selectedOptions?: SelectedOption[] } = {},\n): void {\n context.currentProduct = product;\n context.decodedVariantCache.clear();\n const selectedOptions =\n product.selectedOrFirstAvailableVariant?.selectedOptions ??\n opts.selectedOptions ??\n context.observable.state.selectedOptions;\n const variantState = buildVariantState(product, selectedOptions, context.decodedVariantCache);\n context.observable.setState(deriveFullState(variantState, context.cartStore.getState()));\n}\n\nfunction reset<TProduct extends ProductInput>(context: ProductFormStoreContext<TProduct>): void {\n context.currentProduct = context.initialProduct;\n context.decodedVariantCache.clear();\n const variantState = buildVariantState(\n context.initialProduct,\n context.initialSelectedOptions,\n context.decodedVariantCache,\n );\n context.observable.setState(deriveFullState(variantState, context.cartStore.getState()));\n}\n\nfunction destroy<TProduct extends ProductInput>(context: ProductFormStoreContext<TProduct>): void {\n context.destroyed = true;\n context.unsubCart();\n context.decodedVariantCache.clear();\n}\n\nfunction buildAddToCartDetail<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n): Record<string, unknown> | undefined {\n const { selectedVariant } = context.observable.state;\n if (!selectedVariant) return undefined;\n\n return {\n products: [\n {\n id: selectedVariant.id,\n title: selectedVariant.title,\n product: selectedVariant.product\n ? { title: selectedVariant.product.title, handle: selectedVariant.product.handle }\n : undefined,\n selectedOptions: selectedVariant.selectedOptions,\n image: selectedVariant.image,\n price: selectedVariant.price,\n },\n ],\n };\n}\n\nasync function handleFormSubmit<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n event: SubmitEvent,\n): Promise<void> {\n const eventDetail = buildAddToCartDetail(context);\n await context.cartStore.handleFormSubmit(event, eventDetail);\n}\n\nfunction applySelection<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n result: Exclude<VariantSelectionResult<ProductVariantFrom<TProduct>>, { status: \"invalid\" }>,\n): VariantSelectionResult<ProductVariantFrom<TProduct>> {\n const variantState = buildVariantState(\n context.currentProduct,\n result.selectedOptions,\n context.decodedVariantCache,\n );\n context.observable.setState(deriveFullState(variantState, context.cartStore.getState()));\n return result;\n}\n\nfunction hasCartDerivedFieldsChanged<TVariant extends ProductVariantInput>(\n prev: ProductFormStoreState<TVariant, ProductOptionValueInput>,\n next: ProductFormStoreState<TVariant, ProductOptionValueInput>,\n): boolean {\n return (\n prev.matchedLineItem !== next.matchedLineItem ||\n prev.selectedVariant !== next.selectedVariant ||\n prev.errors.userErrors !== next.errors.userErrors ||\n prev.errors.warnings !== next.errors.warnings ||\n prev.errors.networkErrors !== next.errors.networkErrors\n );\n}\n\nfunction syncFromCart<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n): void {\n const prev = context.observable.state;\n const next = deriveFullState(\n { options: prev.options, selectedOptions: prev.selectedOptions },\n context.cartStore.getState(),\n );\n\n if (hasCartDerivedFieldsChanged(prev, next)) {\n context.observable.setState(next);\n }\n}\n\nfunction invalid<TVariant extends ProductVariantInput>(\n selectedOptions: SelectedOption[],\n reason: string,\n): VariantSelectionResult<TVariant> {\n return {\n status: \"invalid\",\n selectedOptions,\n selectedVariant: null,\n reason,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"product-form.mjs","names":[],"sources":["../../../../src/core/product/product-form.ts"],"sourcesContent":["import type { CartStore } from \"../cart/cart\";\nimport type {\n CartLine,\n CartNetworkEntry,\n CartState,\n CartUserError,\n CartWarning,\n} from \"../cart/state\";\nimport { createObservable } from \"../observable\";\nimport {\n buildProductOptions,\n type DecodedVariantCache,\n selectedOptionsFromMap,\n selectedOptionsToMap,\n} from \"./options\";\nimport type {\n ProductInput,\n ProductOptionValueFrom,\n ProductOptionValueInput,\n ProductVariantFrom,\n ProductVariantInput,\n SelectedOption,\n VariantOptionState,\n} from \"./state\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** Cart and line-item errors surfaced by a {@link ProductFormStore}. */\nexport interface ProductFormErrors {\n userErrors: CartUserError[];\n warnings: CartWarning[];\n networkErrors: CartNetworkEntry[];\n}\n\n/** Live state snapshot emitted by a {@link ProductFormStore}. */\nexport interface ProductFormStoreState<\n TVariant extends ProductVariantInput = ProductVariantInput,\n TOptionValue extends ProductOptionValueInput = ProductOptionValueInput,\n> {\n options: VariantOptionState<TVariant, TOptionValue>[];\n selectedOptions: SelectedOption[];\n selectedVariant: TVariant | null;\n errors: ProductFormErrors;\n matchedLineItem: CartLine | null;\n}\n\n/** The reactive options array from a {@link ProductFormStoreState}, typed to a specific product. */\nexport type ProductFormOptions<TProduct extends ProductInput = ProductInput> =\n ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[\"options\"];\n\n/**\n * Result returned by {@link ProductFormStore.selectOption}.\n *\n * - `resolved` — a specific variant was matched for the full selection.\n * - `unresolved` — the selection is valid but not yet complete.\n * - `invalid` — the option name or value does not exist on the product.\n */\nexport type VariantSelectionResult<TVariant extends ProductVariantInput = ProductVariantInput> =\n | {\n status: \"resolved\";\n selectedOptions: SelectedOption[];\n selectedVariant: TVariant;\n }\n | {\n status: \"unresolved\";\n selectedOptions: SelectedOption[];\n selectedVariant: null;\n }\n | {\n status: \"invalid\";\n selectedOptions: SelectedOption[];\n selectedVariant: null;\n reason: string;\n };\n\n/** A selection result that is not invalid — either resolved or unresolved. */\nexport type ValidProductSelectionResult<TProduct extends ProductInput = ProductInput> = Exclude<\n VariantSelectionResult<ProductVariantFrom<TProduct>>,\n { status: \"invalid\" }\n>;\n\n/** Options for {@link createProductFormStore}. */\nexport type CreateProductFormStoreOptions = {\n /** Fallback selection used when the product has no `selectedOrFirstAvailableVariant`. */\n selectedOptions?: SelectedOption[];\n};\n\n/** Manages variant selection + cart integration for a product form. */\nexport interface ProductFormStore<\n TProduct extends ProductInput = ProductInput,\n TVariant extends ProductVariantInput = ProductVariantFrom<TProduct>,\n> {\n /** Returns the current state snapshot. */\n getState(): ProductFormStoreState<TVariant, ProductOptionValueFrom<TProduct>>;\n /** Registers a listener invoked on every state change. Returns an unsubscribe function. */\n subscribe(\n listener: (state: ProductFormStoreState<TVariant, ProductOptionValueFrom<TProduct>>) => void,\n ): () => void;\n /** Selects an option value and resolves the new variant. */\n selectOption(name: string, value: string): VariantSelectionResult<TVariant>;\n /** Replaces the product data and re-derives state — use after a server-side product reload. */\n hydrate(product: TProduct, opts?: { selectedOptions?: SelectedOption[] }): void;\n /** Restores the store to its initial product and selection state. */\n reset(): void;\n /** Re-subscribes to the cart store and resyncs cart-derived state. Only needed to reuse the store after `destroy()` (e.g. React StrictMode effects). */\n connect(): void;\n /** Tears down the store — removes cart subscription and releases resources. */\n destroy(): void;\n /** 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. */\n handleFormSubmit(event: SubmitEvent): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Utilities\n// ---------------------------------------------------------------------------\n\n/** Returns the selected variant, or `null` when the selection is partial or the variant wasn't part of the query result. */\nexport function getSelectedVariant<TVariant extends ProductVariantInput>(\n options: VariantOptionState<TVariant, ProductOptionValueInput>[],\n): TVariant | null {\n return options[0]?.values.find((v) => v.selected)?.variant ?? null;\n}\n\n/** 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. */\nexport function canAddToCart<TProduct extends ProductInput>(\n product: TProduct,\n options: VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[],\n): boolean {\n if (product.requiresSellingPlan) return false;\n const variant = getSelectedVariant(options);\n return variant !== null && variant.availableForSale;\n}\n\n/**\n * Finds the first cart line whose merchandise ID matches.\n * When selling plans are used, the same merchandise ID can appear on multiple\n * cart lines — this returns the first match. Future attribute-based matching\n * will narrow to the exact line.\n */\nexport function findCartLineByMerchandiseId(\n lines: CartLine[],\n merchandiseId: string,\n): CartLine | null {\n return lines.find((l) => l.merchandise?.id === merchandiseId) ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Internal context\n// ---------------------------------------------------------------------------\n\ntype ProductFormStoreContext<TProduct extends ProductInput> = {\n observable: ReturnType<\n typeof createObservable<\n ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>\n >\n >;\n cartStore: CartStore;\n currentProduct: TProduct;\n initialProduct: TProduct;\n initialSelectedOptions: SelectedOption[];\n decodedVariantCache: DecodedVariantCache;\n destroyed: boolean;\n unsubCart: () => void;\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a reactive store that manages variant selection and cart integration for a product form.\n *\n * The store subscribes to the {@link CartStore} to keep error and line-item\n * state in sync. Call `destroy()` when the form unmounts to clean up the\n * subscription.\n *\n * @example\n * ```ts\n * const store = createProductFormStore(product, cartStore);\n *\n * store.subscribe((state) => {\n * console.log(\"selected variant:\", state.selectedVariant);\n * });\n *\n * store.selectOption(\"Color\", \"Red\");\n * ```\n */\nexport function createProductFormStore<TProduct extends ProductInput>(\n product: TProduct,\n cartStore: CartStore,\n options: CreateProductFormStoreOptions = {},\n): ProductFormStore<TProduct, ProductVariantFrom<TProduct>> {\n const decodedVariantCache: DecodedVariantCache = new Map();\n const initialSelectedOptions = resolveSelectedOptions(product, options.selectedOptions);\n\n const variantState = buildVariantState(product, initialSelectedOptions, decodedVariantCache);\n\n const context: ProductFormStoreContext<TProduct> = {\n observable: createObservable(deriveFullState(variantState, cartStore.getState())),\n cartStore,\n currentProduct: product,\n initialProduct: product,\n initialSelectedOptions,\n decodedVariantCache,\n destroyed: false,\n unsubCart: () => {},\n };\n\n context.unsubCart = cartStore.subscribe(() => {\n if (context.destroyed) return;\n syncFromCart(context);\n });\n\n return {\n getState: () => context.observable.state,\n subscribe: (listener) => context.observable.subscribe(listener),\n selectOption: (name, value) => selectOption(context, name, value),\n hydrate: (nextProduct, opts) => hydrate(context, nextProduct, opts),\n reset: () => reset(context),\n connect: () => connect(context),\n destroy: () => destroy(context),\n handleFormSubmit: (event) => handleFormSubmit(context, event),\n };\n}\n\n// ---------------------------------------------------------------------------\n// State builders\n// ---------------------------------------------------------------------------\n\ntype VariantOnlyState<TProduct extends ProductInput> = {\n options: VariantOptionState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>>[];\n selectedOptions: SelectedOption[];\n};\n\nfunction buildVariantState<TProduct extends ProductInput>(\n product: TProduct,\n selectedOptions: SelectedOption[],\n cache: DecodedVariantCache,\n): VariantOnlyState<TProduct> {\n return {\n options: buildProductOptions(product, selectedOptions, cache),\n selectedOptions,\n };\n}\n\nfunction deriveFullState<TProduct extends ProductInput>(\n variant: VariantOnlyState<TProduct>,\n cartState: CartState,\n): ProductFormStoreState<ProductVariantFrom<TProduct>, ProductOptionValueFrom<TProduct>> {\n const selectedVariant = getSelectedVariant(variant.options);\n const matchedLineItem = selectedVariant\n ? findCartLineByMerchandiseId(cartState.data.lines.nodes, selectedVariant.id)\n : null;\n\n const lineErrors = matchedLineItem ? cartState.errors.lines.get(matchedLineItem.id) : undefined;\n\n return {\n options: variant.options,\n selectedOptions: variant.selectedOptions,\n selectedVariant,\n errors: {\n userErrors: [...cartState.errors.cart.userErrors, ...(lineErrors?.userErrors ?? [])],\n warnings: [...cartState.errors.cart.warnings, ...(lineErrors?.warnings ?? [])],\n networkErrors: cartState.errors.network,\n },\n matchedLineItem,\n };\n}\n\nfunction resolveSelectedOptions<TProduct extends ProductInput>(\n product: TProduct,\n requested?: SelectedOption[],\n): SelectedOption[] {\n return product.selectedOrFirstAvailableVariant?.selectedOptions ?? requested ?? [];\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction selectOption<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n name: string,\n value: string,\n): VariantSelectionResult<ProductVariantFrom<TProduct>> {\n const { options, selectedOptions } = context.observable.state;\n const option = options.find((o) => o.name === name);\n const optionValue = option?.values.find((v) => v.name === value);\n\n if (!option || !optionValue) {\n return invalid(selectedOptions, `Unknown option \"${name}\" value \"${value}\".`);\n }\n\n if (!optionValue.exists) {\n return invalid(\n selectedOptions,\n `Option \"${name}\" value \"${value}\" does not resolve to a product variant.`,\n );\n }\n\n const nextSelectedOptionMap = {\n ...selectedOptionsToMap(selectedOptions),\n [name]: value,\n };\n const nextSelectedOptions = selectedOptionsFromMap(context.currentProduct, nextSelectedOptionMap);\n\n return applySelection(\n context,\n optionValue.variant\n ? {\n status: \"resolved\",\n selectedOptions: optionValue.variant.selectedOptions,\n selectedVariant: optionValue.variant as ProductVariantFrom<TProduct>,\n }\n : {\n status: \"unresolved\",\n selectedOptions: nextSelectedOptions,\n selectedVariant: null,\n },\n );\n}\n\nfunction hydrate<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n product: TProduct,\n opts: { selectedOptions?: SelectedOption[] } = {},\n): void {\n context.currentProduct = product;\n context.decodedVariantCache.clear();\n const selectedOptions =\n product.selectedOrFirstAvailableVariant?.selectedOptions ??\n opts.selectedOptions ??\n context.observable.state.selectedOptions;\n const variantState = buildVariantState(product, selectedOptions, context.decodedVariantCache);\n context.observable.setState(deriveFullState(variantState, context.cartStore.getState()));\n}\n\nfunction reset<TProduct extends ProductInput>(context: ProductFormStoreContext<TProduct>): void {\n context.currentProduct = context.initialProduct;\n context.decodedVariantCache.clear();\n const variantState = buildVariantState(\n context.initialProduct,\n context.initialSelectedOptions,\n context.decodedVariantCache,\n );\n context.observable.setState(deriveFullState(variantState, context.cartStore.getState()));\n}\n\nfunction connect<TProduct extends ProductInput>(context: ProductFormStoreContext<TProduct>): void {\n context.unsubCart();\n context.destroyed = false;\n context.unsubCart = context.cartStore.subscribe(() => {\n if (context.destroyed) return;\n syncFromCart(context);\n });\n syncFromCart(context);\n}\n\nfunction destroy<TProduct extends ProductInput>(context: ProductFormStoreContext<TProduct>): void {\n context.destroyed = true;\n context.unsubCart();\n context.decodedVariantCache.clear();\n}\n\nfunction buildAddToCartDetail<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n): Record<string, unknown> | undefined {\n const { selectedVariant } = context.observable.state;\n if (!selectedVariant) return undefined;\n\n return {\n products: [\n {\n id: selectedVariant.id,\n title: selectedVariant.title,\n product: selectedVariant.product\n ? { title: selectedVariant.product.title, handle: selectedVariant.product.handle }\n : undefined,\n selectedOptions: selectedVariant.selectedOptions,\n image: selectedVariant.image,\n price: selectedVariant.price,\n },\n ],\n };\n}\n\nasync function handleFormSubmit<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n event: SubmitEvent,\n): Promise<void> {\n const eventDetail = buildAddToCartDetail(context);\n await context.cartStore.handleFormSubmit(event, eventDetail);\n}\n\nfunction applySelection<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n result: Exclude<VariantSelectionResult<ProductVariantFrom<TProduct>>, { status: \"invalid\" }>,\n): VariantSelectionResult<ProductVariantFrom<TProduct>> {\n const variantState = buildVariantState(\n context.currentProduct,\n result.selectedOptions,\n context.decodedVariantCache,\n );\n context.observable.setState(deriveFullState(variantState, context.cartStore.getState()));\n return result;\n}\n\nfunction hasCartDerivedFieldsChanged<TVariant extends ProductVariantInput>(\n prev: ProductFormStoreState<TVariant, ProductOptionValueInput>,\n next: ProductFormStoreState<TVariant, ProductOptionValueInput>,\n): boolean {\n return (\n prev.matchedLineItem !== next.matchedLineItem ||\n prev.selectedVariant !== next.selectedVariant ||\n prev.errors.userErrors !== next.errors.userErrors ||\n prev.errors.warnings !== next.errors.warnings ||\n prev.errors.networkErrors !== next.errors.networkErrors\n );\n}\n\nfunction syncFromCart<TProduct extends ProductInput>(\n context: ProductFormStoreContext<TProduct>,\n): void {\n const prev = context.observable.state;\n const next = deriveFullState(\n { options: prev.options, selectedOptions: prev.selectedOptions },\n context.cartStore.getState(),\n );\n\n if (hasCartDerivedFieldsChanged(prev, next)) {\n context.observable.setState(next);\n }\n}\n\nfunction invalid<TVariant extends ProductVariantInput>(\n selectedOptions: SelectedOption[],\n reason: string,\n): VariantSelectionResult<TVariant> {\n return {\n status: \"invalid\",\n selectedOptions,\n selectedVariant: null,\n reason,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
//#region src/core/product/state.d.ts
|
|
2
|
+
/** Monetary amount in a specific currency, mirroring the Storefront API `MoneyV2` type. */
|
|
2
3
|
interface Money {
|
|
3
4
|
amount: string;
|
|
4
5
|
currencyCode: string;
|
|
5
6
|
}
|
|
7
|
+
/** Minimum and maximum variant prices for a product, mirroring the Storefront API `ProductPriceRange` type. */
|
|
6
8
|
interface ProductPriceRange {
|
|
7
9
|
minVariantPrice: Money;
|
|
10
|
+
/** Optional so queries that only select `minVariantPrice` still type-check. */
|
|
8
11
|
maxVariantPrice?: Money;
|
|
9
12
|
}
|
|
13
|
+
/** A single selected product option, e.g. `{ name: "Color", value: "Red" }`. */
|
|
10
14
|
interface SelectedOption {
|
|
11
15
|
name: string;
|
|
12
16
|
value: string;
|
|
13
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimum variant shape that the product form system requires.
|
|
20
|
+
*
|
|
21
|
+
* Consumers typically pass a wider type from their Storefront API query —
|
|
22
|
+
* `ProductVariantInput` declares only the fields the form logic reads.
|
|
23
|
+
*/
|
|
14
24
|
interface ProductVariantInput {
|
|
15
25
|
id: string;
|
|
16
26
|
title: string;
|
|
17
27
|
availableForSale: boolean;
|
|
18
28
|
selectedOptions: SelectedOption[];
|
|
19
29
|
price: Money;
|
|
30
|
+
/** Merchant-set compare-at price, typically shown struck-through when higher than `price`. `null` when unset; `undefined` when not queried. */
|
|
20
31
|
compareAtPrice?: Money | null;
|
|
21
32
|
image?: unknown;
|
|
22
33
|
product?: {
|
|
@@ -25,40 +36,70 @@ interface ProductVariantInput {
|
|
|
25
36
|
} | null;
|
|
26
37
|
sku?: string | null;
|
|
27
38
|
}
|
|
39
|
+
/** A product option (e.g. "Size" or "Color") and its available values. */
|
|
28
40
|
interface ProductOptionInput<TVariant extends ProductVariantInput = ProductVariantInput> {
|
|
29
41
|
name: string;
|
|
30
42
|
optionValues: Array<ProductOptionValueInput<TVariant>>;
|
|
31
43
|
}
|
|
44
|
+
/** A single option value (e.g. "Small", "Red") within a {@link ProductOptionInput}. */
|
|
32
45
|
interface ProductOptionValueInput<TVariant extends ProductVariantInput = ProductVariantInput> {
|
|
33
46
|
name: string;
|
|
47
|
+
/** The variant combining this value with the lowest-position values of every other option (SFAPI). Strongly recommended: it seeds the variant cache used for `exists`/`selectedOptions` and combined-listing detection. */
|
|
34
48
|
firstSelectableVariant?: TVariant | null;
|
|
35
49
|
swatch?: unknown;
|
|
36
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Minimum product shape that `createProductFormStore` requires.
|
|
53
|
+
*
|
|
54
|
+
* Fields mirror the Storefront API `Product` object. Consumers typically
|
|
55
|
+
* pass a wider query result — this interface declares only what the
|
|
56
|
+
* product form logic reads.
|
|
57
|
+
*/
|
|
37
58
|
interface ProductInput<TVariant extends ProductVariantInput = ProductVariantInput> {
|
|
38
59
|
id: string;
|
|
39
60
|
title: string;
|
|
40
61
|
handle: string;
|
|
41
62
|
vendor?: string | null;
|
|
42
63
|
priceRange?: ProductPriceRange;
|
|
64
|
+
/** When `true`, the product cannot be added to cart without a selling plan. */
|
|
43
65
|
requiresSellingPlan?: boolean | null;
|
|
66
|
+
/** Encoded representation of which option-value combinations map to real variants. Consumers should treat this as opaque. */
|
|
44
67
|
encodedVariantExistence?: string | null;
|
|
68
|
+
/** Encoded representation of which existing variants are currently available for sale. Consumers should treat this as opaque. */
|
|
45
69
|
encodedVariantAvailability?: string | null;
|
|
46
70
|
options: ProductOptionInput<TVariant>[];
|
|
71
|
+
/** SFAPI `selectedOrFirstAvailableVariant`: the variant matching the query's `selectedOptions` argument, else the first available variant, else the first variant (which may be unavailable). */
|
|
47
72
|
selectedOrFirstAvailableVariant: TVariant | null;
|
|
73
|
+
/** Variants adjacent to the selected variant — used to resolve option values without a full variant list. */
|
|
48
74
|
adjacentVariants: TVariant[];
|
|
49
75
|
}
|
|
76
|
+
/** Extracts the concrete variant type from a {@link ProductInput} subtype. */
|
|
50
77
|
type ProductVariantFrom<TProduct extends ProductInput> = TProduct extends ProductInput<infer TVariant> ? TVariant : ProductVariantInput;
|
|
78
|
+
/** Extracts the concrete option-value type from a {@link ProductInput} subtype. */
|
|
51
79
|
type ProductOptionValueFrom<TProduct extends ProductInput> = TProduct["options"][number]["optionValues"][number];
|
|
80
|
+
/**
|
|
81
|
+
* Computed state for a single option value (e.g. "Red" under "Color").
|
|
82
|
+
*
|
|
83
|
+
* Derived each time the selection changes — fields reflect the current
|
|
84
|
+
* selection context, not static product data.
|
|
85
|
+
*/
|
|
52
86
|
interface VariantOptionValueState<TVariant extends ProductVariantInput = ProductVariantInput, TOptionValue extends ProductOptionValueInput = ProductOptionValueInput> {
|
|
53
87
|
name: string;
|
|
54
88
|
swatch?: TOptionValue["swatch"];
|
|
89
|
+
/** Whether this value is the current selection for its option. */
|
|
55
90
|
selected: boolean;
|
|
91
|
+
/** Whether a variant exists for the target selection. Falls back to `true` when `encodedVariantExistence` wasn't queried. */
|
|
56
92
|
exists: boolean;
|
|
93
|
+
/** Whether the target selection is available for sale. Uses `encodedVariantAvailability` when queried, otherwise the loaded variant's `availableForSale` (`false` if not loaded). */
|
|
57
94
|
available: boolean;
|
|
95
|
+
/** The loaded variant for the target selection, or `null` if the selection is partial or that variant wasn't part of the query result. */
|
|
58
96
|
variant: TVariant | null;
|
|
97
|
+
/** The selection this value targets. Use for link building; `selectOption()` is the source of truth for the resulting selection. */
|
|
59
98
|
selectedOptions: SelectedOption[];
|
|
99
|
+
/** Product handle, useful for building navigation links to combined-listing child products. */
|
|
60
100
|
handle: string;
|
|
61
101
|
}
|
|
102
|
+
/** Computed state for a product option (e.g. "Color"), grouping its {@link VariantOptionValueState} entries. */
|
|
62
103
|
interface VariantOptionState<TVariant extends ProductVariantInput = ProductVariantInput, TOptionValue extends ProductOptionValueInput = ProductOptionValueInput> {
|
|
63
104
|
name: string;
|
|
64
105
|
values: VariantOptionValueState<TVariant, TOptionValue>[];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { SelectedOption } from "./state.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/core/product/url.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Controls how variant selection is encoded in URL search params.
|
|
6
|
+
*
|
|
7
|
+
* - `"options"` — one param per option, e.g. `?Color=Red&Size=M`.
|
|
8
|
+
* - `"variant"` — a single `?variant=<numeric id>` param (Liquid parity).
|
|
9
|
+
*/
|
|
4
10
|
type ProductSelectionLinkStyle = "options" | "variant";
|
|
5
11
|
/**
|
|
6
12
|
* Builds the search params for a product-page link representing a variant selection.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.mjs","names":[],"sources":["../../../../src/core/product/url.ts"],"sourcesContent":["import type { SelectedOption } from \"./state\";\n\nconst VARIANT_GID_PREFIX = \"gid://shopify/ProductVariant/\";\n\n/**\n * Reserved product-page search param carrying a numeric variant id (`?variant=41820371452004`),\n * matching Liquid storefront URLs. Never treated as a product option name.\n */\nexport const VARIANT_SEARCH_PARAM = \"variant\";\n\n/**\n * Converts a `?variant=` search param value into a variant id (gid).\n *\n * Anything that is not a plain positive integer of a plausible length is treated as absent\n * (`null`) so malformed links degrade to the default variant instead of producing invalid\n * API lookups or log spam.\n */\nexport function parseVariantSearchParam(value: string | null): string | null {\n if (!value || !/^\\d{1,30}$/.test(value)) return null;\n return `${VARIANT_GID_PREFIX}${value}`;\n}\n\n/**\n * Extracts the numeric `?variant=` search param value from a variant id (gid).\n * Returns `null` for ids that are not canonical `gid://shopify/ProductVariant/<numeric>` ids.\n */\nexport function getVariantSearchParamValue(variantId: string): string | null {\n if (!variantId.startsWith(VARIANT_GID_PREFIX)) return null;\n\n const numericId = variantId.slice(VARIANT_GID_PREFIX.length);\n return /^\\d+$/.test(numericId) ? numericId : null;\n}\n\nexport type ProductSelectionLinkStyle = \"options\" | \"variant\";\n\n/**\n * Builds the search params for a product-page link representing a variant selection.\n *\n * Existing `base` params are preserved (`?ref=campaign` survives navigation), except the\n * reserved `variant` param and every param named in `optionNames` or `selectedOptions`,\n * which are always removed before the new selection is written. This keeps stale selection\n * state from leaking between navigations, including across combined-listing products.\n *\n * Two link styles:\n * - `\"options\"` (default): one param per selected option, e.g. `?Color=Red&Size=M`.\n * - `\"variant\"`: a single `?variant=<numeric id>` param (Liquid parity), for shareable\n * links. Falls back to option params when `variant` is absent — a partial selection has\n * no matching variant, so a variant link is not constructible.\n *\n * @example\n * ```ts\n * const searchParams = buildProductSelectionSearchParams({\n * selectedOptions: result.selectedOptions,\n * variant: result.selectedVariant,\n * optionNames: product.options.map((option) => option.name),\n * base: new URLSearchParams(location.search),\n * });\n * const url = `/products/${handle}${searchParams.size ? `?${searchParams}` : \"\"}`;\n * ```\n */\nexport function buildProductSelectionSearchParams({\n style = \"options\",\n selectedOptions,\n variant,\n optionNames,\n base,\n}: {\n style?: ProductSelectionLinkStyle;\n selectedOptions: readonly SelectedOption[];\n variant?: { id: string } | null;\n optionNames: readonly string[];\n base?: URLSearchParams;\n}): URLSearchParams {\n const searchParams = new URLSearchParams(base);\n\n searchParams.delete(VARIANT_SEARCH_PARAM);\n for (const name of optionNames) searchParams.delete(name);\n for (const option of selectedOptions) searchParams.delete(option.name);\n\n const variantSearchParamValue =\n style === \"variant\" && variant ? getVariantSearchParamValue(variant.id) : null;\n\n if (variantSearchParamValue) {\n searchParams.set(VARIANT_SEARCH_PARAM, variantSearchParamValue);\n return searchParams;\n }\n\n for (const option of selectedOptions) {\n searchParams.set(option.name, option.value);\n }\n\n return searchParams;\n}\n"],"mappings":";AAEA,MAAM,qBAAqB;;;;;AAM3B,MAAa,uBAAuB;;;;;;;;AASpC,SAAgB,wBAAwB,OAAqC;AAC3E,KAAI,CAAC,SAAS,CAAC,aAAa,KAAK,MAAM,CAAE,QAAO;AAChD,QAAO,GAAG,qBAAqB;;;;;;AAOjC,SAAgB,2BAA2B,WAAkC;AAC3E,KAAI,CAAC,UAAU,WAAW,mBAAmB,CAAE,QAAO;CAEtD,MAAM,YAAY,UAAU,MAAM,GAA0B;AAC5D,QAAO,QAAQ,KAAK,UAAU,GAAG,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"url.mjs","names":[],"sources":["../../../../src/core/product/url.ts"],"sourcesContent":["import type { SelectedOption } from \"./state\";\n\nconst VARIANT_GID_PREFIX = \"gid://shopify/ProductVariant/\";\n\n/**\n * Reserved product-page search param carrying a numeric variant id (`?variant=41820371452004`),\n * matching Liquid storefront URLs. Never treated as a product option name.\n */\nexport const VARIANT_SEARCH_PARAM = \"variant\";\n\n/**\n * Converts a `?variant=` search param value into a variant id (gid).\n *\n * Anything that is not a plain positive integer of a plausible length is treated as absent\n * (`null`) so malformed links degrade to the default variant instead of producing invalid\n * API lookups or log spam.\n */\nexport function parseVariantSearchParam(value: string | null): string | null {\n if (!value || !/^\\d{1,30}$/.test(value)) return null;\n return `${VARIANT_GID_PREFIX}${value}`;\n}\n\n/**\n * Extracts the numeric `?variant=` search param value from a variant id (gid).\n * Returns `null` for ids that are not canonical `gid://shopify/ProductVariant/<numeric>` ids.\n */\nexport function getVariantSearchParamValue(variantId: string): string | null {\n if (!variantId.startsWith(VARIANT_GID_PREFIX)) return null;\n\n const numericId = variantId.slice(VARIANT_GID_PREFIX.length);\n return /^\\d+$/.test(numericId) ? numericId : null;\n}\n\n/**\n * Controls how variant selection is encoded in URL search params.\n *\n * - `\"options\"` — one param per option, e.g. `?Color=Red&Size=M`.\n * - `\"variant\"` — a single `?variant=<numeric id>` param (Liquid parity).\n */\nexport type ProductSelectionLinkStyle = \"options\" | \"variant\";\n\n/**\n * Builds the search params for a product-page link representing a variant selection.\n *\n * Existing `base` params are preserved (`?ref=campaign` survives navigation), except the\n * reserved `variant` param and every param named in `optionNames` or `selectedOptions`,\n * which are always removed before the new selection is written. This keeps stale selection\n * state from leaking between navigations, including across combined-listing products.\n *\n * Two link styles:\n * - `\"options\"` (default): one param per selected option, e.g. `?Color=Red&Size=M`.\n * - `\"variant\"`: a single `?variant=<numeric id>` param (Liquid parity), for shareable\n * links. Falls back to option params when `variant` is absent — a partial selection has\n * no matching variant, so a variant link is not constructible.\n *\n * @example\n * ```ts\n * const searchParams = buildProductSelectionSearchParams({\n * selectedOptions: result.selectedOptions,\n * variant: result.selectedVariant,\n * optionNames: product.options.map((option) => option.name),\n * base: new URLSearchParams(location.search),\n * });\n * const url = `/products/${handle}${searchParams.size ? `?${searchParams}` : \"\"}`;\n * ```\n */\nexport function buildProductSelectionSearchParams({\n style = \"options\",\n selectedOptions,\n variant,\n optionNames,\n base,\n}: {\n style?: ProductSelectionLinkStyle;\n selectedOptions: readonly SelectedOption[];\n variant?: { id: string } | null;\n optionNames: readonly string[];\n base?: URLSearchParams;\n}): URLSearchParams {\n const searchParams = new URLSearchParams(base);\n\n searchParams.delete(VARIANT_SEARCH_PARAM);\n for (const name of optionNames) searchParams.delete(name);\n for (const option of selectedOptions) searchParams.delete(option.name);\n\n const variantSearchParamValue =\n style === \"variant\" && variant ? getVariantSearchParamValue(variant.id) : null;\n\n if (variantSearchParamValue) {\n searchParams.set(VARIANT_SEARCH_PARAM, variantSearchParamValue);\n return searchParams;\n }\n\n for (const option of selectedOptions) {\n searchParams.set(option.name, option.value);\n }\n\n return searchParams;\n}\n"],"mappings":";AAEA,MAAM,qBAAqB;;;;;AAM3B,MAAa,uBAAuB;;;;;;;;AASpC,SAAgB,wBAAwB,OAAqC;AAC3E,KAAI,CAAC,SAAS,CAAC,aAAa,KAAK,MAAM,CAAE,QAAO;AAChD,QAAO,GAAG,qBAAqB;;;;;;AAOjC,SAAgB,2BAA2B,WAAkC;AAC3E,KAAI,CAAC,UAAU,WAAW,mBAAmB,CAAE,QAAO;CAEtD,MAAM,YAAY,UAAU,MAAM,GAA0B;AAC5D,QAAO,QAAQ,KAAK,UAAU,GAAG,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoC/C,SAAgB,kCAAkC,EAChD,QAAQ,WACR,iBACA,SACA,aACA,QAOkB;CAClB,MAAM,eAAe,IAAI,gBAAgB,KAAK;AAE9C,cAAa,OAAO,qBAAqB;AACzC,MAAK,MAAM,QAAQ,YAAa,cAAa,OAAO,KAAK;AACzD,MAAK,MAAM,UAAU,gBAAiB,cAAa,OAAO,OAAO,KAAK;CAEtE,MAAM,0BACJ,UAAU,aAAa,UAAU,2BAA2B,QAAQ,GAAG,GAAG;AAE5E,KAAI,yBAAyB;AAC3B,eAAa,IAAI,sBAAsB,wBAAwB;AAC/D,SAAO;;AAGT,MAAK,MAAM,UAAU,gBACnB,cAAa,IAAI,OAAO,MAAM,OAAO,MAAM;AAG7C,QAAO"}
|
|
@@ -30,9 +30,6 @@ type ShopifyRequestContextBase = {
|
|
|
30
30
|
*/
|
|
31
31
|
readonly __hydrogenShopifyRequestContextBrand: never; /** @internal */
|
|
32
32
|
cookie?: string; /** @internal */
|
|
33
|
-
uniqueToken?: string; /** @internal */
|
|
34
|
-
visitToken?: string; /** @internal */
|
|
35
|
-
legacyTokens?: boolean; /** @internal */
|
|
36
33
|
readonly buyerIp?: string; /** @internal */
|
|
37
34
|
requestGroupId: string; /** @internal */
|
|
38
35
|
signal?: AbortSignal; /** @internal */
|
|
@@ -44,12 +41,12 @@ type ShopifyRequestContextBase = {
|
|
|
44
41
|
*/
|
|
45
42
|
applyStorefrontRequestHeaders(headers: Headers): void;
|
|
46
43
|
/**
|
|
47
|
-
* Capture the first fresh storefront response
|
|
44
|
+
* Capture cookies from the first fresh storefront response that sets them for replay.
|
|
48
45
|
* @internal
|
|
49
46
|
*/
|
|
50
47
|
captureSubrequestHeaders(headers: Headers): void;
|
|
51
48
|
/**
|
|
52
|
-
* Consume storefront proxy
|
|
49
|
+
* Consume storefront proxy cookies for gated replay onto the final app response.
|
|
53
50
|
* @internal
|
|
54
51
|
*/
|
|
55
52
|
consumeStorefrontResponseHeaders(headers: Headers): void;
|
|
@@ -58,6 +55,12 @@ type ShopifyRequestContextBase = {
|
|
|
58
55
|
* @internal
|
|
59
56
|
*/
|
|
60
57
|
markResponseAsPersonalized(reason: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Mark this request as a sanctioned session-establishing endpoint (like the
|
|
60
|
+
* consent flow), permitting Shopify state to return even on a cold session.
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
markResponseAsSessionEstablishing(reason: string): void;
|
|
61
64
|
i18n: NormalizedI18nConfig; /** Return incoming request headers plus request lifecycle headers for proxy/origin handoff. */
|
|
62
65
|
getForwardedRequestHeaders(): Headers; /** Apply important response headers for the correct functioning of Hydrogen storefronts. */
|
|
63
66
|
applyResponseHeaders(headers: Headers): void;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { normalizePathPrefix } from "./standard-routes/path.mjs";
|
|
2
2
|
import { STOREFRONT_API_VERSION } from "./constants.mjs";
|
|
3
|
-
import { CONSENT_MANAGEMENT_HEADER, HYDROGEN_VERSION_HEADER, REQUEST_GROUP_ID_HEADER, SDK_VARIANT_HEADER, SDK_VARIANT_SOURCE_HEADER, SDK_VERSION_HEADER,
|
|
3
|
+
import { CONSENT_MANAGEMENT_HEADER, HYDROGEN_VERSION_HEADER, REQUEST_GROUP_ID_HEADER, SDK_VARIANT_HEADER, SDK_VARIANT_SOURCE_HEADER, SDK_VERSION_HEADER, SEC_GPC_HEADER, SHOPIFY_STOREFRONT_ORIGIN_HEADER, STOREFRONT_URL_HEADER, applyPrivateResponseCacheHeaders } from "./headers.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/core/request-context.ts
|
|
6
6
|
const SHOPIFY_ESSENTIAL_COOKIE = "_shopify_essential";
|
|
7
|
-
const SHOPIFY_TRACKING_COOKIES = ["_shopify_analytics", "_shopify_marketing"];
|
|
8
|
-
const SHOPIFY_COOKIES = new Set([SHOPIFY_ESSENTIAL_COOKIE, ...SHOPIFY_TRACKING_COOKIES]);
|
|
9
7
|
function createShopifyRequestContext(input) {
|
|
10
8
|
const { request } = input;
|
|
11
9
|
if (!input.i18n?.country || !input.i18n?.language) throw new Error("i18n with country and language is required for Shopify request contexts.");
|
|
@@ -13,14 +11,13 @@ function createShopifyRequestContext(input) {
|
|
|
13
11
|
const requestMethod = request.method?.toUpperCase();
|
|
14
12
|
const i18n = normalizeI18n(input.i18n);
|
|
15
13
|
const cookieHeader = request.headers.get("cookie") || void 0;
|
|
16
|
-
const
|
|
17
|
-
const hasEssentialCookie = inboundCookies.has(SHOPIFY_ESSENTIAL_COOKIE);
|
|
18
|
-
const hasTrackingCookie = SHOPIFY_TRACKING_COOKIES.some((name) => inboundCookies.has(name));
|
|
14
|
+
const hasEssentialCookie = parseCookieHeader(cookieHeader).has(SHOPIFY_ESSENTIAL_COOKIE);
|
|
19
15
|
const isConsentManagementRequest = request.headers.get(CONSENT_MANAGEMENT_HEADER) === "1";
|
|
20
16
|
const url = request.url ?? request.headers.get("x-storefront-url") ?? void 0;
|
|
21
17
|
const storefrontOrigin = getUrlOrigin(url);
|
|
22
18
|
const context = {
|
|
23
19
|
...cookieHeader && { cookie: cookieHeader },
|
|
20
|
+
globalPrivacyControl: request.headers.get("Sec-GPC") ?? void 0,
|
|
24
21
|
i18n,
|
|
25
22
|
...url && { url },
|
|
26
23
|
...storefrontOrigin && { storefrontOrigin },
|
|
@@ -29,23 +26,13 @@ function createShopifyRequestContext(input) {
|
|
|
29
26
|
...isDocumentRequest(request) && { documentRequest: true },
|
|
30
27
|
...request.signal && { signal: request.signal }
|
|
31
28
|
};
|
|
32
|
-
let
|
|
29
|
+
let capturedCookies;
|
|
33
30
|
let personalizedResponseReason;
|
|
31
|
+
let sessionEstablishingReason;
|
|
34
32
|
const captureSubrequestHeaders = (headers) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
setCookie: headers.getSetCookie()
|
|
38
|
-
};
|
|
33
|
+
const cookies = headers.getSetCookie();
|
|
34
|
+
if (cookies.length > 0) capturedCookies ??= cookies;
|
|
39
35
|
};
|
|
40
|
-
if (!hasTrackingCookie) {
|
|
41
|
-
const legacyUniqueToken = inboundCookies.get("_shopify_y");
|
|
42
|
-
const legacyVisitToken = inboundCookies.get("_shopify_s");
|
|
43
|
-
const headerUniqueToken = request.headers.get("X-Shopify-UniqueToken") ?? void 0;
|
|
44
|
-
const headerVisitToken = request.headers.get("X-Shopify-VisitToken") ?? void 0;
|
|
45
|
-
if (legacyUniqueToken || legacyVisitToken) context.legacyTokens = true;
|
|
46
|
-
context.uniqueToken = legacyUniqueToken ?? headerUniqueToken;
|
|
47
|
-
context.visitToken = legacyVisitToken ?? headerVisitToken;
|
|
48
|
-
}
|
|
49
36
|
return {
|
|
50
37
|
...context,
|
|
51
38
|
getForwardedRequestHeaders() {
|
|
@@ -59,30 +46,27 @@ function createShopifyRequestContext(input) {
|
|
|
59
46
|
},
|
|
60
47
|
captureSubrequestHeaders,
|
|
61
48
|
consumeStorefrontResponseHeaders(headers) {
|
|
62
|
-
|
|
49
|
+
captureSubrequestHeaders(headers);
|
|
63
50
|
headers.delete("set-cookie");
|
|
64
|
-
headers.delete(SERVER_TIMING_HEADER);
|
|
65
51
|
},
|
|
66
52
|
markResponseAsPersonalized(reason) {
|
|
67
53
|
personalizedResponseReason ??= reason;
|
|
68
54
|
},
|
|
55
|
+
markResponseAsSessionEstablishing(reason) {
|
|
56
|
+
sessionEstablishingReason ??= reason;
|
|
57
|
+
},
|
|
69
58
|
applyResponseHeaders(headers) {
|
|
70
59
|
headers.set("powered-by", "Shopify, Hydrogen");
|
|
71
60
|
const isDocumentResponse = context.documentRequest || (headers.get("content-type")?.startsWith("text/html") ?? false);
|
|
72
|
-
|
|
73
|
-
if (capturedSubrequestHeaders && mayReturnShopifyState) {
|
|
61
|
+
if (capturedCookies && requestMethod !== void 0 && requestMethod !== "GET" && requestMethod !== "HEAD" && !isDocumentResponse && (hasEssentialCookie || isConsentManagementRequest || sessionEstablishingReason !== void 0)) {
|
|
74
62
|
const existingSetCookies = new Set(headers.getSetCookie());
|
|
75
|
-
for (const value of
|
|
63
|
+
for (const value of capturedCookies) {
|
|
76
64
|
if (existingSetCookies.has(value)) continue;
|
|
77
65
|
headers.append("set-cookie", value);
|
|
78
66
|
existingSetCookies.add(value);
|
|
79
67
|
}
|
|
80
|
-
const capturedServerTiming = capturedSubrequestHeaders.serverTiming;
|
|
81
|
-
const existingServerTiming = headers.get("Server-Timing") ?? "";
|
|
82
|
-
if (capturedServerTiming !== "" && existingServerTiming !== capturedServerTiming && !existingServerTiming.startsWith(`${capturedServerTiming}, `) && !existingServerTiming.endsWith(`, ${capturedServerTiming}`)) headers.set(SERVER_TIMING_HEADER, existingServerTiming ? `${existingServerTiming}, ${capturedServerTiming}` : capturedServerTiming);
|
|
83
68
|
}
|
|
84
|
-
|
|
85
|
-
if (personalizedResponseReason || returnsCapturedState || headers.getSetCookie().some(isShopifySetCookie)) applyPrivateResponseCacheHeaders(headers);
|
|
69
|
+
if (personalizedResponseReason || isConsentManagementRequest || sessionEstablishingReason !== void 0 || headers.has("set-cookie")) applyPrivateResponseCacheHeaders(headers);
|
|
86
70
|
}
|
|
87
71
|
};
|
|
88
72
|
}
|
|
@@ -96,16 +80,14 @@ function applyStorefrontRequestHeaders(context, headers) {
|
|
|
96
80
|
headers.set(SDK_VARIANT_HEADER, "hydrogen");
|
|
97
81
|
headers.set(SDK_VARIANT_SOURCE_HEADER, "kit");
|
|
98
82
|
headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);
|
|
99
|
-
headers.set(HYDROGEN_VERSION_HEADER, "2026.10.0-preview.
|
|
83
|
+
headers.set(HYDROGEN_VERSION_HEADER, "2026.10.0-preview.4");
|
|
100
84
|
headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);
|
|
101
85
|
if (context.cookie) headers.set("cookie", context.cookie);
|
|
102
86
|
else headers.delete("cookie");
|
|
87
|
+
if (context.globalPrivacyControl !== void 0) headers.set(SEC_GPC_HEADER, context.globalPrivacyControl);
|
|
88
|
+
else headers.delete(SEC_GPC_HEADER);
|
|
103
89
|
if (context.storefrontOrigin) headers.set(SHOPIFY_STOREFRONT_ORIGIN_HEADER, context.storefrontOrigin);
|
|
104
90
|
else headers.delete(SHOPIFY_STOREFRONT_ORIGIN_HEADER);
|
|
105
|
-
if (context.uniqueToken) headers.set(SHOPIFY_UNIQUE_TOKEN_HEADER, context.uniqueToken);
|
|
106
|
-
if (context.visitToken) headers.set(SHOPIFY_VISIT_TOKEN_HEADER, context.visitToken);
|
|
107
|
-
if (context.legacyTokens && context.uniqueToken) headers.set(SHOPIFY_STOREFRONT_Y_HEADER, context.uniqueToken);
|
|
108
|
-
if (context.legacyTokens && context.visitToken) headers.set(SHOPIFY_STOREFRONT_S_HEADER, context.visitToken);
|
|
109
91
|
}
|
|
110
92
|
function getUrlOrigin(url) {
|
|
111
93
|
if (!url) return void 0;
|
|
@@ -131,10 +113,6 @@ function parseCookieHeader(cookieHeader) {
|
|
|
131
113
|
}
|
|
132
114
|
return cookies;
|
|
133
115
|
}
|
|
134
|
-
function isShopifySetCookie(value) {
|
|
135
|
-
const name = value.match(/^\s*([^=;\s]+)\s*=/)?.[1];
|
|
136
|
-
return name !== void 0 && SHOPIFY_COOKIES.has(name);
|
|
137
|
-
}
|
|
138
116
|
|
|
139
117
|
//#endregion
|
|
140
118
|
export { createShopifyRequestContext };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-context.mjs","names":[],"sources":["../../../src/core/request-context.ts"],"sourcesContent":["import type {\n CountryCode as CustomerAccountCountryCode,\n LanguageCode as CustomerAccountLanguageCode,\n} from \"../graphql/generated/customer-account-api-types\";\nimport type {\n CountryCode as StorefrontCountryCode,\n LanguageCode as StorefrontLanguageCode,\n} from \"../graphql/generated/storefront-api-types\";\nimport { STOREFRONT_API_VERSION } from \"./constants\";\nimport {\n applyPrivateResponseCacheHeaders,\n CONSENT_MANAGEMENT_HEADER,\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SERVER_TIMING_HEADER,\n SHOPIFY_STOREFRONT_ORIGIN_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n STOREFRONT_URL_HEADER,\n} from \"./headers\";\nimport { normalizePathPrefix } from \"./standard-routes/path\";\n\nconst SHOPIFY_ESSENTIAL_COOKIE = \"_shopify_essential\";\nconst SHOPIFY_TRACKING_COOKIES = [\"_shopify_analytics\", \"_shopify_marketing\"];\nconst SHOPIFY_COOKIES = new Set([SHOPIFY_ESSENTIAL_COOKIE, ...SHOPIFY_TRACKING_COOKIES]);\n\ntype StorefrontRequest = Pick<Request, \"headers\"> &\n Partial<Pick<Request, \"method\" | \"signal\" | \"url\">>;\n\nexport type ShopifyLanguageCode = Extract<StorefrontLanguageCode, CustomerAccountLanguageCode>;\nexport type ShopifyCountryCode = Extract<StorefrontCountryCode, CustomerAccountCountryCode>;\n\nexport type I18nConfig = {\n language: ShopifyLanguageCode;\n country: ShopifyCountryCode;\n /** Optional app route prefix for localized paths, for example \"/es-es\". */\n pathPrefix?: string;\n};\n\ntype NormalizedI18nConfig<I18n extends I18nConfig = I18nConfig> = Omit<I18n, \"pathPrefix\"> & {\n pathPrefix: string;\n};\n\ntype ShopifyRequestContextInputBase<I18n extends I18nConfig = I18nConfig> = {\n request: StorefrontRequest;\n i18n: I18n;\n};\n\ntype ShopifyRequestContextInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp?: never };\n\ntype ShopifyRequestContextWithBuyerIpInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp: string };\n\ntype ShopifyRequestContextBase = {\n // -- Private fields --\n /**\n * Compile-time brand so callers use createShopifyRequestContext().\n * @internal\n */\n readonly __hydrogenShopifyRequestContextBrand: never;\n /** @internal */\n cookie?: string;\n /** @internal */\n uniqueToken?: string;\n /** @internal */\n visitToken?: string;\n /** @internal */\n legacyTokens?: boolean;\n /** @internal */\n readonly buyerIp?: string;\n /** @internal */\n requestGroupId: string;\n /** @internal */\n signal?: AbortSignal;\n /** @internal */\n url?: string;\n /** @internal */\n storefrontOrigin?: string;\n /**\n * Apply request-scoped headers required by every Shopify storefront subrequest.\n * @internal\n */\n applyStorefrontRequestHeaders(headers: Headers): void;\n /**\n * Capture the first fresh storefront response headers for replay onto the final app response.\n * @internal\n */\n captureSubrequestHeaders(headers: Headers): void;\n /**\n * Consume storefront proxy response state for gated replay onto the final app response.\n * @internal\n */\n consumeStorefrontResponseHeaders(headers: Headers): void;\n /**\n * Mark the final app response as influenced by private customer state.\n * @internal\n */\n markResponseAsPersonalized(reason: string): void;\n\n // -- Public fields --\n i18n: NormalizedI18nConfig;\n /** Return incoming request headers plus request lifecycle headers for proxy/origin handoff. */\n getForwardedRequestHeaders(): Headers;\n /** Apply important response headers for the correct functioning of Hydrogen storefronts. */\n applyResponseHeaders(headers: Headers): void;\n};\n\nexport type ShopifyRequestContext<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextBase & {\n i18n: NormalizedI18nConfig<I18n>;\n };\n\nexport type ShopifyRequestContextWithBuyerIp<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContext<I18n> & { readonly buyerIp: string };\n\ntype Context<I18n extends I18nConfig = I18nConfig> = {\n cookie?: string;\n uniqueToken?: string;\n visitToken?: string;\n legacyTokens?: boolean;\n buyerIp?: string;\n requestGroupId: string;\n signal?: AbortSignal;\n url?: string;\n storefrontOrigin?: string;\n i18n: NormalizedI18nConfig<I18n>;\n documentRequest?: boolean;\n};\n\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextWithBuyerIpInput<I18n>,\n): ShopifyRequestContextWithBuyerIp<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInput<I18n>,\n): ShopifyRequestContext<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInputBase<I18n> & { buyerIp?: string },\n): ShopifyRequestContext<I18n> {\n const { request } = input;\n\n if (!input.i18n?.country || !input.i18n?.language) {\n throw new Error(\"i18n with country and language is required for Shopify request contexts.\");\n }\n\n if (input.buyerIp !== undefined && !input.buyerIp) {\n throw new Error(\"buyerIp must be non-empty when provided\");\n }\n\n const requestMethod = request.method?.toUpperCase();\n const i18n = normalizeI18n(input.i18n);\n const cookieHeader = request.headers.get(\"cookie\") || undefined;\n const inboundCookies = parseCookieHeader(cookieHeader);\n const hasEssentialCookie = inboundCookies.has(SHOPIFY_ESSENTIAL_COOKIE);\n const hasTrackingCookie = SHOPIFY_TRACKING_COOKIES.some((name) => inboundCookies.has(name));\n const isConsentManagementRequest = request.headers.get(CONSENT_MANAGEMENT_HEADER) === \"1\";\n const url = request.url ?? request.headers.get(STOREFRONT_URL_HEADER) ?? undefined;\n const storefrontOrigin = getUrlOrigin(url);\n const context = {\n ...(cookieHeader && { cookie: cookieHeader }),\n i18n,\n ...(url && { url }),\n ...(storefrontOrigin && { storefrontOrigin }),\n ...(input.buyerIp && { buyerIp: input.buyerIp }),\n requestGroupId:\n request.headers.get(REQUEST_GROUP_ID_HEADER) ??\n request.headers.get(\"x-request-id\") ??\n request.headers.get(\"request-id\") ??\n crypto.randomUUID(),\n ...(isDocumentRequest(request) && { documentRequest: true }),\n ...(request.signal && { signal: request.signal }),\n } as Context<I18n>;\n\n let capturedSubrequestHeaders:\n | {\n serverTiming: string;\n setCookie: string[];\n }\n | undefined;\n let personalizedResponseReason: string | undefined;\n\n const captureSubrequestHeaders = (headers: Headers): void => {\n // Capture this the first time we get a fresh response to increase the\n // chance of returning it from the main server response. The main response\n // needs headers set at send time, while the body can stream later, so this\n // may not be used if subrequests finish after the main response is sent.\n capturedSubrequestHeaders ??= {\n serverTiming: headers.get(SERVER_TIMING_HEADER) ?? \"\",\n setCookie: headers.getSetCookie(),\n };\n };\n\n if (!hasTrackingCookie) {\n const legacyUniqueToken = inboundCookies.get(\"_shopify_y\");\n const legacyVisitToken = inboundCookies.get(\"_shopify_s\");\n const headerUniqueToken = request.headers.get(SHOPIFY_UNIQUE_TOKEN_HEADER) ?? undefined;\n const headerVisitToken = request.headers.get(SHOPIFY_VISIT_TOKEN_HEADER) ?? undefined;\n\n if (legacyUniqueToken || legacyVisitToken) context.legacyTokens = true;\n\n context.uniqueToken = legacyUniqueToken ?? headerUniqueToken;\n context.visitToken = legacyVisitToken ?? headerVisitToken;\n }\n\n return {\n ...context,\n getForwardedRequestHeaders() {\n const headers = new Headers(request.headers);\n applyStorefrontRequestHeaders(context, headers);\n if (context.url) headers.set(STOREFRONT_URL_HEADER, context.url);\n return headers;\n },\n applyStorefrontRequestHeaders(headers) {\n applyStorefrontRequestHeaders(context, headers);\n },\n captureSubrequestHeaders,\n consumeStorefrontResponseHeaders(headers) {\n if (headers.has(SERVER_TIMING_HEADER) || headers.getSetCookie().length > 0) {\n captureSubrequestHeaders(headers);\n }\n headers.delete(\"set-cookie\");\n headers.delete(SERVER_TIMING_HEADER);\n },\n markResponseAsPersonalized(reason) {\n personalizedResponseReason ??= reason;\n },\n applyResponseHeaders(headers) {\n headers.set(\"powered-by\", \"Shopify, Hydrogen\");\n\n // Documents may be shared or streamed, so they must not carry buyer-specific state.\n const isDocumentResponse =\n context.documentRequest || (headers.get(\"content-type\")?.startsWith(\"text/html\") ?? false);\n\n // Keep GET and HEAD responses cacheable, and fail closed when the request method is unknown.\n // Cold sessions may only be established by the explicitly marked consent request.\n const mayReturnShopifyState =\n requestMethod !== undefined &&\n requestMethod !== \"GET\" &&\n requestMethod !== \"HEAD\" &&\n !isDocumentResponse &&\n (hasEssentialCookie || isConsentManagementRequest);\n\n // Replay state captured from fresh SFAPI and proxy responses when allowed.\n if (capturedSubrequestHeaders && mayReturnShopifyState) {\n const existingSetCookies = new Set(headers.getSetCookie());\n for (const value of capturedSubrequestHeaders.setCookie) {\n if (existingSetCookies.has(value)) continue;\n headers.append(\"set-cookie\", value);\n existingSetCookies.add(value);\n }\n\n const capturedServerTiming = capturedSubrequestHeaders.serverTiming;\n const existingServerTiming = headers.get(SERVER_TIMING_HEADER) ?? \"\";\n const shouldAppendServerTiming =\n capturedServerTiming !== \"\" &&\n existingServerTiming !== capturedServerTiming &&\n !existingServerTiming.startsWith(`${capturedServerTiming}, `) &&\n !existingServerTiming.endsWith(`, ${capturedServerTiming}`);\n if (shouldAppendServerTiming) {\n headers.set(\n SERVER_TIMING_HEADER,\n existingServerTiming\n ? `${existingServerTiming}, ${capturedServerTiming}`\n : capturedServerTiming,\n );\n }\n }\n\n // Responses containing buyer-specific or replayed state must not enter shared caches.\n const returnsCapturedState =\n mayReturnShopifyState &&\n Boolean(\n capturedSubrequestHeaders?.serverTiming || capturedSubrequestHeaders?.setCookie.length,\n );\n\n if (\n personalizedResponseReason ||\n returnsCapturedState ||\n headers.getSetCookie().some(isShopifySetCookie)\n ) {\n applyPrivateResponseCacheHeaders(headers);\n }\n },\n } as ShopifyRequestContext<I18n>;\n}\n\nfunction normalizeI18n<I18n extends I18nConfig>(i18n: I18n): NormalizedI18nConfig<I18n> {\n return {\n ...i18n,\n pathPrefix: normalizePathPrefix(i18n.pathPrefix),\n } as NormalizedI18nConfig<I18n>;\n}\n\nfunction applyStorefrontRequestHeaders(context: Context, headers: Headers): void {\n headers.set(SDK_VARIANT_HEADER, \"hydrogen\");\n headers.set(SDK_VARIANT_SOURCE_HEADER, \"kit\");\n headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);\n headers.set(HYDROGEN_VERSION_HEADER, __HYDROGEN_VERSION__);\n headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);\n\n if (context.cookie) headers.set(\"cookie\", context.cookie);\n else headers.delete(\"cookie\");\n if (context.storefrontOrigin) {\n headers.set(SHOPIFY_STOREFRONT_ORIGIN_HEADER, context.storefrontOrigin);\n } else headers.delete(SHOPIFY_STOREFRONT_ORIGIN_HEADER);\n\n // Some Storefront API consumers still rely on these headers instead of cookies.\n if (context.uniqueToken) headers.set(SHOPIFY_UNIQUE_TOKEN_HEADER, context.uniqueToken);\n if (context.visitToken) headers.set(SHOPIFY_VISIT_TOKEN_HEADER, context.visitToken);\n if (context.legacyTokens && context.uniqueToken) {\n headers.set(SHOPIFY_STOREFRONT_Y_HEADER, context.uniqueToken);\n }\n if (context.legacyTokens && context.visitToken) {\n headers.set(SHOPIFY_STOREFRONT_S_HEADER, context.visitToken);\n }\n}\n\nfunction getUrlOrigin(url: string | undefined): string | undefined {\n if (!url) return undefined;\n try {\n return new URL(url).origin;\n } catch {\n return undefined;\n }\n}\n\nfunction isDocumentRequest(request: StorefrontRequest): boolean {\n if (request.method && request.method !== \"GET\" && request.method !== \"HEAD\") return false;\n\n const destination = request.headers.get(\"sec-fetch-dest\");\n if (destination === \"document\") return true;\n\n return request.headers.get(\"accept\")?.includes(\"text/html\") ?? false;\n}\n\nfunction parseCookieHeader(cookieHeader: string | undefined): Map<string, string> {\n const cookies = new Map<string, string>();\n if (!cookieHeader) return cookies;\n\n for (const cookie of cookieHeader.split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0) continue;\n\n const name = cookie.slice(0, separator).trim();\n if (!cookies.has(name)) cookies.set(name, cookie.slice(separator + 1));\n }\n\n return cookies;\n}\n\nfunction isShopifySetCookie(value: string): boolean {\n const name = value.match(/^\\s*([^=;\\s]+)\\s*=/)?.[1];\n return name !== undefined && SHOPIFY_COOKIES.has(name);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"request-context.mjs","names":[],"sources":["../../../src/core/request-context.ts"],"sourcesContent":["import type {\n CountryCode as CustomerAccountCountryCode,\n LanguageCode as CustomerAccountLanguageCode,\n} from \"../graphql/generated/customer-account-api-types\";\nimport type {\n CountryCode as StorefrontCountryCode,\n LanguageCode as StorefrontLanguageCode,\n} from \"../graphql/generated/storefront-api-types\";\nimport { STOREFRONT_API_VERSION } from \"./constants\";\nimport {\n applyPrivateResponseCacheHeaders,\n CONSENT_MANAGEMENT_HEADER,\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SEC_GPC_HEADER,\n SHOPIFY_STOREFRONT_ORIGIN_HEADER,\n STOREFRONT_URL_HEADER,\n} from \"./headers\";\nimport { normalizePathPrefix } from \"./standard-routes/path\";\n\nconst SHOPIFY_ESSENTIAL_COOKIE = \"_shopify_essential\";\n\ntype StorefrontRequest = Pick<Request, \"headers\"> &\n Partial<Pick<Request, \"method\" | \"signal\" | \"url\">>;\n\nexport type ShopifyLanguageCode = Extract<StorefrontLanguageCode, CustomerAccountLanguageCode>;\nexport type ShopifyCountryCode = Extract<StorefrontCountryCode, CustomerAccountCountryCode>;\n\nexport type I18nConfig = {\n language: ShopifyLanguageCode;\n country: ShopifyCountryCode;\n /** Optional app route prefix for localized paths, for example \"/es-es\". */\n pathPrefix?: string;\n};\n\ntype NormalizedI18nConfig<I18n extends I18nConfig = I18nConfig> = Omit<I18n, \"pathPrefix\"> & {\n pathPrefix: string;\n};\n\ntype ShopifyRequestContextInputBase<I18n extends I18nConfig = I18nConfig> = {\n request: StorefrontRequest;\n i18n: I18n;\n};\n\ntype ShopifyRequestContextInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp?: never };\n\ntype ShopifyRequestContextWithBuyerIpInput<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextInputBase<I18n> & { buyerIp: string };\n\ntype ShopifyRequestContextBase = {\n // -- Private fields --\n /**\n * Compile-time brand so callers use createShopifyRequestContext().\n * @internal\n */\n readonly __hydrogenShopifyRequestContextBrand: never;\n /** @internal */\n cookie?: string;\n /** @internal */\n readonly buyerIp?: string;\n /** @internal */\n requestGroupId: string;\n /** @internal */\n signal?: AbortSignal;\n /** @internal */\n url?: string;\n /** @internal */\n storefrontOrigin?: string;\n /**\n * Apply request-scoped headers required by every Shopify storefront subrequest.\n * @internal\n */\n applyStorefrontRequestHeaders(headers: Headers): void;\n /**\n * Capture cookies from the first fresh storefront response that sets them for replay.\n * @internal\n */\n captureSubrequestHeaders(headers: Headers): void;\n /**\n * Consume storefront proxy cookies for gated replay onto the final app response.\n * @internal\n */\n consumeStorefrontResponseHeaders(headers: Headers): void;\n /**\n * Mark the final app response as influenced by private customer state.\n * @internal\n */\n markResponseAsPersonalized(reason: string): void;\n /**\n * Mark this request as a sanctioned session-establishing endpoint (like the\n * consent flow), permitting Shopify state to return even on a cold session.\n * @internal\n */\n markResponseAsSessionEstablishing(reason: string): void;\n\n // -- Public fields --\n i18n: NormalizedI18nConfig;\n /** Return incoming request headers plus request lifecycle headers for proxy/origin handoff. */\n getForwardedRequestHeaders(): Headers;\n /** Apply important response headers for the correct functioning of Hydrogen storefronts. */\n applyResponseHeaders(headers: Headers): void;\n};\n\nexport type ShopifyRequestContext<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContextBase & {\n i18n: NormalizedI18nConfig<I18n>;\n };\n\nexport type ShopifyRequestContextWithBuyerIp<I18n extends I18nConfig = I18nConfig> =\n ShopifyRequestContext<I18n> & { readonly buyerIp: string };\n\ntype Context<I18n extends I18nConfig = I18nConfig> = {\n cookie?: string;\n globalPrivacyControl?: string;\n buyerIp?: string;\n requestGroupId: string;\n signal?: AbortSignal;\n url?: string;\n storefrontOrigin?: string;\n i18n: NormalizedI18nConfig<I18n>;\n documentRequest?: boolean;\n};\n\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextWithBuyerIpInput<I18n>,\n): ShopifyRequestContextWithBuyerIp<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInput<I18n>,\n): ShopifyRequestContext<I18n>;\nexport function createShopifyRequestContext<const I18n extends I18nConfig>(\n input: ShopifyRequestContextInputBase<I18n> & { buyerIp?: string },\n): ShopifyRequestContext<I18n> {\n const { request } = input;\n\n if (!input.i18n?.country || !input.i18n?.language) {\n throw new Error(\"i18n with country and language is required for Shopify request contexts.\");\n }\n\n if (input.buyerIp !== undefined && !input.buyerIp) {\n throw new Error(\"buyerIp must be non-empty when provided\");\n }\n\n const requestMethod = request.method?.toUpperCase();\n const i18n = normalizeI18n(input.i18n);\n const cookieHeader = request.headers.get(\"cookie\") || undefined;\n const inboundCookies = parseCookieHeader(cookieHeader);\n const hasEssentialCookie = inboundCookies.has(SHOPIFY_ESSENTIAL_COOKIE);\n const isConsentManagementRequest = request.headers.get(CONSENT_MANAGEMENT_HEADER) === \"1\";\n const url = request.url ?? request.headers.get(STOREFRONT_URL_HEADER) ?? undefined;\n const storefrontOrigin = getUrlOrigin(url);\n const context = {\n ...(cookieHeader && { cookie: cookieHeader }),\n globalPrivacyControl: request.headers.get(SEC_GPC_HEADER) ?? undefined,\n i18n,\n ...(url && { url }),\n ...(storefrontOrigin && { storefrontOrigin }),\n ...(input.buyerIp && { buyerIp: input.buyerIp }),\n requestGroupId:\n request.headers.get(REQUEST_GROUP_ID_HEADER) ??\n request.headers.get(\"x-request-id\") ??\n request.headers.get(\"request-id\") ??\n crypto.randomUUID(),\n ...(isDocumentRequest(request) && { documentRequest: true }),\n ...(request.signal && { signal: request.signal }),\n } as Context<I18n>;\n\n let capturedCookies: string[] | undefined;\n let personalizedResponseReason: string | undefined;\n let sessionEstablishingReason: string | undefined;\n\n const captureSubrequestHeaders = (headers: Headers): void => {\n // Capture the first fresh response cookies to increase the\n // chance of returning them from the main server response. The main response\n // needs headers set at send time, while the body can stream later, so this\n // may not be used if subrequests finish after the main response is sent.\n const cookies = headers.getSetCookie();\n if (cookies.length > 0) capturedCookies ??= cookies;\n };\n\n return {\n ...context,\n getForwardedRequestHeaders() {\n const headers = new Headers(request.headers);\n applyStorefrontRequestHeaders(context, headers);\n if (context.url) headers.set(STOREFRONT_URL_HEADER, context.url);\n return headers;\n },\n applyStorefrontRequestHeaders(headers) {\n applyStorefrontRequestHeaders(context, headers);\n },\n captureSubrequestHeaders,\n consumeStorefrontResponseHeaders(headers) {\n captureSubrequestHeaders(headers);\n headers.delete(\"set-cookie\");\n },\n markResponseAsPersonalized(reason) {\n personalizedResponseReason ??= reason;\n },\n markResponseAsSessionEstablishing(reason) {\n sessionEstablishingReason ??= reason;\n },\n applyResponseHeaders(headers) {\n headers.set(\"powered-by\", \"Shopify, Hydrogen\");\n\n // Documents may be shared or streamed, so they must not carry buyer-specific state.\n const isDocumentResponse =\n context.documentRequest || (headers.get(\"content-type\")?.startsWith(\"text/html\") ?? false);\n\n // Keep GET and HEAD responses cacheable, and fail closed when the request method is unknown.\n // Cold sessions may only be established by the explicitly marked consent request.\n const mayReturnShopifyState =\n requestMethod !== undefined &&\n requestMethod !== \"GET\" &&\n requestMethod !== \"HEAD\" &&\n !isDocumentResponse &&\n (hasEssentialCookie ||\n isConsentManagementRequest ||\n sessionEstablishingReason !== undefined);\n\n // Replay cookies captured from fresh SFAPI and proxy responses when allowed.\n if (capturedCookies && mayReturnShopifyState) {\n const existingSetCookies = new Set(headers.getSetCookie());\n for (const value of capturedCookies) {\n if (existingSetCookies.has(value)) continue;\n headers.append(\"set-cookie\", value);\n existingSetCookies.add(value);\n }\n }\n\n // Consent/session responses can contain private state in the body without setting cookies.\n if (\n personalizedResponseReason ||\n isConsentManagementRequest ||\n sessionEstablishingReason !== undefined ||\n headers.has(\"set-cookie\")\n ) {\n applyPrivateResponseCacheHeaders(headers);\n }\n },\n } as ShopifyRequestContext<I18n>;\n}\n\nfunction normalizeI18n<I18n extends I18nConfig>(i18n: I18n): NormalizedI18nConfig<I18n> {\n return {\n ...i18n,\n pathPrefix: normalizePathPrefix(i18n.pathPrefix),\n } as NormalizedI18nConfig<I18n>;\n}\n\nfunction applyStorefrontRequestHeaders(context: Context, headers: Headers): void {\n headers.set(SDK_VARIANT_HEADER, \"hydrogen\");\n headers.set(SDK_VARIANT_SOURCE_HEADER, \"kit\");\n headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);\n headers.set(HYDROGEN_VERSION_HEADER, __HYDROGEN_VERSION__);\n headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);\n\n if (context.cookie) headers.set(\"cookie\", context.cookie);\n else headers.delete(\"cookie\");\n if (context.globalPrivacyControl !== undefined) {\n headers.set(SEC_GPC_HEADER, context.globalPrivacyControl);\n } else headers.delete(SEC_GPC_HEADER);\n if (context.storefrontOrigin) {\n headers.set(SHOPIFY_STOREFRONT_ORIGIN_HEADER, context.storefrontOrigin);\n } else headers.delete(SHOPIFY_STOREFRONT_ORIGIN_HEADER);\n}\n\nfunction getUrlOrigin(url: string | undefined): string | undefined {\n if (!url) return undefined;\n try {\n return new URL(url).origin;\n } catch {\n return undefined;\n }\n}\n\nfunction isDocumentRequest(request: StorefrontRequest): boolean {\n if (request.method && request.method !== \"GET\" && request.method !== \"HEAD\") return false;\n\n const destination = request.headers.get(\"sec-fetch-dest\");\n if (destination === \"document\") return true;\n\n return request.headers.get(\"accept\")?.includes(\"text/html\") ?? false;\n}\n\nfunction parseCookieHeader(cookieHeader: string | undefined): Map<string, string> {\n const cookies = new Map<string, string>();\n if (!cookieHeader) return cookies;\n\n for (const cookie of cookieHeader.split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0) continue;\n\n const name = cookie.slice(0, separator).trim();\n if (!cookies.has(name)) cookies.set(name, cookie.slice(separator + 1));\n }\n\n return cookies;\n}\n"],"mappings":"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"}
|
|
@@ -2,9 +2,10 @@ import { safeApplyResponseHeaders } from "./safe-apply-response-headers.mjs";
|
|
|
2
2
|
import { handleProductVariantId } from "../product/accept-variant-id.mjs";
|
|
3
3
|
import { handleAjaxApi } from "./interceptors/ajax-api.mjs";
|
|
4
4
|
import { handleShopifyApiProxy } from "./interceptors/api-proxy.mjs";
|
|
5
|
-
import { handleCheckoutRedirect } from "./interceptors/checkout.mjs";
|
|
5
|
+
import { handleBuyPermalinkRedirect, handleCheckoutRedirect } from "./interceptors/checkout.mjs";
|
|
6
6
|
import { handleMcpProxy } from "./interceptors/mcp-proxy.mjs";
|
|
7
7
|
import { handleSfapiProxy } from "./interceptors/sfapi-proxy.mjs";
|
|
8
|
+
import { handleUcpMcpProxy } from "./interceptors/ucp-mcp-proxy.mjs";
|
|
8
9
|
import { handleWellKnownProxy } from "./interceptors/well-known.mjs";
|
|
9
10
|
import { handleShopifyRouteHandlers } from "./registered-routes.mjs";
|
|
10
11
|
|
|
@@ -14,8 +15,10 @@ const SHOPIFY_ROUTE_INTERCEPTORS = [
|
|
|
14
15
|
handleSfapiProxy,
|
|
15
16
|
handleProductVariantId,
|
|
16
17
|
handleShopifyRouteHandlers,
|
|
18
|
+
handleBuyPermalinkRedirect,
|
|
17
19
|
handleCheckoutRedirect,
|
|
18
20
|
handleWellKnownProxy,
|
|
21
|
+
handleUcpMcpProxy,
|
|
19
22
|
handleMcpProxy,
|
|
20
23
|
handleAjaxApi
|
|
21
24
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handle-shopify-routes.mjs","names":[],"sources":["../../../../src/core/request-routing/handle-shopify-routes.ts"],"sourcesContent":["import { handleProductVariantId } from \"../product/accept-variant-id\";\nimport { handleAjaxApi } from \"./interceptors/ajax-api\";\nimport { handleShopifyApiProxy } from \"./interceptors/api-proxy\";\nimport { handleCheckoutRedirect } from \"./interceptors/checkout\";\nimport { handleMcpProxy } from \"./interceptors/mcp-proxy\";\nimport { handleSfapiProxy } from \"./interceptors/sfapi-proxy\";\nimport { handleWellKnownProxy } from \"./interceptors/well-known\";\nimport { handleShopifyRouteHandlers } from \"./registered-routes\";\nimport type { HydrogenRouteHandler, HydrogenRouteInterceptor } from \"./route-types\";\nimport { safeApplyResponseHeaders } from \"./safe-apply-response-headers\";\n\nconst SHOPIFY_ROUTE_INTERCEPTORS = [\n handleShopifyApiProxy,\n handleSfapiProxy,\n handleProductVariantId,\n handleShopifyRouteHandlers,\n handleCheckoutRedirect,\n handleWellKnownProxy,\n handleMcpProxy,\n handleAjaxApi,\n] satisfies readonly HydrogenRouteInterceptor[];\n\n/**\n * Matches a request against Shopify standard routes and any registered handler\n * groups, returning a raw `Response` (redirect or JSON) when one matches, or\n * `null` when none do. Use it as the first step of request handling, before\n * framework routing. Matched responses already include request-context\n * response headers.\n */\nexport const handleShopifyRoutes: HydrogenRouteHandler = (options) => {\n if (options.requestContext !== options.storefrontClient.requestContext) {\n throw new Error(\n \"handleShopifyRoutes must receive the same requestContext used by storefrontClient.\",\n );\n }\n\n const url = new URL(options.request.url);\n\n for (const interceptor of SHOPIFY_ROUTE_INTERCEPTORS) {\n const responsePromise = interceptor(url, options);\n if (!responsePromise) continue;\n\n return responsePromise.then((response) =>\n safeApplyResponseHeaders(response, options.requestContext),\n );\n }\n\n return null;\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"handle-shopify-routes.mjs","names":[],"sources":["../../../../src/core/request-routing/handle-shopify-routes.ts"],"sourcesContent":["import { handleProductVariantId } from \"../product/accept-variant-id\";\nimport { handleAjaxApi } from \"./interceptors/ajax-api\";\nimport { handleShopifyApiProxy } from \"./interceptors/api-proxy\";\nimport { handleBuyPermalinkRedirect, handleCheckoutRedirect } from \"./interceptors/checkout\";\nimport { handleMcpProxy } from \"./interceptors/mcp-proxy\";\nimport { handleSfapiProxy } from \"./interceptors/sfapi-proxy\";\nimport { handleUcpMcpProxy } from \"./interceptors/ucp-mcp-proxy\";\nimport { handleWellKnownProxy } from \"./interceptors/well-known\";\nimport { handleShopifyRouteHandlers } from \"./registered-routes\";\nimport type { HydrogenRouteHandler, HydrogenRouteInterceptor } from \"./route-types\";\nimport { safeApplyResponseHeaders } from \"./safe-apply-response-headers\";\n\nconst SHOPIFY_ROUTE_INTERCEPTORS = [\n handleShopifyApiProxy,\n handleSfapiProxy,\n handleProductVariantId,\n handleShopifyRouteHandlers,\n handleBuyPermalinkRedirect,\n handleCheckoutRedirect,\n handleWellKnownProxy,\n handleUcpMcpProxy,\n handleMcpProxy,\n handleAjaxApi,\n] satisfies readonly HydrogenRouteInterceptor[];\n\n/**\n * Matches a request against Shopify standard routes and any registered handler\n * groups, returning a raw `Response` (redirect or JSON) when one matches, or\n * `null` when none do. Use it as the first step of request handling, before\n * framework routing. Matched responses already include request-context\n * response headers.\n */\nexport const handleShopifyRoutes: HydrogenRouteHandler = (options) => {\n if (options.requestContext !== options.storefrontClient.requestContext) {\n throw new Error(\n \"handleShopifyRoutes must receive the same requestContext used by storefrontClient.\",\n );\n }\n\n const url = new URL(options.request.url);\n\n for (const interceptor of SHOPIFY_ROUTE_INTERCEPTORS) {\n const responsePromise = interceptor(url, options);\n if (!responsePromise) continue;\n\n return responsePromise.then((response) =>\n safeApplyResponseHeaders(response, options.requestContext),\n );\n }\n\n return null;\n};\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,6BAA6B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,MAAa,uBAA6C,YAAY;AACpE,KAAI,QAAQ,mBAAmB,QAAQ,iBAAiB,eACtD,OAAM,IAAI,MACR,qFACD;CAGH,MAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,IAAI;AAExC,MAAK,MAAM,eAAe,4BAA4B;EACpD,MAAM,kBAAkB,YAAY,KAAK,QAAQ;AACjD,MAAI,CAAC,gBAAiB;AAEtB,SAAO,gBAAgB,MAAM,aAC3B,yBAAyB,UAAU,QAAQ,eAAe,CAC3D;;AAGH,QAAO"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AJAX_API_REQUEST_HEADER_ALLOWLIST } from "../../headers.mjs";
|
|
2
1
|
import { AJAX_CART_RE } from "../../url.mjs";
|
|
2
|
+
import { AJAX_API_REQUEST_HEADER_ALLOWLIST } from "../../headers.mjs";
|
|
3
3
|
import { createProxyInterceptor } from "./proxy.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/core/request-routing/interceptors/ajax-api.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PROXY_REQUEST_HEADER_DENYLIST } from "../../headers.mjs";
|
|
2
1
|
import { SHOPIFY_API_PROXY_PREFIX, SHOPIFY_API_PROXY_RE } from "../../url.mjs";
|
|
2
|
+
import { PROXY_REQUEST_HEADER_DENYLIST } from "../../headers.mjs";
|
|
3
3
|
import { createProxyInterceptor } from "./proxy.mjs";
|
|
4
4
|
import { rewriteShopifyJsPathname } from "./ajax-api.mjs";
|
|
5
5
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getLogger } from "../../logging/logging.mjs";
|
|
2
|
-
import { CHECKOUT_RE, isHydrogenServerHandoffPath } from "../../url.mjs";
|
|
2
|
+
import { BUY_PERMALINK_RE, CHECKOUT_RE, isHydrogenServerHandoffPath } from "../../url.mjs";
|
|
3
3
|
import { getCart, getCartId } from "../../cart/get-cart.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/core/request-routing/interceptors/checkout.ts
|
|
6
6
|
const log = getLogger("checkout");
|
|
7
|
-
const
|
|
7
|
+
const MOCK_SHOP_PERMALINK_ORIGIN = "https://demostore.mock.shop";
|
|
8
8
|
const handleCheckoutRedirect = (url, { request, storefrontClient }) => {
|
|
9
9
|
if (!isHydrogenServerHandoffPath(url.pathname)) return null;
|
|
10
10
|
if (request.method !== "GET" && request.method !== "HEAD") return Promise.resolve(new Response("Method Not Allowed", { status: 405 }));
|
|
@@ -26,6 +26,19 @@ const handleCheckoutRedirect = (url, { request, storefrontClient }) => {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
|
+
const handleBuyPermalinkRedirect = (url, { request, storefrontClient }) => {
|
|
30
|
+
if (!BUY_PERMALINK_RE.test(url.pathname)) return null;
|
|
31
|
+
if (request.method !== "GET") return Promise.resolve(new Response("Method Not Allowed", { status: 405 }));
|
|
32
|
+
const location = `${getPermalinkOrigin(storefrontClient.storeUrl)}${url.pathname}${url.search}`;
|
|
33
|
+
return Promise.resolve(new Response(null, {
|
|
34
|
+
status: 303,
|
|
35
|
+
headers: {
|
|
36
|
+
location,
|
|
37
|
+
"cache-control": "no-store",
|
|
38
|
+
"referrer-policy": "no-referrer"
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
29
42
|
async function getCheckoutRedirectUrl(request, storefrontClient) {
|
|
30
43
|
const cartId = getCartId(request);
|
|
31
44
|
if (!cartId) return new URL("/", request.url);
|
|
@@ -34,7 +47,7 @@ async function getCheckoutRedirectUrl(request, storefrontClient) {
|
|
|
34
47
|
}
|
|
35
48
|
async function getCartRedirectUrl(request, storefrontClient) {
|
|
36
49
|
const sourceUrl = new URL(request.url);
|
|
37
|
-
const redirectUrl = new URL(sourceUrl.pathname,
|
|
50
|
+
const redirectUrl = new URL(sourceUrl.pathname, getPermalinkOrigin(storefrontClient.storeUrl));
|
|
38
51
|
const cartId = getCartId(request);
|
|
39
52
|
if (!cartId) return redirectUrl;
|
|
40
53
|
try {
|
|
@@ -45,14 +58,17 @@ async function getCartRedirectUrl(request, storefrontClient) {
|
|
|
45
58
|
}
|
|
46
59
|
return redirectUrl;
|
|
47
60
|
}
|
|
48
|
-
function
|
|
61
|
+
function getPermalinkOrigin(storeUrl) {
|
|
49
62
|
const url = new URL(storeUrl);
|
|
50
|
-
return url.hostname
|
|
63
|
+
return isMockShopHost(url.hostname) ? MOCK_SHOP_PERMALINK_ORIGIN : url.origin;
|
|
64
|
+
}
|
|
65
|
+
function isMockShopHost(hostname) {
|
|
66
|
+
return hostname === "mock.shop" || hostname.endsWith(".mock.shop");
|
|
51
67
|
}
|
|
52
68
|
function mergeSearchParams(target, source) {
|
|
53
69
|
for (const [key, value] of source) if (!target.searchParams.has(key)) target.searchParams.append(key, value);
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
//#endregion
|
|
57
|
-
export { handleCheckoutRedirect };
|
|
73
|
+
export { handleBuyPermalinkRedirect, handleCheckoutRedirect };
|
|
58
74
|
//# sourceMappingURL=checkout.mjs.map
|