@shop-on/theme-contract 0.5.0 → 0.7.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.
Files changed (43) hide show
  1. package/dist/ai-files.d.ts +18 -0
  2. package/dist/ai-files.js +47 -0
  3. package/dist/block-allowlist.d.ts +27 -0
  4. package/dist/block-allowlist.js +21 -0
  5. package/dist/closest-scope.d.ts +51 -0
  6. package/dist/closest-scope.js +66 -0
  7. package/dist/editor-tools/apply.d.ts +85 -0
  8. package/dist/editor-tools/apply.js +544 -0
  9. package/dist/editor-tools/args.d.ts +153 -0
  10. package/dist/editor-tools/args.js +145 -0
  11. package/dist/editor-tools/coerce.d.ts +43 -0
  12. package/dist/editor-tools/coerce.js +200 -0
  13. package/dist/editor-tools/digest.d.ts +75 -0
  14. package/dist/editor-tools/digest.js +99 -0
  15. package/dist/editor-tools/index.d.ts +21 -0
  16. package/dist/editor-tools/index.js +24 -0
  17. package/dist/editor-tools/recipes.d.ts +122 -0
  18. package/dist/editor-tools/recipes.js +192 -0
  19. package/dist/fonts.d.ts +9 -0
  20. package/dist/fonts.js +11 -0
  21. package/dist/google-fonts.generated.d.ts +7 -0
  22. package/dist/google-fonts.generated.js +753 -0
  23. package/dist/index.d.ts +14 -5
  24. package/dist/index.js +14 -5
  25. package/dist/link-url.d.ts +9 -0
  26. package/dist/link-url.js +33 -0
  27. package/dist/metafield-binding.d.ts +89 -9
  28. package/dist/metafield-binding.js +254 -43
  29. package/dist/ops.d.ts +126 -0
  30. package/dist/ops.js +453 -0
  31. package/dist/richtext.d.ts +23 -0
  32. package/dist/richtext.js +115 -1
  33. package/dist/schemas.d.ts +325 -0
  34. package/dist/schemas.js +57 -7
  35. package/dist/section-scope.d.ts +17 -0
  36. package/dist/section-scope.js +22 -0
  37. package/dist/store-path.d.ts +9 -0
  38. package/dist/store-path.js +13 -1
  39. package/dist/template-limits.d.ts +34 -0
  40. package/dist/template-limits.js +71 -0
  41. package/dist/video-url.d.ts +10 -0
  42. package/dist/video-url.js +36 -0
  43. package/package.json +8 -1
@@ -1,5 +1,6 @@
1
1
  import { walkBlocks, walkSections } from "./resolvers.js";
2
2
  import { metaobjectIdFromGid } from "./metaobject-gid.js";
3
+ import { stripInlineTokenSentinels } from "./richtext.js";
3
4
  //#region src/metafield-binding.ts
4
5
  const metafieldTypeToSettingTypes = {
5
6
  SINGLE_LINE_TEXT: [
@@ -204,17 +205,47 @@ function applyWholeValueBindings(template, resolve, opts) {
204
205
  }
205
206
  }
206
207
  }
