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

@moonbase.sh/storefront

v3.8.2

Published

Package to let you embed Moonbase.sh into your storefront as a payment and delivery provider

Downloads

10,097

Readme

@moonbase.sh/storefront

Embeddable Moonbase storefront widget for browser apps.

This package wraps auth, cart, checkout, voucher redemption, product downloads, activation, and subscription management behind one client-side API.

Learn more about our embedded storefront in our official docs: https://moonbase.sh/docs/storefronts/embedded/

Install

pnpm add @moonbase.sh/storefront

Quick start

import Moonbase, { MoonbaseEvent } from '@moonbase.sh/storefront'

Moonbase.setup('https://demo.moonbase.sh', {
  toolbar: {
    enabled: true,
    location: 'top-right',
  },
})

Moonbase.on(MoonbaseEvent.CheckoutCompleted, ({ order }) => {
  console.log('Order completed', order.id)
})

setup should be run once, client-side.

When you load the widget via the CDN script tag:

<script src="https://assets.moonbase.sh/storefront/moonbase.js"></script>
<script>
  Moonbase.setup('https://demo.moonbase.sh')
  Moonbase.on('checkout-completed', ({ order }) => console.log(order.id))
</script>

…you can call Moonbase.setup(...), Moonbase.on(...), and any intent method from anywhere on the page — including inline <script> in <head> — without waiting for DOMContentLoaded. The CDN script is a tiny loader that queues calls and replays them once the real bundle and the DOM are both ready.

When you import from npm (import Moonbase from '@moonbase.sh/storefront'), setup still needs the DOM to be available before it runs, since there is no loader in that path.

Testing pre-release builds with ?mb_storefront_version=

The CDN loader recognises an ?mb_storefront_version= query parameter for opting into a non-default storefront build on a per-page-load basis. This is intended for testing pre-release changes on a live merchant site without changing the embed code.

  • ?mb_storefront_version=next — loads the latest @next snapshot from /storefront/next/moonbase.js
  • ?mb_storefront_version=pr-<N> — loads the snapshot from a labeled PR <N> from /storefront/pr-<N>/moonbase.js (only present while the PR carries the release-snapshot label)
  • ?mb_storefront_version=2.1.0 — pins to a specific published version from /storefront/<version>/moonbase.js
  • ?mb_storefront_version=latest or omitted — loads the current stable release (default)

Don't use this to pin a production embed to a specific version — pin the <script src> URL instead.

Host page requirements

The widget renders directly into the host page's DOM and inherits its viewport. For the widget to render correctly on mobile, the host page should declare a standard responsive viewport:

<meta name="viewport" content="width=device-width, initial-scale=1" />

