efficient-web-builder
v30.1.2
Published
The EFFICIENT website builder: the website editor, the general sections and the renderer that draws them, compiled. Hostable by any EFFICIENT product; nothing here imports a host.
Readme
efficient-web-builder
The EFFICIENT website builder: the website editor, the general sections, and the renderer that draws a published page from them. It has no host in it. The EFFICIENT storefront, the admin portal and Foyerboard each mount it and hand it what is theirs.
This repository was the folder packages/web-builder of
efficient-storefront
until 2026-10-07, and carries that folder's history: each file's log goes back
to the day it was written there
(doc/adr/0001).
The one rule
Nothing in src/ imports anything outside src/ except the packages
package.json declares as peer dependencies. Not a commerce block, not a page,
not a host's API client — by value or by type.
What a host adds, it hands over on the editor's session
(src/lib/editor-session.ts), read once for the life of the mount:
| The host's | Declared in | What it is |
|---|---|---|
| block registry | src/lib/puck/registry.ts, general-registry.tsx | Which blocks exist, in what order. The builder's own registry is the general blocks; a host registers its own among them. |
| backend | src/lib/web-backend.ts | Everything the editor asks a server for: pages, the edit lock, the site record, media, and the optional groups (blog, chat, social, theme, …). |
| chrome | src/lib/editor-chrome.tsx | The host's furniture round the editor, as components: a wrapper, panels, Pages-panel rows, its products and its row under a Link field. |
| document hooks | src/lib/editor-document-hooks.ts | What the host does to a document as it opens and as an edit is stored. |
| failure reader | src/lib/web-backend.ts (provideHostFailures) | How to read the host client's own errors. |
| record types | src/lib/content/record-types.ts (registerRecordTypes) | What a dynamic page of this host may draw besides a product: a key, a label and the fields a block may read. Declared once, for the app and not for one mount. |
| block sources | src/lib/content/block-sources.ts (registerBlockSources) | Where a block of this host may read its rows from instead of holding them: a key, a label, the blocks it fills and sample rows for the editor. The host's backend puts the rows in as a page is served. Declared once, for the app and not for one mount. |
The storefront's are assembled in its lib/storefront-editor.ts.
The editor's words in a host's language
The editor speaks English, and a host that runs in another language lays its own words over it (6.1.0). The English stays in the code, where it is said, and many of the words are also keys (a row is filed under its tab and given its glyph by its English label), so it is translated where it is SHOWN:
- The words are keyed by the English they replace.
efficient-web-builder/format/lib/editor-words-catalogueexportsWEB_BUILDER_WORDS, every phrase the editor says (a settings row's label, hint and choices, a group heading, a panel's and a dialog's words, a block's name and its Add-panel blurb, a placeholder drawn on the canvas while editing), and the type of its keys,WebBuilderPhrase. It is written from the source bypnpm run wordsand held to it byscripts/test-editor-words.ts, so a host typing its catalogue asEditorWordsFor<WebBuilderPhrase>fails its typecheck on a phrase it lacks. - A sentence with a value names it in braces, in the English and in the
host's word alike:
"Delete {name}": "{name} löschen". A host may give a function of the values instead, for a plural or an agreement the braces cannot say ("{n} pages": ({ n }) => …). A count is two phrases (sayCount("{n} page", "{n} pages", n)). - Names of a host's places are phrases of their own, so a host renames them once, in English too: the editor says "Storefront ▸ Sites ▸ Settings" and "the admin", where an EFFICIENT admin has them, and a host whose menu is elsewhere says its own.
- The host hands its words once, while its frame renders, with
configureEditorWords(words, locale)fromlib/editor-words— as it configureslib/web-runtime. The words are fixed for the life of a page (harness UI-25), one editor is mounted at a time, and they are read by plain functions as well as components, so they are the module's state, not a React context. A word left out reads in English. - What is written into a page stays English: a block's default content, a saved name. And what a visitor reads on the published page is never the editor's words.
- The engine's words are efficient-easel's (
EaselStringsProvider); a host wraps the editor in it. The parts the builder draws itself that take easel's words as a prop (a picture box, a Locations tree) are handeduseEaselStrings()'s.
efficient-storefront-blocks adds the phrases of its own blocks the same way
and takes a host's words on its WebsiteEditor (words, locale).
Two checks hold the rule, both in CI and both without an allow-list:
pnpm run typecheck—@/…points atsrc/and nowhere else, so an import of a host does not resolve.pnpm run test:boundary— the packages the sources import are exactly the manifest's peer dependencies, and the manifest declares no module type.
Layout
src/ keeps the paths these files had in the storefront — src/lib/…,
src/components/… — and imports are written @/lib/…, @/components/….
Comments that name a file say lib/puck/controls; read them from src/.
How the storefront uses it
The storefront installs this repository's npm package, like any host, at one
exact version in its package.json (efficient-storefront doc/adr/0011,
2026-10-11). Its commerce and service blocks are written with the builder's
control builders and passes, which they import by package name and side
(efficient-web-builder/render/…, /format/…, /editor/…; 24.0.0). Their
@/… resolves in the storefront's own tree and in its blocks only.
A change to the builder is made HERE and reaches the storefront as a release. Merge it, cut a GitHub Release
vX.Y.Z(which publishes to npm), move efficient-storefront-blocks to that release, then open one storefront pull request that moves the version in itspackage.jsonand its blocks pin together. The storefront's check fails while it and the blocks' repository install two different builders.To try a branch against the storefront's suite before a release, install its build there as a tarball:
pnpm run build && pnpm pack --pack-destination /tmp/builder-trial # in an efficient-storefront worktree pnpm add "efficient-web-builder@file:/tmp/builder-trial/efficient-web-builder-<version>.tgz" just checkExactly one storefront guard fails on a trial, the one that will not let an unreleased builder merge.
What a host receives is compiled. A function's own name is a letter in
dist/, so a host's test cannot find a component by that name: it imports the component and compares, or asks what it draws. Export what a host has to be able to find.
So this repository's CI proves the builder stands alone, and the storefront's proves it still does what it did, on the package. The guards that run the editor assembled with a host stay in the host's repository.
From 2026-10-07 to 2026-10-11 the storefront kept a vendored copy of src/
instead, pinned to a commit here (doc/adr/0001,
doc/adr/0002).
Published as efficient-web-builder
From 1.0.0 the npm package efficient-web-builder is this repository and
nothing else, published from here. (Up to 0.10.0 that name was built in the
storefront: these sources assembled WITH the storefront's blocks and
EFFICIENT's backend, for the admin portal. That assembly is now
efficient-storefront-blocks.)
The source is closed, so the package is compiled: pnpm run build writes one
minified .js and one .d.ts for every file under src/, with no source map
and no comment. There is no bundle.
Three sides (24.0.0, doc/adr/0005).
Every module is on one side, and ships under it at the path it has in src/:
| Side | What it is | A host imports it as |
|---|---|---|
| render | what a published page draws from | efficient-web-builder/render/lib/puck/page-frame |
| format | pure data and rules, no React | efficient-web-builder/format/lib/puck/section-catalog |
| editor | the settings panel, the editor's registries and screen parts | efficient-web-builder/editor/lib/puck/controls |
import { PublishedPageFrame } from "efficient-web-builder/render/lib/puck/page-frame";
import { createEditorSession } from "efficient-web-builder/render/lib/editor-session";
import type { WebsiteBackend } from "efficient-web-builder/editor/lib/web-backend";
import "efficient-web-builder/editor/components/storefront-editor.css";The exports map is the three sides and nothing else, so a module is only
reachable through its own side: an editor module has no file under render/,
and a page that imports through render/ cannot reach the editor in any
bundler. Which side a module is on is decided once, in
scripts/entry-sides.mjs, and the build fails when the render side reaches
the editor. To find a module's side, look for it under dist/<side>/.
Inside the package @/… is already a relative path, so a host needs no
mapping of its own. It installs the peer dependencies the manifest lists.
The sheets load in one order. Since efficient-easel 1.0 the editor's
controls are efficient-shell/ui's, and easel's tokens resolve to the
shell's with no fallback, so efficient-shell/ui.css comes first, compiled
by the host's own Tailwind (its @source scans the shell's dist/). Then
easel's sheets (efficient-easel/screen.css), then
components/storefront-editor.css.
Releasing
A merge is not a release. The pull request that wants hosts to see a change
raises version in package.json and adds a section to CHANGELOG.md. After
it merges, publish a GitHub Release tagged vX.Y.Z on the merge commit:
gh release create vX.Y.Z --target <full merge sha> --title vX.Y.Z --notes-file <notes>.github/workflows/release.yml runs the whole gate again and publishes through
npm trusted publishing. There is no token anywhere; npm accepts the publish
only from that workflow file in this repository. The registry can answer with
the previous version for a couple of minutes after a green run.
Working here
pnpm install
pnpm run verifyverify is the typecheck, the boundary check, the screen rule
(scripts/test-screen-in-easel.mjs, doc/adr/0004), the build and
scripts/verify-dist.mjs, which reads dist/ as a host would: every file
there, every import resolving, every "use client" kept, no source in it.
Node 26 (.node-version), pnpm 11. Do not add "type": "module" to
package.json: the storefront loads these files beside its own, and a module
type here makes its test runner load every one of them twice.
Shared conventions — commits, branches, CI, naming — are the organisation's
harness (efficient-harness), as in every platform repository.
