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

efficient-storefront-blocks

v0.61.0

Published

The EFFICIENT storefront's website editor — the website builder assembled with the storefront's own sections — for an app that edits a storefront website inside its own page. Built from the efficient-storefront-blocks repository.

Readme

efficient-storefront-blocks

The EFFICIENT storefront's website editor — the website builder (the page, template and section editors) assembled with every section the storefront renders — for an app that edits a storefront website inside its own page. The admin portal's Storefront editor windows mount it.

Published as efficient-web up to 0.3.0 and as efficient-web-builder from 0.7.0 to 0.10.0; 0.11.0 is the first release under this name (CHANGELOG.md says how to migrate). The name efficient-web-builder now belongs to the builder alone, published from its own repository (Efficient-Pty-Ltd/efficient-web-builder): the editor, the general sections and the renderer, with no storefront in it.

It is not a second copy of anything: build.mjs compiles this repository's src/ (the storefront's blocks, editor session and API client), starting at package/src/index.tsx, against the efficient-web-builder npm package, and draws the editor canvas with the site's own sheet (src/render/app/globals.css, through package/canvas.css). The storefront itself never installs it: it vendors src/ and its pages import the same source directly.

Use

import "efficient-storefront-blocks/editor.css";
import { WebsiteEditor, createEditorHost } from "efficient-storefront-blocks";

const { host, deliver } = createEditorHost({
  onMessage: (m) => { /* toasts, open the media picker, the form builder … */ },
  address: { search: () => "", show: () => {}, load: retargetWindow, reload: remount },
});
// Grant a token when the editor says it is ready, and again on auth-expired:
//   deliver({ type: "storefront-editor:auth", token, siteId, can })

<WebsiteEditor host={host} target={{ kind: "page", slug: "homepage", siteId }} site={{ slug, origin }} />;
  • Messages are the storefront-editor:* protocol the editor once spoke over postMessage from the shop's /edit routes, as function calls — no origin, no iframe. EditorToHostMessage / HostToEditorMessage type every one.
  • site — the website's slug (public catalog reads name it with ?site=, because the embedder's own host names no website) and its public origin (the routes only the storefront serves, and the Preview link). Re-mount the editor with a key to move to another website.
  • One editor mounted at a time. Parts of the editor keep the open website and page in module state; route every target into one window.
  • Styles. efficient-storefront-blocks/editor.css is the chrome, scoped under .sf-editor; the engine's sheets are imported from the embedder's own efficient-easel. Neither carries the shell's: since efficient-easel 1.0 (0.15.0) every editor control is an efficient-shell/ui component, so the embedder compiles efficient-shell/ui.css (or styles.css) through its own Tailwind v4 build and loads it before any easel sheet. The canvas is styled from inside its own frame (the storefront's compiled app/globals.css), so neither page's styles reach the other.
  • Peers: react/react-dom 19, efficient-easel, efficient-shell.

A backend of your own (0.4.0)

The editor reaches its server only through a WebsiteBackend (doc/adr/0002). Leave backend out and it is EFFICIENT's — /api/storefront/*, authorised by the token you deliver with storefront-editor:auth, exactly as above. A product with its own backend passes its own:

import { WebsiteEditor, WebsiteBackendError, type WebsiteBackend } from "efficient-storefront-blocks";

const backend: WebsiteBackend = {
  pages: {
    list: () => api.pages(),                                   // EditorPageRow[]
    load: (ref) => api.page(ref),                              // { id, layout, version } | null
    save: async (id, doc, expect) => {
      const res = await api.savePage(id, doc.layout, expect.version);
      if (res.conflict) {
        throw new WebsiteBackendError("changed-elsewhere", undefined, {
          conflict: { savedBy: res.by, savedByYou: res.mine, secondsAgo: res.ago },
        });
      }
      return { version: res.version };
    },
  },
  site: { get: () => api.site() },                             // { name }
  media: { resolve: (ref) => (ref.startsWith("file:") ? cdn(ref) : null) },
};

