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

@natanvotre/builder-shopify-plugin

v0.1.1

Published

Multi-store Shopify plugin for Builder.io — search products and collections across regional storefronts (US/UK/EU) in a single picker

Downloads

207

Readme

@natanvotre/builder-shopify-plugin

A Builder.io commerce plugin that connects multiple Shopify storefronts as a single resource source. Search products and collections across regional storefronts (e.g. US/UK/EU) in one picker, with availability pills showing which stores carry each item.

Status: Internal FormFactory reference implementation. Currently configured as the production plugin for e.l.f. Cosmetics (US/CA/IN, UK, EU). Fork and adapt for new clients — see Adapting for a new client.


Why this exists

Builder.io ships a first-party Shopify Commerce Plugin. It works well — for a single store.

Brands with regional Shopify storefronts hit a wall:

| Problem with the default plugin | Impact | | ---------------------------------------------- | ------------------------------------------------------------------------ | | One Shopify domain + token per Builder space | Editors can only reference resources from one store | | Resource IDs are bound to a specific store | A "Product" model in Builder can't represent the same SKU across regions | | No visibility into where a SKU actually exists | Editors guess; broken links ship to regions where the product is missing |

For a multi-region brand, this either forces one Builder space per region (triple the content management overhead) or per-region Builder content models (every page template duplicated by region). Neither scales.

What this plugin does instead

  • Connects to all configured stores at once (US, UK, EU) via three Storefront API tokens
  • Searches in parallel across stores and merges by handle — the same product across regions appears as a single result with store pills (🇺🇸 🇬🇧 🇪🇺) showing availability
  • Stores the product handle as the canonical reference — the storefront resolves the right Shopify store at render time based on the request locale

This means one Builder content model, one editor experience, and clean multi-region rendering downstream.


How it works

                    ┌─────────────────────────────┐
                    │    Builder.io Editor        │
                    │  (Resource Picker UI)       │
                    └────────────┬────────────────┘
                                 │ search("brow power")
                                 ▼
                    ┌─────────────────────────────┐
                    │   ElfResourcePicker         │
                    │   (debounced search)        │
                    └────────────┬────────────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              ▼                  ▼                  ▼
        ┌──────────┐       ┌──────────┐       ┌──────────┐
        │ US Client│       │ UK Client│       │ EU Client│
        └────┬─────┘       └────┬─────┘       └────┬─────┘
             │                  │                  │
             ▼                  ▼                  ▼
        ┌──────────┐       ┌──────────┐       ┌──────────┐
        │ Shopify  │       │ Shopify  │       │ Shopify  │
        │  US/CA   │       │   UK     │       │   EU     │
        └──────────┘       └──────────┘       └──────────┘
                                 │
                                 ▼
                    ┌─────────────────────────────┐
                    │   mergeByHandle()           │
                    │   one row per handle,       │
                    │   stores: ['US','UK','EU']  │
                    └────────────┬────────────────┘
                                 ▼
                    ┌─────────────────────────────┐
                    │  Picker rows w/ store pills │
                    └─────────────────────────────┘

Key concepts

  • Handle as canonical ID — Shopify products with the same handle across stores are treated as the same product. The plugin stores handle (not the Shopify GID) as the resource ID Builder persists.
  • Promise.allSettled across stores — one store's failure doesn't break the picker. If the UK API is down, US and EU results still load.
  • Store pills — small flag chips next to each result so editors can see at a glance which regions carry a product before they pick it.

File map

| File | Purpose | | --------------------------- | ------------------------------------------------------------------ | | src/plugin.ts | Builder plugin registration + the 6 settings (3 tokens, 3 domains) | | src/data-plugin.ts | Shopify clients, search, findByHandleAcrossStores, merge logic | | src/ElfResourcePicker.tsx | The picker UI (debounced search, list rows, store pills) | | src/StorePill.tsx | The flag chip component (US/UK/EU SVG flags) | | src/stores.ts | Single source of truth for the supported StoreKey union |


Setup (using the plugin in Builder.io)

1. Get Shopify Storefront API tokens

For each store, in the Shopify admin:

  • AppsDevelop appsCreate an appConfigure Storefront API integration
  • Grant scopes: unauthenticated_read_product_listings, unauthenticated_read_product_inventory, unauthenticated_read_collection_listings
  • Copy the Storefront access token

2. Host the built plugin

