@shop-on/theme-contract 0.5.0 → 0.6.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/dist/ai-files.d.ts +18 -0
- package/dist/ai-files.js +47 -0
- package/dist/block-allowlist.d.ts +27 -0
- package/dist/block-allowlist.js +21 -0
- package/dist/closest-scope.d.ts +51 -0
- package/dist/closest-scope.js +66 -0
- package/dist/editor-tools/apply.d.ts +85 -0
- package/dist/editor-tools/apply.js +544 -0
- package/dist/editor-tools/args.d.ts +153 -0
- package/dist/editor-tools/args.js +145 -0
- package/dist/editor-tools/coerce.d.ts +43 -0
- package/dist/editor-tools/coerce.js +200 -0
- package/dist/editor-tools/digest.d.ts +75 -0
- package/dist/editor-tools/digest.js +99 -0
- package/dist/editor-tools/index.d.ts +21 -0
- package/dist/editor-tools/index.js +24 -0
- package/dist/editor-tools/recipes.d.ts +122 -0
- package/dist/editor-tools/recipes.js +192 -0
- package/dist/fonts.d.ts +9 -0
- package/dist/fonts.js +11 -0
- package/dist/google-fonts.generated.d.ts +7 -0
- package/dist/google-fonts.generated.js +753 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +14 -5
- package/dist/link-url.d.ts +9 -0
- package/dist/link-url.js +33 -0
- package/dist/metafield-binding.d.ts +89 -9
- package/dist/metafield-binding.js +254 -43
- package/dist/ops.d.ts +85 -0
- package/dist/ops.js +442 -0
- package/dist/richtext.d.ts +23 -0
- package/dist/richtext.js +115 -1
- package/dist/schemas.d.ts +325 -0
- package/dist/schemas.js +57 -7
- package/dist/section-scope.d.ts +17 -0
- package/dist/section-scope.js +22 -0
- package/dist/store-path.d.ts +9 -0
- package/dist/store-path.js +13 -1
- package/dist/template-limits.d.ts +34 -0
- package/dist/template-limits.js +71 -0
- package/dist/video-url.d.ts +10 -0
- package/dist/video-url.js +36 -0
- 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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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":
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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.
|
|
572
|
-
|
|
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 !==
|
|
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,
|
|
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,85 @@
|
|
|
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
|
+
export declare const MAX_PATH_DEPTH = 24;
|
|
34
|
+
export declare const MAX_SEGMENT_LENGTH = 64;
|
|
35
|
+
export declare const MAX_BATCH_OPS = 200;
|
|
36
|
+
export declare const MAX_BATCH_BYTES: number;
|
|
37
|
+
/**
|
|
38
|
+
* Applies a batch copy-on-write along each op's path, then normalises every
|
|
39
|
+
* container the batch touched: drops order ids with no node, strips static
|
|
40
|
+
* block ids, and re-appends ids that were in that order when the batch began,
|
|
41
|
+
* still have a node and were not deleted by the batch. Ids never in the prior
|
|
42
|
+
* order stay out: presets hide content by omitting it from `order`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function applyOps<D extends ThemeDocument>(doc: D, ops: Op[]): D;
|
|
45
|
+
/**
|
|
46
|
+
* The ops that turn `prev` into `next`. Setting values, `order` and
|
|
47
|
+
* `block_order` are leaves (set whole); an added node is set whole, a removed
|
|
48
|
+
* one is one delete, a changed one is recursed. Short-circuits on `===`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function diffDocument(prev: ThemeDocument, next: ThemeDocument): Op[];
|
|
51
|
+
/** The ops that restore `prev` at exactly the paths `ops` touched. */
|
|
52
|
+
export declare function invertOps(prev: ThemeDocument, ops: Op[]): Op[];
|
|
53
|
+
/** Room for `clientId`, `batchNo`, `batchId` and the envelope. */
|
|
54
|
+
/** The entries a run of ops writes: each one a whole template or group. */
|
|
55
|
+
export interface TouchedEntries {
|
|
56
|
+
settings: boolean;
|
|
57
|
+
templates: Set<string>;
|
|
58
|
+
groups: Set<string>;
|
|
59
|
+
}
|
|
60
|
+
/** The entries `ops` write, added to `into` when given (a log read batch by
|
|
61
|
+
* batch). */
|
|
62
|
+
export declare function touchedEntries(ops: readonly Op[], into?: TouchedEntries): TouchedEntries;
|
|
63
|
+
/**
|
|
64
|
+
* Splits ops into batches within the op and byte caps, keeping their order:
|
|
65
|
+
* `diffDocument` emits a container's new nodes before its order, so an order
|
|
66
|
+
* op never lands in an earlier batch than its nodes. An op larger than the cap
|
|
67
|
+
* travels alone (and the server refuses it).
|
|
68
|
+
*/
|
|
69
|
+
export declare function chunkOps(ops: Op[], maxBytes?: number, maxOps?: number): Op[][];
|
|
70
|
+
export declare const OpSchema: z.ZodObject<{
|
|
71
|
+
p: z.ZodArray<z.ZodString>;
|
|
72
|
+
v: z.ZodOptional<z.ZodUnknown>;
|
|
73
|
+
}, z.core.$strict>;
|
|
74
|
+
/** The body of `POST /draft/ops`. The byte cap is the route's body limit. */
|
|
75
|
+
export declare const OpBatchBodySchema: z.ZodObject<{
|
|
76
|
+
clientId: z.ZodString;
|
|
77
|
+
batchNo: z.ZodNumber;
|
|
78
|
+
batchId: z.ZodString;
|
|
79
|
+
ops: z.ZodArray<z.ZodObject<{
|
|
80
|
+
p: z.ZodArray<z.ZodString>;
|
|
81
|
+
v: z.ZodOptional<z.ZodUnknown>;
|
|
82
|
+
}, z.core.$strict>>;
|
|
83
|
+
}, z.core.$strict>;
|
|
84
|
+
export type OpBatchBody = z.infer<typeof OpBatchBodySchema>;
|
|
85
|
+
//#endregion
|