<WebsiteEditor host={host} target={{ kind: "page", slug: "home" }} site={null} backend={backend} />;
  • Required: pages (list, load, save), site.get, media.resolve. Optional, each switching its own control on: pages.version, pages.publish (the main button reads Publish, not Save), pages.create / pages.remove (inline in the page list), lock (the "somebody else is editing" notices and take-over), site.update (the Maps key), preview (the Preview link; with draftLink, Preview opens the saved draft), sectionTemplates, templates. A missing group is never a dead button: the editor does not draw what it cannot do.
  • Failures are kinds. Reject with WebsiteBackendError: unauthorized, changed-elsewhere (+ conflict), lock-taken (+ holder), not-found, invalid, unavailable. The editor never reads a status code or a body.
  • No token. How a call is authorised is the backend's business. The editor waits for no auth message and needs none; it announces ready once, and when a save, a publish or a lock claim is refused unauthorized it tells the host auth-expired and lets the autosave try again.
  • No request of its own. Under your backend the package's own HTTP client is closed and the canvas loads no font sheet from Google.
  • PageBundle is { layout, translations?, seo? }. 0.4.0 reads and writes layout only; store the bundle anyway — translations and seo are where per-language content will live, and an absent member means "leave it".
  • Not behind the interface yet (they work under EFFICIENT's backend, and are absent under yours): site styles, the pinned header and footer, menus, forms, the catalog, "duplicate to page". Block settings that list such data (product link targets, product sources) list nothing, and a Form section's picker is inert: it offers no list, no "New form" and sends no open-form.
  • backend.pages.linkTargets (optional): your pages with their layouts, for the link picker. Without it the picker lists pages.list and no anchors.
  • backend.media.maxUploadBytes (optional): the largest file a media field takes before it reaches your panel or picker; leave it out to refuse none.
  • backend.theme (optional): get / update the website's appearance — the SiteThemeRow behind Site styles (colour themes, fonts and sizes, corners, cards, widths). update is handed only the keys that changed and answers the whole row. Without it the editor shows no Site styles and the website is drawn in the built-in look.
  • backend.social (optional): get / update the website's social links, one address per platform name. Reject a bad address with new WebsiteBackendError("invalid", message, { fields: { instagram: "…" } }) and the reason is shown beside that platform's box.
  • backend.chat (optional): get / update the live-chat bubble's settings. (The Live chat panel itself is still drawn only where the site's settings are — EFFICIENT's backend — until those settings cross too.)
  • backend.blog (optional): posts(scope?) answers your published posts. With it a section's "Blog post source" and the link picker's "Blog post" list your blog; without it neither offers a list.
  • backend is read once, when the editor mounts — re-mount (a key) to change it.

A host that is not the admin portal (0.19.0)

The editor's calls to EFFICIENT's backend are authorised by the host's own token, and each call names the portal that token was minted for (X-Portal-Audience). The backend refuses a token whose portal is not the one named ("Token was issued for a different portal"). Until 0.19.0 every call said admin. A host that is another portal names its own, once, on the backend it passes:

import { WebsiteEditor, createEditorHost, createEfficientBackend } from "efficient-storefront-blocks";

const [backend] = useState(() => createEfficientBackend({ portalAudience: "venue" }));

<WebsiteEditor host={host} target={target} site={{ slug, origin }} backend={backend} mediaPanel={MediaPanel} />;
  • portalAudience is "admin" (the default: leave it out and nothing changes) or "venue", the venue portal, where a business edits its own website. It is carried by every request the package makes with the host's token: the page and its lock, publish, site styles, the header, menus, forms, media uploads, section templates. A value the package does not know is sent as admin.
  • It is still EFFICIENT's backend. The editor waits for the token (storefront-editor:auth) and keeps its own client open, exactly as with no backend at all.
  • It is the host's, not one editor's. A host is one portal, so the audience is kept for the page from the moment it is named: every editor mounted afterwards carries it, and so do an editor's last calls after it unmounts (the lock's release, a final save). Make the backend before the first editor mounts. A backend made without portalAudience leaves it as it is.
  • What the backend answers is the backend's rule. For venue: efficient-backend docs/adr/0049 ("Amended 2026-10-12"). A venue login that holds the website right reaches the routes of a brochure website (pages, the lock, publish, the site and its settings, theme, header and social links, menus, forms, media, section templates, and the template catalogue to read). The shop's routes stay the admin portal's.

What such a host supplies

The editor draws the canvas, the block panels, Site styles and the header panel. Everything round it is the host's, as it is the admin portal's (admin-portal/src/pages/storefront/, src/components/website/):

