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

@zenky/storefront-vue

v0.10.10

Published

Zenky Storefront SDK for Vue 3.

Readme

@zenky/storefront-vue

Headless storefront engine for Zenky — Vue 3 + Pinia, no framework lock-in.

CI npm

One engine behind many storefronts. The cart, order building, checkout, catalog resolution, delivery slots, SEO/JSON-LD, theme, and store/profile state all live here as framework-agnostic composables and Pinia stores. The consumer — a Nuxt site, a QR-code menu, a new template — brings the framework glue (cookies, config, toasts, HTTP transport, useHead) and draws its own UI on top.

The design is Ports & Adapters: the core declares small ports for everything it needs from the outside world, the consumer provides adapters once, and every composable reads them through useRuntime(). The core never imports useCookie, useRoute, useHead, $fetch, or anything Nuxt-specific.

Installation

npm install @zenky/storefront-vue

Peer dependencies (the consumer already has these):

{
  "vue": "^3.5.0",
  "pinia": "^3.0.0",
  "@zenky/storefront-api": "^1.0.1",
  "@zenky/integrations-sdk": ">=0.1.3"
}

Quick Start

Assemble the runtime once at your composition root and install it as a Vue plugin. After that, core composables just work.

import { createStorefront } from '@zenky/storefront-vue'

app.use(
  createStorefront({
    sdk,                                    // ZenkyStorefront instance (one per app)
    config: { baseUrl, baseAuthUrl },
    storage: {                              // where order id/token, settings, etc. persist
      get: (key) => readCookie(key),
      set: (key, value, opts) => writeCookie(key, value, opts),
      remove: (key, opts) => deleteCookie(key, opts),
    },
    notifier: {                             // user-facing messages
      error: (m) => toast.error(m),
      success: (m) => toast.success(m),
      warning: (m) => toast.warning(m),
      message: (title, opts) => toast(title, opts),
    },
    messages: {                             // consumer-owned localization
      format: (key, params) => i18n.t(key, params),
    },
    activity: {                             // busy indicators for long operations
      begin: (kind) => busy.begin(kind),
      end: (kind) => busy.end(kind),
    },
  }),
)

Then, anywhere in a component or composable:

import { useStorefrontCart } from '@zenky/storefront-vue/cart'

const cart = useStorefrontCart()

await cart.initializeOrder()
await cart.addToCart({ productId, productVariantId, quantity: 1 })

cart.itemCount.value // reactive
cart.totalPrice.value

Call composables and stores synchronously in setup

Composables read the runtime through inject(), which works while a component instance is active or while the owning Vue app is executing app.runWithContext(). The important cases behave differently:

  • Top-level await in <script setup> is safe. The SFC compiler wraps it in withAsyncContext(), which restores the instance afterwards — so composables called after await bootstrap() at the top level still resolve correctly.
  • A lifecycle hook has context only in its synchronous prefix. After its first await, the component instance is no longer active.
  • Event handlers, watch callbacks, timers, and .then() callbacks do not inherit component injection context. Resolve dependencies before registering the callback, or deliberately re-enter the owning app with app.runWithContext().
  • A hand-written async setup() is not compiler-wrapped. Resolve dependencies before its first await.

Resolve the composable up front and call the returned functions later.

// ✅ resolve in setup, call after the await
const { dispatchCheck } = usePromotionsChecker()

onMounted(async () => {
  await initializeOrder()
  dispatchCheck()
})

// ❌ resolving after an await — inject() has no instance to read from
onMounted(async () => {
  await initializeOrder()
  const { dispatchCheck } = usePromotionsChecker()
})

The same applies inside event handlers, watch callbacks, timers, and .then() blocks: resolve in setup(), keep the reference, call it from the callback. Watch for this inside try blocks in particular — the catch swallows the failure and reports it as whatever the surrounding operation was supposed to do, so the real cause never surfaces.

Stores fail differently, and worse. The Pinia stores exported from @zenky/storefront-vue/stores read the runtime too, but a missed context does not throw: Pinia falls back to its module-level active instance. Under concurrent SSR that instance belongs to whichever request touched it last, so the lookup succeeds with another user's state instead of failing.

Two cases behave differently:

  • Inside a store action — safe. Pinia activates the action's own instance for the duration of the call, so resolving another store there returns the right one.
  • In a plain function after an await, outside any action and outside runWithContext() — leaks. This is the shape to watch for: it is silent, and it crosses requests.

Resolve core stores in setup() for the same reason you resolve composables there. A silent leak is far harder to notice than an exception.

Runtime Ports

createStorefront(runtime) takes one object. Five ports are required; the rest are optional — omit an optional port and the feature that needs it is simply skipped.

| Port | Required | What the consumer provides | | --- | --- | --- | | sdk | yes | A ZenkyStorefront instance (@zenky/storefront-api). | | config | no | Reserved consumer configuration; the current core does not read it. | | storage | yes | Key/value persistence — cookies, localStorage, memory. | | notifier | yes | Toasts: error / success / warning / message. | | messages | yes | Maps stable domain message keys to localized consumer copy. | | activity | yes | begin(kind) / end(kind) for busy state. | | events | no | Lazy { get() } provider for ZenkyIntegrationsSDK; resolve on every event. | | realtime | no | Lazy { get() } provider for the WebSocket adapter (subscribe / unsubscribe / isReady). | | catalogReloader | no | Reloads the catalog when the stock changes. | | rum | no | Performance marks (mark / measure). | | commerceEvents | no | Commerce-event tracking (trackAddedToCart, getIdentity, …). |

