@occluze/tether 0.6.6 → 0.6.8

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 (135) hide show
  1. package/README.md +3 -3
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
  4. package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
  5. package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
  6. package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
  7. package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
  8. package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
  9. package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
  10. package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
  11. package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
  12. package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
  13. package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
  14. package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
  15. package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
  16. package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
  17. package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
  18. package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
  19. package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
  20. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
  21. package/dist/esm/chunks/search-OU75RNQL.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -1
  24. package/dist/presets/barba.esm.js +56 -36
  25. package/dist/tether.account-pages.js +776 -432
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +655 -321
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +318 -191
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +334 -243
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +78 -33
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +3171 -1998
  36. package/dist/tether.markets.js +231 -126
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +221 -126
  41. package/dist/tether.search.min.js +1 -1
  42. package/package.json +23 -4
  43. package/src/account/addresses-view.js +92 -0
  44. package/src/account/browser-session.js +359 -0
  45. package/src/account/markup.js +48 -0
  46. package/src/account/operations.js +180 -0
  47. package/src/account/order-lines.js +96 -0
  48. package/src/account/order-list.js +91 -0
  49. package/src/account/order-page.js +192 -0
  50. package/src/account/page-events.js +288 -0
  51. package/src/account/server-client.js +172 -0
  52. package/src/account/shared.js +111 -0
  53. package/src/account/shopify-auth.js +150 -0
  54. package/src/account/subscriptions-view.js +158 -0
  55. package/src/account-api.js +118 -0
  56. package/src/account-links.js +35 -0
  57. package/src/account-pages.js +68 -284
  58. package/src/account.js +281 -317
  59. package/src/amounts.js +36 -0
  60. package/src/analytics-shopify.js +241 -0
  61. package/src/analytics.js +304 -184
  62. package/src/api-version.js +75 -25
  63. package/src/astro.js +233 -68
  64. package/src/cart/checkout.js +97 -0
  65. package/src/cart/drawer.js +138 -0
  66. package/src/cart/mutations.js +234 -0
  67. package/src/cart/render.js +294 -0
  68. package/src/cart/store.js +326 -0
  69. package/src/cart.js +322 -481
  70. package/src/client.js +214 -78
  71. package/src/collection.js +126 -57
  72. package/src/config.js +323 -59
  73. package/src/controls/cart-controls.js +155 -0
  74. package/src/controls/discount.js +96 -0
  75. package/src/controls/extras.js +63 -0
  76. package/src/controls/navigation.js +83 -0
  77. package/src/diagnose-markup.js +333 -0
  78. package/src/diagnose.js +251 -202
  79. package/src/dom.js +219 -220
  80. package/src/extras.js +119 -37
  81. package/src/fragments.js +115 -22
  82. package/src/images.js +104 -0
  83. package/src/index.js +245 -285
  84. package/src/initial.js +78 -66
  85. package/src/lazy-iife.js +92 -22
  86. package/src/lazy.js +25 -4
  87. package/src/loader.js +263 -124
  88. package/src/market-codes.js +35 -0
  89. package/src/markets-ui.js +169 -55
  90. package/src/markets.js +286 -85
  91. package/src/markup.js +64 -0
  92. package/src/mounting.js +214 -91
  93. package/src/page-market.js +116 -0
  94. package/src/parts/account-pages.js +4 -1
  95. package/src/parts/account.js +4 -1
  96. package/src/parts/analytics.js +4 -1
  97. package/src/parts/diagnose.js +4 -1
  98. package/src/parts/extras.js +4 -1
  99. package/src/parts/markets.js +4 -1
  100. package/src/parts/search.js +4 -1
  101. package/src/presets/barba.js +120 -30
  102. package/src/product-events.js +130 -0
  103. package/src/product-render.js +257 -0
  104. package/src/product-state.js +318 -0
  105. package/src/product.js +233 -459
  106. package/src/repeaters.js +173 -0
  107. package/src/search.js +277 -107
  108. package/src/selling-plans.js +107 -47
  109. package/src/server.js +168 -205
  110. package/src/shopify-values.js +89 -0
  111. package/src/status.js +99 -39
  112. package/src/store-html.js +0 -0
  113. package/src/storefront/catalog.js +199 -0
  114. package/src/storefront/json-ld.js +167 -0
  115. package/src/variant-render.js +165 -0
  116. package/types/astro.d.ts +13 -2
  117. package/types/index.d.ts +178 -27
  118. package/types/server.d.ts +89 -17
  119. package/dist/esm/chunks/account-QUBHQG3C.js +0 -1
  120. package/dist/esm/chunks/account-pages-VWDHF6WA.js +0 -1
  121. package/dist/esm/chunks/analytics-SLATWPLC.js +0 -1
  122. package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
  123. package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
  124. package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
  125. package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
  126. package/dist/esm/chunks/chunk-L2TRRKYV.js +0 -1
  127. package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
  128. package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
  129. package/dist/esm/chunks/chunk-VCPI7MJ2.js +0 -1
  130. package/dist/esm/chunks/chunk-VPVJPNB4.js +0 -1
  131. package/dist/esm/chunks/chunk-ZIUIWEON.js +0 -1
  132. package/dist/esm/chunks/diagnose-VXQBSD3A.js +0 -14
  133. package/dist/esm/chunks/extras-R4X34VQL.js +0 -1
  134. package/dist/esm/chunks/markets-ui-F3R5T5EL.js +0 -1
  135. package/dist/esm/chunks/search-NILMGYXG.js +0 -1
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  (() => {
2
3
  var __defProp = Object.defineProperty;
3
4
  var __export = (target, all) => {
@@ -16,7 +17,7 @@
16
17
 
17
18
  // tether-core:config.js
18
19
  var m = window.Tether.__core["config.js"];
19
- var { DEFAULTS, cfg, configure, isConfigured, log, reportError, warn } = m;
20
+ var { DEFAULTS, cfg, configure, isConfigured, log, reportError, resolveConfig, toMetafieldIdentifier, warn } = m;
20
21
 
21
22
  // tether-core:client.js
22
23
  var m2 = window.Tether.__core["client.js"];
@@ -24,7 +25,7 @@
24
25
 
25
26
  // tether-core:dom.js
26
27
  var m3 = window.Tether.__core["dom.js"];
27
- var { attr, busy, cdnUrl, dataKey, emit, formatDate, formatMoney, formatPercent, get, has, hasTemplate, imageUrl, notifyLayoutChanged, parseJSONAttr, productUrl, qsa, readText, repeat, repeatKeyed, sameImage, scoped, sel, setImage, setSelected, setText, setTextTracked, show, toAttributes, watchImages } = m3;
28
+ var { attr, busy, cdnUrl, emit, formatDate, formatMoney, formatPercent, get, has, hasTemplate, imageUrl, notifyLayoutChanged, parseJSONAttr, productUrl, qsa, repeat, repeatKeyed, sameImage, scoped, sel, setImage, setSelected, setStoreHtml, setText, setTextTracked, show, toAttributes, watchImages } = m3;
28
29
 
29
30
  // tether-core:loader.js
30
31
  var m4 = window.Tether.__core["loader.js"];
@@ -39,118 +40,55 @@
39
40
  var { STATUS_KEYS, boundary, isNotFound, ownedBy, resolveProductKey, setScopeStatus } = m6;
40
41
 
41
42
  // src/api-version.js
42
- var parse = (v) => {
43
- const m7 = /^(\d{4})-(01|04|07|10)$/.exec(String(v || ""));
44
- return m7 ? { y: +m7[1], m: +m7[2] } : null;
45
- };
46
- var fmt = (y, m7) => `${y}-${String(m7).padStart(2, "0")}`;
47
- function nextApiVersion(v, steps = 1) {
48
- const p = parse(v);
49
- if (!p) throw new Error(`not a quarterly Shopify API version: ${v}`);
50
- const q = p.y * 4 + (p.m - 1) / 3 + steps;
51
- return fmt(Math.floor(q / 4), q % 4 * 3 + 1);
43
+ var QUARTERLY_VERSION = /^(\d{4})-(01|04|07|10)$/;
44
+ var MONTHS_PER_QUARTER = 3;
45
+ var QUARTERS_PER_YEAR = 4;
46
+ var MONTHS_SUPPORTED = 12;
47
+ var UPGRADE_WARNING_MONTHS = 3;
48
+ var AVERAGE_MONTH_MS = 30.44 * 24 * 3600 * 1e3;
49
+ function parseVersion(version) {
50
+ const match = QUARTERLY_VERSION.exec(typeof version === "string" ? version : "");
51
+ if (!match) return null;
52
+ return { year: Number(match[1]), month: Number(match[2]) };
53
+ }
54
+ var formatVersion = (year, month) => `${year}-${String(month).padStart(2, "0")}`;
55
+ function nextApiVersion(version, steps = 1) {
56
+ const parsed = parseVersion(version);
57
+ if (!parsed) throw new Error(`not a quarterly Shopify API version: ${version}`);
58
+ const quarter = parsed.year * QUARTERS_PER_YEAR + (parsed.month - 1) / MONTHS_PER_QUARTER + steps;
59
+ return formatVersion(Math.floor(quarter / QUARTERS_PER_YEAR), quarter % QUARTERS_PER_YEAR * MONTHS_PER_QUARTER + 1);
52
60
  }
53
61
  function apiVersionStatus(version, now = /* @__PURE__ */ new Date()) {
54
- if (version === "unstable") return { version, ok: false, note: '"unstable" changes without notice: pin a dated version such as 2026-07' };
55
- const p = parse(version);
56
- if (!p) return { version, ok: false, note: "not a quarterly version (YYYY-01, -04, -07 or -10)" };
57
- const released = new Date(Date.UTC(p.y, p.m - 1, 1));
58
- const supportedUntil = new Date(Date.UTC(p.y + 1, p.m - 1, 1));
59
- const monthsLeft = (supportedUntil - now) / (30.44 * 24 * 3600 * 1e3);
62
+ if (version === "unstable") {
63
+ return { version, ok: false, note: '"unstable" changes without notice: pin a dated version such as 2026-07' };
64
+ }
65
+ const parsed = parseVersion(version);
66
+ if (!parsed) return { version, ok: false, note: "not a quarterly version (YYYY-01, -04, -07 or -10)" };
67
+ const released = new Date(Date.UTC(parsed.year, parsed.month - 1, 1));
68
+ const supportedUntil = new Date(Date.UTC(parsed.year, parsed.month - 1 + MONTHS_SUPPORTED, 1));
69
+ const monthsLeft = (supportedUntil.getTime() - now.getTime()) / AVERAGE_MONTH_MS;
70
+ const isReleased = released <= now;
60
71
  let note = `supported until about ${supportedUntil.toISOString().slice(0, 7)}`;
61
- if (released > now) note = "not released yet (release candidate at best)";
62
- else if (monthsLeft <= 0) note = "no longer supported: Shopify falls back to the oldest supported version. Upgrade (see README)";
63
- else if (monthsLeft < 3) note = `${note}: plan the upgrade to ${nextApiVersion(version)} or later (see README)`;
64
- return { version, ok: released <= now && monthsLeft > 0, monthsLeft: Math.round(monthsLeft), next: nextApiVersion(version), note };
65
- }
66
-
67
- // src/markets.js
68
- var COUNTRY = /^[A-Z]{2}$/;
69
- var REGIONAL_LANGUAGES = /* @__PURE__ */ new Set(["PT_BR", "PT_PT", "ZH_CN", "ZH_TW"]);
70
- var storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
71
- var defaultEnv = () => ({
72
- document: typeof document !== "undefined" ? document : null,
73
- location: typeof window !== "undefined" ? window.location : null,
74
- storage: (() => {
75
- try {
76
- return window.localStorage;
77
- } catch {
78
- return null;
79
- }
80
- })(),
81
- global: typeof window !== "undefined" ? window : {}
82
- });
83
- function toCountry(v) {
84
- const c = String(v || "").trim().toUpperCase();
85
- return COUNTRY.test(c) && c !== "XX" && c !== "T1" ? c : null;
86
- }
87
- function toLanguage(v) {
88
- const raw = String(v || "").trim().replace("-", "_").toUpperCase();
89
- if (!raw) return null;
90
- if (REGIONAL_LANGUAGES.has(raw)) return raw;
91
- const primary = raw.split("_")[0];
92
- return /^[A-Z]{2,3}$/.test(primary) ? primary : null;
93
- }
94
- function readCookie(doc, name) {
95
- if (!doc || !name) return null;
96
- const hit = String(doc.cookie || "").split(";").map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
97
- return hit ? decodeURIComponent(hit.slice(name.length + 1)) : null;
98
- }
99
- var lookup = (locales, key) => {
100
- if (!locales || !key) return null;
101
- const k = String(key).toLowerCase();
102
- const found = Object.keys(locales).find((x) => x.toLowerCase() === k);
103
- return found ? locales[found] : null;
104
- };
105
- var readers = {
106
- saved(m7, env) {
107
- if (m7.remember === false || !env.storage) return null;
108
- try {
109
- return JSON.parse(env.storage.getItem(storageKey()) || "null");
110
- } catch {
111
- return null;
112
- }
113
- },
114
- server(m7, env) {
115
- const meta = (name) => env.document?.querySelector?.(`meta[name="${name}"]`)?.getAttribute("content");
116
- const country = env.global.TETHER_COUNTRY || meta("tether-country") || readCookie(env.document, m7.cookie || "tether_country");
117
- const language = env.global.TETHER_LANGUAGE || meta("tether-language");
118
- return { country, language };
119
- },
120
- webflow(m7, env) {
121
- const segment = (env.location?.pathname || "/").split("/").filter(Boolean)[0];
122
- const fromPath = lookup(m7.locales, segment);
123
- if (fromPath) return fromPath;
124
- const lang = env.document?.documentElement?.getAttribute("lang");
125
- if (!lang) return null;
126
- const mapped = lookup(m7.locales, lang) || lookup(m7.locales, lang.split("-")[0]);
127
- if (mapped) return { locale: lang, ...mapped };
128
- if (m7.locales && m7.strict) return null;
129
- const region = lang.split("-")[1];
130
- return { language: lang, country: region && region.length === 2 ? region : null, locale: lang };
72
+ if (!isReleased) note = "not released yet (release candidate at best)";
73
+ else if (monthsLeft <= 0) {
74
+ note = "no longer supported: Shopify falls back to the oldest supported version. Upgrade (see README)";
75
+ } else if (monthsLeft < UPGRADE_WARNING_MONTHS) {
76
+ note = `${note}: plan the upgrade to ${nextApiVersion(version)} or later (see README)`;
131
77
  }
132
- };
133
- function detectMarket(markets = cfg.markets, env = defaultEnv()) {
134
- const m7 = markets || {};
135
- if (m7.detect === false) return {};
136
- const list = !m7.detect ? ["saved"] : m7.detect === true ? ["saved", "server", "webflow"] : [].concat(m7.detect);
137
- markets = m7;
138
- const out = { sources: {} };
139
- list.forEach((name) => {
140
- const r = readers[name]?.(markets, env);
141
- if (!r) return;
142
- const vals = { country: toCountry(r.country), language: toLanguage(r.language), locale: r.locale || null };
143
- Object.entries(vals).forEach(([k, v]) => {
144
- if (v && !out[k]) {
145
- out[k] = v;
146
- out.sources[k] = name;
147
- }
148
- });
149
- });
150
- return out;
78
+ return {
79
+ version,
80
+ ok: isReleased && monthsLeft > 0,
81
+ monthsLeft: Math.round(monthsLeft),
82
+ next: nextApiVersion(version),
83
+ note
84
+ };
151
85
  }
152
86
 
153
- // src/diagnose.js
87
+ // tether-core:lazy.js
88
+ var m7 = window.Tether.__core["lazy.js"];
89
+ var { coreModules, loadPart } = m7;
90
+
91
+ // src/diagnose-markup.js
154
92
  var SHOW = /* @__PURE__ */ new Set([
155
93
  "in-stock",
156
94
  "out-of-stock",
@@ -189,8 +127,10 @@
189
127
  "orders-loading",
190
128
  "addresses-loading",
191
129
  "addresses-empty",
130
+ "addresses-error",
192
131
  "subscriptions-loading",
193
- "subscriptions-empty"
132
+ "subscriptions-empty",
133
+ "subscriptions-error"
194
134
  ]);
195
135
  var BIND = /* @__PURE__ */ new Set([
196
136
  "title",
@@ -232,7 +172,8 @@
232
172
  "swatch",
233
173
  "image"
234
174
  ]);
235
- var ATTRS = new Set(`account account-bind account-link account-login account-logout add-label add-to-cart adding-text
175
+ var ATTRIBUTES = new Set(
176
+ `account account-bind account-link account-login account-logout add-label add-to-cart adding-text
236
177
  always-enabled bind cart-checkout-charge cart-clear cart-close cart-count cart-discounts cart-drawer cart-label cart-lines
237
178
  cart-note cart-open cart-subtotal cart-total cart-warning checkout code collection collection-bind countries country
238
179
  country-bind country-label country-select default-plan default-purchase discount discount-code discount-form discount-input
@@ -245,191 +186,341 @@
245
186
  plan-value product products purchase purchase-mode qty qty-minus qty-plus quantity recommendations reverse search
246
187
  search-bind search-collections search-input search-limit search-min search-products search-queries search-results
247
188
  search-state show soldout-text sort state subscribe-text variant variant-select variant-url max-width max-height crop
248
- sig label hydrated busy`.split(/\s+/).filter(Boolean));
249
- function distance(a, b) {
250
- const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
251
- for (let j = 1; j <= b.length; j++) d[0][j] = j;
252
- for (let i = 1; i <= a.length; i++) {
253
- for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
189
+ sig label hydrated busy`.split(/\s+/).filter(Boolean)
190
+ );
191
+ var PRODUCT_ONLY = [
192
+ "bind",
193
+ "option-list",
194
+ "option",
195
+ "variant-select",
196
+ "add-to-cart",
197
+ "purchase",
198
+ "plan-list",
199
+ "plan",
200
+ "qty",
201
+ "line-attribute"
202
+ ];
203
+ var REPEATERS = ["option-list", "plan-list", "cart-lines", "list"];
204
+ var METAFIELD_BIND = "metafield:";
205
+ var IMAGE_BIND = /^image:\d+$/;
206
+ var DESCRIBED_CLASSES = 2;
207
+ function editDistance(from, to) {
208
+ let previous = Array.from({ length: to.length + 1 }, (_, index) => index);
209
+ for (let i = 1; i <= from.length; i++) {
210
+ const row = [i];
211
+ for (let j = 1; j <= to.length; j++) {
212
+ const substitution = (previous[j - 1] ?? 0) + (from[i - 1] === to[j - 1] ? 0 : 1);
213
+ row.push(Math.min((previous[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, substitution));
214
+ }
215
+ previous = row;
254
216
  }
255
- return d[a.length][b.length];
217
+ return previous[to.length] ?? 0;
256
218
  }
257
219
  function didYouMean(value, options, prefix = "") {
258
220
  let best = null;
259
- let score = Infinity;
260
- for (const o of options) {
261
- const n = distance(String(value), o);
262
- if (n < score) {
263
- score = n;
264
- best = o;
221
+ let bestDistance = Infinity;
222
+ for (const option of options) {
223
+ const distance = editDistance(value, option);
224
+ if (distance < bestDistance) {
225
+ bestDistance = distance;
226
+ best = option;
265
227
  }
266
228
  }
267
- return best && score <= Math.max(2, Math.floor(String(value).length / 4)) ? ` Did you mean "${prefix}${best}"?` : "";
229
+ const closeEnough = Math.max(2, Math.floor(value.length / 4));
230
+ return best !== null && bestDistance <= closeEnough ? ` Did you mean "${prefix}${best}"?` : "";
268
231
  }
269
- var mask = (t) => t ? `${String(t).slice(0, 4)}\u2026${String(t).slice(-4)} (${String(t).length} chars)` : "(missing)";
270
232
  function describe(el) {
271
233
  const id = el.id ? `#${el.id}` : "";
272
- const cls = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
273
- return `<${el.tagName.toLowerCase()}${id}${cls}>`;
234
+ const classes = el.className && typeof el.className === "string" ? el.className.trim().split(/\s+/) : [];
235
+ const classText = classes.length ? `.${classes.slice(0, DESCRIBED_CLASSES).join(".")}` : "";
236
+ return `<${el.tagName.toLowerCase()}${id}${classText}>`;
274
237
  }
275
- function checkMarkup(root = document) {
276
- const problems = [];
277
- const add = (level, message, el) => problems.push({ level, message, element: el ? describe(el) : void 0, el });
278
- const inProduct = (el) => Boolean(el.closest(sel("product")));
279
- const listSel = `${sel("collection")},${sel("products")},${sel("recommendations")}`;
238
+ var listSelector = () => `${sel("collection")},${sel("products")},${sel("recommendations")}`;
239
+ function checkProductKeys(root, add) {
280
240
  qsa(root, sel("product")).forEach((el) => {
281
- if (el.closest(listSel) && el.closest(sel("list"))) return;
241
+ if (el.closest(listSelector()) && el.closest(sel("list"))) return;
282
242
  const { key, fromUrl, raw } = resolveProductKey(el);
283
- if (!raw) add("error", `${attr("product")} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`, el);
284
- else if (fromUrl && !key) add("warn", `${attr("product")}="${raw}" but this URL has no value for it (e.g. ?handle=my-product).`, el);
243
+ if (!raw) {
244
+ add(
245
+ "error",
246
+ `${attr("product")} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`,
247
+ el
248
+ );
249
+ } else if (fromUrl && !key) {
250
+ add("warn", `${attr("product")}="${raw}" but this URL has no value for it (e.g. ?handle=my-product).`, el);
251
+ }
285
252
  });
286
- const productOnly = ["bind", "option-list", "option", "variant-select", "add-to-cart", "purchase", "plan-list", "plan", "qty", "line-attribute"];
287
- productOnly.forEach((name) => {
253
+ }
254
+ function checkPlacement(root, add) {
255
+ for (const name of PRODUCT_ONLY) {
288
256
  qsa(root, sel(name)).forEach((el) => {
289
- if (inProduct(el)) return;
257
+ if (el.closest(sel("product"))) return;
290
258
  if (name === "line-attribute" && el.closest(sel("group"))) return;
291
259
  add("warn", `${attr(name)} outside any ${attr("product")}: it does nothing here.`, el);
292
260
  });
293
- });
261
+ }
294
262
  qsa(root, sel("group-add")).forEach((el) => {
295
263
  if (!el.closest(sel("group"))) add("warn", `${attr("group-add")} outside a ${attr("group")}.`, el);
296
264
  });
297
- ["option-list", "plan-list", "cart-lines", "list"].forEach((name) => qsa(root, sel(name)).forEach((el) => {
298
- if (!el.firstElementChild && !hasTemplate(el)) {
299
- add("warn", `${attr(name)} without a template child: put one example element inside it.`, el);
265
+ for (const name of REPEATERS) {
266
+ qsa(root, sel(name)).forEach((el) => {
267
+ if (!el.firstElementChild && !hasTemplate(el)) {
268
+ add("warn", `${attr(name)} without a template child: put one example element inside it.`, el);
269
+ }
270
+ });
271
+ }
272
+ qsa(root, listSelector()).forEach((el) => {
273
+ if (!qsa(el, sel("list")).length) {
274
+ add("error", `List without a ${attr("list")} child: products have nowhere to render.`, el);
300
275
  }
301
- }));
302
- qsa(root, listSel).forEach((el) => {
303
- if (!qsa(el, sel("list")).length) add("error", `List without a ${attr("list")} child: products have nowhere to render.`, el);
304
276
  });
277
+ }
278
+ function checkValues(root, add) {
305
279
  qsa(root, sel("show")).forEach((el) => {
306
- const k = get(el, "show");
307
- if (!SHOW.has(k)) add("warn", `Unknown ${attr("show")}="${k}".${didYouMean(k, SHOW)}`, el);
280
+ const value = get(el, "show") ?? "";
281
+ if (!SHOW.has(value)) add("warn", `Unknown ${attr("show")}="${value}".${didYouMean(value, SHOW)}`, el);
308
282
  });
309
283
  qsa(root, sel("bind")).forEach((el) => {
310
- const k = get(el, "bind") || "";
311
- if (!k || BIND.has(k) || /^image:\d+$/.test(k) || k.startsWith("metafield:")) return;
312
- add("warn", `Unknown ${attr("bind")}="${k}": it stays empty.${didYouMean(k, BIND)}`, el);
284
+ const value = get(el, "bind") || "";
285
+ if (!value || BIND.has(value) || IMAGE_BIND.test(value) || value.startsWith(METAFIELD_BIND)) return;
286
+ add("warn", `Unknown ${attr("bind")}="${value}": it stays empty.${didYouMean(value, BIND)}`, el);
313
287
  });
288
+ }
289
+ function checkMetafieldBinds(root, add) {
290
+ qsa(root, sel("bind")).forEach((el) => {
291
+ const value = get(el, "bind") || "";
292
+ if (!value.startsWith(METAFIELD_BIND)) return;
293
+ const ref = value.slice(METAFIELD_BIND.length);
294
+ const listed = cfg.metafields.some(
295
+ (metafield) => (typeof metafield === "string" ? metafield : `${metafield.namespace}.${metafield.key}`) === ref
296
+ );
297
+ if (!listed) {
298
+ add("error", `${attr("bind")}="${value}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
299
+ }
300
+ });
301
+ }
302
+ function checkAttributeNames(root, add) {
314
303
  const prefix = `${cfg.prefix}-`;
315
- const seen = /* @__PURE__ */ new Set();
304
+ const reported = /* @__PURE__ */ new Set();
316
305
  qsa(root, "*").forEach((el) => {
317
- for (const a of el.attributes) {
318
- if (!a.name.startsWith(prefix)) continue;
319
- const name = a.name.slice(prefix.length);
320
- if (ATTRS.has(name) || seen.has(name)) continue;
321
- seen.add(name);
322
- add("warn", `Unknown attribute ${a.name}: Tether ignores it.${didYouMean(name, ATTRS, prefix)}`, el);
306
+ for (const { name: fullName } of el.attributes) {
307
+ if (!fullName.startsWith(prefix)) continue;
308
+ const name = fullName.slice(prefix.length);
309
+ if (ATTRIBUTES.has(name) || reported.has(name)) continue;
310
+ reported.add(name);
311
+ add("warn", `Unknown attribute ${fullName}: Tether ignores it.${didYouMean(name, ATTRIBUTES, prefix)}`, el);
323
312
  }
324
313
  });
325
- qsa(root, sel("bind")).forEach((el) => {
326
- const k = get(el, "bind") || "";
327
- if (!k.startsWith("metafield:")) return;
328
- const ref = k.slice(10);
329
- const listed = (cfg.metafields || []).some((m7) => (typeof m7 === "string" ? m7 : `${m7.namespace}.${m7.key}`) === ref);
330
- if (!listed) add("error", `${attr("bind")}="${k}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
331
- });
332
- if (qsa(root, sel("cart-open")).length && !qsa(root, sel("cart-drawer")).length) {
314
+ }
315
+ function checkCartAndScopes(root, add) {
316
+ const drawers = qsa(root, sel("cart-drawer")).length;
317
+ if (qsa(root, sel("cart-open")).length && !drawers) {
333
318
  add("warn", `${attr("cart-open")} exists but there is no ${attr("cart-drawer")} on this page.`);
334
319
  }
335
- if (qsa(root, sel("cart-drawer")).length > 1) add("warn", `More than one ${attr("cart-drawer")}.`);
320
+ if (drawers > 1) add("warn", `More than one ${attr("cart-drawer")}.`);
336
321
  qsa(root, `[${attr("state")}="error"]`).forEach((el) => {
337
322
  if (el.matches(boundary())) add("error", `Scope failed to load (${get(el, "error") || "error"}).`, el);
338
323
  });
324
+ }
325
+ function checkMarkup(root = document) {
326
+ const problems = [];
327
+ const add = (level, message, el) => {
328
+ problems.push(el ? { level, message, element: describe(el), el } : { level, message });
329
+ };
330
+ checkProductKeys(root, add);
331
+ checkPlacement(root, add);
332
+ checkValues(root, add);
333
+ checkAttributeNames(root, add);
334
+ checkMetafieldBinds(root, add);
335
+ checkCartAndScopes(root, add);
336
+ return problems;
337
+ }
338
+
339
+ // src/diagnose.js
340
+ var MAX_PRODUCTS_CHECKED = 20;
341
+ var API_UPGRADE_WARNING_MONTHS = 3;
342
+ var TOKEN_EDGE_CHARACTERS = 4;
343
+ function maskToken(token) {
344
+ if (!token) return "(missing)";
345
+ return `${token.slice(0, TOKEN_EDGE_CHARACTERS)}\u2026${token.slice(-TOKEN_EDGE_CHARACTERS)} (${token.length} chars)`;
346
+ }
347
+ function errorText(error, fallback) {
348
+ if (error instanceof Error && error.message) return error.message;
349
+ const code = typeof error === "object" && error !== null ? Reflect.get(error, "code") : void 0;
350
+ return typeof code === "string" && code ? code : fallback;
351
+ }
352
+ function configSummary() {
353
+ return {
354
+ domain: cfg.domain || "(missing)",
355
+ token: maskToken(cfg.token),
356
+ apiVersion: cfg.apiVersion,
357
+ endpoint: cfg.domain ? endpoint() : null,
358
+ country: cfg.country,
359
+ language: cfg.language,
360
+ locale: cfg.locale,
361
+ markets: cfg.markets,
362
+ prefix: cfg.prefix,
363
+ productPath: cfg.productPath,
364
+ metafields: cfg.metafields,
365
+ autoMount: cfg.autoMount,
366
+ observe: cfg.observe,
367
+ onError: typeof cfg.onError === "function"
368
+ };
369
+ }
370
+ async function marketSummary() {
371
+ return {
372
+ country: cfg.country || null,
373
+ language: cfg.language || null,
374
+ locale: cfg.locale || null,
375
+ detected: cfg.markets?.detect ? (await loadPart("markets")).detectMarket(cfg.markets) : null
376
+ };
377
+ }
378
+ function apiVersionProblem({ ok, monthsLeft, note }) {
379
+ if (ok && (monthsLeft === void 0 || monthsLeft >= API_UPGRADE_WARNING_MONTHS)) return null;
380
+ return { level: ok ? "warn" : "error", message: `apiVersion ${cfg.apiVersion}: ${note}` };
381
+ }
382
+ async function checkStorefront() {
383
+ if (!isConfigured()) return { kind: "not-configured" };
384
+ const startedAt = Date.now();
385
+ try {
386
+ const data = await gql(
387
+ "query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }",
388
+ {},
389
+ { context: false }
390
+ );
391
+ const shop = {
392
+ name: data.shop.name,
393
+ url: data.shop.primaryDomain.url,
394
+ currency: data.shop.paymentSettings.currencyCode
395
+ };
396
+ return { kind: "reachable", shop, latencyMs: Date.now() - startedAt };
397
+ } catch (error) {
398
+ return { kind: "unreachable", error, latencyMs: Date.now() - startedAt };
399
+ }
400
+ }
401
+ function checkOptions(root, key, product) {
402
+ const problems = [];
403
+ const names = product.options.map((option) => option.name);
404
+ const roots = qsa(root, sel("product")).filter(
405
+ (el) => resolveProductKey(el).key === key && el.querySelector(sel("add-to-cart"))
406
+ );
407
+ for (const el of roots) {
408
+ const shown = new Set(
409
+ qsa(el, `${sel("option-list")},select${sel("option")}`).map(
410
+ (list) => get(list, "option-list") || get(list, "option")
411
+ )
412
+ );
413
+ if (!shown.size && qsa(el, `select${sel("variant-select")}`).length) continue;
414
+ if (!cfg.language) {
415
+ for (const name of shown) {
416
+ if (!name || names.includes(name)) continue;
417
+ problems.push({
418
+ level: "warn",
419
+ message: `Product "${product.handle}" has no option "${name}" (its options: ${names.join(", ") || "none"}).${didYouMean(name, names)}`
420
+ });
421
+ }
422
+ }
423
+ for (const option of product.options) {
424
+ if (option.optionValues.length <= 1 || shown.has(option.name)) continue;
425
+ problems.push({
426
+ level: "warn",
427
+ message: `Product "${product.handle}": option "${option.name}" (${option.optionValues.length} values) has no tether-option-list, so visitors can't choose it.`
428
+ });
429
+ }
430
+ }
339
431
  return problems;
340
432
  }
433
+ async function checkProducts(root, report) {
434
+ const allKeys = qsa(root, sel("product")).map((el) => resolveProductKey(el).key);
435
+ const keys = [...new Set(allKeys.filter((key) => key !== null && key !== ""))].slice(0, MAX_PRODUCTS_CHECKED);
436
+ const results = await Promise.allSettled(keys.map((key) => getProduct(key)));
437
+ results.forEach((result, index) => {
438
+ const key = keys[index] ?? "";
439
+ if (result.status === "rejected") {
440
+ const reason = errorText(result.reason, "not loaded");
441
+ report.products.push({ key, error: result.reason instanceof Error ? result.reason.message : void 0 });
442
+ report.problems.push({
443
+ level: "error",
444
+ message: `Product "${key}": ${reason}. Is it published to the Headless channel?`
445
+ });
446
+ return;
447
+ }
448
+ const product = result.value;
449
+ const plans = allPlans(product);
450
+ report.products.push({
451
+ key,
452
+ handle: product.handle,
453
+ title: product.title,
454
+ plans: plans.length,
455
+ requiresSellingPlan: product.requiresSellingPlan
456
+ });
457
+ for (const plan of plans) {
458
+ report.sellingPlans.push({ product: product.handle, group: plan.group.name, plan: plan.name, id: plan.id });
459
+ }
460
+ report.problems.push(...checkOptions(root, key, product));
461
+ });
462
+ }
341
463
  async function diagnose({ root = document, silent = false, version, now = /* @__PURE__ */ new Date() } = {}) {
464
+ const storefront = await checkStorefront();
342
465
  const report = {
343
466
  version,
344
- config: {
345
- domain: cfg.domain || "(missing)",
346
- token: mask(cfg.token),
347
- apiVersion: cfg.apiVersion,
348
- endpoint: cfg.domain ? endpoint() : null,
349
- country: cfg.country,
350
- language: cfg.language,
351
- locale: cfg.locale,
352
- markets: cfg.markets || null,
353
- prefix: cfg.prefix,
354
- productPath: cfg.productPath,
355
- metafields: cfg.metafields,
356
- autoMount: cfg.autoMount,
357
- observe: cfg.observe,
358
- onError: typeof cfg.onError === "function"
359
- },
467
+ config: configSummary(),
360
468
  apiVersion: apiVersionStatus(cfg.apiVersion, now),
361
- // The market in use (a visitor's choice included), plus what detection would pick.
362
- market: { country: cfg.country || null, language: cfg.language || null, locale: cfg.locale || null, detected: cfg.markets?.detect ? detectMarket(cfg.markets) : null },
363
- reachable: false,
364
- shop: null,
365
- latencyMs: null,
469
+ market: await marketSummary(),
470
+ reachable: storefront.kind === "reachable",
471
+ shop: storefront.kind === "reachable" ? storefront.shop : null,
472
+ latencyMs: storefront.kind === "not-configured" ? null : storefront.latencyMs,
366
473
  products: [],
367
474
  sellingPlans: [],
368
475
  problems: []
369
476
  };
370
- if (!isConfigured()) {
371
- report.problems.push({ level: "error", message: "TETHER_CONFIG needs domain and token." });
372
- } else {
373
- const t0 = Date.now();
374
- try {
375
- const data = await gql("query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }", {}, { context: false });
376
- report.reachable = true;
377
- report.shop = { name: data.shop?.name, url: data.shop?.primaryDomain?.url, currency: data.shop?.paymentSettings?.currencyCode };
378
- } catch (err) {
379
- report.problems.push({ level: "error", message: `Storefront API not reachable: ${err.message || err.code || "unknown error"}` });
380
- }
381
- report.latencyMs = Date.now() - t0;
382
- if (report.reachable) {
383
- const keys = [...new Set(qsa(root, sel("product")).map((el) => resolveProductKey(el).key).filter(Boolean))].slice(0, 20);
384
- const results = await Promise.allSettled(keys.map((k) => getProduct(k)));
385
- results.forEach((r, i) => {
386
- if (r.status === "fulfilled") {
387
- const p = r.value;
388
- const plans = allPlans(p);
389
- report.products.push({ key: keys[i], handle: p.handle, title: p.title, plans: plans.length, requiresSellingPlan: p.requiresSellingPlan });
390
- plans.forEach((pl) => report.sellingPlans.push({ product: p.handle, group: pl.group?.name, plan: pl.name, id: pl.id }));
391
- qsa(root, sel("product")).filter((el) => resolveProductKey(el).key === keys[i] && el.querySelector(sel("add-to-cart"))).forEach((el) => {
392
- const shown = new Set(qsa(el, `${sel("option-list")},select${sel("option")}`).map((x) => get(x, "option-list") || get(x, "option")));
393
- if (!shown.size && qsa(el, `select${sel("variant-select")}`).length) return;
394
- const names = (p.options || []).map((o) => o.name);
395
- if (!cfg.language) {
396
- [...shown].filter((n) => n && !names.includes(n)).forEach((n) => {
397
- report.problems.push({ level: "warn", message: `Product "${p.handle}" has no option "${n}" (its options: ${names.join(", ") || "none"}).${didYouMean(n, names)}` });
398
- });
399
- }
400
- (p.options || []).filter((o) => o.optionValues.length > 1 && !shown.has(o.name)).forEach((o) => {
401
- report.problems.push({ level: "warn", message: `Product "${p.handle}": option "${o.name}" (${o.optionValues.length} values) has no tether-option-list, so visitors can't choose it.` });
402
- });
403
- });
404
- } else {
405
- report.products.push({ key: keys[i], error: r.reason?.message });
406
- report.problems.push({ level: "error", message: `Product "${keys[i]}": ${r.reason?.message || r.reason?.code || "not loaded"}. Is it published to the Headless channel?` });
407
- }
477
+ switch (storefront.kind) {
478
+ case "not-configured":
479
+ report.problems.push({ level: "error", message: "TETHER_CONFIG needs domain and token." });
480
+ break;
481
+ case "unreachable":
482
+ report.problems.push({
483
+ level: "error",
484
+ message: `Storefront API not reachable: ${errorText(storefront.error, "unknown error")}`
408
485
  });
409
- }
410
- }
411
- if (!report.apiVersion.ok || (report.apiVersion.monthsLeft ?? 99) < 3) {
412
- report.problems.push({ level: report.apiVersion.ok ? "warn" : "error", message: `apiVersion ${cfg.apiVersion}: ${report.apiVersion.note}` });
486
+ break;
487
+ case "reachable":
488
+ await checkProducts(root, report);
489
+ break;
413
490
  }
491
+ const versionProblem = apiVersionProblem(report.apiVersion);
492
+ if (versionProblem) report.problems.push(versionProblem);
414
493
  if (typeof document !== "undefined") report.problems.push(...checkMarkup(root));
415
494
  if (!silent) print(report);
416
495
  return report;
417
496
  }
418
- function print(r) {
419
- const c = console;
420
- const errors = r.problems.filter((p) => p.level === "error").length;
421
- c.group?.(`[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : "no errors"}, ${r.problems.length - errors} warning(s)`);
422
- c.log("version", r.version, "| API", r.apiVersion.version, `(${r.apiVersion.note})`);
423
- c.log("config", r.config);
424
- if (r.market) c.log("market", `in use: ${[r.market.country, r.market.language].filter(Boolean).join(" / ") || "the store default"}`, r.market.detected ? `(detection picks ${JSON.stringify(r.market.detected)})` : "");
425
- c.log(r.reachable ? `Storefront API reachable: ${r.shop?.name} (${r.latencyMs} ms)` : "Storefront API NOT reachable");
426
- if (r.products.length) (c.table || c.log)(r.products);
427
- if (r.sellingPlans.length) (c.table || c.log)(r.sellingPlans);
428
- else if (r.reachable) c.log("No selling plans on the products of this page.");
429
- r.problems.forEach((p) => (p.level === "error" ? c.error : c.warn)(p.message, p.el || ""));
430
- c.groupEnd?.();
497
+ function print(report) {
498
+ const errors = report.problems.filter((problem) => problem.level === "error").length;
499
+ const warnings = report.problems.length - errors;
500
+ console.group(`[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : "no errors"}, ${warnings} warning(s)`);
501
+ console.log("version", report.version, "| API", report.apiVersion.version, `(${report.apiVersion.note})`);
502
+ console.log("config", report.config);
503
+ const { country, language, detected } = report.market;
504
+ const inUse = [country, language].filter(Boolean).join(" / ") || "the store default";
505
+ console.log("market", `in use: ${inUse}`, detected ? `(detection picks ${JSON.stringify(detected)})` : "");
506
+ console.log(
507
+ report.shop ? `Storefront API reachable: ${report.shop.name} (${report.latencyMs ?? 0} ms)` : "Storefront API NOT reachable"
508
+ );
509
+ if (report.products.length) console.table(report.products);
510
+ if (report.sellingPlans.length) console.table(report.sellingPlans);
511
+ else if (report.reachable) console.log("No selling plans on the products of this page.");
512
+ for (const problem of report.problems) {
513
+ if (problem.level === "error") console.error(problem.message, problem.el ?? "");
514
+ else console.warn(problem.message, problem.el ?? "");
515
+ }
516
+ console.groupEnd();
431
517
  }
432
518
 
433
519
  // src/parts/diagnose.js
434
- window.Tether.__parts.diagnose = diagnose_exports;
520
+ var { Tether } = (
521
+ /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */
522
+ window
523
+ );
524
+ if (!Tether) throw new Error("tether.diagnose.js runs after the Tether script, which loads it");
525
+ Tether.__parts["diagnose"] = diagnose_exports;
435
526
  })();