@shopify/hydrogen 2026.10.0 → 2026.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/cli/index.mjs +6 -3
  2. package/dist/client/client.mjs +2 -4
  3. package/dist/client/client.mjs.map +1 -1
  4. package/dist/client/i18n-variables.d.mts +13 -0
  5. package/dist/client/i18n-variables.mjs +84 -0
  6. package/dist/client/i18n-variables.mjs.map +1 -0
  7. package/dist/client/types.d.mts +13 -6
  8. package/dist/core/cart/cart.mjs +110 -23
  9. package/dist/core/cart/cart.mjs.map +1 -1
  10. package/dist/core/request-context.mjs +1 -1
  11. package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
  12. package/dist/core/request-routing/registered-routes.d.mts +5 -1
  13. package/dist/core/request-routing/registered-routes.mjs +21 -7
  14. package/dist/core/request-routing/registered-routes.mjs.map +1 -1
  15. package/dist/core/request-routing/route-types.d.mts +16 -6
  16. package/dist/core/shopify-scripts/perfkit.mjs +8 -2
  17. package/dist/core/shopify-scripts/perfkit.mjs.map +1 -1
  18. package/dist/core/utils/load-script.mjs +6 -6
  19. package/dist/core/utils/load-script.mjs.map +1 -1
  20. package/dist/customer-account/client.mjs +1 -1
  21. package/dist/customer-account/session.mjs +1 -1
  22. package/dist/development/client/client.mjs +2 -4
  23. package/dist/development/client/client.mjs.map +1 -1
  24. package/dist/development/client/i18n-variables.d.mts +13 -0
  25. package/dist/development/client/i18n-variables.mjs +84 -0
  26. package/dist/development/client/i18n-variables.mjs.map +1 -0
  27. package/dist/development/client/types.d.mts +13 -6
  28. package/dist/development/core/cart/cart.mjs +110 -23
  29. package/dist/development/core/cart/cart.mjs.map +1 -1
  30. package/dist/development/core/request-context.mjs +1 -1
  31. package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
  32. package/dist/development/core/request-routing/registered-routes.d.mts +5 -1
  33. package/dist/development/core/request-routing/registered-routes.mjs +21 -7
  34. package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
  35. package/dist/development/core/request-routing/route-types.d.mts +16 -6
  36. package/dist/development/core/shopify-scripts/perfkit.mjs +8 -2
  37. package/dist/development/core/shopify-scripts/perfkit.mjs.map +1 -1
  38. package/dist/development/core/utils/load-script.mjs +6 -6
  39. package/dist/development/core/utils/load-script.mjs.map +1 -1
  40. package/dist/development/graphql/type-resolver.d.mts +27 -1
  41. package/dist/development/react/cart.mjs +79 -25
  42. package/dist/development/react/cart.mjs.map +1 -1
  43. package/dist/graphql/type-resolver.d.mts +27 -1
  44. package/dist/react/cart.mjs +79 -25
  45. package/dist/react/cart.mjs.map +1 -1
  46. package/dist/vite/index.d.mts +11 -1
  47. package/dist/vite/index.mjs +21 -1
  48. package/dist/vite/index.mjs.map +1 -0
  49. package/dist/vue/shopify-scripts.d.mts +1 -1
  50. package/package.json +1 -1
  51. package/skills/hydrogen-cart-ui/SKILL.md +2 -2
  52. package/skills/hydrogen-request-handlers/SKILL.md +2 -2
  53. package/skills/hydrogen-request-handlers/references/frameworks.md +2 -7
  54. package/skills/hydrogen-request-handlers/references/nuxt.md +1 -3
  55. package/skills/hydrogen-storefront-client/SKILL.md +4 -2
  56. package/skills/migrate-to-hydrogen-2026-10/SKILL.md +332 -0
  57. package/skills/migrate-to-hydrogen-2026-10/references/analytics.md +24 -0
  58. package/skills/migrate-to-hydrogen-2026-10/references/cart.md +9 -0
  59. package/skills/migrate-to-hydrogen-2026-10/references/customer-account.md +10 -0
  60. package/skills/migrate-to-hydrogen-2026-10/references/dependencies-and-tooling.md +10 -0
  61. package/skills/migrate-to-hydrogen-2026-10/references/optional-subsystems.md +25 -0
  62. package/skills/migrate-to-hydrogen-2026-10/references/routing-and-loaders.md +10 -0
  63. package/skills/migrate-to-hydrogen-2026-10/references/sitemap-and-seo.md +5 -0
  64. package/skills/migrate-to-hydrogen-2026-10/references/typescript-and-gql-tada.md +7 -0
  65. package/skills/migrate-to-hydrogen-2026-10/references/ui-and-components.md +9 -0