Builder.io loads plugins from a URL. Build the bundle and host it somewhere Builder can fetch (HTTPS, public).

pnpm install
pnpm build
# → dist/plugin.system.js

Hosting options:

  • GitHub Pages off this repo's dist/ (simplest for internal use)
  • Your own CDN (S3 + CloudFront, Vercel static, etc.)
  • unpkg.com if published to npm (https://unpkg.com/@natanvotre/builder-shopify-plugin@<version>/dist/plugin.system.js)

3. Register the plugin in Builder.io

In your Builder org: AccountOrganizationPluginsAdd plugin → paste the URL of plugin.system.js. Reload Builder.

4. Configure the 6 settings

Builder will prompt for the plugin settings. Provide:

| Setting | Example | | ------------------------- | ------------------------------- | | storefrontAccessTokenUS | shpat_… | | domainUS | elfcosmetics.myshopify.com | | storefrontAccessTokenUK | shpat_… | | domainUK | elfcosmetics-uk.myshopify.com | | storefrontAccessTokenEU | shpat_… | | domainEU | elfcosmetics-eu.myshopify.com |

Once saved, any Builder field of type ProductPreview or CollectionPreview will use this plugin's picker.


Development

Prerequisites

  • Node 20+
  • pnpm 9+

Install + build

pnpm install
pnpm build           # bundles to dist/plugin.system.js
pnpm types:check     # tsc --noEmit
pnpm lint:check      # oxlint + eslint
pnpm format:check    # prettier

Storybook

The picker UI components have isolated stories. Useful when iterating on the store pills or list rows without touching the Builder.io editor.

pnpm storybook:dev   # http://localhost:6007

Local plugin development against Builder.io

Builder loads plugins from a URL, so for local dev you need to either:

  1. Tunnel — run pnpm build && npx http-server dist -p 1268 --cors then ngrok http 1268 and register the ngrok URL in Builder
  2. Build → push → reload loop — push to your hosted location and refresh Builder

Tunnel is the fast inner loop. Push-and-reload is fine for finishing touches.

Adding a new store

  1. Add the new key to src/stores.ts:
    export const STORES = {
      US: {id: 'US' as const, label: 'US'},
      UK: {id: 'UK' as const, label: 'UK'},
      EU: {id: 'EU' as const, label: 'EU'},
      CA: {id: 'CA' as const, label: 'CA'}, // ← new
    } as const
  2. Add a flag SVG in src/StorePill.tsx under FLAG_SVGS
  3. Add the two settings (token + domain) in src/plugin.ts
  4. Add a make() line in buildClients() in src/data-plugin.ts
  5. Add the new client to the Promise.all([...]) calls in search()
  6. Rebuild + re-register the plugin in Builder

TypeScript will guide you — StoreKey is the source of truth and most call sites are typed against it.


Adapting for a new client

This repo is currently configured as e.l.f.'s plugin instance. To fork it for a new multi-store client:

  1. Repo: fork or template-copy this repo to a new internal repo
  2. Plugin identity in src/plugin.ts:
    • id — change to @theformfactory/<client>-builder-shopify-plugin (must be unique within Builder)
    • name — display name in Builder admin (e.g. ClientShopify)
    • ctaText — wording shown in the "Connect…" button
  3. Settings: rename helperText examples to the client's domains
  4. Stores: edit src/stores.ts to match the client's regions
  5. Picker class name: ElfResourcePicker<Client>ResourcePicker (cosmetic)
  6. Package name: update name in package.json

Most of the merge/search logic is store-shape-agnostic and doesn't need touching.


Architecture notes / trade-offs

  • Why handle and not GID as the canonical ID? GIDs are per-store; handles are typically the same across regions. Using handle as the ID makes cross-region content portable in Builder.
  • What if handles diverge across regions? They show up as separate rows. This is the right default — diverged handles usually mean "different product, similar name" and shouldn't be conflated.
  • Why Promise.allSettled and not Promise.all? A single store's outage shouldn't grey out the whole picker. Settled-with-errors falls back to a partial list.
  • Why bundle as SystemJS? Builder's plugin host is SystemJS-based — this is what registerCommercePlugin expects.
  • @material-ui/core v4 peer dep? Builder's plugin runtime ships MUI v4 globally. Plugins must use the host's copy (hence the peer dep + Rollup external).

License

Internal — FormFactory and its clients only. Not for redistribution.