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

@misiki/gocommerce-connector

v0.13.0

Published

API connector for GoCommerce with Svelte Commerce

Readme

@misiki/gocommerce-connector

Svelte Commerce on a GoCommerce engine.

The storefront talks to every backend through a connector: a package that maps one platform's API onto a shared surface of some forty services (product, cart, checkout, order, user, address, …). This is GoCommerce's. It covers the engine's public API and the storefront modules it ships — accounts, reviews, wishlists, content pages, menus, FAQ, contact and search — and says plainly where the two do not meet.

Install

bun remove @misiki/litekart-connector @misiki/vendure-connector
bun add @misiki/gocommerce-connector

The storefront runs on whichever @misiki/*-connector is installed. If you keep another one installed, set PUBLIC_CONNECTOR=@misiki/gocommerce-connector.

# .env
PUBLIC_GOCOMMERCE_API_URL=https://api.shop.example    # the engine
PUBLIC_GOCOMMERCE_PROXY_PATH=/gocommerce              # see "The browser and CORS"
# PUBLIC_GOCOMMERCE_CHANNEL=web                       # a sales channel, if the store has several
# PUBLIC_GOCOMMERCE_STORE=acme                        # under `gocommerce platform`, on a shared API host
# PUBLIC_GOCOMMERCE_BASE_DOMAIN=shopnx.in             # or every store: acme.shopnx.in is store `acme`
# PUBLIC_GOCOMMERCE_LANGUAGE=fr                       # ask for this language (?lang=) on every read

Leave PUBLIC_LITEKART_API_URL unset. The storefront rewrites every server-side request whose path starts with /api to that host, and the engine's routes start with /api too.

The engine

Start it with the modules the storefront has screens for. The flags go before serve; the engine ignores any that come after it.

gocommerce -identity -wishlist -reviews -cms -faq -menus serve

Each is optional. Without -identity there is no sign-in (guest checkout always works); without -reviews products show no stars; and so on. Add -meilisearch (with MEILI_HOST and keys) for price sorting and price filters, -newsletter and -contact for the signup box and contact form, and -b2b for business buyers (see "Beyond the storefront's screens").

The browser and CORS

The engine sends CORS headers only to the origins listed in GOCOMMERCE_CORS_ORIGINS (comma-separated, or * for any). Unset, a browser on the storefront's origin cannot read a response from the engine's. Server-side rendering is unaffected; it is the bag, sign-in and checkout — which run in the browser — that need a route.

The simplest is to list the storefront's origin when starting the engine, GOCOMMERCE_CORS_ORIGINS=https://shop.example, and leave PUBLIC_GOCOMMERCE_PROXY_PATH unset. The browser then calls PUBLIC_GOCOMMERCE_API_URL directly, so that has to be an address the shopper's browser can reach.

Or proxy a path on the storefront's own origin to the engine and name it in PUBLIC_GOCOMMERCE_PROXY_PATH. The browser then calls https://shop.example/gocommerce/api/carts, and the server keeps calling PUBLIC_GOCOMMERCE_API_URL directly. With Caddy:

shop.example {
	handle_path /gocommerce/* {
		reverse_proxy engine:8080
	}
	reverse_proxy storefront:3000
}

or nginx:

location /gocommerce/ { proxy_pass http://engine:8080/; }
location /            { proxy_pass http://storefront:3000; }

Serving the storefront and the engine from one origin works too; then leave the proxy path unset and point the API URL at that origin.

One storefront for every store

Under gocommerce platform a storefront can sell for one store, named in PUBLIC_GOCOMMERCE_STORE, or for all of them. Set PUBLIC_GOCOMMERCE_BASE_DOMAIN to the domain the engine hands out subdomains of and point every <slug>.<base domain> at the one deployment: a request for acme.shopnx.in is sent to the engine with X-Store: acme, by the same rule the engine applies to its own host (one label, case-insensitive, a port ignored). A host outside the base domain falls back to PUBLIC_GOCOMMERCE_STORE; without one it is sent as it is, X-Store: chillish.in, which the engine answers when a store has added and verified that domain. Point a store's own domain at this deployment and it shows that store.

The browser knows its own host. The server does not, so tell the connector where to read it, once, from src/hooks.server.ts:

import { getRequestEvent } from '$app/server'
import { setRequestHost } from '@misiki/gocommerce-connector'

setRequestHost(() => getRequestEvent().url.hostname)

Each store renders with the name, email and phone its owner set in the panel (GET /api/store); the static record's values stand only where the owner left one empty. Store-level facts (name, plugins, currency, menus) are cached per store, so two stores on one process never see each other's. A host under the base domain that names a store the engine does not have makes getStoreByIdOrDomain throw a GoCommerceError with status 404 and "store not found" in its message, which Svelte Commerce renders as its own "Store not found" page. An engine that is down stays best-effort: the storefront renders from its static record.

What maps to what

| Storefront | Engine | | --- | --- | | product page, listings, search | /api/products, /api/products/slug/{slug} (an id or SKU also works) | | category pages and tree | /api/categories, /api/categories/{slug} — a category sells its descendants | | collections | /api/collections, /api/collections/{slug} | | featured / trending shelves | the collection with slug featured / trending; newest products when there is none | | product page extras | on the product from /api/products/slug/{slug} (D134): highlights and freeGift from the product's own metadata, soldAtLeast when the shop shows sold counts (null otherwise, never zero), and the product's videos after its pictures in images (and alone in videos), so the gallery plays them | | bag | /api/carts…, /api/carts/{token}/discount | | Complete the set (add-ons) | /api/add-ons?product_id= — productService.listAddOns({ product }), each at the price ticking it adds (price) beside its own (regularPrice); cartService.addAddOn({ addOnId }) after the product is in the bag. The engine keeps the offer price only while the product is in the bag, one for each | | You may also like | /api/cross-sells?product_id= — productService.listCrossSells({ product }), curated only; the bag's /api/carts/{token}/cross-sells — cartService.listCrossSells(), which includes the collection the shop chose for every basket (D134), falling back to the first item's related products | | the bag's offers and free delivery | /api/carts/{token}/offers — cartService.getOffers() and the coupon drawer's couponService.listCoupons(): only the codes the shop chose to show; /api/carts/{token}/free-shipping — cartService.getFreeShipping(), from the store's own rates, null when it cannot say | | delivery options | /api/checkout/rates | | payment methods | /api/checkout — cash on delivery, Razorpay, and the first hosted-page gateway (see below) | | place order | POST /api/checkout/{code} with an Idempotency-Key | | order lookup | /api/orders/{number}?token= (guest), /x/identity/me/orders (account) | | sign-in, sign-up, profile, password reset | /x/identity/* | | address book | /x/identity/me/addresses, with its label (Home, Work, Other) | | PIN code autofill | /api/postcodes/IN/{pin} — addressService.lookupPostcode({ country, postcode }): city and state, or null when the engine has no directory or the PIN is unknown (D134) | | wishlist | /x/wishlist | | reviews | /x/reviews | | content pages (/p/…, legal pages) | /x/cms/pages | | FAQ | /x/faq | | composed home page | /x/layouts/home (?preview= for a draft) — layoutService.getHomeLayout(), null when the store has none; carries themeSettings and each collection list's collections | | product and collection templates | /x/layouts/product, /x/layouts/collection (?preview= for a draft) — layoutService.getProductLayout() and getCollectionLayout(), the sections the shop arranged around the page's core (main-product, main-collection); null when none is published, and the page draws as before | | header and footer | /x/layouts/header, /x/layouts/footer (?preview= for a draft) — layoutService.getHeader() and getFooter(): the announcement strip and header menu, the footer's columns menu, words, newsletter switch, social links and copyright line, with the named menu read live from /x/navigation/menus/{handle}; null when none is published or on any failure, and the storefront keeps its own. The store's header and footer menus (menuService.engineMenus(), the store record's menu) are the ones those name | | theme settings | /x/layouts/theme (?preview= for a draft) — layoutService.getThemeSettings(), the store's colours, fonts and rounding for every page; null when there are none | | menus | /x/navigation/menus/{header,footer}, laid over store.menu; any menu by handle | | product specs table | the product's category attribute answers | | attribute filters (?attributes.color=Red) | /api/products?attr=color:Red | | newsletter box | /x/newsletter/subscribe | | contact form, product enquiries | /x/contact/messages | | price sort and filter, type-ahead | /x/meilisearch/search, falling back to the engine's own search | | store record | the storefront's own config, with the engine's currency, menus and module switches over it |

Blogs, banners, reels, chat, warranties, deals and uploads have no engine equivalent. Their lists answer empty and anything that would write or open one record throws NotSupportedError, naming the service and the method — a silent success would tell a shopper their message was sent when nothing left the browser.

Beyond the storefront's screens

Every shopper-facing route the engine serves is reachable through this package — CI fails when the engine gains one that is not (scripts/route-coverage.mjs). The routes Svelte Commerce has no screen for are reached through services named for what they do. These answer the engine's own shapes — numeric ids, money as { amount_minor, currency } — where the family's services answer Litekart's.

| Service | Covers | | --- | --- | | b2bService (ext/b2b) | the company, roles and credit (me); members and roles; invitations; a quick order by SKU (addLines) and a repeat order (reorder); checkout on account with a PO number and partial lines, or an approval request; approve, reject, withdraw; quotes requested, read, accepted, declined; the public dealer form (submitLead) and a dealer's leads | | checkoutService.checkout(code) | any payment method the engine has switched on, answering a client secret, a redirect URL or nothing to do; listMethods() names them all | | variantService | a product's variants, one variant, and the marketplace's offers on it | | newsletterService | subscribe and unsubscribe | | userService | requestEmailVerification(), refreshSession(), and claimOrder({ number }) — files a guest order under the account, explicitly: never at sign-in, where a shared computer would file one person's orders under the next | | menuService | listMenus() and getMenu(handle) beyond the header and footer | | wishlistService | setEmail() for back-in-stock mail |

The admin API is deliberately not here. It takes an operator's token, and a storefront package runs in every shopper's browser.

import { b2bService } from '@misiki/gocommerce-connector'

const { role, credit } = await b2bService.me()
const { cart, rejected } = await b2bService.addLines({ lines: [{ sku: 'TEE-M', quantity: 24 }] })
const placed = await b2bService.checkout({ poNumber: 'PO-1042' })
if (placed.kind === 'approval') console.log('waiting for an approver', placed.approval.id)

Two things this package is careful about

Money. The engine speaks minor units and a currency code and never a formatted string. The storefront wants price: 19.99. The conversion happens in money.ts and nowhere else, and it is per currency: the yen has no decimals and the dinar has three, so ¥2,500 stays 2500 rather than becoming 25.

Nothing is invented. There is no popularity score, so "most popular" sorts nothing rather than sorting by a made-up number. No compare-at price means mrp equals the price, not 0 — which would render as a 100% discount. Counts and page totals are the engine's own.

Where the two do not meet yet

Each of these is a real gap, listed so nobody has to find it in production.

  • The order-success page shows no details. The storefront looks the order up on the server with only the number and cart id from the URL. A guest's order is readable only with the access token checkout returned, which lives in the browser that placed it, so the server answers "not available yet". The bag still empties, and the order is readable from that browser and, for a signed-in shopper, under My orders. Using the cart token in the URL as a credential would have fixed the page and made that URL — which lands in history and referrers — a key to the shopper's address.
  • A missing product is a 503, not a 404. The storefront decides by the status of a request to exactly /api/products/<slug>; the engine's slug route is /api/products/slug/<slug>, and /api/products/<slug> answers 400 for a non-numeric id.
  • Payment. The storefront drives a fixed list of gateway flows and passes none of them the gateway the shopper chose. Cash on delivery and Razorpay's modal match engine gateways directly. The storefront's redirect flow (which it reaches by the code PAYPAL) carries the store's first hosted-page gateway — Adyen, Paddle, Lemon Squeezy, Creem, Hyperswitch or RevenueCat — under that gateway's own name and a neutral card icon; a second one cannot be told apart from the first and is not offered. GoCommerce's Stripe is a Payment Element (a client secret), which this storefront does not mount. Every method left off is reported once in the console, and every one is reachable through checkoutService.checkout(code) for a storefront that drives its own payment UI. Payment is confirmed by the gateway's webhook to the engine; the order shows paymentStatus once it lands.
  • Filters on an older engine. Listing filters and their counts (tags, vendor, product type, category attributes, price) need an engine that counts them (GET /api/products?facets=1, D130). Against an older one the filter rail is empty and a category page's attribute filters are not applied.
  • Tax in the bag. The engine's cart carries no tax or total; tax is computed when the order is placed. The bag shows subtotal − discount + delivery, and the order shows the real figures.
  • Phone sign-in by code, account deletion, guest order lookup by email and code are not engine features and throw.

State kept in the browser

The engine holds a cart's lines and email; the address, phone, delivery choice and coupon are sent with the checkout request, so they are kept in the shopper's localStorage until then. So is the account session token (the engine's identity module is bearer-only), each order's access token, and the wishlist token. Nothing about a shopper is ever held in module scope: the storefront renders some services from a server singleton shared by every request.

Development

npm install
npm run typecheck
npm test                 # builds, then runs both suites

test/unit.test.mjs covers the translation with no engine. The live suites, test/live.test.mjs and test/live-more.test.mjs, run the connector against a real one, seeding what they need through the admin API under names unique to the run:

gocommerce -admin-token dev-token -identity -wishlist -reviews -cms -faq -menus -newsletter -contact -b2b serve
GOCOMMERCE_URL=http://127.0.0.1:8080 GOCOMMERCE_ADMIN_TOKEN=dev-token \
GOCOMMERCE_TEST_DB=postgres://…/the-stores-database npm test

GOCOMMERCE_TEST_DB lets the b2b suite mark its test accounts' emails confirmed — the engine confirms one only from a link it mails — and without it those tests skip. Use a throwaway store. The suites skip, loudly, when there is no engine. CI runs them against GoCommerce's main on every push and nightly, along with the route-coverage check.

Releasing

Bump version in package.json and push it to main. The Publish workflow sees a version npm does not have yet, builds, tests, publishes it with provenance, and creates the matching v<version> tag and GitHub release. It needs an NPM_TOKEN secret in the repository's npm-publish environment, able to publish under @misiki.

License

MIT