@occluze/tether 0.6.8 → 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 (215) hide show
  1. package/README.md +28 -28
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
  4. package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
  5. package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
  6. package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
  7. package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
  8. package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
  9. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  10. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  11. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  12. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  13. package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
  14. package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
  15. package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
  16. package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
  17. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  18. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  19. package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
  20. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  21. package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
  22. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/presets/barba.esm.js +19 -23
  26. package/dist/tether.account-pages.js +332 -367
  27. package/dist/tether.account-pages.min.js +1 -1
  28. package/dist/tether.account.js +253 -492
  29. package/dist/tether.account.min.js +1 -1
  30. package/dist/tether.analytics.js +82 -89
  31. package/dist/tether.analytics.min.js +1 -1
  32. package/dist/tether.diagnose.js +52 -34
  33. package/dist/tether.diagnose.min.js +2 -2
  34. package/dist/tether.extras.js +42 -37
  35. package/dist/tether.extras.min.js +1 -1
  36. package/dist/tether.js +1361 -935
  37. package/dist/tether.markets.js +89 -91
  38. package/dist/tether.markets.min.js +1 -1
  39. package/dist/tether.min.js +0 -0
  40. package/dist/tether.min.js.map +4 -4
  41. package/dist/tether.search.js +60 -69
  42. package/dist/tether.search.min.js +1 -1
  43. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  44. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  45. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  46. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  47. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  48. package/dist/types/server/src/shared/json.d.ts +25 -0
  49. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  50. package/dist/types/src/account/addresses-view.d.ts +5 -0
  51. package/dist/types/src/account/answers.d.ts +40 -0
  52. package/dist/types/src/account/markup.d.ts +19 -0
  53. package/dist/types/src/account/operations.d.ts +41 -0
  54. package/dist/types/src/account/order-lines.d.ts +13 -0
  55. package/dist/types/src/account/order-list.d.ts +12 -0
  56. package/dist/types/src/account/order-page.d.ts +6 -0
  57. package/dist/types/src/account/page-answers.d.ts +182 -0
  58. package/dist/types/src/account/page-events.d.ts +2 -0
  59. package/dist/types/src/account/server-client.d.ts +60 -0
  60. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  61. package/dist/types/src/account-api.d.ts +18 -0
  62. package/dist/types/src/account-marks.d.ts +15 -0
  63. package/dist/types/src/account-pages.d.ts +8 -0
  64. package/dist/types/src/account.d.ts +64 -0
  65. package/dist/types/src/amounts.d.ts +12 -0
  66. package/dist/types/src/analytics-shopify.d.ts +41 -0
  67. package/dist/types/src/analytics.d.ts +2 -0
  68. package/dist/types/src/api-version.d.ts +28 -0
  69. package/dist/types/src/astro.d.ts +42 -0
  70. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  71. package/dist/types/src/cart/checkout.d.ts +16 -0
  72. package/dist/types/src/cart/drawer.d.ts +18 -0
  73. package/dist/types/src/cart/mutations.d.ts +85 -0
  74. package/dist/types/src/cart/render.d.ts +17 -0
  75. package/dist/types/src/cart/store.d.ts +185 -0
  76. package/dist/types/src/cart.d.ts +106 -0
  77. package/dist/types/src/client.d.ts +134 -0
  78. package/dist/types/src/collection.d.ts +25 -0
  79. package/dist/types/src/config-sections.d.ts +40 -0
  80. package/dist/types/src/config.d.ts +190 -0
  81. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  82. package/dist/types/src/controls/discount.d.ts +2 -0
  83. package/dist/types/src/controls/extras.d.ts +5 -0
  84. package/dist/types/src/controls/navigation.d.ts +12 -0
  85. package/dist/types/src/diagnose-markup.d.ts +19 -0
  86. package/dist/types/src/diagnose.d.ts +60 -0
  87. package/dist/types/src/dom.d.ts +75 -0
  88. package/dist/types/src/extras.d.ts +21 -0
  89. package/dist/types/src/fragments.d.ts +158 -0
  90. package/dist/types/src/globals.d.ts +15 -0
  91. package/dist/types/src/images.d.ts +20 -0
  92. package/dist/types/src/index.d.ts +167 -0
  93. package/dist/types/src/initial.d.ts +56 -0
  94. package/dist/types/src/lazy.d.ts +28 -0
  95. package/dist/types/src/loader.d.ts +84 -0
  96. package/dist/types/src/market-codes.d.ts +6 -0
  97. package/dist/types/src/markets-ui.d.ts +32 -0
  98. package/dist/types/src/markets.d.ts +72 -0
  99. package/dist/types/src/markup.d.ts +131 -0
  100. package/dist/types/src/mounting.d.ts +46 -0
  101. package/dist/types/src/page-market.d.ts +27 -0
  102. package/dist/types/src/presets/barba.d.ts +56 -0
  103. package/dist/types/src/product-events.d.ts +2 -0
  104. package/dist/types/src/product-render.d.ts +17 -0
  105. package/dist/types/src/product-shape.d.ts +90 -0
  106. package/dist/types/src/product-state.d.ts +110 -0
  107. package/dist/types/src/product.d.ts +66 -0
  108. package/dist/types/src/repeaters.d.ts +16 -0
  109. package/dist/types/src/search.d.ts +33 -0
  110. package/dist/types/src/selling-plans.d.ts +64 -0
  111. package/dist/types/src/server.d.ts +88 -0
  112. package/dist/types/src/shopify-values.d.ts +46 -0
  113. package/dist/types/src/status.d.ts +33 -0
  114. package/dist/types/src/storage.d.ts +12 -0
  115. package/dist/types/src/store-html.d.ts +39 -0
  116. package/dist/types/src/storefront/catalog.d.ts +38 -0
  117. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  118. package/dist/types/src/variant-render.d.ts +14 -0
  119. package/package.json +24 -24
  120. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  121. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  122. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  123. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  124. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  125. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  126. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  127. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  128. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  129. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  130. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  131. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  132. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  133. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  134. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  135. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  136. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  137. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  138. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  139. package/src/account/addresses-view.js +0 -92
  140. package/src/account/browser-session.js +0 -359
  141. package/src/account/markup.js +0 -48
  142. package/src/account/operations.js +0 -180
  143. package/src/account/order-lines.js +0 -96
  144. package/src/account/order-list.js +0 -91
  145. package/src/account/order-page.js +0 -192
  146. package/src/account/page-events.js +0 -288
  147. package/src/account/server-client.js +0 -172
  148. package/src/account/shared.js +0 -111
  149. package/src/account/shopify-auth.js +0 -150
  150. package/src/account/subscriptions-view.js +0 -158
  151. package/src/account-api.js +0 -118
  152. package/src/account-links.js +0 -35
  153. package/src/account-pages.js +0 -89
  154. package/src/account.js +0 -342
  155. package/src/amounts.js +0 -36
  156. package/src/analytics-shopify.js +0 -241
  157. package/src/analytics.js +0 -352
  158. package/src/api-version.js +0 -85
  159. package/src/astro.js +0 -266
  160. package/src/cart/checkout.js +0 -97
  161. package/src/cart/drawer.js +0 -138
  162. package/src/cart/mutations.js +0 -234
  163. package/src/cart/render.js +0 -294
  164. package/src/cart/store.js +0 -326
  165. package/src/cart.js +0 -391
  166. package/src/client.js +0 -267
  167. package/src/collection.js +0 -183
  168. package/src/config.js +0 -349
  169. package/src/controls/cart-controls.js +0 -155
  170. package/src/controls/discount.js +0 -96
  171. package/src/controls/extras.js +0 -63
  172. package/src/controls/navigation.js +0 -83
  173. package/src/diagnose-markup.js +0 -333
  174. package/src/diagnose.js +0 -295
  175. package/src/dom.js +0 -287
  176. package/src/extras.js +0 -145
  177. package/src/fragments.js +0 -237
  178. package/src/images.js +0 -104
  179. package/src/index.js +0 -353
  180. package/src/initial.js +0 -87
  181. package/src/lazy-iife.js +0 -116
  182. package/src/lazy.js +0 -39
  183. package/src/loader.js +0 -337
  184. package/src/market-codes.js +0 -35
  185. package/src/markets-ui.js +0 -204
  186. package/src/markets.js +0 -334
  187. package/src/markup.js +0 -64
  188. package/src/mounting.js +0 -277
  189. package/src/page-market.js +0 -116
  190. package/src/parts/account-pages.js +0 -6
  191. package/src/parts/account.js +0 -6
  192. package/src/parts/analytics.js +0 -6
  193. package/src/parts/diagnose.js +0 -6
  194. package/src/parts/extras.js +0 -6
  195. package/src/parts/markets.js +0 -6
  196. package/src/parts/search.js +0 -6
  197. package/src/presets/barba.js +0 -190
  198. package/src/product-events.js +0 -130
  199. package/src/product-render.js +0 -257
  200. package/src/product-state.js +0 -318
  201. package/src/product.js +0 -344
  202. package/src/repeaters.js +0 -173
  203. package/src/search.js +0 -323
  204. package/src/selling-plans.js +0 -154
  205. package/src/server.js +0 -215
  206. package/src/shopify-values.js +0 -89
  207. package/src/status.js +0 -126
  208. package/src/store-html.js +0 -0
  209. package/src/storefront/catalog.js +0 -199
  210. package/src/storefront/json-ld.js +0 -167
  211. package/src/variant-render.js +0 -165
  212. package/types/astro.d.ts +0 -28
  213. package/types/barba.d.ts +0 -16
  214. package/types/index.d.ts +0 -577
  215. package/types/server.d.ts +0 -182
