@occluze/tether 0.7.0 → 0.7.2

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 (139) hide show
  1. package/README.md +51 -109
  2. package/dist/esm/astro.js +1 -1
  3. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-W4FBRKXA.js} +1 -1
  4. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IM3OQTOP.js} +1 -1
  5. package/dist/esm/chunks/analytics-QDVOLZY3.js +1 -0
  6. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-AUVAN3KZ.js} +1 -1
  7. package/dist/esm/chunks/chunk-BMDYNMRC.js +1 -0
  8. package/dist/esm/chunks/chunk-CUL66KRA.js +1 -0
  9. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-CVNRODH3.js} +1 -1
  10. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-CXHPBT7N.js} +1 -1
  11. package/dist/esm/chunks/chunk-GF6JTI65.js +1 -0
  12. package/dist/esm/chunks/chunk-MRSDJPBC.js +1 -0
  13. package/dist/esm/chunks/chunk-NYDZPTLP.js +1 -0
  14. package/dist/esm/chunks/chunk-QDHLXXAZ.js +1 -0
  15. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-QUECUXHD.js} +1 -1
  16. package/dist/esm/chunks/chunk-RJY76ESL.js +1 -0
  17. package/dist/esm/chunks/chunk-RPI5C3EJ.js +1 -0
  18. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-YC4JXWRG.js} +2 -2
  19. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-3XHPZZNP.js} +1 -1
  20. package/dist/esm/chunks/extras-C724KKG6.js +1 -0
  21. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-WBWWQAGU.js} +1 -1
  22. package/dist/esm/chunks/{search-UJ73YBQ5.js → search-5NFIZBPJ.js} +1 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/tether.account-pages.js +13 -3
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +13 -3
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +18 -3
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +20 -5
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +52 -28
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +353 -288
  36. package/dist/tether.markets.js +31 -5
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +2 -2
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +18 -3
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  43. package/dist/types/src/cart/mutations.d.ts +6 -0
  44. package/dist/types/src/cart/store.d.ts +5 -2
  45. package/dist/types/src/cart.d.ts +3 -2
  46. package/dist/types/src/client.d.ts +61 -22
  47. package/dist/types/src/config.d.ts +6 -1
  48. package/dist/types/src/index.d.ts +2 -2
  49. package/dist/types/src/markets.d.ts +9 -2
  50. package/dist/types/src/product-shape.d.ts +3 -1
  51. package/dist/types/src/uri.d.ts +6 -0
  52. package/package.json +54 -6
  53. package/dist/esm/chunks/analytics-2V632IRV.js +0 -1
  54. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  55. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  56. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  57. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  58. package/dist/esm/chunks/chunk-E5YO6274.js +0 -1
  59. package/dist/esm/chunks/chunk-MPDWM2NN.js +0 -1
  60. package/dist/esm/chunks/chunk-YLUULATY.js +0 -1
  61. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  62. package/src/account/addresses-view.ts +0 -58
  63. package/src/account/answers.ts +0 -79
  64. package/src/account/markup.ts +0 -36
  65. package/src/account/operations.ts +0 -145
  66. package/src/account/order-lines.ts +0 -75
  67. package/src/account/order-list.ts +0 -107
  68. package/src/account/order-page.ts +0 -137
  69. package/src/account/page-answers.ts +0 -317
  70. package/src/account/page-events.ts +0 -253
  71. package/src/account/server-client.ts +0 -183
  72. package/src/account/subscriptions-view.ts +0 -115
  73. package/src/account-api.ts +0 -98
  74. package/src/account-marks.ts +0 -35
  75. package/src/account-pages.ts +0 -81
  76. package/src/account.ts +0 -284
  77. package/src/amounts.ts +0 -64
  78. package/src/analytics-shopify.ts +0 -246
  79. package/src/analytics.ts +0 -315
  80. package/src/api-version.ts +0 -79
  81. package/src/astro.ts +0 -272
  82. package/src/cart/cart-shape.ts +0 -25
  83. package/src/cart/checkout.ts +0 -101
  84. package/src/cart/drawer.ts +0 -125
  85. package/src/cart/mutations.ts +0 -237
  86. package/src/cart/render.ts +0 -266
  87. package/src/cart/store.ts +0 -279
  88. package/src/cart.ts +0 -345
  89. package/src/client.ts +0 -391
  90. package/src/collection.ts +0 -175
  91. package/src/config-sections.ts +0 -284
  92. package/src/config.ts +0 -480
  93. package/src/controls/cart-controls.ts +0 -123
  94. package/src/controls/discount.ts +0 -75
  95. package/src/controls/extras.ts +0 -54
  96. package/src/controls/navigation.ts +0 -77
  97. package/src/diagnose-markup.ts +0 -298
  98. package/src/diagnose.ts +0 -294
  99. package/src/dom.ts +0 -268
  100. package/src/extras.ts +0 -136
  101. package/src/fragments.ts +0 -261
  102. package/src/globals.d.ts +0 -15
  103. package/src/images.ts +0 -91
  104. package/src/index.ts +0 -415
  105. package/src/initial.ts +0 -101
  106. package/src/lazy-iife.ts +0 -197
  107. package/src/lazy.ts +0 -42
  108. package/src/loader.ts +0 -363
  109. package/src/market-codes.ts +0 -6
  110. package/src/markets-ui.ts +0 -227
  111. package/src/markets.ts +0 -246
  112. package/src/markup.ts +0 -127
  113. package/src/mounting.ts +0 -264
  114. package/src/page-market.ts +0 -110
  115. package/src/part-integrity.ts +0 -7
  116. package/src/parts/account-pages.ts +0 -7
  117. package/src/parts/account.ts +0 -7
  118. package/src/parts/analytics.ts +0 -7
  119. package/src/parts/diagnose.ts +0 -7
  120. package/src/parts/extras.ts +0 -7
  121. package/src/parts/markets.ts +0 -7
  122. package/src/parts/search.ts +0 -7
  123. package/src/presets/barba.ts +0 -194
  124. package/src/product-events.ts +0 -115
  125. package/src/product-render.ts +0 -218
  126. package/src/product-shape.ts +0 -49
  127. package/src/product-state.ts +0 -318
  128. package/src/product.ts +0 -308
  129. package/src/repeaters.ts +0 -161
  130. package/src/search.ts +0 -305
  131. package/src/selling-plans.ts +0 -145
  132. package/src/server.ts +0 -211
  133. package/src/shopify-values.ts +0 -91
  134. package/src/status.ts +0 -106
  135. package/src/storage.ts +0 -34
  136. package/src/store-html.ts +0 -359
  137. package/src/storefront/catalog.ts +0 -196
  138. package/src/storefront/json-ld.ts +0 -144
  139. package/src/variant-render.ts +0 -139
