@shopify/hydrogen 2026.10.1 → 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.
- package/dist/cli/index.mjs +6 -3
- package/dist/client/client.mjs +2 -4
- package/dist/client/client.mjs.map +1 -1
- package/dist/client/i18n-variables.d.mts +13 -0
- package/dist/client/i18n-variables.mjs +84 -0
- package/dist/client/i18n-variables.mjs.map +1 -0
- package/dist/client/types.d.mts +13 -6
- package/dist/core/cart/cart.mjs +110 -23
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/cart/queries.d.mts +57 -57
- package/dist/core/predictive-search/queries.d.mts +2 -2
- package/dist/core/request-context.mjs +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/registered-routes.d.mts +5 -1
- package/dist/core/request-routing/registered-routes.mjs +21 -7
- package/dist/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/core/request-routing/route-types.d.mts +16 -6
- package/dist/core/shopify-scripts/perfkit.mjs +8 -2
- package/dist/core/shopify-scripts/perfkit.mjs.map +1 -1
- package/dist/core/utils/load-script.mjs +6 -6
- package/dist/core/utils/load-script.mjs.map +1 -1
- package/dist/customer-account/client.mjs +1 -1
- package/dist/customer-account/session.mjs +1 -1
- package/dist/development/client/client.mjs +2 -4
- package/dist/development/client/client.mjs.map +1 -1
- package/dist/development/client/i18n-variables.d.mts +13 -0
- package/dist/development/client/i18n-variables.mjs +84 -0
- package/dist/development/client/i18n-variables.mjs.map +1 -0
- package/dist/development/client/types.d.mts +13 -6
- package/dist/development/core/cart/cart.mjs +110 -23
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/request-context.mjs +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/registered-routes.d.mts +5 -1
- package/dist/development/core/request-routing/registered-routes.mjs +21 -7
- package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/route-types.d.mts +16 -6
- package/dist/development/core/shopify-scripts/perfkit.mjs +8 -2
- package/dist/development/core/shopify-scripts/perfkit.mjs.map +1 -1
- package/dist/development/core/utils/load-script.mjs +6 -6
- package/dist/development/core/utils/load-script.mjs.map +1 -1
- package/dist/development/graphql/type-resolver.d.mts +27 -1
- package/dist/development/react/cart.mjs +79 -25
- package/dist/development/react/cart.mjs.map +1 -1
- package/dist/graphql/type-resolver.d.mts +27 -1
- package/dist/react/cart.mjs +79 -25
- package/dist/react/cart.mjs.map +1 -1
- package/dist/vite/index.d.mts +11 -1
- package/dist/vite/index.mjs +21 -1
- package/dist/vite/index.mjs.map +1 -0
- package/dist/vue/shopify-scripts.d.mts +1 -1
- package/package.json +1 -1
- package/skills/hydrogen-cart-ui/SKILL.md +2 -2
- package/skills/hydrogen-request-handlers/SKILL.md +2 -2
- package/skills/hydrogen-request-handlers/references/frameworks.md +2 -7
- package/skills/hydrogen-request-handlers/references/nuxt.md +1 -3
- package/skills/hydrogen-storefront-client/SKILL.md +4 -2
|
@@ -73,8 +73,8 @@ declare const PREDICTIVE_SEARCH_QUERY: StorefrontQueryString<{
|
|
|
73
73
|
term: string;
|
|
74
74
|
limitScope?: "ALL" | "EACH" | null | undefined;
|
|
75
75
|
limit?: number | null | undefined;
|
|
76
|
-
language?: "ID" | "AF" | "
|
|
77
|
-
country?: "ID" | "AF" | "
|
|
76
|
+
language?: "ID" | "AF" | "DZ" | "AR" | "AM" | "AZ" | "BS" | "BE" | "BM" | "BO" | "BR" | "BN" | "BG" | "CA" | "KY" | "KM" | "HR" | "CU" | "CY" | "SV" | "EE" | "ET" | "FO" | "FI" | "FR" | "GA" | "DE" | "GL" | "GD" | "HU" | "IS" | "IT" | "KI" | "KW" | "LA" | "LV" | "LB" | "LT" | "LU" | "MO" | "MG" | "MY" | "ML" | "MT" | "MR" | "MN" | "MS" | "NL" | "NE" | "MK" | "NO" | "OM" | "PS" | "PA" | "PL" | "PT" | "RO" | "RU" | "RW" | "SH" | "KN" | "SA" | "SN" | "SC" | "SL" | "SG" | "SK" | "SI" | "SO" | "ES" | "SD" | "SR" | "SE" | "TH" | "TG" | "TK" | "TO" | "TT" | "TA" | "TR" | "UG" | "UZ" | "AK" | "AS" | "CE" | "CKB" | "CS" | "DA" | "EL" | "EN" | "EO" | "EU" | "FA" | "FF" | "FIL" | "FY" | "GU" | "GV" | "HA" | "HE" | "HI" | "HY" | "IA" | "IG" | "II" | "JA" | "JV" | "KA" | "KK" | "KL" | "KO" | "KS" | "KU" | "LG" | "LN" | "LO" | "MI" | "NB" | "ND" | "NN" | "OR" | "OS" | "PT_BR" | "PT_PT" | "QU" | "RM" | "RN" | "SQ" | "SU" | "SW" | "TE" | "TI" | "UK" | "UR" | "VI" | "WO" | "XH" | "YI" | "YO" | "ZH_CN" | "ZH_TW" | "ZU" | "ZH" | "VO" | null | undefined;
|
|
77
|
+
country?: "ID" | "AF" | "AX" | "AL" | "DZ" | "AD" | "AO" | "AI" | "AG" | "AR" | "AM" | "AW" | "AC" | "AU" | "AT" | "AZ" | "BS" | "BH" | "BD" | "BB" | "BY" | "BE" | "BZ" | "BJ" | "BM" | "BT" | "BO" | "BA" | "BW" | "BV" | "BR" | "IO" | "BN" | "BG" | "BF" | "BI" | "KH" | "CA" | "CV" | "BQ" | "KY" | "CF" | "TD" | "CL" | "CN" | "CX" | "CC" | "CO" | "KM" | "CG" | "CD" | "CK" | "CR" | "HR" | "CU" | "CW" | "CY" | "CZ" | "CI" | "DK" | "DJ" | "DM" | "DO" | "EC" | "EG" | "SV" | "GQ" | "ER" | "EE" | "SZ" | "ET" | "FK" | "FO" | "FJ" | "FI" | "FR" | "GF" | "PF" | "TF" | "GA" | "GM" | "GE" | "DE" | "GH" | "GI" | "GR" | "GL" | "GD" | "GP" | "GT" | "GG" | "GN" | "GW" | "GY" | "HT" | "HM" | "VA" | "HN" | "HK" | "HU" | "IS" | "IN" | "IR" | "IQ" | "IE" | "IM" | "IL" | "IT" | "JM" | "JP" | "JE" | "JO" | "KZ" | "KE" | "KI" | "KP" | "XK" | "KW" | "KG" | "LA" | "LV" | "LB" | "LS" | "LR" | "LY" | "LI" | "LT" | "LU" | "MO" | "MG" | "MW" | "MY" | "MV" | "ML" | "MT" | "MQ" | "MR" | "MU" | "YT" | "MX" | "MD" | "MC" | "MN" | "ME" | "MS" | "MA" | "MZ" | "MM" | "NA" | "NR" | "NP" | "NL" | "AN" | "NC" | "NZ" | "NI" | "NE" | "NG" | "NU" | "NF" | "MK" | "NO" | "OM" | "PK" | "PS" | "PA" | "PG" | "PY" | "PE" | "PH" | "PN" | "PL" | "PT" | "QA" | "CM" | "RE" | "RO" | "RU" | "RW" | "BL" | "SH" | "KN" | "LC" | "MF" | "PM" | "WS" | "SM" | "ST" | "SA" | "SN" | "RS" | "SC" | "SL" | "SG" | "SX" | "SK" | "SI" | "SB" | "SO" | "ZA" | "GS" | "KR" | "SS" | "ES" | "LK" | "VC" | "SD" | "SR" | "SJ" | "SE" | "CH" | "SY" | "TW" | "TJ" | "TZ" | "TH" | "TL" | "TG" | "TK" | "TO" | "TT" | "TA" | "TN" | "TR" | "TM" | "TC" | "TV" | "UG" | "UA" | "AE" | "GB" | "US" | "UM" | "UY" | "UZ" | "VU" | "VE" | "VN" | "VG" | "WF" | "EH" | "YE" | "ZM" | "ZW" | "ZZ" | null | undefined;
|
|
78
78
|
}, "query PredictiveSearch(\n $country: CountryCode\n $language: LanguageCode\n $limit: Int\n $limitScope: PredictiveSearchLimitScope\n $term: String!\n $types: [PredictiveSearchType!]\n $searchableFields: [SearchableField!]\n $unavailableProducts: SearchUnavailableProductsType\n ) @inContext(country: $country, language: $language) {\n predictiveSearch(\n limit: $limit\n limitScope: $limitScope\n query: $term\n types: $types\n searchableFields: $searchableFields\n unavailableProducts: $unavailableProducts\n ) {\n articles {\n ...HydrogenPredictiveSearchArticleFragment\n ...PredictiveSearchArticleFragment\n }\n collections {\n ...HydrogenPredictiveSearchCollectionFragment\n ...PredictiveSearchCollectionFragment\n }\n pages {\n ...HydrogenPredictiveSearchPageFragment\n ...PredictiveSearchPageFragment\n }\n products {\n ...HydrogenPredictiveSearchProductFragment\n ...PredictiveSearchProductFragment\n }\n queries {\n ...HydrogenPredictiveSearchQueryFragment\n ...PredictiveSearchQueryFragment\n }\n }\n }\n\n fragment HydrogenPredictiveSearchProductFragment on Product {\n __typename\n id\n title\n handle\n trackingParameters\n selectedOrFirstAvailableVariant(\n selectedOptions: []\n ignoreUnknownOptions: true\n caseInsensitiveMatch: true\n ) {\n id\n image {\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n }\n }\n\n\n fragment HydrogenPredictiveSearchCollectionFragment on Collection {\n __typename\n id\n title\n handle\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n\n fragment HydrogenPredictiveSearchPageFragment on Page {\n __typename\n id\n title\n handle\n trackingParameters\n }\n\n\n fragment HydrogenPredictiveSearchArticleFragment on Article {\n __typename\n id\n title\n handle\n blog {\n handle\n }\n image {\n url\n altText\n width\n height\n }\n trackingParameters\n }\n\n\n fragment HydrogenPredictiveSearchQueryFragment on SearchQuerySuggestion {\n __typename\n text\n styledText\n trackingParameters\n }\n">;
|
|
79
79
|
declare const DEFAULT_PRODUCT_FRAGMENT: StorefrontQueryString<{
|
|
80
80
|
id: string;
|
|
@@ -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.
|
|
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":";;;;AAIA,MAAM,mBAAmB;AAGzB,MAAM,4BAA4B;AAmElC,SAAgB,uBAAuB,YAAuD;CAC5F,MAAM,MAAM,UAAU,WAAW,MAAM;CACvC,MAAM,cAAc,WAAW,eAAe;AAE9C,SAAQ,KAAK,YAAY;EACvB,MAAM,EAAE,YAAY;AACpB,MAAI,CAAC,WAAW,MAAM,KAAK,IAAI,SAAS,CAAE,QAAO;AAGjD,MAAI,IAAI,aAAa,IAAI,WAAW,CAClC,QAAO,QAAQ,QACb,6BACE;GACE,QAAQ;GACR,MAAM,YAAY,mCAAmC;GACtD,EACD,QAAQ,OACT,CACF;AAGH,MAAI,WAAW,WAAW,CAAC,WAAW,QAAQ,SAAS,QAAQ,OAAO,CAEpE,QAAO,QAAQ,QACb,IAAI,SACF,QAAQ,WAAW,SAAS,OAAO,KAAK,UAAU,YAAY,qBAAqB,CAAC,EACpF;GACE,QAAQ;GACR,SAAS;IACP,OAAO,WAAW,QAAQ,KAAK,KAAK;IACpC,gBAAgB;IAChB,iBAAiB;IAClB;GACF,CACF,CACF;EAGH,IAAI;AACJ,MAAI;AACF,iBAAc,uBAAuB,YAAY,SAAS,IAAI;WACvD,OAAO;AACd,OAAI,MAAM,kBAAkB,EAAE,OAAO,CAAC;AACtC,UAAO,QAAQ,QACb,yBAAyB,OAAO,SAAS,WAAW,UAAU,aAAa,QAAQ,OAAO,CAC3F;;AAGH,SAAO,MAAM,YAAY,aAAa,YAAY,KAAK,CACpD,MAAM,qBACL,mBAAmB,kBAAkB,YAAY,SAAS,KAAK,IAAI,CACpE,CACA,OAAO,UAAU;AAChB,OAAI,MAAM,kBAAkB,EAAE,OAAO,CAAC;AACtC,UAAO,yBACL,OACA,SACA,WAAW,UACX,aACA,QAAQ,OACT;IACD;;;AAIR,SAAS,uBACP,YACA,SACA,KACkB;CAClB,MAAM,EAAE,SAAS,qBAAqB;CACtC,MAAM,mBAAmB,WAAW,kBAAkB,IAAI,SAAS,IAAI,IAAI;CAC3E,MAAM,SAAS,WAAW,kBAAkB,QAAQ,KAAK,IAAI;CAC7D,MAAM,cAAc,IAAI,IAAI,mBAAmB,QAAQ,iBAAiB,SAAS;CACjF,MAAM,OAA0C;EAC9C,QAAQ,QAAQ;EAChB,MAAM,QAAQ;EACd,SAAS,0BAA0B,YAAY;GAAE,GAAG;GAAS;GAAK,CAAC;EACnE,QAAQ,YAAY,QAAQ,WAAW,aAAa,iBAAiB;EACrE,UAAU,WAAW,YAAY;EAClC;AAGD,KAAI,QAAQ,KAAM,MAAK,SAAS;AAChC,QAAO;EAAE;EAAa;EAAM;;AAG9B,SAAS,mBACP,kBACA,YACA,SACA,KACA,KACU;CACV,MAAM,WAA8B;EAClC,IAAI,iBAAiB;EACrB,QAAQ,iBAAiB;EACzB,YAAY,iBAAiB;EAC7B,SAAS,iBAAiB;EAC3B;CAED,MAAM,YAAY,WAAW,qBAAqB,UAAU,SAAS,IAAI;AACzE,KAAI,WAAW;AAGb,mBAAiB,MAAM,QAAQ,CAAC,YAAY,GAAG;AAC/C,MAAI,UAAU,WAAY,KAAI,MAAM,UAAU,YAAY,UAAU,OAAO,EAAE,CAAC;AAC9E,SAAO,6BAA6B,WAAW,QAAQ,QAAQ,OAAO;;CAGxE,MAAM,UAAU,0BACd,iBAAiB,SACjB,WAAW,iBAAiB,MAC7B;AACD,YAAW,iBAAiB,UAAU,SAAS;EAAE,GAAG;EAAS;EAAK,UAAU;EAAU,CAAC;AACvF,SAAQ,eAAe,iCAAiC,QAAQ;AAEhE,QAAO,IAAI,SAAS,iBAAiB,MAAM;EACzC,QAAQ,iBAAiB;EACzB,YAAY,iBAAiB;EAC7B;EACD,CAAC;;AAGJ,SAAS,mBAAmB,SAAoC;AAC9D,QAAO,EAAE,OAAO,SAAS;;AAG3B,SAAS,yBACP,OACA,OACA,UACA,aACA,QACU;CACV,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;CACzD,MAAM,UAAU,WAAW,OAAO,MAAM;AAGxC,QAAO,6BAA6B;EAAE,QAFvB,SAAS,WAAW,UAAU,UAAU,MAAM;EAEf,MADjC,SAAS,SAAS,SAAY,YAAY,QAAQ,GAAG,QAAQ;EACtB,EAAE,OAAO;;AAG/D,SAAS,6BAA6B,WAAmC,QAA0B;AACjG,QAAO,IAAI,SAAS,WAAW,SAAS,OAAO,KAAK,UAAU,UAAU,KAAK,EAAE;EAC7E,QAAQ,UAAU;EAClB,SAAS;GACP,gBAAgB;GAChB,iBAAiB;GAClB;EACF,CAAC;;AAGJ,SAAS,0BACP,YACA,SACS;CACT,MAAM,EAAE,SAAS,mBAAmB;CACpC,MAAM,EAAE,OAAO,MAAM,YAAY,WAAW;CAC5C,MAAM,UAAU,QACZ,IAAI,QAAQ,gBAAgB,QAAQ,QAAQ,QAAQ,IAAI,IAAI,EAAE,MAAM,CAAC,GACrE,IAAI,QAAQ,QAAQ,QAAQ;AAEhC,KAAI,WAAW,eAAe,2BAA2B,MACvD,gBAAe,8BAA8B,QAAQ;AAGvD,MAAK,MAAM,UAAU,QAAQ,EAAE,CAAE,SAAQ,OAAO,OAAO;AACvD,WAAU,SAAS,QAAQ;AAE3B,QAAO;;AAGT,SAAS,0BAA0B,iBAA0B,OAAoC;AAC/F,KAAI,OAAO;EACT,MAAM,UAAU,IAAI,SAAS;AAC7B,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,QAAQ,gBAAgB,IAAI,KAAK;AACvC,OAAI,UAAU,KAAM,SAAQ,IAAI,MAAM,MAAM;;AAE9C,SAAO;;AAET,QAAO,2BAA2B,gBAAgB;;AAGpD,SAAgB,2BAA2B,iBAAmC;CAC5E,MAAM,UAAU,IAAI,QAAQ,gBAAgB;AAC5C,SAAQ,OAAO,mBAAmB;AAClC,SAAQ,OAAO,iBAAiB;AAChC,SAAQ,OAAO,gBAAgB;AAC/B,QAAO"}
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
6
|
+
const SHS_BEACON_ENDPOINT = `${SHOPIFY_API_PROXY_PREFIX}/api/collect`;
|
|
7
|
+
function getPerfKitScript(shop, extraAttributes, { shs = true } = {}) {
|
|
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":"
|
|
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,SAAmC,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
|
|
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"}
|
|
@@ -15,7 +15,7 @@ const LOCAL_ORIGIN_HOSTS = new Set([
|
|
|
15
15
|
"::1",
|
|
16
16
|
"[::1]"
|
|
17
17
|
]);
|
|
18
|
-
const USER_AGENT = `Hydrogen 2026.10.
|
|
18
|
+
const USER_AGENT = `Hydrogen 2026.10.2`;
|
|
19
19
|
const CUSTOMER_ACCOUNT_GRAPHQL_PERSONALIZATION_REASON = "customer-account-graphql";
|
|
20
20
|
/**
|
|
21
21
|
* Creates a client that sends Customer Account API requests from your server. Create the client with the current request's context.
|
|
@@ -44,7 +44,7 @@ const AUTHORIZATION_CODE_GRANT_TYPE = "authorization_code";
|
|
|
44
44
|
const REFRESH_TOKEN_GRANT_TYPE = "refresh_token";
|
|
45
45
|
const CUSTOMER_ACCOUNT_SCOPE = "openid email customer-account-api:full";
|
|
46
46
|
const CODE_CHALLENGE_METHOD = "S256";
|
|
47
|
-
const USER_AGENT = `Hydrogen 2026.10.
|
|
47
|
+
const USER_AGENT = `Hydrogen 2026.10.2`;
|
|
48
48
|
const EXPIRY_BUFFER_IN_SECONDS = 120;
|
|
49
49
|
const PENDING_LOGIN_TTL_IN_MINUTES = 10;
|
|
50
50
|
const SECONDS_PER_MINUTE = 60;
|
|
@@ -4,10 +4,9 @@ import { createFetchWithCache } from "../core/cache/fetch-with-cache.mjs";
|
|
|
4
4
|
import { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from "../core/constants.mjs";
|
|
5
5
|
import { HYDROGEN_VERSION_HEADER, REQUEST_GROUP_ID_HEADER, SDK_VARIANT_HEADER, SDK_VARIANT_SOURCE_HEADER, SDK_VERSION_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, STOREFRONT_ACCESS_TOKEN_HEADER, STOREFRONT_BUYER_IP_HEADER, STOREFRONT_ID_HEADER, STOREFRONT_PRIVATE_TOKEN_HEADER } from "../core/headers.mjs";
|
|
6
6
|
import { StorefrontApiError, StorefrontTimeoutError } from "./errors.mjs";
|
|
7
|
+
import { getI18nVariableNames } from "./i18n-variables.mjs";
|
|
7
8
|
|
|
8
9
|
//#region src/client/client.ts
|
|
9
|
-
const COUNTRY_VAR_RE = /\$country\s*:/;
|
|
10
|
-
const LANGUAGE_VAR_RE = /\$language\s*:/;
|
|
11
10
|
const REQUEST_CACHE_KEY_HEADERS = new Set([
|
|
12
11
|
"content-type",
|
|
13
12
|
"user-agent",
|
|
@@ -174,8 +173,7 @@ function createStorefrontClient(args) {
|
|
|
174
173
|
}
|
|
175
174
|
function buildVariables(queryText, userVariables, resolvedI18n) {
|
|
176
175
|
const variables = { ...userVariables };
|
|
177
|
-
|
|
178
|
-
if (LANGUAGE_VAR_RE.test(queryText)) variables.language = resolvedI18n.language;
|
|
176
|
+
for (const name of getI18nVariableNames(queryText)) variables[name] = resolvedI18n[name];
|
|
179
177
|
return variables;
|
|
180
178
|
}
|
|
181
179
|
function createStorefrontCacheOptions(apiUrl, init, queryText, strategy, shouldCacheResult, backgroundTimeoutInMs) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../../../src/client/client.ts"],"sourcesContent":["import type { TadaDocumentNode } from \"gql.tada\";\n\nimport { getHydrogenCacheStatus } from \"../core/cache/cache-status\";\nimport { createFetchWithCache } from \"../core/cache/fetch-with-cache\";\nimport type { FetchCacheOptions, FetchCacheResponseContext } from \"../core/cache/fetch-with-cache\";\nimport type { CacheInstance } from \"../core/cache/run-with-cache\";\nimport type { CachingStrategy } from \"../core/cache/strategies\";\nimport { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from \"../core/constants\";\nimport {\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n STOREFRONT_ACCESS_TOKEN_HEADER,\n STOREFRONT_BUYER_IP_HEADER,\n STOREFRONT_ID_HEADER,\n STOREFRONT_PRIVATE_TOKEN_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n} from \"../core/headers\";\nimport type { I18nConfig, ShopifyRequestContext } from \"../core/request-context\";\nimport { normalizeStoreDomain } from \"../core/url\";\nimport type { AnyStorefrontQueryString } from \"../graphql\";\nimport { StorefrontApiError, StorefrontTimeoutError } from \"./errors\";\nimport type {\n ClientType,\n CreateStorefrontClientArgs,\n GraphQLFormattedError,\n StorefrontClient,\n StorefrontGraphqlResult,\n} from \"./types\";\n\ntype DocLike = TadaDocumentNode<unknown, unknown> | AnyStorefrontQueryString;\ntype FetchInput = Parameters<typeof globalThis.fetch>[0];\ntype FetchInit = Parameters<typeof globalThis.fetch>[1];\ntype ResolvedStorefrontFetch = (\n input: FetchInput,\n init: FetchInit,\n cacheOptions: FetchCacheOptions | undefined,\n) => Promise<Response>;\n\nconst COUNTRY_VAR_RE = /\\$country\\s*:/;\nconst LANGUAGE_VAR_RE = /\\$language\\s*:/;\nconst REQUEST_CACHE_KEY_HEADERS = new Set([\n \"content-type\",\n \"user-agent\",\n SDK_VARIANT_HEADER.toLowerCase(),\n SDK_VARIANT_SOURCE_HEADER.toLowerCase(),\n SDK_VERSION_HEADER.toLowerCase(),\n HYDROGEN_VERSION_HEADER.toLowerCase(),\n STOREFRONT_ACCESS_TOKEN_HEADER.toLowerCase(),\n STOREFRONT_PRIVATE_TOKEN_HEADER.toLowerCase(),\n]);\nconst REQUEST_IDENTITY_HEADERS = new Set([\n \"cookie\",\n REQUEST_GROUP_ID_HEADER.toLowerCase(),\n STOREFRONT_BUYER_IP_HEADER.toLowerCase(),\n SHOPIFY_UNIQUE_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_VISIT_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_S_HEADER.toLowerCase(),\n]);\nconst CACHE_ENABLED_GRAPHQL_CLIENTS = new WeakSet<StorefrontClient[\"graphql\"]>();\nconst SHOULD_CACHE_RESULT = Symbol();\n\ntype ShouldCacheResult = (body: object) => boolean;\n\nclass StorefrontCacheConfigError extends Error {}\n\nexport function withStorefrontClientCache<TOptions extends object>(\n client: Pick<StorefrontClient, \"graphql\">,\n options: TOptions,\n strategy: CachingStrategy,\n shouldCacheResult?: ShouldCacheResult,\n): TOptions {\n if (!CACHE_ENABLED_GRAPHQL_CLIENTS.has(client.graphql)) return options;\n\n return { ...options, cache: strategy, [SHOULD_CACHE_RESULT]: shouldCacheResult };\n}\n\n/**\n * Creates a Storefront API client that runs typed queries and mutations against your store.\n *\n * Set `type` based on where your code runs and whether customer context is available:\n *\n * | | `public` | `private` | `private_no_buyer_context` |\n * |------------------|------------------------------|-------------------------------|------------------------------|\n * | Runs in | Browser or mobile | Server-side rendering | Background server code |\n * | Token | Public access token, or none | Private access token | Private access token |\n * | Customer context | Browser request context | You forward trusted `buyerIp` | None |\n * | Best for | Client-side fetches | SSR with customer context | Webhooks, background jobs |\n *\n * Pass a request context from `createShopifyRequestContext`. The client forwards the incoming\n * request's cookies and `Sec-GPC` header to the Storefront API, and fills `$country`\n * and `$language` variables from the context's locale. The client reads the request headers once,\n * when you create the client. Create a client for each incoming request, and create a client at\n * module scope only with a static request context.\n *\n * Keep private clients in server code, where the token can't reach browsers. The function throws an\n * error when you create either private client type in a browser. The function throws a `TypeError`\n * when the request context of a `private` client has no `buyerIp`.\n *\n * @see [Storefront API authentication](https://shopify.dev/docs/api/storefront#authentication)\n * @publicDocs\n */\nexport function createStorefrontClient<\n const Type extends ClientType,\n const RequestContext extends ShopifyRequestContext,\n const CacheConfig extends CacheInstance | undefined = undefined,\n>(\n args: CreateStorefrontClientArgs<RequestContext, Type, CacheConfig>,\n): StorefrontClient<\n // Tuple-wrapped to stay non-distributive so an explicit\n // `CacheInstance | undefined` type argument yields one client type, not a\n // union. Note a *value* of that type still unlocks cache options: optional\n // property inference strips `undefined` before it reaches CacheConfig, so\n // definiteness is undecidable here — the runtime guard rejects cache\n // options when no cache instance was actually configured.\n [CacheConfig] extends [CacheInstance] ? { cache?: CachingStrategy } : {},\n Type,\n RequestContext\n>;\n/**\n * @param args The client type, request context, and store configuration for the new client.\n * @returns The client, with a `graphql()` method that runs queries and mutations.\n */\nexport function createStorefrontClient(args: CreateStorefrontClientArgs): StorefrontClient {\n const { config, requestContext, type: clientType } = args;\n\n if (!config.storeDomain) {\n throw new Error(\"storeDomain is required and must be non-empty\");\n }\n\n const storeUrl = normalizeStoreDomain(config.storeDomain);\n const apiVersion = config.apiVersion ?? STOREFRONT_API_VERSION;\n const apiUrl = `${storeUrl}/api/${apiVersion}/graphql.json`;\n\n const originFetch = config.fetch ?? globalThis.fetch;\n if (typeof originFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n const resolvedFetch = config.cache\n ? createFetchWithCache({\n cache: config.cache,\n waitUntil: config.waitUntil,\n fetch: originFetch as typeof globalThis.fetch,\n })\n : originFetch;\n\n const timeoutInMs = config.defaultTimeoutInMs ?? DEFAULT_TIMEOUT_IN_MS;\n if (timeoutInMs < 0 || Number.isNaN(timeoutInMs)) {\n throw new Error(\"defaultTimeoutInMs must be a non-negative number\");\n }\n\n const staticHeaders: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (config.storefrontId) {\n staticHeaders[STOREFRONT_ID_HEADER] = config.storefrontId;\n }\n\n switch (clientType) {\n case \"private\":\n case \"private_no_buyer_context\":\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"private storefront tokens cannot be used in a browser context. Use a publicStorefrontToken instead.\",\n );\n }\n if (!config.privateStorefrontToken) {\n throw new Error(\"privateStorefrontToken is required\");\n }\n staticHeaders[STOREFRONT_PRIVATE_TOKEN_HEADER] = config.privateStorefrontToken;\n break;\n case \"public\":\n if (config.publicStorefrontToken !== undefined && !config.publicStorefrontToken) {\n throw new Error(\"publicStorefrontToken must be non-empty when provided\");\n }\n if (config.publicStorefrontToken) {\n staticHeaders[STOREFRONT_ACCESS_TOKEN_HEADER] = config.publicStorefrontToken;\n }\n break;\n default:\n clientType satisfies never;\n throw new Error(`Unsupported client type: ${clientType}`);\n }\n\n const i18n = requestContext.i18n;\n const requestHeaders = new Headers(staticHeaders);\n requestContext.applyStorefrontRequestHeaders(requestHeaders);\n\n if (clientType === \"private\") {\n const { buyerIp } = requestContext;\n if (!buyerIp) {\n throw new TypeError(\"requestContext.buyerIp is required for private Storefront API clients\");\n }\n requestHeaders.set(STOREFRONT_BUYER_IP_HEADER, buyerIp);\n }\n\n async function graphql(\n doc: DocLike | string,\n ...rest: [options?: Record<string, unknown>]\n ): Promise<StorefrontGraphqlResult<DocLike>> {\n const opts = (rest[0] ?? {}) as {\n variables?: Record<string, unknown>;\n signal?: AbortSignal;\n cache?: CachingStrategy;\n [SHOULD_CACHE_RESULT]?: ShouldCacheResult;\n };\n\n const queryText = typeof doc === \"string\" ? doc : (doc as unknown as string);\n if (typeof queryText !== \"string\") {\n throw new StorefrontApiError(\n \"Expected gql() to return a string at runtime. Check gql.tada version compatibility.\",\n );\n }\n\n if (opts.cache && !config.cache) {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache options require a cache configured in createStorefrontClient().\",\n );\n }\n\n if (opts.cache?.mode === \"private\") {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache mode 'private' is not supported yet. Use Cache() without private mode, or cache buyer-specific work with createRunWithCache() and an explicit private key.\",\n );\n }\n\n const variables = buildVariables(queryText, opts.variables, i18n);\n const externalSignals = [requestContext.signal, opts.signal].filter(\n (signal): signal is AbortSignal => Boolean(signal),\n );\n\n const earlyAbortSignal = externalSignals.find((signal) => signal.aborted);\n if (earlyAbortSignal) {\n throw (\n earlyAbortSignal.reason ??\n new DOMException(\"signal is aborted without reason\", \"AbortError\")\n );\n }\n\n let requestId: string | undefined;\n let responseHeaders: Headers;\n let responseText: string;\n let status: number;\n let timeoutSignal: AbortSignal | undefined;\n\n if (timeoutInMs > 0) {\n timeoutSignal = AbortSignal.timeout(timeoutInMs);\n externalSignals.push(timeoutSignal);\n }\n\n try {\n const init: PlainRequestInit = {\n method: \"POST\",\n headers: new Headers(requestHeaders),\n body: JSON.stringify({ query: queryText, variables }),\n signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : undefined,\n };\n\n const cacheOptions = createStorefrontCacheOptions(\n apiUrl,\n init,\n queryText,\n opts.cache,\n opts[SHOULD_CACHE_RESULT],\n timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT_IN_MS,\n );\n const response = await (resolvedFetch as ResolvedStorefrontFetch)(apiUrl, init, cacheOptions);\n\n responseHeaders = response.headers;\n const hydrogenCacheStatus = getHydrogenCacheStatus(responseHeaders);\n if (hydrogenCacheStatus !== \"hit\") {\n requestContext.captureSubrequestHeaders(responseHeaders);\n }\n\n requestId = responseHeaders.get(\"x-request-id\") ?? undefined;\n status = response.status;\n\n if (!response.ok) {\n const bodyHint = await response.text().catch(() => \"\");\n throw new StorefrontApiError(\n `SFAPI responded with ${status}${bodyHint ? `: ${bodyHint.slice(0, 200)}` : \"\"}`,\n { status, requestId, queryText, variables },\n );\n }\n\n responseText = await response.text();\n } catch (error) {\n if (error instanceof StorefrontApiError) throw error;\n if (error instanceof StorefrontTimeoutError) throw error;\n if (error instanceof StorefrontCacheConfigError) throw error;\n if (\n error instanceof DOMException &&\n error.name === \"TimeoutError\" &&\n timeoutSignal?.aborted\n ) {\n throw new StorefrontTimeoutError(timeoutInMs);\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new StorefrontApiError(\"SFAPI request failed\", { cause: error });\n }\n\n let json: unknown;\n try {\n json = JSON.parse(responseText);\n } catch (cause) {\n throw new StorefrontApiError(\n `Failed to parse SFAPI response as JSON (${apiUrl}, status ${status})` +\n (responseText ? `: ${responseText.slice(0, 200)}` : \"\"),\n { status, requestId, cause, queryText, variables },\n );\n }\n\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) {\n throw new StorefrontApiError(\n `SFAPI returned unexpected JSON type: ${Array.isArray(json) ? \"array\" : typeof json}`,\n { status, requestId, queryText, variables },\n );\n }\n\n const body = json as { data?: unknown; errors?: GraphQLFormattedError[] };\n // Branch to populate the two arms of the StorefrontGraphqlResult union: callers\n // narrowing on `errors` get a non-null `data` only when no errors are present.\n if (body.errors) {\n return {\n data: body.data ?? null,\n errors: body.errors,\n headers: responseHeaders,\n };\n }\n return {\n data: body.data,\n headers: responseHeaders,\n };\n }\n\n const client: StorefrontClient = {\n type: clientType,\n i18n,\n graphql: graphql as StorefrontClient[\"graphql\"],\n apiUrl,\n storeUrl,\n storefrontId: config.storefrontId,\n requestContext,\n };\n\n if (config.cache) CACHE_ENABLED_GRAPHQL_CLIENTS.add(client.graphql);\n\n return client;\n}\n\nfunction buildVariables(\n queryText: string,\n userVariables: Record<string, unknown> | undefined,\n resolvedI18n: I18nConfig,\n): Record<string, unknown> {\n const variables: Record<string, unknown> = { ...userVariables };\n\n if (COUNTRY_VAR_RE.test(queryText)) {\n variables.country = resolvedI18n.country;\n }\n if (LANGUAGE_VAR_RE.test(queryText)) {\n variables.language = resolvedI18n.language;\n }\n\n return variables;\n}\n\ntype PlainRequestInit = RequestInit & { headers: Headers; body: string; method: string };\n\nfunction createStorefrontCacheOptions(\n apiUrl: string,\n init: PlainRequestInit,\n queryText: string,\n strategy: CachingStrategy | undefined,\n shouldCacheResult: ShouldCacheResult | undefined,\n backgroundTimeoutInMs: number,\n): FetchCacheOptions | undefined {\n if (!strategy) return undefined;\n\n // Runtime only needs a safety belt for untyped callers. False positives are\n // acceptable because they bypass cache rather than risking mutation caching.\n if (/\\bmutation\\b/.test(queryText)) return undefined;\n\n return {\n key: createStorefrontCacheKey(apiUrl, init),\n strategy,\n shouldCacheResponse: async ({ json }: FetchCacheResponseContext) => {\n if (!json) return false;\n\n const body = await json().catch(() => undefined);\n if (typeof body !== \"object\" || body == null || Array.isArray(body) || \"errors\" in body) {\n return false;\n }\n\n return shouldCacheResult?.(body) ?? true;\n },\n // A refresh outlives the request that triggered it, so request and caller\n // aborts must not cancel it. It still needs a deadline, even when the caller\n // disabled the client timeout to own the foreground one.\n backgroundSignal: () => AbortSignal.timeout(backgroundTimeoutInMs),\n };\n}\n\nfunction createStorefrontCacheKey(apiUrl: string, init: PlainRequestInit) {\n return [\n \"storefront-api\",\n apiUrl,\n init.method,\n init.body,\n [...init.headers]\n .filter(([name]) => {\n const normalized = name.toLowerCase();\n return (\n REQUEST_CACHE_KEY_HEADERS.has(normalized) && !REQUEST_IDENTITY_HEADERS.has(normalized)\n );\n })\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([name, value]) => `${name}:${value}`)\n .join(\"\\n\"),\n ] as const;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../../../src/client/client.ts"],"sourcesContent":["import type { TadaDocumentNode } from \"gql.tada\";\n\nimport { getHydrogenCacheStatus } from \"../core/cache/cache-status\";\nimport { createFetchWithCache } from \"../core/cache/fetch-with-cache\";\nimport type { FetchCacheOptions, FetchCacheResponseContext } from \"../core/cache/fetch-with-cache\";\nimport type { CacheInstance } from \"../core/cache/run-with-cache\";\nimport type { CachingStrategy } from \"../core/cache/strategies\";\nimport { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from \"../core/constants\";\nimport {\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n STOREFRONT_ACCESS_TOKEN_HEADER,\n STOREFRONT_BUYER_IP_HEADER,\n STOREFRONT_ID_HEADER,\n STOREFRONT_PRIVATE_TOKEN_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n} from \"../core/headers\";\nimport type { I18nConfig, ShopifyRequestContext } from \"../core/request-context\";\nimport { normalizeStoreDomain } from \"../core/url\";\nimport type { AnyStorefrontQueryString } from \"../graphql\";\nimport { StorefrontApiError, StorefrontTimeoutError } from \"./errors\";\nimport { getI18nVariableNames } from \"./i18n-variables\";\nimport type {\n ClientType,\n CreateStorefrontClientArgs,\n GraphQLFormattedError,\n StorefrontClient,\n StorefrontGraphqlResult,\n} from \"./types\";\n\ntype DocLike = TadaDocumentNode<unknown, unknown> | AnyStorefrontQueryString;\ntype FetchInput = Parameters<typeof globalThis.fetch>[0];\ntype FetchInit = Parameters<typeof globalThis.fetch>[1];\ntype ResolvedStorefrontFetch = (\n input: FetchInput,\n init: FetchInit,\n cacheOptions: FetchCacheOptions | undefined,\n) => Promise<Response>;\n\nconst REQUEST_CACHE_KEY_HEADERS = new Set([\n \"content-type\",\n \"user-agent\",\n SDK_VARIANT_HEADER.toLowerCase(),\n SDK_VARIANT_SOURCE_HEADER.toLowerCase(),\n SDK_VERSION_HEADER.toLowerCase(),\n HYDROGEN_VERSION_HEADER.toLowerCase(),\n STOREFRONT_ACCESS_TOKEN_HEADER.toLowerCase(),\n STOREFRONT_PRIVATE_TOKEN_HEADER.toLowerCase(),\n]);\nconst REQUEST_IDENTITY_HEADERS = new Set([\n \"cookie\",\n REQUEST_GROUP_ID_HEADER.toLowerCase(),\n STOREFRONT_BUYER_IP_HEADER.toLowerCase(),\n SHOPIFY_UNIQUE_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_VISIT_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_S_HEADER.toLowerCase(),\n]);\nconst CACHE_ENABLED_GRAPHQL_CLIENTS = new WeakSet<StorefrontClient[\"graphql\"]>();\nconst SHOULD_CACHE_RESULT = Symbol();\n\ntype ShouldCacheResult = (body: object) => boolean;\n\nclass StorefrontCacheConfigError extends Error {}\n\nexport function withStorefrontClientCache<TOptions extends object>(\n client: Pick<StorefrontClient, \"graphql\">,\n options: TOptions,\n strategy: CachingStrategy,\n shouldCacheResult?: ShouldCacheResult,\n): TOptions {\n if (!CACHE_ENABLED_GRAPHQL_CLIENTS.has(client.graphql)) return options;\n\n return { ...options, cache: strategy, [SHOULD_CACHE_RESULT]: shouldCacheResult };\n}\n\n/**\n * Creates a Storefront API client that runs typed queries and mutations against your store.\n *\n * Set `type` based on where your code runs and whether customer context is available:\n *\n * | | `public` | `private` | `private_no_buyer_context` |\n * |------------------|------------------------------|-------------------------------|------------------------------|\n * | Runs in | Browser or mobile | Server-side rendering | Background server code |\n * | Token | Public access token, or none | Private access token | Private access token |\n * | Customer context | Browser request context | You forward trusted `buyerIp` | None |\n * | Best for | Client-side fetches | SSR with customer context | Webhooks, background jobs |\n *\n * Pass a request context from `createShopifyRequestContext`. The client forwards the incoming\n * request's cookies and `Sec-GPC` header to the Storefront API, and fills `$country`\n * and `$language` variables from the context's locale. The client reads the request headers once,\n * when you create the client. Create a client for each incoming request, and create a client at\n * module scope only with a static request context.\n *\n * Keep private clients in server code, where the token can't reach browsers. The function throws an\n * error when you create either private client type in a browser. The function throws a `TypeError`\n * when the request context of a `private` client has no `buyerIp`.\n *\n * @see [Storefront API authentication](https://shopify.dev/docs/api/storefront#authentication)\n * @publicDocs\n */\nexport function createStorefrontClient<\n const Type extends ClientType,\n const RequestContext extends ShopifyRequestContext,\n const CacheConfig extends CacheInstance | undefined = undefined,\n>(\n args: CreateStorefrontClientArgs<RequestContext, Type, CacheConfig>,\n): StorefrontClient<\n // Tuple-wrapped to stay non-distributive so an explicit\n // `CacheInstance | undefined` type argument yields one client type, not a\n // union. Note a *value* of that type still unlocks cache options: optional\n // property inference strips `undefined` before it reaches CacheConfig, so\n // definiteness is undecidable here — the runtime guard rejects cache\n // options when no cache instance was actually configured.\n [CacheConfig] extends [CacheInstance] ? { cache?: CachingStrategy } : {},\n Type,\n RequestContext\n>;\n/**\n * @param args The client type, request context, and store configuration for the new client.\n * @returns The client, with a `graphql()` method that runs queries and mutations.\n */\nexport function createStorefrontClient(args: CreateStorefrontClientArgs): StorefrontClient {\n const { config, requestContext, type: clientType } = args;\n\n if (!config.storeDomain) {\n throw new Error(\"storeDomain is required and must be non-empty\");\n }\n\n const storeUrl = normalizeStoreDomain(config.storeDomain);\n const apiVersion = config.apiVersion ?? STOREFRONT_API_VERSION;\n const apiUrl = `${storeUrl}/api/${apiVersion}/graphql.json`;\n\n const originFetch = config.fetch ?? globalThis.fetch;\n if (typeof originFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n const resolvedFetch = config.cache\n ? createFetchWithCache({\n cache: config.cache,\n waitUntil: config.waitUntil,\n fetch: originFetch as typeof globalThis.fetch,\n })\n : originFetch;\n\n const timeoutInMs = config.defaultTimeoutInMs ?? DEFAULT_TIMEOUT_IN_MS;\n if (timeoutInMs < 0 || Number.isNaN(timeoutInMs)) {\n throw new Error(\"defaultTimeoutInMs must be a non-negative number\");\n }\n\n const staticHeaders: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (config.storefrontId) {\n staticHeaders[STOREFRONT_ID_HEADER] = config.storefrontId;\n }\n\n switch (clientType) {\n case \"private\":\n case \"private_no_buyer_context\":\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"private storefront tokens cannot be used in a browser context. Use a publicStorefrontToken instead.\",\n );\n }\n if (!config.privateStorefrontToken) {\n throw new Error(\"privateStorefrontToken is required\");\n }\n staticHeaders[STOREFRONT_PRIVATE_TOKEN_HEADER] = config.privateStorefrontToken;\n break;\n case \"public\":\n if (config.publicStorefrontToken !== undefined && !config.publicStorefrontToken) {\n throw new Error(\"publicStorefrontToken must be non-empty when provided\");\n }\n if (config.publicStorefrontToken) {\n staticHeaders[STOREFRONT_ACCESS_TOKEN_HEADER] = config.publicStorefrontToken;\n }\n break;\n default:\n clientType satisfies never;\n throw new Error(`Unsupported client type: ${clientType}`);\n }\n\n const i18n = requestContext.i18n;\n const requestHeaders = new Headers(staticHeaders);\n requestContext.applyStorefrontRequestHeaders(requestHeaders);\n\n if (clientType === \"private\") {\n const { buyerIp } = requestContext;\n if (!buyerIp) {\n throw new TypeError(\"requestContext.buyerIp is required for private Storefront API clients\");\n }\n requestHeaders.set(STOREFRONT_BUYER_IP_HEADER, buyerIp);\n }\n\n async function graphql(\n doc: DocLike | string,\n ...rest: [options?: Record<string, unknown>]\n ): Promise<StorefrontGraphqlResult<DocLike>> {\n const opts = (rest[0] ?? {}) as {\n variables?: Record<string, unknown>;\n signal?: AbortSignal;\n cache?: CachingStrategy;\n [SHOULD_CACHE_RESULT]?: ShouldCacheResult;\n };\n\n const queryText = typeof doc === \"string\" ? doc : (doc as unknown as string);\n if (typeof queryText !== \"string\") {\n throw new StorefrontApiError(\n \"Expected gql() to return a string at runtime. Check gql.tada version compatibility.\",\n );\n }\n\n if (opts.cache && !config.cache) {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache options require a cache configured in createStorefrontClient().\",\n );\n }\n\n if (opts.cache?.mode === \"private\") {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache mode 'private' is not supported yet. Use Cache() without private mode, or cache buyer-specific work with createRunWithCache() and an explicit private key.\",\n );\n }\n\n const variables = buildVariables(queryText, opts.variables, i18n);\n const externalSignals = [requestContext.signal, opts.signal].filter(\n (signal): signal is AbortSignal => Boolean(signal),\n );\n\n const earlyAbortSignal = externalSignals.find((signal) => signal.aborted);\n if (earlyAbortSignal) {\n throw (\n earlyAbortSignal.reason ??\n new DOMException(\"signal is aborted without reason\", \"AbortError\")\n );\n }\n\n let requestId: string | undefined;\n let responseHeaders: Headers;\n let responseText: string;\n let status: number;\n let timeoutSignal: AbortSignal | undefined;\n\n if (timeoutInMs > 0) {\n timeoutSignal = AbortSignal.timeout(timeoutInMs);\n externalSignals.push(timeoutSignal);\n }\n\n try {\n const init: PlainRequestInit = {\n method: \"POST\",\n headers: new Headers(requestHeaders),\n body: JSON.stringify({ query: queryText, variables }),\n signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : undefined,\n };\n\n const cacheOptions = createStorefrontCacheOptions(\n apiUrl,\n init,\n queryText,\n opts.cache,\n opts[SHOULD_CACHE_RESULT],\n timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT_IN_MS,\n );\n const response = await (resolvedFetch as ResolvedStorefrontFetch)(apiUrl, init, cacheOptions);\n\n responseHeaders = response.headers;\n const hydrogenCacheStatus = getHydrogenCacheStatus(responseHeaders);\n if (hydrogenCacheStatus !== \"hit\") {\n requestContext.captureSubrequestHeaders(responseHeaders);\n }\n\n requestId = responseHeaders.get(\"x-request-id\") ?? undefined;\n status = response.status;\n\n if (!response.ok) {\n const bodyHint = await response.text().catch(() => \"\");\n throw new StorefrontApiError(\n `SFAPI responded with ${status}${bodyHint ? `: ${bodyHint.slice(0, 200)}` : \"\"}`,\n { status, requestId, queryText, variables },\n );\n }\n\n responseText = await response.text();\n } catch (error) {\n if (error instanceof StorefrontApiError) throw error;\n if (error instanceof StorefrontTimeoutError) throw error;\n if (error instanceof StorefrontCacheConfigError) throw error;\n if (\n error instanceof DOMException &&\n error.name === \"TimeoutError\" &&\n timeoutSignal?.aborted\n ) {\n throw new StorefrontTimeoutError(timeoutInMs);\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new StorefrontApiError(\"SFAPI request failed\", { cause: error });\n }\n\n let json: unknown;\n try {\n json = JSON.parse(responseText);\n } catch (cause) {\n throw new StorefrontApiError(\n `Failed to parse SFAPI response as JSON (${apiUrl}, status ${status})` +\n (responseText ? `: ${responseText.slice(0, 200)}` : \"\"),\n { status, requestId, cause, queryText, variables },\n );\n }\n\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) {\n throw new StorefrontApiError(\n `SFAPI returned unexpected JSON type: ${Array.isArray(json) ? \"array\" : typeof json}`,\n { status, requestId, queryText, variables },\n );\n }\n\n const body = json as { data?: unknown; errors?: GraphQLFormattedError[] };\n // Branch to populate the two arms of the StorefrontGraphqlResult union: callers\n // narrowing on `errors` get a non-null `data` only when no errors are present.\n if (body.errors) {\n return {\n data: body.data ?? null,\n errors: body.errors,\n headers: responseHeaders,\n };\n }\n return {\n data: body.data,\n headers: responseHeaders,\n };\n }\n\n const client: StorefrontClient = {\n type: clientType,\n i18n,\n graphql: graphql as StorefrontClient[\"graphql\"],\n apiUrl,\n storeUrl,\n storefrontId: config.storefrontId,\n requestContext,\n };\n\n if (config.cache) CACHE_ENABLED_GRAPHQL_CLIENTS.add(client.graphql);\n\n return client;\n}\n\nfunction buildVariables(\n queryText: string,\n userVariables: Record<string, unknown> | undefined,\n resolvedI18n: I18nConfig,\n): Record<string, unknown> {\n const variables: Record<string, unknown> = { ...userVariables };\n\n for (const name of getI18nVariableNames(queryText)) {\n variables[name] = resolvedI18n[name];\n }\n\n return variables;\n}\n\ntype PlainRequestInit = RequestInit & { headers: Headers; body: string; method: string };\n\nfunction createStorefrontCacheOptions(\n apiUrl: string,\n init: PlainRequestInit,\n queryText: string,\n strategy: CachingStrategy | undefined,\n shouldCacheResult: ShouldCacheResult | undefined,\n backgroundTimeoutInMs: number,\n): FetchCacheOptions | undefined {\n if (!strategy) return undefined;\n\n // Runtime only needs a safety belt for untyped callers. False positives are\n // acceptable because they bypass cache rather than risking mutation caching.\n if (/\\bmutation\\b/.test(queryText)) return undefined;\n\n return {\n key: createStorefrontCacheKey(apiUrl, init),\n strategy,\n shouldCacheResponse: async ({ json }: FetchCacheResponseContext) => {\n if (!json) return false;\n\n const body = await json().catch(() => undefined);\n if (typeof body !== \"object\" || body == null || Array.isArray(body) || \"errors\" in body) {\n return false;\n }\n\n return shouldCacheResult?.(body) ?? true;\n },\n // A refresh outlives the request that triggered it, so request and caller\n // aborts must not cancel it. It still needs a deadline, even when the caller\n // disabled the client timeout to own the foreground one.\n backgroundSignal: () => AbortSignal.timeout(backgroundTimeoutInMs),\n };\n}\n\nfunction createStorefrontCacheKey(apiUrl: string, init: PlainRequestInit) {\n return [\n \"storefront-api\",\n apiUrl,\n init.method,\n init.body,\n [...init.headers]\n .filter(([name]) => {\n const normalized = name.toLowerCase();\n return (\n REQUEST_CACHE_KEY_HEADERS.has(normalized) && !REQUEST_IDENTITY_HEADERS.has(normalized)\n );\n })\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([name, value]) => `${name}:${value}`)\n .join(\"\\n\"),\n ] as const;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/client/i18n-variables.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Variables the client fills from `requestContext.i18n`, with the named type
|
|
4
|
+
* each must be declared as. Same-named variables of any other type belong to
|
|
5
|
+
* the caller.
|
|
6
|
+
*/
|
|
7
|
+
declare const I18N_VARIABLE_TYPES: {
|
|
8
|
+
readonly country: "CountryCode";
|
|
9
|
+
readonly language: "LanguageCode";
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { I18N_VARIABLE_TYPES };
|
|
13
|
+
//# sourceMappingURL=i18n-variables.d.mts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/client/i18n-variables.ts
|
|
2
|
+
/**
|
|
3
|
+
* Variables the client fills from `requestContext.i18n`, with the named type
|
|
4
|
+
* each must be declared as. Same-named variables of any other type belong to
|
|
5
|
+
* the caller.
|
|
6
|
+
*/
|
|
7
|
+
const I18N_VARIABLE_TYPES = {
|
|
8
|
+
country: "CountryCode",
|
|
9
|
+
language: "LanguageCode"
|
|
10
|
+
};
|
|
11
|
+
const TOKEN_RE = /"""(?:\\"""|[^])*?"""|"(?:\\.|[^"\\\n\r])*"|#[^\n\r]*|[_A-Za-z][_0-9A-Za-z]*|[^\s,]/g;
|
|
12
|
+
const NAME_RE = /^[_A-Za-z]/;
|
|
13
|
+
const OPERATION_TYPES = new Set([
|
|
14
|
+
"query",
|
|
15
|
+
"mutation",
|
|
16
|
+
"subscription"
|
|
17
|
+
]);
|
|
18
|
+
const OPENING = new Set([
|
|
19
|
+
"(",
|
|
20
|
+
"[",
|
|
21
|
+
"{"
|
|
22
|
+
]);
|
|
23
|
+
const CLOSING = new Set([
|
|
24
|
+
")",
|
|
25
|
+
"]",
|
|
26
|
+
"}"
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Returns the i18n variables that the document's first operation declares as
|
|
30
|
+
* `$country: CountryCode` or `$language: LanguageCode`, nullable or non-null.
|
|
31
|
+
* This is the same rule the `graphql()` variables type applies.
|
|
32
|
+
*/
|
|
33
|
+
function getI18nVariableNames(source) {
|
|
34
|
+
const names = [];
|
|
35
|
+
for (const [name, colon, ...rest] of readFirstOperationVariables(tokenize(source))) {
|
|
36
|
+
if (colon !== ":" || !isI18nVariableName(name)) continue;
|
|
37
|
+
const typeEnd = rest.findIndex((token) => token === "=" || token === "@");
|
|
38
|
+
const type = rest.slice(0, typeEnd === -1 ? void 0 : typeEnd).join("");
|
|
39
|
+
const expected = I18N_VARIABLE_TYPES[name];
|
|
40
|
+
if (type === expected || type === `${expected}!`) names.push(name);
|
|
41
|
+
}
|
|
42
|
+
return names;
|
|
43
|
+
}
|
|
44
|
+
function isI18nVariableName(name) {
|
|
45
|
+
return name !== void 0 && Object.hasOwn(I18N_VARIABLE_TYPES, name);
|
|
46
|
+
}
|
|
47
|
+
function* tokenize(source) {
|
|
48
|
+
for (const [token] of source.matchAll(TOKEN_RE)) if (token[0] !== "#") yield token;
|
|
49
|
+
}
|
|
50
|
+
function readFirstOperationVariables(tokens) {
|
|
51
|
+
let depth = 0;
|
|
52
|
+
let inFragment = false;
|
|
53
|
+
for (const token of tokens) {
|
|
54
|
+
if (depth === 0 && !inFragment) {
|
|
55
|
+
if (OPERATION_TYPES.has(token)) return readOperationVariables(tokens);
|
|
56
|
+
if (token === "{") return [];
|
|
57
|
+
inFragment = token === "fragment";
|
|
58
|
+
}
|
|
59
|
+
if (OPENING.has(token)) depth++;
|
|
60
|
+
else if (CLOSING.has(token) && --depth === 0 && token === "}") inFragment = false;
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
function readOperationVariables(tokens) {
|
|
65
|
+
let next = tokens.next().value;
|
|
66
|
+
if (next && NAME_RE.test(next)) next = tokens.next().value;
|
|
67
|
+
return next === "(" ? readVariableDefinitions(tokens) : [];
|
|
68
|
+
}
|
|
69
|
+
function readVariableDefinitions(tokens) {
|
|
70
|
+
const definitions = [];
|
|
71
|
+
let depth = 0;
|
|
72
|
+
for (const token of tokens) {
|
|
73
|
+
if (depth === 0 && token === ")") break;
|
|
74
|
+
if (depth === 0 && token === "$") definitions.push([]);
|
|
75
|
+
else definitions.at(-1)?.push(token);
|
|
76
|
+
if (OPENING.has(token)) depth++;
|
|
77
|
+
else if (CLOSING.has(token)) depth--;
|
|
78
|
+
}
|
|
79
|
+
return definitions;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { getI18nVariableNames };
|
|
84
|
+
//# sourceMappingURL=i18n-variables.mjs.map
|