Types for every port live in the ports subpath, alongside no-op defaults and an in-memory storage helper for SSR/tests:

import {
  type StorefrontRuntime,
  type StorageAdapter,
  KEY_MESSAGE_FORMATTER,
  NOOP_NOTIFIER,
  NOOP_ACTIVITY_REPORTER,
  createMemoryStorage,
} from '@zenky/storefront-vue/ports'

The full public contract, adapter examples, SSR lifecycle, and security boundary are in Runtime Ports. For a step-by-step consumer integration and upgrade checklist, see Consumer Setup. For a map of what lives in each domain folder and how to decide whether new logic belongs here or in the consumer app, see Architecture.

Subpath Imports

Each public domain is its own entry point, so bundlers only pull in what you import.

import { minorToMajor } from '@zenky/storefront-vue/money'
import { buildCssVariableMap } from '@zenky/storefront-vue/theme'
import { getRemoteCatalog, findCategoryByShortId } from '@zenky/storefront-vue/catalog'
import { useStorefrontCart, useCheckoutSubmit } from '@zenky/storefront-vue/cart'
import { generateFreeTimeGroups } from '@zenky/storefront-vue/order'
import { buildProductSchema, buildOgTags } from '@zenky/storefront-vue/seo'
import { usePaginatedList } from '@zenky/storefront-vue/pagination'

Available subpaths: ports, money, utils, theme, stores, products, catalog, cart, order, promotions, seo, pagination, snippets, website-settings.

Routing, tenant theme presets, localized copy, header/footer composition, and conversion to framework-specific head records belong to the consumer. Core SEO builders return plain records; a Nuxt adapter may add Unhead-specific fields such as tagPriority.

Runtime Compatibility

  • ESM-only, targeting ES2022.
  • Vue 3.5+ and Pinia 3.
  • Node.js 20.19+ for SSR and package tooling; Node.js 22 is recommended for production and development.
  • Modern browsers and edge runtimes with standard Web Platform APIs.

CI imports every public entry in Node SSR and scans the emitted ESM for CommonJS, Node built-ins, Buffer, and process. Browser-only behavior is guarded; for example, online-payment retry accepts an explicit currentUrl when no window is available.

For SSR, create a new Vue app, Pinia instance, storefront runtime, and storage adapter per request. Never share createMemoryStorage() between users.

Package Size

Every domain has a tree-shakeable subpath. CI measures the transitive gzip graph for the root, cart, and utils entries plus all emitted JavaScript. The budgets are regression ceilings, not size targets; increasing one requires review and a release-note explanation.

npm run build
npm run check:size

Pinia Stores

The stores subpath exposes setup-style Pinia stores (ids namespaced zenky-*): cart, order settings, store profile, catalog, products, auth, promotions. They are plain state containers — no API calls, no UI logic — and they are the reactive backbone the composables read from. Install Pinia before mounting the storefront plugin.

import { useCartStore, useStoreProfileStore } from '@zenky/storefront-vue/stores'

Money

Prices are stored and computed in minor units (kopecks) everywhere. Convert only at the API boundary.

import { minorToMajor, minorToMajorString, majorToMinor } from '@zenky/storefront-vue/money'

minorToMajor(65000, 'RUB')       // 650    — numbers for bill/amount fields
minorToMajorString(65000, 'RUB') // "650.00" — strings where the API expects them
majorToMinor(650, 'RUB')         // 65000  — user input back into the store

Development

npm run dev            # tsup --watch
npm run build          # ESM + .d.ts to dist/
npm run lint           # Biome lint
npm run format         # Biome format --write
npm run typecheck      # tsc --noEmit
npm test               # vitest
npm run test:coverage  # vitest + V8 coverage report
npm run check          # format + lint + types + tests + build + exports
npm run check:exports  # publint + are-the-types-wrong (esm-only profile)
npm run check:boundaries # reject Nuxt/Unhead and consumer policy in src/
npm run check:context  # reject ambient composables/stores in deferred helpers
npm run check:runtime  # import all entries in SSR + reject Node-only output
npm run check:size     # enforce gzip bundle budgets

Package Scripts

  • build — bundle the multi-entry ESM package with tsup.
  • dev — rebuild on change.
  • test / test:watch — run the Vitest suite.
  • typecheck — type-only compile.
  • check:exports — validate the exports map and published types.
  • check:boundaries — reject Nuxt/Unhead APIs, routes, and tenant presets in core source.
  • check:context — reject deferred composable/store resolution outside composition setup.
  • check:runtime — verify Node SSR imports and browser/edge-safe ESM output.
  • check:size — enforce root/domain/all-output gzip budgets.
  • check:ci — quality gate with the V8 coverage ratchet.
  • prepublishOnly — run the release quality gate.

Contributing and Releases

Read CONTRIBUTING.md before opening a pull request. User-visible changes belong in CHANGELOG.md; maintainer steps are documented in the release process.

Security vulnerabilities must be reported privately according to SECURITY.md, never through a public issue.

License

MIT