207
- /** Overlay resolved product-metafield values onto a template's settings — for
208
- * each `product_metafield` binding, `metafields["namespace.key"]`. PDP-only (the
209
- * route product context); other sources are skipped. See applyWholeValueBindings. */
208
+ const ROUTE_METAFIELD_SOURCES = [
209
+ "collection_metafield",
210
+ "page_metafield",
211
+ "blog_metafield",
212
+ "article_metafield"
213
+ ];
214
+ /** The route resource each source reads — the key of that resource on the
215
+ * loader's request and the editor's template families. */
216
+ const ROUTE_METAFIELD_RESOURCE = {
217
+ collection_metafield: "collection",
218
+ page_metafield: "page",
219
+ blog_metafield: "blog",
220
+ article_metafield: "article"
221
+ };
222
+ const RESOURCE_METAFIELD_SOURCES = ["product_metafield", ...ROUTE_METAFIELD_SOURCES];
223
+ const RESOURCE_METAFIELD_SOURCE_SET = new Set(RESOURCE_METAFIELD_SOURCES);
224
+ /** True for a binding/descriptor that reads a route resource's metafield. */
225
+ function isResourceMetafieldSource(source) {
226
+ return RESOURCE_METAFIELD_SOURCE_SET.has(source);
227
+ }
228
+ /** True for a binding that reads a route resource's metafield. */
229
+ function isResourceMetafieldBinding(binding) {
230
+ return isResourceMetafieldSource(binding.source);
231
+ }
232
+ /** A resource-metafield binding's `{type,value}`: the metafield itself, or —
233
+ * with a `field` — one hop through its METAOBJECT_REFERENCE entry. */
234
+ function resolveResourceMetafield(binding, metafields, metaobjectFields) {
235
+ const mf = metafields[`${binding.namespace}.${binding.key}`];
236
+ if (binding.field === void 0) return mf ?? null;
237
+ return traverseMetaobjectReference(mf, binding.field, metaobjectFields);
238
+ }
239
+ /** Overlay resolved resource-metafield values onto a template's settings — for
240
+ * each binding of `opts.source` (default `product_metafield`),
241
+ * `metafields["namespace.key"]`. The caller passes the route resource's map
242
+ * (the PDP's product, or the collection, page, blog or article a template
243
+ * renders); other sources are
244
+ * skipped. See applyWholeValueBindings. */
210
245
  function applyMetafieldBindings(template, metafields, opts = {}) {
211
246
  const metaobjectFields = opts.metaobjectFields ?? {};
212
- applyWholeValueBindings(template, (binding) => {
213
- if (binding.source !== "product_metafield") return null;
214
- const mf = metafields[`${binding.namespace}.${binding.key}`];
215
- if (binding.field === void 0) return mf ?? null;
216
- return traverseMetaobjectReference(mf, binding.field, metaobjectFields);
217
- }, opts);
247
+ const source = opts.source ?? "product_metafield";
248
+ applyWholeValueBindings(template, (binding) => binding.source === source ? resolveResourceMetafield(binding, metafields, metaobjectFields) : null, opts);
218
249
  }
219
250
  /** Whole-value sibling for `metaobject_field` bindings — `metaobjectFields[entry][key]`.
220
251
  * Runs on every template (the entry is pinned, not the route product). */
@@ -309,11 +340,6 @@ function resolveBoundValue(binding, mf, serializeRichText, sinkType) {
309
340
  }
310
341
  const TOKEN_OPEN = "";
311
342
  const TOKEN_CLOSE = "";