package/src/diagnose.js DELETED
@@ -1,295 +0,0 @@
1
- /**
2
- * `Tether.diagnose()`: one call in the browser console that explains what's wrong on a page. Logs a readable report
3
- * and returns it as an object. The markup checks are in diagnose-markup.js.
4
- *
5
- * await Tether.diagnose()
6
- * await Tether.diagnose({ silent: true }) // just the object
7
- */
8
- import { cfg, isConfigured } from './config.js';
9
- import { gql, endpoint } from './client.js';
10
- import { sel, get, qsa } from './dom.js';
11
- import { getProduct } from './loader.js';
12
- import { allPlans } from './selling-plans.js';
13
- import { resolveProductKey } from './status.js';
14
- import { apiVersionStatus } from './api-version.js';
15
- import { loadPart } from './lazy.js';
16
- import { checkMarkup, didYouMean } from './diagnose-markup.js';
17
- /** @import { ApiVersionStatus } from './api-version.js' */
18
- /** @import { DetectedMarket } from './markets.js' */
19
- /** @import { Problem } from './diagnose-markup.js' */
20
- /** @import { Product } from './fragments.js' */
21
-
22
- export { apiVersionStatus, checkMarkup, didYouMean };
23
-
24
- /**
25
- * @typedef {object} MarketReport The market in use (a visitor's choice included), and what detection would pick.
26
- * @property {string | null} country
27
- * @property {string | null} language
28
- * @property {string | null} locale
29
- * @property {DetectedMarket | null} detected `null` without `markets.detect`.
30
- */
31
- /** @typedef {{ name: string, url: string, currency: string }} ShopReport */
32
- /**
33
- * @typedef {{ key: string, handle: string, title: string, plans: number, requiresSellingPlan: boolean }
34
- * | { key: string, error: string | undefined }} ProductReport
35
- */
36
- /** @typedef {{ product: string, group: string, plan: string, id: string }} SellingPlanReport */
37
- /**
38
- * @typedef {object} DiagnoseReport
39
- * @property {string | undefined} version
40
- * @property {Record<string, unknown>} config The config, with the token masked.
41
- * @property {ApiVersionStatus} apiVersion
42
- * @property {MarketReport} market
43
- * @property {boolean} reachable
44
- * @property {ShopReport | null} shop
45
- * @property {number | null} latencyMs
46
- * @property {ProductReport[]} products
47
- * @property {SellingPlanReport[]} sellingPlans
48
- * @property {Problem[]} problems
49
- */
50
- /**
51
- * @typedef {{ kind: 'not-configured' }
52
- * | { kind: 'reachable', shop: ShopReport, latencyMs: number }
53
- * | { kind: 'unreachable', error: unknown, latencyMs: number }} StorefrontCheck
54
- */
55
- /**
56
- * @typedef {object} DiagnoseOptions
57
- * @property {ParentNode} [root]
58
- * @property {boolean} [silent] Return the report without logging it.
59
- * @property {string} [version] Tether's version, for the report.
60
- * @property {Date} [now] The date the API version's support is measured against.
61
- */
62
-
63
- /** Products checked at most, so a long CMS page doesn't send a request per card. */
64
- const MAX_PRODUCTS_CHECKED = 20;
65
- /** Matches the warning window of `apiVersionStatus`. */
66
- const API_UPGRADE_WARNING_MONTHS = 3;
67
- const TOKEN_EDGE_CHARACTERS = 4;
68
-
69
- /** @param {string} token */
70
- function maskToken(token) {
71
- if (!token) return '(missing)';
72
- return `${token.slice(0, TOKEN_EDGE_CHARACTERS)}…${token.slice(-TOKEN_EDGE_CHARACTERS)} (${token.length} chars)`;
73
- }
74
-
75
- /**
76
- * @param {unknown} error
77
- * @param {string} fallback
78
- * @returns {string} The error's message, else its code (a `TetherError` has one), else `fallback`.
79
- */
80
- function errorText(error, fallback) {
81
- if (error instanceof Error && error.message) return error.message;
82
- /** @type {unknown} */
83
- const code = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
84
- return typeof code === 'string' && code ? code : fallback;
85
- }
86
-
87
- /** @returns {Record<string, unknown>} */
88
- function configSummary() {
89
- return {
90
- domain: cfg.domain || '(missing)',
91
- token: maskToken(cfg.token),
92
- apiVersion: cfg.apiVersion,
93
- endpoint: cfg.domain ? endpoint() : null,
94
- country: cfg.country,
95
- language: cfg.language,
96
- locale: cfg.locale,
97
- markets: cfg.markets,
98
- prefix: cfg.prefix,
99
- productPath: cfg.productPath,
100
- metafields: cfg.metafields,
101
- autoMount: cfg.autoMount,
102
- observe: cfg.observe,
103
- onError: typeof cfg.onError === 'function',
104
- };
105
- }
106
-
107
- /**
108
- * Detection runs in the markets part, which init has already loaded whenever `markets.detect` is set.
109
- *
110
- * @returns {Promise<MarketReport>}
111
- */
112
- async function marketSummary() {
113
- return {
114
- country: cfg.country || null,
115
- language: cfg.language || null,
116
- locale: cfg.locale || null,
117
- detected: cfg.markets?.detect ? (await loadPart('markets')).detectMarket(cfg.markets) : null,
118
- };
119
- }
120
-
121
- /**
122
- * @param {ApiVersionStatus} status
123
- * @returns {Problem | null} An error for an unsupported version, a warning when support ends soon.
124
- */
125
- function apiVersionProblem({ ok, monthsLeft, note }) {
126
- if (ok && (monthsLeft === undefined || monthsLeft >= API_UPGRADE_WARNING_MONTHS)) return null;
127
- return { level: ok ? 'warn' : 'error', message: `apiVersion ${cfg.apiVersion}: ${note}` };
128
- }
129
-
130
- /** @returns {Promise<StorefrontCheck>} */
131
- async function checkStorefront() {
132
- if (!isConfigured()) return { kind: 'not-configured' };
133
- const startedAt = Date.now();
134
- try {
135
- /** @type {{ shop: { name: string, primaryDomain: { url: string }, paymentSettings: { currencyCode: string } } }} */
136
- const data = await gql(
137
- 'query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }',
138
- {},
139
- { context: false },
140
- );
141
- const shop = {
142
- name: data.shop.name,
143
- url: data.shop.primaryDomain.url,
144
- currency: data.shop.paymentSettings.currencyCode,
145
- };
146
- return { kind: 'reachable', shop, latencyMs: Date.now() - startedAt };
147
- } catch (error) {
148
- return { kind: 'unreachable', error, latencyMs: Date.now() - startedAt };
149
- }
150
- }
151
-
152
- /**
153
- * Options a visitor can't choose on this page get the first available variant's value.
154
- *
155
- * @param {ParentNode} root
156
- * @param {string} key
157
- * @param {Product} product
158
- * @returns {Problem[]}
159
- */
160
- function checkOptions(root, key, product) {
161
- /** @type {Problem[]} */
162
- const problems = [];
163
- const names = product.options.map((option) => option.name);
164
- const roots = qsa(root, sel('product')).filter(
165
- (el) => resolveProductKey(el).key === key && el.querySelector(sel('add-to-cart')),
166
- );
167
- for (const el of roots) {
168
- const shown = new Set(
169
- qsa(el, `${sel('option-list')},select${sel('option')}`).map(
170
- (list) => get(list, 'option-list') || get(list, 'option'),
171
- ),
172
- );
173
- if (!shown.size && qsa(el, `select${sel('variant-select')}`).length) continue;
174
- // Translated option names (a language in the market) don't match the names in the markup.
175
- if (!cfg.language) {
176
- for (const name of shown) {
177
- if (!name || names.includes(name)) continue;
178
- problems.push({
179
- level: 'warn',
180
- message: `Product "${product.handle}" has no option "${name}" (its options: ${names.join(', ') || 'none'}).${didYouMean(name, names)}`,
181
- });
182
- }
183
- }
184
- for (const option of product.options) {
185
- if (option.optionValues.length <= 1 || shown.has(option.name)) continue;
186
- problems.push({
187
- level: 'warn',
188
- message: `Product "${product.handle}": option "${option.name}" (${option.optionValues.length} values) has no tether-option-list, so visitors can't choose it.`,
189
- });
190
- }
191
- }
192
- return problems;
193
- }
194
-
195
- /**
196
- * Loads the page's products and adds what it finds about each to `report`.
197
- *
198
- * @param {ParentNode} root
199
- * @param {DiagnoseReport} report
200
- */
201
- async function checkProducts(root, report) {
202
- const allKeys = qsa(root, sel('product')).map((el) => resolveProductKey(el).key);
203
- const keys = [...new Set(allKeys.filter((key) => key !== null && key !== ''))].slice(0, MAX_PRODUCTS_CHECKED);
204
- const results = await Promise.allSettled(keys.map((key) => getProduct(key)));
205
- results.forEach((result, index) => {
206
- const key = keys[index] ?? '';
207
- if (result.status === 'rejected') {
208
- const reason = errorText(result.reason, 'not loaded');
209
- report.products.push({ key, error: result.reason instanceof Error ? result.reason.message : undefined });
210
- report.problems.push({
211
- level: 'error',
212
- message: `Product "${key}": ${reason}. Is it published to the Headless channel?`,
213
- });
214
- return;
215
- }
216
- const product = result.value;
217
- const plans = allPlans(product);
218
- report.products.push({
219
- key,
220
- handle: product.handle,
221
- title: product.title,
222
- plans: plans.length,
223
- requiresSellingPlan: product.requiresSellingPlan,
224
- });
225
- for (const plan of plans) {
226
- report.sellingPlans.push({ product: product.handle, group: plan.group.name, plan: plan.name, id: plan.id });
227
- }
228
- report.problems.push(...checkOptions(root, key, product));
229
- });
230
- }
231
-
232
- /**
233
- * @param {DiagnoseOptions} [options]
234
- * @returns {Promise<DiagnoseReport>} What's wrong on the page: config, token, API version, products, plans, markup.
235
- */
236
- export async function diagnose({ root = document, silent = false, version, now = new Date() } = {}) {
237
- const storefront = await checkStorefront();
238
- /** @type {DiagnoseReport} */
239
- const report = {
240
- version,
241
- config: configSummary(),
242
- apiVersion: apiVersionStatus(cfg.apiVersion, now),
243
- market: await marketSummary(),
244
- reachable: storefront.kind === 'reachable',
245
- shop: storefront.kind === 'reachable' ? storefront.shop : null,
246
- latencyMs: storefront.kind === 'not-configured' ? null : storefront.latencyMs,
247
- products: [],
248
- sellingPlans: [],
249
- problems: [],
250
- };
251
- switch (storefront.kind) {
252
- case 'not-configured':
253
- report.problems.push({ level: 'error', message: 'TETHER_CONFIG needs domain and token.' });
254
- break;
255
- case 'unreachable':
256
- report.problems.push({
257
- level: 'error',
258
- message: `Storefront API not reachable: ${errorText(storefront.error, 'unknown error')}`,
259
- });
260
- break;
261
- case 'reachable':
262
- await checkProducts(root, report);
263
- break;
264
- }
265
- const versionProblem = apiVersionProblem(report.apiVersion);
266
- if (versionProblem) report.problems.push(versionProblem);
267
- if (typeof document !== 'undefined') report.problems.push(...checkMarkup(root));
268
- if (!silent) print(report);
269
- return report;
270
- }
271
-
272
- /** @param {DiagnoseReport} report */
273
- function print(report) {
274
- const errors = report.problems.filter((problem) => problem.level === 'error').length;
275
- const warnings = report.problems.length - errors;
276
- console.group(`[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : 'no errors'}, ${warnings} warning(s)`);
277
- console.log('version', report.version, '| API', report.apiVersion.version, `(${report.apiVersion.note})`);
278
- console.log('config', report.config);
279
- const { country, language, detected } = report.market;
280
- const inUse = [country, language].filter(Boolean).join(' / ') || 'the store default';
281
- console.log('market', `in use: ${inUse}`, detected ? `(detection picks ${JSON.stringify(detected)})` : '');
282
- console.log(
283
- report.shop
284
- ? `Storefront API reachable: ${report.shop.name} (${report.latencyMs ?? 0} ms)`
285
- : 'Storefront API NOT reachable',
286
- );
287
- if (report.products.length) console.table(report.products);
288
- if (report.sellingPlans.length) console.table(report.sellingPlans);
289
- else if (report.reachable) console.log('No selling plans on the products of this page.');
290
- for (const problem of report.problems) {
291
- if (problem.level === 'error') console.error(problem.message, problem.el ?? '');
292
- else console.warn(problem.message, problem.el ?? '');
293
- }
294
- console.groupEnd();
295
- }
package/src/dom.js DELETED
@@ -1,287 +0,0 @@
1
- /**
2
- * The browser modules' DOM toolkit: what Shopify values look like on the page (money, dates, text, images, lists)
3
- * and how elements show, hide and report layout changes. Attribute names go through `attr()` so the prefix is
4
- * configurable. The optional parts import all of it from here (`window.Tether.__core['dom.js']`).
5
- */
6
- import { cfg, reportError, warn } from './config.js';
7
- import { attr, emit, get } from './markup.js';
8
- import { storeHtmlFragment } from './store-html.js';
9
- import { formatMoney as formatMoneyIn, productUrl as productUrlFor } from './shopify-values.js';
10
- /** @import { Money } from './shopify-values.js' */
11
-
12
- export { attr, sel, get, has, qsa, scoped, emit } from './markup.js';
13
- export { imageUrl, cdnUrl, sameImage, setImage } from './images.js';
14
- export { hasTemplate, repeat, repeatKeyed } from './repeaters.js';
15
-
16
- /**
17
- * State Tether keeps on elements, as `data-tether-*` attributes whatever the prefix: sites style and test against
18
- * these names, so they stay fixed where the `tether-*` markup attributes follow `prefix`.
19
- */
20
- const SEEN = 'data-tether-seen';
21
- const SHOWN = 'data-tether-shown';
22
- const PREVIOUS_DISPLAY = 'data-tether-prev-display';
23
-
24
- /** Async renders change the page height in bursts; one notification per burst. */
25
- const LAYOUT_DEBOUNCE_MS = 120;
26
-
27
- /**
28
- * The product page of a product (`productPath`).
29
- *
30
- * @param {string | null | undefined} handle
31
- * @param {string | null} [id]
32
- */
33
- export function productUrl(handle, id) {
34
- return productUrlFor(cfg.productPath || '/product', handle, id);
35
- }
36
-
37
- /**
38
- * `Intl` throws a RangeError for a locale or currency it doesn't know; the caller then shows a plain fallback.
39
- *
40
- * @template T
41
- * @param {() => T} format
42
- * @param {T} fallback
43
- * @returns {T}
44
- */
45
- function formatOr(format, fallback) {
46
- try {
47
- return format();
48
- } catch (error) {
49
- if (error instanceof RangeError) return fallback;
50
- throw error;
51
- }
52
- }
53
-
54
- /**
55
- * `30` → `30%` (en, nl) or `30 %` (de, fr), in the configured locale.
56
- *
57
- * @param {number | null | undefined} percentage
58
- * @param {string | undefined} [locale]
59
- * @returns {string} `''` for no or a zero percentage.
60
- */
61
- export function formatPercent(percentage, locale = cfg.locale) {
62
- if (!percentage) return '';
63
- const format = () =>
64
- new Intl.NumberFormat(locale || undefined, { style: 'percent', maximumFractionDigits: 0 }).format(percentage / 100);
65
- return formatOr(format, `${percentage}%`);
66
- }
67
-
68
- /**
69
- * Money in the configured locale, through the site's `formatMoney` when it has one.
70
- *
71
- * @param {Money | null | undefined} money
72
- */
73
- export function formatMoney(money) {
74
- if (!money) return '';
75
- if (cfg.formatMoney) return cfg.formatMoney(money, cfg.locale);
76
- return formatMoneyIn(money, cfg.locale);
77
- }
78
-
79
- /**
80
- * @param {string | null | undefined} iso
81
- * @returns {string} The date in the configured locale, `''` without one.
82
- */
83
- export function formatDate(iso) {
84
- if (!iso) return '';
85
- return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || undefined), '');
86
- }
87
-
88
- /**
89
- * Writes HTML from the Shopify admin (product and collection descriptions) into `el`, with script removed
90
- * (`store-html.js`). Everything else from Shopify is written as text (`setText`).
91
- *
92
- * @param {Element} el
93
- * @param {string | null | undefined} html
94
- */
95
- export function setStoreHtml(el, html) {
96
- el.replaceChildren(storeHtmlFragment(html, el.ownerDocument));
97
- }
98
-
99
- /**
100
- * @param {Element} el
101
- * @returns {el is HTMLInputElement | HTMLTextAreaElement}
102
- */
103
- const isTextField = (el) => el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
104
-
105
- /**
106
- * @param {Element} el
107
- * @returns {el is HTMLInputElement}
108
- */
109
- const isInput = (el) => el.tagName === 'INPUT';
110
-
111
- /**
112
- * Sets the text of `el`, or its value when it is a form field.
113
- *
114
- * @param {Element} el
115
- * @param {string | number | boolean | null | undefined} value `null` and `undefined` clear it.
116
- */
117
- export function setText(el, value) {
118
- const text = value == null ? '' : String(value);
119
- if (isTextField(el)) el.value = text;
120
- else el.textContent = text;
121
- }
122
-
123
- /** @param {Element} el */
124
- const readText = (el) => (isTextField(el) ? el.value : el.textContent);
125
-
126
- /**
127
- * `setText` that hands a change after the first render to `onValueChange`, for scramble and roll effects.
128
- *
129
- * @param {HTMLElement} el
130
- * @param {string | number | boolean | null | undefined} value
131
- */
132
- export function setTextTracked(el, value) {
133
- const before = readText(el);
134
- setText(el, value);
135
- if (before !== readText(el) && el.hasAttribute(SEEN) && cfg.onValueChange) {
136
- try {
137
- cfg.onValueChange(el, before);
138
- } catch (error) {
139
- reportError(error, 'onValueChange');
140
- }
141
- }
142
- el.setAttribute(SEEN, '1');
143
- }
144
-
145
- /**
146
- * Shows an element hidden before the script ran (a class, an inline `display: none`, a `hidden` attribute), as
147
- * `block` or the value of `tether-display`.
148
- *
149
- * @param {HTMLElement} el
150
- */
151
- function reveal(el) {
152
- if (el.hasAttribute(SHOWN)) return;
153
- el.setAttribute(SHOWN, '');
154
- el.removeAttribute('hidden');
155
- const previous = el.getAttribute(PREVIOUS_DISPLAY);
156
- if (previous !== null) {
157
- el.style.display = previous;
158
- el.removeAttribute(PREVIOUS_DISPLAY);
159
- } else if (el.style.display === 'none') {
160
- el.style.display = '';
161
- }
162
- if (typeof getComputedStyle === 'function' && getComputedStyle(el).display === 'none') {
163
- el.style.display = el.getAttribute(attr('display')) || 'block';
164
- }
165
- }
166
-
167
- /**
168
- * Shows or hides `el`, restoring its own display value when shown again.
169
- *
170
- * @param {HTMLElement} el
171
- * @param {boolean} visible
172
- */
173
- export function show(el, visible) {
174
- if (visible) {
175
- reveal(el);
176
- return;
177
- }
178
- el.removeAttribute(SHOWN);
179
- // The display value to restore is a real one, never 'none'.
180
- if (!el.hasAttribute(PREVIOUS_DISPLAY) && el.style.display !== 'none') {
181
- el.setAttribute(PREVIOUS_DISPLAY, el.style.display);
182
- }
183
- el.style.display = 'none';
184
- }
185
-
186
- /**
187
- * Marks an option button or input as chosen: `is-selected`, `aria-pressed`, and its own or inner radio/checkbox.
188
- *
189
- * @param {Element} el
190
- * @param {boolean} selected
191
- */
192
- export function setSelected(el, selected) {
193
- el.classList.toggle('is-selected', selected);
194
- el.setAttribute('aria-pressed', selected ? 'true' : 'false');
195
- if (isInput(el) && (el.type === 'radio' || el.type === 'checkbox')) el.checked = selected;
196
- const input = el.querySelector('input[type="radio"],input[type="checkbox"]');
197
- if (input && isInput(input)) input.checked = selected;
198
- }
199
-
200
- /** @type {ReturnType<typeof setTimeout> | undefined} */
201
- let layoutTimer;
202
-
203
- /**
204
- * Async renders change the page height after the site's page-transition hooks ran. After a burst of them this calls
205
- * `onLayoutChange` (else refreshes ScrollTrigger) and emits `tether:layout-changed`.
206
- *
207
- * @param {string} reason
208
- */
209
- export function notifyLayoutChanged(reason) {
210
- clearTimeout(layoutTimer);
211
- layoutTimer = setTimeout(() => {
212
- // GSAP's ScrollTrigger, when the site has it, measures positions that a height change makes stale.
213
- const { ScrollTrigger } = /** @type {Window & { ScrollTrigger?: { refresh?: () => void } }} */ (window);
214
- if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
215
- else ScrollTrigger?.refresh?.();
216
- emit('layout-changed', { reason });
217
- }, LAYOUT_DEBOUNCE_MS);
218
- }
219
-
220
- /**
221
- * Notifies a layout change (`<reason>:images`) once every image in `root` that is still loading has loaded or failed.
222
- *
223
- * @param {ParentNode} root
224
- * @param {string} reason
225
- */
226
- export function watchImages(root, reason) {
227
- const loading = [...root.querySelectorAll('img')].filter((img) => !img.complete);
228
- let left = loading.length;
229
- const done = () => {
230
- left -= 1;
231
- if (left === 0) notifyLayoutChanged(`${reason}:images`);
232
- };
233
- loading.forEach((img) => {
234
- img.addEventListener('load', done, { once: true });
235
- img.addEventListener('error', done, { once: true });
236
- });
237
- }
238
-
239
- /**
240
- * Runs `task` with `aria-busy` on `el`; does nothing while `el` is already busy.
241
- *
242
- * @template T
243
- * @param {Element} el
244
- * @param {() => T | Promise<T>} task
245
- * @returns {Promise<T | undefined>} The task's result, `undefined` when it didn't run.
246
- */
247
- export function busy(el, task) {
248
- if (el.getAttribute('aria-busy') === 'true') return Promise.resolve(undefined);
249
- el.setAttribute('aria-busy', 'true');
250
- return Promise.resolve()
251
- .then(task)
252
- .finally(() => {
253
- el.removeAttribute('aria-busy');
254
- });
255
- }
256
-
257
- /**
258
- * @param {Element | null | undefined} el
259
- * @param {string} name
260
- * @returns {unknown} The attribute's JSON, `null` when it is missing or not JSON (with a warning).
261
- */
262
- export function parseJSONAttr(el, name) {
263
- const raw = el ? get(el, name) : null;
264
- if (!raw) return null;
265
- try {
266
- return /** @type {unknown} */ (JSON.parse(raw));
267
- } catch {
268
- warn(`invalid JSON in ${attr(name)}`, el);
269
- return null;
270
- }
271
- }
272
-
273
- /** @typedef {string | number | boolean | null | undefined} AttributeValue */
274
-
275
- /**
276
- * `{ a: 1 }` or `[{ key, value }]` → `[{ key, value }]` as Shopify wants cart and line attributes.
277
- *
278
- * @param {Record<string, AttributeValue> | { key: string | number, value?: AttributeValue }[] | null | undefined} attributes
279
- * @returns {{ key: string, value: string }[]}
280
- */
281
- export function toAttributes(attributes) {
282
- if (!attributes) return [];
283
- if (Array.isArray(attributes)) {
284
- return attributes.map((attribute) => ({ key: String(attribute.key), value: String(attribute.value ?? '') }));
285
- }
286
- return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? '') }));
287
- }