@@ -84,7 +84,7 @@ function applyStorefrontRequestHeaders(context, headers) {
84
84
  headers.set(SDK_VARIANT_HEADER, "hydrogen");
85
85
  headers.set(SDK_VARIANT_SOURCE_HEADER, "kit");
86
86
  headers.set(SDK_VERSION_HEADER, STOREFRONT_API_VERSION);
87
- headers.set(HYDROGEN_VERSION_HEADER, "2026.10.0");
87
+ headers.set(HYDROGEN_VERSION_HEADER, "2026.10.2");
88
88
  headers.set(REQUEST_GROUP_ID_HEADER, context.requestGroupId);
89
89
  if (context.cookie) headers.set("cookie", context.cookie);
90
90
  else headers.delete("cookie");
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.mjs","names":[],"sources":["../../../../../src/core/request-routing/interceptors/proxy.ts"],"sourcesContent":["import { extractHeaders } from \"../../headers\";\nimport { getLogger } from \"../../logging\";\nimport type { HydrogenRouteInterceptor, HydrogenRoutesOptions } from \"../route-types\";\n\nconst PROXY_TIMEOUT_MS = 30_000;\n\n// Proxy errors are transient and may be buyer-specific: never cache them.\nconst PROXY_ERROR_CACHE_CONTROL = \"no-store\";\n\ntype ScopedLogger = ReturnType<typeof getLogger>;\n\ntype ProxyHeaderContext = HydrogenRoutesOptions & { url: URL };\n\ntype PrepareHeaders<ExtraContext extends object = object> = (\n headers: Headers,\n context: ProxyHeaderContext & ExtraContext,\n) => void;\n\ntype ProxyRequestHeaderOptions = (\n | { allow: readonly string[]; deny?: never }\n | { allow?: never; deny: readonly string[] }\n) & {\n prepare?: PrepareHeaders;\n applyStorefrontHeaders?: boolean;\n};\n\ntype ProxyUpstreamInfo = {\n ok: boolean;\n status: number;\n statusText: string;\n headers: Headers;\n};\n\ntype ProxyResponseHeaderOptions = {\n allow?: readonly string[];\n prepare?: PrepareHeaders<{ response: ProxyUpstreamInfo }>;\n};\n\ntype ProxyResponseRejection = {\n status: number;\n body: unknown;\n logMessage?: string;\n log?: Record<string, unknown>;\n};\n\ntype ProxyResponseValidation = (\n upstream: ProxyUpstreamInfo,\n options: HydrogenRoutesOptions,\n url: URL,\n) => ProxyResponseRejection | null;\n\ntype ProxyErrorPhase = \"setup\" | \"fetch\";\n\ntype ProxyErrorMapping = { status?: number; body?: unknown };\n\ntype MapProxyError = (error: unknown, phase: ProxyErrorPhase) => ProxyErrorMapping;\n\ntype ProxyDescriptor = {\n match: RegExp;\n methods?: readonly string[];\n formatError?: (message: string) => unknown;\n redirect?: RequestRedirect;\n scope: string;\n timeoutMs?: number;\n forwardSearch?: boolean;\n rewritePathname?: (pathname: string) => string;\n requestHeaders: ProxyRequestHeaderOptions;\n responseHeaders?: ProxyResponseHeaderOptions;\n responseValidation?: ProxyResponseValidation;\n mapError?: MapProxyError;\n};\n\ntype ProxyRequestInit = { upstreamUrl: URL; init: RequestInit & { duplex?: \"half\" } };\n\nexport function createProxyInterceptor(descriptor: ProxyDescriptor): HydrogenRouteInterceptor {\n const log = getLogger(descriptor.scope);\n const formatError = descriptor.formatError ?? defaultFormatError;\n\n return (url, options) => {\n const { request } = options;\n if (!descriptor.match.test(url.pathname)) return null;\n\n // Disable JSONP requests for security:\n if (url.searchParams.has(\"callback\")) {\n return Promise.resolve(\n createProxyRejectionResponse(\n {\n status: 400,\n body: formatError(\"JSONP requests are not supported\"),\n },\n request.method,\n ),\n );\n }\n\n if (descriptor.methods && !descriptor.methods.includes(request.method)) {\n // Method not allowed. Shape the body via formatError, like other errors.\n return Promise.resolve(\n new Response(\n request.method === \"HEAD\" ? null : JSON.stringify(formatError(\"Method Not Allowed\")),\n {\n status: 405,\n headers: {\n allow: descriptor.methods.join(\", \"),\n \"content-type\": \"application/json\",\n \"cache-control\": PROXY_ERROR_CACHE_CONTROL,\n },\n },\n ),\n );\n }\n\n let requestInit: ProxyRequestInit;\n try {\n requestInit = createProxyRequestInit(descriptor, options, url);\n } catch (error) {\n log.error(\"request failed\", { error });\n return Promise.resolve(\n createProxyErrorResponse(error, \"setup\", descriptor.mapError, formatError, request.method),\n );\n }\n\n return fetch(requestInit.upstreamUrl, requestInit.init)\n .then((upstreamResponse) =>\n buildProxyResponse(upstreamResponse, descriptor, options, url, log),\n )\n .catch((error) => {\n log.error(\"request failed\", { error });\n return createProxyErrorResponse(\n error,\n \"fetch\",\n descriptor.mapError,\n formatError,\n request.method,\n );\n });\n };\n}\n\nfunction createProxyRequestInit(\n descriptor: ProxyDescriptor,\n options: HydrogenRoutesOptions,\n url: URL,\n): ProxyRequestInit {\n const { request, storefrontClient } = options;\n const upstreamPathname = descriptor.rewritePathname?.(url.pathname) ?? url.pathname;\n const search = descriptor.forwardSearch === false ? \"\" : url.search;\n const upstreamUrl = new URL(upstreamPathname + search, storefrontClient.storeUrl);\n const init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n body: request.body,\n headers: createProxyRequestHeaders(descriptor, { ...options, url }),\n signal: AbortSignal.timeout(descriptor.timeoutMs ?? PROXY_TIMEOUT_MS),\n redirect: descriptor.redirect ?? \"manual\",\n };\n\n // Node's fetch requires this when forwarding a streaming request body.\n if (request.body) init.duplex = \"half\";\n return { upstreamUrl, init };\n}\n\nfunction buildProxyResponse(\n upstreamResponse: Response,\n descriptor: ProxyDescriptor,\n options: HydrogenRoutesOptions,\n url: URL,\n log: ScopedLogger,\n): Response {\n const upstream: ProxyUpstreamInfo = {\n ok: upstreamResponse.ok,\n status: upstreamResponse.status,\n statusText: upstreamResponse.statusText,\n headers: upstreamResponse.headers,\n };\n\n const rejection = descriptor.responseValidation?.(upstream, options, url);\n if (rejection) {\n // Centralized drain: response hooks receive only a body-less view, so the factory\n // owns the sole cancellation and no validator can leak the upstream connection.\n upstreamResponse.body?.cancel().catch(() => {});\n if (rejection.logMessage) log.error(rejection.logMessage, rejection.log ?? {});\n return createProxyRejectionResponse(rejection, options.request.method);\n }\n\n const headers = buildProxyResponseHeaders(\n upstreamResponse.headers,\n descriptor.responseHeaders?.allow,\n );\n descriptor.responseHeaders?.prepare?.(headers, { ...options, url, response: upstream });\n options.requestContext.consumeStorefrontResponseHeaders(headers);\n\n return new Response(upstreamResponse.body, {\n status: upstreamResponse.status,\n statusText: upstreamResponse.statusText,\n headers,\n });\n}\n\nfunction defaultFormatError(message: string): { error: string } {\n return { error: message };\n}\n\nfunction createProxyErrorResponse(\n error: unknown,\n phase: ProxyErrorPhase,\n mapError: MapProxyError | undefined,\n formatError: (message: string) => unknown,\n method: string,\n): Response {\n const message = error instanceof Error ? error.message : \"Internal proxy error\";\n const mapping = mapError?.(error, phase);\n const status = mapping?.status ?? (phase === \"setup\" ? 500 : 502);\n const body = mapping?.body === undefined ? formatError(message) : mapping.body;\n return createProxyRejectionResponse({ status, body }, method);\n}\n\nfunction createProxyRejectionResponse(rejection: ProxyResponseRejection, method: string): Response {\n return new Response(method === \"HEAD\" ? null : JSON.stringify(rejection.body), {\n status: rejection.status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": PROXY_ERROR_CACHE_CONTROL,\n },\n });\n}\n\nfunction createProxyRequestHeaders(\n descriptor: ProxyDescriptor,\n options: ProxyHeaderContext,\n): Headers {\n const { request, requestContext } = options;\n const { allow, deny, prepare } = descriptor.requestHeaders;\n const headers = allow\n ? new Headers(extractHeaders((key) => request.headers.get(key), allow))\n : new Headers(request.headers);\n\n if (descriptor.requestHeaders.applyStorefrontHeaders !== false) {\n requestContext.applyStorefrontRequestHeaders(headers);\n }\n\n for (const header of deny ?? []) headers.delete(header);\n prepare?.(headers, options);\n\n return headers;\n}\n\nfunction buildProxyResponseHeaders(upstreamHeaders: Headers, allow?: readonly string[]): Headers {\n if (allow) {\n const headers = new Headers();\n for (const name of allow) {\n const value = upstreamHeaders.get(name);\n if (value !== null) headers.set(name, value);\n }\n return headers;\n }\n return createProxyResponseHeaders(upstreamHeaders);\n}\n\nexport function createProxyResponseHeaders(upstreamHeaders: Headers): Headers {\n const headers = new Headers(upstreamHeaders);\n headers.delete(\"content-encoding\");\n headers.delete(\"content-length\");\n headers.delete(\"server-timing\");\n return headers;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"proxy.mjs","names":[],"sources":["../../../../../src/core/request-routing/interceptors/proxy.ts"],"sourcesContent":["import { extractHeaders } from \"../../headers\";\nimport { getLogger } from \"../../logging\";\nimport type { HydrogenRouteInterceptor, HydrogenRouteInterceptorOptions } from \"../route-types\";\n\nconst PROXY_TIMEOUT_MS = 30_000;\n\n// Proxy errors are transient and may be buyer-specific: never cache them.\nconst PROXY_ERROR_CACHE_CONTROL = \"no-store\";\n\ntype ScopedLogger = ReturnType<typeof getLogger>;\n\ntype ProxyHeaderContext = HydrogenRouteInterceptorOptions & { url: URL };\n\ntype PrepareHeaders<ExtraContext extends object = object> = (\n headers: Headers,\n context: ProxyHeaderContext & ExtraContext,\n) => void;\n\ntype ProxyRequestHeaderOptions = (\n | { allow: readonly string[]; deny?: never }\n | { allow?: never; deny: readonly string[] }\n) & {\n prepare?: PrepareHeaders;\n applyStorefrontHeaders?: boolean;\n};\n\ntype ProxyUpstreamInfo = {\n ok: boolean;\n status: number;\n statusText: string;\n headers: Headers;\n};\n\ntype ProxyResponseHeaderOptions = {\n allow?: readonly string[];\n prepare?: PrepareHeaders<{ response: ProxyUpstreamInfo }>;\n};\n\ntype ProxyResponseRejection = {\n status: number;\n body: unknown;\n logMessage?: string;\n log?: Record<string, unknown>;\n};\n\ntype ProxyResponseValidation = (\n upstream: ProxyUpstreamInfo,\n options: HydrogenRouteInterceptorOptions,\n url: URL,\n) => ProxyResponseRejection | null;\n\ntype ProxyErrorPhase = \"setup\" | \"fetch\";\n\ntype ProxyErrorMapping = { status?: number; body?: unknown };\n\ntype MapProxyError = (error: unknown, phase: ProxyErrorPhase) => ProxyErrorMapping;\n\ntype ProxyDescriptor = {\n match: RegExp;\n methods?: readonly string[];\n formatError?: (message: string) => unknown;\n redirect?: RequestRedirect;\n scope: string;\n timeoutMs?: number;\n forwardSearch?: boolean;\n rewritePathname?: (pathname: string) => string;\n requestHeaders: ProxyRequestHeaderOptions;\n responseHeaders?: ProxyResponseHeaderOptions;\n responseValidation?: ProxyResponseValidation;\n mapError?: MapProxyError;\n};\n\ntype ProxyRequestInit = { upstreamUrl: URL; init: RequestInit & { duplex?: \"half\" } };\n\nexport function createProxyInterceptor(descriptor: ProxyDescriptor): HydrogenRouteInterceptor {\n const log = getLogger(descriptor.scope);\n const formatError = descriptor.formatError ?? defaultFormatError;\n\n return (url, options) => {\n const { request } = options;\n if (!descriptor.match.test(url.pathname)) return null;\n\n // Disable JSONP requests for security:\n if (url.searchParams.has(\"callback\")) {\n return Promise.resolve(\n createProxyRejectionResponse(\n {\n status: 400,\n body: formatError(\"JSONP requests are not supported\"),\n },\n request.method,\n ),\n );\n }\n\n if (descriptor.methods && !descriptor.methods.includes(request.method)) {\n // Method not allowed. Shape the body via formatError, like other errors.\n return Promise.resolve(\n new Response(\n request.method === \"HEAD\" ? null : JSON.stringify(formatError(\"Method Not Allowed\")),\n {\n status: 405,\n headers: {\n allow: descriptor.methods.join(\", \"),\n \"content-type\": \"application/json\",\n \"cache-control\": PROXY_ERROR_CACHE_CONTROL,\n },\n },\n ),\n );\n }\n\n let requestInit: ProxyRequestInit;\n try {\n requestInit = createProxyRequestInit(descriptor, options, url);\n } catch (error) {\n log.error(\"request failed\", { error });\n return Promise.resolve(\n createProxyErrorResponse(error, \"setup\", descriptor.mapError, formatError, request.method),\n );\n }\n\n return fetch(requestInit.upstreamUrl, requestInit.init)\n .then((upstreamResponse) =>\n buildProxyResponse(upstreamResponse, descriptor, options, url, log),\n )\n .catch((error) => {\n log.error(\"request failed\", { error });\n return createProxyErrorResponse(\n error,\n \"fetch\",\n descriptor.mapError,\n formatError,\n request.method,\n );\n });\n };\n}\n\nfunction createProxyRequestInit(\n descriptor: ProxyDescriptor,\n options: HydrogenRouteInterceptorOptions,\n url: URL,\n): ProxyRequestInit {\n const { request, storefrontClient } = options;\n const upstreamPathname = descriptor.rewritePathname?.(url.pathname) ?? url.pathname;\n const search = descriptor.forwardSearch === false ? \"\" : url.search;\n const upstreamUrl = new URL(upstreamPathname + search, storefrontClient.storeUrl);\n const init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n body: request.body,\n headers: createProxyRequestHeaders(descriptor, { ...options, url }),\n signal: AbortSignal.timeout(descriptor.timeoutMs ?? PROXY_TIMEOUT_MS),\n redirect: descriptor.redirect ?? \"manual\",\n };\n\n // Node's fetch requires this when forwarding a streaming request body.\n if (request.body) init.duplex = \"half\";\n return { upstreamUrl, init };\n}\n\nfunction buildProxyResponse(\n upstreamResponse: Response,\n descriptor: ProxyDescriptor,\n options: HydrogenRouteInterceptorOptions,\n url: URL,\n log: ScopedLogger,\n): Response {\n const upstream: ProxyUpstreamInfo = {\n ok: upstreamResponse.ok,\n status: upstreamResponse.status,\n statusText: upstreamResponse.statusText,\n headers: upstreamResponse.headers,\n };\n\n const rejection = descriptor.responseValidation?.(upstream, options, url);\n if (rejection) {\n // Centralized drain: response hooks receive only a body-less view, so the factory\n // owns the sole cancellation and no validator can leak the upstream connection.\n upstreamResponse.body?.cancel().catch(() => {});\n if (rejection.logMessage) log.error(rejection.logMessage, rejection.log ?? {});\n return createProxyRejectionResponse(rejection, options.request.method);\n }\n\n const headers = buildProxyResponseHeaders(\n upstreamResponse.headers,\n descriptor.responseHeaders?.allow,\n );\n descriptor.responseHeaders?.prepare?.(headers, { ...options, url, response: upstream });\n options.requestContext.consumeStorefrontResponseHeaders(headers);\n\n return new Response(upstreamResponse.body, {\n status: upstreamResponse.status,\n statusText: upstreamResponse.statusText,\n headers,\n });\n}\n\nfunction defaultFormatError(message: string): { error: string } {\n return { error: message };\n}\n\nfunction createProxyErrorResponse(\n error: unknown,\n phase: ProxyErrorPhase,\n mapError: MapProxyError | undefined,\n formatError: (message: string) => unknown,\n method: string,\n): Response {\n const message = error instanceof Error ? error.message : \"Internal proxy error\";\n const mapping = mapError?.(error, phase);\n const status = mapping?.status ?? (phase === \"setup\" ? 500 : 502);\n const body = mapping?.body === undefined ? formatError(message) : mapping.body;\n return createProxyRejectionResponse({ status, body }, method);\n}\n\nfunction createProxyRejectionResponse(rejection: ProxyResponseRejection, method: string): Response {\n return new Response(method === \"HEAD\" ? null : JSON.stringify(rejection.body), {\n status: rejection.status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": PROXY_ERROR_CACHE_CONTROL,\n },\n });\n}\n\nfunction createProxyRequestHeaders(\n descriptor: ProxyDescriptor,\n options: ProxyHeaderContext,\n): Headers {\n const { request, requestContext } = options;\n const { allow, deny, prepare } = descriptor.requestHeaders;\n const headers = allow\n ? new Headers(extractHeaders((key) => request.headers.get(key), allow))\n : new Headers(request.headers);\n\n if (descriptor.requestHeaders.applyStorefrontHeaders !== false) {\n requestContext.applyStorefrontRequestHeaders(headers);\n }\n\n for (const header of deny ?? []) headers.delete(header);\n prepare?.(headers, options);\n\n return headers;\n}\n\nfunction buildProxyResponseHeaders(upstreamHeaders: Headers, allow?: readonly string[]): Headers {\n if (allow) {\n const headers = new Headers();\n for (const name of allow) {\n const value = upstreamHeaders.get(name);\n if (value !== null) headers.set(name, value);\n }\n return headers;\n }\n return createProxyResponseHeaders(upstreamHeaders);\n}\n\nexport function createProxyResponseHeaders(upstreamHeaders: Headers): Headers {\n const headers = new Headers(upstreamHeaders);\n headers.delete(\"content-encoding\");\n headers.delete(\"content-length\");\n headers.delete(\"server-timing\");\n return headers;\n}\n"],"mappings":"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"}
@@ -7,13 +7,17 @@ import { CallableRouteHandler, ShopifyRedirectStatus, ShopifyRouteError, Shopify
7
7
  *
8
8
  * Use paths other than `/api/cart`, which the cart server handlers use. Return a JSON, redirect, or error result from the handler. Hydrogen sends a JSON result with status `200`, an error result with status `400` by default, and a redirect result with status `303` by default. When a redirect result uses a status other than `301`, `302`, `303`, 307, or 308, the promise from `handleShopifyRoutes` rejects.
9
9
  *
10
+ * Annotate the handler's context parameter with only the fields the handler reads, for example
11
+ * `Omit<ShopifyRouteHandlerContext, "sessionManager">`, so apps without a session manager can
12
+ * register it.
13
+ *
10
14
  * @param pathname The exact URL pathname that the handler matches. Hydrogen doesn't match path parameters or prefixes.
11
15
  * @param method The HTTP method that the handler matches, in uppercase, such as `POST`.
12
16
  * @param handler The function that receives the request, its context, the session, and the Storefront API client, and returns a JSON, redirect, or error result.
13
17
  * @returns The handler to add to a handler object.
14
18
  * @publicDocs
15
19
  */
16
- declare function createShopifyRouteHandler<const TPathname extends string, const TMethod extends string>(pathname: TPathname, method: TMethod, handler: (context: ShopifyRouteHandlerContext) => Promise<ShopifyRouteHandlerResult>): ShopifyRouteHandler<TPathname, TMethod>;
20
+ declare function createShopifyRouteHandler<const TPathname extends string, const TMethod extends string, TContext extends Partial<ShopifyRouteHandlerContext> = ShopifyRouteHandlerContext>(pathname: TPathname, method: TMethod, handler: (context: TContext) => Promise<ShopifyRouteHandlerResult>): CallableRouteHandler<TContext, ShopifyRouteHandlerResult, TPathname, TMethod>;
17
21
  //#endregion
18
22
  export { createShopifyRouteHandler };
19
23
  //# sourceMappingURL=registered-routes.d.mts.map
@@ -16,6 +16,10 @@ const VALID_REDIRECT_STATUSES = [
16
16
  *
17
17
  * Use paths other than `/api/cart`, which the cart server handlers use. Return a JSON, redirect, or error result from the handler. Hydrogen sends a JSON result with status `200`, an error result with status `400` by default, and a redirect result with status `303` by default. When a redirect result uses a status other than `301`, `302`, `303`, 307, or 308, the promise from `handleShopifyRoutes` rejects.
18
18
  *
19
+ * Annotate the handler's context parameter with only the fields the handler reads, for example
20
+ * `Omit<ShopifyRouteHandlerContext, "sessionManager">`, so apps without a session manager can
21
+ * register it.
22
+ *
19
23
  * @param pathname The exact URL pathname that the handler matches. Hydrogen doesn't match path parameters or prefixes.
20
24
  * @param method The HTTP method that the handler matches, in uppercase, such as `POST`.
21
25
  * @param handler The function that receives the request, its context, the session, and the Storefront API client, and returns a JSON, redirect, or error result.
@@ -32,20 +36,30 @@ function createCallableRouteHandler(pathname, method, handler) {
32
36
  });
33
37
  }
