@shopify/hydrogen 2026.10.0-preview.2 → 2026.10.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/cli/index.mjs +464 -73
- package/dist/core/cart/actions.mjs +2 -0
- package/dist/core/cart/actions.mjs.map +1 -1
- package/dist/core/cart/cart.mjs +16 -6
- package/dist/core/cart/cart.mjs.map +1 -1
- package/dist/core/product/form.d.mts +21 -2
- package/dist/core/product/form.mjs +13 -0
- package/dist/core/product/form.mjs.map +1 -1
- package/dist/core/product/index.d.mts +1 -1
- package/dist/core/request-context.d.mts +6 -0
- package/dist/core/request-context.mjs +6 -2
- package/dist/core/request-context.mjs.map +1 -1
- package/dist/core/request-routing/handle-shopify-routes.mjs +2 -0
- package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/checkout.mjs +4 -1
- package/dist/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/proxy.mjs +19 -8
- package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/core/shopify-scripts/analytics.mjs +1 -1
- package/dist/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/core/shopify-scripts/constants.mjs +3 -1
- package/dist/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs +2 -1
- package/dist/core/shopify-scripts/deprecated-cookies.mjs.map +1 -1
- package/dist/core/shopify-scripts/global.mjs +5 -4
- package/dist/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/core/shopify-scripts/index.d.mts +1 -0
- package/dist/core/shopify-scripts/index.mjs +14 -12
- package/dist/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/core/shopify-scripts/types.d.mts +5 -0
- package/dist/core/url.mjs +2 -1
- package/dist/core/url.mjs.map +1 -1
- package/dist/customer-account/client.mjs +1 -1
- package/dist/customer-account/session.mjs +1 -1
- package/dist/development/core/cart/actions.mjs +2 -0
- package/dist/development/core/cart/actions.mjs.map +1 -1
- package/dist/development/core/cart/cart.mjs +16 -6
- package/dist/development/core/cart/cart.mjs.map +1 -1
- package/dist/development/core/cart/queries.d.mts +57 -57
- package/dist/development/core/predictive-search/queries.d.mts +2 -2
- package/dist/development/core/product/form.d.mts +21 -2
- package/dist/development/core/product/form.mjs +13 -0
- package/dist/development/core/product/form.mjs.map +1 -1
- package/dist/development/core/product/index.d.mts +1 -1
- package/dist/development/core/request-context.d.mts +6 -0
- package/dist/development/core/request-context.mjs +6 -2
- package/dist/development/core/request-context.mjs.map +1 -1
- package/dist/development/core/request-routing/handle-shopify-routes.mjs +2 -0
- package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs +4 -1
- package/dist/development/core/request-routing/interceptors/checkout.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/proxy.mjs +19 -8
- package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs +35 -0
- package/dist/development/core/request-routing/interceptors/ucp-mcp-proxy.mjs.map +1 -0
- package/dist/development/core/shopify-scripts/analytics.mjs +1 -1
- package/dist/development/core/shopify-scripts/analytics.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/constants.mjs +3 -1
- package/dist/development/core/shopify-scripts/constants.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs +2 -1
- package/dist/development/core/shopify-scripts/deprecated-cookies.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/global.mjs +5 -4
- package/dist/development/core/shopify-scripts/global.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/index.d.mts +1 -0
- package/dist/development/core/shopify-scripts/index.mjs +14 -12
- package/dist/development/core/shopify-scripts/index.mjs.map +1 -1
- package/dist/development/core/shopify-scripts/types.d.mts +5 -0
- package/dist/development/core/url.mjs +2 -1
- package/dist/development/core/url.mjs.map +1 -1
- package/dist/development/globals.d.mts +4 -1
- package/dist/development/react/index.d.mts +2 -2
- package/dist/development/vue/shopify-scripts.d.mts +9 -0
- package/dist/development/vue/shopify-scripts.mjs +5 -0
- package/dist/development/vue/shopify-scripts.mjs.map +1 -1
- package/dist/globals.d.mts +4 -1
- package/dist/react/index.d.mts +2 -2
- package/dist/vue/shopify-scripts.d.mts +10 -1
- package/dist/vue/shopify-scripts.mjs +5 -0
- package/dist/vue/shopify-scripts.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/hydrogen-cart-metafields/SKILL.md +133 -0
- package/skills/hydrogen-cart-ui/SKILL.md +2 -1
- package/skills/hydrogen-setup/steps/11-verify.md +1 -0
- package/skills/hydrogen-setup/steps/2-scaffold.md +21 -0
- package/skills/hydrogen-storefront-client/SKILL.md +4 -0
- package/skills/hydrogen-variant-form/SKILL.md +10 -4
- package/dist/core/shopify-scripts/consent.mjs +0 -17
- package/dist/core/shopify-scripts/consent.mjs.map +0 -1
- package/dist/development/core/shopify-scripts/consent.mjs +0 -17
- package/dist/development/core/shopify-scripts/consent.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.mjs","names":[],"sources":["../../../src/core/cart/actions.ts"],"sourcesContent":["import { normalizeCartId } from \"./cookie\";\nimport { getCartAttributeFormEntries } from \"./form\";\n\nexport type CartAttributeInput = { key: string; value: string };\n\nexport type CartLineAddInput = {\n merchandiseId: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartLineUpdateInput = {\n id: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartAction =\n | { intent: \"add\"; lines: CartLineAddInput[] }\n | { intent: \"update\"; lines: CartLineUpdateInput[] }\n | { intent: \"remove\"; lineIds: string[] }\n | { intent: \"discount-update\"; discountCodes: string[] }\n | { intent: \"discount-apply\"; code: string }\n | { intent: \"discount-remove\"; code: string }\n | { intent: \"attributes-update\"; attributes: CartAttributeInput[] }\n | { intent: \"note-update\"; note: string };\n\ntype ParsedCartRequest = {\n action: CartAction;\n cartId: string | null;\n};\n\nclass CartActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CartActionError\";\n }\n}\n\nexport async function parseCartRequest(request: Request): Promise<ParsedCartRequest> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n\n if (contentType.includes(\"application/json\")) {\n return parseJsonBody(await request.json());\n }\n\n if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n return { action: parseFormData(await request.formData()), cartId: null };\n }\n\n throw new CartActionError(\n `Unsupported content-type: \"${contentType}\". Expected application/json or form data.`,\n );\n}\n\n// --- JSON parsing ---\n\nfunction parseJsonBody(body: unknown): ParsedCartRequest {\n assertObject(body);\n const cartId = typeof body.cartId === \"string\" ? normalizeCartId(body.cartId) : null;\n\n if (\"note\" in body && typeof body.note === \"string\") {\n return { action: { intent: \"note-update\", note: body.note }, cartId };\n }\n\n if (\"discountCodes\" in body && Array.isArray(body.discountCodes)) {\n for (const code of body.discountCodes) {\n assertString(code, \"discountCodes entries\");\n }\n return {\n action: { intent: \"discount-update\", discountCodes: body.discountCodes as string[] },\n cartId,\n };\n }\n\n if (\"attributes\" in body && Array.isArray(body.attributes)) {\n return {\n action: { intent: \"attributes-update\", attributes: parseAttributes(body.attributes) },\n cartId,\n };\n }\n\n if (!(\"lines\" in body) || !Array.isArray(body.lines)) {\n throw new CartActionError(\n 'Request body must contain \"lines\", \"discountCodes\", \"attributes\", or \"note\".',\n );\n }\n\n const lines = body.lines as unknown[];\n if (lines.length === 0) {\n throw new CartActionError(\"Lines array must not be empty.\");\n }\n\n return { action: partitionLines(lines), cartId };\n}\n\nfunction partitionLines(rawLines: unknown[]): CartAction {\n const adds: CartLineAddInput[] = [];\n const updates: CartLineUpdateInput[] = [];\n const removeIds: string[] = [];\n\n for (const raw of rawLines) {\n assertObject(raw);\n const line = raw as Record<string, unknown>;\n const hasId = \"id\" in line && typeof line.id === \"string\" && line.id !== \"\";\n const hasMerchandiseId =\n \"merchandiseId\" in line &&\n typeof line.merchandiseId === \"string\" &&\n line.merchandiseId !== \"\";\n\n assertNonNegativeInteger(line.quantity, \"quantity\");\n const quantity = line.quantity as number;\n\n if (!hasId && !hasMerchandiseId) {\n throw new CartActionError(\n 'Each line must have either \"id\" (for update/remove) or \"merchandiseId\" (for add).',\n );\n }\n\n if (hasId && (line.id as string) === \"\") {\n throw new CartActionError('Line \"id\" must not be empty.');\n }\n\n const optionalFields = extractOptionalLineFields(line);\n\n if (!hasId && hasMerchandiseId) {\n adds.push({\n merchandiseId: line.merchandiseId as string,\n quantity,\n ...optionalFields,\n });\n } else if (hasId && quantity === 0) {\n removeIds.push(line.id as string);\n } else if (hasId) {\n updates.push({\n id: line.id as string,\n quantity,\n ...optionalFields,\n });\n }\n }\n\n const populatedBuckets = [adds.length, updates.length, removeIds.length].filter(\n (n) => n > 0,\n ).length;\n\n if (populatedBuckets > 1) {\n throw new CartActionError(\n \"Mixed line operations are not allowed. Separate add, update, and remove into distinct requests.\",\n );\n }\n\n if (adds.length > 0) return { intent: \"add\", lines: adds };\n if (updates.length > 0) return { intent: \"update\", lines: updates };\n return { intent: \"remove\", lineIds: removeIds };\n}\n\nfunction extractOptionalLineFields(\n line: Record<string, unknown>,\n): Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> {\n const result: Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> = {};\n\n if (\"attributes\" in line && Array.isArray(line.attributes)) {\n result.attributes = line.attributes as CartAttributeInput[];\n }\n\n if (\"sellingPlanId\" in line && typeof line.sellingPlanId === \"string\") {\n result.sellingPlanId = line.sellingPlanId;\n }\n\n return result;\n}\n\n// --- FormData parsing ---\n//\n// HTML forms can't express the same structures as JSON. Key compromises:\n//\n// Add line: single item per submission (forms can't batch multiple adds)\n// Update qty: server does the math — form sends current qty, server increments/decrements\n// Remove line: explicit intent field (can't express \"quantity=0 means remove\" implicitly)\n// Discount bulk: no FormData equivalent — forms handle one code at a time\n// Cart attributes: attributes.<key> fields preserve each key/value relationship\n// Selling plan: add only — no way to change selling plan on existing line via form\n//\n// Design decisions:\n// - `intent` field disambiguates operations (forms need explicit routing; JSON infers from structure)\n// - Field names match where possible: merchandiseId, quantity, lineId, discountCode, note\n// - Form submissions get 303 redirect to referer; JSON gets JSON response\n// - `increase`/`decrease` intents exist because forms can't atomically read-modify-write quantity\n\nconst LINE_INTENTS = new Set([\"increase\", \"decrease\", \"remove\", \"set\"]);\nconst DISCOUNT_INTENTS = new Set([\"discount-apply\", \"discount-remove\"]);\n\nfunction parseFormData(form: FormData): CartAction {\n const intent = formString(form, \"intent\");\n const merchandiseId = formString(form, \"merchandiseId\");\n\n if (!intent && merchandiseId) {\n return parseAddIntent(form, merchandiseId);\n }\n\n if (intent === \"add\") {\n if (!merchandiseId) {\n throw new CartActionError('Intent \"add\" requires a \"merchandiseId\" field.');\n }\n return parseAddIntent(form, merchandiseId);\n }\n\n if (!intent) {\n throw new CartActionError(\n 'FormData must include an \"intent\" field or a \"merchandiseId\" field.',\n );\n }\n\n if (LINE_INTENTS.has(intent)) {\n return parseLineIntent(intent, form);\n }\n\n if (DISCOUNT_INTENTS.has(intent)) {\n return parseDiscountIntent(intent as \"discount-apply\" | \"discount-remove\", form);\n }\n\n if (intent === \"note-update\") {\n return parseNoteIntent(form);\n }\n\n if (intent === \"attributes-update\") {\n return parseAttributesIntent(form);\n }\n\n throw new CartActionError(\n `Unknown intent \"${intent}\". Expected one of: add, increase, decrease, remove, set, discount-apply, discount-remove, attributes-update, note-update.`,\n );\n}\n\nfunction parseLineIntent(intent: string, form: FormData): CartAction {\n const lineId = formString(form, \"lineId\");\n if (!lineId) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"lineId\" field.`);\n }\n\n if (intent === \"remove\") {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n\n const quantity = formInt(form, \"quantity\");\n if (quantity === null) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"quantity\" field.`);\n }\n\n if (intent === \"set\") {\n if (quantity <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity }] };\n }\n\n if (intent === \"increase\") {\n return { intent: \"update\", lines: [{ id: lineId, quantity: quantity + 1 }] };\n }\n\n const decremented = quantity - 1;\n if (decremented <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity: decremented }] };\n}\n\nfunction parseDiscountIntent(\n intent: \"discount-apply\" | \"discount-remove\",\n form: FormData,\n): CartAction {\n const code = formString(form, \"discountCode\");\n if (!code) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"discountCode\" field.`);\n }\n return { intent, code };\n}\n\nfunction parseAddIntent(form: FormData, merchandiseId: string): CartAction {\n const quantity = formInt(form, \"quantity\") ?? 1;\n const sellingPlanId = formString(form, \"sellingPlanId\");\n\n const line: CartLineAddInput = { merchandiseId, quantity };\n if (sellingPlanId) line.sellingPlanId = sellingPlanId;\n\n return { intent: \"add\", lines: [line] };\n}\n\nfunction parseNoteIntent(form: FormData): CartAction {\n const noteValue = form.get(\"note\");\n if (noteValue === null) {\n throw new CartActionError('Intent \"note-update\" requires a \"note\" field.');\n }\n return { intent: \"note-update\", note: String(noteValue) };\n}\n\nfunction parseAttributesIntent(form: FormData): CartAction {\n return {\n intent: \"attributes-update\",\n attributes: parseAttributes(getCartAttributeFormEntries(form)),\n };\n}\n\nfunction parseAttributes(values: unknown[]): CartAttributeInput[] {\n return values.map((value, index) => {\n assertObject(value);\n assertString(value.key, `attributes[${index}].key`);\n assertString(value.value, `attributes[${index}].value`);\n if (!value.key) {\n throw new CartActionError(`Expected \"attributes[${index}].key\" not to be empty.`);\n }\n return { key: value.key, value: value.value };\n });\n}\n\n// --- Assertion helpers ---\n\nfunction assertObject(value: unknown): asserts value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new CartActionError(\"Expected an object.\");\n }\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new CartActionError(`Expected \"${label}\" to be a string.`);\n }\n}\n\nfunction assertNonNegativeInteger(value: unknown, label: string): asserts value is number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n throw new CartActionError(\n `Expected \"${label}\" to be a non-negative integer, got ${JSON.stringify(value)}.`,\n );\n }\n}\n\n// --- FormData field helpers ---\n\nfunction formString(form: FormData, key: string): string | null {\n const value = form.get(key);\n if (value === null || typeof value !== \"string\") return null;\n return value || null;\n}\n\nfunction formInt(form: FormData, key: string): number | null {\n const raw = form.get(key);\n if (raw === null || typeof raw !== \"string\") return null;\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed)) return null;\n return parsed;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"actions.mjs","names":[],"sources":["../../../src/core/cart/actions.ts"],"sourcesContent":["import { normalizeCartId } from \"./cookie\";\nimport { getCartAttributeFormEntries } from \"./form\";\n\nexport type CartAttributeInput = { key: string; value: string };\n\nexport type CartLineAddInput = {\n merchandiseId: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartLineUpdateInput = {\n id: string;\n quantity: number;\n attributes?: CartAttributeInput[];\n sellingPlanId?: string;\n};\n\nexport type CartAction =\n | { intent: \"add\"; lines: CartLineAddInput[] }\n | { intent: \"update\"; lines: CartLineUpdateInput[] }\n | { intent: \"remove\"; lineIds: string[] }\n | { intent: \"discount-update\"; discountCodes: string[] }\n | { intent: \"discount-apply\"; code: string }\n | { intent: \"discount-remove\"; code: string }\n | { intent: \"attributes-update\"; attributes: CartAttributeInput[] }\n | { intent: \"note-update\"; note: string };\n\ntype ParsedCartRequest = {\n action: CartAction;\n cartId: string | null;\n};\n\nclass CartActionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CartActionError\";\n }\n}\n\nexport async function parseCartRequest(request: Request): Promise<ParsedCartRequest> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n\n if (contentType.includes(\"application/json\")) {\n return parseJsonBody(await request.json());\n }\n\n if (\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\")\n ) {\n return { action: parseFormData(await request.formData()), cartId: null };\n }\n\n throw new CartActionError(\n `Unsupported content-type: \"${contentType}\". Expected application/json or form data.`,\n );\n}\n\n// --- JSON parsing ---\n\nfunction parseJsonBody(body: unknown): ParsedCartRequest {\n assertObject(body);\n const cartId = typeof body.cartId === \"string\" ? normalizeCartId(body.cartId) : null;\n\n if (\"note\" in body && typeof body.note === \"string\") {\n return { action: { intent: \"note-update\", note: body.note }, cartId };\n }\n\n if (\"discountCodes\" in body && Array.isArray(body.discountCodes)) {\n for (const code of body.discountCodes) {\n assertString(code, \"discountCodes entries\");\n }\n return {\n action: { intent: \"discount-update\", discountCodes: body.discountCodes as string[] },\n cartId,\n };\n }\n\n if (\"attributes\" in body && Array.isArray(body.attributes)) {\n return {\n action: { intent: \"attributes-update\", attributes: parseAttributes(body.attributes) },\n cartId,\n };\n }\n\n if (!(\"lines\" in body) || !Array.isArray(body.lines)) {\n throw new CartActionError(\n 'Request body must contain \"lines\", \"discountCodes\", \"attributes\", or \"note\".',\n );\n }\n\n const lines = body.lines as unknown[];\n if (lines.length === 0) {\n throw new CartActionError(\"Lines array must not be empty.\");\n }\n\n return { action: partitionLines(lines), cartId };\n}\n\nfunction partitionLines(rawLines: unknown[]): CartAction {\n const adds: CartLineAddInput[] = [];\n const updates: CartLineUpdateInput[] = [];\n const removeIds: string[] = [];\n\n for (const raw of rawLines) {\n assertObject(raw);\n const line = raw as Record<string, unknown>;\n const hasId = \"id\" in line && typeof line.id === \"string\" && line.id !== \"\";\n const hasMerchandiseId =\n \"merchandiseId\" in line &&\n typeof line.merchandiseId === \"string\" &&\n line.merchandiseId !== \"\";\n\n assertNonNegativeInteger(line.quantity, \"quantity\");\n const quantity = line.quantity as number;\n\n if (!hasId && !hasMerchandiseId) {\n throw new CartActionError(\n 'Each line must have either \"id\" (for update/remove) or \"merchandiseId\" (for add).',\n );\n }\n\n if (hasId && (line.id as string) === \"\") {\n throw new CartActionError('Line \"id\" must not be empty.');\n }\n\n const optionalFields = extractOptionalLineFields(line);\n\n if (!hasId && hasMerchandiseId) {\n adds.push({\n merchandiseId: line.merchandiseId as string,\n quantity,\n ...optionalFields,\n });\n } else if (hasId && quantity === 0) {\n removeIds.push(line.id as string);\n } else if (hasId) {\n updates.push({\n id: line.id as string,\n quantity,\n ...optionalFields,\n });\n }\n }\n\n const populatedBuckets = [adds.length, updates.length, removeIds.length].filter(\n (n) => n > 0,\n ).length;\n\n if (populatedBuckets > 1) {\n throw new CartActionError(\n \"Mixed line operations are not allowed. Separate add, update, and remove into distinct requests.\",\n );\n }\n\n if (adds.length > 0) return { intent: \"add\", lines: adds };\n if (updates.length > 0) return { intent: \"update\", lines: updates };\n return { intent: \"remove\", lineIds: removeIds };\n}\n\nfunction extractOptionalLineFields(\n line: Record<string, unknown>,\n): Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> {\n const result: Pick<CartLineAddInput, \"attributes\" | \"sellingPlanId\"> = {};\n\n if (\"attributes\" in line && Array.isArray(line.attributes)) {\n result.attributes = line.attributes as CartAttributeInput[];\n }\n\n if (\"sellingPlanId\" in line && typeof line.sellingPlanId === \"string\") {\n result.sellingPlanId = line.sellingPlanId;\n }\n\n return result;\n}\n\n// --- FormData parsing ---\n//\n// HTML forms can't express the same structures as JSON. Key compromises:\n//\n// Add line: single item per submission (forms can't batch multiple adds)\n// Update qty: server does the math — form sends current qty, server increments/decrements\n// Remove line: explicit intent field (can't express \"quantity=0 means remove\" implicitly)\n// Discount bulk: no FormData equivalent — forms handle one code at a time\n// Cart attributes: attributes.<key> fields preserve each key/value relationship\n// Selling plan: add only — no way to change selling plan on existing line via form\n//\n// Design decisions:\n// - `intent` field disambiguates operations (forms need explicit routing; JSON infers from structure)\n// - Field names match where possible: merchandiseId, quantity, lineId, discountCode, note\n// - Form submissions get 303 redirect to referer; JSON gets JSON response\n// - `increase`/`decrease` intents exist because forms can't atomically read-modify-write quantity\n\nconst LINE_INTENTS = new Set([\"increase\", \"decrease\", \"remove\", \"set\"]);\nconst DISCOUNT_INTENTS = new Set([\"discount-apply\", \"discount-remove\"]);\n\nfunction parseFormData(form: FormData): CartAction {\n const intent = formString(form, \"intent\");\n const merchandiseId = formString(form, \"merchandiseId\");\n\n if (!intent && merchandiseId) {\n return parseAddIntent(form, merchandiseId);\n }\n\n if (intent === \"add\") {\n if (!merchandiseId) {\n throw new CartActionError('Intent \"add\" requires a \"merchandiseId\" field.');\n }\n return parseAddIntent(form, merchandiseId);\n }\n\n if (!intent) {\n throw new CartActionError(\n 'FormData must include an \"intent\" field or a \"merchandiseId\" field.',\n );\n }\n\n if (LINE_INTENTS.has(intent)) {\n return parseLineIntent(intent, form);\n }\n\n if (DISCOUNT_INTENTS.has(intent)) {\n return parseDiscountIntent(intent as \"discount-apply\" | \"discount-remove\", form);\n }\n\n if (intent === \"note-update\") {\n return parseNoteIntent(form);\n }\n\n if (intent === \"attributes-update\") {\n return parseAttributesIntent(form);\n }\n\n throw new CartActionError(\n `Unknown intent \"${intent}\". Expected one of: add, increase, decrease, remove, set, discount-apply, discount-remove, attributes-update, note-update.`,\n );\n}\n\nfunction parseLineIntent(intent: string, form: FormData): CartAction {\n const lineId = formString(form, \"lineId\");\n if (!lineId) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"lineId\" field.`);\n }\n\n if (intent === \"remove\") {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n\n const quantity = formInt(form, \"quantity\");\n if (quantity === null) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"quantity\" field.`);\n }\n\n if (intent === \"set\") {\n if (quantity <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity }] };\n }\n\n if (intent === \"increase\") {\n return { intent: \"update\", lines: [{ id: lineId, quantity: quantity + 1 }] };\n }\n\n const decremented = quantity - 1;\n if (decremented <= 0) {\n return { intent: \"remove\", lineIds: [lineId] };\n }\n return { intent: \"update\", lines: [{ id: lineId, quantity: decremented }] };\n}\n\nfunction parseDiscountIntent(\n intent: \"discount-apply\" | \"discount-remove\",\n form: FormData,\n): CartAction {\n const code = formString(form, \"discountCode\");\n if (!code) {\n throw new CartActionError(`Intent \"${intent}\" requires a \"discountCode\" field.`);\n }\n return { intent, code };\n}\n\nfunction parseAddIntent(form: FormData, merchandiseId: string): CartAction {\n const quantity = formInt(form, \"quantity\") ?? 1;\n const sellingPlanId = formString(form, \"sellingPlanId\");\n\n const line: CartLineAddInput = { merchandiseId, quantity };\n if (sellingPlanId) line.sellingPlanId = sellingPlanId;\n\n const rawAttributes = getCartAttributeFormEntries(form);\n if (rawAttributes.length > 0) {\n line.attributes = parseAttributes(rawAttributes);\n }\n\n return { intent: \"add\", lines: [line] };\n}\n\nfunction parseNoteIntent(form: FormData): CartAction {\n const noteValue = form.get(\"note\");\n if (noteValue === null) {\n throw new CartActionError('Intent \"note-update\" requires a \"note\" field.');\n }\n return { intent: \"note-update\", note: String(noteValue) };\n}\n\nfunction parseAttributesIntent(form: FormData): CartAction {\n return {\n intent: \"attributes-update\",\n attributes: parseAttributes(getCartAttributeFormEntries(form)),\n };\n}\n\nfunction parseAttributes(values: unknown[]): CartAttributeInput[] {\n return values.map((value, index) => {\n assertObject(value);\n assertString(value.key, `attributes[${index}].key`);\n assertString(value.value, `attributes[${index}].value`);\n if (!value.key) {\n throw new CartActionError(`Expected \"attributes[${index}].key\" not to be empty.`);\n }\n return { key: value.key, value: value.value };\n });\n}\n\n// --- Assertion helpers ---\n\nfunction assertObject(value: unknown): asserts value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new CartActionError(\"Expected an object.\");\n }\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new CartActionError(`Expected \"${label}\" to be a string.`);\n }\n}\n\nfunction assertNonNegativeInteger(value: unknown, label: string): asserts value is number {\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0) {\n throw new CartActionError(\n `Expected \"${label}\" to be a non-negative integer, got ${JSON.stringify(value)}.`,\n );\n }\n}\n\n// --- FormData field helpers ---\n\nfunction formString(form: FormData, key: string): string | null {\n const value = form.get(key);\n if (value === null || typeof value !== \"string\") return null;\n return value || null;\n}\n\nfunction formInt(form: FormData, key: string): number | null {\n const raw = form.get(key);\n if (raw === null || typeof raw !== \"string\") return null;\n const parsed = parseInt(raw, 10);\n if (Number.isNaN(parsed)) return null;\n return parsed;\n}\n"],"mappings":"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"}
|
package/dist/core/cart/cart.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLogger } from "../logging/logging.mjs";
|
|
2
2
|
import { createObservable } from "../observable.mjs";
|
|
3
|
-
import { SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT } from "../shopify-scripts/constants.mjs";
|
|
3
|
+
import { CONSENT_TRACKING_API_LOADED_EVENT, SHOPIFY_STOREFRONT_STANDARD_ACTIONS_SCRIPT, VISITOR_CONSENT_COLLECTED_EVENT } from "../shopify-scripts/constants.mjs";
|
|
4
4
|
import { getCartAttributeFormEntries } from "./form.mjs";
|
|
5
5
|
import { DEFAULT_MINIMUM_QUANTITY, sanitizeQuantity } from "./quantity.mjs";
|
|
6
6
|
import { createCartState, createEmptyCartState, createEmptyErrorGroup } from "./state.mjs";
|
|
@@ -949,6 +949,7 @@ function enqueueTransaction(store, type, payload, promise, reservedTransaction,
|
|
|
949
949
|
const expectedEventIndex = store.expectedEvents.findIndex((expected) => expected.token === eventToken);
|
|
950
950
|
if (expectedEventIndex !== -1) {
|
|
951
951
|
store.expectedEvents.splice(expectedEventIndex, 1);
|
|
952
|
+
promise.then(NOOP, NOOP);
|
|
952
953
|
return;
|
|
953
954
|
}
|
|
954
955
|
if (store.observedPromises.has(promise)) return;
|
|
@@ -1229,17 +1230,20 @@ function destroyCartStore(store, handlers) {
|
|
|
1229
1230
|
store.cartSyncAttached = false;
|
|
1230
1231
|
publishVisibleState(store);
|
|
1231
1232
|
}
|
|
1232
|
-
function
|
|
1233
|
+
function handleConsentStateChanged() {
|
|
1233
1234
|
revalidateConnectedCartCheckoutUrls();
|
|
1234
1235
|
}
|
|
1235
1236
|
function attachCartConsentListener() {
|
|
1236
1237
|
if (cartConsentListenerAttached) return;
|
|
1237
|
-
document.addEventListener(
|
|
1238
|
+
document.addEventListener(CONSENT_TRACKING_API_LOADED_EVENT, handleConsentStateChanged);
|
|
1239
|
+
document.addEventListener(VISITOR_CONSENT_COLLECTED_EVENT, handleConsentStateChanged);
|
|
1238
1240
|
cartConsentListenerAttached = true;
|
|
1241
|
+
if (window.Shopify?.customerPrivacy?.consentStatus === "loaded") revalidateConnectedCartCheckoutUrls();
|
|
1239
1242
|
}
|
|
1240
1243
|
function detachCartConsentListenerIfIdle() {
|
|
1241
1244
|
if (!cartConsentListenerAttached || connectedCartStores.size > 0) return;
|
|
1242
|
-
document.removeEventListener(
|
|
1245
|
+
document.removeEventListener(CONSENT_TRACKING_API_LOADED_EVENT, handleConsentStateChanged);
|
|
1246
|
+
document.removeEventListener(VISITOR_CONSENT_COLLECTED_EVENT, handleConsentStateChanged);
|
|
1243
1247
|
cartConsentListenerAttached = false;
|
|
1244
1248
|
}
|
|
1245
1249
|
function createCartObservable(initialState, onReadyPromiseRemoved) {
|
|
@@ -1476,11 +1480,17 @@ function getAddPayload(formData, eventDetail) {
|
|
|
1476
1480
|
const rawQuantity = Number.parseInt(formData.get("quantity"), 10);
|
|
1477
1481
|
const quantity = Number.isNaN(rawQuantity) ? DEFAULT_ADD_QUANTITY : Math.max(DEFAULT_ADD_QUANTITY, rawQuantity);
|
|
1478
1482
|
const sellingPlanId = formData.get("sellingPlanId") || void 0;
|
|
1483
|
+
const rawAttributes = getCartAttributeFormEntries(formData);
|
|
1484
|
+
const attributes = rawAttributes.length > 0 ? rawAttributes.filter(({ key }) => key !== "").map(({ key, value }) => ({
|
|
1485
|
+
key,
|
|
1486
|
+
value: String(value)
|
|
1487
|
+
})) : void 0;
|
|
1479
1488
|
return {
|
|
1480
1489
|
lines: [{
|
|
1481
1490
|
merchandiseId,
|
|
1482
1491
|
quantity,
|
|
1483
|
-
...sellingPlanId ? { sellingPlanId } : {}
|
|
1492
|
+
...sellingPlanId ? { sellingPlanId } : {},
|
|
1493
|
+
...attributes ? { attributes } : {}
|
|
1484
1494
|
}],
|
|
1485
1495
|
products: extractProductDetails(eventDetail),
|
|
1486
1496
|
...eventDetail ? { eventDetail } : {}
|
|
@@ -1505,7 +1515,7 @@ async function handleFormSubmitInStore(store, handlers, event, eventDetail) {
|
|
|
1505
1515
|
if (LINE_INTENTS.has(intent)) return submitLineIntent(store, intent, formData);
|
|
1506
1516
|
if (DISCOUNT_INTENTS.has(intent)) return submitDiscountIntent(store, intent, formData);
|
|
1507
1517
|
if (intent === "note-update") return dispatchTransaction(store, "set_note", { note: formData.get("note") ?? "" });
|
|
1508
|
-
if (intent === "attributes-update") return dispatchTransaction(store, "set_attributes", { attributes: getCartAttributeFormEntries(formData).map(({ key, value }) => ({
|
|
1518
|
+
if (intent === "attributes-update") return dispatchTransaction(store, "set_attributes", { attributes: getCartAttributeFormEntries(formData).filter(({ key }) => key !== "").map(({ key, value }) => ({
|
|
1509
1519
|
key,
|
|
1510
1520
|
value: String(value)
|
|
1511
1521
|
})) });
|