@@ -1,54 +0,0 @@
1
- /**
2
- * The document-wide listeners whose work is in the optional extras part, loaded when one of them first has work:
3
- * group add buttons, the cart or market changing in another tab, and shareable variant links. A part that can't load
4
- * (a self-hosted site without `tether.extras.min.js`) is reported, so the missing file shows.
5
- */
6
- import { cfg, reportError } from '../config.ts';
7
- import { attr, sel, busy, isOwnEvent } from '../dom.ts';
8
- import { loadPart } from '../lazy.ts';
9
- import type { ShopifyCart } from '../cart/store.ts';
10
-
11
- /** `[tether-group]` with `[tether-group-add]`: adds every product scope inside the group in one request. */
12
- export const addGroup = (button: Element): Promise<ShopifyCart | null> =>
13
- loadPart('extras').then((part) => part.addGroup(button));
14
-
15
- const reportExtrasError = (error: unknown) => {
16
- reportError(error, 'extras');
17
- };
18
-
19
- function onClick(event: MouseEvent): void {
20
- const { target } = event;
21
- const button = target instanceof window.Element ? target.closest(sel('group-add')) : null;
22
- if (!button) return;
23
- event.preventDefault();
24
- busy(button, () => addGroup(button)).catch(reportExtrasError);
25
- }
26
-
27
- function onStorage(event: StorageEvent): void {
28
- if (!cfg.domain || !event.key?.startsWith(`${cfg.prefix}:`)) return;
29
- loadPart('extras')
30
- .then((part) => {
31
- part.onStorage(event);
32
- })
33
- .catch(reportExtrasError);
34
- }
35
-
36
- /** @param event `tether:variant-changed`, which a product scope emits; one another script dispatches is ignored. */
37
- function onVariantChanged(event: Event): void {
38
- if (!isOwnEvent(event, 'variant-changed')) return;
39
- const { target } = event;
40
- if (!(target instanceof window.Element)) return;
41
- if (!cfg.variantUrl && !target.hasAttribute(attr('variant-url'))) return;
42
- loadPart('extras')
43
- .then((part) => {
44
- part.writeVariantUrl(target, event.detail);
45
- })
46
- .catch(reportExtrasError);
47
- }
48
-
49
- /** Binds the document-wide listeners, once per page load of the script. */
50
- export function bindExtrasControls(): void {
51
- document.addEventListener('click', onClick);
52
- window.addEventListener('storage', onStorage);
53
- document.addEventListener(`${cfg.prefix}:variant-changed`, onVariantChanged);
54
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * How the page follows the visitor across pages and markets: Astro's ClientRouter (`astro:page-load`,
3
- * `astro:before-swap`) mounts each new page in its market and closes the cart while leaving; a country select
4
- * switches the market; back online, what failed to load loads again.
5
- */
6
- import { cfg, reportError } from '../config.ts';
7
- import { attr, sel } from '../dom.ts';
8
- import { liveLists, retryFailed } from '../mounting.ts';
9
- import { marketForArrival, setCountry } from '../page-market.ts';
10
-
11
- /** What the entry module does for a page. */
12
- interface PageHooks {
13
- /** Settles once `init()` has applied the visitor's market. */
14
- ready: () => Promise<unknown>;
15
- mount: (root: Document) => unknown;
16
- unmount: () => void;
17
- }
18
-
19
- /** The body the last full-document mount ran on: Astro's first `astro:page-load` comes after that mount. */
20
- let mountedBody: HTMLElement | null = null;
21
-
22
- /** Records a mount of the whole document, so the `astro:page-load` of the same page sends no second round of requests. */
23
- export function rememberDocumentMount(): void {
24
- mountedBody = document.body;
25
- }
26
-
27
- /** Mounts the page Astro swapped in. It rejects when `init()` failed or the mount threw; the listener reports that. */
28
- async function onPageLoad(hooks: PageHooks): Promise<void> {
29
- await hooks.ready();
30
- const target = marketForArrival();
31
- if (!target) {
32
- if (mountedBody !== document.body) hooks.mount(document);
33
- return;
34
- }
35
- try {
36
- await setCountry(target.country, { language: target.language ?? null, locale: target.locale, remember: false });
37
- } catch (error) {
38
- reportError(error, 'country');
39
- }
40
- hooks.mount(document);
41
- }
42
-
43
- function bindAstroRouter(hooks: PageHooks): void {
44
- document.addEventListener('astro:page-load', () => {
45
- onPageLoad(hooks).catch((error: unknown) => {
46
- reportError(error, 'page mount');
47
- });
48
- });
49
- document.addEventListener('astro:before-swap', () => {
50
- mountedBody = null;
51
- hooks.unmount();
52
- });
53
- }
54
-
55
- function onChange(event: Event): void {
56
- const { target } = event;
57
- if (!(target instanceof window.HTMLSelectElement) || !target.matches(sel('country-select'))) return;
58
- if (!target.value) return;
59
- setCountry(target.value).catch((error: unknown) => {
60
- reportError(error, 'country');
61
- });
62
- }
63
-
64
- /** Loads the failed products and lists again. */
65
- function onOnline() {
66
- retryFailed();
67
- liveLists.forEach((list) => {
68
- if (list.root.getAttribute(attr('state')) === 'error') void list.load();
69
- });
70
- }
71
-
72
- /** Binds the document-wide listeners, once per page load of the script. */
73
- export function bindNavigation(hooks: PageHooks): void {
74
- if (cfg.astro) bindAstroRouter(hooks);
75
- document.addEventListener('change', onChange);
76
- window.addEventListener('online', onOnline);
77
- }
@@ -1,298 +0,0 @@
1
- /**
2
- * The markup half of `Tether.diagnose()`: problems that show without the network. It knows every value and attribute
3
- * name Tether reads, so a typo gets a "did you mean".
4
- */
5
- import { cfg } from './config.ts';
6
- import { attr, sel, get, qsa, hasTemplate } from './dom.ts';
7
- import { resolveProductKey, boundary } from './status.ts';
8
-
9
- type ProblemLevel = 'error' | 'warn';
10
-
11
- export interface Problem {
12
- level: ProblemLevel;
13
- message: string;
14
- /** The element, described for people: `<div#id.class>`. */
15
- element?: string;
16
- /** The element itself, for the console. */
17
- el?: Element;
18
- }
19
-
20
- type AddProblem = (level: ProblemLevel, message: string, el?: Element) => void;
21
-
22
- /** Every `tether-show` value. */
23
- const SHOW = new Set([
24
- 'in-stock',
25
- 'out-of-stock',
26
- 'backorder',
27
- 'low-stock',
28
- 'plenty-in-stock',
29
- 'on-sale',
30
- 'not-on-sale',
31
- 'has-variants',
32
- 'single-variant',
33
- 'subscription-available',
34
- 'subscription-unavailable',
35
- 'subscription-only',
36
- 'one-time-available',
37
- 'subscription-selected',
38
- 'one-time-selected',
39
- 'has-savings',
40
- 'loading',
41
- 'ready',
42
- 'error',
43
- 'not-found',
44
- 'cart-empty',
45
- 'cart-filled',
46
- 'cart-has-subscription',
47
- 'cart-no-subscription',
48
- 'cart-has-discount',
49
- 'cart-error',
50
- 'cart-warning',
51
- 'list-empty',
52
- 'list-filled',
53
- 'search-empty',
54
- 'search-results',
55
- 'logged-in',
56
- 'logged-out',
57
- 'orders-empty',
58
- 'orders-loading',
59
- 'addresses-loading',
60
- 'addresses-empty',
61
- 'addresses-error',
62
- 'subscriptions-loading',
63
- 'subscriptions-empty',
64
- 'subscriptions-error',
65
- ]);
66
- /** Every `tether-bind` value, apart from `image:<n>` and `metafield:<namespace.key>`. */
67
- const BIND = new Set([
68
- 'title',
69
- 'description',
70
- 'description-text',
71
- 'vendor',
72
- 'type',
73
- 'handle',
74
- 'published-at',
75
- 'price',
76
- 'total-price',
77
- 'compare-at-price',
78
- 'unit-price',
79
- 'min-price',
80
- 'max-price',
81
- 'discount-percentage',
82
- 'discount-amount',
83
- 'variant-title',
84
- 'variant-id',
85
- 'sku',
86
- 'barcode',
87
- 'stock',
88
- 'quantity',
89
- 'featured-image',
90
- 'variant-image',
91
- 'images',
92
- 'tags',
93
- 'link',
94
- 'url',
95
- 'one-time-price',
96
- 'subscription-price',
97
- 'subscription-savings',
98
- 'subscription-savings-amount',
99
- 'checkout-charge',
100
- 'plan-name',
101
- 'plan-description',
102
- 'plan-group',
103
- 'option-value',
104
- 'swatch',
105
- 'image',
106
- ]);
107
- /** Every attribute name Tether reads, without the prefix. */
108
- const ATTRIBUTES = new Set(
109
- `account account-bind account-link account-login account-logout add-label add-to-cart adding-text
110
- always-enabled bind cart-checkout-charge cart-clear cart-close cart-count cart-discounts cart-drawer cart-label cart-lines
111
- cart-note cart-open cart-subtotal cart-total cart-warning checkout code collection collection-bind countries country
112
- country-bind country-label country-select default-plan default-purchase discount discount-code discount-form discount-input
113
- discount-remove display error group group-add hide-single language limit line line-attribute line-attributes line-id
114
- line-minus line-plan line-plus line-remove line-show list load-more market-pending one-time-label option option-list
115
- option-selected option-value order order-bind order-line order-lines order-line-show order-show order-state order-fulfillments
116
- fulfillment-bind order-return return-quantity return-reason return-note orders profile-form address-form addresses address-bind
117
- address-show address-edit address-new address-delete address-default subscriptions subscription-bind subscription-lines
118
- subscription-line subscription-show subscription-action subscription-status subscription-state confirm form-state form-error page-country page-language page-locale page-title plan plan-bind plan-list
119
- plan-value product products purchase purchase-mode qty qty-minus qty-plus quantity recommendations reverse search
120
- search-bind search-collections search-input search-limit search-min search-products search-queries search-results
121
- search-state show soldout-text sort state subscribe-text variant variant-select variant-url max-width max-height crop
122
- sig label hydrated busy`
123
- .split(/\s+/)
124
- .filter(Boolean),
125
- );
126
- /** Attributes that only work inside a `tether-product`. */
127
- const PRODUCT_ONLY = [
128
- 'bind',
129
- 'option-list',
130
- 'option',
131
- 'variant-select',
132
- 'add-to-cart',
133
- 'purchase',
134
- 'plan-list',
135
- 'plan',
136
- 'qty',
137
- 'line-attribute',
138
- ];
139
- const REPEATERS = ['option-list', 'plan-list', 'cart-lines', 'list'];
140
- const METAFIELD_BIND = 'metafield:';
141
- const IMAGE_BIND = /^image:\d+$/;
142
- /** The class names of an element that its description shows. */
143
- const DESCRIBED_CLASSES = 2;
144
-
145
- /** The Levenshtein distance: the fewest single-character edits from one string to the other. */
146
- function editDistance(from: string, to: string): number {
147
- let previous: number[] = Array.from({ length: to.length + 1 }, (_, index) => index);
148
- for (let i = 1; i <= from.length; i++) {
149
- const row = [i];
150
- for (let j = 1; j <= to.length; j++) {
151
- const substitution = (previous[j - 1] ?? 0) + (from[i - 1] === to[j - 1] ? 0 : 1);
152
- row.push(Math.min((previous[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, substitution));
153
- }
154
- previous = row;
155
- }
156
- return previous[to.length] ?? 0;
157
- }
158
-
159
- /**
160
- * @param value What the markup says.
161
- * @param options What Tether reads.
162
- * @param prefix Put before the suggestion, e.g. `tether-`.
163
- * @returns ` Did you mean "…"?` for an option close enough to `value`, else `''`.
164
- */
165
- export function didYouMean(value: string, options: Iterable<string>, prefix = ''): string {
166
- let best: string | null = null;
167
- let bestDistance = Infinity;
168
- for (const option of options) {
169
- const distance = editDistance(value, option);
170
- if (distance < bestDistance) {
171
- bestDistance = distance;
172
- best = option;
173
- }
174
- }
175
- const closeEnough = Math.max(2, Math.floor(value.length / 4));
176
- return best !== null && bestDistance <= closeEnough ? ` Did you mean "${prefix}${best}"?` : '';
177
- }
178
-
179
- /** `el` described for people: `<div#id.first.second>`. */
180
- function describe(el: Element): string {
181
- const id = el.id ? `#${el.id}` : '';
182
- const classes = el.className && typeof el.className === 'string' ? el.className.trim().split(/\s+/) : [];
183
- const classText = classes.length ? `.${classes.slice(0, DESCRIBED_CLASSES).join('.')}` : '';
184
- return `<${el.tagName.toLowerCase()}${id}${classText}>`;
185
- }
186
-
187
- const listSelector = () => `${sel('collection')},${sel('products')},${sel('recommendations')}`;
188
-
189
- function checkProductKeys(root: ParentNode, add: AddProblem): void {
190
- qsa(root, sel('product')).forEach((el) => {
191
- // A list card gets its handle when the list renders.
192
- if (el.closest(listSelector()) && el.closest(sel('list'))) return;
193
- const { key, fromUrl, raw } = resolveProductKey(el);
194
- if (!raw) {
195
- add(
196
- 'error',
197
- `${attr('product')} has no value. Bind it to the Shopify handle (CMS field), or use "url:handle".`,
198
- el,
199
- );
200
- } else if (fromUrl && !key) {
201
- add('warn', `${attr('product')}="${raw}" but this URL has no value for it (e.g. ?handle=my-product).`, el);
202
- }
203
- });
204
- }
205
-
206
- function checkPlacement(root: ParentNode, add: AddProblem): void {
207
- for (const name of PRODUCT_ONLY) {
208
- qsa(root, sel(name)).forEach((el) => {
209
- if (el.closest(sel('product'))) return;
210
- if (name === 'line-attribute' && el.closest(sel('group'))) return;
211
- add('warn', `${attr(name)} outside any ${attr('product')}: it does nothing here.`, el);
212
- });
213
- }
214
- qsa(root, sel('group-add')).forEach((el) => {
215
- if (!el.closest(sel('group'))) add('warn', `${attr('group-add')} outside a ${attr('group')}.`, el);
216
- });
217
- for (const name of REPEATERS) {
218
- qsa(root, sel(name)).forEach((el) => {
219
- if (!el.firstElementChild && !hasTemplate(el)) {
220
- add('warn', `${attr(name)} without a template child: put one example element inside it.`, el);
221
- }
222
- });
223
- }
224
- qsa(root, listSelector()).forEach((el) => {
225
- if (!qsa(el, sel('list')).length) {
226
- add('error', `List without a ${attr('list')} child: products have nowhere to render.`, el);
227
- }
228
- });
229
- }
230
-
231
- function checkValues(root: ParentNode, add: AddProblem): void {
232
- qsa(root, sel('show')).forEach((el) => {
233
- const value = get(el, 'show') ?? '';
234
- if (!SHOW.has(value)) add('warn', `Unknown ${attr('show')}="${value}".${didYouMean(value, SHOW)}`, el);
235
- });
236
- qsa(root, sel('bind')).forEach((el) => {
237
- const value = get(el, 'bind') || '';
238
- if (!value || BIND.has(value) || IMAGE_BIND.test(value) || value.startsWith(METAFIELD_BIND)) return;
239
- add('warn', `Unknown ${attr('bind')}="${value}": it stays empty.${didYouMean(value, BIND)}`, el);
240
- });
241
- }
242
-
243
- function checkMetafieldBinds(root: ParentNode, add: AddProblem): void {
244
- qsa(root, sel('bind')).forEach((el) => {
245
- const value = get(el, 'bind') || '';
246
- if (!value.startsWith(METAFIELD_BIND)) return;
247
- const ref = value.slice(METAFIELD_BIND.length);
248
- const listed = cfg.metafields.some(
249
- (metafield) => (typeof metafield === 'string' ? metafield : `${metafield.namespace}.${metafield.key}`) === ref,
250
- );
251
- if (!listed) {
252
- add('error', `${attr('bind')}="${value}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
253
- }
254
- });
255
- }
256
-
257
- /**
258
- * Typos in attribute names (tether-bnd, tether-add-to-card) do nothing, silently. Each unknown name is reported once.
259
- */
260
- function checkAttributeNames(root: ParentNode, add: AddProblem): void {
261
- const prefix = `${cfg.prefix}-`;
262
- const reported = new Set<string>();
263
- qsa(root, '*').forEach((el) => {
264
- for (const { name: fullName } of el.attributes) {
265
- if (!fullName.startsWith(prefix)) continue;
266
- const name = fullName.slice(prefix.length);
267
- if (ATTRIBUTES.has(name) || reported.has(name)) continue;
268
- reported.add(name);
269
- add('warn', `Unknown attribute ${fullName}: Tether ignores it.${didYouMean(name, ATTRIBUTES, prefix)}`, el);
270
- }
271
- });
272
- }
273
-
274
- function checkCartAndScopes(root: ParentNode, add: AddProblem): void {
275
- const drawers = qsa(root, sel('cart-drawer')).length;
276
- if (qsa(root, sel('cart-open')).length && !drawers) {
277
- add('warn', `${attr('cart-open')} exists but there is no ${attr('cart-drawer')} on this page.`);
278
- }
279
- if (drawers > 1) add('warn', `More than one ${attr('cart-drawer')}.`);
280
- qsa(root, `[${attr('state')}="error"]`).forEach((el) => {
281
- if (el.matches(boundary())) add('error', `Scope failed to load (${get(el, 'error') || 'error'}).`, el);
282
- });
283
- }
284
-
285
- /** The markup problems in `root`, in page order per check. */
286
- export function checkMarkup(root: ParentNode = document): Problem[] {
287
- const problems: Problem[] = [];
288
- const add: AddProblem = (level, message, el) => {
289
- problems.push(el ? { level, message, element: describe(el), el } : { level, message });
290
- };
291
- checkProductKeys(root, add);
292
- checkPlacement(root, add);
293
- checkValues(root, add);
294
- checkAttributeNames(root, add);
295
- checkMetafieldBinds(root, add);
296
- checkCartAndScopes(root, add);
297
- return problems;
298
- }