npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tabcommerceio/buy-together-sdk

v1.0.0

Published

Storefront JavaScript SDK for Product Bundle offers, exact Variant quotes, Add bundle, and isolated Bundle event delivery.

Readme

Product Bundle SDK

@tabcommerceio/buy-together-sdk provides the Product Bundle storefront workflow: Offer lookup, exact Variant re-quotes, verified Add bundle, and independent Bundle event delivery. It is browser-targeted ESM with TypeScript declarations and no runtime dependencies. It does not import or mount the Cross Sell SDK.

Install and choose the SDK

npm install @tabcommerceio/buy-together-sdk

Use @tabcommerceio/cross-sell-sdk for individual recommendations and Cross Sell tracking. Use this package for Product Bundle Offers. An Online Store can use both packages, with separate clients, roots, tokens, and event observers.

1.0.0 is the first public release of the current component-based contract. It replaces the repository's earlier experimental role-based API: components are identified by component_id, and every purchasable selection carries a short quote_expires_at. Code using older workspace builds must migrate all component tuples and implement the re-quote flow below. The package name stays buy-together-sdk; a product display-name change does not require an npm rename.

Before connecting

Use the installed shop's actual App Proxy path; the examples use the development path /apps/tab-cross-sell-dev. Requests run in the shopper's Online Store browser through Shopify's signed proxy. Do not put Admin API credentials into browser code. Resolve and quote require the shop's api_access entitlement (currently Optimize, or an otherwise eligible enabled plan).

An Offer is a peer Product set. Every member Product can be its PDP source. The server owns eligible variants, contextual prices, totals, tokens, and the Bundle instance identity. Never calculate a replacement quote in the browser. The current protocol accepts two to four distinct component Products.

Custom UI: resolve, re-quote, add

The following controller uses public exports only. Bind your option controls to chooseVariants, refresh displayed totals from its returned Offer, and enable Add only when canAdd() is true. Disable option controls while Add is running. Each selection contains every component tuple, including unchanged members. Replace the example country, currency, proxy path, slot, and locale root with current shop facts. Use offer_ids instead of slot_key when selecting explicit Offers; never send both selectors.

import {
  init,
  createQuoteRequestFence,
  type BundleOffer,
  type BundleQuoteSelection,
} from '@tabcommerceio/buy-together-sdk'

const client = init({ proxyBase: '/apps/tab-cross-sell-dev' })
const context = { country: 'US' }
const cart = { currency: 'USD' }
const routeRoot = '/' // Use the shop's Shopify.routes.root, e.g. '/fr/'.
const fence = createQuoteRequestFence()
const resolved = await client.resolve({ slot_key: 'summer-bundles', context, cart })
let tokenSource: BundleOffer | null = resolved?.offers[0] ?? null
let ready: BundleOffer | null = tokenSource
let adding = false
let needsCartRecovery = false

export function canAdd(): boolean {
  return !adding && !needsCartRecovery && ready !== null &&
    Date.parse(ready.quote_expires_at) > Date.now()
}

export async function chooseVariants(selections: BundleQuoteSelection[]) {
  if (!tokenSource || adding || needsCartRecovery) return null
  const tracking_token = tokenSource.tracking_token
  ready = null // The previous selection cannot remain purchasable.
  const attempt = fence.begin() // Aborts and invalidates the previous request.
  const result = await client.quote({ tracking_token, selections, context, cart }, attempt.signal)
  if (!fence.isCurrent(attempt)) return null
  if (!result?.eligible || !result.offer) return null
  tokenSource = result.offer
  ready = result.offer
  return ready // Render these server-returned totals and selected variants.
}

export async function addSelectedBundle() {
  if (!canAdd() || !ready) return null
  adding = true
  try {
    const result = await client.addAll(ready, { routeRoot })
    if (result.status === 'added') {
      ready = null
      tokenSource = null // Resolve again before starting another Bundle instance.
    } else if (result.status === 'recoverable_error') {
      needsCartRecovery = true
      ready = null
      window.location.assign(`${routeRoot}cart`)
    }
    return result
  } finally {
    adding = false
  }
}

export function destroyBundleUI() {
  fence.cancel()
  client.destroy()
}

Read a component's eligible variants to construct its selected { component_id, product_id, variant_id } tuple. Pass only the latest eligible server Offer to addAll(). Preserve its bundle_instance_id and tracking token; never mint a replacement instance after the server has signed it. Re-resolve when the live Offer is stale. Update country/cart facts and re-resolve when the shopper's market or currency changes.

Add outcomes

| Status | Meaning and next action | |---|---| | added | Both the add response and a fresh cart read prove the exact component/property tuples. Refresh your cart UI; resolve a new instance for another add. | | failed | A fresh read proves a clean no-op. Let the shopper retry only with a still-valid quote. | | compensated | Partial instance lines were removed and the clean result was verified. A deliberate retry with a valid quote is safe. | | recoverable_error | The cart outcome could not be proven. Stop retries and navigate to a fresh cart; do not mint another request/instance to retry blindly. |

The SDK sends one Shopify Ajax /cart/add.js multi-item request with the five _tab_bundle_* private properties. Its compensation targets only lines from that exact instance/token. It queues add_to_cart only after full cart proof; custom consumers must not send a duplicate event. Resolve/quote return null for transport failures; an ineligible quote must leave Add disabled.

Rendering and tracking

resolvePDP() returns server-rendered HTML. mountPDP() inserts that HTML, fences overlapping mounts, and observes impressions. Its request requires the currently selected source Product/Variant GIDs and a new crypto.randomUUID() instance before requesting a signed fragment. It does not wire custom UI option/add handlers: those remain the host's responsibility. The official Theme App Block bundles its own interaction controller and needs no npm install.

For custom cards, use createBundleImpressionObserver with an observe target containing the actual Offer's offer_id, tracking_token, and request_id; forward its onImpression payload to client.track. The public observer enforces 50% visibility for 500 ms and excludes hidden-tab time. Track clicks with the same Offer identity. client.flush() drains queued events, and client.destroy() releases client resources; disconnect any observer you create separately. No Bundle root or event belongs to the Cross Sell client.

In browsers init uses the page's document/window for visibility and pagehide flushes. Explicit refs are only needed for custom environments.

Using both packages

import { init as initCrossSell } from '@tabcommerceio/cross-sell-sdk'
import { init as initBundles } from '@tabcommerceio/buy-together-sdk'

const proxyBase = '/apps/tab-cross-sell-dev'
const crossSell = initCrossSell({ proxyBase })
const bundles = initBundles({ proxyBase })
// Bind each client to its own cards and event identities.
// On teardown: crossSell.destroy(); bundles.destroy().

The public ESM API supplies data/commerce primitives for your own markup. The internal official Theme IIFE is built separately as tab-buy-together-storefront.js and is excluded from this npm package. After a proven official add it synchronizes the theme cart and emits tab:buy-together:cart-changed; custom ESM consumers refresh their own cart UI.

Maintainer release

Source: tabcommerce/tab-cross-sell, directory packages/web/buy-together-sdk. The shared workflow publish-cross-sell-sdk.yml publishes exact manifest versions for both SDKs from main using package-scoped npm Trusted Publishing. The initial publication requires an authenticated package owner; configure the Bundle package's trust binding after that first publish. Local validation uses pnpm publish:buy-together:dry; the authorized manual release command is pnpm publish:buy-together. See scripts/release/README.md in the repository for digest, authentication, tagging, and post-publication checks.