@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
package/dist/cli/index.mjs
CHANGED
|
@@ -1048,16 +1048,17 @@ async function scaffoldTemplate(appRoot, env, runCommand, log) {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
async function ensureHydrogenInstalled(appRoot, env, runCommand, log) {
|
|
1050
1050
|
const packageJson = readPackageJson(appRoot);
|
|
1051
|
-
if (hasHydrogenDependency(packageJson) && getInstalledPackageRoot(appRoot)) return;
|
|
1051
|
+
if (hasHydrogenDependency(packageJson) && getInstalledPackageRoot(appRoot)) return { addedHydrogen: false };
|
|
1052
1052
|
if (hasHydrogenDependency(packageJson)) {
|
|
1053
1053
|
const packageManager = detectPackageManager(appRoot, packageJson, env);
|
|
1054
1054
|
log(`${PACKAGE_NAME} is declared but not installed. Running ${packageManager} install...`);
|
|
1055
1055
|
await runCommand(packageManager, ["install"], { cwd: appRoot });
|
|
1056
|
-
return;
|
|
1056
|
+
return { addedHydrogen: false };
|
|
1057
1057
|
}
|
|
1058
1058
|
const packageManager = detectPackageManager(appRoot, packageJson, env);
|
|
1059
1059
|
log(`Installing ${PACKAGE_NAME} with ${packageManager}...`);
|
|
1060
1060
|
await installHydrogen(appRoot, packageManager, runCommand);
|
|
1061
|
+
return { addedHydrogen: true };
|
|
1061
1062
|
}
|
|
1062
1063
|
function resolveSetupOptions(options) {
|
|
1063
1064
|
const env = options.env ?? process.env;
|
|
@@ -1075,7 +1076,8 @@ function isEmptyDirectory(dir) {
|
|
|
1075
1076
|
}
|
|
1076
1077
|
async function setupHydrogen(options = {}) {
|
|
1077
1078
|
const { appRoot, env, runCommand, log, prompt } = resolveSetupOptions(options);
|
|
1078
|
-
|
|
1079
|
+
let addedHydrogen = false;
|
|
1080
|
+
if (hasPackageJson(appRoot)) ({addedHydrogen} = await ensureHydrogenInstalled(appRoot, env, runCommand, log));
|
|
1079
1081
|
else if (isEmptyDirectory(appRoot)) {
|
|
1080
1082
|
let choice;
|
|
1081
1083
|
if (prompt) choice = await prompt(log);
|
|
@@ -1091,6 +1093,7 @@ async function setupHydrogen(options = {}) {
|
|
|
1091
1093
|
packageRoot: options.packageRoot,
|
|
1092
1094
|
log
|
|
1093
1095
|
});
|
|
1096
|
+
if (addedHydrogen) log("Setup is ready. Next: ask your agent to use the hydrogen-setup skill.");
|
|
1094
1097
|
}
|
|
1095
1098
|
|
|
1096
1099
|
//#endregion
|
package/dist/client/client.mjs
CHANGED
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n-variables.mjs","names":[],"sources":["../../src/client/i18n-variables.ts"],"sourcesContent":["/**\n * Variables the client fills from `requestContext.i18n`, with the named type\n * each must be declared as. Same-named variables of any other type belong to\n * the caller.\n */\nexport const I18N_VARIABLE_TYPES = {\n country: \"CountryCode\",\n language: \"LanguageCode\",\n} as const;\n\nexport type I18nVariableName = keyof typeof I18N_VARIABLE_TYPES;\n\n// Significant GraphQL tokens plus comments, which are dropped. Matching whole\n// strings keeps their contents from being read as declarations.\nconst TOKEN_RE =\n /\"\"\"(?:\\\\\"\"\"|[^])*?\"\"\"|\"(?:\\\\.|[^\"\\\\\\n\\r])*\"|#[^\\n\\r]*|[_A-Za-z][_0-9A-Za-z]*|[^\\s,]/g;\nconst NAME_RE = /^[_A-Za-z]/;\nconst OPERATION_TYPES = new Set([\"query\", \"mutation\", \"subscription\"]);\nconst OPENING = new Set([\"(\", \"[\", \"{\"]);\nconst CLOSING = new Set([\")\", \"]\", \"}\"]);\n\n/**\n * Returns the i18n variables that the document's first operation declares as\n * `$country: CountryCode` or `$language: LanguageCode`, nullable or non-null.\n * This is the same rule the `graphql()` variables type applies.\n */\nexport function getI18nVariableNames(source: string): I18nVariableName[] {\n const names: I18nVariableName[] = [];\n for (const [name, colon, ...rest] of readFirstOperationVariables(tokenize(source))) {\n if (colon !== \":\" || !isI18nVariableName(name)) continue;\n const typeEnd = rest.findIndex((token) => token === \"=\" || token === \"@\");\n const type = rest.slice(0, typeEnd === -1 ? undefined : typeEnd).join(\"\");\n const expected = I18N_VARIABLE_TYPES[name];\n if (type === expected || type === `${expected}!`) names.push(name);\n }\n return names;\n}\n\nfunction isI18nVariableName(name: string | undefined): name is I18nVariableName {\n return name !== undefined && Object.hasOwn(I18N_VARIABLE_TYPES, name);\n}\n\nfunction* tokenize(source: string): Generator<string> {\n for (const [token] of source.matchAll(TOKEN_RE)) {\n if (token[0] !== \"#\") yield token;\n }\n}\n\nfunction readFirstOperationVariables(tokens: Generator<string>): string[][] {\n let depth = 0;\n let inFragment = false;\n for (const token of tokens) {\n if (depth === 0 && !inFragment) {\n if (OPERATION_TYPES.has(token)) return readOperationVariables(tokens);\n if (token === \"{\") return [];\n inFragment = token === \"fragment\";\n }\n if (OPENING.has(token)) depth++;\n else if (CLOSING.has(token) && --depth === 0 && token === \"}\") inFragment = false;\n }\n return [];\n}\n\nfunction readOperationVariables(tokens: Generator<string>): string[][] {\n let next = tokens.next().value;\n if (next && NAME_RE.test(next)) next = tokens.next().value;\n return next === \"(\" ? readVariableDefinitions(tokens) : [];\n}\n\n// Default values and directive arguments can nest `$` and `)`, so only depth 0 delimits definitions.\nfunction readVariableDefinitions(tokens: Generator<string>): string[][] {\n const definitions: string[][] = [];\n let depth = 0;\n for (const token of tokens) {\n if (depth === 0 && token === \")\") break;\n if (depth === 0 && token === \"$\") definitions.push([]);\n else definitions.at(-1)?.push(token);\n if (OPENING.has(token)) depth++;\n else if (CLOSING.has(token)) depth--;\n }\n return definitions;\n}\n"],"mappings":";;;;;;AAKA,MAAa,sBAAsB;CACjC,SAAS;CACT,UAAU;CACX;AAMD,MAAM,WACJ;AACF,MAAM,UAAU;AAChB,MAAM,kBAAkB,IAAI,IAAI;CAAC;CAAS;CAAY;CAAe,CAAC;AACtE,MAAM,UAAU,IAAI,IAAI;CAAC;CAAK;CAAK;CAAI,CAAC;AACxC,MAAM,UAAU,IAAI,IAAI;CAAC;CAAK;CAAK;CAAI,CAAC;;;;;;AAOxC,SAAgB,qBAAqB,QAAoC;CACvE,MAAM,QAA4B,EAAE;AACpC,MAAK,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,4BAA4B,SAAS,OAAO,CAAC,EAAE;AAClF,MAAI,UAAU,OAAO,CAAC,mBAAmB,KAAK,CAAE;EAChD,MAAM,UAAU,KAAK,WAAW,UAAU,UAAU,OAAO,UAAU,IAAI;EACzE,MAAM,OAAO,KAAK,MAAM,GAAG,YAAY,KAAK,SAAY,QAAQ,CAAC,KAAK,GAAG;EACzE,MAAM,WAAW,oBAAoB;AACrC,MAAI,SAAS,YAAY,SAAS,GAAG,SAAS,GAAI,OAAM,KAAK,KAAK;;AAEpE,QAAO;;AAGT,SAAS,mBAAmB,MAAoD;AAC9E,QAAO,SAAS,UAAa,OAAO,OAAO,qBAAqB,KAAK;;AAGvE,UAAU,SAAS,QAAmC;AACpD,MAAK,MAAM,CAAC,UAAU,OAAO,SAAS,SAAS,CAC7C,KAAI,MAAM,OAAO,IAAK,OAAM;;AAIhC,SAAS,4BAA4B,QAAuC;CAC1E,IAAI,QAAQ;CACZ,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,UAAU,KAAK,CAAC,YAAY;AAC9B,OAAI,gBAAgB,IAAI,MAAM,CAAE,QAAO,uBAAuB,OAAO;AACrE,OAAI,UAAU,IAAK,QAAO,EAAE;AAC5B,gBAAa,UAAU;;AAEzB,MAAI,QAAQ,IAAI,MAAM,CAAE;WACf,QAAQ,IAAI,MAAM,IAAI,EAAE,UAAU,KAAK,UAAU,IAAK,cAAa;;AAE9E,QAAO,EAAE;;AAGX,SAAS,uBAAuB,QAAuC;CACrE,IAAI,OAAO,OAAO,MAAM,CAAC;AACzB,KAAI,QAAQ,QAAQ,KAAK,KAAK,CAAE,QAAO,OAAO,MAAM,CAAC;AACrD,QAAO,SAAS,MAAM,wBAAwB,OAAO,GAAG,EAAE;;AAI5D,SAAS,wBAAwB,QAAuC;CACtE,MAAM,cAA0B,EAAE;CAClC,IAAI,QAAQ;AACZ,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,UAAU,KAAK,UAAU,IAAK;AAClC,MAAI,UAAU,KAAK,UAAU,IAAK,aAAY,KAAK,EAAE,CAAC;MACjD,aAAY,GAAG,GAAG,EAAE,KAAK,MAAM;AACpC,MAAI,QAAQ,IAAI,MAAM,CAAE;WACf,QAAQ,IAAI,MAAM,CAAE;;AAE/B,QAAO"}
|
package/dist/client/types.d.mts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CacheInstance, WaitUntil } from "../core/cache/run-with-cache.mjs";
|
|
2
2
|
import { I18nConfig, ShopifyRequestContext, ShopifyRequestContextWithBuyerIp } from "../core/request-context.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { introspection } from "../graphql/generated/graphql-env.mjs";
|
|
4
|
+
import { InferOperationKind, InferResult, InferVariableNamesDeclaredAs, InferVariables } from "../graphql/type-resolver.mjs";
|
|
4
5
|
import { AnyStorefrontQueryString, SourceOf, StorefrontQueryString } from "../graphql/graphql.mjs";
|
|
6
|
+
import { I18N_VARIABLE_TYPES } from "./i18n-variables.mjs";
|
|
5
7
|
import { ResultOf, TadaDocumentNode, VariablesOf } from "gql.tada";
|
|
6
8
|
|
|
7
9
|
//#region src/client/types.d.ts
|
|
@@ -140,9 +142,13 @@ type CreateStorefrontClientArgs<RequestContext extends ShopifyRequestContext = S
|
|
|
140
142
|
cache?: CacheConfig;
|
|
141
143
|
};
|
|
142
144
|
};
|
|
143
|
-
type
|
|
144
|
-
|
|
145
|
-
type
|
|
145
|
+
type I18nVariableTypes = typeof I18N_VARIABLE_TYPES;
|
|
146
|
+
type I18nEnum<K extends keyof I18nVariableTypes> = introspection["types"][I18nVariableTypes[K]]["enumValues"];
|
|
147
|
+
type IsExactly<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
|
|
148
|
+
type I18nVariableNamesByValue<Variables> = { [K in keyof Variables & keyof I18nVariableTypes]-?: IsExactly<NonNullable<Variables[K]>, I18nEnum<K>> extends true ? K : never }[keyof Variables & keyof I18nVariableTypes];
|
|
149
|
+
type I18nVariableNamesOfDoc<Doc> = [SourceText<Doc>] extends [never] ? I18nVariableNamesByValue<VariablesOfDoc<Doc>> : string extends SourceText<Doc> ? I18nVariableNamesByValue<VariablesOfDoc<Doc>> : InferVariableNamesDeclaredAs<SourceText<Doc>, I18nVariableTypes>;
|
|
150
|
+
/** Variables the caller passes: everything except the i18n variables the client injects. */
|
|
151
|
+
type UserVariables<Doc> = Omit<VariablesOfDoc<Doc>, I18nVariableNamesOfDoc<Doc>>;
|
|
146
152
|
/** Resolves to `true` when an object type has no required keys. */
|
|
147
153
|
type HasNoRequiredKeys<T> = Record<string, never> extends T ? true : false;
|
|
148
154
|
/**
|
|
@@ -213,8 +219,9 @@ type StorefrontClient<Extra extends Record<string, unknown> = {}, Type extends C
|
|
|
213
219
|
i18n: RequestContext["i18n"];
|
|
214
220
|
/**
|
|
215
221
|
* Runs a Storefront API query or mutation and returns a typed result. Pass a `gql` document or a
|
|
216
|
-
* plain query string. When the operation declares `$country` or
|
|
217
|
-
* variable from the request context and overrides
|
|
222
|
+
* plain query string. When the operation declares `$country: CountryCode` or
|
|
223
|
+
* `$language: LanguageCode`, the client sets the variable from the request context and overrides
|
|
224
|
+
* any value you pass. Variables with other types stay caller-owned.
|
|
218
225
|
*
|
|
219
226
|
* The method throws an error when a query passes a `cache` strategy and the client has no cache,
|
|
220
227
|
* or when the strategy uses `private` mode. A catch block for `StorefrontApiError` doesn't catch
|
package/dist/core/cart/cart.mjs
CHANGED
|
@@ -203,13 +203,18 @@ function reuseVisibleReferences(previous, next) {
|
|
|
203
203
|
pending
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
|
-
function
|
|
206
|
+
function getVisiblePendingKeys(store, transaction) {
|
|
207
|
+
if (transaction.type !== "change_lines_quantity") return transaction.pendingKeys;
|
|
208
|
+
const ownedKeys = getOwnedSignalKeys(store, transaction);
|
|
209
|
+
return transaction.pendingKeys.filter((key) => !transaction.signalKeys.includes(key) || ownedKeys.includes(key));
|
|
210
|
+
}
|
|
211
|
+
function derivePending(store, state) {
|
|
207
212
|
const lines = /* @__PURE__ */ new Set();
|
|
208
213
|
const discountCodes = /* @__PURE__ */ new Set();
|
|
209
214
|
let note = false;
|
|
210
215
|
let attributes = false;
|
|
211
216
|
let cost = false;
|
|
212
|
-
for (const transaction of transactions) for (const key of transaction
|
|
217
|
+
for (const transaction of store.transactions) for (const key of getVisiblePendingKeys(store, transaction)) {
|
|
213
218
|
if (key === NOTE_KEY) note = true;
|
|
214
219
|
if (key === ATTRIBUTES_KEY) attributes = true;
|
|
215
220
|
if (key.startsWith(LINE_KEY_PREFIX)) {
|
|
@@ -236,10 +241,13 @@ function derivePending(state, transactions) {
|
|
|
236
241
|
}
|
|
237
242
|
function projectVisibleState(store) {
|
|
238
243
|
let state = store.settled;
|
|
239
|
-
for (const transaction of store.transactions)
|
|
244
|
+
for (const transaction of store.transactions) {
|
|
245
|
+
const ownedKeys = getOwnedSignalKeys(store, transaction);
|
|
246
|
+
state = transaction.projectPayload(state, (key) => ownedKeys.includes(key));
|
|
247
|
+
}
|
|
240
248
|
state = {
|
|
241
249
|
...state,
|
|
242
|
-
pending: derivePending(
|
|
250
|
+
pending: derivePending(store, state)
|
|
243
251
|
};
|
|
244
252
|
for (const error of store.projectedErrors) state = error.project(state);
|
|
245
253
|
if (store.revalidation.visible) state = {
|
|
@@ -288,9 +296,9 @@ function createOwner(store, keys) {
|
|
|
288
296
|
}
|
|
289
297
|
return owner;
|
|
290
298
|
}
|
|
291
|
-
function
|
|
292
|
-
if (!transaction.owner) return
|
|
293
|
-
return transaction.signalKeys.
|
|
299
|
+
function getOwnedSignalKeys(store, transaction) {
|
|
300
|
+
if (!transaction.owner) return transaction.signalKeys;
|
|
301
|
+
return transaction.signalKeys.filter((key) => store.keyedOwners.get(key)?.token === transaction.owner?.token);
|
|
294
302
|
}
|
|
295
303
|
function releaseSignalKeys(store, transaction) {
|
|
296
304
|
if (!transaction.owner) return;
|
|
@@ -413,7 +421,9 @@ function writeNetworkError(state, error, timestampMs) {
|
|
|
413
421
|
}
|
|
414
422
|
function projectLineErrors(state, failure, lineIds, timestampMs) {
|
|
415
423
|
if (!failure) return state;
|
|
416
|
-
|
|
424
|
+
return writeLineErrors(state, groupLineErrors(failure, lineIds), timestampMs);
|
|
425
|
+
}
|
|
426
|
+
function writeLineErrors(state, grouped, timestampMs) {
|
|
417
427
|
return {
|
|
418
428
|
...state,
|
|
419
429
|
errors: {
|
|
@@ -567,6 +577,22 @@ function mergeServerLine(previous, next) {
|
|
|
567
577
|
...merchandise ? { merchandise } : {}
|
|
568
578
|
};
|
|
569
579
|
}
|
|
580
|
+
function addOwnedLineErrors(failure, lineIds, ownedLineIds, addError) {
|
|
581
|
+
const grouped = groupLineErrors(failure, lineIds);
|
|
582
|
+
for (const [lineId, group] of grouped.lines) {
|
|
583
|
+
if (lineIds.includes(lineId) && !ownedLineIds.has(lineId)) continue;
|
|
584
|
+
const lines = new Map([[lineId, group]]);
|
|
585
|
+
addError((current, timestampMs) => writeLineErrors(current, {
|
|
586
|
+
lines,
|
|
587
|
+
cart: createEmptyErrorGroup()
|
|
588
|
+
}, timestampMs), [lineKey(lineId)]);
|
|
589
|
+
}
|
|
590
|
+
if (grouped.cart.userErrors.length === 0 && grouped.cart.warnings.length === 0) return;
|
|
591
|
+
addError((current, timestampMs) => writeLineErrors(current, {
|
|
592
|
+
lines: /* @__PURE__ */ new Map(),
|
|
593
|
+
cart: grouped.cart
|
|
594
|
+
}, timestampMs), [...ownedLineIds].map(lineKey));
|
|
595
|
+
}
|
|
570
596
|
function hasProjectedErrors(result) {
|
|
571
597
|
return (result?.userErrors?.length ?? 0) > 0 || (result?.warnings?.length ?? 0) > 0;
|
|
572
598
|
}
|
|
@@ -579,6 +605,31 @@ function transportAddToCart(payload, signal, updateCart, cartId) {
|
|
|
579
605
|
...payload.eventDetail && { event: { detail: payload.eventDetail } }
|
|
580
606
|
});
|
|
581
607
|
}
|
|
608
|
+
function projectLineQuantity(state, payload) {
|
|
609
|
+
const previous = getLines(state.data);
|
|
610
|
+
if (!previous.some((line) => line.id === payload.lineId)) return state;
|
|
611
|
+
const lines = payload.quantity === 0 ? previous.filter((line) => line.id !== payload.lineId) : previous.map((line) => line.id === payload.lineId ? {
|
|
612
|
+
...line,
|
|
613
|
+
quantity: payload.quantity
|
|
614
|
+
} : line);
|
|
615
|
+
return {
|
|
616
|
+
...state,
|
|
617
|
+
data: reconcileCartLines(state.data, lines)
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function settleLineQuantity(state, serverLines, payload, requestRevalidation) {
|
|
621
|
+
const previous = getLines(state.data);
|
|
622
|
+
const matching = serverLines.find((line) => line.id === payload.lineId);
|
|
623
|
+
if (!matching && payload.quantity > 0) {
|
|
624
|
+
requestRevalidation();
|
|
625
|
+
return state;
|
|
626
|
+
}
|
|
627
|
+
const lines = matching ? previous.map((line) => line.id === payload.lineId ? mergeServerLine(line, matching) : line) : previous.filter((line) => line.id !== payload.lineId);
|
|
628
|
+
return {
|
|
629
|
+
...state,
|
|
630
|
+
data: reconcileCartLines(state.data, lines)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
582
633
|
const CART_TRANSACTION_TYPES = defineTransactionTypes({
|
|
583
634
|
add_to_cart: {
|
|
584
635
|
payload: {},
|
|
@@ -702,6 +753,22 @@ const CART_TRANSACTION_TYPES = defineTransactionTypes({
|
|
|
702
753
|
},
|
|
703
754
|
getSignalKeys: (_state, payload) => lineKey(payload.lineId)
|
|
704
755
|
},
|
|
756
|
+
change_lines_quantity: {
|
|
757
|
+
payload: {},
|
|
758
|
+
transport: (payload, signal, updateCart) => updateCart({ lines: payload.lines.map(({ lineId, quantity }) => ({
|
|
759
|
+
id: lineId,
|
|
760
|
+
quantity
|
|
761
|
+
})) }, { signal }),
|
|
762
|
+
projectPayload: (state, payload, ownsKey) => payload.lines.filter(({ lineId }) => ownsKey(lineKey(lineId))).reduce(projectLineQuantity, state),
|
|
763
|
+
projectPromise: (state, result, payload, addError, { ownsKey, requestRevalidation }) => {
|
|
764
|
+
const owned = payload.lines.filter(({ lineId }) => ownsKey(lineKey(lineId)));
|
|
765
|
+
if (hasProjectedErrors(result)) addOwnedLineErrors(result, payload.lines.map(({ lineId }) => lineId), new Set(owned.map(({ lineId }) => lineId)), addError);
|
|
766
|
+
if (!result.cart) return state;
|
|
767
|
+
const serverLines = getLines(cartResponseFromStandardEvent(result.cart));
|
|
768
|
+
return owned.reduce((current, line) => settleLineQuantity(current, serverLines, line, requestRevalidation), state);
|
|
769
|
+
},
|
|
770
|
+
getSignalKeys: (_state, payload) => payload.lines.map(({ lineId }) => lineKey(lineId))
|
|
771
|
+
},
|
|
705
772
|
set_discount_codes: {
|
|
706
773
|
payload: {},
|
|
707
774
|
transport: (payload, signal, updateCart) => updateCart({ discountCodes: payload.discountCodes }, { signal }),
|
|
@@ -844,8 +911,12 @@ function createPendingTransaction(store, type, definition, payload, promise, res
|
|
|
844
911
|
requiresRevalidation,
|
|
845
912
|
owner,
|
|
846
913
|
promise,
|
|
847
|
-
projectPayload: (current) => definition.projectPayload(current, payload),
|
|
848
|
-
projectPromise: (current, result, addError) => definition.projectPromise(current, result, payload, addError, {
|
|
914
|
+
projectPayload: (current, ownsKey) => definition.projectPayload(current, payload, ownsKey),
|
|
915
|
+
projectPromise: (current, result, addError, ownsKey, requestRevalidation) => definition.projectPromise(current, result, payload, addError, {
|
|
916
|
+
mergeServerCart: !transaction.requiresRevalidation,
|
|
917
|
+
ownsKey,
|
|
918
|
+
requestRevalidation
|
|
919
|
+
}),
|
|
849
920
|
trimAfter: (successful) => trimPendingTransaction(store, type, definition, payload, promise, {
|
|
850
921
|
id,
|
|
851
922
|
sequence,
|
|
@@ -915,15 +986,16 @@ function fulfillTransaction(store, transactionId, result) {
|
|
|
915
986
|
const transaction = store.transactions.find((candidate) => candidate.id === transactionId);
|
|
916
987
|
if (!transaction || transaction.generation !== store.generation) return;
|
|
917
988
|
store.observedPromises.delete(transaction.promise);
|
|
918
|
-
|
|
989
|
+
const ownedKeys = getOwnedSignalKeys(store, transaction);
|
|
990
|
+
if (ownedKeys.length === 0 && transaction.signalKeys.length > 0) {
|
|
919
991
|
removeTransaction(store, transactionId);
|
|
920
992
|
publishVisibleState(store);
|
|
921
993
|
revalidateCartWhenIdle(store);
|
|
922
994
|
return;
|
|
923
995
|
}
|
|
924
|
-
store.settled = transaction.projectPromise(store.settled, result, (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector));
|
|
996
|
+
store.settled = transaction.projectPromise(store.settled, result, (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector), (key) => ownedKeys.includes(key), () => requestCartRevalidation(store));
|
|
925
997
|
store.settled = addSnapshotIdentity(store.settled, result);
|
|
926
|
-
if (result.cart && !transaction.requiresRevalidation && transaction.sequence >= store.lastSnapshotSequence) {
|
|
998
|
+
if (result.cart && ownedKeys.length === transaction.signalKeys.length && !transaction.requiresRevalidation && transaction.sequence >= store.lastSnapshotSequence) {
|
|
927
999
|
store.settled = addSnapshotFields(store.settled, result);
|
|
928
1000
|
store.lastSnapshotSequence = transaction.sequence;
|
|
929
1001
|
}
|
|
@@ -943,13 +1015,14 @@ function rejectTransaction(store, transactionId, error) {
|
|
|
943
1015
|
const transaction = store.transactions.find((candidate) => candidate.id === transactionId);
|
|
944
1016
|
if (!transaction || transaction.generation !== store.generation) return;
|
|
945
1017
|
store.observedPromises.delete(transaction.promise);
|
|
946
|
-
const
|
|
1018
|
+
const ownedKeys = getOwnedSignalKeys(store, transaction);
|
|
1019
|
+
const ownsAllKeys = ownedKeys.length === transaction.signalKeys.length;
|
|
947
1020
|
removeTransaction(store, transactionId);
|
|
948
1021
|
releaseSignalKeys(store, transaction);
|
|
949
|
-
if (
|
|
1022
|
+
if (ownedKeys.length > 0 || ownsAllKeys) {
|
|
950
1023
|
const failure = extractCartActionFailure(error);
|
|
951
|
-
if (failure) transaction.projectPromise(store.settled, rejectedResult(failure), (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector));
|
|
952
|
-
if (!failure && !isAbortError(error)) addProjectedError(store, transaction.errorKeys, (state, timestampMs) => writeNetworkError(state, error, timestampMs));
|
|
1024
|
+
if (failure) transaction.projectPromise(store.settled, rejectedResult(failure), (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector), (key) => ownedKeys.includes(key), NOOP);
|
|
1025
|
+
if (!failure && !isAbortError(error)) addProjectedError(store, ownsAllKeys ? transaction.errorKeys : ownedKeys, (state, timestampMs) => writeNetworkError(state, error, timestampMs));
|
|
953
1026
|
}
|
|
954
1027
|
publishVisibleState(store);
|
|
955
1028
|
revalidateCartWhenIdle(store);
|
|
@@ -980,7 +1053,11 @@ function enqueueTransaction(store, type, payload, promise, reservedTransaction,
|
|
|
980
1053
|
promise.then((result) => fulfillTransaction(store, transaction.id, result), (error) => rejectTransaction(store, transaction.id, error));
|
|
981
1054
|
}
|
|
982
1055
|
function markOverlappingTransactionForRevalidation(store, transaction) {
|
|
983
|
-
if (store.transactions.length === 0 && !store.revalidation.active && store.activeMutationTransports.size === 0) return;
|
|
1056
|
+
if (store.transactions.length === 0 && !store.revalidation.active && store.activeMutationTransports.size === 0 && !store.activeCartLoad) return;
|
|
1057
|
+
if (store.activeCartLoad) store.settled = {
|
|
1058
|
+
...store.settled,
|
|
1059
|
+
loading: false
|
|
1060
|
+
};
|
|
984
1061
|
transaction.requiresRevalidation = true;
|
|
985
1062
|
for (const pending of store.transactions) pending.requiresRevalidation = true;
|
|
986
1063
|
requestCartRevalidation(store);
|
|
@@ -1173,12 +1250,16 @@ function handleLinesEvent(store, event) {
|
|
|
1173
1250
|
enqueueTransaction(store, "add_to_cart", payloadFromAddEvent(event), event.promise, void 0, eventToken);
|
|
1174
1251
|
return;
|
|
1175
1252
|
}
|
|
1176
|
-
|
|
1177
|
-
const line = event.lines[0];
|
|
1178
|
-
enqueueTransaction(store, "change_line_quantity", {
|
|
1253
|
+
const lines = event.lines.map((line) => ({
|
|
1179
1254
|
lineId: line.id,
|
|
1180
1255
|
quantity: line.quantity
|
|
1181
|
-
}
|
|
1256
|
+
}));
|
|
1257
|
+
if (lines.length === 0) return;
|
|
1258
|
+
if (lines.length === 1) {
|
|
1259
|
+
enqueueTransaction(store, "change_line_quantity", lines[0], event.promise, void 0, eventToken);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
enqueueTransaction(store, "change_lines_quantity", { lines }, event.promise, void 0, eventToken);
|
|
1182
1263
|
}
|
|
1183
1264
|
function handleDiscountEvent(store, event) {
|
|
1184
1265
|
enqueueTransaction(store, "set_discount_codes", { discountCodes: event.discountCodes.map((discount) => discount.code) }, event.promise, void 0, getTransactionEventToken(event.detail));
|
|
@@ -1201,6 +1282,10 @@ function connectCartStore(store, handlers) {
|
|
|
1201
1282
|
document.addEventListener("shopify:cart:note-update", handlers.note);
|
|
1202
1283
|
document.addEventListener("shopify:cart:attributes-update", handlers.attributes);
|
|
1203
1284
|
getShopifyStandardActions().catch(() => {});
|
|
1285
|
+
if (store.revalidateOnConnect) {
|
|
1286
|
+
store.revalidateOnConnect = false;
|
|
1287
|
+
refreshCartInStore(store);
|
|
1288
|
+
}
|
|
1204
1289
|
return true;
|
|
1205
1290
|
}
|
|
1206
1291
|
function cancelCartRevalidation(store) {
|
|
@@ -1218,7 +1303,6 @@ function clearPendingTransactions(store) {
|
|
|
1218
1303
|
store.transactions = [];
|
|
1219
1304
|
store.projectedErrors = [];
|
|
1220
1305
|
store.observedPromises.clear();
|
|
1221
|
-
store.activeMutationTransports.clear();
|
|
1222
1306
|
store.expectedEvents = [];
|
|
1223
1307
|
store.reservation = null;
|
|
1224
1308
|
store.identityTransportGate.active = null;
|
|
@@ -1227,6 +1311,7 @@ function clearPendingTransactions(store) {
|
|
|
1227
1311
|
cancelCartRevalidation(store);
|
|
1228
1312
|
}
|
|
1229
1313
|
function destroyCartStore(store, handlers) {
|
|
1314
|
+
store.revalidateOnConnect ||= store.revalidation.requested || store.revalidation.active !== null;
|
|
1230
1315
|
clearPendingTransactions(store);
|
|
1231
1316
|
if (typeof document !== "undefined" && store.cartSyncAttached) {
|
|
1232
1317
|
document.removeEventListener("shopify:cart:lines-update", handlers.lines);
|
|
@@ -1323,6 +1408,7 @@ function resetCartStore(store) {
|
|
|
1323
1408
|
const hadActiveRevalidation = store.revalidation.active !== null;
|
|
1324
1409
|
const shouldReloadAfterReset = store.cartSyncAttached && !hadActiveRevalidation && (hadActiveCartLoad || hasLocalCartData(store.observable.state));
|
|
1325
1410
|
clearPendingTransactions(store);
|
|
1411
|
+
store.activeMutationTransports.clear();
|
|
1326
1412
|
invalidateActiveCartLoad(store);
|
|
1327
1413
|
cancelCartRevalidation(store);
|
|
1328
1414
|
store.lastSnapshotSequence = 0;
|
|
@@ -1435,6 +1521,7 @@ function createCartStore(options = {}) {
|
|
|
1435
1521
|
waiting: []
|
|
1436
1522
|
},
|
|
1437
1523
|
cartSyncAttached: false,
|
|
1524
|
+
revalidateOnConnect: false,
|
|
1438
1525
|
reservation: null,
|
|
1439
1526
|
lastSnapshotSequence: 0
|
|
1440
1527
|
};
|