Replace @shopify/hydrogen with direct Storefront API queries

Vercel's 'Hydrogen (v1)' preset matches any project depending on
@shopify/hydrogen and runs its v1 builder, which fails looking for
dist/worker. We only used the preview package as a Storefront API client,
so talk to the API directly instead:

- services/shopify/client.ts: plain fetch to /api/{version}/graphql.json
  with the public token header, @inContext country/language defaults, and
  the same graphql() / unwrapStorefrontResult API as before
- graphql/gql.ts: small helper that joins a document with its fragments
  (de-duplicated); the queries themselves are unchanged
- Explicit response types at each call site in place of the generated
  gql.tada types; local ProductFilter input type
- Drop @shopify/hydrogen, graphql, the TS plugin and 'hydrogen gql check'
- Copy no longer mentions Hydrogen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rami Bitar
2026-10-03 15:28:02 -04:00
co-authored by Claude Opus 5.5
parent d33a6894f3
commit 6fcfe59d0d
20 changed files with 273 additions and 249 deletions
-2
View File
@@ -6,6 +6,4 @@ dist
src/routeTree.gen.ts
*.tsbuildinfo
# Temp tsconfig written by `hydrogen gql check`; normally cleaned up on exit
.hydrogen-gql-*.json
.tanstack
+7 -6
View File
@@ -1,9 +1,10 @@
# Vite Shopify Storefront
A server-rendered Shopify storefront built with Vite, TanStack Start (TanStack
Router + TanStack Query), Hono and Tailwind CSS v4. `@shopify/hydrogen` is used
only as the Storefront API client and for typed `gql()` documents. It's a port
of `nextjs-templates/shopify-storefront` without the AI store assistant.
Router + TanStack Query), Hono and Tailwind CSS v4, talking to Shopify's
[Storefront API](https://shopify.dev/docs/api/storefront) directly over
GraphQL — no Shopify SDK. It's a port of `nextjs-templates/shopify-storefront`
without the AI store assistant.
Runs in the nodesandbox runtime: `npm install` → `npm run dev` → port 3000.
@@ -20,7 +21,7 @@ yarn dev # http://localhost:3000
| ---------------- | --------------------------------------------------------- |
| `yarn dev` | Vite dev server with SSR and the Hono `/api` routes |
| `yarn build` | Production build to `dist/` |
| `yarn typecheck` | `tsc` plus `hydrogen gql check` for the GraphQL documents |
| `yarn typecheck` | `tsc` |
## nodesandbox compatibility
@@ -50,8 +51,8 @@ src/
account.ts login, logout, register, recover, reset, activate, me
account.functions.ts server functions for account reads in loaders
hooks/ TanStack Query options + hooks over the Storefront API
services/shopify Storefront client, catalogue and customer functions
graphql/ Typed `gql()` documents
services/shopify Storefront API client (plain fetch), catalogue and customer functions
graphql/ Storefront API queries and mutations (`gql()` joins fragments)
components/ Storefront UI (header, cart drawer, product detail, ...)
```
+1 -3
View File
@@ -6,14 +6,13 @@
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
"build": "vite build",
"typecheck": "tsc --noEmit && hydrogen gql check"
"typecheck": "tsc --noEmit"
},
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e",
"dependencies": {
"@fontsource-variable/geist": "^5.3.0",
"@fontsource-variable/geist-mono": "^5.3.0",
"@remixicon/react": "^4.9.0",
"@shopify/hydrogen": "0.0.0-preview-116d5d7-20260730141607",
"@tanstack/query-core": "^5.104.1",
"@tanstack/react-query": "^5.104.1",
"@tanstack/react-router": "^1.170.41",
@@ -23,7 +22,6 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.42.2",
"graphql": "^17.0.2",
"hono": "^4.13.12",
"lucide-react": "^0.562.0",
"radix-ui": "^1.6.7",
+2 -2
View File
@@ -13,8 +13,8 @@ interface ProductsProps {
}
const Products: React.FC<ProductsProps> = ({
title = 'Shopify Hydrogen Storefront',
subtitle = 'A Shopify storefront built with Vite, TanStack and Hydrogen.',
title = 'Shopify Storefront',
subtitle = 'A Shopify storefront built with Vite, TanStack and the Storefront API.',
limit = 12,
showLoadMore = true,
}) => {
+2 -1
View File
@@ -1,7 +1,8 @@
/** Chrome shared by every route — rendered once in the root layout. */
export const site = {
storeName: 'Shop',
description: 'A Shopify storefront built with Vite, TanStack and Hydrogen.',
description:
'A Shopify storefront built with Vite, TanStack and the Storefront API.',
// Absolute origin for canonical and Open Graph URLs (set in vite.config.ts).
url: import.meta.env.VITE_SITE_URL ?? 'http://localhost:3000',
logoUrl: '',
+1 -1
View File
@@ -1,4 +1,4 @@
import { gql } from '@shopify/hydrogen';
import { gql } from '@/graphql/gql';
// Cart Fragment for consistent cart data
const CartFragment = gql(`
+1 -1
View File
@@ -1,4 +1,4 @@
import { gql } from '@shopify/hydrogen';
import { gql } from '@/graphql/gql';
import { ProductFragment } from '@/graphql/products';
// Get all collections
+1 -1
View File
@@ -1,6 +1,6 @@
// Customer account operations (classic Storefront customer accounts).
// https://shopify.dev/docs/api/storefront/latest/objects/Customer
import { gql } from '@shopify/hydrogen';
import { gql } from '@/graphql/gql';
const CustomerFragment = gql(`
fragment CustomerFragment on Customer {
+27
View File
@@ -0,0 +1,27 @@
/**
* Builds a GraphQL document string: the operation or fragment source, followed
* by any fragments it spreads. Fragments that themselves include fragments are
* flattened and de-duplicated, so each definition appears once — the
* Storefront API rejects a document that defines the same fragment twice.
*/
export function gql(source: string, fragments: readonly string[] = []): string {
const definitions: string[] = [];
const seen = new Set<string>();
const add = (text: string) => {
// A composed fragment is "its own source + its fragments", joined by blank
// lines; split it back into definitions so shared ones de-duplicate.
for (const part of text.split(/\n(?=\s*fragment\s)/)) {
const definition = part.trim();
if (definition && !seen.has(definition)) {
seen.add(definition);
definitions.push(definition);
}
}
};
add(source);
for (const fragment of fragments) add(fragment);
return definitions.join('\n\n');
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { gql } from '@shopify/hydrogen';
import { gql } from '@/graphql/gql';
// Shop policies are exposed on the `shop` object of the Storefront API.
// There is no lookup-by-handle field, so we fetch all of them and match.
+1 -1
View File
@@ -1,4 +1,4 @@
import { gql } from '@shopify/hydrogen';
import { gql } from '@/graphql/gql';
// Product Fragment for consistent product data
export const ProductFragment = gql(`
+1 -1
View File
@@ -1,4 +1,4 @@
import { gql } from '@shopify/hydrogen';
import { gql } from '@/graphql/gql';
import { ProductFragment } from '@/graphql/products';
// Storefront search over products. `productFilters` accepts the raw `input`
+34 -11
View File
@@ -89,6 +89,12 @@ export interface Cart {
};
}
// Every cart mutation selects this payload; both it and its cart are nullable.
type CartPayload = {
cart: Cart | null;
userErrors: Array<{ field?: string[] | null; message: string }>;
} | null;
// ─── Shopify API functions ───────────────────────────────────────────
/**
@@ -118,9 +124,12 @@ function unwrapCartPayload<T>(
async function createCartApi(lines: CartLineInput[] = []): Promise<Cart> {
const data = unwrapStorefrontResult(
await storefront.graphql(CREATE_CART_MUTATION, {
await storefront.graphql<{ cartCreate: CartPayload }>(
CREATE_CART_MUTATION,
{
variables: { lines: lines.length > 0 ? lines : null },
}),
}
),
'CreateCart'
);
@@ -132,9 +141,12 @@ async function addCartLinesApi(
lines: CartLineInput[]
): Promise<Cart> {
const data = unwrapStorefrontResult(
await storefront.graphql(ADD_CART_LINES_MUTATION, {
await storefront.graphql<{ cartLinesAdd: CartPayload }>(
ADD_CART_LINES_MUTATION,
{
variables: { cartId, lines },
}),
}
),
'AddCartLines'
);
@@ -146,9 +158,12 @@ async function updateCartLinesApi(
lines: CartLineUpdateInput[]
): Promise<Cart> {
const data = unwrapStorefrontResult(
await storefront.graphql(UPDATE_CART_LINES_MUTATION, {
await storefront.graphql<{ cartLinesUpdate: CartPayload }>(
UPDATE_CART_LINES_MUTATION,
{
variables: { cartId, lines },
}),
}
),
'UpdateCartLines'
);
@@ -160,9 +175,12 @@ async function removeCartLinesApi(
lineIds: string[]
): Promise<Cart> {
const data = unwrapStorefrontResult(
await storefront.graphql(REMOVE_CART_LINES_MUTATION, {
await storefront.graphql<{ cartLinesRemove: CartPayload }>(
REMOVE_CART_LINES_MUTATION,
{
variables: { cartId, lineIds },
}),
}
),
'RemoveCartLines'
);
@@ -174,9 +192,12 @@ async function updateCartDiscountCodesApi(
discountCodes: string[]
): Promise<Cart> {
const data = unwrapStorefrontResult(
await storefront.graphql(UPDATE_CART_DISCOUNT_CODES_MUTATION, {
await storefront.graphql<{ cartDiscountCodesUpdate: CartPayload }>(
UPDATE_CART_DISCOUNT_CODES_MUTATION,
{
variables: { cartId, discountCodes },
}),
}
),
'UpdateCartDiscountCodes'
);
@@ -185,7 +206,9 @@ async function updateCartDiscountCodesApi(
async function getCartApi(cartId: string): Promise<Cart | null> {
const data = unwrapStorefrontResult(
await storefront.graphql(GET_CART_QUERY, { variables: { cartId } }),
await storefront.graphql<{ cart: Cart | null }>(GET_CART_QUERY, {
variables: { cartId },
}),
'GetCart'
);
+3 -1
View File
@@ -22,7 +22,9 @@ export const POLICY_HANDLES = [
export async function getShopPolicies(): Promise<ShopPolicy[]> {
try {
const data = unwrapStorefrontResult(
await storefront.graphql(GET_SHOP_POLICIES_QUERY),
await storefront.graphql<{
shop: Record<string, ShopPolicy | null> | null;
}>(GET_SHOP_POLICIES_QUERY),
'GetShopPolicies'
);
+2 -2
View File
@@ -15,8 +15,8 @@ export const Route = createFileRoute('/')({
head: () => seo({ title: site.storeName, absolute: true, path: '/' }),
component: () => (
<Products
title="Vite + Shopify Hydrogen"
subtitle="A Shopify storefront built with Vite, TanStack and Hydrogen."
title="Vite + Shopify Storefront"
subtitle="A Shopify storefront built with Vite, TanStack and the Storefront API."
/>
),
});
+50 -23
View File
@@ -3,7 +3,6 @@
// These are plain async functions with no React imports, so they can be called
// from the Hono server as well as from the TanStack Query options in
// hooks/use-shopify-*.ts, which re-export them.
import type { StorefrontApi } from '@shopify/hydrogen';
import { storefront, unwrapStorefrontResult } from '@/services/shopify/client';
import {
GET_PRODUCTS_QUERY,
@@ -19,6 +18,19 @@ import {
SEARCH_SUGGESTIONS_QUERY,
} from '@/graphql/search';
// Storefront API connection shape (`edges { node }` + `pageInfo`).
interface Connection<T> {
edges: Array<{ node: T }>;
pageInfo?: { hasNextPage: boolean; endCursor: string | null } | null;
}
// `search` returns a union; `types: PRODUCT` narrows it, `__typename` proves it.
type SearchNode<T> = (T & { __typename: 'Product' }) | { __typename: string };
const isProductNode = <T>(
node: SearchNode<T>
): node is T & { __typename: 'Product' } => node.__typename === 'Product';
// Optional fields are `| null` rather than just optional: the Storefront API
// returns explicit nulls, and the typed `gql()` documents now surface that.
interface ProductImage {
@@ -150,9 +162,10 @@ export async function getProductsPage({
reverse = false,
}: UseProductsOptions = {}): Promise<ProductsPage> {
const data = unwrapStorefrontResult(
await storefront.graphql(GET_PRODUCTS_QUERY, {
variables: { first, after, query, sortKey, reverse },
}),
await storefront.graphql<{ products: Connection<Product> }>(
GET_PRODUCTS_QUERY,
{ variables: { first, after, query, sortKey, reverse } }
),
'GetProducts'
);
@@ -168,7 +181,9 @@ export async function getProductsPage({
// Fetch a single product by handle
export async function getProduct(handle: string): Promise<Product | null> {
const data = unwrapStorefrontResult(
await storefront.graphql(GET_PRODUCT_QUERY, { variables: { handle } }),
await storefront.graphql<{ product: Product | null }>(GET_PRODUCT_QUERY, {
variables: { handle },
}),
'GetProduct'
);
@@ -178,9 +193,10 @@ export async function getProduct(handle: string): Promise<Product | null> {
// Fetch product recommendations
export async function getProductRecommendations(productId: string): Promise<Product[]> {
const data = unwrapStorefrontResult(
await storefront.graphql(QUERY_PRODUCT_RECOMMENDATIONS, {
variables: { productId },
}),
await storefront.graphql<{ productRecommendations: Product[] | null }>(
QUERY_PRODUCT_RECOMMENDATIONS,
{ variables: { productId } }
),
'GetProductRecommendations'
);
@@ -245,7 +261,10 @@ export interface ProductFilterFacet {
// Fetch all collections
export async function getCollections(first = 50): Promise<Collection[]> {
const data = unwrapStorefrontResult(
await storefront.graphql(GET_COLLECTIONS_QUERY, { variables: { first } }),
await storefront.graphql<{ collections: Connection<Collection> }>(
GET_COLLECTIONS_QUERY,
{ variables: { first } }
),
'GetCollections'
);
@@ -277,7 +296,13 @@ export async function getCollectionProductsPage(
const filters = parseFilterInputs(filterInputs);
const data = unwrapStorefrontResult(
await storefront.graphql(GET_COLLECTION_PRODUCTS_QUERY, {
await storefront.graphql<{
collection:
| (Collection & {
products: Connection<Product> & { filters: ProductFilterFacet[] };
})
| null;
}>(GET_COLLECTION_PRODUCTS_QUERY, {
variables: {
handle,
first,
@@ -364,11 +389,10 @@ interface SearchProductsOptions {
filterInputs?: string[];
}
// The `ProductFilter` input shape, taken from the query that consumes it so it
// tracks the schema rather than being restated here.
type ProductFilterInput = NonNullable<
StorefrontApi.VariablesOf<typeof GET_COLLECTION_PRODUCTS_QUERY>['filters']
>[number];
// The Storefront API's `ProductFilter` input. Facet `input` strings decode to
// one of these, so only the shape matters here.
// https://shopify.dev/docs/api/storefront/latest/input-objects/ProductFilter
type ProductFilterInput = Record<string, unknown>;
// Facet `input` values are opaque JSON strings produced by Shopify and handed
// straight back as filter inputs, so they are parsed, not constructed.
@@ -392,7 +416,12 @@ export async function searchProducts({
filterInputs = [],
}: SearchProductsOptions): Promise<SearchProductsResult> {
const data = unwrapStorefrontResult(
await storefront.graphql(SEARCH_PRODUCTS_QUERY, {
await storefront.graphql<{
search: Connection<SearchNode<Product>> & {
totalCount: number;
productFilters: SearchFilter[];
};
}>(SEARCH_PRODUCTS_QUERY, {
variables: {
query,
first,
@@ -412,9 +441,7 @@ export async function searchProducts({
return {
products: search.edges
.map((edge) => edge.node)
.filter((node): node is Extract<typeof node, { __typename: 'Product' }> =>
node.__typename === 'Product'
),
.filter(isProductNode),
totalCount: search.totalCount ?? 0,
filters: search.productFilters ?? [],
hasNextPage: Boolean(search.pageInfo?.hasNextPage),
@@ -427,7 +454,9 @@ export async function searchSuggestions(
first = 3
): Promise<{ products: SearchSuggestion[]; totalCount: number }> {
const data = unwrapStorefrontResult(
await storefront.graphql(SEARCH_SUGGESTIONS_QUERY, {
await storefront.graphql<{
search: Connection<SearchNode<SearchSuggestion>> & { totalCount: number };
}>(SEARCH_SUGGESTIONS_QUERY, {
variables: { query, first },
}),
'SearchSuggestions'
@@ -438,9 +467,7 @@ export async function searchSuggestions(
return {
products: search.edges
.map((edge) => edge.node)
.filter((node): node is Extract<typeof node, { __typename: 'Product' }> =>
node.__typename === 'Product'
),
.filter(isProductNode),
totalCount: search.totalCount ?? 0,
};
}
+105 -56
View File
@@ -1,80 +1,129 @@
// Storefront API clients, built on `@shopify/hydrogen`.
// Storefront API client: plain `fetch` against the GraphQL endpoint, no SDK.
// https://shopify.dev/docs/api/storefront
//
// Call sites use the package's own API — `storefront.graphql(DOCUMENT, {
// variables })` — and pass the result through `unwrapStorefrontResult`, which
// applies this app's error policy: fail loudly. Hydrogen deliberately does not
// do that itself, because a 200 carrying partial data and GraphQL errors is a
// valid response that some callers want to render.
import {
createShopifyRequestContext,
createStorefrontClient,
type GraphQLFormattedError,
} from '@shopify/hydrogen';
// Call sites use `storefront.graphql<TData>(QUERY, { variables })` and pass the
// result through `unwrapStorefrontResult`, which applies this app's error
// policy: fail loudly. `graphql()` itself returns `{ data, errors }` like the
// API does, because a 200 can carry partial data alongside GraphQL errors.
import {
SHOPIFY_API_VERSION,
SHOPIFY_PUBLIC_ACCESS_TOKEN,
SHOPIFY_STORE_DOMAIN,
} from '@/services/shopify/config';
// No incoming request and no buyer context: this client is module-scoped and
// serves both the Hono API and the browser-side hooks, so it must not close
// over per-request state. `$country`/`$language` are injected from this i18n.
const requestContext = createShopifyRequestContext({
request: { headers: new Headers() },
i18n: { country: 'US', language: 'EN' },
});
export interface StorefrontGraphQLError {
message: string;
path?: ReadonlyArray<string | number>;
extensions?: Record<string, unknown>;
}
/**
* Workaround for a bug in this preview build of `@shopify/hydrogen`.
*
* The client tags every request with `X-Hydrogen-Version`, but the Storefront
* API does not list that header in its CORS `access-control-allow-headers`.
* Browsers therefore reject the preflight and `fetch` throws, which hydrogen
* reports as the generic "SFAPI request failed". It only bites against real
* stores — `mock.shop` answers `access-control-allow-headers: *`.
*
* Stripped in the browser only: server-side requests are not subject to CORS,
* so they keep sending the header. Remove this once the API allows it (or once
* these queries move server-side, which is the better long-term fix).
*/
const CORS_BLOCKED_HEADERS = ['X-Hydrogen-Version'];
export interface StorefrontResult<TData> {
data: TData | null;
errors?: StorefrontGraphQLError[];
}
const storefrontFetch = ((url, init) => {
const headers = new Headers(init?.headers);
/** Transport failures: non-2xx responses and bodies that aren't GraphQL JSON. */
export class StorefrontApiError extends Error {
constructor(
message: string,
readonly status?: number
) {
super(message);
this.name = 'StorefrontApiError';
}
}
if (typeof document !== 'undefined') {
for (const header of CORS_BLOCKED_HEADERS) headers.delete(header);
// Buyer context for `@inContext(country: $country, language: $language)`.
// The client is module-scoped and serves both the Hono API and the browser, so
// it carries no per-request state; queries that declare these variables get
// them filled in unless the caller passes its own.
const DEFAULT_CONTEXT = { country: 'US', language: 'EN' } as const;
const STORE_URL = /^https?:\/\//.test(SHOPIFY_STORE_DOMAIN)
? SHOPIFY_STORE_DOMAIN.replace(/\/+$/, '')
: `https://${SHOPIFY_STORE_DOMAIN}`;
export const STOREFRONT_API_URL = `${STORE_URL}/api/${SHOPIFY_API_VERSION}/graphql.json`;
function withContextVariables(
query: string,
variables: Record<string, unknown> = {}
): Record<string, unknown> {
const merged = { ...variables };
for (const [name, value] of Object.entries(DEFAULT_CONTEXT)) {
if (merged[name] === undefined && query.includes(`$${name}`)) {
merged[name] = value;
}
}
return merged;
}
async function graphql<TData>(
query: string,
options: { variables?: Record<string, unknown> } = {}
): Promise<StorefrontResult<TData>> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
// Public token only; mock.shop needs none. Never send a private token here.
if (SHOPIFY_PUBLIC_ACCESS_TOKEN) {
headers['X-Shopify-Storefront-Access-Token'] = SHOPIFY_PUBLIC_ACCESS_TOKEN;
}
return globalThis.fetch(url, { ...init, cache: 'no-store', headers });
}) as typeof globalThis.fetch;
const response = await fetch(STOREFRONT_API_URL, {
method: 'POST',
headers,
body: JSON.stringify({
query,
variables: withContextVariables(query, options.variables),
}),
// Carts and customer reads must never be served stale; catalogue caching
// happens in TanStack Query instead (see the `staleTime`s).
cache: 'no-store',
});
/**
* The one Storefront client. Uncached at the HTTP layer, because carts and
* customer reads must never serve a stale response — response caching for
* catalogue data happens in TanStack Query instead (see `staleTime`s).
*/
export const storefront = createStorefrontClient({
type: 'public',
requestContext,
config: {
storeDomain: SHOPIFY_STORE_DOMAIN,
apiVersion: SHOPIFY_API_VERSION,
publicStorefrontToken: SHOPIFY_PUBLIC_ACCESS_TOKEN,
fetch: storefrontFetch,
},
});
let body: unknown;
try {
body = await response.json();
} catch {
throw new StorefrontApiError(
`Storefront API returned a non-JSON response (${response.status}).`,
response.status
);
}
// The API answers GraphQL-level problems with 200 + `errors`; anything else
// that lacks a GraphQL body is a transport failure.
const result = body as Partial<StorefrontResult<TData>> | null;
if (!result || (!('data' in result) && !('errors' in result))) {
throw new StorefrontApiError(
`Storefront API request failed (${response.status}).`,
response.status
);
}
if (!response.ok && !result.errors?.length) {
throw new StorefrontApiError(
`Storefront API request failed (${response.status}).`,
response.status
);
}
return { data: result.data ?? null, errors: result.errors };
}
/** The one Storefront client. */
export const storefront = { graphql };
/**
* Returns the data from a `graphql()` result, throwing if Shopify reported any
* GraphQL errors. Transport failures — non-200, timeouts, unparseable bodies —
* have already thrown as `StorefrontApiError` by this point.
* GraphQL errors. Transport failures have already thrown `StorefrontApiError`.
*
* `operation` names the query in the log and the thrown message, so a failure
* points at the call site rather than just at "Shopify".
*/
export function unwrapStorefrontResult<TData>(
result: { data: TData | null; errors?: GraphQLFormattedError[] },
result: StorefrontResult<TData>,
operation: string
): TData {
if (result.errors?.length) {
+29 -14
View File
@@ -80,6 +80,15 @@ export interface AuthResult {
errors: CustomerUserError[];
}
// Payload shapes the customer mutations select. Shopify returns a null payload
// when a mutation could not run at all.
interface UserErrorsPayload {
customerUserErrors: CustomerUserError[] | null;
}
type TokenPayload = UserErrorsPayload & {
customerAccessToken: AccessToken | null;
};
const firstMessage = (errors: CustomerUserError[]) =>
errors[0]?.message ?? 'Something went wrong. Please try again.';
@@ -100,9 +109,10 @@ export async function createCustomer(input: {
acceptsMarketing?: boolean;
}): Promise<{ errors: CustomerUserError[] }> {
const data = unwrapStorefrontResult(
await storefront.graphql(CUSTOMER_CREATE_MUTATION, {
variables: { input },
}),
await storefront.graphql<{ customerCreate: UserErrorsPayload | null }>(
CUSTOMER_CREATE_MUTATION,
{ variables: { input } }
),
'CustomerCreate'
);
@@ -117,9 +127,10 @@ export async function login(
password: string
): Promise<AuthResult> {
const data = unwrapStorefrontResult(
await storefront.graphql(CUSTOMER_ACCESS_TOKEN_CREATE_MUTATION, {
variables: { input: { email, password } },
}),
await storefront.graphql<{ customerAccessTokenCreate: TokenPayload | null }>(
CUSTOMER_ACCESS_TOKEN_CREATE_MUTATION,
{ variables: { input: { email, password } } }
),
'CustomerAccessTokenCreate'
);
@@ -161,9 +172,10 @@ export async function resetPassword(
password: string
): Promise<AuthResult> {
const data = unwrapStorefrontResult(
await storefront.graphql(CUSTOMER_RESET_MUTATION, {
variables: { id, input: { resetToken, password } },
}),
await storefront.graphql<{ customerReset: TokenPayload | null }>(
CUSTOMER_RESET_MUTATION,
{ variables: { id, input: { resetToken, password } } }
),
'CustomerReset'
);
@@ -182,9 +194,10 @@ export async function activateAccount(
password: string
): Promise<AuthResult> {
const data = unwrapStorefrontResult(
await storefront.graphql(CUSTOMER_ACTIVATE_MUTATION, {
variables: { id, input: { activationToken, password } },
}),
await storefront.graphql<{ customerActivate: TokenPayload | null }>(
CUSTOMER_ACTIVATE_MUTATION,
{ variables: { id, input: { activationToken, password } } }
),
'CustomerActivate'
);
@@ -203,7 +216,7 @@ export async function getCustomer(
): Promise<Customer | null> {
try {
const data = unwrapStorefrontResult(
await storefront.graphql(CUSTOMER_QUERY, {
await storefront.graphql<{ customer: Customer | null }>(CUSTOMER_QUERY, {
variables: { customerAccessToken: accessToken, orderCount },
}),
'GetCustomer'
@@ -227,7 +240,9 @@ export async function updateCustomer(
}
): Promise<{ customer: Customer | null; errors: CustomerUserError[] }> {
const data = unwrapStorefrontResult(
await storefront.graphql(CUSTOMER_UPDATE_MUTATION, {
await storefront.graphql<{
customerUpdate: (UserErrorsPayload & { customer: Customer | null }) | null;
}>(CUSTOMER_UPDATE_MUTATION, {
variables: { customerAccessToken: accessToken, customer },
}),
'CustomerUpdate'
-1
View File
@@ -12,7 +12,6 @@
"isolatedModules": true,
"jsx": "react-jsx",
"types": ["vite/client", "node"],
"plugins": [{ "name": "@shopify/hydrogen/ts-plugin" }],
"paths": {
"@/*": ["./src/*"]
}
-116
View File
@@ -5,31 +5,6 @@ __metadata:
version: 10
cacheKey: 10c0
"@0no-co/graphql.web@npm:^1.0.5, @0no-co/graphql.web@npm:^1.3.1":
version: 1.3.4
resolution: "@0no-co/graphql.web@npm:1.3.4"
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
peerDependenciesMeta:
graphql:
optional: true
checksum: 10c0/58c61e16664121bd0974609890bf68135469fa1af23c70c3966cf50f0215efac7062a4ff2ded60faaf7575e420b73c0800467e46fc0440942d48490d2f6c13bd
languageName: node
linkType: hard
"@0no-co/graphqlsp@npm:^1.12.13":
version: 1.17.5
resolution: "@0no-co/graphqlsp@npm:1.17.5"
dependencies:
"@gql.tada/internal": "npm:^1.2.1"
graphql: "npm:^15.5.0 || ^16.0.0 || ^17.0.0"
peerDependencies:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0
typescript: ^5.0.0 || ^6.0.0
checksum: 10c0/6b40d666ce9b237d0aebb13d17770c009e4416e8955c6636acaef49174b1bde5d612c7a6d7656a3053518cbbfbb85f9d039df78b595233c365878d2091987ed2
languageName: node
linkType: hard
"@babel/code-frame@npm:7.29.7, @babel/code-frame@npm:^7.29.7":
version: 7.29.7
resolution: "@babel/code-frame@npm:7.29.7"
@@ -313,52 +288,6 @@ __metadata:
languageName: node
linkType: hard
"@gql.tada/cli-utils@npm:1.7.3":
version: 1.7.3
resolution: "@gql.tada/cli-utils@npm:1.7.3"
dependencies:
"@0no-co/graphqlsp": "npm:^1.12.13"
"@gql.tada/internal": "npm:1.0.9"
graphql: "npm:^15.5.0 || ^16.0.0 || ^17.0.0"
peerDependencies:
"@0no-co/graphqlsp": ^1.12.13
"@gql.tada/svelte-support": 1.0.2
"@gql.tada/vue-support": 1.0.2
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0
typescript: ^5.0.0 || ^6.0.0
peerDependenciesMeta:
"@gql.tada/svelte-support":
optional: true
"@gql.tada/vue-support":
optional: true
checksum: 10c0/55405687375cbda9a9e963fc627939a55c64e81816c8d6433add3564f2fc2c0600ec60c98e01de77fa21511713daf9e84b644362d9ce571f0ea5c43851c68b6f
languageName: node
linkType: hard
"@gql.tada/internal@npm:1.0.9":
version: 1.0.9
resolution: "@gql.tada/internal@npm:1.0.9"
dependencies:
"@0no-co/graphql.web": "npm:^1.0.5"
peerDependencies:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0
typescript: ^5.0.0 || ^6.0.0
checksum: 10c0/5599ab58bc5859bf9d38f13e4ca0aae564cf7e94fbfa9afdb9932d4719fb945cb19da917fe23ed0cf115550ce07d31bcac89c229385fe7077754b54c2bed16e9
languageName: node
linkType: hard
"@gql.tada/internal@npm:^1.2.1":
version: 1.2.2
resolution: "@gql.tada/internal@npm:1.2.2"
dependencies:
"@0no-co/graphql.web": "npm:^1.3.1"
peerDependencies:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0
typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
checksum: 10c0/2861061a64fa88dbd8bac84cff87bc33a30e9ed971cbee9f8d9a52ec27a17ffc64c389fb6a75f2231458524556829b81f4744b55ccb433e46d28e1066a5d4655
languageName: node
linkType: hard
"@isaacs/fs-minipass@npm:^4.0.0":
version: 4.0.1
resolution: "@isaacs/fs-minipass@npm:4.0.1"
@@ -2010,25 +1939,6 @@ __metadata:
languageName: node
linkType: hard
"@shopify/hydrogen@npm:0.0.0-preview-116d5d7-20260730141607":
version: 0.0.0-preview-116d5d7-20260730141607
resolution: "@shopify/hydrogen@npm:0.0.0-preview-116d5d7-20260730141607"
dependencies:
gql.tada: "npm:1.9.2"
peerDependencies:
react: ^18.0.0 || ^19.0.0
vue: ^3.5.0
peerDependenciesMeta:
react:
optional: true
vue:
optional: true
bin:
hydrogen: bin/hydrogen.mjs
checksum: 10c0/b12e230d174a910d8abb7ab18ecb99357dce82d2125054b30af6dcbecf280c6fea40c8ca5fd67d8931e1079fd73e587e3ea1484f50c294f4a79ad714c9664ecd
languageName: node
linkType: hard
"@tailwindcss/node@npm:4.3.1":
version: 4.3.1
resolution: "@tailwindcss/node@npm:4.3.1"
@@ -2876,23 +2786,6 @@ __metadata:
languageName: node
linkType: hard
"gql.tada@npm:1.9.2":
version: 1.9.2
resolution: "gql.tada@npm:1.9.2"
dependencies:
"@0no-co/graphql.web": "npm:^1.0.5"
"@0no-co/graphqlsp": "npm:^1.12.13"
"@gql.tada/cli-utils": "npm:1.7.3"
"@gql.tada/internal": "npm:1.0.9"
peerDependencies:
typescript: ^5.0.0 || ^6.0.0
bin:
gql-tada: bin/cli.js
gql.tada: bin/cli.js
checksum: 10c0/e6995d5ddd41a0049f7e2494758d605567027c57b270e59c48355d7fbd0ca95db77db2385c14d2add1198ef59b91c1b5a7f9b70d9eac8668c2be534e55a56517
languageName: node
linkType: hard
"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
@@ -2900,13 +2793,6 @@ __metadata:
languageName: node
linkType: hard
"graphql@npm:^15.5.0 || ^16.0.0 || ^17.0.0, graphql@npm:^17.0.2":
version: 17.0.2
resolution: "graphql@npm:17.0.2"
checksum: 10c0/766546216b891439ed09fd8584963df2013e26f2f79f096036eaaa2788c38964e049c8a142d68e3afdabdd8bbe70042639e83f0f4b82edbc7c6850421f072bb6
languageName: node
linkType: hard
"h3-v2@npm:h3@2.0.1-rc.20":
version: 2.0.1-rc.20
resolution: "h3@npm:2.0.1-rc.20"
@@ -3923,7 +3809,6 @@ __metadata:
"@fontsource-variable/geist": "npm:^5.3.0"
"@fontsource-variable/geist-mono": "npm:^5.3.0"
"@remixicon/react": "npm:^4.9.0"
"@shopify/hydrogen": "npm:0.0.0-preview-116d5d7-20260730141607"
"@tailwindcss/vite": "npm:4.3.1"
"@tanstack/query-core": "npm:^5.104.1"
"@tanstack/react-query": "npm:^5.104.1"
@@ -3938,7 +3823,6 @@ __metadata:
clsx: "npm:^2.1.1"
cmdk: "npm:^1.1.1"
framer-motion: "npm:^12.42.2"
graphql: "npm:^17.0.2"
hono: "npm:^4.13.12"
lucide-react: "npm:^0.562.0"
radix-ui: "npm:^1.6.7"