{"id":13632271,"url":"https://github.com/basementstudio/commerce-toolkit","last_synced_at":"2025-10-31T14:44:42.456Z","repository":{"id":37459193,"uuid":"492996863","full_name":"basementstudio/commerce-toolkit","owner":"basementstudio","description":"Ship better storefronts 🛍","archived":false,"fork":false,"pushed_at":"2024-06-03T16:24:19.000Z","size":2312,"stargazers_count":282,"open_issues_count":12,"forks_count":12,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-30T12:09:40.627Z","etag":null,"topics":["commerce","graphql","nextjs","react","shopify","shopify-api","storefront","storefront-api","typescript"],"latest_commit_sha":null,"homepage":"commerce-toolkit-nextjs-shopify.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/basementstudio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-16T20:55:43.000Z","updated_at":"2025-03-26T11:36:28.000Z","dependencies_parsed_at":"2024-01-22T01:14:36.288Z","dependency_job_id":"0894ef05-0e57-4e0a-9b69-9845ed85ddc0","html_url":"https://github.com/basementstudio/commerce-toolkit","commit_stats":null,"previous_names":["basementstudio/react-dropify"],"tags_count":44,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basementstudio%2Fcommerce-toolkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basementstudio%2Fcommerce-toolkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basementstudio%2Fcommerce-toolkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/basementstudio%2Fcommerce-toolkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/basementstudio","download_url":"https://codeload.github.com/basementstudio/commerce-toolkit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247500469,"owners_count":20948880,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["commerce","graphql","nextjs","react","shopify","shopify-api","storefront","storefront-api","typescript"],"created_at":"2024-08-01T22:02:58.413Z","updated_at":"2025-10-31T14:44:42.450Z","avatar_url":"https://github.com/basementstudio.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# BSMNT Commerce Toolkit\n\n![commerce-toolkit](https://user-images.githubusercontent.com/40034115/195423154-223a8187-5c3c-4caa-a19a-843b07d1684a.jpeg)\n\nWelcome to the **BSMNT Commerce Toolkit**: packages to help you ship better storefronts, faster, and with more confidence.\n\nThis toolkit has helped us—[basement.studio](https://basement.studio/)—ship reliable storefronts that could handle crazy amounts of traffic. Some of them include: [shopmrbeast.com](https://shopmrbeast.com/), [karljacobs.co](https://karljacobs.co/), [shopmrballen.com](https://shopmrballen.com/), and [ranboo.fashion](https://ranboo.fashion/).\n\n\u003chr /\u003e\n\n\u003cb\u003e\u003ci\u003e💡 If you're looking for an example with Next.js + Shopify, check out [our example here](./examples/nextjs-shopify).\u003c/i\u003e\u003c/b\u003e\n\n\u003chr /\u003e\n\nThis repository currently holds three packages:\n\n1. `@bsmnt/storefront-hooks`: React Hooks to manage storefront client-side state.\n\n   - ✅ Manage the whole cart lifecycle with the help of [`@tanstack/react-query`](https://tanstack.com/query/v4) and `localStorage`\n   - ✅ Easily manage your cart mutations (like adding stuff into it)\n   - ✅ An opinionated, but powerful, way to structure storefront hooks\n\n2. `@bsmnt/sdk-gen`: a CLI that generates a type-safe, graphql SDK.\n\n   - ✅ Easily connect to any GraphQL API\n   - ✅ Generated TypeScript types from your queries\n   - ✅ Lighter than avarage, as it doesn't depend on `graphql` for production\n\n3. `@bsmnt/drop`: Helpers for managing a countdown. Generally used to create hype around a merch drop.\n   - ✅ Create your countdown in just a couple of minutes\n   - ✅ Reveal your site only when the drop is ready to go ([see this example from one of our drops](https://twitter.com/MikaelSargsyan/status/1578131832331272224))\n\nThese play really well together, but can also be used separately. Let's see how they work!\n\n\u003cbr /\u003e\n\n## `@bsmnt/storefront-hooks`\n\n```zsh\npnpm add @bsmnt/storefront-hooks @tanstack/react-query\n```\n\nThis package exports:\n\n- `createStorefrontHooks`: _function_ that creates the hooks needed to interact with the cart.\n\n```ts\nimport { createStorefrontHooks } from '@bsmnt/storefront-hooks'\n\nexport const hooks = createStorefrontHooks({\n  cartCookieKey: '', // to save cart id in cookie\n  fetchers: {}, // hooks will use these internally\n  mutators: {}, // hooks will use these internally\n  createCartIfNotFound: false, // defaults to false. if true, will create a cart if none is found\n  queryClientConfig: {} // internal query client config\n})\n```\n\nTake a look at some examples:\n\n\u003cdetails\u003e\n    \u003csummary\u003eSimple example, with \u003ccode\u003elocalStorage\u003c/code\u003e\u003c/summary\u003e\n    \n```ts\nimport { createStorefrontHooks } from '@bsmnt/storefront-hooks'\n\ntype LineItem = {\n  merchandiseId: string\n  quantity: number\n}\n\ntype Cart = {\n  id: string\n  lines: LineItem[]\n}\n\nexport const {\n  QueryClientProvider,\n  useCartQuery,\n  useAddLineItemsToCartMutation,\n  useOptimisticCartUpdate,\n  useRemoveLineItemsFromCartMutation,\n  useUpdateLineItemsInCartMutation\n} = createStorefrontHooks\u003cCart\u003e({\n  cartCookieKey: 'example-nextjs-localstorage',\n  fetchers: {\n    fetchCart: (cartId: string) =\u003e {\n      const cartFromLocalStorage = localStorage.getItem(cartId)\n\n      if (!cartFromLocalStorage) throw new Error('Cart not found')\n\n      const cart: Cart = JSON.parse(cartFromLocalStorage)\n\n      return cart\n    }\n  },\n  mutators: {\n    addLineItemsToCart: (cartId, lines) =\u003e {\n      const cartFromLocalStorage = localStorage.getItem(cartId)\n\n      if (!cartFromLocalStorage) throw new Error('Cart not found')\n\n      const cart: Cart = JSON.parse(cartFromLocalStorage)\n      // Add line if not exists, update quantity if exists\n      const updatedCart = lines.reduce((cart, line) =\u003e {\n        const lineIndex = cart.lines.findIndex(\n          (cartLine) =\u003e cartLine.merchandiseId === line.merchandiseId\n        )\n\n        if (lineIndex === -1) {\n          cart.lines.push(line)\n        } else {\n          cart.lines[lineIndex]!.quantity += line.quantity\n        }\n\n        return cart\n      }, cart)\n\n      localStorage.setItem(cartId, JSON.stringify(updatedCart))\n\n      return {\n        data: updatedCart\n      }\n    },\n    createCart: () =\u003e {\n      const cart: Cart = { id: 'cart', lines: [] }\n      localStorage.setItem(cart.id, JSON.stringify(cart))\n\n      return { data: cart }\n    },\n    createCartWithLines: (lines) =\u003e {\n      const cart = { id: 'cart', lines }\n      localStorage.setItem(cart.id, JSON.stringify(cart))\n\n      return { data: cart }\n    },\n    removeLineItemsFromCart: (cartId, lineIds) =\u003e {\n      const cartFromLocalStorage = localStorage.getItem(cartId)\n\n      if (!cartFromLocalStorage) throw new Error('Cart not found')\n\n      const cart: Cart = JSON.parse(cartFromLocalStorage)\n      cart.lines = cart.lines.filter(\n        (line) =\u003e !lineIds.includes(line.merchandiseId)\n      )\n      localStorage.setItem(cart.id, JSON.stringify(cart))\n\n      return {\n        data: cart\n      }\n    },\n    updateLineItemsInCart: (cartId, lines) =\u003e {\n      const cartFromLocalStorage = localStorage.getItem(cartId)\n\n      if (!cartFromLocalStorage) throw new Error('Cart not found')\n\n      const cart: Cart = JSON.parse(cartFromLocalStorage)\n      cart.lines = lines\n      localStorage.setItem(cart.id, JSON.stringify(cart))\n\n      return {\n        data: cart\n      }\n    }\n  },\n  logging: {\n    onError(type, error) {\n      console.info({ type, error })\n    },\n    onSuccess(type, data) {\n      console.info({ type, data })\n    }\n  }\n})\n\n```\n\u003c/details\u003e\n\u003cdetails\u003e\n    \u003csummary\u003eComplete example, with \u003ccode\u003e@bsmnt/sdk-gen\u003c/code\u003e\u003c/summary\u003e\n\n```bash\n# Given the following file tree:\n.\n└── storefront/\n    ├── sdk-gen/\n    │   └── sdk.ts # generated with @bsmnt/sdk-gen\n    └── hooks.ts # \u003c- we'll work here\n```\n\nThis example depends on [@bsmnt/sdk-gen](#bsmntsdk-gen).\n\n```ts\n// ./storefront/hooks.ts\n\nimport { createStorefrontHooks } from '@bsmnt/storefront-hooks'\nimport { storefront } from '../sdk-gen/sdk'\nimport type {\n  CartGenqlSelection,\n  CartUserErrorGenqlSelection,\n  FieldsSelection,\n  Cart as GenqlCart\n} from '../sdk-gen/generated'\n\nconst cartFragment = {\n  id: true,\n  checkoutUrl: true,\n  createdAt: true,\n  cost: { subtotalAmount: { amount: true, currencyCode: true } }\n} satisfies CartGenqlSelection\n\nexport type Cart = FieldsSelection\u003cGenqlCart, typeof cartFragment\u003e\n\nconst userErrorFragment = {\n  message: true,\n  code: true,\n  field: true\n} satisfies CartUserErrorGenqlSelection\n\nexport const {\n  QueryClientProvider,\n  useCartQuery,\n  useAddLineItemsToCartMutation,\n  useOptimisticCartUpdate,\n  useRemoveLineItemsFromCartMutation,\n  useUpdateLineItemsInCartMutation\n} = createStorefrontHooks({\n  cartCookieKey: 'example-nextjs-shopify',\n  fetchers: {\n    fetchCart: async (cartId) =\u003e {\n      const { cart } = await storefront.query({\n        cart: {\n          __args: { id: cartId },\n          ...cartFragment\n        }\n      })\n\n      if (cart === undefined) throw new Error('Request failed')\n      return cart\n    }\n  },\n  mutators: {\n    addLineItemsToCart: async (cartId, lines) =\u003e {\n      const { cartLinesAdd } = await storefront.mutation({\n        cartLinesAdd: {\n          __args: {\n            cartId,\n            lines\n          },\n          cart: cartFragment,\n          userErrors: userErrorFragment\n        }\n      })\n\n      return {\n        data: cartLinesAdd?.cart,\n        userErrors: cartLinesAdd?.userErrors\n      }\n    },\n    createCart: async () =\u003e {\n      const { cartCreate } = await storefront.mutation({\n        cartCreate: {\n          cart: cartFragment,\n          userErrors: userErrorFragment\n        }\n      })\n      return {\n        data: cartCreate?.cart,\n        userErrors: cartCreate?.userErrors\n      }\n    },\n    // TODO we could use the same mutation as createCart?\n    createCartWithLines: async (lines) =\u003e {\n      const { cartCreate } = await storefront.mutation({\n        cartCreate: {\n          __args: { input: { lines } },\n          cart: cartFragment,\n          userErrors: userErrorFragment\n        }\n      })\n      return {\n        data: cartCreate?.cart,\n        userErrors: cartCreate?.userErrors\n      }\n    },\n    removeLineItemsFromCart: async (cartId, lineIds) =\u003e {\n      const { cartLinesRemove } = await storefront.mutation({\n        cartLinesRemove: {\n          __args: { cartId, lineIds },\n          cart: cartFragment,\n          userErrors: userErrorFragment\n        }\n      })\n      return {\n        data: cartLinesRemove?.cart,\n        userErrors: cartLinesRemove?.userErrors\n      }\n    },\n    updateLineItemsInCart: async (cartId, lines) =\u003e {\n      const { cartLinesUpdate } = await storefront.mutation({\n        cartLinesUpdate: {\n          __args: {\n            cartId,\n            lines: lines.map((l) =\u003e ({\n              id: l.merchandiseId,\n              quantity: l.quantity,\n              attributes: l.attributes\n            }))\n          },\n          cart: cartFragment,\n          userErrors: userErrorFragment\n        }\n      })\n      return {\n        data: cartLinesUpdate?.cart,\n        userErrors: cartLinesUpdate?.userErrors\n      }\n    }\n  },\n  createCartIfNotFound: true\n})\n```\n\n\u003c/details\u003e\n\n\u003cbr /\u003e\n \n## `@bsmnt/sdk-gen`\n\n```zsh\npnpm add @bsmnt/sdk-gen --dev\n```\n\nThis package installs a CLI with a single command: `generate`. Running it will hit your GraphQL endpoint and generate TypeScript types from your queries and mutations. \u003cb\u003eIt's powered by [Genql](https://genql.dev/), so be sure to check out [their docs](https://genql.dev/docs).\u003c/b\u003e\n\n```bash\n# By default, you can have a file tree like the following:\n.\n└── sdk-gen/\n    └── config.js\n```\n\n```js\n// ./sdk-gen/config.js\n\n/**\n * @type {import(\"@bsmnt/sdk-gen\").Config}\n */\nmodule.exports = {\n  endpoint: '',\n  headers: {}\n}\n```\n\nAnd then you can run the generator:\n\n```zsh\npnpm sdk-gen\n```\n\nThis will look inside `./sdk-gen/` for a `config.js` file, and for all your `.{graphql,gql}` files under that directory.\n\nIf you want to use a custom directory (and not the default, which is `./sdk-gen/`), you can use the `--dir` argument.\n\n```zsh\npnpm sdk-gen --dir ./my-custom/directory\n```\n\nAfter running the generator, you should get the following result:\n\n```bash\n.\n└── sdk-gen/\n    ├── config.js\n    ├── documents.gql\n    ├── generated/              # \u003c- generated\n    │   ├── index.ts\n    │   └── graphql.schema.json\n    └── sdk.ts                  # \u003c- generated\n```\n\nInside `sdk.ts`, you'll have the `bsmntSdk` being exported:\n\n```ts\nimport config from './config'\nimport { createSdk } from './generated'\n\nexport const bsmntSdk = createSdk(config)\n```\n\nAnd that's all. You should be able to use that to hit your GraphQL API in a type safe manner.\n\nAn added benefit is that this sdk doesn't depend on `graphql`. Many GraphQL Clients require it as a peer dependency (e.g [`graphql-request`](https://github.com/prisma-labs/graphql-request/blob/master/package.json#L53)), which adds important KBs to the bundle.\n\n↳ For a standard way to use this with the [Shopify Storefront API](https://shopify.dev/api/storefront), take a look at our example [With Next.js + Shopify](./examples/nextjs-shopify/src/storefront/sdk-gen).\n\n\u003cbr /\u003e\n\n## `@bsmnt/drop`\n\n```zsh\npnpm add @bsmnt/drop\n```\n\nThis package exports:\n\n- `CountdownProvider`: _Context Provider_ for the `CountdownStore`\n- `useCountdownStore`: _Hook_ that consumes the `CountdownProvider` context and returns the `CountdownStore`\n- `zeroPad`: _utility_ to pad a number with zeroes\n\nTo use, just wrap the `CountdownProvider` wherever you want to add your countdown. For example with Next.js:\n\n```tsx\n// _app.tsx\nimport type { AppProps } from 'next/app'\nimport { CountdownProvider } from '@bsmnt/drop'\nimport { Countdown } from '../components/countdown'\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return (\n    \u003cCountdownProvider\n      endDate={Date.now() + 1000 * 5} // set this to 5 seconds from now just to test\n      countdownChildren={\u003cCountdown /\u003e}\n      exitDelay={1000} // optional, just to give some time to animate the countdown before finally unmounting it\n      startDate={Date.now()} // optional, just if you need some kind of progress UI\n    \u003e\n      \u003cComponent {...pageProps} /\u003e\n    \u003c/CountdownProvider\u003e\n  )\n}\n```\n\nAnd then your Countdown may look something like:\n\n```tsx\nimport { useCountdownStore } from '@bsmnt/drop'\n\nexport const Countdown = () =\u003e {\n  const humanTimeRemaining = useCountdownStore()(\n    (state) =\u003e state.humanTimeRemaining // keep in mind this is zustand, so you can slice this store\n  )\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCountdown\u003c/h1\u003e\n      \u003cul\u003e\n        \u003cli\u003eDays: {humanTimeRemaining.days}\u003c/li\u003e\n        \u003cli\u003eHours: {humanTimeRemaining.hours}\u003c/li\u003e\n        \u003cli\u003eMinutes: {humanTimeRemaining.minutes}\u003c/li\u003e\n        \u003cli\u003eSeconds: {humanTimeRemaining.seconds}\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eImportant note regarding SSR\u003c/summary\u003e\n\nIf you render `humanTimeRemaining.seconds`, there's a high chance that your server will render something different than your client, as that value will change each second.\n\nIn most cases, you can safely `suppressHydrationWarning` (see issue [#21](https://github.com/basementstudio/commerce-toolkit/issues/21) for more info):\n\n```tsx\nimport { useCountdownStore } from '@bsmnt/drop'\n\nexport const Countdown = () =\u003e {\n  const humanTimeRemaining = useCountdownStore()(\n    (state) =\u003e state.humanTimeRemaining // keep in mind this is zustand, so you can slice this store\n  )\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCountdown\u003c/h1\u003e\n      \u003cul\u003e\n        \u003cli suppressHydrationWarning\u003eDays: {humanTimeRemaining.days}\u003c/li\u003e\n        \u003cli suppressHydrationWarning\u003eHours: {humanTimeRemaining.hours}\u003c/li\u003e\n        \u003cli suppressHydrationWarning\u003eMinutes: {humanTimeRemaining.minutes}\u003c/li\u003e\n        \u003cli suppressHydrationWarning\u003eSeconds: {humanTimeRemaining.seconds}\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\nIf you don't want to take that risk, a safer option is waiting until your app is hydrated before rendering the real time remaining:\n\n```tsx\nimport { useEffect, useState } from 'react'\nimport { useCountdownStore } from '@bsmnt/drop'\n\nconst Countdown = () =\u003e {\n  const humanTimeRemaining = useCountdownStore()(\n    (state) =\u003e state.humanTimeRemaining // keep in mind this is zustand, so you can slice this store\n  )\n\n  const [hasRenderedOnce, setHasRenderedOnce] = useState(false)\n\n  useEffect(() =\u003e {\n    setHasRenderedOnce(true)\n  }, [])\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eCountdown\u003c/h1\u003e\n      \u003cul\u003e\n        \u003cli\u003eDays: {humanTimeRemaining.days}\u003c/li\u003e\n        \u003cli\u003eHours: {humanTimeRemaining.hours}\u003c/li\u003e\n        \u003cli\u003eMinutes: {hasRenderedOnce ? humanTimeRemaining.minutes : '59'}\u003c/li\u003e\n        \u003cli\u003eSeconds: {hasRenderedOnce ? humanTimeRemaining.seconds : '59'}\u003c/li\u003e\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n\u003c/details\u003e\n\n\u003cbr /\u003e\n\n## Examples\n\nSome examples to get you started:\n\n- [With Next.js + Shopify](./examples/nextjs-shopify)\n- [With Next.js + `localStorage`](./examples/nextjs-localstorage)\n\n\u003cbr /\u003e\n\n---\n\n## Contributing\n\nPull requests are welcome. Issues are welcome. For major changes, please open an issue first to discuss what you would like to change.\n\n## License\n\n[MIT](./LICENSE/)\n\n## Authors\n\n- Santiago Moran ([@morangsantiago](https://twitter.com/morangsantiago)) – [basement.studio](https://basement.studio)\n- Julian Benegas ([@julianbenegas8](https://twitter.com/julianbenegas8)) – [basement.studio](https://basement.studio)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasementstudio%2Fcommerce-toolkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbasementstudio%2Fcommerce-toolkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbasementstudio%2Fcommerce-toolkit/lists"}