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

emporix

v0.7.0

Published

Unofficial CLI for the Emporix Commerce Engine: schema-as-code, environment cloning and TypeScript generation. Not affiliated with Emporix GmbH.

Readme

emporix

Unofficial. This is a community project and is not affiliated with, endorsed by, or supported by Emporix GmbH. "Emporix" is their trademark; the name is used here only to describe what this tool talks to. For official tooling see the Emporix Terraform provider and the developer portal.

Command-line tooling for the Emporix Commerce Engine: mirror a tenant into git, generate TypeScript from mixin schemas, clone environments, and catch breaking changes before they reach production.

npm install -g emporix
emporix login
emporix pull
emporix types generate

Why this exists

Emporix already ships a Terraform provider that covers configuration-as-code well. This CLI deliberately does not compete with it. It fills the gaps Terraform structurally cannot:

| Gap | What this does | | --- | --- | | No TypeScript types for mixin schemas | emporix types generate emits interfaces, enums and Zod schemas | | Every entity write must repeat the versioned schema URL in metadata.mixins | the generated withMixin() helper injects it | | terraform plan shows what changed, not whether it breaks stored data | push classifies each schema change and refuses breaking ones | | No way to mirror a live tenant | emporix clone --from prod --to staging | | Entities silently pin outdated schema versions | emporix doctor finds the drift, emporix repair fixes it |

Commands

Authentication

Tenant creation itself is Developer-Portal-only. Create the tenant and an API key there, then:

emporix login                      # interactive
emporix login --profile staging --tenant winestg \
  --client-id … --client-secret …  # scripted

emporix profiles                   # list them, * marks the active one
emporix use staging
emporix whoami                     # tenant, credential source, granted scopes

Credentials live in ~/.emporix/config.json (mode 0600), in plain text — the same trade-off gh and npm make. In CI, set EMPORIX_TENANT, EMPORIX_CLIENT_ID and EMPORIX_CLIENT_SECRET instead: they take priority and never touch disk. Setting only some of them is an error rather than a silent fallback to a local profile, so a misconfigured job cannot write to the wrong tenant.

Configuration as code

emporix pull                       # mirror the tenant into ./emporix
emporix validate                   # lint the directory, no API call
emporix diff                       # how does the directory differ from the tenant?
emporix push --dry-run             # what would change
emporix push                       # apply, after confirmation
emporix push --prune               # also delete what's not in the directory

pull writes one JSON file per item, with server-managed fields (version, createdAt, metadata.url) stripped and keys sorted — so a re-pull produces no git diff, and a diff means the tenant actually changed.

Covered resources: currency, country, schema, custom-entity, custom-entity-instance, site, tax, payment-mode, shipping-zone, shipping-method, delivery-time, tenant-configuration, webhook. Filter with --include / --exclude.

Nested collections are expanded across their parents and written to matching directories, so a clone reproduces them completely:

emporix/shipping-methods/main/eu/dhl.json      # site → zone → method
emporix/custom-entity-instances/supplier/acme.json
``` Services a tenant hasn't
enabled return 403 and are skipped with a warning rather than failing the run.

**Secured tenant configurations are never pulled.** Values flagged `secured`
hold credentials, so writing them to disk would commit secrets to the repository
and pushing them would overwrite the target tenant's own. They are reported and
left in place.

**Reads are checked for completeness.** Emporix offset pagination has no stable
ordering, so pages overlap and skip — reading 249 countries can yield anywhere
between 188 and 249 distinct rows from one run to the next. A short read written
to disk would then be deleted from the target by `push --prune`. The CLI
de-duplicates by natural key, asks the service for `X-Total-Count`, re-reads when
it comes up short, and fails rather than returning an incomplete collection.

### Checking before you push

`validate` lints the local directory without credentials and without a single
API call — unknown attribute types, duplicate keys, `ENUM` without values,
nested items whose parent is missing. These are the mistakes the platform would
otherwise report as a 400 halfway through a push, after part of it landed.

```bash
emporix validate
✗ schema/broken — a: unknown type NOPE (expected TEXT, NUMBER, …)
✗ schema/broken — a: duplicate attribute key
✗ shipping-method/ghost/nowhere/x — references site `ghost`, which is not in this directory

