@digital-gravy/etch-public-api
v0.6.0
Published
MIT-licensed typed client and contract for the Etch builder scripting API (window.etch). Etch itself is a separate proprietary product governed by its own commercial terms.
Readme
@digital-gravy/etch-public-api
Typed client and contract for the Etch builder scripting API exposed on
window.etch. Intended for AI assistants and third-party plugins.
The Etch builder injects the runtime onto the page. This package ships the types plus a thin accessor over the global — there is nothing heavy to bundle, and the implementation always comes from the installed Etch version.
Status: experimental (
0.x). The surface may change without a major version bump until it stabilizes. Prefer feature detection over version comparison, and don't pin production plugins to0.x.
Install
npm install @digital-gravy/etch-public-apiUsage
Getting the API object
The Etch builder injects the runtime onto window.etch during its bootstrap.
Acquire it with getEtch(), guarded by isEtchAvailable() for code that might
run outside the builder:
import { getEtch, isEtchAvailable } from "@digital-gravy/etch-public-api";
function run() {
if (!isEtchAvailable()) return; // not running inside the Etch builder
const etch = getEtch();
const textIds = etch.blocks.find({ type: "text" });
etch.blocks.setText(textIds[0], "Hello world");
}getEtch() throws an EtchApiError with code NOT_AVAILABLE when the builder
isn't on the page, so you can also try/catch it. If your script may run
before the builder has finished loading, wait until it appears:
import { getEtch, isEtchAvailable } from "@digital-gravy/etch-public-api";
async function whenEtchReady(timeoutMs = 10_000) {
const start = Date.now();
while (!isEtchAvailable()) {
if (Date.now() - start > timeoutMs) throw new Error("Etch did not load");
await new Promise((resolve) => setTimeout(resolve, 100));
}
return getEtch();
}
const etch = await whenEtchReady();Reading and mutating blocks
// Read
const textIds = etch.blocks.find({ type: "etch/text" });
const json = etch.blocks.getJson(textIds[0]);
// Mutate (routes through the same guarded paths as the UI; undo/redo works)
etch.blocks.setText(textIds[0], "Hello world");
etch.blocks.addClass(textIds[0], "lead");
// Persist (blocks/styles wait for save; stylesheets/components/fields persist immediately)
await etch.saveAsync();Styles
// Create a class or id rule
const styleId = etch.styles.create(".lead", "font-size: 1.25rem;");
// Find existing styles by selector type
const classStyles = etch.styles.list({ type: "class" });
const myStyle = etch.styles.list().find((s) => s.selector === ".lead");
console.log(myStyle?.id); // the id you pass to blocks.addClass etc.
// Global CSS custom properties (default collection)
etch.styles.setVariable("--brand", "#0af");
etch.styles.getVariable("--brand"); // "#0af"
// Variable methods accept an optional collection for multi-collection :root setups
etch.styles.setVariable("--brand", "#0af", "theme-a");
etch.styles.getVariable("--brand", "theme-a"); // "#0af"
etch.styles.listVariables("theme-a");
etch.styles.removeVariable("--brand", "theme-a");Note: The
collectionfield on style objects (StyleSummary) and thecollectionargument oncreate/updateare internal implementation details — always omit them for regular styles. The four:rootvariable methods (listVariables,getVariable,setVariable,removeVariable) are the exception: they accept an optionalcollectionparameter, defaulting to"default"when omitted.
Component edit mode
Use blocks.enterComponentEditMode to open a component's internal block tree
for direct inspection or mutation, then save and exit when done:
// Find a component block
const [compId] = etch.blocks.find({ type: "etch/component" });
// Enter edit mode — the component's block tree becomes accessible
etch.blocks.enterComponentEditMode(compId);
// Inspect or mutate the component's children
const tree = etch.blocks.getTree();
// Persist the component definition to the backend
await etch.blocks.saveComponentEditModeAsync();
// Save the page, then exit edit mode
await etch.saveAsync();
etch.blocks.exitComponentEditMode();To discard in-memory changes and restore the original block:
etch.blocks.exitComponentEditMode({ revert: true });Error handling
Methods throw a typed EtchApiError with a code, rather than returning
sentinels:
import { isEtchApiError } from "@digital-gravy/etch-public-api";
try {
etch.blocks.getJson("does-not-exist");
} catch (err) {
if (isEtchApiError(err)) {
console.warn(err.code, err.message); // e.g. "BLOCK_NOT_FOUND"
}
}Version negotiation
Today (0.x): there is no real negotiation yet. The runtime reports
etch.apiVersion as the coarse marker '0.x' (not a precise version), and
getEtch({ apiVersion }) only does a best-effort major-version check that
console.warns on mismatch — it never throws or adapts. While the surface is
experimental, prefer feature detection over version comparison:
const etch = getEtch();
if (typeof etch.blocks.someNewMethod === "function") {
// safe to use
}In the future (once the contract reaches 1.x): etch.apiVersion will
report a real semver, and getEtch() will negotiate against the runtime's
native connect() — returning an instance pinned to the version your plugin
targets, and failing fast when the runtime can't satisfy it:
// Reserved API — shape of versioned access once the contract is stable:
const etch = getEtch({ apiVersion: "^1.0", id: "my-plugin" });
// └─ delegates to window.etch.connect({ apiVersion: "^1.0", id }) when present,
// yielding a version-pinned instance (throws on an incompatible runtime).getEtch() already accepts { apiVersion, id } today, so plugins can pass them
now and have them take effect automatically once the stable runtime ships — no
code change needed.
Typed block JSON
Block JSON is a discriminated union on type, so the compiler flags a block
whose payload doesn't match its declared type, and getJson() / getTree()
results narrow by type:
const block = etch.blocks.getJson(id);
if (block.type === "etch/text") {
console.log(block.text); // narrowed to the text-block shape
} else if (block.type === "etch/element") {
console.log(block.tag, block.attributes);
}
// Authoring is checked too — this is a type error (an `etch/text` has no `tag`):
etch.blocks.create({
type: "etch/text",
version: 1,
context: {},
children: [],
tag: "div", // ✗ type error
});Special element attributes
Some block types recognise special attributes in addition to standard HTML:
etch/dynamic-image — rendered as <img>:
mediaId— WordPress attachment ID. Etch fetches the media object and uses its URL assrc, overriding any explicitsrc. Supports dynamic expressions (e.g.{post.featured_image_id}).useSrcSet—"true"to generate a responsivesrcsetfrom the media (requiresmediaId).maximumSize— WordPress image size slug (e.g."large","full") used when resolving the image. Defaults to"full".
etch.blocks.setAttribute(imgBlockId, "mediaId", "{post.featured_image_id}");
etch.blocks.setAttribute(imgBlockId, "useSrcSet", "true");etch/svg — inline SVG:
src— URL of an external.svgfile. Etch fetches and inlines the SVG at render time. Supports dynamic expressions.stripColors—"true"to stripfillandstrokecolour declarations from the fetched SVG, so CSS can drive its colours instead.
etch.blocks.setAttribute(svgBlockId, "src", "/icons/logo.svg");
etch.blocks.setAttribute(svgBlockId, "stripColors", "true");Types only
Every contract type is exported and dependency-free, so you can use them directly:
import type {
PublicBlockJson,
EtchBlocksApi,
} from "@digital-gravy/etch-public-api";You can also work against the global directly — the package augments
window.etch, so window.etch?.blocks.find(...) is fully typed once the package
is imported.
What's here
getEtch(options?)/isEtchAvailable()— acquire the API from the page.EtchApiError/isEtchApiError()/EtchApiErrorCode— typed errors.ETCH_API_VERSION— the contract version this package targets (0.x).- The full contract as exported types:
- Blocks —
Etch,EtchBlocksApi,EtchBlockJson,PublicBlockJson,FindBlocksPredicate,BlockPatch, … - Styles —
EtchStylesApi,StyleSummary,StyleListFilter,StyleSelectorType,StylePatch - Stylesheets —
EtchStylesheetsApi,StylesheetSummary,StylesheetInput,StylesheetPatch - Components —
EtchComponentsApi,PublicComponentSummary,PublicComponentJson,ComponentPatch,ComponentProperty, … - Loops —
EtchLoopsApi,EtchLoop,EtchLoopConfig,BlockLoopBinding, … - Navigation —
EtchNavigationApi,NavigationPlace,PostSummary,TemplateSummary - Fields —
EtchFieldsApi,CustomField,CustomFieldGroup, … - UI / History —
EtchUiApi,EtchHistoryApi,ColorScheme
- Blocks —
Versioning
This package's npm version tracks the scripting contract, not the Etch
product. Additive changes are minor; breaking changes are reserved for a major
bump plus a deprecation window — once the contract leaves 0.x.
License
This package — the @digital-gravy/etch-public-api client and type definitions
— is released under the MIT License.
The MIT license applies only to this package. It does not extend to the Etch builder or the Etch WordPress plugin, which are separate proprietary products governed by their own commercial license terms. This package merely describes and communicates with that software; installing or using it grants no rights to Etch itself.