34
38
  const handleShopifyRouteHandlers = (url, { request, sessionManager, storefrontClient, requestContext, handlers = [] }) => {
35
- const context = {
36
- request,
37
- sessionManager,
38
- storefrontClient,
39
- requestContext
40
- };
41
39
  const routeHandlers = handlers.flatMap((group) => Object.values(group));
42
40
  if (routeHandlers.length === 0) return null;
43
41
  const pathMatches = routeHandlers.filter((entry) => entry.pathname === url.pathname);
44
42
  if (pathMatches.length === 0) return null;
45
43
  const match = pathMatches.find((candidate) => candidate.method === request.method);
46
44
  if (!match) return Promise.resolve(new Response("Method Not Allowed", { status: HTTP_METHOD_NOT_ALLOWED_STATUS }));
47
- return match(context).then((result) => createShopifyRouteResponse(result, request));
45
+ return match({
46
+ request,
47
+ sessionManager: sessionManager ?? createMissingSessionManager(url.pathname),
48
+ storefrontClient,
49
+ requestContext
50
+ }).then((result) => createShopifyRouteResponse(result, request));
48
51
  };
52
+ function createMissingSessionManager(pathname) {
53
+ const fail = () => {
54
+ throw new Error(`The ${pathname} handler needs a session. Pass sessionManager to handleShopifyRoutes.`);
55
+ };
56
+ return {
57
+ getSessionOrigin: fail,
58
+ getSessionItem: fail,
59
+ setSessionItem: fail,
60
+ removeSessionItem: fail
61
+ };
62
+ }
49
63
  function createShopifyRouteResponse(result, request) {
50
64
  if (result.type === "redirect") {
51
65
  const headers = new Headers(result.headers);
@@ -1 +1 @@
1
- {"version":3,"file":"registered-routes.mjs","names":[],"sources":["../../../../src/core/request-routing/registered-routes.ts"],"sourcesContent":["import type {\n CallableRouteHandler,\n HydrogenRouteInterceptor,\n ShopifyRouteHandler,\n ShopifyRouteHandlerContext,\n ShopifyRouteHandlerResult,\n ShopifyRedirectStatus,\n} from \"./route-types\";\n\nexport type {\n CallableRouteHandler,\n ShopifyRouteError,\n ShopifyRouteErrorResult,\n ShopifyRouteHandler,\n ShopifyRouteHandlerContext,\n ShopifyRouteHandlerGroup,\n ShopifyRouteHandlerResult,\n ShopifyRouteJsonResult,\n ShopifyRouteRedirectResult,\n ShopifyRedirectStatus,\n ShopifyRouteSessionManager,\n} from \"./route-types\";\n\nconst HTTP_OK_STATUS = 200;\nconst HTTP_SEE_OTHER_STATUS = 303;\nconst HTTP_METHOD_NOT_ALLOWED_STATUS = 405;\nconst HTTP_BAD_REQUEST_STATUS = 400;\nconst VALID_REDIRECT_STATUSES = [\n 301, 302, 303, 307, 308,\n] as const satisfies readonly ShopifyRedirectStatus[];\n\n/**\n * Creates a custom endpoint in your app for one pathname and HTTP method. Collect handlers in an\n * object, and pass an array of handler objects to `handleShopifyRoutes` in its `handlers` option.\n *\n * Use paths other than `/api/cart`, which the cart server handlers use. Return a JSON, redirect, or error result from the handler. Hydrogen sends a JSON result with status `200`, an error result with status `400` by default, and a redirect result with status `303` by default. When a redirect result uses a status other than `301`, `302`, `303`, 307, or 308, the promise from `handleShopifyRoutes` rejects.\n *\n * @param pathname The exact URL pathname that the handler matches. Hydrogen doesn't match path parameters or prefixes.\n * @param method The HTTP method that the handler matches, in uppercase, such as `POST`.\n * @param handler The function that receives the request, its context, the session, and the Storefront API client, and returns a JSON, redirect, or error result.\n * @returns The handler to add to a handler object.\n * @publicDocs\n */\nexport function createShopifyRouteHandler<\n const TPathname extends string,\n const TMethod extends string,\n>(\n pathname: TPathname,\n method: TMethod,\n handler: (context: ShopifyRouteHandlerContext) => Promise<ShopifyRouteHandlerResult>,\n): ShopifyRouteHandler<TPathname, TMethod> {\n return createCallableRouteHandler(pathname, method, handler);\n}\n\nexport function createCallableRouteHandler<\n const TPathname extends string,\n const TMethod extends string,\n TContext,\n TResult,\n>(\n pathname: TPathname,\n method: TMethod,\n handler: (context: TContext) => Promise<TResult>,\n): CallableRouteHandler<TContext, TResult, TPathname, TMethod> {\n return Object.assign(handler, { pathname, method });\n}\n\nexport const handleShopifyRouteHandlers: HydrogenRouteInterceptor = (\n url,\n { request, sessionManager, storefrontClient, requestContext, handlers = [] },\n) => {\n const context = { request, sessionManager, storefrontClient, requestContext };\n const routeHandlers = handlers.flatMap((group) => Object.values(group));\n if (routeHandlers.length === 0) return null;\n\n const pathMatches = routeHandlers.filter((entry) => entry.pathname === url.pathname);\n if (pathMatches.length === 0) return null;\n\n const match = pathMatches.find((candidate) => candidate.method === request.method);\n if (!match)\n return Promise.resolve(\n new Response(\"Method Not Allowed\", { status: HTTP_METHOD_NOT_ALLOWED_STATUS }),\n );\n\n return match(context).then((result) => createShopifyRouteResponse(result, request));\n};\n\nfunction createShopifyRouteResponse(result: ShopifyRouteHandlerResult, request: Request): Response {\n if (result.type === \"redirect\") {\n const headers = new Headers(result.headers);\n headers.set(\"location\", resolveRedirectLocation(result.location, request));\n return new Response(null, {\n status: getRedirectStatus(result.status),\n headers,\n });\n }\n\n if (result.type === \"error\") {\n const headers = new Headers(result.headers);\n headers.set(\"content-type\", \"application/json\");\n return new Response(JSON.stringify({ error: result.error }), {\n status: result.status ?? HTTP_BAD_REQUEST_STATUS,\n headers,\n });\n }\n\n const headers = new Headers(result.headers);\n headers.set(\"content-type\", \"application/json\");\n return new Response(JSON.stringify(result.data), {\n status: HTTP_OK_STATUS,\n headers,\n });\n}\n\nfunction getRedirectStatus(status: ShopifyRedirectStatus | undefined): ShopifyRedirectStatus {\n const redirectStatus = status ?? HTTP_SEE_OTHER_STATUS;\n if (VALID_REDIRECT_STATUSES.some((validStatus) => validStatus === redirectStatus)) {\n return redirectStatus;\n }\n\n throw new Error(\n `Invalid Shopify route redirect status ${redirectStatus}. Expected one of: ${VALID_REDIRECT_STATUSES.join(\", \")}.`,\n );\n}\n\nfunction resolveRedirectLocation(location: string, request: Request): string {\n // Absolute Location headers are the best common denominator: browsers accept\n // relative redirects, but framework proxy runtimes like Next.js can require\n // absolute URLs when returning a Response from middleware/proxy code.\n return new URL(location, new URL(request.url).origin).toString();\n}\n"],"mappings":";AAuBA,MAAM,iBAAiB;AACvB,MAAM,wBAAwB;AAC9B,MAAM,iCAAiC;AACvC,MAAM,0BAA0B;AAChC,MAAM,0BAA0B;CAC9B;CAAK;CAAK;CAAK;CAAK;CACrB;;;;;;;;;;;;;AAcD,SAAgB,0BAId,UACA,QACA,SACyC;AACzC,QAAO,2BAA2B,UAAU,QAAQ,QAAQ;;AAG9D,SAAgB,2BAMd,UACA,QACA,SAC6D;AAC7D,QAAO,OAAO,OAAO,SAAS;EAAE;EAAU;EAAQ,CAAC;;AAGrD,MAAa,8BACX,KACA,EAAE,SAAS,gBAAgB,kBAAkB,gBAAgB,WAAW,EAAE,OACvE;CACH,MAAM,UAAU;EAAE;EAAS;EAAgB;EAAkB;EAAgB;CAC7E,MAAM,gBAAgB,SAAS,SAAS,UAAU,OAAO,OAAO,MAAM,CAAC;AACvE,KAAI,cAAc,WAAW,EAAG,QAAO;CAEvC,MAAM,cAAc,cAAc,QAAQ,UAAU,MAAM,aAAa,IAAI,SAAS;AACpF,KAAI,YAAY,WAAW,EAAG,QAAO;CAErC,MAAM,QAAQ,YAAY,MAAM,cAAc,UAAU,WAAW,QAAQ,OAAO;AAClF,KAAI,CAAC,MACH,QAAO,QAAQ,QACb,IAAI,SAAS,sBAAsB,EAAE,QAAQ,gCAAgC,CAAC,CAC/E;AAEH,QAAO,MAAM,QAAQ,CAAC,MAAM,WAAW,2BAA2B,QAAQ,QAAQ,CAAC;;AAGrF,SAAS,2BAA2B,QAAmC,SAA4B;AACjG,KAAI,OAAO,SAAS,YAAY;EAC9B,MAAM,UAAU,IAAI,QAAQ,OAAO,QAAQ;AAC3C,UAAQ,IAAI,YAAY,wBAAwB,OAAO,UAAU,QAAQ,CAAC;AAC1E,SAAO,IAAI,SAAS,MAAM;GACxB,QAAQ,kBAAkB,OAAO,OAAO;GACxC;GACD,CAAC;;AAGJ,KAAI,OAAO,SAAS,SAAS;EAC3B,MAAM,UAAU,IAAI,QAAQ,OAAO,QAAQ;AAC3C,UAAQ,IAAI,gBAAgB,mBAAmB;AAC/C,SAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,OAAO,OAAO,CAAC,EAAE;GAC3D,QAAQ,OAAO,UAAU;GACzB;GACD,CAAC;;CAGJ,MAAM,UAAU,IAAI,QAAQ,OAAO,QAAQ;AAC3C,SAAQ,IAAI,gBAAgB,mBAAmB;AAC/C,QAAO,IAAI,SAAS,KAAK,UAAU,OAAO,KAAK,EAAE;EAC/C,QAAQ;EACR;EACD,CAAC;;AAGJ,SAAS,kBAAkB,QAAkE;CAC3F,MAAM,iBAAiB,UAAU;AACjC,KAAI,wBAAwB,MAAM,gBAAgB,gBAAgB,eAAe,CAC/E,QAAO;AAGT,OAAM,IAAI,MACR,yCAAyC,eAAe,qBAAqB,wBAAwB,KAAK,KAAK,CAAC,GACjH;;AAGH,SAAS,wBAAwB,UAAkB,SAA0B;AAI3E,QAAO,IAAI,IAAI,UAAU,IAAI,IAAI,QAAQ,IAAI,CAAC,OAAO,CAAC,UAAU"}
1
+ {"version":3,"file":"registered-routes.mjs","names":[],"sources":["../../../../src/core/request-routing/registered-routes.ts"],"sourcesContent":["import type {\n CallableRouteHandler,\n HydrogenRouteInterceptor,\n ShopifyRouteHandlerContext,\n ShopifyRouteHandlerResult,\n ShopifyRedirectStatus,\n ShopifyRouteSessionManager,\n} from \"./route-types\";\n\nexport type {\n CallableRouteHandler,\n ShopifyRouteError,\n ShopifyRouteErrorResult,\n ShopifyRouteHandler,\n ShopifyRouteHandlerContext,\n ShopifyRouteHandlerGroup,\n ShopifyRouteHandlerResult,\n ShopifyRouteJsonResult,\n ShopifyRouteRedirectResult,\n ShopifyRedirectStatus,\n ShopifyRouteSessionManager,\n} from \"./route-types\";\n\nconst HTTP_OK_STATUS = 200;\nconst HTTP_SEE_OTHER_STATUS = 303;\nconst HTTP_METHOD_NOT_ALLOWED_STATUS = 405;\nconst HTTP_BAD_REQUEST_STATUS = 400;\nconst VALID_REDIRECT_STATUSES = [\n 301, 302, 303, 307, 308,\n] as const satisfies readonly ShopifyRedirectStatus[];\n\n/**\n * Creates a custom endpoint in your app for one pathname and HTTP method. Collect handlers in an\n * object, and pass an array of handler objects to `handleShopifyRoutes` in its `handlers` option.\n *\n * Use paths other than `/api/cart`, which the cart server handlers use. Return a JSON, redirect, or error result from the handler. Hydrogen sends a JSON result with status `200`, an error result with status `400` by default, and a redirect result with status `303` by default. When a redirect result uses a status other than `301`, `302`, `303`, 307, or 308, the promise from `handleShopifyRoutes` rejects.\n *\n * Annotate the handler's context parameter with only the fields the handler reads, for example\n * `Omit<ShopifyRouteHandlerContext, \"sessionManager\">`, so apps without a session manager can\n * register it.\n *\n * @param pathname The exact URL pathname that the handler matches. Hydrogen doesn't match path parameters or prefixes.\n * @param method The HTTP method that the handler matches, in uppercase, such as `POST`.\n * @param handler The function that receives the request, its context, the session, and the Storefront API client, and returns a JSON, redirect, or error result.\n * @returns The handler to add to a handler object.\n * @publicDocs\n */\nexport function createShopifyRouteHandler<\n const TPathname extends string,\n const TMethod extends string,\n TContext extends Partial<ShopifyRouteHandlerContext> = ShopifyRouteHandlerContext,\n>(\n pathname: TPathname,\n method: TMethod,\n handler: (context: TContext) => Promise<ShopifyRouteHandlerResult>,\n): CallableRouteHandler<TContext, ShopifyRouteHandlerResult, TPathname, TMethod> {\n return createCallableRouteHandler(pathname, method, handler);\n}\n\nexport function createCallableRouteHandler<\n const TPathname extends string,\n const TMethod extends string,\n TContext,\n TResult,\n>(\n pathname: TPathname,\n method: TMethod,\n handler: (context: TContext) => Promise<TResult>,\n): CallableRouteHandler<TContext, TResult, TPathname, TMethod> {\n return Object.assign(handler, { pathname, method });\n}\n\nexport const handleShopifyRouteHandlers: HydrogenRouteInterceptor = (\n url,\n { request, sessionManager, storefrontClient, requestContext, handlers = [] },\n) => {\n const routeHandlers = handlers.flatMap((group) => Object.values(group));\n if (routeHandlers.length === 0) return null;\n\n const pathMatches = routeHandlers.filter((entry) => entry.pathname === url.pathname);\n if (pathMatches.length === 0) return null;\n\n const match = pathMatches.find((candidate) => candidate.method === request.method);\n if (!match)\n return Promise.resolve(\n new Response(\"Method Not Allowed\", { status: HTTP_METHOD_NOT_ALLOWED_STATUS }),\n );\n\n const context = {\n request,\n sessionManager: sessionManager ?? createMissingSessionManager(url.pathname),\n storefrontClient,\n requestContext,\n };\n return match(context).then((result) => createShopifyRouteResponse(result, request));\n};\n\n// The types already require `sessionManager` for handlers that read it; this\n// covers untyped callers, which would otherwise read `undefined` deep inside a handler.\nfunction createMissingSessionManager(pathname: string): ShopifyRouteSessionManager {\n const fail = (): never => {\n throw new Error(\n `The ${pathname} handler needs a session. Pass sessionManager to handleShopifyRoutes.`,\n );\n };\n return {\n getSessionOrigin: fail,\n getSessionItem: fail,\n setSessionItem: fail,\n removeSessionItem: fail,\n };\n}\n\nfunction createShopifyRouteResponse(result: ShopifyRouteHandlerResult, request: Request): Response {\n if (result.type === \"redirect\") {\n const headers = new Headers(result.headers);\n headers.set(\"location\", resolveRedirectLocation(result.location, request));\n return new Response(null, {\n status: getRedirectStatus(result.status),\n headers,\n });\n }\n\n if (result.type === \"error\") {\n const headers = new Headers(result.headers);\n headers.set(\"content-type\", \"application/json\");\n return new Response(JSON.stringify({ error: result.error }), {\n status: result.status ?? HTTP_BAD_REQUEST_STATUS,\n headers,\n });\n }\n\n const headers = new Headers(result.headers);\n headers.set(\"content-type\", \"application/json\");\n return new Response(JSON.stringify(result.data), {\n status: HTTP_OK_STATUS,\n headers,\n });\n}\n\nfunction getRedirectStatus(status: ShopifyRedirectStatus | undefined): ShopifyRedirectStatus {\n const redirectStatus = status ?? HTTP_SEE_OTHER_STATUS;\n if (VALID_REDIRECT_STATUSES.some((validStatus) => validStatus === redirectStatus)) {\n return redirectStatus;\n }\n\n throw new Error(\n `Invalid Shopify route redirect status ${redirectStatus}. Expected one of: ${VALID_REDIRECT_STATUSES.join(\", \")}.`,\n );\n}\n\nfunction resolveRedirectLocation(location: string, request: Request): string {\n // Absolute Location headers are the best common denominator: browsers accept\n // relative redirects, but framework proxy runtimes like Next.js can require\n // absolute URLs when returning a Response from middleware/proxy code.\n return new URL(location, new URL(request.url).origin).toString();\n}\n"],"mappings":";AAuBA,MAAM,iBAAiB;AACvB,MAAM,wBAAwB;AAC9B,MAAM,iCAAiC;AACvC,MAAM,0BAA0B;AAChC,MAAM,0BAA0B;CAC9B;CAAK;CAAK;CAAK;CAAK;CACrB;;;;;;;;;;;;;;;;;AAkBD,SAAgB,0BAKd,UACA,QACA,SAC+E;AAC/E,QAAO,2BAA2B,UAAU,QAAQ,QAAQ;;AAG9D,SAAgB,2BAMd,UACA,QACA,SAC6D;AAC7D,QAAO,OAAO,OAAO,SAAS;EAAE;EAAU;EAAQ,CAAC;;AAGrD,MAAa,8BACX,KACA,EAAE,SAAS,gBAAgB,kBAAkB,gBAAgB,WAAW,EAAE,OACvE;CACH,MAAM,gBAAgB,SAAS,SAAS,UAAU,OAAO,OAAO,MAAM,CAAC;AACvE,KAAI,cAAc,WAAW,EAAG,QAAO;CAEvC,MAAM,cAAc,cAAc,QAAQ,UAAU,MAAM,aAAa,IAAI,SAAS;AACpF,KAAI,YAAY,WAAW,EAAG,QAAO;CAErC,MAAM,QAAQ,YAAY,MAAM,cAAc,UAAU,WAAW,QAAQ,OAAO;AAClF,KAAI,CAAC,MACH,QAAO,QAAQ,QACb,IAAI,SAAS,sBAAsB,EAAE,QAAQ,gCAAgC,CAAC,CAC/E;AAQH,QAAO,MAAM;EALX;EACA,gBAAgB,kBAAkB,4BAA4B,IAAI,SAAS;EAC3E;EACA;EAEkB,CAAC,CAAC,MAAM,WAAW,2BAA2B,QAAQ,QAAQ,CAAC;;AAKrF,SAAS,4BAA4B,UAA8C;CACjF,MAAM,aAAoB;AACxB,QAAM,IAAI,MACR,OAAO,SAAS,uEACjB;;AAEH,QAAO;EACL,kBAAkB;EAClB,gBAAgB;EAChB,gBAAgB;EAChB,mBAAmB;EACpB;;AAGH,SAAS,2BAA2B,QAAmC,SAA4B;AACjG,KAAI,OAAO,SAAS,YAAY;EAC9B,MAAM,UAAU,IAAI,QAAQ,OAAO,QAAQ;AAC3C,UAAQ,IAAI,YAAY,wBAAwB,OAAO,UAAU,QAAQ,CAAC;AAC1E,SAAO,IAAI,SAAS,MAAM;GACxB,QAAQ,kBAAkB,OAAO,OAAO;GACxC;GACD,CAAC;;AAGJ,KAAI,OAAO,SAAS,SAAS;EAC3B,MAAM,UAAU,IAAI,QAAQ,OAAO,QAAQ;AAC3C,UAAQ,IAAI,gBAAgB,mBAAmB;AAC/C,SAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,OAAO,OAAO,CAAC,EAAE;GAC3D,QAAQ,OAAO,UAAU;GACzB;GACD,CAAC;;CAGJ,MAAM,UAAU,IAAI,QAAQ,OAAO,QAAQ;AAC3C,SAAQ,IAAI,gBAAgB,mBAAmB;AAC/C,QAAO,IAAI,SAAS,KAAK,UAAU,OAAO,KAAK,EAAE;EAC/C,QAAQ;EACR;EACD,CAAC;;AAGJ,SAAS,kBAAkB,QAAkE;CAC3F,MAAM,iBAAiB,UAAU;AACjC,KAAI,wBAAwB,MAAM,gBAAgB,gBAAgB,eAAe,CAC/E,QAAO;AAGT,OAAM,IAAI,MACR,yCAAyC,eAAe,qBAAqB,wBAAwB,KAAK,KAAK,CAAC,GACjH;;AAGH,SAAS,wBAAwB,UAAkB,SAA0B;AAI3E,QAAO,IAAI,IAAI,UAAU,IAAI,IAAI,QAAQ,IAAI,CAAC,OAAO,CAAC,UAAU"}
@@ -66,16 +66,26 @@ type ShopifyRouteHandler<TPathname extends string = string, TMethod extends stri
66
66
  * @publicDocs
67
67
  */
68
68
  type ShopifyRouteHandlerGroup = Record<string, ShopifyRouteHandler>;
69
- /** The request, its context and session, the Storefront API client, your route templates, and your handler groups. */
70
- interface HydrogenRoutesOptions extends ShopifyRouteHandlerContext {
69
+ type ShopifyRouteHandlerGroups = readonly ShopifyRouteHandlerGroup[];
70
+ type HandlerContext<THandler> = THandler extends ((context: infer TContext) => unknown) ? TContext : never;
71
+ type RouteHandlerContext<TGroup> = TGroup extends unknown ? HandlerContext<TGroup[Extract<keyof TGroup, string | number>]> : never;
72
+ type RequiresSessionManager<TContext> = TContext extends unknown ? "sessionManager" extends keyof TContext ? true : false : never;
73
+ type SessionManagerOption<THandlers extends ShopifyRouteHandlerGroups | undefined> = true extends RequiresSessionManager<RouteHandlerContext<NonNullable<THandlers>[number]>> ? {
74
+ sessionManager: ShopifyRouteSessionManager;
75
+ } : {
76
+ sessionManager?: ShopifyRouteSessionManager;
77
+ };
78
+ type HydrogenRoutesBaseOptions = Omit<ShopifyRouteHandlerContext, "sessionManager"> & {
71
79
  /**
72
80
  * Your app's route templates. Hydrogen redirects Liquid-style `?variant=` URLs on your product paths and on Shopify's default product paths.
73
81
  */
74
82
  routeTemplates?: ShopifyRouteTemplates;
75
- /** The handler groups for your app's custom endpoints. */
76
- handlers?: readonly ShopifyRouteHandlerGroup[];
77
- }
78
- type HydrogenRouteHandler<TExtraOptions extends object = object> = (options: HydrogenRoutesOptions & TExtraOptions) => null | Promise<Response>;
83
+ };
84
+ /** The request, its context, the Storefront API client, your route templates, your handler groups, and the session when a handler needs it. */
85
+ type HydrogenRoutesOptions<THandlers extends ShopifyRouteHandlerGroups | undefined = ShopifyRouteHandlerGroups> = HydrogenRoutesBaseOptions & {
86
+ /** The handler groups for your app's custom endpoints. */handlers?: THandlers;
87
+ } & SessionManagerOption<THandlers>;
88
+ type HydrogenRouteHandler<TExtraOptions extends object = object> = <THandlers extends ShopifyRouteHandlerGroups | undefined = readonly []>(options: HydrogenRoutesOptions<THandlers> & TExtraOptions) => null | Promise<Response>;
79
89
  //#endregion
80
90
  export { CallableRouteHandler, HydrogenRouteHandler, ShopifyRedirectStatus, ShopifyRouteError, ShopifyRouteErrorResult, ShopifyRouteHandler, ShopifyRouteHandlerContext, ShopifyRouteHandlerGroup, ShopifyRouteHandlerResult, ShopifyRouteJsonResult, ShopifyRouteRedirectResult, ShopifyRouteSessionManager };
81
91
  //# sourceMappingURL=route-types.d.mts.map
@@ -1,8 +1,10 @@
1
+ import { SHOPIFY_API_PROXY_PREFIX } from "../url.mjs";
1
2
  import { SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT_ID } from "./constants.mjs";
2
3
 
3
4
  //#region src/core/shopify-scripts/perfkit.ts
4
5
  const SHOPIFY_SHOP_ID_PATTERN = /^\d+$/;
5
- function getPerfKitScript(shop, extraAttributes) {
6
+ const SHS_BEACON_ENDPOINT = `${SHOPIFY_API_PROXY_PREFIX}/api/collect`;
7
+ function getPerfKitScript(shop, extraAttributes, { shs = false } = {}) {
6
8
  if (!shop.storefrontId) return;
7
9
  const shopId = parseNumericShopId(shop.shopId);
8
10
  if (!shopId) return;
@@ -18,7 +20,11 @@ function getPerfKitScript(shop, extraAttributes) {
18
20
  "data-storefront-id": shop.storefrontId,
19
21
  "data-monorail-region": "global",
20
22
  "data-spa-mode": "true",
21
- "data-resource-timing-sampling-rate": "10"
23
+ "data-resource-timing-sampling-rate": "10",
24
+ ...shs ? {
25
+ "data-shs": "true",
26
+ "data-shs-beacon-endpoint": SHS_BEACON_ENDPOINT
27
+ } : {}
22
28
  }
23
29
  };
24
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"perfkit.mjs","names":[],"sources":["../../../../src/core/shopify-scripts/perfkit.ts"],"sourcesContent":["import { SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT_ID } from \"./constants\";\nimport type {\n ShopifyScriptDescriptor,\n ShopifyScriptsShop,\n ShopifyScriptTagAttributes,\n} from \"./types\";\n\nconst SHOPIFY_SHOP_ID_PATTERN = /^\\d+$/;\n\nexport function getPerfKitScript(\n shop: ShopifyScriptsShop,\n extraAttributes?: Pick<ShopifyScriptTagAttributes, \"nonce\">,\n): ShopifyScriptDescriptor | undefined {\n if (!shop.storefrontId) return;\n\n const shopId = parseNumericShopId(shop.shopId);\n if (!shopId) return;\n\n return {\n tagName: \"script\",\n attributes: {\n id: SHOPIFY_PERF_KIT_SCRIPT_ID,\n ...extraAttributes,\n async: true,\n src: SHOPIFY_PERF_KIT_SCRIPT,\n \"data-application\": \"hydrogen\",\n \"data-shop-id\": shopId,\n \"data-storefront-id\": shop.storefrontId,\n \"data-monorail-region\": \"global\",\n \"data-spa-mode\": \"true\",\n \"data-resource-timing-sampling-rate\": \"10\",\n },\n };\n}\n\nfunction parseNumericShopId(shopId: ShopifyScriptsShop[\"shopId\"]): string | undefined {\n const parsedShopId = shopId.split(\"/\").pop();\n if (!parsedShopId) return;\n\n return SHOPIFY_SHOP_ID_PATTERN.test(parsedShopId) ? parsedShopId : undefined;\n}\n"],"mappings":";;;AAOA,MAAM,0BAA0B;AAEhC,SAAgB,iBACd,MACA,iBACqC;AACrC,KAAI,CAAC,KAAK,aAAc;CAExB,MAAM,SAAS,mBAAmB,KAAK,OAAO;AAC9C,KAAI,CAAC,OAAQ;AAEb,QAAO;EACL,SAAS;EACT,YAAY;GACV,IAAI;GACJ,GAAG;GACH,OAAO;GACP,KAAK;GACL,oBAAoB;GACpB,gBAAgB;GAChB,sBAAsB,KAAK;GAC3B,wBAAwB;GACxB,iBAAiB;GACjB,sCAAsC;GACvC;EACF;;AAGH,SAAS,mBAAmB,QAA0D;CACpF,MAAM,eAAe,OAAO,MAAM,IAAI,CAAC,KAAK;AAC5C,KAAI,CAAC,aAAc;AAEnB,QAAO,wBAAwB,KAAK,aAAa,GAAG,eAAe"}
1
+ {"version":3,"file":"perfkit.mjs","names":[],"sources":["../../../../src/core/shopify-scripts/perfkit.ts"],"sourcesContent":["import { SHOPIFY_API_PROXY_PREFIX } from \"../url\";\nimport { SHOPIFY_PERF_KIT_SCRIPT, SHOPIFY_PERF_KIT_SCRIPT_ID } from \"./constants\";\nimport type {\n ShopifyScriptDescriptor,\n ShopifyScriptsShop,\n ShopifyScriptTagAttributes,\n} from \"./types\";\n\nconst SHOPIFY_SHOP_ID_PATTERN = /^\\d+$/;\nconst SHS_BEACON_ENDPOINT = `${SHOPIFY_API_PROXY_PREFIX}/api/collect`;\n\ntype PerfKitScriptOptions = {\n shs?: boolean;\n};\n\nexport function getPerfKitScript(\n shop: ShopifyScriptsShop,\n extraAttributes?: Pick<ShopifyScriptTagAttributes, \"nonce\">,\n { shs = !__DEV__ }: PerfKitScriptOptions = {},\n): ShopifyScriptDescriptor | undefined {\n if (!shop.storefrontId) return;\n\n const shopId = parseNumericShopId(shop.shopId);\n if (!shopId) return;\n\n return {\n tagName: \"script\",\n attributes: {\n id: SHOPIFY_PERF_KIT_SCRIPT_ID,\n ...extraAttributes,\n async: true,\n src: SHOPIFY_PERF_KIT_SCRIPT,\n \"data-application\": \"hydrogen\",\n \"data-shop-id\": shopId,\n \"data-storefront-id\": shop.storefrontId,\n \"data-monorail-region\": \"global\",\n \"data-spa-mode\": \"true\",\n \"data-resource-timing-sampling-rate\": \"10\",\n ...(shs\n ? {\n \"data-shs\": \"true\",\n \"data-shs-beacon-endpoint\": SHS_BEACON_ENDPOINT,\n }\n : {}),\n },\n };\n}\n\nfunction parseNumericShopId(shopId: ShopifyScriptsShop[\"shopId\"]): string | undefined {\n const parsedShopId = shopId.split(\"/\").pop();\n if (!parsedShopId) return;\n\n return SHOPIFY_SHOP_ID_PATTERN.test(parsedShopId) ? parsedShopId : undefined;\n}\n"],"mappings":";;;;AAQA,MAAM,0BAA0B;AAChC,MAAM,sBAAsB,GAAG,yBAAyB;AAMxD,SAAgB,iBACd,MACA,iBACA,EAAE,MAAM,UAAmC,EAAE,EACR;AACrC,KAAI,CAAC,KAAK,aAAc;CAExB,MAAM,SAAS,mBAAmB,KAAK,OAAO;AAC9C,KAAI,CAAC,OAAQ;AAEb,QAAO;EACL,SAAS;EACT,YAAY;GACV,IAAI;GACJ,GAAG;GACH,OAAO;GACP,KAAK;GACL,oBAAoB;GACpB,gBAAgB;GAChB,sBAAsB,KAAK;GAC3B,wBAAwB;GACxB,iBAAiB;GACjB,sCAAsC;GACtC,GAAI,MACA;IACE,YAAY;IACZ,4BAA4B;IAC7B,GACD,EAAE;GACP;EACF;;AAGH,SAAS,mBAAmB,QAA0D;CACpF,MAAM,eAAe,OAAO,MAAM,IAAI,CAAC,KAAK;AAC5C,KAAI,CAAC,aAAc;AAEnB,QAAO,wBAAwB,KAAK,aAAa,GAAG,eAAe"}
@@ -10,6 +10,12 @@ function loadScript(src, options) {
10
10
  const promise = new Promise((resolve, reject) => {
11
11
  const script = document.createElement("script");
12
12
  script.type = options?.module ? "module" : "text/javascript";
13
+ if (options?.attributes) {
14
+ const { attributes } = options;
15
+ Object.keys(attributes).forEach((key) => {
16
+ script.setAttribute(key, attributes[key]);
17
+ });
18
+ }
13
19
  script.src = src;
14
20
  script.addEventListener("load", () => resolve(true));
15
21
  script.addEventListener("error", () => {
@@ -18,12 +24,6 @@ function loadScript(src, options) {
18
24
  });
19
25
  if (options?.in === "head") document.head.appendChild(script);
20
26
  else document.body.appendChild(script);
21
- if (options?.attributes) {
22
- const { attributes } = options;
23
- Object.keys(attributes).forEach((key) => {
24
- script.setAttribute(key, attributes[key]);
25
- });
26
- }
27
27
  });
28
28
  SCRIPTS_LOADED[src] = promise;
29
29
  return promise;
@@ -1 +1 @@
1
- {"version":3,"file":"load-script.mjs","names":[],"sources":["../../../../src/core/utils/load-script.ts"],"sourcesContent":["/**\n * Inlined from @shopify/hydrogen-react/load-script.\n * Pure DOM script loader with no framework dependency.\n */\n\nconst SCRIPTS_LOADED: Record<string, Promise<boolean>> = {};\n\ntype LoadScriptOptions = {\n module?: boolean;\n in?: \"head\" | \"body\";\n attributes?: Record<string, string>;\n};\n\nexport function loadScript(src: string, options?: LoadScriptOptions): Promise<boolean> {\n const isScriptLoaded = SCRIPTS_LOADED[src];\n if (isScriptLoaded) return isScriptLoaded;\n\n const promise = new Promise<boolean>((resolve, reject) => {\n const script = document.createElement(\"script\");\n script.type = options?.module ? \"module\" : \"text/javascript\";\n script.src = src;\n script.addEventListener(\"load\", () => resolve(true));\n script.addEventListener(\"error\", () => {\n delete SCRIPTS_LOADED[src];\n reject(false);\n });\n\n if (options?.in === \"head\") {\n document.head.appendChild(script);\n } else {\n document.body.appendChild(script);\n }\n\n if (options?.attributes) {\n const { attributes } = options;\n Object.keys(attributes).forEach((key) => {\n script.setAttribute(key, attributes[key]);\n });\n }\n });\n\n SCRIPTS_LOADED[src] = promise;\n return promise;\n}\n"],"mappings":";;;;;AAKA,MAAM,iBAAmD,EAAE;AAQ3D,SAAgB,WAAW,KAAa,SAA+C;CACrF,MAAM,iBAAiB,eAAe;AACtC,KAAI,eAAgB,QAAO;CAE3B,MAAM,UAAU,IAAI,SAAkB,SAAS,WAAW;EACxD,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,SAAO,OAAO,SAAS,SAAS,WAAW;AAC3C,SAAO,MAAM;AACb,SAAO,iBAAiB,cAAc,QAAQ,KAAK,CAAC;AACpD,SAAO,iBAAiB,eAAe;AACrC,UAAO,eAAe;AACtB,UAAO,MAAM;IACb;AAEF,MAAI,SAAS,OAAO,OAClB,UAAS,KAAK,YAAY,OAAO;MAEjC,UAAS,KAAK,YAAY,OAAO;AAGnC,MAAI,SAAS,YAAY;GACvB,MAAM,EAAE,eAAe;AACvB,UAAO,KAAK,WAAW,CAAC,SAAS,QAAQ;AACvC,WAAO,aAAa,KAAK,WAAW,KAAK;KACzC;;GAEJ;AAEF,gBAAe,OAAO;AACtB,QAAO"}
1
+ {"version":3,"file":"load-script.mjs","names":[],"sources":["../../../../src/core/utils/load-script.ts"],"sourcesContent":["/**\n * Inlined from @shopify/hydrogen-react/load-script.\n * Pure DOM script loader with no framework dependency.\n */\n\nconst SCRIPTS_LOADED: Record<string, Promise<boolean>> = {};\n\ntype LoadScriptOptions = {\n module?: boolean;\n in?: \"head\" | \"body\";\n attributes?: Record<string, string>;\n};\n\nexport function loadScript(src: string, options?: LoadScriptOptions): Promise<boolean> {\n const isScriptLoaded = SCRIPTS_LOADED[src];\n if (isScriptLoaded) return isScriptLoaded;\n\n const promise = new Promise<boolean>((resolve, reject) => {\n const script = document.createElement(\"script\");\n script.type = options?.module ? \"module\" : \"text/javascript\";\n\n if (options?.attributes) {\n const { attributes } = options;\n Object.keys(attributes).forEach((key) => {\n script.setAttribute(key, attributes[key]);\n });\n }\n\n script.src = src;\n script.addEventListener(\"load\", () => resolve(true));\n script.addEventListener(\"error\", () => {\n delete SCRIPTS_LOADED[src];\n reject(false);\n });\n\n if (options?.in === \"head\") {\n document.head.appendChild(script);\n } else {\n document.body.appendChild(script);\n }\n });\n\n SCRIPTS_LOADED[src] = promise;\n return promise;\n}\n"],"mappings":";;;;;AAKA,MAAM,iBAAmD,EAAE;AAQ3D,SAAgB,WAAW,KAAa,SAA+C;CACrF,MAAM,iBAAiB,eAAe;AACtC,KAAI,eAAgB,QAAO;CAE3B,MAAM,UAAU,IAAI,SAAkB,SAAS,WAAW;EACxD,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,SAAO,OAAO,SAAS,SAAS,WAAW;AAE3C,MAAI,SAAS,YAAY;GACvB,MAAM,EAAE,eAAe;AACvB,UAAO,KAAK,WAAW,CAAC,SAAS,QAAQ;AACvC,WAAO,aAAa,KAAK,WAAW,KAAK;KACzC;;AAGJ,SAAO,MAAM;AACb,SAAO,iBAAiB,cAAc,QAAQ,KAAK,CAAC;AACpD,SAAO,iBAAiB,eAAe;AACrC,UAAO,eAAe;AACtB,UAAO,MAAM;IACb;AAEF,MAAI,SAAS,OAAO,OAClB,UAAS,KAAK,YAAY,OAAO;MAEjC,UAAS,KAAK,YAAY,OAAO;GAEnC;AAEF,gBAAe,OAAO;AACtB,QAAO"}
@@ -245,6 +245,32 @@ type InferResult<T extends string> = InferResultForSchema<T, StorefrontSchema>;
245
245
  type InferVariables<T extends string> = InferVariablesForSchema<T, StorefrontSchema>;
246
246
  /** The operation kind of a query string, such as `query`. Resolves to `unknown` unless the query text has exactly one operation. */
247
247
  type InferOperationKind<T extends string> = parseDocument<T> extends infer Doc ? [Doc] extends [never] ? "unknown" : OperationKindType<Doc> : "unknown";
248
+ type DeclaredNameOfType<Definition, Types> = Definition extends {
249
+ variable: {
250
+ name: {
251
+ value: infer Name extends keyof Types;
252
+ };
253
+ };
254
+ type: infer Type;
255
+ } ? (Type extends {
256
+ kind: "NonNullType";
257
+ type: infer Inner;
258
+ } ? Inner : Type) extends {
259
+ kind: "NamedType";
260
+ name: {
261
+ value: Types[Name];
262
+ };
263
+ } ? Name : never : never;
264
+ /**
265
+ * Names of the first operation's variables that are declared with the named
266
+ * type `Types` maps them to, nullable or non-null. List types never match.
267
+ */
268
+ type InferVariableNamesDeclaredAs<T extends string, Types extends Record<string, string>> = parseDocument<T> extends infer Doc ? Doc extends {
269
+ kind: "Document";
270
+ definitions: infer Defs extends readonly unknown[];
271
+ } ? FirstOperation<Defs> extends {
272
+ variableDefinitions: infer Vars extends readonly unknown[];
273
+ } ? DeclaredNameOfType<Vars[number], Types> : never : never : never;
248
274
  //#endregion
249
- export { InferOperationKind, InferResult, InferVariables };
275
+ export { InferOperationKind, InferResult, InferVariableNamesDeclaredAs, InferVariables };
250
276
  //# sourceMappingURL=type-resolver.d.mts.map
@@ -50,9 +50,7 @@ function createCartComponents() {
50
50
  return useOptionalCart((state) => selector(state), isEqual);
51
51
  }
52
52
  function useSuspenseCart(selector, isEqual) {
53
- const readyPromise = useCart((state) => state.readyPromise);
54
- if (readyPromise) throw readyPromise;
55
- return useCart(selector, isEqual);
53
+ return useSuspenseCartSelector(selector, isEqual);
56
54
  }
57
55
  return {
58
56
  CartProvider: TypedCartProvider,
@@ -64,11 +62,14 @@ function createCartComponents() {
64
62
  };
65
63
  }
66
64
  function useCartStore(hookName = "useCart") {
67
- const store = useContext(CartContext);
68
- if (!store) throw new Error(`${hookName} must be used inside <CartProvider>.`);
69
- return store;
65
+ return useCartContext(hookName).store;
70
66
  }
71
- function useOptionalCartStore() {
67
+ function useCartContext(hookName = "useCart") {
68
+ const context = useContext(CartContext);
69
+ if (!context) throw new Error(`${hookName} must be used inside <CartProvider>.`);
70
+ return context;
71
+ }
72
+ function useOptionalCartContext() {
72
73
  return useContext(CartContext);
73
74
  }
74
75
  /**
@@ -88,7 +89,14 @@ function useOptionalCartStore() {
88
89
  * @publicDocs
89
90
  */
90
91
  function CartProvider({ initialData, children }) {
91
- const store = useMemo(() => createCartStore({ initialData }), []);
92
+ const context = useMemo(() => {
93
+ const store = createCartStore({ initialData });
94
+ return {
95
+ store,
96
+ hydrationSnapshot: store.getState()
97
+ };
98
+ }, []);
99
+ const { store } = context;
92
100
  useEffect(() => {
93
101
  configureCartEndpoint(cartEndpoint);
94
102
  store.connect();
@@ -97,7 +105,7 @@ function CartProvider({ initialData, children }) {
97
105
  };
98
106
  }, [store]);
99
107
  return /* @__PURE__ */ jsx(CartContext.Provider, {
100
- value: store,
108
+ value: context,
101
109
  children
102
110
  });
103
111
  }
@@ -127,7 +135,8 @@ function CartProvider({ initialData, children }) {
127
135
  * @publicDocs
128
136
  */
129
137
  function useCart(selector, isEqual) {
130
- return useCartSelector(useCartStore(), selector, isEqual);
138
+ const { store, hydrationSnapshot } = useCartContext();
139
+ return useCartSelector(store, hydrationSnapshot, selector, isEqual);
131
140
  }
132
141
  /**
133
142
  * Returns the cart action that reloads the cart after a cart change outside cart forms.
@@ -194,32 +203,77 @@ function useCartAnalytics() {
194
203
  * @publicDocs
195
204
  */
196
205
  function useOptionalCart(selector, isEqual) {
197
- const store = useOptionalCartStore();
198
- if (!store) return void 0;
199
- return useCartSelector(store, selector, isEqual);
206
+ const context = useOptionalCartContext();
207
+ if (!context) return void 0;
208
+ return useCartSelector(context.store, context.hydrationSnapshot, selector, isEqual);
200
209
  }
201
- function useCartSelector(store, selector, isEqual) {
202
- const cachedRef = useRef(null);
210
+ function useCartSelector(store, hydrationSnapshot, selector, isEqual) {
211
+ const liveCacheRef = useRef(null);
212
+ const hydrationCacheRef = useRef(null);
213
+ const getSnapshot = () => selectCartSnapshot(store.getState(), selector, isEqual, liveCacheRef);
214
+ const getServerSnapshot = () => selectCartSnapshot(hydrationSnapshot, selector, isEqual, hydrationCacheRef);
215
+ return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);
216
+ }
217
+ function selectCartSnapshot(state, selector, isEqual, cacheRef) {
218
+ if (cacheRef.current?.state === state && cacheRef.current.selector === selector) return cacheRef.current.value;
219
+ const next = selector(state);
220
+ if (cacheRef.current && isEqual?.(cacheRef.current.value, next)) {
221
+ cacheRef.current = {
222
+ state,
223
+ selector,
224
+ value: cacheRef.current.value
225
+ };
226
+ return cacheRef.current.value;
227
+ }
228
+ cacheRef.current = {
229
+ state,
230
+ selector,
231
+ value: next
232
+ };
233
+ return next;
234
+ }
235
+ function useSuspenseCartSelector(selector, isEqual) {
236
+ const { store } = useCartContext();
237
+ const cacheRef = useRef(null);
203
238
  const getSnapshot = () => {
204
239
  const state = store.getState();
205
- if (cachedRef.current && cachedRef.current.state === state && cachedRef.current.selector === selector) return cachedRef.current.value;
206
- const next = selector(state);
207
- if (cachedRef.current && isEqual?.(cachedRef.current.value, next)) {
208
- cachedRef.current = {
240
+ if (cacheRef.current?.state === state && cacheRef.current.selector === selector) return cacheRef.current.snapshot;
241
+ if (state.readyPromise) {
242
+ const snapshot = {
243
+ status: "pending",
244
+ promise: state.readyPromise
245
+ };
246
+ cacheRef.current = {
209
247
  state,
210
248
  selector,
211
- value: cachedRef.current.value
249
+ snapshot
212
250
  };
213
- return cachedRef.current.value;
251
+ return snapshot;
214
252
  }
215
- cachedRef.current = {
253
+ const value = selector(state);
254
+ const previous = cacheRef.current?.snapshot;
255
+ if (previous?.status === "ready" && isEqual?.(previous.value, value)) {
256
+ cacheRef.current = {
257
+ state,
258
+ selector,
259
+ snapshot: previous
260
+ };
261
+ return previous;
262
+ }
263
+ const snapshot = {
264
+ status: "ready",
265
+ value
266
+ };
267
+ cacheRef.current = {
216
268
  state,
217
269
  selector,
218
- value: next
270
+ snapshot
219
271
  };
220
- return next;
272
+ return snapshot;
221
273
  };
222
- return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
274
+ const snapshot = useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
275
+ if (snapshot.status === "pending") throw snapshot.promise;
276
+ return snapshot.value;
223
277
  }
224
278
  /**
225
279
  * Returns form props and a field register function for building cart forms.
@@ -1 +1 @@
1
- {"version":3,"file":"cart.mjs","names":[],"sources":["../../../src/react/cart.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type FormHTMLAttributes,\n type ReactNode,\n type RefCallback,\n type SubmitEvent,\n} from \"react\";\n\nimport { trackCartAnalytics } from \"../core/analytics/cart-tracker\";\nimport { attachQuantityInput } from \"../core/cart/attach-quantity-input\";\nimport {\n configureCartEndpoint as configureCoreCartEndpoint,\n createCartStore,\n type CreateCartStoreOptions,\n type CartActions,\n type CartStore,\n} from \"../core/cart/cart\";\nimport { createCartFormRegister, type CartFormRegister } from \"../core/cart/form\";\nimport type { CartDataFromHandlers } from \"../core/cart/server-handlers\";\nimport { type CartData, type CartState } from \"../core/cart/state\";\n\nconst DEFAULT_CART_ENDPOINT = \"/api/cart\";\n\nlet cartEndpoint = DEFAULT_CART_ENDPOINT;\n\nconst CartContext = createContext<CartStore | null>(null);\n\nexport function configureCartEndpoint(endpoint: string): void {\n cartEndpoint = endpoint;\n configureCoreCartEndpoint(endpoint);\n}\n\nexport function getCartEndpoint(): string {\n return cartEndpoint;\n}\n\n/**\n * Props for the cart provider that `createCartComponents` returns.\n */\ntype TypedCartProviderProps<TData extends CartData> = {\n /**\n * The `data` property of the cart GET handler's result, or a promise of that data. Without initial data, the store loads the cart from `/api/cart` after the provider mounts, and the server-rendered page shows an empty cart. The provider creates its store from the first value and ignores later changes.\n */\n initialData?: CartInitialData<TData>;\n /** Content that can read the cart through the cart hooks. */\n children?: ReactNode;\n};\n\ntype CartInitialData<TData extends CartData = CartData> =\n CreateCartStoreOptions<TData>[\"initialData\"];\n\nexport type { CartActions };\n\n/** A hook that returns the slice of cart state that your selector picks. */\ntype CartStateHook<TData extends CartData> =\n /**\n * @param selector - Picks the value to return from the cart state.\n * @param isEqual - Compares the previous and next selected values. Return `true` to skip the re-render. Without the function, the hook compares the values by reference.\n * @returns The selected value.\n */\n <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S;\n\n/** A hook that returns the slice of cart state that your selector picks, or `undefined` outside the cart provider. */\ntype OptionalCartStateHook<TData extends CartData> =\n /**\n * @param selector - Picks the value to return from the cart state.\n * @param isEqual - Compares the previous and next selected values. Return `true` to skip the re-render. Without the function, the hook compares the values by reference.\n * @returns The selected value, or `undefined` outside the cart provider.\n */\n <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S | undefined;\n\n/** Callbacks that run around a cart form submission. */\ninterface CartFormPropsOptions {\n /** Runs before the cart store handles the submission. Call `event.preventDefault()` to skip the cart submission. */\n beforeSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n /** Runs as soon as the cart change starts, before the server responds. */\n afterSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n}\n\n/** The form props function and the register function for a cart form. */\ninterface CartFormBindings {\n /** Returns the `method`, `action`, and `onSubmit` props to spread on the form. The form posts to the cart endpoint, and the cart store handles each submission. */\n formProps: (opts?: CartFormPropsOptions) => FormHTMLAttributes<HTMLFormElement>;\n /** Returns the attributes for a cart field or action button. Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled. */\n register: CartFormRegister;\n}\n\n/**\n * The cart provider and hooks that `createCartComponents` returns, typed with the custom cart fields from your cart server handlers.\n */\ntype TypedCartComponents<TData extends CartData> = {\n /** Creates the cart store and shares the store with the cart hooks inside the provider. The provider connects the store on mount and destroys the store on unmount. */\n CartProvider: (props: TypedCartProviderProps<TData>) => ReactNode;\n /**\n * Returns the slice of cart state that the selector picks. The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.\n */\n useCart: CartStateHook<TData>;\n /** Suspends while the full cart loads, then returns the slice of cart state that the selector picks. Wrap the component in a `Suspense` boundary. */\n useSuspenseCart: CartStateHook<TData>;\n /**\n * Returns the slice of cart state that the selector picks, or `undefined` outside the provider. Use the hook in a component that also renders above the provider, such as a cart badge on an error page.\n */\n useOptionalCart: OptionalCartStateHook<TData>;\n /** Returns the `refresh()` action. Call `refresh()` after a server-side cart change that skips cart forms. */\n useCartActions: typeof useCartActions;\n /** Returns form props and a register function for cart forms. */\n useCartForm: typeof useCartForm;\n};\n\n/**\n * Returns a cart provider and cart hooks typed to the cart fragment in your cart server handlers.\n * Pass `typeof cartServerHandlers` as the `THandlers` type argument. Every hook's cart state then includes your custom cart fields.\n *\n * On mount, the provider sends cart requests to `/api/cart` and connects the store. Every hook except `useOptionalCart` throws when you call it outside the provider.\n *\n * @returns The cart provider, the cart state hooks, and the cart actions and form hooks.\n *\n * @example\n * ```tsx\n * import type { cartServerHandlers } from \"./server/cart.server\";\n *\n * const {\n * CartProvider,\n * useCart,\n * useSuspenseCart,\n * useOptionalCart,\n * useCartActions,\n * useCartForm,\n * } = createCartComponents<typeof cartServerHandlers>();\n * ```\n * @publicDocs\n */\nexport function createCartComponents<THandlers>(): TypedCartComponents<\n CartDataFromHandlers<THandlers>\n> {\n type TData = CartDataFromHandlers<THandlers>;\n\n function TypedCartProvider({ initialData, children }: TypedCartProviderProps<TData>) {\n return (\n <CartProvider initialData={initialData as CartInitialData | undefined}>\n {children}\n </CartProvider>\n );\n }\n\n function useTypedCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n return useCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useTypedOptionalCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S | undefined {\n return useOptionalCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useSuspenseCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n const readyPromise = useCart((state) => state.readyPromise);\n if (readyPromise) throw readyPromise;\n return useCart(selector, isEqual);\n }\n\n return {\n CartProvider: TypedCartProvider,\n useCart: useTypedCart,\n useSuspenseCart,\n useOptionalCart: useTypedOptionalCart,\n useCartActions,\n useCartForm,\n } as const;\n}\n\nexport function useCartStore(hookName = \"useCart\"): CartStore {\n const store = useContext(CartContext);\n if (!store) throw new Error(`${hookName} must be used inside <CartProvider>.`);\n return store;\n}\n\nfunction useOptionalCartStore(): CartStore | null {\n return useContext(CartContext);\n}\n\n/**\n * Creates a cart store and shares the store with the cart hooks inside the provider.\n *\n * Render every cart hook inside the provider. On mount, the provider sends cart requests to `/api/cart` and connects the store. On unmount, the provider destroys the store. The provider creates its store from the first initial data and ignores later changes.\n *\n * @example\n * ```tsx\n * // In your root layout\n * <CartProvider initialData={{ cart: loaderData.cart }}>\n * <App />\n * </CartProvider>\n * ```\n * @param props - The initial cart data and the content that uses the cart hooks.\n * @returns A context provider that shares the cart store with its children.\n * @publicDocs\n */\nexport function CartProvider({\n initialData,\n children,\n}: {\n initialData?: CartInitialData;\n children?: ReactNode;\n}) {\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- store is created once with the initial server data\n const store = useMemo(() => createCartStore({ initialData }), []);\n\n useEffect(() => {\n configureCoreCartEndpoint(cartEndpoint);\n store.connect();\n return () => {\n store.destroy();\n };\n }, [store]);\n\n return <CartContext.Provider value={store}>{children}</CartContext.Provider>;\n}\n\n/**\n * Returns the slice of cart state that the selector picks.\n *\n * The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.\n *\n * @param selector The function that picks a value from the cart state.\n * @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.\n * @returns The selected value.\n *\n * @example\n * ```tsx\n * // Select cart lines\n * const lines = useCart((state) => state.data.lines.nodes);\n *\n * // Select pending state for a specific line\n * const isPending = useCart((state) => state.pending.lines.has(lineId));\n *\n * // Custom equality to avoid re-renders on unchanged totals\n * const total = useCart(\n * (state) => state.data.cost.totalAmount,\n * (a, b) => a.amount === b.amount,\n * );\n * ```\n * @publicDocs\n */\nexport function useCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const store = useCartStore();\n return useCartSelector(store, selector, isEqual) as S;\n}\n\n/**\n * Returns the cart action that reloads the cart after a cart change outside cart forms.\n *\n * Call `refresh()` after a server-side cart change that skips cart forms, such as a server action that creates the cart.\n *\n * @returns The `refresh()` action.\n *\n * @example\n * ```tsx\n * const { refresh } = useCartActions();\n *\n * async function handleServerAction() {\n * await createCartOnServer();\n * refresh(); // re-fetch the cart\n * }\n * ```\n * @publicDocs\n */\nexport function useCartActions(): CartActions {\n const store = useCartStore(\"useCartActions\");\n\n return useMemo(() => ({ refresh: store.refresh }), [store]);\n}\n\n/**\n * Publishes cart analytics events when the customer's cart changes.\n *\n * Call the hook once near the root of your app, inside `CartProvider`. The hook starts tracking on mount and stops on unmount. The hook publishes `cart_updated`, `product_added_to_cart`, and `product_removed_from_cart` events.\n *\n * The hook throws outside `CartProvider`, and when the Shopify analytics bus isn't available. Render `ShopifyScripts` before the hook runs.\n *\n * @returns Nothing. The hook tracks cart analytics for the lifetime of the calling component.\n *\n * @example\n * ```tsx\n * function App() {\n * useCartAnalytics();\n * return <Layout />;\n * }\n * ```\n * @publicDocs\n */\nexport function useCartAnalytics(): void {\n const store = useCartStore(\"useCartAnalytics\");\n\n useEffect(() => trackCartAnalytics(store), [store]);\n}\n\n/**\n * Returns a selected slice of cart state, or `undefined` when the component renders outside the cart provider.\n *\n * Use the hook in a component that also renders above the provider, such as a header cart badge on an error page.\n * Wherever the provider always exists, use `useCart`, which throws when the provider is missing.\n *\n * @param selector The function that picks a value from the cart state.\n * @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.\n * @returns The selected value, or `undefined` outside the provider.\n *\n * @example\n * ```tsx\n * function CartBadge() {\n * const count = useOptionalCart((state) => state.data.totalQuantity);\n * if (count === undefined) return null;\n * return <span>Cart ({count})</span>;\n * }\n * ```\n * @publicDocs\n */\nexport function useOptionalCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S | undefined {\n const store = useOptionalCartStore();\n if (!store) return undefined;\n return useCartSelector(store, selector, isEqual);\n}\n\nfunction useCartSelector<TData extends CartData = CartData, S = unknown>(\n store: CartStore,\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const cachedRef = useRef<{ state: unknown; selector: typeof selector; value: S } | null>(null);\n\n const getSnapshot = () => {\n const state = store.getState() as CartState<TData>;\n\n if (\n cachedRef.current &&\n cachedRef.current.state === state &&\n cachedRef.current.selector === selector\n ) {\n return cachedRef.current.value;\n }\n\n const next = selector(state);\n\n if (cachedRef.current && isEqual?.(cachedRef.current.value, next)) {\n cachedRef.current = { state, selector, value: cachedRef.current.value };\n return cachedRef.current.value;\n }\n\n cachedRef.current = { state, selector, value: next };\n return next;\n };\n\n return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Returns form props and a field register function for building cart forms.\n *\n * Spread `formProps()` on the form. The form posts to the cart endpoint. On submit, the form props stop the browser's navigation and send the form through the cart store.\n * The submit handler ignores a failed cart change. Read failures from the cart state's `errors`.\n * To skip the cart submission, call `event.preventDefault()` in a `beforeSubmit` callback. The `afterSubmit` callback runs as soon as the cart change starts, before the server responds.\n * The register function returns the HTML attributes for each cart field and action.\n *\n * Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled.\n *\n * @returns The form props function and the register function.\n *\n * @example\n * ```tsx\n * function LineItem({ line }) {\n * const { formProps, register } = useCartForm();\n *\n * return (\n * <form {...formProps()}>\n * <input {...register(\"lineId\", { value: line.id })} />\n * <input {...register(\"quantity\", { value: line.quantity, interactive: true })} />\n * <button {...register(\"set\")} />\n * <button {...register(\"increase\")}>+</button>\n * <button {...register(\"decrease\")}>−</button>\n * <button {...register(\"remove\")}>Remove</button>\n * </form>\n * );\n * }\n * ```\n * @publicDocs\n */\nexport function useCartForm(): CartFormBindings {\n const store = useCartStore(\"useCartForm\");\n const coreRegister = useMemo(() => createCartFormRegister(), []);\n\n const register = useMemo(() => {\n type Register = typeof coreRegister;\n const wrapped = ((...args: Parameters<Register>) => {\n const result = (coreRegister as Function)(...args);\n const [field, opts] = args as [string, { interactive?: boolean }?];\n\n if (field === \"quantity\" && opts?.interactive) {\n let cleanup: (() => void) | null = null;\n const ref: RefCallback<HTMLInputElement> = (el) => {\n cleanup?.();\n cleanup = null;\n if (el) {\n const form = el.closest(\"form\");\n if (form) cleanup = attachQuantityInput(el, form);\n }\n };\n const { value: _, ...rest } = result;\n return { ...rest, defaultValue: result.value, ref };\n }\n\n return result;\n }) as Register;\n return wrapped;\n }, [coreRegister]);\n\n const formProps = (opts?: CartFormPropsOptions): FormHTMLAttributes<HTMLFormElement> => ({\n onSubmit: (e: SubmitEvent<HTMLFormElement>) => {\n opts?.beforeSubmit?.(e);\n if (e.defaultPrevented) return;\n e.preventDefault();\n store.handleFormSubmit(e.nativeEvent).catch(() => {});\n opts?.afterSubmit?.(e);\n },\n method: \"post\",\n action: cartEndpoint,\n });\n\n return { formProps, register };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cart.mjs","names":[],"sources":["../../../src/react/cart.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type FormHTMLAttributes,\n type ReactNode,\n type RefCallback,\n type SubmitEvent,\n} from \"react\";\n\nimport { trackCartAnalytics } from \"../core/analytics/cart-tracker\";\nimport { attachQuantityInput } from \"../core/cart/attach-quantity-input\";\nimport {\n configureCartEndpoint as configureCoreCartEndpoint,\n createCartStore,\n type CreateCartStoreOptions,\n type CartActions,\n type CartStore,\n} from \"../core/cart/cart\";\nimport { createCartFormRegister, type CartFormRegister } from \"../core/cart/form\";\nimport type { CartDataFromHandlers } from \"../core/cart/server-handlers\";\nimport { type CartData, type CartState } from \"../core/cart/state\";\n\nconst DEFAULT_CART_ENDPOINT = \"/api/cart\";\n\nlet cartEndpoint = DEFAULT_CART_ENDPOINT;\n\ninterface CartContextValue {\n store: CartStore;\n hydrationSnapshot: CartState;\n}\n\nconst CartContext = createContext<CartContextValue | null>(null);\n\nexport function configureCartEndpoint(endpoint: string): void {\n cartEndpoint = endpoint;\n configureCoreCartEndpoint(endpoint);\n}\n\nexport function getCartEndpoint(): string {\n return cartEndpoint;\n}\n\n/**\n * Props for the cart provider that `createCartComponents` returns.\n */\ntype TypedCartProviderProps<TData extends CartData> = {\n /**\n * The `data` property of the cart GET handler's result, or a promise of that data. Without initial data, the store loads the cart from `/api/cart` after the provider mounts, and the server-rendered page shows an empty cart. The provider creates its store from the first value and ignores later changes.\n */\n initialData?: CartInitialData<TData>;\n /** Content that can read the cart through the cart hooks. */\n children?: ReactNode;\n};\n\ntype CartInitialData<TData extends CartData = CartData> =\n CreateCartStoreOptions<TData>[\"initialData\"];\n\nexport type { CartActions };\n\n/** A hook that returns the slice of cart state that your selector picks. */\ntype CartStateHook<TData extends CartData> =\n /**\n * @param selector - Picks the value to return from the cart state.\n * @param isEqual - Compares the previous and next selected values. Return `true` to skip the re-render. Without the function, the hook compares the values by reference.\n * @returns The selected value.\n */\n <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S;\n\n/** A hook that returns the slice of cart state that your selector picks, or `undefined` outside the cart provider. */\ntype OptionalCartStateHook<TData extends CartData> =\n /**\n * @param selector - Picks the value to return from the cart state.\n * @param isEqual - Compares the previous and next selected values. Return `true` to skip the re-render. Without the function, the hook compares the values by reference.\n * @returns The selected value, or `undefined` outside the cart provider.\n */\n <S>(selector: (state: CartState<TData>) => S, isEqual?: (a: S, b: S) => boolean) => S | undefined;\n\n/** Callbacks that run around a cart form submission. */\ninterface CartFormPropsOptions {\n /** Runs before the cart store handles the submission. Call `event.preventDefault()` to skip the cart submission. */\n beforeSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n /** Runs as soon as the cart change starts, before the server responds. */\n afterSubmit?: (e: SubmitEvent<HTMLFormElement>) => void;\n}\n\n/** The form props function and the register function for a cart form. */\ninterface CartFormBindings {\n /** Returns the `method`, `action`, and `onSubmit` props to spread on the form. The form posts to the cart endpoint, and the cart store handles each submission. */\n formProps: (opts?: CartFormPropsOptions) => FormHTMLAttributes<HTMLFormElement>;\n /** Returns the attributes for a cart field or action button. Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled. */\n register: CartFormRegister;\n}\n\n/**\n * The cart provider and hooks that `createCartComponents` returns, typed with the custom cart fields from your cart server handlers.\n */\ntype TypedCartComponents<TData extends CartData> = {\n /** Creates the cart store and shares the store with the cart hooks inside the provider. The provider connects the store on mount and destroys the store on unmount. */\n CartProvider: (props: TypedCartProviderProps<TData>) => ReactNode;\n /**\n * Returns the slice of cart state that the selector picks. The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.\n */\n useCart: CartStateHook<TData>;\n /** Suspends while the full cart loads, then returns the slice of cart state that the selector picks. Wrap the component in a `Suspense` boundary. */\n useSuspenseCart: CartStateHook<TData>;\n /**\n * Returns the slice of cart state that the selector picks, or `undefined` outside the provider. Use the hook in a component that also renders above the provider, such as a cart badge on an error page.\n */\n useOptionalCart: OptionalCartStateHook<TData>;\n /** Returns the `refresh()` action. Call `refresh()` after a server-side cart change that skips cart forms. */\n useCartActions: typeof useCartActions;\n /** Returns form props and a register function for cart forms. */\n useCartForm: typeof useCartForm;\n};\n\n/**\n * Returns a cart provider and cart hooks typed to the cart fragment in your cart server handlers.\n * Pass `typeof cartServerHandlers` as the `THandlers` type argument. Every hook's cart state then includes your custom cart fields.\n *\n * On mount, the provider sends cart requests to `/api/cart` and connects the store. Every hook except `useOptionalCart` throws when you call it outside the provider.\n *\n * @returns The cart provider, the cart state hooks, and the cart actions and form hooks.\n *\n * @example\n * ```tsx\n * import type { cartServerHandlers } from \"./server/cart.server\";\n *\n * const {\n * CartProvider,\n * useCart,\n * useSuspenseCart,\n * useOptionalCart,\n * useCartActions,\n * useCartForm,\n * } = createCartComponents<typeof cartServerHandlers>();\n * ```\n * @publicDocs\n */\nexport function createCartComponents<THandlers>(): TypedCartComponents<\n CartDataFromHandlers<THandlers>\n> {\n type TData = CartDataFromHandlers<THandlers>;\n\n function TypedCartProvider({ initialData, children }: TypedCartProviderProps<TData>) {\n return (\n <CartProvider initialData={initialData as CartInitialData | undefined}>\n {children}\n </CartProvider>\n );\n }\n\n function useTypedCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n return useCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useTypedOptionalCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S | undefined {\n return useOptionalCart<CartData, S>((state) => selector(state as CartState<TData>), isEqual);\n }\n\n function useSuspenseCart<S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n ): S {\n return useSuspenseCartSelector(selector, isEqual);\n }\n\n return {\n CartProvider: TypedCartProvider,\n useCart: useTypedCart,\n useSuspenseCart,\n useOptionalCart: useTypedOptionalCart,\n useCartActions,\n useCartForm,\n } as const;\n}\n\nexport function useCartStore(hookName = \"useCart\"): CartStore {\n return useCartContext(hookName).store;\n}\n\nfunction useCartContext(hookName = \"useCart\"): CartContextValue {\n const context = useContext(CartContext);\n if (!context) throw new Error(`${hookName} must be used inside <CartProvider>.`);\n return context;\n}\n\nfunction useOptionalCartContext(): CartContextValue | null {\n return useContext(CartContext);\n}\n\n/**\n * Creates a cart store and shares the store with the cart hooks inside the provider.\n *\n * Render every cart hook inside the provider. On mount, the provider sends cart requests to `/api/cart` and connects the store. On unmount, the provider destroys the store. The provider creates its store from the first initial data and ignores later changes.\n *\n * @example\n * ```tsx\n * // In your root layout\n * <CartProvider initialData={{ cart: loaderData.cart }}>\n * <App />\n * </CartProvider>\n * ```\n * @param props - The initial cart data and the content that uses the cart hooks.\n * @returns A context provider that shares the cart store with its children.\n * @publicDocs\n */\nexport function CartProvider({\n initialData,\n children,\n}: {\n initialData?: CartInitialData;\n children?: ReactNode;\n}) {\n const context = useMemo(() => {\n const store = createCartStore({ initialData });\n // Plain consumers must hydrate from the state used for SSR, even if the live store settles first.\n return { store, hydrationSnapshot: store.getState() };\n // oxlint-disable-next-line react-hooks/exhaustive-deps -- provider initialData seeds one store for its lifetime\n }, []);\n const { store } = context;\n\n useEffect(() => {\n configureCoreCartEndpoint(cartEndpoint);\n store.connect();\n return () => {\n store.destroy();\n };\n }, [store]);\n\n return <CartContext.Provider value={context}>{children}</CartContext.Provider>;\n}\n\n/**\n * Returns the slice of cart state that the selector picks.\n *\n * The component re-renders when the selected value changes by reference, or when your `isEqual` function reports a change. A selector that returns the full state re-renders on every cart update.\n *\n * @param selector The function that picks a value from the cart state.\n * @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.\n * @returns The selected value.\n *\n * @example\n * ```tsx\n * // Select cart lines\n * const lines = useCart((state) => state.data.lines.nodes);\n *\n * // Select pending state for a specific line\n * const isPending = useCart((state) => state.pending.lines.has(lineId));\n *\n * // Custom equality to avoid re-renders on unchanged totals\n * const total = useCart(\n * (state) => state.data.cost.totalAmount,\n * (a, b) => a.amount === b.amount,\n * );\n * ```\n * @publicDocs\n */\nexport function useCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const { store, hydrationSnapshot } = useCartContext();\n return useCartSelector(store, hydrationSnapshot, selector, isEqual) as S;\n}\n\n/**\n * Returns the cart action that reloads the cart after a cart change outside cart forms.\n *\n * Call `refresh()` after a server-side cart change that skips cart forms, such as a server action that creates the cart.\n *\n * @returns The `refresh()` action.\n *\n * @example\n * ```tsx\n * const { refresh } = useCartActions();\n *\n * async function handleServerAction() {\n * await createCartOnServer();\n * refresh(); // re-fetch the cart\n * }\n * ```\n * @publicDocs\n */\nexport function useCartActions(): CartActions {\n const store = useCartStore(\"useCartActions\");\n\n return useMemo(() => ({ refresh: store.refresh }), [store]);\n}\n\n/**\n * Publishes cart analytics events when the customer's cart changes.\n *\n * Call the hook once near the root of your app, inside `CartProvider`. The hook starts tracking on mount and stops on unmount. The hook publishes `cart_updated`, `product_added_to_cart`, and `product_removed_from_cart` events.\n *\n * The hook throws outside `CartProvider`, and when the Shopify analytics bus isn't available. Render `ShopifyScripts` before the hook runs.\n *\n * @returns Nothing. The hook tracks cart analytics for the lifetime of the calling component.\n *\n * @example\n * ```tsx\n * function App() {\n * useCartAnalytics();\n * return <Layout />;\n * }\n * ```\n * @publicDocs\n */\nexport function useCartAnalytics(): void {\n const store = useCartStore(\"useCartAnalytics\");\n\n useEffect(() => trackCartAnalytics(store), [store]);\n}\n\n/**\n * Returns a selected slice of cart state, or `undefined` when the component renders outside the cart provider.\n *\n * Use the hook in a component that also renders above the provider, such as a header cart badge on an error page.\n * Wherever the provider always exists, use `useCart`, which throws when the provider is missing.\n *\n * @param selector The function that picks a value from the cart state.\n * @param isEqual The function that compares the previous and next selected values. Return `true` to skip the re-render.\n * @returns The selected value, or `undefined` outside the provider.\n *\n * @example\n * ```tsx\n * function CartBadge() {\n * const count = useOptionalCart((state) => state.data.totalQuantity);\n * if (count === undefined) return null;\n * return <span>Cart ({count})</span>;\n * }\n * ```\n * @publicDocs\n */\nexport function useOptionalCart<TData extends CartData = CartData, S = unknown>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S | undefined {\n const context = useOptionalCartContext();\n if (!context) return undefined;\n return useCartSelector(context.store, context.hydrationSnapshot, selector, isEqual);\n}\n\nfunction useCartSelector<TData extends CartData = CartData, S = unknown>(\n store: CartStore,\n hydrationSnapshot: CartState,\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const liveCacheRef = useRef<SelectorCache<S> | null>(null);\n const hydrationCacheRef = useRef<SelectorCache<S> | null>(null);\n\n const getSnapshot = () =>\n selectCartSnapshot(store.getState() as CartState<TData>, selector, isEqual, liveCacheRef);\n const getServerSnapshot = () =>\n selectCartSnapshot(hydrationSnapshot as CartState<TData>, selector, isEqual, hydrationCacheRef);\n\n return useSyncExternalStore(store.subscribe, getSnapshot, getServerSnapshot);\n}\n\ninterface SelectorCache<S> {\n state: unknown;\n selector: unknown;\n value: S;\n}\n\nfunction selectCartSnapshot<TData extends CartData, S>(\n state: CartState<TData>,\n selector: (state: CartState<TData>) => S,\n isEqual: ((a: S, b: S) => boolean) | undefined,\n cacheRef: { current: SelectorCache<S> | null },\n): S {\n if (cacheRef.current?.state === state && cacheRef.current.selector === selector) {\n return cacheRef.current.value;\n }\n\n const next = selector(state);\n\n if (cacheRef.current && isEqual?.(cacheRef.current.value, next)) {\n cacheRef.current = { state, selector, value: cacheRef.current.value };\n return cacheRef.current.value;\n }\n\n cacheRef.current = { state, selector, value: next };\n return next;\n}\n\ntype SuspenseSnapshot<S> =\n | { status: \"pending\"; promise: PromiseLike<void> }\n | { status: \"ready\"; value: S };\n\ninterface SuspenseSelectorCache<S> {\n state: unknown;\n selector: unknown;\n snapshot: SuspenseSnapshot<S>;\n}\n\nfunction useSuspenseCartSelector<TData extends CartData, S>(\n selector: (state: CartState<TData>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const { store } = useCartContext();\n const cacheRef = useRef<SuspenseSelectorCache<S> | null>(null);\n\n // Suspense retries must read live readiness and data; the hydration snapshot would freeze the fallback.\n const getSnapshot = () => {\n const state = store.getState() as CartState<TData>;\n\n if (cacheRef.current?.state === state && cacheRef.current.selector === selector) {\n return cacheRef.current.snapshot;\n }\n\n if (state.readyPromise) {\n const snapshot: SuspenseSnapshot<S> = { status: \"pending\", promise: state.readyPromise };\n cacheRef.current = { state, selector, snapshot };\n return snapshot;\n }\n\n const value = selector(state);\n const previous = cacheRef.current?.snapshot;\n\n if (previous?.status === \"ready\" && isEqual?.(previous.value, value)) {\n cacheRef.current = { state, selector, snapshot: previous };\n return previous;\n }\n\n const snapshot: SuspenseSnapshot<S> = { status: \"ready\", value };\n cacheRef.current = { state, selector, snapshot };\n return snapshot;\n };\n\n const snapshot = useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n if (snapshot.status === \"pending\") throw snapshot.promise;\n return snapshot.value;\n}\n\n/**\n * Returns form props and a field register function for building cart forms.\n *\n * Spread `formProps()` on the form. The form posts to the cart endpoint. On submit, the form props stop the browser's navigation and send the form through the cart store.\n * The submit handler ignores a failed cart change. Read failures from the cart state's `errors`.\n * To skip the cart submission, call `event.preventDefault()` in a `beforeSubmit` callback. The `afterSubmit` callback runs as soon as the cart change starts, before the server responds.\n * The register function returns the HTML attributes for each cart field and action.\n *\n * Register the quantity field with `interactive: true` to submit the form when the quantity changes. The interactive input takes the quantity as a default value and stays uncontrolled.\n *\n * @returns The form props function and the register function.\n *\n * @example\n * ```tsx\n * function LineItem({ line }) {\n * const { formProps, register } = useCartForm();\n *\n * return (\n * <form {...formProps()}>\n * <input {...register(\"lineId\", { value: line.id })} />\n * <input {...register(\"quantity\", { value: line.quantity, interactive: true })} />\n * <button {...register(\"set\")} />\n * <button {...register(\"increase\")}>+</button>\n * <button {...register(\"decrease\")}>−</button>\n * <button {...register(\"remove\")}>Remove</button>\n * </form>\n * );\n * }\n * ```\n * @publicDocs\n */\nexport function useCartForm(): CartFormBindings {\n const store = useCartStore(\"useCartForm\");\n const coreRegister = useMemo(() => createCartFormRegister(), []);\n\n const register = useMemo(() => {\n type Register = typeof coreRegister;\n const wrapped = ((...args: Parameters<Register>) => {\n const result = (coreRegister as Function)(...args);\n const [field, opts] = args as [string, { interactive?: boolean }?];\n\n if (field === \"quantity\" && opts?.interactive) {\n let cleanup: (() => void) | null = null;\n const ref: RefCallback<HTMLInputElement> = (el) => {\n cleanup?.();\n cleanup = null;\n if (el) {\n const form = el.closest(\"form\");\n if (form) cleanup = attachQuantityInput(el, form);\n }\n };\n const { value: _, ...rest } = result;\n return { ...rest, defaultValue: result.value, ref };\n }\n\n return result;\n }) as Register;\n return wrapped;\n }, [coreRegister]);\n\n const formProps = (opts?: CartFormPropsOptions): FormHTMLAttributes<HTMLFormElement> => ({\n onSubmit: (e: SubmitEvent<HTMLFormElement>) => {\n opts?.beforeSubmit?.(e);\n if (e.defaultPrevented) return;\n e.preventDefault();\n store.handleFormSubmit(e.nativeEvent).catch(() => {});\n opts?.afterSubmit?.(e);\n },\n method: \"post\",\n action: cartEndpoint,\n });\n\n return { formProps, register };\n}\n"],"mappings":"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"}