@occluze/tether 0.7.0 → 0.7.1

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 (132) hide show
  1. package/README.md +21 -21
  2. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-CXMA5RPZ.js} +1 -1
  3. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IZO7FEOZ.js} +1 -1
  4. package/dist/esm/chunks/{analytics-2V632IRV.js → analytics-PG5FPRDN.js} +1 -1
  5. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-4HD4J5JC.js} +1 -1
  6. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-6TFMLT4A.js} +2 -2
  7. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-BBVUVPMO.js} +1 -1
  8. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  9. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  10. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  11. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-N3V45JPS.js} +1 -1
  12. package/dist/esm/chunks/{chunk-YLUULATY.js → chunk-RPMBCAGS.js} +1 -1
  13. package/dist/esm/chunks/{chunk-MPDWM2NN.js → chunk-VFNHIGCS.js} +1 -1
  14. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-WT7RGGJU.js} +1 -1
  15. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  16. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  17. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-XCEBQKBO.js} +1 -1
  18. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  19. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-M4IQ62CT.js} +1 -1
  20. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/server.js +2 -2
  23. package/dist/tether.account-pages.js +13 -3
  24. package/dist/tether.account-pages.min.js +1 -1
  25. package/dist/tether.account.js +13 -3
  26. package/dist/tether.account.min.js +1 -1
  27. package/dist/tether.analytics.js +18 -3
  28. package/dist/tether.analytics.min.js +1 -1
  29. package/dist/tether.diagnose.js +18 -3
  30. package/dist/tether.diagnose.min.js +2 -2
  31. package/dist/tether.extras.js +19 -6
  32. package/dist/tether.extras.min.js +1 -1
  33. package/dist/tether.js +162 -112
  34. package/dist/tether.markets.js +18 -3
  35. package/dist/tether.markets.min.js +1 -1
  36. package/dist/tether.min.js +3 -3
  37. package/dist/tether.min.js.map +4 -4
  38. package/dist/tether.search.js +18 -3
  39. package/dist/tether.search.min.js +1 -1
  40. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  41. package/dist/types/src/cart/mutations.d.ts +6 -0
  42. package/dist/types/src/cart/store.d.ts +5 -2
  43. package/dist/types/src/cart.d.ts +2 -2
  44. package/dist/types/src/client.d.ts +53 -19
  45. package/dist/types/src/config.d.ts +6 -1
  46. package/dist/types/src/index.d.ts +2 -2
  47. package/dist/types/src/product-shape.d.ts +3 -1
  48. package/package.json +1 -2
  49. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  50. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  51. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  52. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  53. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  54. package/dist/esm/chunks/search-UJ73YBQ5.js +0 -1
  55. package/src/account/addresses-view.ts +0 -58
  56. package/src/account/answers.ts +0 -79
  57. package/src/account/markup.ts +0 -36
  58. package/src/account/operations.ts +0 -145
  59. package/src/account/order-lines.ts +0 -75
  60. package/src/account/order-list.ts +0 -107
  61. package/src/account/order-page.ts +0 -137
  62. package/src/account/page-answers.ts +0 -317
  63. package/src/account/page-events.ts +0 -253
  64. package/src/account/server-client.ts +0 -183
  65. package/src/account/subscriptions-view.ts +0 -115
  66. package/src/account-api.ts +0 -98
  67. package/src/account-marks.ts +0 -35
  68. package/src/account-pages.ts +0 -81
  69. package/src/account.ts +0 -284
  70. package/src/amounts.ts +0 -64
  71. package/src/analytics-shopify.ts +0 -246
  72. package/src/analytics.ts +0 -315
  73. package/src/api-version.ts +0 -79
  74. package/src/astro.ts +0 -272
  75. package/src/cart/cart-shape.ts +0 -25
  76. package/src/cart/checkout.ts +0 -101
  77. package/src/cart/drawer.ts +0 -125
  78. package/src/cart/mutations.ts +0 -237
  79. package/src/cart/render.ts +0 -266
  80. package/src/cart/store.ts +0 -279
  81. package/src/cart.ts +0 -345
  82. package/src/client.ts +0 -391
  83. package/src/collection.ts +0 -175
  84. package/src/config-sections.ts +0 -284
  85. package/src/config.ts +0 -480
  86. package/src/controls/cart-controls.ts +0 -123
  87. package/src/controls/discount.ts +0 -75
  88. package/src/controls/extras.ts +0 -54
  89. package/src/controls/navigation.ts +0 -77
  90. package/src/diagnose-markup.ts +0 -298
  91. package/src/diagnose.ts +0 -294
  92. package/src/dom.ts +0 -268
  93. package/src/extras.ts +0 -136
  94. package/src/fragments.ts +0 -261
  95. package/src/globals.d.ts +0 -15
  96. package/src/images.ts +0 -91
  97. package/src/index.ts +0 -415
  98. package/src/initial.ts +0 -101
  99. package/src/lazy-iife.ts +0 -197
  100. package/src/lazy.ts +0 -42
  101. package/src/loader.ts +0 -363
  102. package/src/market-codes.ts +0 -6
  103. package/src/markets-ui.ts +0 -227
  104. package/src/markets.ts +0 -246
  105. package/src/markup.ts +0 -127
  106. package/src/mounting.ts +0 -264
  107. package/src/page-market.ts +0 -110
  108. package/src/part-integrity.ts +0 -7
  109. package/src/parts/account-pages.ts +0 -7
  110. package/src/parts/account.ts +0 -7
  111. package/src/parts/analytics.ts +0 -7
  112. package/src/parts/diagnose.ts +0 -7
  113. package/src/parts/extras.ts +0 -7
  114. package/src/parts/markets.ts +0 -7
  115. package/src/parts/search.ts +0 -7
  116. package/src/presets/barba.ts +0 -194
  117. package/src/product-events.ts +0 -115
  118. package/src/product-render.ts +0 -218
  119. package/src/product-shape.ts +0 -49
  120. package/src/product-state.ts +0 -318
  121. package/src/product.ts +0 -308
  122. package/src/repeaters.ts +0 -161
  123. package/src/search.ts +0 -305
  124. package/src/selling-plans.ts +0 -145
  125. package/src/server.ts +0 -211
  126. package/src/shopify-values.ts +0 -91
  127. package/src/status.ts +0 -106
  128. package/src/storage.ts +0 -34
  129. package/src/store-html.ts +0 -359
  130. package/src/storefront/catalog.ts +0 -196
  131. package/src/storefront/json-ld.ts +0 -144
  132. 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
- }