diff is the read-only counterpart, and answers a different question from push --dry-run: not "what would I write" but "are these two the same".

emporix diff                              # local directory vs the active tenant
emporix diff --from prod --to staging     # one tenant against another
emporix diff --exit-code                  # exit 1 on any difference
Comparing emporix → winestore

schema
  ≠ differs: productCustom (Product Custom Fields)
        attributes

0 only in emporix, 1 differing, 0 only in winestore, 7 identical
⚠ 1 of the differences would be a breaking schema change if applied to winestore.

With --exit-code it makes a good nightly job: fail when staging has drifted away from what the repository says it should be.

Breaking-change detection

push and clone compare each schema against the target and classify the difference. Removing an attribute, narrowing a type, adding a required field, dropping an enum value, or turning a plain field localized all invalidate data that already exists — so they abort the run:

⚠ Breaking schema changes detected
  These invalidate data that already exists in the target tenant.

  productCustom
    ! vintage — attribute removed — values on existing entities are dropped
    ! sweetness — enum value(s) removed: off-dry — entities holding them fail validation

✗ Refusing to push 2 breaking schema change(s).
  Review the list above. Pass --allow-breaking once you have a migration plan.

Additive changes (new optional attributes, new enum values, renamed labels) pass without ceremony. When a change really is breaking, emporix migrate is the way through it rather than --allow-breaking.

Type generation

emporix types generate                    # → src/emporix.gen.ts
emporix types generate --zod              # also emit Zod schemas
emporix types generate --offline          # from ./emporix, no credentials needed
emporix types generate --check            # non-zero exit if stale (for CI)

The output is one self-contained file — no runtime dependency on this package. Nested objects and array elements get their own exported interfaces, so they can be imported and used on their own:

export interface ProductCustomOrigin {
  country: string
  region?: string
}

export interface ProductCustom {
  /** Manufacturer */
  manufacturer: string
  /** Vintage */
  vintage?: number | null
  /** Sweetness */
  sweetness?: "dry" | "off-dry" | "sweet"
  /** Tasting notes */
  tastingNotes?: LocalizedValue<string>
  /** Origin */
  origin?: ProductCustomOrigin
}

export const MIXIN_URLS = {
  "productCustom": "https://res.cloudinary.com/…/productCustom_v3.json",
} as const

Plus the helper that removes the single most error-prone part of the API:

import { withMixin } from "./emporix.gen.js"

await createProduct({
  name: "Château X",
  ...withMixin("productCustom", { manufacturer: "Vino", vintage: 2019 }),
})

withMixin writes both mixins.productCustom and metadata.mixins.productCustom with the current versioned schema URL. Miss either half and the platform rejects the write; the URL changes every time the schema is updated, which is why it is generated rather than hand-written.

Cloning an environment

emporix clone --from prod --to staging --dry-run
emporix clone --from prod --to staging --include schema,site,tax

Reads directly from the source tenant and writes to the target — no local checkout needed. Resources are applied in dependency order (sites before the shipping zones nested under them). The same breaking-change guard applies.

Diagnostics

emporix doctor
  • Scope gaps — which resources this API key cannot write, and the exact scope to add.
  • Mixin version drift — entities pinned to _v2 while the schema is at _v3. Updating a schema bumps the URL but does not rewrite entities, so a tenant silently ends up validating different rows against different versions.
  • Orphaned references — entities pointing at schemas that no longer exist.
⚠ 1/3 sampled product entities pin an outdated `productCustom` schema
  pinned: _v2 · current: _v3
  → Run `emporix repair` to repoint them at the current schema version.

✗ 1 product entities reference schema `legacyAttrs`, which no longer exists

API versions

Several Emporix services select their API generation with an X-Version header rather than a path segment, and the response shape depends on it — a product read without X-Version: v2 comes back without classificationMixins. The CLI sends v2 for product, category, country and customer-segment.

Emporix has been dropping the requirement service by service, and states that clients which keep sending the header continue to work, so sending it is the safe default. Override per service when your tenant differs — null suppresses the header entirely:

{ "apiVersions": { "product": "v2", "country": null } }