312
- const INLINE_TOKEN_DELIMITERS = /[\uE000\uE001]/g;
313
- /** Strip inline-token sentinel delimiters from a string. */
314
- function stripInlineTokenSentinels(value) {
315
- return value.replace(INLINE_TOKEN_DELIMITERS, "");
316
- }
317
343
  const PRODUCT_ATTRIBUTE_KEYS = [
318
344
  "title",
319
345
  "vendor",
@@ -345,30 +371,49 @@ function metafieldTypeSupportsInlineToken(defType) {
345
371
  /** Serialize a source descriptor to its token-payload string. */
346
372
  function serializeSourceDescriptor(binding) {
347
373
  switch (binding.source) {
348
- case "product_metafield": return binding.field ? `product_metafield:${binding.namespace}:${binding.key}:${binding.field}` : `product_metafield:${binding.namespace}:${binding.key}`;
374
+ case "product_metafield":
375
+ case "collection_metafield":
376
+ case "page_metafield":
377
+ case "blog_metafield":
378
+ case "article_metafield": return binding.field ? `${binding.source}:${binding.namespace}:${binding.key}:${binding.field}` : `${binding.source}:${binding.namespace}:${binding.key}`;
349
379
  case "metaobject_field": return `metaobject_field:${binding.entry}:${binding.key}`;
350
380
  case "product_attribute": return `product_attribute:${binding.attribute}`;
351
381
  case "collection_attribute": return `collection_attribute:${binding.attribute}`;
352
382
  case "page_attribute": return `page_attribute:${binding.attribute}`;
353
383
  case "blog_attribute": return `blog_attribute:${binding.attribute}`;
354
384
  case "article_attribute": return `article_attribute:${binding.attribute}`;
385
+ case "closest_product_metafield":
386
+ case "closest_product_attribute":
387
+ case "closest_collection_attribute":
388
+ case "closest_page_attribute":
389
+ case "closest_blog_attribute":
390
+ case "closest_article_attribute": return `${CLOSEST_PREFIX}${serializeSourceDescriptor(routeTwin(binding))}`;
355
391
  default: throw new Error(`unknown source descriptor: ${JSON.stringify(binding)}`);
356
392
  }
357
393
  }
358
394
  /** Parse a token-payload string back to a descriptor, or null if malformed /
359
395
  * unknown source. */
360
396
  function parseSourceDescriptor(payload) {
397
+ if (payload.startsWith(CLOSEST_PREFIX)) {
398
+ const twin = parseSourceDescriptor(payload.slice(CLOSEST_PREFIX.length));
399
+ if (!twin || !(twin.source in CLOSEST_SOURCE_OF_TWIN)) return null;
400
+ if (twin.source === "product_metafield" && twin.field !== void 0) return null;
401
+ return {
402
+ ...twin,
403
+ source: CLOSEST_SOURCE_OF_TWIN[twin.source]
404
+ };
405
+ }
361
406
  const i = payload.indexOf(":");
362
407
  if (i === -1) return null;
363
408
  const source = payload.slice(0, i);
364
409
  const rest = payload.slice(i + 1);
365
- if (source === "product_metafield") {
410
+ if (isResourceMetafieldSource(source)) {
366
411
  const segments = rest.split(":");
367
412
  if (segments.length === 2) {
368
413
  const [namespace, key] = segments;
369
414
  if (!namespace || !key) return null;
370
415
  return {
371
- source: "product_metafield",
416
+ source,
372
417
  namespace,
373
418
  key
374
419
  };
@@ -377,7 +422,7 @@ function parseSourceDescriptor(payload) {
377
422
  const [namespace, key, field] = segments;
378
423
  if (!namespace || !key || !FIELD_KEY_RE.test(field)) return null;
379
424
  return {
380
- source: "product_metafield",
425
+ source,
381
426
  namespace,
382
427
  key,
383
428
  field
@@ -456,11 +501,14 @@ function splitInlineTokens(value) {
456
501
  * empty / undecodable (caller drops the token to ""). All sources for a value
457
502
  * are resolved in a SINGLE pass (see applyInlineSources), so a resolved value is
458
503
  * never re-scanned. */
459
- function resolveSource(descriptor, sources) {
504
+ function resolveSource(descriptor, sources, node) {
460
505
  switch (descriptor.source) {
461
- case "product_metafield": {
462
- const mf = sources.metafields[`${descriptor.namespace}.${descriptor.key}`];
463
- const field = descriptor.field === void 0 ? mf : traverseMetaobjectReference(mf, descriptor.field, sources.metaobjectFields ?? {});
506
+ case "product_metafield":
507
+ case "collection_metafield":
508
+ case "page_metafield":
509
+ case "blog_metafield":
510
+ case "article_metafield": {
511
+ const field = resolveResourceMetafield(descriptor, descriptor.source === "product_metafield" ? sources.metafields : sources.routeMetafields?.[descriptor.source] ?? {}, sources.metaobjectFields ?? {});
464
512
  const decoded = field ? decodeStorefrontMetafield(field.type, field.value) : null;
465
513
  return typeof decoded === "string" ? decoded : null;
466
514
  }
@@ -482,6 +530,17 @@ function resolveSource(descriptor, sources) {
482
530
  case "article_attribute":
483
531
  if (!ARTICLE_INLINE_ATTRIBUTES.has(descriptor.attribute)) return null;
484
532
  return trimmedOrNull(sources.articleAttributes?.[descriptor.attribute]);
533
+ case "closest_product_metafield":
534
+ case "closest_product_attribute":
535
+ case "closest_collection_attribute":
536
+ case "closest_page_attribute":
537
+ case "closest_blog_attribute":
538
+ case "closest_article_attribute": {
539
+ const resource = closestResourceOf(descriptor);
540
+ const item = sources.closest?.singleAt(node, resource);
541
+ if (!item) return null;
542
+ return resolveSource(routeTwin(descriptor), itemInlineSources(resource, item), node);
543
+ }
485
544
  default: return null;
486
545
  }
487
546
  }
@@ -504,16 +563,16 @@ function escapeInertHtmlText(value) {
504
563
  }
505
564
  const SENTINEL_RE = new RegExp(`${TOKEN_OPEN}([^${TOKEN_OPEN}${TOKEN_CLOSE}]*)${TOKEN_CLOSE}`, "g");
506
565
  const DYN_SPAN_RE = /<span\b[^>]*?\bdata-dyn="([^"]*)"[^>]*><\/span>/g;
507
- function substitute(payload, sources, escape) {
566
+ function substitute(payload, sources, escape, node) {
508
567
  const descriptor = parseSourceDescriptor(payload);
509
568
  if (!descriptor) return "";
510
- const resolved = resolveSource(descriptor, sources);
569
+ const resolved = resolveSource(descriptor, sources, node);
511
570
  if (resolved === null) return "";
512
571
  return escape ? escapeHtmlText(resolved) : resolved;
513
572
  }
514
- function interpolate(value, sources) {
515
- if (value.includes(TOKEN_OPEN)) return value.replace(SENTINEL_RE, (_m, payload) => substitute(payload, sources, false));
516
- if (value.includes("data-dyn=")) return value.replace(DYN_SPAN_RE, (_m, payload) => substitute(payload, sources, true));
573
+ function interpolate(value, sources, node) {
574
+ if (value.includes(TOKEN_OPEN)) return value.replace(SENTINEL_RE, (_m, payload) => substitute(payload, sources, false, node));
575
+ if (value.includes("data-dyn=")) return value.replace(DYN_SPAN_RE, (_m, payload) => substitute(payload, sources, true, node));
517
576
  return value;
518
577
  }
519
578
  /** Overlay inline source tokens in a template's string settings. Walks every
@@ -526,30 +585,174 @@ function applyInlineSources(template, sources) {
526
585
  for (const node of iterNodes(template)) {
527
586
  const settings = node.settings;
528
587
  if (!settings) continue;
588
+ const each = sources.closest?.eachAt(node) ?? null;
529
589
  for (const [id, val] of Object.entries(settings)) {
530
590
  if (typeof val !== "string") continue;
531
- const next = interpolate(val, sources);
591
+ const next = interpolate(val, sources, node);
532
592
  if (next !== val) settings[id] = next;
593
+ if (!each || each.items.length === 0) continue;
594
+ if (!val.includes(`${CLOSEST_PREFIX}${each.resource}_`)) continue;
595
+ const overlay = node.closestSettings ??= {};
596
+ for (const item of each.items) {
597
+ const scoped = {
598
+ ...sources,
599
+ closest: withSingleItem(sources.closest, each.resource, item.sources)
600
+ };
601
+ const into = Object.hasOwn(overlay, item.id) ? overlay[item.id] : overlay[item.id] = {};
602
+ into[id] = interpolate(val, scoped, node);
603
+ }
533
604
  }
534
605
  }
535
606
  }
607
+ const CLOSEST_PREFIX = "closest_";
608
+ const CLOSEST_SOURCE_OF_TWIN = {
609
+ product_metafield: "closest_product_metafield",
610
+ product_attribute: "closest_product_attribute",
611
+ collection_attribute: "closest_collection_attribute",
612
+ page_attribute: "closest_page_attribute",
613
+ blog_attribute: "closest_blog_attribute",
614
+ article_attribute: "closest_article_attribute"
615
+ };
616
+ const CLOSEST_ARMS = {
617
+ closest_product_metafield: {
618
+ twin: "product_metafield",
619
+ resource: "product"
620
+ },
621
+ closest_product_attribute: {
622
+ twin: "product_attribute",
623
+ resource: "product"
624
+ },
625
+ closest_collection_attribute: {
626
+ twin: "collection_attribute",
627
+ resource: "collection"
628
+ },
629
+ closest_page_attribute: {
630
+ twin: "page_attribute",
631
+ resource: "page"
632
+ },
633
+ closest_blog_attribute: {
634
+ twin: "blog_attribute",
635
+ resource: "blog"
636
+ },
637
+ closest_article_attribute: {
638
+ twin: "article_attribute",
639
+ resource: "article"
640
+ }
641
+ };
642
+ /** The route arm a closest arm mirrors (same payload, `source` swapped), or
643
+ * null for a binding that is not a closest arm. */
644
+ function routeTwin(binding) {
645
+ const arm = CLOSEST_ARMS[binding.source];
646
+ return arm ? {
647
+ ...binding,
648
+ source: arm.twin
649
+ } : null;
650
+ }
651
+ /** The resource a closest arm resolves against, or null for any other arm. */
652
+ function closestResourceOf(binding) {
653
+ return CLOSEST_ARMS[binding.source]?.resource ?? null;
654
+ }
655
+ /** The lookup with `resource` pinned to one item — the per-item overlay pass. */
656
+ function withSingleItem(base, resource, item) {
657
+ return {
658
+ singleAt: (node, r) => r === resource ? item : base?.singleAt(node, r) ?? null,
659
+ eachAt: () => null
660
+ };
661
+ }
662
+ /** The item's maps in the twin resolver's `InlineSources` slots, so the twin
663
+ * applies its own key gates (`COLLECTION_INLINE_ATTRIBUTES` and friends). */
664
+ function itemInlineSources(resource, item) {
665
+ switch (resource) {
666
+ case "product": return {
667
+ metafields: item.metafields ?? {},
668
+ attributes: item.attributes
669
+ };
670
+ case "collection": return {
671
+ metafields: {},
672
+ attributes: {},
673
+ collectionAttributes: item.attributes
674
+ };
675
+ case "page": return {
676
+ metafields: {},
677
+ attributes: {},
678
+ pageAttributes: item.attributes
679
+ };
680
+ case "blog": return {
681
+ metafields: {},
682
+ attributes: {},
683
+ blogAttributes: item.attributes
684
+ };
685
+ case "article": return {
686
+ metafields: {},
687
+ attributes: {},
688
+ articleAttributes: item.attributes
689
+ };
690
+ }
691
+ }
692
+ const CLOSEST_SINKS = /* @__PURE__ */ new Set([
693
+ "text",
694
+ "textarea",
695
+ "inline_richtext",
696
+ "richtext",
697
+ "color",
698
+ "url"
699
+ ]);
700
+ /** True when a setting of `settingType` may carry a closest binding. */
701
+ function isClosestSink(settingType) {
702
+ return CLOSEST_SINKS.has(settingType);
703
+ }
704
+ /** Resolve every whole-value `closest_<resource>_*` binding on ONE node against
705
+ * ONE item, writing into `into` (a per-item overlay) or, by default, the node's
706
+ * own settings (a single scope). Other resources' arms and the route twins are
707
+ * untouched. A known sink is required — the schema's (`sinkTypeAt`) or the
708
+ * stamp's — and it must be one of the text / color / url sinks; the product
709
+ * metafield then goes through `resolveBoundValue` exactly as its twin does,
710
+ * and the collection description through the same inert-HTML path as
711
+ * `applyCollectionAttributeBindings`. The attribute arms are inline-only. */
712
+ function applyClosestBindings(node, resource, item, opts = {}, into) {
713
+ const n = node;
714
+ if (!n.bindings) return;
715
+ let target = into;
716
+ for (const [settingId, binding] of Object.entries(n.bindings)) {
717
+ if (closestResourceOf(binding) !== resource) continue;
718
+ const twin = routeTwin(binding);
719
+ const sink = opts.sinkTypeAt?.(node, settingId) ?? binding.settingType;
720
+ if (!sink || !CLOSEST_SINKS.has(sink)) continue;
721
+ let resolved = null;
722
+ if (twin.source === "product_metafield") {
723
+ const mf = item.metafields?.[`${twin.namespace}.${twin.key}`];
724
+ if (mf) resolved = resolveBoundValue(twin, mf, opts.serializeRichText, sink);
725
+ } else if (twin.source === "collection_attribute" && twin.attribute === "description") {
726
+ const html = item.descriptionHtml;
727
+ if (sink === "richtext" && html && html.trim() !== "") resolved = inertBoundHtml(html);
728
+ }
729
+ if (resolved === null) continue;
730
+ target ??= n.settings ??= {};
731
+ target[settingId] = resolved;
732
+ }
733
+ }
536
734
  /** Every source descriptor referenced anywhere in the template — whole-value
537
735
  * `bindings` sidecars AND inline tokens (both the PUA-sentinel and the richtext
538
736
  * data-dyn span forms), yielded as parsed `SettingBinding`s. The single walk both
539
737
  * `collectMetaobjectEntryIds` and `collectTraversedEntryIds` share so they can't
540
738
  * drift on which nodes/token-forms count. A malformed inline payload is skipped. */
541
739
  function* iterSourceDescriptors(template) {
542
- for (const node of iterNodes(template)) {
543
- const n = node;
544
- if (n.bindings) for (const b of Object.values(n.bindings)) yield b;
545
- if (n.settings) for (const val of Object.values(n.settings)) {
546
- if (typeof val !== "string") continue;
547
- const forms = val.includes(TOKEN_OPEN) ? val.matchAll(SENTINEL_RE) : val.includes("data-dyn=") ? val.matchAll(DYN_SPAN_RE) : null;
548
- if (!forms) continue;
549
- for (const m of forms) {
550
- const d = parseSourceDescriptor(m[1]);
551
- if (d) yield d;
552
- }
740
+ for (const node of iterNodes(template)) yield* nodeSourceDescriptors(node);
741
+ }
742
+ /** Every source descriptor ONE node references — its `bindings` sidecar and
743
+ * the inline tokens in its string settings (both forms). The per-node half of
744
+ * `iterSourceDescriptors`, exported for the loader's closest-needs walk, which
745
+ * must attribute each descriptor to the node's own scope. */
746
+ function* nodeSourceDescriptors(node) {
747
+ const n = node;
748
+ if (n.bindings) for (const b of Object.values(n.bindings)) yield b;
749
+ if (n.settings) for (const val of Object.values(n.settings)) {
750
+ if (typeof val !== "string") continue;
751
+ const forms = val.includes(TOKEN_OPEN) ? val.matchAll(SENTINEL_RE) : val.includes("data-dyn=") ? val.matchAll(DYN_SPAN_RE) : null;
752
+ if (!forms) continue;
753
+ for (const m of forms) {
754
+ const d = parseSourceDescriptor(m[1]);
755
+ if (d) yield d;
553
756
  }
554
757
  }
555
758
  }
@@ -561,6 +764,13 @@ function collectMetaobjectEntryIds(template) {
561
764
  for (const d of iterSourceDescriptors(template)) if (d.source === "metaobject_field") ids.add(d.entry);
562
765
  return [...ids];
563
766
  }
767
+ /** True when the template references `source` anywhere — a `bindings` sidecar
768
+ * or an inline token of either form. The loader asks this before fetching a
769
+ * resource's metafields, so a template that binds none makes no request. */
770
+ function templateReferencesSource(template, source) {
771
+ for (const d of iterSourceDescriptors(template)) if (d.source === source) return true;
772
+ return false;
773
+ }
564
774
  /** Distinct numeric entry ids reached by field-bearing `product_metafield`
565
775
  * descriptors (bindings + both token forms). Unlike pinned entries these vary per
566
776
  * product, so the resolved `metafields` map (keyed `"ns.key"`, the shape
@@ -568,11 +778,12 @@ function collectMetaobjectEntryIds(template) {
568
778
  * dereference each METAOBJECT_REFERENCE value to its entry GID. Shares both
569
779
  * resolve-time gates (`FIELD_KEY_RE` + `metaobjectRefEntryId`) with
570
780
  * `traverseMetaobjectReference` so the fetched set is exactly the resolvable
571
- * set. Empty on non-PDP loads (no product). */
572
- function collectTraversedEntryIds(template, metafields) {
781
+ * set. `source` names whose map `metafields` is (default the route product's);
782
+ * empty when that resource is absent from the load. */
783
+ function collectTraversedEntryIds(template, metafields, source = "product_metafield") {
573
784
  const ids = /* @__PURE__ */ new Set();
574
785
  for (const d of iterSourceDescriptors(template)) {
575
- if (d.source !== "product_metafield" || d.field === void 0) continue;
786
+ if (d.source !== source || d.field === void 0) continue;
576
787
  if (!FIELD_KEY_RE.test(d.field)) continue;
577
788
  const entryId = metaobjectRefEntryId(metafields[`${d.namespace}.${d.key}`]);
578
789
  if (entryId !== null) ids.add(entryId);
@@ -584,4 +795,4 @@ function* iterNodes(template) {
584
795
  yield* walkBlocks(template);
585
796
  }
586
797
  //#endregion
587
- export { ARTICLE_ATTRIBUTE_KEYS, BLOG_ATTRIBUTE_KEYS, COLLECTION_ATTRIBUTE_KEYS, COLLECTION_INLINE_ATTRIBUTE_KEYS, PAGE_ATTRIBUTE_KEYS, PRODUCT_ATTRIBUTE_KEYS, applyCollectionAttributeBindings, applyInlineSources, applyMetafieldBindings, applyMetaobjectFieldBindings, collectCollectionAttributeBindings, collectMetaobjectEntryIds, collectTraversedEntryIds, decodeStorefrontMetafield, encodeSourceToken, escapeInertHtmlText, metafieldDefinitionFitsSetting, metafieldTypeSupportsInlineToken, metafieldTypeToSettingTypes, parseSourceDescriptor, serializeSourceDescriptor, settingTypeSupportsBinding, splitInlineTokens, stripInlineTokenSentinels };
798
+ export { ARTICLE_ATTRIBUTE_KEYS, BLOG_ATTRIBUTE_KEYS, COLLECTION_ATTRIBUTE_KEYS, COLLECTION_INLINE_ATTRIBUTE_KEYS, PAGE_ATTRIBUTE_KEYS, PRODUCT_ATTRIBUTE_KEYS, RESOURCE_METAFIELD_SOURCES, ROUTE_METAFIELD_RESOURCE, ROUTE_METAFIELD_SOURCES, applyClosestBindings, applyCollectionAttributeBindings, applyInlineSources, applyMetafieldBindings, applyMetaobjectFieldBindings, closestResourceOf, collectCollectionAttributeBindings, collectMetaobjectEntryIds, collectTraversedEntryIds, decodeStorefrontMetafield, encodeSourceToken, escapeInertHtmlText, isClosestSink, isResourceMetafieldBinding, isResourceMetafieldSource, metafieldDefinitionFitsSetting, metafieldTypeSupportsInlineToken, metafieldTypeToSettingTypes, nodeSourceDescriptors, parseSourceDescriptor, routeTwin, serializeSourceDescriptor, settingTypeSupportsBinding, splitInlineTokens, templateReferencesSource };
package/dist/ops.d.ts ADDED
@@ -0,0 +1,126 @@
1
+ import { TemplateSection } from "./schemas.js";
2
+ import { z } from "zod";
3
+ //#region src/ops.d.ts
4
+ /** One template or section group as the server holds it. */
5
+ export interface EntryDoc {
6
+ sections: Record<string, TemplateSection>;
7
+ order: string[];
8
+ [key: string]: unknown;
9
+ }
10
+ export interface ThemeDocument {
11
+ settings: Record<string, unknown>;
12
+ templates: Record<string, EntryDoc>;
13
+ groups: Record<string, EntryDoc>;
14
+ }
15
+ /**
16
+ * `["settings", id]`, or `["template" | "group", id, ...nodePath]` where the
17
+ * node path descends `sections` → `blocks` → `blocks`… An absent `v` deletes.
18
+ */
19
+ export type Op = {
20
+ p: string[];
21
+ v?: unknown;
22
+ };
23
+ export type OpReset = "publish" | "discard" | "upgrade" | "reseed";
24
+ /** One entry of the per-install op stream. */
25
+ export interface OpBatch {
26
+ seq: number;
27
+ clientId: string;
28
+ batchId: string;
29
+ userId: string;
30
+ ops: Op[];
31
+ reset?: OpReset;
32
+ }
33
+ /** The client id an MCP editor tool call writes its batches under. */
34
+ export declare const agentClientId: (sid: string, callId: string) => string;
35
+ /** Whether a batch came from an MCP editor tool call, not an editor tab. */
36
+ export declare const isAgentClientId: (clientId: string) => boolean;
37
+ export declare const MAX_PATH_DEPTH = 24;
38
+ export declare const MAX_SEGMENT_LENGTH = 64;
39
+ export declare const MAX_BATCH_OPS = 200;
40
+ export declare const MAX_BATCH_BYTES: number;
41
+ /** A node that holds ordered children: an entry's sections, or a section's or block's blocks. */
42
+ export type Container = {
43
+ path: string[];
44
+ childKey: "sections" | "blocks";
45
+ orderKey: "order" | "block_order";
46
+ };
47
+ export type PathShape = {
48
+ kind: "settings";
49
+ } | {
50
+ kind: "entry";
51
+ } | {
52
+ kind: "order";
53
+ container: Container;
54
+ } | {
55
+ kind: "child";
56
+ container: Container;
57
+ childId: string;
58
+ } | {
59
+ kind: "children";
60
+ container: Container;
61
+ } | {
62
+ kind: "field";
63
+ container: Container;
64
+ };
65
+ /** Where an op lands relative to the order-bearing containers it passes. */
66
+ export declare function shapeOf(p: string[]): PathShape;
67
+ /**
68
+ * Applies a batch copy-on-write along each op's path, then normalises every
69
+ * container the batch touched: drops order ids with no node, strips static
70
+ * block ids, and re-appends ids that were in that order when the batch began,
71
+ * still have a node and were not deleted by the batch. Ids never in the prior
72
+ * order stay out: presets hide content by omitting it from `order`.
73
+ */
74
+ export declare function applyOps<D extends ThemeDocument>(doc: D, ops: Op[]): D;
75
+ /**
76
+ * The ops that turn `prev` into `next`. Setting values, `order` and
77
+ * `block_order` are leaves (set whole); an added node is set whole, a removed
78
+ * one is one delete, a changed one is recursed. Short-circuits on `===`.
79
+ */
80
+ export declare function diffDocument(prev: ThemeDocument, next: ThemeDocument): Op[];
81
+ /** The ops that restore `prev` at exactly the paths `ops` touched. */
82
+ export declare function invertOps(prev: ThemeDocument, ops: Op[]): Op[];
83
+ /** Room for `clientId`, `batchNo`, `batchId` and the envelope. */
84
+ /** The entries a run of ops writes: each one a whole template or group. */
85
+ export interface TouchedEntries {
86
+ settings: boolean;
87
+ templates: Set<string>;
88
+ groups: Set<string>;
89
+ }
90
+ /** The entries `ops` write, added to `into` when given (a log read batch by
91
+ * batch). */
92
+ export declare function touchedEntries(ops: readonly Op[], into?: TouchedEntries): TouchedEntries;
93
+ /**
94
+ * Splits ops into batches within the op and byte caps, keeping their order:
95
+ * `diffDocument` emits a container's new nodes before its order, so an order
96
+ * op never lands in an earlier batch than its nodes. An op larger than the cap
97
+ * travels alone (and the server refuses it).
98
+ */
99
+ export declare function chunkOps(ops: Op[], maxBytes?: number, maxOps?: number): Op[][];
100
+ export declare const OpSchema: z.ZodObject<{
101
+ p: z.ZodArray<z.ZodString>;
102
+ v: z.ZodOptional<z.ZodUnknown>;
103
+ }, z.core.$strict>;
104
+ /** The body of `POST /draft/ops`. The byte cap is the route's body limit. */
105
+ export declare const OpBatchBodySchema: z.ZodObject<{
106
+ clientId: z.ZodString;
107
+ batchNo: z.ZodNumber;
108
+ batchId: z.ZodString;
109
+ ops: z.ZodArray<z.ZodObject<{
110
+ p: z.ZodArray<z.ZodString>;
111
+ v: z.ZodOptional<z.ZodUnknown>;
112
+ }, z.core.$strict>>;
113
+ }, z.core.$strict>;
114
+ export type OpBatchBody = z.infer<typeof OpBatchBodySchema>;
115
+ /** A batch an editor tab sends: the `mcp:` client id prefix marks an editor
116
+ * tool call's batches, and other tabs follow those, so a tab can't claim it. */
117
+ export declare const TabOpBatchBodySchema: z.ZodObject<{
118
+ clientId: z.ZodString;
119
+ batchNo: z.ZodNumber;
120
+ batchId: z.ZodString;
121
+ ops: z.ZodArray<z.ZodObject<{
122
+ p: z.ZodArray<z.ZodString>;
123
+ v: z.ZodOptional<z.ZodUnknown>;
124
+ }, z.core.$strict>>;
125
+ }, z.core.$strict>;
126
+ //#endregion