@artossoftware/stoq-sdk
v0.5.0
Published
STOQ preorders & back-in-stock for headless Shopify storefronts — typed client, React hooks for Hydrogen, drop-in script tag via jsDelivr, notify-me + preorder modals
Maintainers
Readme
@artossoftware/stoq-sdk
Headless storefront SDK for STOQ preorders and back-in-stock signups. Framework-free, fully typed, SSR-safe (nothing touches window/document at module load). Zero runtime dependencies.
Use it to build custom storefronts (Hydrogen, headless, heavily customized themes) on the same data the STOQ theme app embed uses.
Setup is two fields: your shop domain and its public Storefront API access token — the same token your headless store already uses. The SDK handles the data loading from there: the merchant's STOQ configuration in one query at init, and per-variant availability on demand (batched and cached) when you ask about a variant — all served from your shop's own Storefront API endpoint on Shopify's edge, so reads are fast, close to your shoppers, and as fresh as Shopify itself. Signups (createSignup) POST to the STOQ API and only fire on actual signups.
Install
npm install @artossoftware/stoq-sdk
# or
yarn add @artossoftware/stoq-sdkScript tag (CDN head-embed)
dist/stoq.min.js is a minified IIFE that exposes a global Stoq and auto-initializes from its own data- attributes on DOMContentLoaded:
<script
src="https://cdn.jsdelivr.net/npm/@artossoftware/stoq-sdk@0/dist/stoq.min.js"
data-shop="my-store.myshopify.com"
data-storefront-token="your-public-storefront-api-token"
defer></script>
<script>
window.addEventListener('stoq:loaded', async () => {
const state = await Stoq.client.getVariantState(123456789)
// ...
})
</script>data-shop and data-storefront-token are both required for auto-init. Optional attributes: data-market, data-locale, data-storefront-api-version, data-namespace. Without data-shop the script only defines the global — call Stoq.init({...}) yourself.
Once loaded, the client is available as Stoq.client (all methods from the API reference below), and the UI helpers can be called directly: Stoq.openModal(...), Stoq.openInlineForm(...), Stoq.removeInlineForm(), Stoq.openPreorderModal(...), Stoq.preorderButtonFor(variant).
npm / ESM
import { init } from '@artossoftware/stoq-sdk'
const stoq = await init({
shop: 'my-store.myshopify.com',
storefrontToken: '...', // public Storefront API token (required)
market: 123, // optional: numeric id or gid://shopify/Market/123
locale: 'fr-CA', // optional
})
// Ask about any variant by id (number, numeric string, or GID) — the SDK
// fetches its availability itself. Already have the variant object from
// your own product query? Pass it instead and the lookup is skipped:
// stoq.getVariantState({ id, availableForSale, currentlyNotInStock })
const state = await stoq.getVariantState(variantId)
if (state.isPreorder) {
// Add to cart via the Storefront Cart API:
const line = await stoq.cartLineFor(variantId, 2)
// { merchandiseId: 'gid://shopify/ProductVariant/...',
// sellingPlanId: 'gid://shopify/SellingPlan/...', quantity: 2 }
} else if (stoq.signupsEnabled /* && your data says it's sold out */) {
const result = await stoq.createSignup({ variantId, productId, email: '[email protected]' })
if (result.ok) {
// subscribed — `stoq:restock-modal:submitted` was fired on window
}
}Use the PUBLIC token. Shopify issues both public and private Storefront API tokens — storefrontToken must be the public one (designed to be exposed in browser code; in Hydrogen it ships as the PUBLIC_STOREFRONT_API_TOKEN env var). Never put a private Storefront API token (or any Admin API credential) in client-side code.
API
init(config): Promise<StoqClient>
Loads the shop's settings and selling plans and resolves to a ready client. Fires the documented stoq:loaded CustomEvent on window (detail.pageType === 'sdk'). Rejects when shop/storefrontToken are missing, when the settings metafield cannot be loaded, or when the Storefront API query fails as a whole.
| config field | Type | Notes |
|---|---|---|
| shop | string | Required. myshopify domain. Sent as X-Shopify-Shop-Domain on signups. |
| storefrontToken | string | Required. The shop's public Storefront API access token. |
| market | string \| number? | Shopify Market for market-scoped plans/limits. Accepts numeric id or GID. |
| country | string? | ISO country code for @inContext on the SDK's own availability lookups (market-correct bare-id calls). |
| locale | string? | Used for translated settings and forwarded on signups. |
| storefrontApiVersion | string? | Storefront API version for metafield reads. Default 2025-07. |
| namespace | string? | Metafield namespace. Default restockrocket_production. |
| host | string? | STOQ API host override (signups only). Default https://app.stoqapp.com. |
Variant inputs
Every variant-taking method accepts either a bare id (number, numeric string, or GID) or a variant object — your own variant with availability fields:
type VariantInput = number | string | {
id: number | string
availableForSale?: boolean // Storefront API field
currentlyNotInStock?: boolean // Storefront API field
quantityAvailable?: number // Storefront API field (needs the inventory read scope)
inventoryQuantity?: number // raw alternative
inventoryPolicy?: 'continue' | 'deny'
}A bare id always works — the SDK fetches the availability itself. Passing the object (you already have it on any product page) skips that lookup and uses your data verbatim.
StoqClient
| Member | Returns | Description |
|---|---|---|
| getVariantState(variant) | Promise<VariantState> | Derived state for one variant (see below). Resolves without network when the input carries availability fields. |
| cartLineFor(variant, quantity?) | Promise<CartLine> | Storefront Cart API line. merchandiseId is always set; sellingPlanId (gid://shopify/SellingPlan/<id>) is included only when the variant should be purchased as a preorder. quantity defaults to 1. |
| createSignup(input) | Promise<SignupResult> | Creates a back-in-stock signup (POST /api/v1/intents.json). Requires email or phone. Fires stoq:restock-modal:submitted on success. |
| openModal(options) | ModalHandle | Opens the notify-me (back-in-stock) signup modal — see below. |
| openInlineForm(options) | ModalHandle \| null | Renders the signup form inline in a container you supply. |
| removeInlineForm(container?) | void | Removes inline forms (all of them without an argument). |
| openPreorderModal(options) | Promise<PreorderConfirmation \| null> | Payment-option picker modal (see below). |
| preorderButtonFor(variant) | PreorderButtonContent \| null | Merchant-configured CTA copy: { label, disclaimer, shippingText }. null when no plan applies. |
| preorderPaymentOptionsFor(variant) | PreorderPaymentOption[] | Payment options (full vs partial), translated, default first. |
| preorderLineFor(variant, quantity?, sellingPlanId?) | CartLine \| null | Cart line for an explicitly chosen payment option. |
| sellingPlanFor(variant) | SellingPlan \| null | The plan that applies to the variant in the configured market (market-scoped plans win over global ones). |
| refresh() | Promise<void> | Busts the caches (configuration + availability) and refetches. |
| preorderEnabled | boolean | Whether the merchant has preorders enabled (shop-level). |
| signupsEnabled | boolean | Whether back-in-stock signups (notify-me) are enabled (shop-level). |
| settings | Readonly<StoqSettings> | Parsed shop settings (frozen) — prefer the named getters above for the common flags. |
| sellingPlans | ReadonlyArray<SellingPlan> | The shop's selling plans (frozen). |
| shop | string | The bound shop domain. |
Notify-me modal & inline form
openModal renders STOQ's back-in-stock signup modal — form, validation, duplicate handling, success state — with texts and colors defaulting to the merchant's STOQ design settings (override per call via texts/styles):
stoq.openModal({
variantId,
productId, // required — the backend rejects signups without it
productTitle: 'Trail Runner',
variantTitle: '43 / Blue',
imageUrl: 'https://cdn.shopify.com/…/shoe.jpg',
prefill: { email: customerEmail }, // optional
})openInlineForm({ container: '#notify-me', ...sameOptions }) renders the same form inside your own element instead of a modal; removeInlineForm() tears it down. Fires stoq:restock-modal:opened/closed/submitted. Browser-only — throws during SSR. See docs/modal.md for the full option reference.
Preorder button + modal
preorderButtonFor gives you the merchant-configured copy so you can render your own preorder button; openPreorderModal handles payment-option selection (full vs partial payment), quantity limits and the acknowledgement checkbox, then resolves with a ready-to-use cart line:
const variant = { id, availableForSale, currentlyNotInStock } // from your product query
const button = stoq.preorderButtonFor(variant)
// { label: 'Preorder now', disclaimer: '<p>Ships later…</p>', shippingText: 'Ships by May 2026' }
buttonEl.textContent = button.label
buttonEl.addEventListener('click', async () => {
const confirmation = await stoq.openPreorderModal({
variantId: variant,
productTitle: 'Trail Runner',
variantTitle: '43 / Blue',
imageUrl: 'https://cdn.shopify.com/…/shoe.jpg',
})
if (confirmation) {
// confirmation.cartLine carries the CHOSEN option's selling plan id:
// { merchandiseId, quantity, sellingPlanId: 'gid://shopify/SellingPlan/…' }
await addToCart(confirmation.cartLine)
}
})When the plan has a single payment option, no acknowledgement requirement and the quantity is within the preorder limits, openPreorderModal resolves immediately without rendering anything (mirroring the theme embed, which only opens its modal when a choice or correction is needed). See docs/preorder-modal.md for the full option/behavior reference.
Rendered widgets (drop-in)
The methods above are the building blocks. If you don't want to wire them up yourself, the render* methods create STOQ's own element, insert it into a container you point at, wire the click → modal → cart flow, and return a handle to re-render or tear down. They're the imperative twins of the React components — usable from a head-embed or any vanilla-JS storefront (no JSX needed):
const handle = await stoq.renderPreorderWidget(variant, {
container: '#stoq-slot', // element or CSS selector
onAddToCart: (line) => addToCart(line),
badge: true, // also render a "Preorder" badge
notifyMeFallback: true, // show notify-me when not preorderable
productId, productTitle, variantTitle, imageUrl,
})
// Re-render when the shopper switches variant (idempotent — replaces in place):
await handle.update(nextVariant)
handle.destroy() // remove it| Method | Renders | Twin of |
|---|---|---|
| renderPreorderWidget(variant, opts) | Badge + CTA button (with optional notify-me fallback) — the one-call entry point | — |
| renderPreorderButton(variant, opts) | The preorder CTA <button>, shown only when preorderable | <StoqPreorderButton> |
| renderPreorderBadge(variant, opts) | A Preorder <span> badge | <StoqPreorderBadge> |
| renderNotifyMe(variant, opts) | A button that expands into the back-in-stock signup form | <StoqNotifyMeButton> |
Each returns a Promise<WidgetHandle> — { element, update(variant?), destroy() }. Re-running a render* call against the same container replaces the previous widget. Styling uses the same stoq-* classnames and --stoq-* custom properties as the React components (see STOQ_COMPONENT_STYLES).
Unlike the theme app embed's renderPreorderButton, which hijacks the theme's existing add-to-cart button, these render their own element into a container you own — the right model for headless storefronts. In the head-embed they're also on the global: Stoq.renderPreorderWidget(...) / window._RestockRocket.renderPreorderWidget(...).
Custom elements (zero-JS drop-in)
For the simplest possible integration — no JS wiring at all — use the custom elements. The CDN head-embed registers them automatically on auto-init; just drop the tag in your markup:
<stoq-preorder-widget
variant-id="40020223..."
product-id="73912..."
product-title="Trail Runner"
badge
notify-me-fallback>
</stoq-preorder-widget>
<script>
document.addEventListener('stoq:add-to-cart', (e) => addToCart(e.detail.line))
</script>Attributes in, events out. Inputs are attributes — change variant-id (directly, or via a framework binding like variant-id={selected.id}) and the element re-renders itself (no update() call). Because HTML attributes can't carry a callback, add-to-cart surfaces as a stoq:add-to-cart CustomEvent (detail: { line, confirmation }, bubbles + composed). Pass availability via available-for-sale / currently-not-in-stock attributes to resolve with no lookup. For a no-framework storefront, watch="<selector>" (e.g. watch="select[name=id]") copies the source's value into variant-id on change.
Elements: <stoq-preorder-widget> (badge + CTA, with badge / notify-me-fallback / label / quantity), <stoq-preorder-badge> (text), <stoq-notify-me> (product-id required, expanded, label).
For npm/ESM consumers (no auto-init), register them against your init()-ed client:
import { init, defineStoqElements } from '@artossoftware/stoq-sdk'
const stoq = await init({ shop, storefrontToken })
defineStoqElements(stoq) // now <stoq-*> tags work anywhere in the pageVariantState
| Field | Type | Meaning |
|---|---|---|
| isPreorder | boolean | An enabled plan applies in the current market AND the variant is currently sellable as a preorder (decided by its availability — supplied by you or looked up by the SDK). |
| sellingPlanId | number \| null | Numeric Shopify selling plan id to purchase with. |
| sellingPlanGid | string \| null | Same as gid://shopify/SellingPlan/<id>. |
| shippingText | string \| null | Estimated-shipping text (market-scoped). |
| maxCount | number \| null | Configured preorder unit limit (null = unlimited/not set). |
| remainingCount | number \| null | maxCount - sold, clamped at 0 (null when unlimited). |
There is deliberately no out-of-stock flag: your storefront already knows availability. Gate notify-me UI on your own availability data plus client.signupsEnabled.
SignupInput / SignupResult
createSignup({
variantId, // required — id (or variant object; only the id is used)
productId, // required — the backend rejects signups without it
email?, phone?, // at least one required; phone in E.164
channel?, // 'email' | 'sms'
quantity?, // default 1
name?, locale?,
acceptsMarketing?, // marketing consent → customer.accepts_marketing
country?, // → customer.country + intent.country
countryCode?, // → customer.country_code
shopifyCustomerId?, // number | GID — logged-in shopper → customer.shopify_customer_id
product?: { // → product params (snake_cased)
title?, variantTitle?, vendor?, sku?, variantCount?,
},
}) => { ok, status, intents, errors }ok is true when the backend created the signup (HTTP 201). Validation failures (e.g. duplicate signup) come back as ok: false with errors populated. The configured market is forwarded automatically on both the intent and the customer.
Headless tip: pass product (at least title, variantTitle, variantCount) with every signup. STOQ uses it to render notifications even when the product hasn't been synced into STOQ's database yet — common on headless storefronts where the shop's catalog webhooks may lag a brand-new product.
Events
Dispatched on window; silently skipped during SSR. Names and payloads match the documented STOQ custom events.
| Event | When | detail |
|---|---|---|
| stoq:loaded | after init resolves | { pageType: 'sdk', enabled, settings, preorderEnabled } |
| stoq:restock-modal:submitted | after a successful createSignup | { action: 'submitted', product: { id }, variant: { id }, customer: { email, phone } } |
| stoq:preorder-modal:opened | preorder modal rendered | { productData, variantId, sellingPlan } |
| stoq:preorder-modal:closed | preorder modal confirmed/dismissed | { action: 'accepted' \| 'rejected', productData, variantId, selectedSellingPlanId, sellingPlan, quantity?, acknowledgedPreorder? } |
| stoq:preorder-payment-option:selected | shopper picks a payment option in the modal | { paymentWidget, variantId, selectedSellingPlanId } (dispatched on the widget element, bubbles to window) |
How data loads
Two kinds of data, both from your shop's own Storefront API endpoint (https://{shop}/api/{version}/graphql.json, Shopify's edge):
- Merchant configuration (settings, preorder offers, shipping texts, limits) — STOQ mirrors it onto shop metafields;
initreads them with one GraphQL query, session-cached for 5 minutes. - Per-variant availability — fetched on demand when you ask about a variant by bare id: every lookup in the same tick is batched into one
nodes(ids: [...])query, and results are cached in memory for 60 seconds. Pass your own variant object (you already have it from your product query) and the lookup is skipped entirely.
Availability is what decides whether an attached offer is currently sellable:
| The variant says | Meaning | Sellable under |
|---|---|---|
| currentlyNotInStock: true | out of stock but still purchasable (inventory_policy: continue) | STOQ-inventory offers |
| availableForSale: true and currentlyNotInStock: false | in stock | Shopify-inventory offers (sell in-stock units as preorders) |
When you pass a variant object, quantityAvailable or raw inventoryQuantity + inventoryPolicy work too.
Markets: when market is configured, the metafields query selects STOQ's _for_market_<id> metafields and prefers them — same precedence as the theme embed. The SDK's own availability lookups answer for the shop's default country unless you set country (applied as @inContext); if you localize inventory, passing your own @inContext-queried variant objects is the most precise option.
If the metafields query fails as a whole (bad token, wrong API version) init rejects with a StoqStorefrontApiError; a missing/invalid individual metafield — or a failed availability lookup — degrades gracefully (defaults + console warning).
Caching, freshness & rate limits
The metafield response is cached in sessionStorage (TTL 5 minutes) so repeat page views within a session don't re-fetch; keys include the shop, market, namespace and API version. When sessionStorage is unavailable (SSR, private browsing, quota) an in-memory cache is used. Availability lookups are cached in memory only, for 60 seconds — availability is volatile by nature. refresh() busts both and refetches.
Two things to design for:
- Merchant configuration can be briefly stale. Between Shopify's edge caching, the metafield sync cadence and the 5-minute session cache, offer configuration may lag merchant changes by a few minutes. Availability is much fresher (60-second cache, or exactly your own data when you pass the variant object) — and Shopify enforces the real sellable state at checkout regardless.
- Writes are rate limited. The signup endpoint (
POST /api/v1/intents.json) is subject to rate limiting. Normal shopper-driven signups never get near the limits; if you batch or replay signups programmatically, expect429s and back off.
Behavior notes & caveats
- Pass the variant object when you have it. It skips the SDK's availability lookup and uses your (possibly
@inContext-localized) data verbatim; bare ids cost one batched Storefront API call per minute per variant. - Gate notify-me on your own availability data (
!variant.availableForSale) plusclient.signupsEnabled. - Property-based plans (
use_shopify_selling_plan: false) intentionally never get asellingPlanIdincartLineFor— those preorders are tracked via line item properties by the theme embed, not Shopify selling plans. - Selling plan id preference:
default_shopify_selling_plan_id(multi-payment-option plans) wins overshopify_selling_plan_id, matching the theme embed.
React / Hydrogen
@artossoftware/stoq-sdk/react is a hooks layer over the same client, built for Hydrogen and other React 18+ storefronts. React is an optional peer dependency — vanilla consumers of the core entry never need it installed.
npm install @artossoftware/stoq-sdk reactSSR-safe: importing the module never touches window/document, and the client initializes inside an effect — server renders see status: 'loading' (the bundled components render nothing until the client is ready).
Provider
Wrap your app (or just the product route) once. In Hydrogen, pass the public Storefront API token your app already has — the PUBLIC_STOREFRONT_API_TOKEN env var every Hydrogen project ships with:
import { StoqProvider } from '@artossoftware/stoq-sdk/react'
export default function App() {
return (
<StoqProvider
config={{
shop: 'my-store.myshopify.com',
storefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
}}
>
<Outlet />
</StoqProvider>
)
}config takes the same fields as init. The client re-initializes if any field changes; inline config={{ ... }} literals are fine.
Hooks
Every hook takes a VariantInput — a bare id works (the SDK fetches availability itself), and passing your variant object skips the lookup. Inline literals are fine; memoization keys on the field values, not object identity:
| Hook | Returns | Notes |
|---|---|---|
| useStoq() | { client, status, error, revision, refresh } | status is 'loading' \| 'ready' \| 'error'. Throws outside <StoqProvider>. |
| useStoqVariant(variant) | VariantState \| null | null while loading. Re-derives on ready, on variant change, and after refresh(). |
| useStoqCartLine(variant, quantity?) | CartLine \| null | Wraps client.cartLineFor — includes sellingPlanId only when the variant should be sold as a preorder. |
| useStoqPreorder(variant) | { state, button, options, openModal, resolveLine } | Preorder UI state: merchant CTA copy, payment options (default first), openModal() (the preorder modal), and resolveLine(quantity?, sellingPlanId?) for the final cart line. |
| useStoqSignup() | { submit, submitting, result, error } | submit(input) wraps client.createSignup; validation failures land in result.ok === false, thrown errors in error. |
StoqClient has no subscription mechanism — derived state only changes when data is refetched. Call useStoq().refresh() to bust the cache and refetch; it bumps an internal revision counter so every hook consumer re-renders with freshly derived state.
Adding a preorder to the cart with Hydrogen's CartForm
The cart line from useStoqCartLine (or client.cartLineFor) is already in CartLineInput shape — merchandiseId, quantity, and sellingPlanId when the variant should be purchased as a preorder — so it can be passed straight to cartLinesAdd. Include availableForSale and currentlyNotInStock in your variant fragment and pass the variant object through:
import { CartForm } from '@shopify/hydrogen'
import { useStoqVariant, useStoqCartLine } from '@artossoftware/stoq-sdk/react'
function AddToCartButton({ variant }: {
variant: { id: string; availableForSale: boolean; currentlyNotInStock: boolean }
}) {
const state = useStoqVariant(variant)
const line = useStoqCartLine(variant, 1)
if (line === null) return null // client still loading (or SSR)
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesAdd}
inputs={{ lines: [line] }}
>
<button type="submit">
{state?.isPreorder ? 'Preorder now' : 'Add to cart'}
</button>
{state?.isPreorder && state.shippingText ? <p>{state.shippingText}</p> : null}
</CartForm>
)
}When state.isPreorder is true, line.sellingPlanId is gid://shopify/SellingPlan/<id> and Shopify attaches the selling plan (deferred purchase option) to the line. Property-based plans (use_shopify_selling_plan: false) intentionally never get a sellingPlanId.
Components
Polaris-free, with minimal overrideable default styling: the components emit stoq-* classnames and inject one small stylesheet whose selectors are all wrapped in :where() — zero specificity, so any rule in your own CSS wins. Quick theming via CSS custom properties:
:root {
--stoq-accent: #1a56db; /* buttons */
--stoq-accent-contrast: #fff;
--stoq-radius: 8px;
--stoq-badge-background: #eef1f4;
--stoq-badge-color: #303030;
--stoq-border: #c9cccf; /* inputs */
--stoq-error: #b3261e;
--stoq-success: #1a7f37;
}To opt out of the defaults entirely, ship your own <style data-stoq-sdk-components> (or <link data-stoq-sdk-components>) element — when one exists the SDK injects nothing. The full default stylesheet is exported as STOQ_COMPONENT_STYLES. All components render nothing during SSR/loading.
import { StoqNotifyMeButton, StoqPreorderBadge, StoqPreorderButton } from '@artossoftware/stoq-sdk/react'
<StoqPreorderBadge variantId={variant} /> {/* default text "Preorder" */}
<StoqPreorderBadge variantId={variant}>Ships in March</StoqPreorderBadge>
<StoqPreorderButton
variantId={variant}
productTitle={product.title}
imageUrl={product.featuredImage?.url}
onAddToCart={(line) => addLinesToCart([line])} {/* CartLineInput with the chosen option's sellingPlanId */}
/>
<StoqNotifyMeButton
variantId={variant.id}
productId={product.id}
onSuccess={(result) => console.log('subscribed', result)}
/>StoqPreorderBadgerenders its children (default"Preorder") in<span class="stoq-preorder-badge">only when the variant is currently preorderable — pass the variant object so that check is exact.StoqPreorderButtonrenders<button class="stoq-preorder-button">with the merchant-configured CTA label (preorder_button_text; override viachildren) only when the variant is currently preorderable. Clicking opens the preorder modal when the plan has multiple payment options or a required acknowledgement — otherwise it confirms immediately without rendering a modal — and callsonAddToCart(line, confirmation)with the finalCartLineInputhonoring the chosen option's selling plan id. Props:variantId(aVariantInput),onAddToCart(required),quantity?,productId?,productTitle?,variantTitle?,imageUrl?,modal?(per-calltexts/styles). For full control build your own button onuseStoqPreorder(). See docs/preorder-modal.md.StoqNotifyMeButtonrenders a button that expands into an inline email form and creates a back-in-stock signup. It hides itself when the shop's signup widget is disabled, but is intentionally not gated on stock state — gate rendering on your own availability data, e.g.!variant.availableForSale. Classnames:stoq-notify-me,stoq-notify-me__button,stoq-notify-me__form,stoq-notify-me__input,stoq-notify-me__submit,stoq-notify-me__error,stoq-notify-me__success.
For full control, build your own UI on useStoqSignup() / useStoqVariant() — the components are thin wrappers over those hooks.
Testing against a real store
Two harnesses ship in the package:
- Playground —
yarn build && yarn playground, then openhttp://localhost:4477/examples/playground.html. Enter a shop domain and its public Storefront API token (persisted in localStorage), probe variants under different availability states, open both modals, and run the docs' verification checklist as one-click checks. - CLI smoke —
yarn smoke(env:SHOP,STOREFRONT_TOKEN,VARIANT,PRODUCT,EMAIL) runs init → state → button → cart line → signup against a live store and exits non-zero on failure; suitable as a release gate.
Development
yarn install
yarn build # tsup → dist/index.js (+ .d.ts, ESM) and dist/stoq.min.js (IIFE, global `Stoq`)
yarn test # vitest (fetch fully mocked, no network)
yarn typecheckThis package is intentionally self-contained: it has its own lockfile and does not participate in the Rails app's webpack build.
