Template
Move Storefront queries onto @shopify/hydrogen preview client
Replaces the hand-rolled shopifyFetch wrapper and raw query strings with
the hydrogen preview package (0.0.0-preview-116d5d7-20260730141607), the
same build pinned in hydrogen-preview/.
- services/shopify/client: createStorefrontClient({type: "public"}) with a
static request context. Two module-scoped clients — uncached for carts,
products and customers; revalidate-3600 for shop policies — using the
client's custom fetch option to carry Next's caching hints.
- graphql/*: every document wrapped in gql(), fragments composed via the
second argument instead of string interpolation, and $country/$language
declared with @inContext so hydrogen injects them.
- shopifyFetch keeps its {query, variables} call shape so call sites are
unchanged, but is now generic over the document, so data is inferred. It
re-raises GraphQL errors, which hydrogen returns rather than throws.
Env var names, the 2025-07 API version pin, and the client-side fetching
architecture are unchanged.
Turning on type coverage surfaced real defects, not just annotations:
- hydrogen gql check caught $discountCodes: [String!] used where the field
requires [String!]!.
- Cart and customer mutation payloads are nullable and were dereferenced
unconditionally, so a failed mutation threw a TypeError. Adds a shared
unwrapCartPayload helper (collapsing five copies of the same userErrors
check) and explicit null handling in the customer service, so a null
payload reads as an error rather than success with no errors.
- Search results are a Product | Page | Article union; adds __typename to
the queries and narrows on it.
- Widens nullable fields (altText, image, customer.email, totalTaxAmount)
in the domain interfaces and in the structural duplicates some
components declare locally.
Adds a typecheck script (tsc --noEmit && hydrogen gql check); it passes
with 0 errors. Two deprecation warnings are left alone as acting on them
would change behaviour: ProductOption.values and CartCost.totalTaxAmount.
Verified against mock.shop: build prerenders the policy pages through the
cached client, and in the browser the product grid, search with facets,
collection filter round-trip, add-to-cart and quantity update all work.
Customer account flows are untested — they need real credentials.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JaxgqPbFxLsSuLPZom2kdC
This commit is contained in:
co-authored by
Claude Opus 5
parent
95b191eeb8
commit
f8c3ef9e0e
+36
-33
@@ -45,12 +45,12 @@ interface CartLine {
|
||||
currencyCode: string;
|
||||
};
|
||||
image?: {
|
||||
id: string;
|
||||
id?: string | null;
|
||||
url: string;
|
||||
altText?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
altText?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
} | null;
|
||||
product: {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -82,7 +82,7 @@ export interface Cart {
|
||||
totalTaxAmount?: {
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
lines: {
|
||||
edges: Array<{
|
||||
@@ -93,17 +93,38 @@ export interface Cart {
|
||||
|
||||
// ─── Shopify API functions ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Every cart mutation returns the same `{ cart, userErrors }` payload, and both
|
||||
* the payload and the cart inside it are nullable — Shopify returns no cart when
|
||||
* the mutation could not be applied. Callers want a cart or an exception.
|
||||
*/
|
||||
function unwrapCartPayload<T>(
|
||||
payload:
|
||||
| {
|
||||
cart?: T | null;
|
||||
userErrors: ReadonlyArray<{ message: string }>;
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
): T {
|
||||
if (payload?.userErrors.length) {
|
||||
throw new Error(payload.userErrors[0].message);
|
||||
}
|
||||
|
||||
if (!payload?.cart) {
|
||||
throw new Error('Cart update failed. Please try again.');
|
||||
}
|
||||
|
||||
return payload.cart;
|
||||
}
|
||||
|
||||
async function createCartApi(lines: CartLineInput[] = []): Promise<Cart> {
|
||||
const response = await shopifyFetch({
|
||||
query: CREATE_CART_MUTATION,
|
||||
variables: { lines: lines.length > 0 ? lines : null },
|
||||
});
|
||||
|
||||
if (response.data.cartCreate.userErrors.length > 0) {
|
||||
throw new Error(response.data.cartCreate.userErrors[0].message);
|
||||
}
|
||||
|
||||
return response.data.cartCreate.cart;
|
||||
return unwrapCartPayload(response.data.cartCreate);
|
||||
}
|
||||
|
||||
async function addCartLinesApi(
|
||||
@@ -115,11 +136,7 @@ async function addCartLinesApi(
|
||||
variables: { cartId, lines },
|
||||
});
|
||||
|
||||
if (response.data.cartLinesAdd.userErrors.length > 0) {
|
||||
throw new Error(response.data.cartLinesAdd.userErrors[0].message);
|
||||
}
|
||||
|
||||
return response.data.cartLinesAdd.cart;
|
||||
return unwrapCartPayload(response.data.cartLinesAdd);
|
||||
}
|
||||
|
||||
async function updateCartLinesApi(
|
||||
@@ -131,11 +148,7 @@ async function updateCartLinesApi(
|
||||
variables: { cartId, lines },
|
||||
});
|
||||
|
||||
if (response.data.cartLinesUpdate.userErrors.length > 0) {
|
||||
throw new Error(response.data.cartLinesUpdate.userErrors[0].message);
|
||||
}
|
||||
|
||||
return response.data.cartLinesUpdate.cart;
|
||||
return unwrapCartPayload(response.data.cartLinesUpdate);
|
||||
}
|
||||
|
||||
async function removeCartLinesApi(
|
||||
@@ -147,11 +160,7 @@ async function removeCartLinesApi(
|
||||
variables: { cartId, lineIds },
|
||||
});
|
||||
|
||||
if (response.data.cartLinesRemove.userErrors.length > 0) {
|
||||
throw new Error(response.data.cartLinesRemove.userErrors[0].message);
|
||||
}
|
||||
|
||||
return response.data.cartLinesRemove.cart;
|
||||
return unwrapCartPayload(response.data.cartLinesRemove);
|
||||
}
|
||||
|
||||
async function updateCartDiscountCodesApi(
|
||||
@@ -163,13 +172,7 @@ async function updateCartDiscountCodesApi(
|
||||
variables: { cartId, discountCodes },
|
||||
});
|
||||
|
||||
if (response.data.cartDiscountCodesUpdate.userErrors.length > 0) {
|
||||
throw new Error(
|
||||
response.data.cartDiscountCodesUpdate.userErrors[0].message
|
||||
);
|
||||
}
|
||||
|
||||
return response.data.cartDiscountCodesUpdate.cart;
|
||||
return unwrapCartPayload(response.data.cartDiscountCodesUpdate);
|
||||
}
|
||||
|
||||
async function getCartApi(cartId: string): Promise<Cart | null> {
|
||||
|
||||
Reference in New Issue
Block a user