@occluze/tether 0.6.8 → 0.7.0

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