@digital-gravy/etch-public-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digital Gravy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @digital-gravy/etch-public-api
2
+
3
+ Typed client and contract for the **Etch builder scripting API** exposed on
4
+ `window.etch`. Intended for AI assistants and third-party plugins.
5
+
6
+ The Etch builder injects the runtime onto the page. This package ships the
7
+ **types** plus a thin accessor over the global — there is nothing heavy to
8
+ bundle, and the implementation always comes from the installed Etch version.
9
+
10
+ > **Status: experimental (`0.x`).** The surface may change without a major
11
+ > version bump until it stabilizes. Prefer feature detection over version
12
+ > comparison, and don't pin production plugins to `0.x`.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install @digital-gravy/etch-public-api
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import { getEtch, isEtchAvailable, EtchApiError } from '@digital-gravy/etch-public-api';
24
+
25
+ if (!isEtchAvailable()) {
26
+ // Not running inside the Etch builder (or it hasn't finished loading).
27
+ return;
28
+ }
29
+
30
+ const etch = getEtch();
31
+
32
+ // Read
33
+ const textIds = etch.blocks.find({ type: 'text' });
34
+ const json = etch.blocks.getJson(textIds[0]);
35
+
36
+ // Mutate (routes through the same guarded paths as the UI; undo/redo works)
37
+ etch.blocks.setText(textIds[0], 'Hello world');
38
+ etch.blocks.addClass(textIds[0], 'lead');
39
+
40
+ const styleId = etch.styles.create('.lead', 'font-size: 1.25rem;');
41
+ etch.styles.setVariable('--brand', '#0af');
42
+
43
+ // Persist (blocks/styles wait for save; stylesheets/components/fields persist immediately)
44
+ await etch.saveAsync();
45
+ ```
46
+
47
+ ### Error handling
48
+
49
+ Methods throw a typed `EtchApiError` with a `code`, rather than returning
50
+ sentinels:
51
+
52
+ ```ts
53
+ import { isEtchApiError } from '@digital-gravy/etch-public-api';
54
+
55
+ try {
56
+ etch.blocks.getJson('does-not-exist');
57
+ } catch (err) {
58
+ if (isEtchApiError(err)) {
59
+ console.warn(err.code, err.message); // e.g. "BLOCK_NOT_FOUND"
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### Version negotiation
65
+
66
+ `getEtch()` accepts the contract version your plugin targets. On today's `0.x`
67
+ runtime this is a best-effort check (a console warning on mismatch). On a future
68
+ stable runtime that exposes a native `connect()`, the call is delegated to it
69
+ and you receive a version-pinned instance:
70
+
71
+ ```ts
72
+ const etch = getEtch({ apiVersion: '^1.0', id: 'my-plugin' });
73
+ ```
74
+
75
+ ### Types only
76
+
77
+ Every contract type is exported and dependency-free, so you can use them
78
+ directly:
79
+
80
+ ```ts
81
+ import type { PublicBlockJson, EtchBlocksApi } from '@digital-gravy/etch-public-api';
82
+ ```
83
+
84
+ You can also work against the global directly — the package augments
85
+ `window.etch`, so `window.etch?.blocks.find(...)` is fully typed once the package
86
+ is imported.
87
+
88
+ ## What's here
89
+
90
+ - `getEtch(options?)` / `isEtchAvailable()` — acquire the API from the page.
91
+ - `EtchApiError` / `isEtchApiError()` / `EtchApiErrorCode` — typed errors.
92
+ - `ETCH_API_VERSION` — the contract version this package targets (`0.x`).
93
+ - The full contract as exported types (`Etch`, `EtchBlocksApi`, `PublicBlockJson`, …).
94
+
95
+ ## Versioning
96
+
97
+ This package's npm version tracks the **scripting contract**, not the Etch
98
+ product. Additive changes are minor; breaking changes are reserved for a major
99
+ bump plus a deprecation window — once the contract leaves `0.x`.
100
+
101
+ ## License
102
+
103
+ This package — the `@digital-gravy/etch-public-api` client and type definitions
104
+ — is released under the [MIT License](./LICENSE).
105
+
106
+ The MIT license applies **only to this package**. It does **not** extend to the
107
+ Etch builder or the Etch WordPress plugin, which are separate proprietary
108
+ products governed by their own commercial license terms. This package merely
109
+ describes and communicates with that software; installing or using it grants no
110
+ rights to Etch itself.
package/dist/index.cjs ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var EtchApiError = class extends Error {
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.name = "EtchApiError";
8
+ this.code = code;
9
+ }
10
+ };
11
+ function isEtchApiError(value) {
12
+ return value instanceof EtchApiError || value?.name === "EtchApiError";
13
+ }
14
+
15
+ // src/version.ts
16
+ var ETCH_API_VERSION = "0.x";
17
+
18
+ // src/client.ts
19
+ function readEtch() {
20
+ const scope = globalThis;
21
+ return scope.etch;
22
+ }
23
+ function isEtchAvailable() {
24
+ return readEtch() !== void 0;
25
+ }
26
+ function majorOf(version) {
27
+ const match = /^\D*(\d+)/.exec(version);
28
+ return match ? Number(match[1]) : null;
29
+ }
30
+ function warnIfIncompatible(requested, runtime) {
31
+ const want = majorOf(requested);
32
+ const have = majorOf(runtime);
33
+ if (want === null || have === null || want === have) return;
34
+ console.warn(
35
+ `[@digital-gravy/etch-public-api] Requested Etch API v${requested} but the page provides v${runtime}. Behavior may differ.`
36
+ );
37
+ }
38
+ function getEtch(options = {}) {
39
+ const etch = readEtch();
40
+ if (!etch) {
41
+ throw new EtchApiError(
42
+ "NOT_AVAILABLE",
43
+ "window.etch is not available. The Etch builder is not loaded on this page, or getEtch() ran before it finished initializing."
44
+ );
45
+ }
46
+ if (typeof etch.connect === "function") {
47
+ return etch.connect(options);
48
+ }
49
+ if (options.apiVersion) {
50
+ warnIfIncompatible(options.apiVersion, etch.apiVersion ?? ETCH_API_VERSION);
51
+ }
52
+ return etch;
53
+ }
54
+
55
+ exports.ETCH_API_VERSION = ETCH_API_VERSION;
56
+ exports.EtchApiError = EtchApiError;
57
+ exports.getEtch = getEtch;
58
+ exports.isEtchApiError = isEtchApiError;
59
+ exports.isEtchAvailable = isEtchAvailable;
60
+ //# sourceMappingURL=index.cjs.map
61
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/version.ts","../src/client.ts"],"names":[],"mappings":";;;AAyBO,IAAM,YAAA,GAAN,cAA2B,KAAA,CAAM;AAAA,EAGvC,WAAA,CAAY,MAAwB,OAAA,EAAiB;AACpD,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACb;AACD;AAGO,SAAS,eAAe,KAAA,EAAuC;AACrE,EAAA,OAAO,KAAA,YAAiB,YAAA,IAAiB,KAAA,EAAiB,IAAA,KAAS,cAAA;AACpE;;;AC9BO,IAAM,gBAAA,GAAmB;;;ACIhC,SAAS,QAAA,GAA6B;AACrC,EAAA,MAAM,KAAA,GAAQ,UAAA;AACd,EAAA,OAAO,KAAA,CAAM,IAAA;AACd;AAMO,SAAS,eAAA,GAA2B;AAC1C,EAAA,OAAO,UAAS,KAAM,MAAA;AACvB;AAGA,SAAS,QAAQ,OAAA,EAAgC;AAChD,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,IAAA,CAAK,OAAO,CAAA;AACtC,EAAA,OAAO,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,IAAA;AACnC;AAOA,SAAS,kBAAA,CAAmB,WAAmB,OAAA,EAAuB;AACrE,EAAA,MAAM,IAAA,GAAO,QAAQ,SAAS,CAAA;AAC9B,EAAA,MAAM,IAAA,GAAO,QAAQ,OAAO,CAAA;AAC5B,EAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,IAAA,KAAS,IAAA,IAAQ,SAAS,IAAA,EAAM;AAErD,EAAA,OAAA,CAAQ,IAAA;AAAA,IACP,CAAA,qDAAA,EAAwD,SAAS,CAAA,wBAAA,EAC9C,OAAO,CAAA,sBAAA;AAAA,GAC3B;AACD;AAsBO,SAAS,OAAA,CAAQ,OAAA,GAA0B,EAAC,EAAS;AAC3D,EAAA,MAAM,OAAO,QAAA,EAAS;AACtB,EAAA,IAAI,CAAC,IAAA,EAAM;AACV,IAAA,MAAM,IAAI,YAAA;AAAA,MACT,eAAA;AAAA,MACA;AAAA,KAED;AAAA,EACD;AAEA,EAAA,IAAI,OAAO,IAAA,CAAK,OAAA,KAAY,UAAA,EAAY;AACvC,IAAA,OAAO,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,EAC5B;AAEA,EAAA,IAAI,QAAQ,UAAA,EAAY;AACvB,IAAA,kBAAA,CAAmB,OAAA,CAAQ,UAAA,EAAY,IAAA,CAAK,UAAA,IAAc,gBAAgB,CAAA;AAAA,EAC3E;AACA,EAAA,OAAO,IAAA;AACR","file":"index.cjs","sourcesContent":["/**\n * Error codes thrown by the public `window.etch` API.\n *\n * The API throws typed errors (rather than returning sentinels) so that AI\n * assistants and plugin authors can `try`/`catch` and react to a precise cause.\n *\n * The union ends with `(string & {})` so that codes added by newer Etch\n * runtimes still type-check as `EtchApiErrorCode` while keeping autocomplete for\n * the known values.\n */\nexport type EtchApiErrorCode =\n\t| 'BLOCK_NOT_FOUND'\n\t| 'WRONG_BLOCK_TYPE'\n\t| 'READONLY'\n\t| 'INVALID_ARGUMENT'\n\t| 'LOOP_NOT_FOUND'\n\t| 'STYLE_NOT_FOUND'\n\t| 'STYLESHEET_NOT_FOUND'\n\t| 'COMPONENT_NOT_FOUND'\n\t| 'POST_NOT_FOUND'\n\t| 'OPERATION_FAILED'\n\t| 'NOT_AVAILABLE'\n\t| (string & {});\n\n/** Error thrown by the public Etch API (and by this client). */\nexport class EtchApiError extends Error {\n\treadonly code: EtchApiErrorCode;\n\n\tconstructor(code: EtchApiErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'EtchApiError';\n\t\tthis.code = code;\n\t}\n}\n\n/** Narrow an unknown caught value to an {@link EtchApiError}. */\nexport function isEtchApiError(value: unknown): value is EtchApiError {\n\treturn value instanceof EtchApiError || (value as Error)?.name === 'EtchApiError';\n}\n","/**\n * Version of the Etch scripting **contract** this package targets, independent\n * of the Etch product version and of this package's own npm version.\n *\n * `0.x` signals the surface is **experimental** and may change without a major\n * bump until it stabilizes. It matches the value returned by the runtime's\n * {@link Etch.apiVersion} getter on `window.etch`.\n */\nexport const ETCH_API_VERSION = '0.x';\n","import type { ConnectOptions, Etch } from './contract';\nimport { EtchApiError } from './errors';\nimport { ETCH_API_VERSION } from './version';\n\ndeclare global {\n\tinterface Window {\n\t\t/** The Etch scripting API, present once the builder has loaded. */\n\t\tetch?: Etch;\n\t}\n}\n\n/** Read `window.etch` from whatever global object exists, or `undefined`. */\nfunction readEtch(): Etch | undefined {\n\tconst scope = globalThis as { etch?: Etch };\n\treturn scope.etch;\n}\n\n/**\n * Whether the Etch scripting API is present on the page. Use this to guard code\n * that should no-op when not running inside the builder.\n */\nexport function isEtchAvailable(): boolean {\n\treturn readEtch() !== undefined;\n}\n\n/** The major version number of a semver-ish string, or `null` if unparseable. */\nfunction majorOf(version: string): number | null {\n\tconst match = /^\\D*(\\d+)/.exec(version);\n\treturn match ? Number(match[1]) : null;\n}\n\n/**\n * Best-effort compatibility check for `0.x` runtimes that have no native\n * `connect()`. Warns (does not throw) when the requested major differs from the\n * runtime's, so experimental consumers aren't hard-blocked.\n */\nfunction warnIfIncompatible(requested: string, runtime: string): void {\n\tconst want = majorOf(requested);\n\tconst have = majorOf(runtime);\n\tif (want === null || have === null || want === have) return;\n\t// eslint-disable-next-line no-console\n\tconsole.warn(\n\t\t`[@digital-gravy/etch-public-api] Requested Etch API v${requested} but the ` +\n\t\t\t`page provides v${runtime}. Behavior may differ.`\n\t);\n}\n\n/**\n * Acquire the Etch scripting API from the page.\n *\n * The runtime lives on `window.etch` (injected by the builder); this returns it\n * typed. When the page exposes a future stable runtime with a native\n * `connect()`, version negotiation is delegated to it. On today's `0.x`\n * runtime, the global is returned directly after a best-effort version check.\n *\n * @throws {EtchApiError} `NOT_AVAILABLE` when the builder is not present.\n *\n * @example\n * ```ts\n * import { getEtch } from '@etchwp/public-api';\n *\n * const etch = getEtch();\n * const ids = etch.blocks.find({ type: 'text' });\n * etch.blocks.setText(ids[0], 'Hello');\n * await etch.saveAsync();\n * ```\n */\nexport function getEtch(options: ConnectOptions = {}): Etch {\n\tconst etch = readEtch();\n\tif (!etch) {\n\t\tthrow new EtchApiError(\n\t\t\t'NOT_AVAILABLE',\n\t\t\t'window.etch is not available. The Etch builder is not loaded on this page, ' +\n\t\t\t\t'or getEtch() ran before it finished initializing.'\n\t\t);\n\t}\n\n\tif (typeof etch.connect === 'function') {\n\t\treturn etch.connect(options);\n\t}\n\n\tif (options.apiVersion) {\n\t\twarnIfIncompatible(options.apiVersion, etch.apiVersion ?? ETCH_API_VERSION);\n\t}\n\treturn etch;\n}\n"]}