| The host supplies | How the editor asks for it | |---|---| | The token, and a fresh one when it runs out | It sends storefront-editor:ready, and storefront-editor:auth-expired when a call is refused; answer each with deliver({ type: "storefront-editor:auth", token, siteId }) | | A window to mount it in, and its address | createEditorHost({ address }): search, show, load, reload; it sends storefront-editor:navigated when it moves to another page | | A media library | the mediaPanel prop (MediaPanelComponent, drawn once as the editor's popup: open while pick or browsing is set, closed by onClose). Without one, each image field sends storefront-editor:open-media-picker; answer with storefront-editor:media-picked or storefront-editor:media-pick-cancelled | | A picture dropped from that popup | the mediaPlace prop (MediaPlace): what the file becomes on this host before the editor places it (its pick path). Without one, the file as it came | | The page list's settings screen, and the new-page flow | storefront-editor:open-page (slug) and storefront-editor:new-page. The new-page flow is where a page is made blank or from the website's template library (GET /api/storefront/pages/template-library/, POST /api/storefront/pages/from-template/) | | Menus | storefront-editor:open-menus (menuId); tell the editor with storefront-editor:menus-updated when one was saved | | Forms, and the inbox of what was sent | storefront-editor:open-form (mode: "create" with a requestId, or "edit" with a formId); answer a create with storefront-editor:form-saved or storefront-editor:form-closed | | The website's settings and its address | nothing asks: a screen of the host's own (/api/storefront/sites/, sites/{id}/address/). The editor itself saves one site field, the Maps key | | The template picker | nothing asks: making a website from a look or from nothing is the host's (GET /api/storefront/page-templates/, POST /api/storefront/sites/ with template) | | Toasts and the unsaved-changes guard | storefront-editor:dirty, saved, published, save-failed |

storefront-editor:open-product, open-category, open-post and save-as-template are sent only from the shop's blocks, the blog source and an operator's template editor. A host that offers none of those never receives them (blocks: { sets: [...] }, above; leave can out of the auth message).

What still assumes the admin portal

Listed so that a host knows what it will meet. None of it is switched by portalAudience.

  • Calls the backend answers only to the admin portal. With portalAudience: "venue" the backend refuses these (403): the product catalogue and one product's details (/api/storefront/products/, wheel-curations/), categories (categories/), the product field answers (field-usage/), blog posts (blog-posts/) and the live-chat settings (chat-settings/). A refused list reads as an empty one (the catalogue, categories, the field answers) or as "Couldn't reach" (the blog source). The Live chat panel is still drawn: it loads nothing and its save is refused. Offer only the block sets a brochure website needs (blocks.sets) and the catalogue is never asked for. To stop the editor asking for the chat settings or the blog, leave the group out of the backend you pass: const { chat, blog, ...backend } = createEfficientBackend({ portalAudience: "venue" }) (it is still EFFICIENT's backend).
  • Words that name the admin portal's menu, until the host renames them. Hints such as "Create menus in Storefront → Menus", "Goes to Storefront → Forms", "Syncing from Storefront → Blog" and "Storefront → Categories" say where those screens are in the admin portal. Since 0.26.0 each place is a phrase of its own in the editor's words ("Storefront → Menus", "Storefront → Forms", "Storefront → Blog", "Storefront → Categories", "Storefront → Booking Services", "Settings"; the builder's "Storefront ▸ Sites ▸ Settings" and "the admin"), so a host names its own places in words, in English too (below).
  • brandId and can.canCreatePlatformTemplate in the auth message are the admin portal's and the operator's. Leave both out.
  • Template targets. target: { kind: "template" } edits a row of the template catalogue, which only the admin portal may write.
  • The Preview link and the shop's own routes (fitment, geocoding, logo analysis) go to site.origin, the published website, whatever the host is.

Nothing in the package reads /api/auth/me/, a permission name or a feature flag: what the signed-in person may do is the host's to know, and it says so by what it mounts and what it puts in can.

The editor in the host's language (0.26.0)

The editor speaks English. A host in another language hands it its words, the language they are in, and the language the backend should answer in:

import { EaselStringsProvider } from "efficient-easel/strings";
import { WebsiteEditor, createEfficientBackend } from "efficient-storefront-blocks";
import type { WebsiteEditorCatalogue } from "efficient-storefront-blocks/words";

const de: WebsiteEditorCatalogue = { Pages: "Seiten", "Delete {name}": "{name} löschen", /* … */ };
const [backend] = useState(() => createEfficientBackend({ portalAudience: "venue", language: "de" }));

<EaselStringsProvider value={easelDe} locale="de">
  <WebsiteEditor host={host} target={target} site={site} backend={backend} words={de} locale="de" />
</EaselStringsProvider>;
  • words are keyed by the English they replace. Every phrase is listed by efficient-storefront-blocks/words (a light entry, like /contract): WEBSITE_EDITOR_WORDS, the website builder's phrases (WEB_BUILDER_WORDS) and the blocks' own (STOREFRONT_BLOCKS_WORDS), each its own English. Type a language's file as WebsiteEditorCatalogue and a phrase a release adds fails your typecheck until it is said; WebsiteEditorWords takes any of them.
    • A sentence names its values in braces, in English and in your word alike ("Delete {name}": "{name} löschen"), or give a function of the values for a plural or an agreement the braces cannot say. A count is two phrases ("{n} item", "{n} items").
    • A word left out reads in English. Words are read when the editor mounts and for as long as it is mounted: a language is fixed for the life of a page.
    • The names of the admin portal's places are phrases of their own (above): give your own places for them, even in English.
  • The engine's words are efficient-easel's: wrap the editor in its EaselStringsProvider. The editor hands them on to the engine's parts it draws itself.
  • locale is the BCP 47 tag your words are in.
  • createEfficientBackend({ language }) sends Accept-Language on every call the editor makes, so what the backend says back — a refused save, a page it will not open — is in your language where your portal offers it. Left out, the browser's own.
  • What is written into a page stays English, and what a visitor reads on the published website is never the editor's words: a block's default content, a form a visitor fills, the cart and the checkout are the site's.

Which blocks the editor offers (0.7.0)

Every block is in one of three sets:

| Set | What it is | |---|---| | general | Draws from its own props and what the host hands it (media, the site's logo, its Maps key). Any host may offer it. | | commerce | The shop: products, the catalogue, the cart, the checkout, the pinned Header. | | service | Needs one of the EFFICIENT backend's site services: forms, bookings, reviews, the newsletter, the blog, menus, the pinned Footer. |

Which block is in which set is GENERAL_BLOCKS, COMMERCE_BLOCKS and SERVICE_BLOCKS in efficient-storefront-blocks/contract.

Leave blocks out and the editor offers all of them, exactly as before 0.7.0. Pass it to choose:

import { WebsiteEditor, type WebsiteEditorBlocks } from "efficient-storefront-blocks";
import type { ComponentConfig } from "efficient-easel/blocks";

const Roster: ComponentConfig = {
  label: "Roster",
  fields: { heading: { type: "text", label: "Heading" } },
  defaultProps: { heading: "On tonight" },
  // `puck.metadata` is whatever you hand the editor as `blocks.metadata`.
  render: ({ heading, puck }) => <RosterSection heading={heading} people={puck.metadata.roster} />,
};

const blocks: WebsiteEditorBlocks = {
  sets: ["general"],                       // default: all three
  deny: ["CustomHTML"],                    // never these
  // allow: [...],                         // or: only these (after `sets`)
  host: { "acme-roster": Roster },         // your own blocks, by type
  categories: [{ key: "acme", title: "Venue", components: ["acme-roster"], before: "openers" }],
  metadata: { roster },                    // live: change it and the canvas redraws
};

<WebsiteEditor host={host} target={target} site={null} backend={backend} blocks={blocks} />;
  • Offered, not registered. A block you leave out is in no group of the Add panel, no slot's "Add block" menu and no drop target, and no section is seeded with it. It is still registered: a page that already holds one draws it, and it can be selected and deleted.
  • A section is offered only with what it is built from. Adding a Hero writes Hero Slides, and "Brand Text" lays down Boxes, a Heading and a Text — so a Hero is not offered where HeroSlide is not, and Brand Text is not where the Box is not. allow: ["Hero"] therefore offers nothing; allow: ["Hero", "HeroSlide"] offers the Hero. To see what a section writes, add it and run blockTypesIn on the saved layout. A section that is only wrapped in a Box when it is added (most sections with a heading) is added whole instead where the Box or the Section Header is not offered.
  • With a set left out, what needs it is not drawn. Without service: no "Blog post" link target, no "Blog post source" on Feature Banner, Article Text and Post Card, an inert Form picker. Without commerce: no "Product" link target, no catalog seed. The pinned Header and Footer need both.
  • Your own blocks. Written as the engine's ComponentConfig, with its field types (text, textarea, number, select, radio, array, object, custom, slot). A type the editor already registers is refused — the editor throws when it mounts — so give yours a prefix. A block is added from the Add panel, in the group categories names; one named in no group is registered and offered nowhere, and none is offered inside a Box. A categories entry whose key is an existing group (content, structure, …) adds to the end of that group.
  • Your blocks get none of the editor's own section rows. The Padding, Width, Height, Anchor, Theme, Corners and Animation rows, the four settings tabs and click-to-edit text are added to the editor's own blocks by ~25 passes that run once, when the package loads, over lists fixed in its source. A block that arrives when the editor mounts is not on those lists, so it is drawn exactly as its render draws it and edited with exactly the fields it declares. The one exception is the Layers tree's eye, which hides your block as it hides any other (it stores _hidden: true on the node's props). Giving host blocks the rest needs an applyBlockPasses(type, block) the package does not have yet: each pass rewritten from a loop over a fixed list into a function of one block, run in the same order.
  • metadata is live; the rest is read once. sets, allow, deny, host and categories are read when the editor mounts, with backend — re-mount (a key) to change them. metadata is followed: hand the editor a new object and every block in the canvas redraws in place. It is never written to the page or to the undo history. Keep the object the same between renders while the data is the same (a useMemo, a state value): a new object each render redraws the canvas each render.

Checking a saved page on your server

Hiding a block in the editor is not a security boundary. blocks decides what the editor offers; a page is a JSON document, and whoever can call your save endpoint can send one holding any block type. Check it where you store it:

import { GENERAL_BLOCKS, blockTypesIn } from "efficient-storefront-blocks/contract";

const ALLOWED = new Set([...GENERAL_BLOCKS, "acme-roster"]);

function assertAllowed(layout: unknown) {
  const refused = blockTypesIn(layout).filter((type) => !ALLOWED.has(type));
  if (refused.length) throw new Error(`This page holds blocks this site does not allow: ${refused.join(", ")}`);
}

efficient-storefront-blocks/contract is one small file that imports nothing — no React, no engine, no editor — so a server can load it. It exports BLOCK_SETS, GENERAL_BLOCKS, COMMERCE_BLOCKS, SERVICE_BLOCKS, blockSetOf(type) and blockTypesIn(layout).

blockTypesIn reads a layout by shape: any { type, props } in the page's content, in a zones entry, or in an array under a node's props is a block, to any depth. It errs wide — something that merely looks like a block is counted — so a check built on it can refuse a page it might have accepted, and never the reverse. If ALLOWED is narrower than what your editor offers, list the blocks your sections are built from as well (see above), or the editor will build pages your server refuses.

Drawing a link the way the editor does

efficient-storefront-blocks/links is the editor's link rules, for a host that draws the shared link picker (efficient-easel LinkPicker) somewhere else — the admin's menu items. It is built from the same file the editor's own Link field reads, so a link written in one place reads back the same in the other.

import {
  LINK_KINDS, inferLinkKind, isSafeLink, listingFilterFields,
  listingTarget, productListingPaths, readListingFilter, withListingFilter,
} from "efficient-storefront-blocks/links";

const listing = productListingPaths(pages, { fallback: sellsProducts });
inferLinkKind("/shop?type=tyre", pagePaths, listing);          // "page"
withListingFilter("/wheels", { param: "collection", value: "Precision Forged" });
//   "/wheels?collection=Precision%20Forged"
isSafeLink("javascript:alert(1)");                             // false

isSafeLink is efficient-shell's one address rule, isSafeLinkHref, asked on the trimmed text — the rule the shell's LinkPicker applies in the editor.

listingFilterFields(catalogue, { listing, fieldUsage }) lists the filters a link onto the product listing may offer — the listing's own rail for the kinds the section shows, after the site's Product Types answers and the one-value rule — each with the values the site's catalogue has. Light like /contract: one file, no import of any kind.

Release

The version here is bumped by hand in the pull request that wants the admin to see a change (and a CHANGELOG.md section added). After it merges:

gh release create vX.Y.Z --target <merge sha> --notes-file <the CHANGELOG section>

.github/workflows/release.yml builds and publishes it (npm trusted publishing). Then bump the admin portal's pin. Up to 0.13.0 the package was built in efficient-storefront (packages/website) and released there under web-vX.Y.Z tags.

Closed source: © Efficient Pty Ltd, all rights reserved (LICENSE); the published files are minified and ship no source maps.