emporix doctor lists what is in effect, because the header is otherwise invisible.

Entity collections

migrate, repair and doctor walk these collections: PRODUCT, CATEGORY, CUSTOMER, SITE, ORDER, QUOTE, RETURN, COUPON, PRICE_LIST, MEDIA, VENDOR, COMPANY, LOCATION.

Five more exist as schema types but cannot be walked. The CLI names the reason rather than reporting them as unknown:

| Collection | Why not | | --- | --- | | CART | cannot be listed without customerId and sessionId | | AVAILABILITY | cannot be listed without site | | CART_ITEM, ORDER_ENTRY, CUSTOMER_ADDRESS | no collection of their own — migrate the parent document instead |

If your tenant exposes one the CLI does not know, or on a different path, add it to emporix.json instead of waiting for a release:

{
  "entityEndpoints": {
    "LOCATION": { "list": "/vendor/{tenant}/locations", "item": "/vendor/{tenant}/locations/{id}" }
  }
}

Migrating data

A breaking schema change needs the stored values transformed before the schema can move. A migration is a plain JavaScript file — Node runs it directly, so there is no build step:

emporix migrate generate --schema productCustom --field vintage
emporix migrate run migrations/2026-08-23-productCustom-vintage.mjs
emporix migrate run migrations/2026-08-23-productCustom-vintage.mjs --apply
export default {
  entityType: "PRODUCT",
  schema: "productCustom",
  migrate(values, entity) {
    if (typeof values.vintage !== "number") return null   // null = leave alone
    return { ...values, vintage: String(values.vintage) }
  },
}

Scans up to --limit entities (default 1000) rather than an unbounded catalogue. Plans by default and prints a before/after for each entity. migrate also repoints each entity at the current schema URL in the same write, so a run does not leave behind the drift doctor would flag. Re-running is safe: a transform that has already been applied returns an unchanged value and is counted as unaffected.

Repairing drift

emporix repair                     # plan only — nothing is written
emporix repair --apply             # PATCH metadata.mixins to the current URLs

repair scans one page per entity type (--sample). It only touches metadata.mixins, and only for schemas that still exist — orphaned references are reported by doctor but never rewritten, because there is no correct URL to invent. It is idempotent: a second run finds nothing to do.

Scripting

--json replaces the human output with exactly one JSON document on stdout — including on failure, so a wrapper never has to tell the two apart:

emporix --json diff | jq -e '.inSync'
emporix --json pull | jq '.resources[] | select(.changed > 0)'
emporix --json doctor | jq '.findings[] | select(.level == "error")'
{ "ok": false, "error": "Unknown resource(s): widgets", "hint": "Available: currency, …" }

CI

- run: npx emporix validate                 # no credentials needed
- run: npx emporix types generate --check   # fails if committed types are stale
- run: npx emporix push --dry-run           # fails the PR on breaking changes
  env:
    EMPORIX_TENANT: ${{ vars.EMPORIX_TENANT }}
    EMPORIX_CLIENT_ID: ${{ secrets.EMPORIX_CLIENT_ID }}
    EMPORIX_CLIENT_SECRET: ${{ secrets.EMPORIX_CLIENT_SECRET }}

push and clone refuse to run non-interactively without --yes.

Project layout

emporix.json          # dir + codegen defaults, written by `emporix init`
emporix/              # pulled tenant configuration, one JSON file per item
  schemas/
  sites/
  shipping-zones/main/
src/emporix.gen.ts    # generated, gitignored

Development

npm install
npm run build
npm test          # 99 tests, incl. end-to-end runs against a mock API
npm run typecheck

The end-to-end suite drives the built binary against a local stand-in for the Emporix API, and the codegen tests compile their own output under strict TypeScript — so a generated file that would not type-check fails the build.

Known limits

Validated read-only against a live tenant: endpoint paths, scope names, pagination, X-Version, secured-value withholding, and round-trip fidelity — after a pull, push --dry-run reports nothing to do.

The PATCH writes used by migrate and repair are still unverified, because that validation never wrote anything. Both commands plan by default and need an explicit --apply; use it on a test tenant first.

License

MIT. Unofficial — not affiliated with Emporix GmbH; see the note at the top.