@digital-gravy/etch-public-api 0.10.0 → 0.12.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/README.md CHANGED
@@ -254,6 +254,63 @@ etch.blocks.setAttribute(svgBlockId, 'src', '/icons/logo.svg');
254
254
  etch.blocks.setAttribute(svgBlockId, 'stripColors', 'true');
255
255
  ```
256
256
 
257
+ ### Data sources
258
+
259
+ `etch.dataSources` is the `dataSources` capability's model of dynamic data, and
260
+ supersedes `etch.loops`. A source is defined once for the project under a `key`
261
+ and read anywhere an expression is accepted, through `data('key')`. The
262
+ namespace is **optional** — a runtime without the capability omits it.
263
+
264
+ ```ts
265
+ if (etch.environment?.capabilities.dataSources === true) {
266
+ const sources = etch.dataSources!;
267
+
268
+ await sources.createAsync({
269
+ key: 'courses',
270
+ name: 'Courses',
271
+ type: 'api',
272
+ metadata: { url: 'https://example.com/courses.json' }
273
+ });
274
+
275
+ // See the real shape before authoring paths against it
276
+ const value = await sources.resolveAsync('courses');
277
+
278
+ // A loop block points its target at the expression — there is no loop id
279
+ etch.blocks.update(loopBlockId, { target: "data('courses')" });
280
+ }
281
+ ```
282
+
283
+ Reads (`list`, `get`, `findDataSource`) are synchronous; writes persist to the
284
+ backend immediately and do not wait for `saveAsync()`.
285
+
286
+ ### Routes
287
+
288
+ `etch.routes` is the site's route tree: segments hanging from the site root,
289
+ each serving content records through a template. The namespace is **optional** — a runtime
290
+ without the `routes` capability omits it.
291
+
292
+ ```ts
293
+ if (etch.environment?.capabilities.routes === true) {
294
+ const routes = etch.routes!;
295
+
296
+ // A template is a record of a template content type
297
+ const [template] = await etch.navigation.listTemplatesAsync();
298
+
299
+ const blog = await routes.createAsync({ segment: 'blog' });
300
+ await routes.createAsync({
301
+ parentId: blog.id,
302
+ segment: '{this.title.toSlug()}',
303
+ source: { postTypes: ['post'] },
304
+ templateId: template.id
305
+ });
306
+
307
+ // Where every record lands, as the build would publish it
308
+ const paths = await routes.getPathsAsync();
309
+ }
310
+ ```
311
+
312
+ Writes persist to the backend immediately and do not wait for `saveAsync()`.
313
+
257
314
  ### Skills
258
315
 
259
316
  `etch.skills` exposes the bundled, read-only authoring guides Etch ships (e.g.
@@ -317,6 +374,8 @@ is imported.
317
374
  - **Stylesheets** — `EtchStylesheetsApi`, `StylesheetSummary`, `StylesheetInput`, `StylesheetPatch`
318
375
  - **Components** — `EtchComponentsApi`, `PublicComponentSummary`, `PublicComponentJson`, `ComponentPatch`, `ComponentProperty`, …
319
376
  - **Loops** — `EtchLoopsApi`, `EtchLoop`, `EtchLoopConfig`, `BlockLoopBinding`, …
377
+ - **Data sources** — `EtchDataSourcesApi`, `EtchDataSource`, `EtchDataSourceType`, `EtchDataSourcePatch`
378
+ - **Routes** — `EtchRoutesApi`, `EtchRoute`, `EtchRouteInput`, `EtchRoutePatch`, `EtchRouteSource`, `EtchRoutedPath`
320
379
  - **Navigation** — `EtchNavigationApi`, `NavigationPlace`, `PostSummary`, `TemplateSummary`
321
380
  - **Fields** — `EtchFieldsApi`, `CustomField`, `CustomFieldGroup`, …
322
381
  - **Skills** — `EtchSkillsApi`, `SkillSummary`, `SkillDetail`
@@ -1 +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,OACC,KAAA,YAAiB,YAAA,IAChB,KAAA,EAAiB,IAAA,KAAS,cAAA;AAE7B;;;ACjCO,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;AACrD,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;AAAA,MACC,OAAA,CAAQ,UAAA;AAAA,MACR,KAAK,UAAA,IAAc;AAAA,KACpB;AAAA,EACD;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 (\n\t\tvalue instanceof EtchApiError ||\n\t\t(value as Error)?.name === 'EtchApiError'\n\t);\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\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(\n\t\t\toptions.apiVersion,\n\t\t\tetch.apiVersion ?? ETCH_API_VERSION\n\t\t);\n\t}\n\treturn etch;\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/version.ts","../src/client.ts"],"names":[],"mappings":";;;AA2BO,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,OACC,KAAA,YAAiB,YAAA,IAChB,KAAA,EAAiB,IAAA,KAAS,cAAA;AAE7B;;;ACnCO,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;AACrD,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;AAAA,MACC,OAAA,CAAQ,UAAA;AAAA,MACR,KAAK,UAAA,IAAc;AAAA,KACpB;AAAA,EACD;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| 'DATA_SOURCE_NOT_FOUND'\n\t| 'STYLE_NOT_FOUND'\n\t| 'STYLESHEET_NOT_FOUND'\n\t| 'COMPONENT_NOT_FOUND'\n\t| 'POST_NOT_FOUND'\n\t| 'ROUTE_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 (\n\t\tvalue instanceof EtchApiError ||\n\t\t(value as Error)?.name === 'EtchApiError'\n\t);\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\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(\n\t\t\toptions.apiVersion,\n\t\t\tetch.apiVersion ?? ETCH_API_VERSION\n\t\t);\n\t}\n\treturn etch;\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -717,12 +717,16 @@ type EtchProduct = 'etch-wp' | 'etch-studio' | (string & {});
717
717
  *
718
718
  * - `fields` — `etch.fields.*` is backed by a real field store.
719
719
  * - `templates` — template posts exist and `navigation.goTo('templates')` works.
720
+ * On Studio a template post is a record of a template content type, and
721
+ * `goTo('templates')` opens the content hub, where they live.
722
+ * - `routes` — `etch.routes.*` is backed: the site's URLs are a route tree the
723
+ * author draws, and each route picks a template for the records it serves.
720
724
  * - `wpMedia` — media ids resolve against the **WordPress media library**, so a
721
725
  * URL can be turned into an id through `/wp/v2/media`. Without it the ids are
722
726
  * the product's own asset store instead. Either way an `etch/dynamic-image`
723
727
  * block binds the same: a numeric `mediaId` attribute.
724
728
  */
725
- type EtchCapability = 'loops' | 'dataSources' | 'fields' | 'templates' | 'wpMedia';
729
+ type EtchCapability = 'loops' | 'dataSources' | 'fields' | 'templates' | 'wpMedia' | 'routes';
726
730
  /**
727
731
  * Capability map. Every key is optional on purpose: a runtime older than a
728
732
  * capability's introduction simply omits it, so **absent means unavailable**.
@@ -747,6 +751,101 @@ interface EtchEnvironment {
747
751
  readonly blockTypes: readonly string[];
748
752
  }
749
753
 
754
+ /**
755
+ * Project data sources and the `etch.dataSources` API surface.
756
+ *
757
+ * Data sources are the `dataSources` capability's model of dynamic data, and
758
+ * they supersede {@link EtchLoopsApi | loops}: a named, typed source is defined
759
+ * once for the project and read anywhere an expression is accepted, through
760
+ * `data('key')`. A loop block points its `target` at one of those expressions
761
+ * rather than binding to a loop id.
762
+ *
763
+ * Check `etch.environment.capabilities.dataSources === true` before using this
764
+ * namespace — a runtime without it omits the whole thing.
765
+ */
766
+ /**
767
+ * A source type. The three built-ins are listed; the registry behind the
768
+ * runtime is open, so a host may serve types this contract has never heard of.
769
+ */
770
+ type EtchDataSourceType = 'json' | 'api' | 'js' | (string & {});
771
+ /**
772
+ * A project data source, identified by its `key` — the name `data('key')`
773
+ * calls it by.
774
+ */
775
+ interface EtchDataSource {
776
+ /**
777
+ * Stable identifier used in expressions: `data('products')`. Unique within
778
+ * the project, and part of every authored expression that reads the source,
779
+ * so renaming one breaks those expressions.
780
+ */
781
+ key: string;
782
+ /** Display name shown in the data manager. */
783
+ name: string;
784
+ /** Which kind of source this is; decides how `metadata` is read. */
785
+ type: EtchDataSourceType;
786
+ /**
787
+ * Type-specific configuration. The built-in types read:
788
+ *
789
+ * - `json` — `{ data: unknown }`, the payload served verbatim.
790
+ * - `api` — `{ url: string; method?: string; headers?: [string, string][] }`,
791
+ * fetched and parsed as JSON. A non-OK response fails the source.
792
+ * - `js` — `{ code: string }`, a function body run in a sandbox. Arguments
793
+ * passed by the expression (`data('rows', 5)`) arrive as `args`.
794
+ */
795
+ metadata: Record<string, unknown>;
796
+ }
797
+ /** Fields of an {@link EtchDataSource} that `updateAsync` can change. */
798
+ type EtchDataSourcePatch = Partial<EtchDataSource>;
799
+ /**
800
+ * Project data sources: definitions, and resolving one to its value.
801
+ *
802
+ * Reads are synchronous — the project's sources are loaded before `window.etch`
803
+ * exists. Writes persist to the backend immediately (they do not wait for
804
+ * {@link Etch.saveAsync}) and drop any value already resolved from the source,
805
+ * so the canvas re-reads it.
806
+ */
807
+ interface EtchDataSourcesApi {
808
+ /** Every data source defined for this project. */
809
+ list(): EtchDataSource[];
810
+ /**
811
+ * One data source by key.
812
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
813
+ */
814
+ get(key: string): EtchDataSource;
815
+ /**
816
+ * Fuzzy-search data sources by `name` or `key`, ranked best-first. Returns
817
+ * an empty array for a blank query or when nothing matches.
818
+ */
819
+ findDataSource(query: string): EtchDataSource[];
820
+ /**
821
+ * Define a new data source.
822
+ * @throws {EtchApiError} `INVALID_ARGUMENT` on a malformed source, or when
823
+ * the key is already taken.
824
+ */
825
+ createAsync(source: EtchDataSource): Promise<EtchDataSource>;
826
+ /**
827
+ * Change an existing data source. Only the fields present are applied.
828
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
829
+ */
830
+ updateAsync(key: string, patch: EtchDataSourcePatch): Promise<EtchDataSource>;
831
+ /**
832
+ * Delete a data source. Expressions still naming it resolve to `undefined`.
833
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
834
+ */
835
+ deleteAsync(key: string): Promise<void>;
836
+ /**
837
+ * Resolve a data source to its value, exactly as `data('key', ...args)`
838
+ * would on the canvas — read this before authoring the property paths that
839
+ * depend on its shape.
840
+ *
841
+ * A source that fails resolves to `undefined` rather than rejecting, which
842
+ * is what the render sees too.
843
+ *
844
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
845
+ */
846
+ resolveAsync(key: string, ...args: unknown[]): Promise<unknown>;
847
+ }
848
+
750
849
  /**
751
850
  * Global style (CSS rule) shapes and the `etch.styles` API surface.
752
851
  */
@@ -1151,9 +1250,11 @@ interface EtchComponentsApi {
1151
1250
  * - `content-hub` — the pages/posts browser
1152
1251
  * - `style-manager` — the global style manager
1153
1252
  * - `loop-manager` — the loop manager
1253
+ * - `data-manager` — the data source manager
1154
1254
  * - `asset-manager` — the asset (media) library
1255
+ * - `routes` — the routes manager
1155
1256
  */
1156
- type NavigationPlace = 'builder' | 'templates' | 'style-manager' | 'content-hub' | 'loop-manager' | 'data-manager' | 'asset-manager';
1257
+ type NavigationPlace = 'builder' | 'templates' | 'style-manager' | 'content-hub' | 'loop-manager' | 'data-manager' | 'asset-manager' | 'routes';
1157
1258
  /** Lightweight post entry returned by `navigation.listPostsAsync()`. */
1158
1259
  interface PostSummary {
1159
1260
  /** Post id. */
@@ -1198,6 +1299,126 @@ interface EtchNavigationApi {
1198
1299
  listTemplatesAsync(): Promise<TemplateSummary[]>;
1199
1300
  }
1200
1301
 
1302
+ /**
1303
+ * The site's route tree and the `etch.routes` API surface.
1304
+ *
1305
+ * Routes are a tree of segments hanging from the site root; a route's URL is
1306
+ * its ancestors' segments joined by `/`. What a segment is depends on how it is
1307
+ * written:
1308
+ *
1309
+ * - `''` — the site root. Exactly one, never created or deleted.
1310
+ * - `about` — static: lowercase letters and digits joined by `-` or `_`.
1311
+ * Serves at most one record.
1312
+ * - `(marketing)` — a group: nests routes without adding to the URL. Serves
1313
+ * nothing itself.
1314
+ * - `{this.title.toSlug()}` — dynamic: resolved once per record it serves,
1315
+ * from that record's fields. Serves any mix of records and whole content
1316
+ * types. `this.slug` and `this.isHomepage` cannot be used — the routes
1317
+ * decide them.
1318
+ *
1319
+ * Records are what `navigation.listPostsAsync()` lists, and their content type
1320
+ * is its `postType` — so the fields below keep the `post` names. Templates are
1321
+ * records of a template content type (`navigation.listTemplatesAsync()`). A
1322
+ * route renders the records it serves through its template, or the nearest
1323
+ * ancestor's when it has none.
1324
+ *
1325
+ * Check `etch.environment.capabilities.routes === true` before using this
1326
+ * namespace — a runtime without it omits the whole thing.
1327
+ */
1328
+ /** What a route is, read off its segment. */
1329
+ type EtchRouteSegmentKind = 'root' | 'static' | 'group' | 'dynamic';
1330
+ /** What a route serves: these records, plus every record of these content types. */
1331
+ interface EtchRouteSource {
1332
+ /** Record ids, as `navigation.listPostsAsync()` returns them. */
1333
+ postIds: number[];
1334
+ /** Content type keys — `PostSummary.postType`. Dynamic routes only. */
1335
+ postTypes: string[];
1336
+ }
1337
+ /** One node of the route tree. */
1338
+ interface EtchRoute {
1339
+ /** Opaque string id. */
1340
+ id: string;
1341
+ /** The route this one sits inside. Absent only on the site root. */
1342
+ parentId?: string;
1343
+ /** This route's own part of the URL — see the module docs for the grammar. */
1344
+ segment: string;
1345
+ kind: EtchRouteSegmentKind;
1346
+ source: EtchRouteSource;
1347
+ /** Id of the template record. Absent means inherited from the nearest ancestor. */
1348
+ templateId?: number;
1349
+ }
1350
+ /** A route to create with `routes.createAsync()`. */
1351
+ interface EtchRouteInput {
1352
+ segment: string;
1353
+ /** Defaults to the site root. */
1354
+ parentId?: string;
1355
+ /** Defaults to serving nothing; a missing list is empty. */
1356
+ source?: Partial<EtchRouteSource>;
1357
+ templateId?: number;
1358
+ }
1359
+ /** Fields of an {@link EtchRoute} that `routes.updateAsync()` can change. */
1360
+ interface EtchRoutePatch {
1361
+ /** Renames the route; everything beneath it moves with it. */
1362
+ segment?: string;
1363
+ /** Moves the route (and its subtree) inside another route. */
1364
+ parentId?: string;
1365
+ /** Replaces what the route serves, whole. A missing list is empty. */
1366
+ source?: Partial<EtchRouteSource>;
1367
+ /** `null` clears the template, so the route inherits again. */
1368
+ templateId?: number | null;
1369
+ }
1370
+ /**
1371
+ * A URL the tree resolves to, and what the build does with it: publishes a
1372
+ * record there, loses it to a more specific route (`shadowed`), has two records
1373
+ * fighting for it (`conflict`), or refuses the segment for a record (`invalid`).
1374
+ */
1375
+ interface EtchRoutedPath {
1376
+ routeId: string;
1377
+ /** The record served there. Absent for a static route that only holds its path. */
1378
+ postId?: number;
1379
+ /** The record's title. */
1380
+ title?: string;
1381
+ /** The URL path; the route as written when `status` is `invalid`. */
1382
+ path: string;
1383
+ status: 'published' | 'shadowed' | 'conflict' | 'invalid';
1384
+ /** Why it is not published. */
1385
+ reason?: string;
1386
+ }
1387
+ /**
1388
+ * The project's routes. Writes persist to the backend immediately — they do
1389
+ * not wait for {@link Etch.saveAsync}.
1390
+ */
1391
+ interface EtchRoutesApi {
1392
+ /** Every route, the site root included. */
1393
+ listAsync(): Promise<EtchRoute[]>;
1394
+ /**
1395
+ * Every URL the tree resolves to, as the build would publish it. Read this
1396
+ * after a change to see where records actually land.
1397
+ */
1398
+ getPathsAsync(): Promise<EtchRoutedPath[]>;
1399
+ /**
1400
+ * Add a route.
1401
+ * @throws {EtchApiError} `INVALID_ARGUMENT` on a malformed segment, a source
1402
+ * its kind forbids, a name a sibling already has, an unknown content type, or
1403
+ * a template that is not a template record.
1404
+ * @throws {EtchApiError} `ROUTE_NOT_FOUND` when `parentId` names no route.
1405
+ */
1406
+ createAsync(route: EtchRouteInput): Promise<EtchRoute>;
1407
+ /**
1408
+ * Change a route. Only the fields present are applied.
1409
+ * @throws {EtchApiError} `ROUTE_NOT_FOUND`
1410
+ * @throws {EtchApiError} `INVALID_ARGUMENT` — as `createAsync`, or a move
1411
+ * that would put a route inside itself.
1412
+ */
1413
+ updateAsync(routeId: string, patch: EtchRoutePatch): Promise<EtchRoute>;
1414
+ /**
1415
+ * Delete a route and every route beneath it.
1416
+ * @throws {EtchApiError} `ROUTE_NOT_FOUND`
1417
+ * @throws {EtchApiError} `INVALID_ARGUMENT` for the site root.
1418
+ */
1419
+ deleteAsync(routeId: string): Promise<void>;
1420
+ }
1421
+
1201
1422
  /**
1202
1423
  * Custom field group/value shapes and the `etch.fields` API surface.
1203
1424
  */
@@ -1546,6 +1767,24 @@ interface Etch {
1546
1767
  blocks: EtchBlocksApi;
1547
1768
  /** Loop definitions and binding loops to blocks. */
1548
1769
  loops: EtchLoopsApi;
1770
+ /**
1771
+ * Project data sources — the `dataSources` capability's model of dynamic
1772
+ * data, which supersedes {@link loops}.
1773
+ *
1774
+ * Optional: a runtime that does not back the capability omits the namespace
1775
+ * entirely rather than stubbing it, so check
1776
+ * `environment.capabilities.dataSources === true` (or the presence of this
1777
+ * property) before calling it.
1778
+ */
1779
+ dataSources?: EtchDataSourcesApi;
1780
+ /**
1781
+ * The site's route tree, and which template renders each route's posts.
1782
+ *
1783
+ * Optional: a runtime that does not back the `routes` capability omits the
1784
+ * namespace, so check `environment.capabilities.routes === true` (or the
1785
+ * presence of this property) before calling it.
1786
+ */
1787
+ routes?: EtchRoutesApi;
1549
1788
  /** Global style (CSS) definitions and CSS variables. */
1550
1789
  styles: EtchStylesApi;
1551
1790
  /** Global stylesheets and `@custom-media` definitions. */
@@ -1597,7 +1836,7 @@ interface Etch {
1597
1836
  readonly version: string;
1598
1837
  /**
1599
1838
  * What this runtime is and which optional surfaces it backs. Read this
1600
- * before using `loops`, `fields`, templates or WordPress media, and before
1839
+ * before using `loops`, `fields`, `routes`, templates or WordPress media, and before
1601
1840
  * authoring a block type that may not exist here.
1602
1841
  *
1603
1842
  * Optional: runtimes older than the descriptor omit it. Treat a missing
@@ -1650,7 +1889,7 @@ declare function getEtch(options?: ConnectOptions): Etch;
1650
1889
  * runtimes still type-check as `EtchApiErrorCode` while keeping autocomplete for
1651
1890
  * the known values.
1652
1891
  */
1653
- type EtchApiErrorCode = 'BLOCK_NOT_FOUND' | 'WRONG_BLOCK_TYPE' | 'READONLY' | 'INVALID_ARGUMENT' | 'LOOP_NOT_FOUND' | 'STYLE_NOT_FOUND' | 'STYLESHEET_NOT_FOUND' | 'COMPONENT_NOT_FOUND' | 'POST_NOT_FOUND' | 'OPERATION_FAILED' | 'NOT_AVAILABLE' | (string & {});
1892
+ type EtchApiErrorCode = 'BLOCK_NOT_FOUND' | 'WRONG_BLOCK_TYPE' | 'READONLY' | 'INVALID_ARGUMENT' | 'LOOP_NOT_FOUND' | 'DATA_SOURCE_NOT_FOUND' | 'STYLE_NOT_FOUND' | 'STYLESHEET_NOT_FOUND' | 'COMPONENT_NOT_FOUND' | 'POST_NOT_FOUND' | 'ROUTE_NOT_FOUND' | 'OPERATION_FAILED' | 'NOT_AVAILABLE' | (string & {});
1654
1893
  /** Error thrown by the public Etch API (and by this client). */
1655
1894
  declare class EtchApiError extends Error {
1656
1895
  readonly code: EtchApiErrorCode;
@@ -1669,4 +1908,4 @@ declare function isEtchApiError(value: unknown): value is EtchApiError;
1669
1908
  */
1670
1909
  declare const ETCH_API_VERSION = "0.x";
1671
1910
 
1672
- export { type ArrayComponentProperty, type BlockLoopBinding, type BlockPatch, type BooleanComponentProperty, type BooleanParam, type BridgeConnectOptions, type ClassComponentProperty, type ColorScheme, type ComponentPatch, type ComponentProperty, type ComponentPropertyBase, type ConditionComponentProperty, type ConnectOptions, type CopyObject, type CustomField, type CustomFieldAssignment, type CustomFieldGroup, type CustomFieldType, ETCH_API_VERSION, type Etch, type EtchAiApi, EtchApiError, type EtchApiErrorCode, type EtchBlockCommon, type EtchBlockContext, type EtchBlockJson, type EtchBlockOptions, type EtchBlockScript, type EtchBlockType, type EtchBlockTypeName, type EtchBlocksApi, type EtchBridgeConnection, type EtchCapabilities, type EtchCapability, type EtchComponentBlockJson, type EtchComponentsApi, type EtchConditionBlockJson, type EtchDynamicElementBlockJson, type EtchDynamicImageBlockJson, type EtchElementBlockJson, type EtchEnvironment, type EtchFieldsApi, type EtchHistoryApi, type EtchHtmlAttributes, type EtchLoop, type EtchLoopBlockJson, type EtchLoopConfig, type EtchLoopObj, type EtchLoopsApi, type EtchNavigationApi, type EtchPassthroughBlockJson, type EtchPostContentBlockJson, type EtchProduct, type EtchRawHtmlBlockJson, type EtchRenderBlockJson, type EtchSkillsApi, type EtchSlotContentBlockJson, type EtchSlotPlaceholderBlockJson, type EtchStylesApi, type EtchStylesheetsApi, type EtchSvgBlockJson, type EtchTextBlockJson, type EtchUiApi, type ExternalAiState, type FindBlocksPredicate, type GroupComponentProperty, type GutenbergBlock, type LoopParamRef, type MetaQueryItem, type NavigationPlace, type NumberComponentProperty, type NumericParam, type ObjectComponentProperty, type PostCustomFieldGroupEntry, type PostCustomFieldValueEntry, type PostCustomFieldValueResponse, type PostCustomFieldValuesResponse, type PostSummary, type PublicBlockJson, type PublicComponentJson, type PublicComponentSummary, type RepeaterComponentProperty, type ResolvedCustomField, type SelectOptionsString, type SkillDetail, type SkillSummary, type StringComponentProperty, type StyleListFilter, type StylePatch, type StyleSelectorType, type StyleSummary, type StylesheetInput, type StylesheetPatch, type StylesheetSummary, type StylesheetType, type TaxQueryItem, type TemplateSummary, type WpQueryArgs, type WpTermsArgs, type WpUsersArgs, getEtch, isEtchApiError, isEtchAvailable };
1911
+ export { type ArrayComponentProperty, type BlockLoopBinding, type BlockPatch, type BooleanComponentProperty, type BooleanParam, type BridgeConnectOptions, type ClassComponentProperty, type ColorScheme, type ComponentPatch, type ComponentProperty, type ComponentPropertyBase, type ConditionComponentProperty, type ConnectOptions, type CopyObject, type CustomField, type CustomFieldAssignment, type CustomFieldGroup, type CustomFieldType, ETCH_API_VERSION, type Etch, type EtchAiApi, EtchApiError, type EtchApiErrorCode, type EtchBlockCommon, type EtchBlockContext, type EtchBlockJson, type EtchBlockOptions, type EtchBlockScript, type EtchBlockType, type EtchBlockTypeName, type EtchBlocksApi, type EtchBridgeConnection, type EtchCapabilities, type EtchCapability, type EtchComponentBlockJson, type EtchComponentsApi, type EtchConditionBlockJson, type EtchDataSource, type EtchDataSourcePatch, type EtchDataSourceType, type EtchDataSourcesApi, type EtchDynamicElementBlockJson, type EtchDynamicImageBlockJson, type EtchElementBlockJson, type EtchEnvironment, type EtchFieldsApi, type EtchHistoryApi, type EtchHtmlAttributes, type EtchLoop, type EtchLoopBlockJson, type EtchLoopConfig, type EtchLoopObj, type EtchLoopsApi, type EtchNavigationApi, type EtchPassthroughBlockJson, type EtchPostContentBlockJson, type EtchProduct, type EtchRawHtmlBlockJson, type EtchRenderBlockJson, type EtchRoute, type EtchRouteInput, type EtchRoutePatch, type EtchRouteSegmentKind, type EtchRouteSource, type EtchRoutedPath, type EtchRoutesApi, type EtchSkillsApi, type EtchSlotContentBlockJson, type EtchSlotPlaceholderBlockJson, type EtchStylesApi, type EtchStylesheetsApi, type EtchSvgBlockJson, type EtchTextBlockJson, type EtchUiApi, type ExternalAiState, type FindBlocksPredicate, type GroupComponentProperty, type GutenbergBlock, type LoopParamRef, type MetaQueryItem, type NavigationPlace, type NumberComponentProperty, type NumericParam, type ObjectComponentProperty, type PostCustomFieldGroupEntry, type PostCustomFieldValueEntry, type PostCustomFieldValueResponse, type PostCustomFieldValuesResponse, type PostSummary, type PublicBlockJson, type PublicComponentJson, type PublicComponentSummary, type RepeaterComponentProperty, type ResolvedCustomField, type SelectOptionsString, type SkillDetail, type SkillSummary, type StringComponentProperty, type StyleListFilter, type StylePatch, type StyleSelectorType, type StyleSummary, type StylesheetInput, type StylesheetPatch, type StylesheetSummary, type StylesheetType, type TaxQueryItem, type TemplateSummary, type WpQueryArgs, type WpTermsArgs, type WpUsersArgs, getEtch, isEtchApiError, isEtchAvailable };
package/dist/index.d.ts CHANGED
@@ -717,12 +717,16 @@ type EtchProduct = 'etch-wp' | 'etch-studio' | (string & {});
717
717
  *
718
718
  * - `fields` — `etch.fields.*` is backed by a real field store.
719
719
  * - `templates` — template posts exist and `navigation.goTo('templates')` works.
720
+ * On Studio a template post is a record of a template content type, and
721
+ * `goTo('templates')` opens the content hub, where they live.
722
+ * - `routes` — `etch.routes.*` is backed: the site's URLs are a route tree the
723
+ * author draws, and each route picks a template for the records it serves.
720
724
  * - `wpMedia` — media ids resolve against the **WordPress media library**, so a
721
725
  * URL can be turned into an id through `/wp/v2/media`. Without it the ids are
722
726
  * the product's own asset store instead. Either way an `etch/dynamic-image`
723
727
  * block binds the same: a numeric `mediaId` attribute.
724
728
  */
725
- type EtchCapability = 'loops' | 'dataSources' | 'fields' | 'templates' | 'wpMedia';
729
+ type EtchCapability = 'loops' | 'dataSources' | 'fields' | 'templates' | 'wpMedia' | 'routes';
726
730
  /**
727
731
  * Capability map. Every key is optional on purpose: a runtime older than a
728
732
  * capability's introduction simply omits it, so **absent means unavailable**.
@@ -747,6 +751,101 @@ interface EtchEnvironment {
747
751
  readonly blockTypes: readonly string[];
748
752
  }
749
753
 
754
+ /**
755
+ * Project data sources and the `etch.dataSources` API surface.
756
+ *
757
+ * Data sources are the `dataSources` capability's model of dynamic data, and
758
+ * they supersede {@link EtchLoopsApi | loops}: a named, typed source is defined
759
+ * once for the project and read anywhere an expression is accepted, through
760
+ * `data('key')`. A loop block points its `target` at one of those expressions
761
+ * rather than binding to a loop id.
762
+ *
763
+ * Check `etch.environment.capabilities.dataSources === true` before using this
764
+ * namespace — a runtime without it omits the whole thing.
765
+ */
766
+ /**
767
+ * A source type. The three built-ins are listed; the registry behind the
768
+ * runtime is open, so a host may serve types this contract has never heard of.
769
+ */
770
+ type EtchDataSourceType = 'json' | 'api' | 'js' | (string & {});
771
+ /**
772
+ * A project data source, identified by its `key` — the name `data('key')`
773
+ * calls it by.
774
+ */
775
+ interface EtchDataSource {
776
+ /**
777
+ * Stable identifier used in expressions: `data('products')`. Unique within
778
+ * the project, and part of every authored expression that reads the source,
779
+ * so renaming one breaks those expressions.
780
+ */
781
+ key: string;
782
+ /** Display name shown in the data manager. */
783
+ name: string;
784
+ /** Which kind of source this is; decides how `metadata` is read. */
785
+ type: EtchDataSourceType;
786
+ /**
787
+ * Type-specific configuration. The built-in types read:
788
+ *
789
+ * - `json` — `{ data: unknown }`, the payload served verbatim.
790
+ * - `api` — `{ url: string; method?: string; headers?: [string, string][] }`,
791
+ * fetched and parsed as JSON. A non-OK response fails the source.
792
+ * - `js` — `{ code: string }`, a function body run in a sandbox. Arguments
793
+ * passed by the expression (`data('rows', 5)`) arrive as `args`.
794
+ */
795
+ metadata: Record<string, unknown>;
796
+ }
797
+ /** Fields of an {@link EtchDataSource} that `updateAsync` can change. */
798
+ type EtchDataSourcePatch = Partial<EtchDataSource>;
799
+ /**
800
+ * Project data sources: definitions, and resolving one to its value.
801
+ *
802
+ * Reads are synchronous — the project's sources are loaded before `window.etch`
803
+ * exists. Writes persist to the backend immediately (they do not wait for
804
+ * {@link Etch.saveAsync}) and drop any value already resolved from the source,
805
+ * so the canvas re-reads it.
806
+ */
807
+ interface EtchDataSourcesApi {
808
+ /** Every data source defined for this project. */
809
+ list(): EtchDataSource[];
810
+ /**
811
+ * One data source by key.
812
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
813
+ */
814
+ get(key: string): EtchDataSource;
815
+ /**
816
+ * Fuzzy-search data sources by `name` or `key`, ranked best-first. Returns
817
+ * an empty array for a blank query or when nothing matches.
818
+ */
819
+ findDataSource(query: string): EtchDataSource[];
820
+ /**
821
+ * Define a new data source.
822
+ * @throws {EtchApiError} `INVALID_ARGUMENT` on a malformed source, or when
823
+ * the key is already taken.
824
+ */
825
+ createAsync(source: EtchDataSource): Promise<EtchDataSource>;
826
+ /**
827
+ * Change an existing data source. Only the fields present are applied.
828
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
829
+ */
830
+ updateAsync(key: string, patch: EtchDataSourcePatch): Promise<EtchDataSource>;
831
+ /**
832
+ * Delete a data source. Expressions still naming it resolve to `undefined`.
833
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
834
+ */
835
+ deleteAsync(key: string): Promise<void>;
836
+ /**
837
+ * Resolve a data source to its value, exactly as `data('key', ...args)`
838
+ * would on the canvas — read this before authoring the property paths that
839
+ * depend on its shape.
840
+ *
841
+ * A source that fails resolves to `undefined` rather than rejecting, which
842
+ * is what the render sees too.
843
+ *
844
+ * @throws {EtchApiError} `DATA_SOURCE_NOT_FOUND`
845
+ */
846
+ resolveAsync(key: string, ...args: unknown[]): Promise<unknown>;
847
+ }
848
+
750
849
  /**
751
850
  * Global style (CSS rule) shapes and the `etch.styles` API surface.
752
851
  */
@@ -1151,9 +1250,11 @@ interface EtchComponentsApi {
1151
1250
  * - `content-hub` — the pages/posts browser
1152
1251
  * - `style-manager` — the global style manager
1153
1252
  * - `loop-manager` — the loop manager
1253
+ * - `data-manager` — the data source manager
1154
1254
  * - `asset-manager` — the asset (media) library
1255
+ * - `routes` — the routes manager
1155
1256
  */
1156
- type NavigationPlace = 'builder' | 'templates' | 'style-manager' | 'content-hub' | 'loop-manager' | 'data-manager' | 'asset-manager';
1257
+ type NavigationPlace = 'builder' | 'templates' | 'style-manager' | 'content-hub' | 'loop-manager' | 'data-manager' | 'asset-manager' | 'routes';
1157
1258
  /** Lightweight post entry returned by `navigation.listPostsAsync()`. */
1158
1259
  interface PostSummary {
1159
1260
  /** Post id. */
@@ -1198,6 +1299,126 @@ interface EtchNavigationApi {
1198
1299
  listTemplatesAsync(): Promise<TemplateSummary[]>;
1199
1300
  }
1200
1301
 
1302
+ /**
1303
+ * The site's route tree and the `etch.routes` API surface.
1304
+ *
1305
+ * Routes are a tree of segments hanging from the site root; a route's URL is
1306
+ * its ancestors' segments joined by `/`. What a segment is depends on how it is
1307
+ * written:
1308
+ *
1309
+ * - `''` — the site root. Exactly one, never created or deleted.
1310
+ * - `about` — static: lowercase letters and digits joined by `-` or `_`.
1311
+ * Serves at most one record.
1312
+ * - `(marketing)` — a group: nests routes without adding to the URL. Serves
1313
+ * nothing itself.
1314
+ * - `{this.title.toSlug()}` — dynamic: resolved once per record it serves,
1315
+ * from that record's fields. Serves any mix of records and whole content
1316
+ * types. `this.slug` and `this.isHomepage` cannot be used — the routes
1317
+ * decide them.
1318
+ *
1319
+ * Records are what `navigation.listPostsAsync()` lists, and their content type
1320
+ * is its `postType` — so the fields below keep the `post` names. Templates are
1321
+ * records of a template content type (`navigation.listTemplatesAsync()`). A
1322
+ * route renders the records it serves through its template, or the nearest
1323
+ * ancestor's when it has none.
1324
+ *
1325
+ * Check `etch.environment.capabilities.routes === true` before using this
1326
+ * namespace — a runtime without it omits the whole thing.
1327
+ */
1328
+ /** What a route is, read off its segment. */
1329
+ type EtchRouteSegmentKind = 'root' | 'static' | 'group' | 'dynamic';
1330
+ /** What a route serves: these records, plus every record of these content types. */
1331
+ interface EtchRouteSource {
1332
+ /** Record ids, as `navigation.listPostsAsync()` returns them. */
1333
+ postIds: number[];
1334
+ /** Content type keys — `PostSummary.postType`. Dynamic routes only. */
1335
+ postTypes: string[];
1336
+ }
1337
+ /** One node of the route tree. */
1338
+ interface EtchRoute {
1339
+ /** Opaque string id. */
1340
+ id: string;
1341
+ /** The route this one sits inside. Absent only on the site root. */
1342
+ parentId?: string;
1343
+ /** This route's own part of the URL — see the module docs for the grammar. */
1344
+ segment: string;
1345
+ kind: EtchRouteSegmentKind;
1346
+ source: EtchRouteSource;
1347
+ /** Id of the template record. Absent means inherited from the nearest ancestor. */
1348
+ templateId?: number;
1349
+ }
1350
+ /** A route to create with `routes.createAsync()`. */
1351
+ interface EtchRouteInput {
1352
+ segment: string;
1353
+ /** Defaults to the site root. */
1354
+ parentId?: string;
1355
+ /** Defaults to serving nothing; a missing list is empty. */
1356
+ source?: Partial<EtchRouteSource>;
1357
+ templateId?: number;
1358
+ }
1359
+ /** Fields of an {@link EtchRoute} that `routes.updateAsync()` can change. */
1360
+ interface EtchRoutePatch {
1361
+ /** Renames the route; everything beneath it moves with it. */
1362
+ segment?: string;
1363
+ /** Moves the route (and its subtree) inside another route. */
1364
+ parentId?: string;
1365
+ /** Replaces what the route serves, whole. A missing list is empty. */
1366
+ source?: Partial<EtchRouteSource>;
1367
+ /** `null` clears the template, so the route inherits again. */
1368
+ templateId?: number | null;
1369
+ }
1370
+ /**
1371
+ * A URL the tree resolves to, and what the build does with it: publishes a
1372
+ * record there, loses it to a more specific route (`shadowed`), has two records
1373
+ * fighting for it (`conflict`), or refuses the segment for a record (`invalid`).
1374
+ */
1375
+ interface EtchRoutedPath {
1376
+ routeId: string;
1377
+ /** The record served there. Absent for a static route that only holds its path. */
1378
+ postId?: number;
1379
+ /** The record's title. */
1380
+ title?: string;
1381
+ /** The URL path; the route as written when `status` is `invalid`. */
1382
+ path: string;
1383
+ status: 'published' | 'shadowed' | 'conflict' | 'invalid';
1384
+ /** Why it is not published. */
1385
+ reason?: string;
1386
+ }
1387
+ /**
1388
+ * The project's routes. Writes persist to the backend immediately — they do
1389
+ * not wait for {@link Etch.saveAsync}.
1390
+ */
1391
+ interface EtchRoutesApi {
1392
+ /** Every route, the site root included. */
1393
+ listAsync(): Promise<EtchRoute[]>;
1394
+ /**
1395
+ * Every URL the tree resolves to, as the build would publish it. Read this
1396
+ * after a change to see where records actually land.
1397
+ */
1398
+ getPathsAsync(): Promise<EtchRoutedPath[]>;
1399
+ /**
1400
+ * Add a route.
1401
+ * @throws {EtchApiError} `INVALID_ARGUMENT` on a malformed segment, a source
1402
+ * its kind forbids, a name a sibling already has, an unknown content type, or
1403
+ * a template that is not a template record.
1404
+ * @throws {EtchApiError} `ROUTE_NOT_FOUND` when `parentId` names no route.
1405
+ */
1406
+ createAsync(route: EtchRouteInput): Promise<EtchRoute>;
1407
+ /**
1408
+ * Change a route. Only the fields present are applied.
1409
+ * @throws {EtchApiError} `ROUTE_NOT_FOUND`
1410
+ * @throws {EtchApiError} `INVALID_ARGUMENT` — as `createAsync`, or a move
1411
+ * that would put a route inside itself.
1412
+ */
1413
+ updateAsync(routeId: string, patch: EtchRoutePatch): Promise<EtchRoute>;
1414
+ /**
1415
+ * Delete a route and every route beneath it.
1416
+ * @throws {EtchApiError} `ROUTE_NOT_FOUND`
1417
+ * @throws {EtchApiError} `INVALID_ARGUMENT` for the site root.
1418
+ */
1419
+ deleteAsync(routeId: string): Promise<void>;
1420
+ }
1421
+
1201
1422
  /**
1202
1423
  * Custom field group/value shapes and the `etch.fields` API surface.
1203
1424
  */
@@ -1546,6 +1767,24 @@ interface Etch {
1546
1767
  blocks: EtchBlocksApi;
1547
1768
  /** Loop definitions and binding loops to blocks. */
1548
1769
  loops: EtchLoopsApi;
1770
+ /**
1771
+ * Project data sources — the `dataSources` capability's model of dynamic
1772
+ * data, which supersedes {@link loops}.
1773
+ *
1774
+ * Optional: a runtime that does not back the capability omits the namespace
1775
+ * entirely rather than stubbing it, so check
1776
+ * `environment.capabilities.dataSources === true` (or the presence of this
1777
+ * property) before calling it.
1778
+ */
1779
+ dataSources?: EtchDataSourcesApi;
1780
+ /**
1781
+ * The site's route tree, and which template renders each route's posts.
1782
+ *
1783
+ * Optional: a runtime that does not back the `routes` capability omits the
1784
+ * namespace, so check `environment.capabilities.routes === true` (or the
1785
+ * presence of this property) before calling it.
1786
+ */
1787
+ routes?: EtchRoutesApi;
1549
1788
  /** Global style (CSS) definitions and CSS variables. */
1550
1789
  styles: EtchStylesApi;
1551
1790
  /** Global stylesheets and `@custom-media` definitions. */
@@ -1597,7 +1836,7 @@ interface Etch {
1597
1836
  readonly version: string;
1598
1837
  /**
1599
1838
  * What this runtime is and which optional surfaces it backs. Read this
1600
- * before using `loops`, `fields`, templates or WordPress media, and before
1839
+ * before using `loops`, `fields`, `routes`, templates or WordPress media, and before
1601
1840
  * authoring a block type that may not exist here.
1602
1841
  *
1603
1842
  * Optional: runtimes older than the descriptor omit it. Treat a missing
@@ -1650,7 +1889,7 @@ declare function getEtch(options?: ConnectOptions): Etch;
1650
1889
  * runtimes still type-check as `EtchApiErrorCode` while keeping autocomplete for
1651
1890
  * the known values.
1652
1891
  */
1653
- type EtchApiErrorCode = 'BLOCK_NOT_FOUND' | 'WRONG_BLOCK_TYPE' | 'READONLY' | 'INVALID_ARGUMENT' | 'LOOP_NOT_FOUND' | 'STYLE_NOT_FOUND' | 'STYLESHEET_NOT_FOUND' | 'COMPONENT_NOT_FOUND' | 'POST_NOT_FOUND' | 'OPERATION_FAILED' | 'NOT_AVAILABLE' | (string & {});
1892
+ type EtchApiErrorCode = 'BLOCK_NOT_FOUND' | 'WRONG_BLOCK_TYPE' | 'READONLY' | 'INVALID_ARGUMENT' | 'LOOP_NOT_FOUND' | 'DATA_SOURCE_NOT_FOUND' | 'STYLE_NOT_FOUND' | 'STYLESHEET_NOT_FOUND' | 'COMPONENT_NOT_FOUND' | 'POST_NOT_FOUND' | 'ROUTE_NOT_FOUND' | 'OPERATION_FAILED' | 'NOT_AVAILABLE' | (string & {});
1654
1893
  /** Error thrown by the public Etch API (and by this client). */
1655
1894
  declare class EtchApiError extends Error {
1656
1895
  readonly code: EtchApiErrorCode;
@@ -1669,4 +1908,4 @@ declare function isEtchApiError(value: unknown): value is EtchApiError;
1669
1908
  */
1670
1909
  declare const ETCH_API_VERSION = "0.x";
1671
1910
 
1672
- export { type ArrayComponentProperty, type BlockLoopBinding, type BlockPatch, type BooleanComponentProperty, type BooleanParam, type BridgeConnectOptions, type ClassComponentProperty, type ColorScheme, type ComponentPatch, type ComponentProperty, type ComponentPropertyBase, type ConditionComponentProperty, type ConnectOptions, type CopyObject, type CustomField, type CustomFieldAssignment, type CustomFieldGroup, type CustomFieldType, ETCH_API_VERSION, type Etch, type EtchAiApi, EtchApiError, type EtchApiErrorCode, type EtchBlockCommon, type EtchBlockContext, type EtchBlockJson, type EtchBlockOptions, type EtchBlockScript, type EtchBlockType, type EtchBlockTypeName, type EtchBlocksApi, type EtchBridgeConnection, type EtchCapabilities, type EtchCapability, type EtchComponentBlockJson, type EtchComponentsApi, type EtchConditionBlockJson, type EtchDynamicElementBlockJson, type EtchDynamicImageBlockJson, type EtchElementBlockJson, type EtchEnvironment, type EtchFieldsApi, type EtchHistoryApi, type EtchHtmlAttributes, type EtchLoop, type EtchLoopBlockJson, type EtchLoopConfig, type EtchLoopObj, type EtchLoopsApi, type EtchNavigationApi, type EtchPassthroughBlockJson, type EtchPostContentBlockJson, type EtchProduct, type EtchRawHtmlBlockJson, type EtchRenderBlockJson, type EtchSkillsApi, type EtchSlotContentBlockJson, type EtchSlotPlaceholderBlockJson, type EtchStylesApi, type EtchStylesheetsApi, type EtchSvgBlockJson, type EtchTextBlockJson, type EtchUiApi, type ExternalAiState, type FindBlocksPredicate, type GroupComponentProperty, type GutenbergBlock, type LoopParamRef, type MetaQueryItem, type NavigationPlace, type NumberComponentProperty, type NumericParam, type ObjectComponentProperty, type PostCustomFieldGroupEntry, type PostCustomFieldValueEntry, type PostCustomFieldValueResponse, type PostCustomFieldValuesResponse, type PostSummary, type PublicBlockJson, type PublicComponentJson, type PublicComponentSummary, type RepeaterComponentProperty, type ResolvedCustomField, type SelectOptionsString, type SkillDetail, type SkillSummary, type StringComponentProperty, type StyleListFilter, type StylePatch, type StyleSelectorType, type StyleSummary, type StylesheetInput, type StylesheetPatch, type StylesheetSummary, type StylesheetType, type TaxQueryItem, type TemplateSummary, type WpQueryArgs, type WpTermsArgs, type WpUsersArgs, getEtch, isEtchApiError, isEtchAvailable };
1911
+ export { type ArrayComponentProperty, type BlockLoopBinding, type BlockPatch, type BooleanComponentProperty, type BooleanParam, type BridgeConnectOptions, type ClassComponentProperty, type ColorScheme, type ComponentPatch, type ComponentProperty, type ComponentPropertyBase, type ConditionComponentProperty, type ConnectOptions, type CopyObject, type CustomField, type CustomFieldAssignment, type CustomFieldGroup, type CustomFieldType, ETCH_API_VERSION, type Etch, type EtchAiApi, EtchApiError, type EtchApiErrorCode, type EtchBlockCommon, type EtchBlockContext, type EtchBlockJson, type EtchBlockOptions, type EtchBlockScript, type EtchBlockType, type EtchBlockTypeName, type EtchBlocksApi, type EtchBridgeConnection, type EtchCapabilities, type EtchCapability, type EtchComponentBlockJson, type EtchComponentsApi, type EtchConditionBlockJson, type EtchDataSource, type EtchDataSourcePatch, type EtchDataSourceType, type EtchDataSourcesApi, type EtchDynamicElementBlockJson, type EtchDynamicImageBlockJson, type EtchElementBlockJson, type EtchEnvironment, type EtchFieldsApi, type EtchHistoryApi, type EtchHtmlAttributes, type EtchLoop, type EtchLoopBlockJson, type EtchLoopConfig, type EtchLoopObj, type EtchLoopsApi, type EtchNavigationApi, type EtchPassthroughBlockJson, type EtchPostContentBlockJson, type EtchProduct, type EtchRawHtmlBlockJson, type EtchRenderBlockJson, type EtchRoute, type EtchRouteInput, type EtchRoutePatch, type EtchRouteSegmentKind, type EtchRouteSource, type EtchRoutedPath, type EtchRoutesApi, type EtchSkillsApi, type EtchSlotContentBlockJson, type EtchSlotPlaceholderBlockJson, type EtchStylesApi, type EtchStylesheetsApi, type EtchSvgBlockJson, type EtchTextBlockJson, type EtchUiApi, type ExternalAiState, type FindBlocksPredicate, type GroupComponentProperty, type GutenbergBlock, type LoopParamRef, type MetaQueryItem, type NavigationPlace, type NumberComponentProperty, type NumericParam, type ObjectComponentProperty, type PostCustomFieldGroupEntry, type PostCustomFieldValueEntry, type PostCustomFieldValueResponse, type PostCustomFieldValuesResponse, type PostSummary, type PublicBlockJson, type PublicComponentJson, type PublicComponentSummary, type RepeaterComponentProperty, type ResolvedCustomField, type SelectOptionsString, type SkillDetail, type SkillSummary, type StringComponentProperty, type StyleListFilter, type StylePatch, type StyleSelectorType, type StyleSummary, type StylesheetInput, type StylesheetPatch, type StylesheetSummary, type StylesheetType, type TaxQueryItem, type TemplateSummary, type WpQueryArgs, type WpTermsArgs, type WpUsersArgs, getEtch, isEtchApiError, isEtchAvailable };
package/dist/index.js.map CHANGED
@@ -1 +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,OACC,KAAA,YAAiB,YAAA,IAChB,KAAA,EAAiB,IAAA,KAAS,cAAA;AAE7B;;;ACjCO,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;AACrD,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;AAAA,MACC,OAAA,CAAQ,UAAA;AAAA,MACR,KAAK,UAAA,IAAc;AAAA,KACpB;AAAA,EACD;AACA,EAAA,OAAO,IAAA;AACR","file":"index.js","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 (\n\t\tvalue instanceof EtchApiError ||\n\t\t(value as Error)?.name === 'EtchApiError'\n\t);\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\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(\n\t\t\toptions.apiVersion,\n\t\t\tetch.apiVersion ?? ETCH_API_VERSION\n\t\t);\n\t}\n\treturn etch;\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/version.ts","../src/client.ts"],"names":[],"mappings":";AA2BO,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,OACC,KAAA,YAAiB,YAAA,IAChB,KAAA,EAAiB,IAAA,KAAS,cAAA;AAE7B;;;ACnCO,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;AACrD,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;AAAA,MACC,OAAA,CAAQ,UAAA;AAAA,MACR,KAAK,UAAA,IAAc;AAAA,KACpB;AAAA,EACD;AACA,EAAA,OAAO,IAAA;AACR","file":"index.js","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| 'DATA_SOURCE_NOT_FOUND'\n\t| 'STYLE_NOT_FOUND'\n\t| 'STYLESHEET_NOT_FOUND'\n\t| 'COMPONENT_NOT_FOUND'\n\t| 'POST_NOT_FOUND'\n\t| 'ROUTE_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 (\n\t\tvalue instanceof EtchApiError ||\n\t\t(value as Error)?.name === 'EtchApiError'\n\t);\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\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(\n\t\t\toptions.apiVersion,\n\t\t\tetch.apiVersion ?? ETCH_API_VERSION\n\t\t);\n\t}\n\treturn etch;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digital-gravy/etch-public-api",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "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.",
5
5
  "license": "MIT",
6
6
  "repository": {