Hosts that ship a fixed-width legacy mobile viewport (for example Wix's classic mobile sites use <meta name="viewport" content="width=320, user-scalable=yes">) cause the browser to upscale every CSS pixel on the page. The widget auto-detects this and counter-scales itself with CSS zoom to remain readable; opt out with disableViewportCompensation: true if you want the widget to match the host's upscaled density. On Wix specifically, mounting the widget into a contained element via target is also an option — it switches the drawer from a viewport overlay to an in-flow component and sidesteps the upscale entirely.

Trigger intents

Use intent methods to open specific views or execute actions:

Moonbase.sign_in({ email: '[email protected]' })

Moonbase.add_to_cart({
  product_id: 'my-product',
  quantity: 1,
})

Moonbase.view_cart()
Moonbase.checkout()
Moonbase.view_products()

All intents are snake_case methods on the Moonbase instance (for example view_product, manage_subscription, redeem_voucher, join_group).

Configure behavior and theme

You can pass options in setup(...) and update them later with configure(...):

Moonbase.configure({
  auth: {
    signUp: {
      enabled: false,
    },
  },
  cart: {
    quantity: 'single',
  },
  checkout: {
    redirect: 'auto',
  },
  pricing: {
    trailingZeros: 'always',
    currencyDisplay: 'code',
  },
  theme: {
    colors: {
      primary: '#E5A000',
      background: 'white',
    },
    fonts: {
      heading: 'Aleo',
      body: 'Inter',
    },
  },
})

checkout.redirect decides where checkout happens:

| Value | Behavior | | --- | --- | | 'auto' (default) | Overlay on desktop, full-page redirect to the hosted checkout on mobile. | | 'always' / true | Always redirect to the hosted checkout. | | 'never' / false | Always use the overlay. |

The overlay keeps buyers on your page, but it cannot show Apple Pay or Google Pay. Those wallets require the top-level page's domain to be a registered payment-method domain, and for an embedded storefront that is your own site, not Moonbase's — no amount of configuration on our side changes that. Redirecting makes the hosted checkout the top-level document, so the wallets appear. 'auto' takes that trade only on mobile, where wallet usage concentrates, and keeps the overlay everywhere else. Phones redirect; tablets and desktops get the overlay.

A redirected buyer is always brought back to your page once they've paid, so checkout-completed fires and the cart clears exactly as it does with the overlay. The hosted checkout stays anonymous for the trip — it prefills from the order rather than signing the buyer into a Moonbase account, matching the overlay's behavior.

Customer groups

If you keep customer lists in Moonbase and have opened one to public sign-ups, name it by ID and the widget enrolls people who use the matching form:

Moonbase.configure({
  groups: {
    signUp: ['newsletter-2026'],
    subscribe: ['newsletter-2026'],
    checkout: ['buyers-2026'],
  },
})

Enrollment is silent. There is no checkbox and no label, because the API deliberately tells a storefront nothing about a group: an ID naming a list the merchant has not opened to public sign-ups is skipped without a word, so a stale ID here can never fail somebody's registration or somebody's purchase. At most five groups per request are applied, and IDs are matched lowercase.

signUp and subscribe can be added to per link with mb_groups:

<a href="?mb_intent=sign_up&mb_groups=beta,vip">Join the beta</a>

The two are unioned rather than overridden: configure names the lists everyone signing up through your site joins, the link adds whichever the campaign is for. A sign-up form on your own page adds to subscribe the same way, with a field named groups. checkout is configuration-only, because the cart outlives any one link.

For somebody who already has an account, join_group does it as a request of its own, and unlike the three above it reports whether it worked:

<a href="?mb_intent=join_group&mb_group_id=beta">Join the beta list</a>

It signs the visitor in first if they aren't, and emits joined-group on success. Joining a list twice is not an error.

Fonts

theme.fonts.heading and theme.fonts.body each take one of:

| Value | What gets loaded | | --- | --- | | A built-in name, e.g. 'Montserrat' | The widget loads that family from assets.moonbase.sh. | | 'inherit' | Nothing. The widget renders in whatever font your page already uses. | | 'system' | Nothing. The widget uses its own system stack. | | { family } | Nothing. Names any family your page already loads. | | { family, stylesheet } | The stylesheet you name, from wherever you host it. |

The built-in families are Inter, Roboto, EB Garamond, Merriweather, Poppins, PT Serif, Montserrat and Aleo. Either slot accepts any of them.

They are served from Moonbase's own asset host — the same origin that serves the widget itself, and the same payload the hosted checkout uses, so the two share a cache entry. Nothing is requested from Google Fonts, which matters if you sell into mainland China (where fonts.googleapis.com is unreachable) or want to keep visitor IPs away from Google.

The distinction that catches people out: 'Inter' means "load Inter for me"; { family: 'Inter' } means "the family is Inter, I already load it".

// Inherit your page's fonts. Loads nothing, requests nothing.
Moonbase.configure({ theme: { fonts: { heading: 'inherit', body: 'inherit' } } })

// Self-hosted faces — the widget injects the stylesheet.
Moonbase.configure({
  theme: {
    fonts: {
      heading: { family: '"Noto Serif SC", serif', stylesheet: '/assets/fonts/noto-serif-sc.css' },
      body: { family: '"Noto Sans SC", sans-serif', stylesheet: '/assets/fonts/noto-sans-sc.css' },
    },
  },
})

// A family your page already loads — the widget just names it.
Moonbase.configure({ theme: { fonts: { body: { family: '"Inter var", sans-serif' } } } })

pricing.trailingZeros controls how whole prices render. The default 'auto' drops the decimals — a $10.00 product shows as $10. Set it to 'always' to keep the currency's own fraction digits instead ($10.00, and still ¥1000 for zero-decimal currencies like JPY). It applies to the widget's own UI and to data-moonbase-render price elements on your page alike.

pricing.currencyDisplay picks which marker identifies the currency. Taking $10 in USD read from a British locale, the default 'narrowSymbol' renders the shortest symbol ($10), 'symbol' the locale's usual one (US$10), 'code' the ISO 4217 code (USD 10), and 'name' spells it out in the visitor's language (10 US dollars). The symbol and its placement still follow the visitor's own locale — this only chooses between the four forms.

Both pricing options cover the widget's UI, your data-moonbase-render price elements, and the checkout itself: the widget passes them to Moonbase's own checkout page, so the prices a buyer pays are written the same way as the ones that got them there.

cart.offers.cartWide covers offers keyed off the cart as a whole, such as "10% off orders over $50", rather than off a single product. These apply themselves the moment the cart qualifies, so there is nothing for the buyer to press. While the cart is short of the threshold the widget shows a meter and how much further to go; once it qualifies, the saving appears as its own row above the total.

Moonbase.configure({
  cart: {
    offers: {
      cartWide: {
        showProgress: true,
        savingLabel: 'You save',
      },
    },
  },
})

Set showProgress to false to let such an offer apply silently: the saving still shows in the totals, the nudge does not. With no cart-wide offer configured on your storefront, the meter never renders either way. savingLabel names the saving row in the cart totals.

Offers keyed off a single product work the other way round: the widget surfaces one as a card in the cart, and the buyer presses it to take the offer. That card is hidden once the offer's product is already in the cart, so a buyer who adds that product before the one that qualifies them for the offer never gets a chance to claim it. cart.offers.autoApply closes that gap.

Moonbase.configure({
  cart: {
    offers: {
      autoApply: true,
    },
  },
})

With it on, the cart applies an eligible offer to the product it discounts by itself, once every product that offer names is in the cart. It never adds anything to the cart on the buyer's behalf, and it never takes an offer the cart is still selling: an offer naming a product they have not added keeps showing as a card and still takes a press, since an offer is good for one line only and applying it early would take that card away. It never replaces an offer already on a line either. Off by default.

That governs when an offer is taken rather than where it stays: remove one of the products that qualified an offer and it stays on the line it landed on, just as an offer claimed from a card does. A buyer can already put a multi-target offer on one product and never add the other, so nothing here is a cart they couldn't have built by hand.

cart.minimumTotal sets the smallest order the cart will check out. Below it, the checkout button gives way to a meter of the same size that fills as the cart grows and tells the buyer how much more to spend; it turns back into the button once the cart reaches the minimum.

Moonbase.configure({
  cart: {
    minimumTotal: { USD: 25, EUR: 25, GBP: 20 },
  },
})

Amounts are keyed by currency code, in major units, because Moonbase picks each visitor's currency and the widget never converts one into another. A currency you leave out has no minimum. The minimum is measured on the cart's Total, after product discounts and offers but before coupons and tax, and a cart holding nothing but free items is exempt. The checkout intent respects it too: below the minimum it opens the cart instead of checking out. The widget enforces this, not the API, so purchase() and checkouts you build outside the widget are not held to it. A value you pass replaces the previous one; pass {} to remove it. No minimum by default.

You can also control where the widget mounts by providing target.

Promotions

When you run a sale across your catalogue, the widget surfaces it on its own — as a banner, a popup, or both, depending on what you asked for when you set the promotion up in Moonbase. Nothing to wire in: the sale appears while it is live and to the shoppers it applies to, and disappears when it ends.

Once a shopper dismisses a promotion it stays dismissed in that browser, for as long as the promotion runs. Editing the campaign doesn't bring it back — a shopper who has turned a sale away isn't asked again. Start a new promotion to reach them again.

The two surfaces are tracked separately, so closing the popup leaves the banner up as a reminder that the sale is still on. A popup also gets a single interaction: clicking through to the campaign retires it for good, the same as closing it would, since it interrupted the shopper to ask. A banner interrupted nobody, so clicking through leaves it standing — only an explicit dismissal takes it down.

By default the banner pins itself to the top of the viewport. Give the widget a container and it renders inline in your own layout instead — anywhere on the page:

<div data-moonbase-promotion></div>
Moonbase.configure({
  promotions: {
    // Set to false to render sales yourself.
    enabled: true,
    // 'top' | 'bottom' — only used when the page offers no container.
    location: 'top',
    dismissLabel: 'Dismiss',
  },
})

Where a sale leads, and the wording on the link, both come from the promotion itself — only the merchant running the campaign knows what it is offering. A sale with no destination set shows the dismiss action as its only button.

To render sales entirely yourself, turn the built-in surfaces off and listen for promotion-shown, or read them from the usePromotions() composable in @moonbase.sh/vue.

Newsletter sign-up forms

A sign-up form on your own page, in your own design, can go straight to Moonbase. Mark it with data-moonbase-form="subscribe" and the widget sends it in the background instead of letting the browser load a new page:

<form data-moonbase-form="subscribe">
  <input name="name" placeholder="Name" maxlength="200">
  <input name="email" type="email" placeholder="[email protected]" required>
  <button>Subscribe</button>
</form>

The widget reads the fields by name:

| Field | | | --- | --- | | email | Required. | | name | Optional, up to 200 characters. | | groups | Optional group IDs, comma-separated in one field or repeated (checkboxes, say). Added to groups.subscribe, as mb_groups is on a link. |

The emails signed up for are the ones communicationPreferences.show names, as for the drawer's own subscribe form, and whether the visitor has to confirm by email first is your account's double opt-in setting. The form is yours, so the consent wording is too: the drawer's form says "By subscribing you agree to receive newsletter and product update emails", and yours should say as much.

Showing the result

By default the drawer opens on the outcome: "You're subscribed!", or "Check your email to finish" under double opt-in. If the server turns the email down, the drawer opens its own form, filled in from yours, with the reason under the button, so the visitor can correct it there.

To keep everything on your page, add data-moonbase-feedback="inline". The drawer then stays closed, and the widget reports progress on the form itself for you to style:

<form class="newsletter" data-moonbase-form="subscribe" data-moonbase-feedback="inline">
  <div class="fields">
    <input name="email" type="email" required>
    <button>Subscribe</button>
  </div>
  <p data-moonbase-message></p>
</form>

| data-moonbase-state | When | | --- | --- | | submitting | The request is out. The form's submit buttons are disabled until it returns. | | subscribed | The visitor is on the list. | | confirmation_sent | Double opt-in: a confirmation email went out, and the visitor is on the list once they click it. | | error | The server turned the email down, or the request failed. |

A [data-moonbase-message] element inside the form gets the outcome as text, worded as the drawer words it, and is made a live region so screen readers announce it. Leave it out to show your own wording instead:

.newsletter:is([data-moonbase-state="subscribed"], [data-moonbase-state="confirmation_sent"]) .fields {
  display: none;
}
.newsletter[data-moonbase-state="error"] [data-moonbase-message] {
  color: #c00;
}

Style both success states. For a visitor who is not signed in, the status follows your double opt-in setting rather than whether the address was new, so the form cannot be used to find out who is subscribed already. With double opt-in on, nearly every sign-up ends in confirmation_sent.

data-moonbase-state is set in the default mode as well, so a spinner can cover the moment before the drawer opens.

Reacting in code

Every successful sign-up emits subscribed, from these forms and from the drawer's own:

Moonbase.on(MoonbaseEvent.Subscribed, ({ email, status, source }) => {
  // source is 'form' for a data-moonbase-form form, 'drawer' for the drawer's own
})

Good to know

  • The attribute hands the form to Moonbase: the widget claims the submit before any other script on the page sees it, so a site builder's own form handling does not post it somewhere else as well. Your own submit listeners on that form do not run either; use the subscribed event instead.
  • Loaded from the CDN, the widget holds a form submitted before it is ready and sends it once setup() has run. Installed from npm, call setup() early: until then the browser submits the form as a plain form.
  • A signed-in customer subscribes the address they type in, which is usually their own.
  • Forms inside a shadow root are not picked up.
  • An unknown data-moonbase-form value is not submitted, and logs a warning to the console.
  • To open the drawer's own form instead, filled in, call Moonbase.subscribe({ email, name }) or link to ?mb_intent=subscribe&mb_email=...&mb_name=....

Events

Subscribe to widget lifecycle events with Moonbase.on(...).

Available events include:

  • signed-in
  • signed-up
  • signed-out
  • subscribed
  • storefront-updated
  • promotion-shown
  • promotion-clicked
  • promotion-dismissed
  • redeemed-voucher
  • joined-group
  • downloaded-product
  • activated-product
  • added-to-cart
  • checkout-initiated
  • checkout-closed
  • checkout-completed

Integrations

The widget can forward its commerce and auth events to common marketing/analytics SDKs without any extra glue code. Enable a provider by passing its config in integrations:

Moonbase.setup('https://demo.moonbase.sh', {
  integrations: {
    metaPixel: { pixelId: '1234567890' },
    googleAnalytics: { measurementId: 'G-XXXXXXX' },
    googleTagManager: { containerId: 'GTM-XXXXXXX' },
    klaviyo: { companyId: 'ABC123' },
    tiktokPixel: { pixelId: 'C12345...' },
  },
})

The widget never loads SDK scripts itself — it only forwards to globals the host page has already initialised (window.fbq, window.gtag, window.dataLayer, window.klaviyo, window.ttq). If a global is missing when an event fires, the forwarding is a silent no-op (with a one-time console.warn so a misconfiguration is discoverable).

You install the SDK; Moonbase forwards to it

You load each SDK with its own official snippet, exactly as you would without Moonbase — Moonbase only needs the same id so it can forward. Load order doesn't matter: if the global appears after setup(), forwarding begins as soon as it's there. For example, Meta Pixel:

<!-- Your existing Meta Pixel snippet -->
<script>
  !function(f,b,e,v,n,t,s){/* …standard fbq loader… */}(window,document,'script',
  'https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', '1234567890');
  fbq('track', 'PageView');
</script>
// Hand Moonbase the same id so it can forward commerce/auth events to fbq:
Moonbase.setup('https://demo.moonbase.sh', {
  integrations: { metaPixel: { pixelId: '1234567890' } },
})

The same pattern applies to GA4 (gtag), GTM (dataLayer), Klaviyo (klaviyo), and TikTok (ttq): install the vendor snippet, then pass the matching id. For Klaviyo, both the modern onsite object (klaviyo.identify() / klaviyo.track()) and the legacy klaviyo.push([...]) queue are supported automatically. The googleTagManager.containerId is informational/reserved — GTM reads from window.dataLayer regardless of which container loaded it, so it isn't used at runtime.

Consent

Forwarding is unconditional: Moonbase forwards to whatever SDK global is present, and relies on that SDK's own consent state (e.g. Google Consent Mode v2, or your CMP gating fbq/ttq). To suppress forwarding entirely until consent is granted, omit the provider from setup() and add it once consent lands:

onConsentGranted(() => Moonbase.configure({ integrations: { metaPixel: { pixelId: '1234567890' } } }))

configure({ integrations }) can add, change, or remove providers at any time. Merge semantics apply per provider: pass a provider to add or update it, pass it as undefined (or null) to remove it, and omit it to leave it unchanged — so granting one provider after consent never disturbs the others.

// Stop forwarding to Meta Pixel, leave every other provider running:
Moonbase.configure({ integrations: { metaPixel: undefined } })

Identity

signed-in / signed-up forward an identify call when an email/id is available (Meta advanced match, GA4 user_id, Klaviyo identify, TikTok identify). On signed-out, Moonbase clears what each SDK supports (GA4 user_id is reset, GTM gets a logout push); Meta, TikTok, and Klaviyo expose no per-user logout, so their profiles persist for the session.

Data conventions

Forwarded order value is the amount the customer pays — total.due, post-discount and tax-inclusive (no separate tax field is sent). Per-item prices are the net (post-discount) unit price. checkout-initiated is the one exception: an order has no settled total until billing details are entered, so its value is the cart's own post-discount total and excludes tax. checkout-completed events carry the order id as a dedup key (Meta eventID, TikTok event_id, GA4/GTM transaction_id) so a completion isn't double-counted.

Event mapping

| Moonbase event | Meta Pixel | GA4 | GTM (dataLayer) | Klaviyo | TikTok Pixel | | -------------------- | ----------------------- | -------------------- | -------------------- | ------------------------ | --------------------- | | added-to-cart | AddToCart | add_to_cart | add_to_cart | Added to Cart | AddToCart | | checkout-initiated | InitiateCheckout | begin_checkout | begin_checkout | Started Checkout | InitiateCheckout | | checkout-completed | Purchase | purchase | purchase | Placed Order | CompletePayment | | signed-in | advanced match | login + user_id | login | identify | identify | | signed-up | CompleteRegistration | sign_up + user_id| sign_up | identify | CompleteRegistration| | signed-out | — | clears user_id | logout | — | — |

For any event not listed (or a provider that isn't built in), use Moonbase.on(...) and forward by hand.