@lime-bundles/core
v5.0.0
Published
Headless storefront primitives for the Lime Bundles Shopify app. Storefront API client, bundle parser, pricing math, analytics, and A/B helpers.
Maintainers
Readme
@lime-bundles/core
Headless storefront primitives for the Lime Bundles Shopify app. If you're not a merchant using Lime Bundles, this package probably isn't what you're looking for.
Used internally by @lime-bundles/react and @lime-bundles/widget. Import it directly when you're building on Vue, Svelte, a native app, or any non-React / non-custom-element surface with fetch.
Install
npm install @lime-bundles/coreNo peer dependencies. Works in any modern browser or Node runtime with fetch and AbortController.
Quick start
Fetch a bundle
import {
createStorefrontClient,
BUNDLE_METAOBJECT_QUERY,
parseMetaobjectBundleStrict,
type BundleMetaobjectResponse,
} from "@lime-bundles/core";
// Inside your server-side handler (Hydrogen loader, Next.js route
// handler, Express middleware, etc.) where `request` is the incoming
// Request object:
const client = createStorefrontClient({
shopDomain: "my-shop.myshopify.com",
accessToken: process.env.LIME_BUNDLES_TOKEN!,
// Required on SSR. Forwarded as Shopify-Storefront-Buyer-IP; Shopify
// may return 430 for server traffic without it.
buyerIp: request.headers.get("x-forwarded-for")?.split(",")[0].trim(),
});
const data = await client.query<BundleMetaobjectResponse>(
BUNDLE_METAOBJECT_QUERY,
{ id: "gid://shopify/Metaobject/42" },
);
const bundle = parseMetaobjectBundleStrict(
data.metaobject!.id,
data.metaobject!.fields,
);parseMetaobjectBundleStrict throws BundleParseError with a typed reason (not_found, invalid_type, inactive, not_started, expired). Use parseMetaobjectBundle for the non-throwing variant that returns null.
All bundles for a product
import { fetchBundlesForProduct } from "@lime-bundles/core";
const bundles = await fetchBundlesForProduct({
shopDomain,
storefrontAccessToken,
productHandle: "snowboard",
});Returns only active, in-schedule bundles. No further filtering needed.
Pricing that matches Shopify checkout
import { computeFixedPricing, formatCents } from "@lime-bundles/core";
const pricing = computeFixedPricing(bundle);
// { rows, totalCents, saleCents, savingsCents, headerBadge, currency }
console.log(formatCents(pricing.saleCents, pricing.currency));Uses per-unit floor rounding so widget totals match what the customer pays. Naive total * (1 - discount) arithmetic can drift a cent or two. Use computeBundleSaleCents(totalCents, discount) for mix-match or custom-quantity scenarios.
Apply merchant widget styling
import { applyWidgetConfigVars } from "@lime-bundles/core";
applyWidgetConfigVars(wrapperEl, bundle.widgetConfig);
// Sets every --lb-* CSS custom property the merchant configured in the
// admin. Your own stylesheet consumes them via var(--lb-*).A/B test resolution (link-group model)
import {
getLinkGroupAssignment,
parseMetaobjectBundle,
BUNDLE_METAOBJECT_QUERY,
withInContext,
} from "@lime-bundles/core";
// `bundle` is the primary of a link group. `bundle.linkGroup.variants` lists
// every variant by Shopify metaobject GID + weight (the primary's GID is
// among them). On `ParsedBundle`, `bundle.id` is the metaobject GID.
let renderBundle = bundle;
if (bundle.linkGroup?.isPrimary && bundle.linkGroup.variants?.length) {
const assignment = await getLinkGroupAssignment(
bundle.linkGroup.id,
bundle.linkGroup.variants,
);
// `assignment` is null when consent is missing or no variant could be
// chosen — fall through to the primary in that case.
if (assignment && assignment.variantMetaobjectId !== bundle.id) {
// Fetch the chosen variant's metaobject by GID and parse it. The
// primary's payload only carries variant GIDs + weights, so resolving
// a non-primary variant requires a second roundtrip (gated by the
// assignment cookie, so it only happens on first hit).
const data = await storefrontClient.query(
withInContext(BUNDLE_METAOBJECT_QUERY),
{ id: assignment.variantMetaobjectId },
);
renderBundle = parseMetaobjectBundle(data.metaobject);
}
}
// `renderBundle` is what the component should render. The library handles
// cookie writes (`__Host-_lb_ab_<linkGroupId>`) automatically when the
// shopper has consent. The React SDK's `useBundleData` wraps the fetch
// above behind a single `bundleGid` prop — use it when you don't need to
// own the resolution logic.Consent-gated via Shopify's customerPrivacy framework or the setConsent(true) helper; without consent the SDK returns null and the caller renders the primary with no cookies written. Variant attribution flows through cart line attributes (_lime_bundle_gid, _lime_link_group) that the orders/create webhook resolves server-side — no separate persistence call is needed.
Analytics
import { reportImpression, reportAddToCart, observeImpression } from "@lime-bundles/core";
const cleanup = observeImpression(widgetEl, () => {
reportImpression(
{ shopDomain, appUrl },
{ bundleGid: bundle.id, bundleType: bundle.bundleType },
);
});Full export list
Data: createStorefrontClient, hasInContext, withInContext, getCacheKey, fetchBundlesForProduct, parseMetaobjectBundle, parseMetaobjectBundleStrict, BUNDLE_METAOBJECT_QUERY, BUNDLES_FOR_PRODUCT_QUERY, CART_CREATE_MUTATION, CART_LINES_ADD_MUTATION, SHOP_CUSTOM_CSS_QUERY.
Pricing: parseCents, formatCents, percentageDiscountUnit, computeFixedPricing, computeBundleSaleCents, calculateTierSavings, getActiveTier, validateQuantity, formatMoney, calculateDiscount.
Widget config: WIDGET_CONFIG_DEFAULTS, mergeWidgetConfig, flattenWidgetConfig, applyWidgetConfigVars, CSS_VAR_MAP, PX_KEYS.
A/B + consent: getLinkGroupAssignment, pickVariantFromWeights, fnv1a, setConsent, hasConsent.
Analytics: reportImpression, reportAddToCart, observeImpression.
CSS: injectCustomCss, sanitizeCustomCss.
Image + countdown: transformImageUrl, formatCountdown.
Types (all exported): ParsedBundle, FixedBundleData, VolumeBundleData, MixMatchBundleData, WidgetConfig + 8 sub-configs, VolumeTier, DiscountConfig, ABVariantOverrides, CartLineInput, Product, ProductVariant, BundleParseError, StorefrontApiError, PricingRow, FixedBundlePricing, ImageTransform, BuyerInput, BuyerResolver, CacheKeyContext.
Markets & B2B
StorefrontClientConfig accepts country, language, and buyer for Storefront @inContext. When any is set, use withInContext(query) to add the directive to your query; otherwise emit the plain variant to keep responses publicly cacheable. hasInContext(config) picks for you.
buyer is either a BuyerInput ({ customerAccessToken, companyLocationId? }) or a callback that resolves it per request. Prefer the callback form so the customer access token doesn't sit in static prop trees or DOM snapshots.
For SWR / TanStack Query consumers, getCacheKey(queryName, vars, ctx) returns a stable cache key with the buyer token hashed (never raw). Buyer-contextual responses MUST be Cache-Control: private.
ParsedBundle gains marketVisibility: "all" | "specific" and marketIds: string[]. The bundle parser reads them from the metaobject's market_visibility / markets fields. Defaults to "all" when absent (existing not-yet-backfilled bundles work without behavior change).
Version policy
ParsedBundle, WidgetConfig, and CartLineInput are locked at v2.0.0. Narrowing via bundleType is safe; widening or changing field shapes is a breaking change. All three packages bump majors together.
License
MIT.
Support
Merchant support and bug reports: email via the Lime Bundles listing on the Shopify App Store.
