@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
package/src/config.ts ADDED
@@ -0,0 +1,480 @@
1
+ /**
2
+ * The runtime configuration. `cfg` is one shared object that every browser module imports; `configure()` replaces its
3
+ * contents in place, so those imports see the new values, and `setCountry()` changes its market keys the same way.
4
+ * Every option is validated here, once, where it arrives: values that end up in query text, URLs, attribute names or
5
+ * storage keys never reach the code that uses them unchecked.
6
+ */
7
+ import { STOREFRONT_API_VERSION } from '../server/src/shared/api-versions.ts';
8
+ import { isQuarterlyVersion } from './api-version.ts';
9
+ import {
10
+ accountOption,
11
+ analyticsOption,
12
+ domainOption,
13
+ embedOriginsOption,
14
+ marketsOption,
15
+ metafieldsOption,
16
+ partsUrlOption,
17
+ } from './config-sections.ts';
18
+
19
+ export { MARKET_SOURCES } from './config-sections.ts';
20
+ import { toCountry, toLanguage } from './market-codes.ts';
21
+ import type { ApiVersion } from '../server/src/shared/api-versions.ts';
22
+ import type { ShopDomain } from './config-sections.ts';
23
+ import type { Money } from './shopify-values.ts';
24
+ import type { CountryCode, LanguageCode } from './market-codes.ts';
25
+
26
+ /** Where the visitor's market comes from (`markets.ts`). */
27
+ export interface MarketsConfig {
28
+ /** True: every source; a list: only those. */
29
+ detect?: boolean | ('saved' | 'server' | 'webflow')[];
30
+ /** Webflow locale (subdirectory or html lang) to market. */
31
+ locales?: Record<string, { country?: string; language?: string; locale?: string }>;
32
+ /** The cookie a server sets with the visitor's country. */
33
+ cookie?: string;
34
+ /** Remember the visitor's choice (default true). */
35
+ remember?: boolean;
36
+ /** With `locales`: ignore an html lang that isn't in the map. */
37
+ strict?: boolean;
38
+ }
39
+
40
+ /** The optional analytics part. */
41
+ export interface AnalyticsConfig {
42
+ /** GA4/GTM events, into `window.dataLayer` or the named array. */
43
+ dataLayer?: boolean | string;
44
+ /** Shopify's own analytics. `proxy`: the site serves the Storefront API on its own domain. */
45
+ shopify?: boolean | { consent?: 'no-banner' | 'default-banner' | 'custom-banner'; proxy?: boolean };
46
+ onEvent?: (name: string, data: Record<string, unknown>) => void;
47
+ }
48
+
49
+ /** The optional customer accounts part, which runs through the site's Worker. */
50
+ export interface AccountConfig {
51
+ /** The Worker's account routes, a URL or a path on this site. */
52
+ server: string;
53
+ /** The site's own order page, with `{id}` for the numeric order id. */
54
+ orderUrl?: string;
55
+ /** Shopify's subscription management page. */
56
+ subscriptionsUrl?: string;
57
+ /** A separate page for payment methods (default `subscriptionsUrl`). */
58
+ paymentUrl?: string;
59
+ /** Where Shopify sends the visitor after logout (default the home page). */
60
+ logoutUrl?: string;
61
+ /** The language of Shopify's login pages. */
62
+ locale?: string;
63
+ /** Replaces `location.assign` for the login redirect (popups, tests). */
64
+ navigate?: (url: string) => void;
65
+ }
66
+
67
+ export interface MetafieldIdentifier {
68
+ namespace: string;
69
+ key: string;
70
+ }
71
+ /** `'namespace.key'` or the identifier itself. */
72
+ export type MetafieldRef = string | MetafieldIdentifier;
73
+ /** Shopify's note on a cart change. */
74
+ export interface CartWarning {
75
+ code: string;
76
+ message: string;
77
+ target?: string;
78
+ }
79
+ export type CheckoutTarget = '_self' | '_blank';
80
+ type PurchaseKind = 'one-time' | 'subscription';
81
+
82
+ /** Every option, with its default filled in. */
83
+ export interface TetherConfig {
84
+ /** The store's domain, without protocol or path; `''` until one is set. */
85
+ domain: ShopDomain | '';
86
+ /** The public Storefront API access token. */
87
+ token: string;
88
+ /** A dated Shopify API version (or `unstable`, with a warning). */
89
+ apiVersion: ApiVersion | 'unstable';
90
+ /** The market's country, for `@inContext(country:)`. */
91
+ country: CountryCode | undefined;
92
+ /** The market's language, for `@inContext(language:)`. */
93
+ language: LanguageCode | undefined;
94
+ /** An `Intl` locale for money and dates. */
95
+ locale: string | undefined;
96
+ markets: MarketsConfig | null;
97
+ /** The attribute prefix: `tether` reads `tether-product`, `tether-bind`. */
98
+ prefix: string;
99
+ /** A folder (`/product`) or a template with `{handle}` / `{id}`. */
100
+ productPath: string;
101
+ collectionPath: string;
102
+ /** The product metafields to fetch. */
103
+ metafields: MetafieldRef[];
104
+ /** Origins (`https://video.example`) a description's iframes may load from, besides the built-in video players. */
105
+ embedOrigins: string[];
106
+ imagesFirst: number;
107
+ variantsFirst: number;
108
+ sellingPlanGroupsFirst: number;
109
+ sellingPlansFirst: number;
110
+ collectionFirst: number;
111
+ /** Retries of a request that failed on the network, a 429/5xx or Shopify's throttling. */
112
+ retries: number;
113
+ /** Refresh server-rendered products once in the background. */
114
+ revalidate: boolean;
115
+ /** The localStorage key of the cart id (default `<prefix>:cart:<domain>`). */
116
+ storageKey: string;
117
+ cartDrawerOpenClass: string;
118
+ openCartOnAdd: boolean;
119
+ /** Move focus into the open cart, keep Tab inside it, restore it on close. */
120
+ cartFocus: boolean;
121
+ checkoutTarget: CheckoutTarget;
122
+ defaultPurchase: PurchaseKind;
123
+ /** `?variant=` follows the visitor's choice on the page's main product. */
124
+ variantUrl: boolean;
125
+ /** Seconds a Shopify answer may be cached; 0 leaves it to Shopify, which can be hours. */
126
+ maxAge: number;
127
+ /** `tether-show="low-stock"` at this many or fewer (needs inventory access). */
128
+ lowStock: number;
129
+ /** Milliseconds the search part waits after the last keystroke. */
130
+ searchDelay: number;
131
+ /** Mount the document on DOMContentLoaded. */
132
+ autoMount: boolean;
133
+ /** Mount elements added later (CMS load more, filters). */
134
+ observe: boolean;
135
+ /** Mount on Astro's `astro:page-load` and close the cart on navigation. */
136
+ astro: boolean;
137
+ /** The folder of the optional parts, for the `<script>` build. */
138
+ partsUrl: string | null;
139
+ analytics: AnalyticsConfig | null;
140
+ account: AccountConfig | null;
141
+ /** The site's text for a cart warning; nothing keeps Shopify's. */
142
+ warningText: ((warning: CartWarning) => string | null | undefined) | null;
143
+ formatMoney: ((money: Money, locale: string | undefined) => string) | null;
144
+ /** Default: `ScrollTrigger.refresh()` when present. */
145
+ onLayoutChange: ((reason: string) => void) | null;
146
+ onCartOpen: ((drawers: HTMLElement[]) => void) | null;
147
+ onCartClose: ((drawers: HTMLElement[]) => void) | null;
148
+ onLineEnter: ((node: HTMLElement) => void) | null;
149
+ /** Animates a line out, then calls `done()`. */
150
+ onLineLeave: ((node: HTMLElement, done: () => void) => void) | null;
151
+ onValueChange: ((element: HTMLElement, previousText: string) => void) | null;
152
+ /** Every failure that reaches the visitor. */
153
+ onError: ((error: unknown, context: string) => void) | null;
154
+ debug: boolean;
155
+ }
156
+
157
+ /** The options a site writes as plain text, which `resolveConfig` checks into their typed form. */
158
+ type ParsedFromText = 'domain' | 'apiVersion' | 'country' | 'language' | 'storageKey';
159
+
160
+ /**
161
+ * What a site passes: any option, the market as the codes people write (`'de'`, `'pt-BR'`), and `null` for the
162
+ * default storage key.
163
+ */
164
+ export type TetherOptions = {
165
+ [Key in Exclude<keyof TetherConfig, ParsedFromText>]?: TetherConfig[Key];
166
+ } & Partial<Record<Exclude<ParsedFromText, 'storageKey'>, string | undefined>> & {
167
+ storageKey?: string | null | undefined;
168
+ };
169
+
170
+ export const DEFAULTS: Readonly<TetherConfig> = {
171
+ domain: '',
172
+ token: '',
173
+ apiVersion: STOREFRONT_API_VERSION,
174
+ country: undefined,
175
+ language: undefined,
176
+ locale: undefined,
177
+ markets: null,
178
+ prefix: 'tether',
179
+ productPath: '/product',
180
+ collectionPath: '/collection',
181
+ metafields: [],
182
+ embedOrigins: [],
183
+ imagesFirst: 25,
184
+ variantsFirst: 100,
185
+ sellingPlanGroupsFirst: 10,
186
+ sellingPlansFirst: 25,
187
+ collectionFirst: 24,
188
+ retries: 2,
189
+ revalidate: true,
190
+ storageKey: '',
191
+ cartDrawerOpenClass: 'is-open',
192
+ openCartOnAdd: true,
193
+ cartFocus: true,
194
+ checkoutTarget: '_self',
195
+ defaultPurchase: 'one-time',
196
+ variantUrl: false,
197
+ maxAge: 60,
198
+ lowStock: 5,
199
+ searchDelay: 150,
200
+ autoMount: true,
201
+ observe: false,
202
+ astro: true,
203
+ partsUrl: null,
204
+ analytics: null,
205
+ account: null,
206
+ warningText: null,
207
+ formatMoney: null,
208
+ onLayoutChange: null,
209
+ onCartOpen: null,
210
+ onCartClose: null,
211
+ onLineEnter: null,
212
+ onLineLeave: null,
213
+ onValueChange: null,
214
+ onError: null,
215
+ debug: false,
216
+ };
217
+
218
+ export const cfg: TetherConfig = { ...DEFAULTS };
219
+
220
+ /**
221
+ * @returns A view of `target` and the objects in it that reads the live values and refuses every change: a write
222
+ * throws in strict-mode code (as on a frozen object) and warns in any code.
223
+ */
224
+ function readOnlyView<T extends object>(target: T): T {
225
+ const refuse = (key: string | symbol) => {
226
+ warn(`Tether.config is read-only (${String(key)}): pass options to Tether.init() instead`);
227
+ return false;
228
+ };
229
+ return new Proxy(target, {
230
+ get(object, key) {
231
+ const value: unknown = Reflect.get(object, key);
232
+ // The config's objects are its own copies (`resolveConfig`), never frozen, so a view of each is allowed.
233
+ return typeof value === 'object' && value !== null ? readOnlyView(value) : value;
234
+ },
235
+ set: (_object, key) => refuse(key),
236
+ deleteProperty: (_object, key) => refuse(key),
237
+ defineProperty: (_object, key) => refuse(key),
238
+ });
239
+ }
240
+
241
+ /**
242
+ * `cfg` as `Tether.config` shows it. A value written there would skip the validation `configure()` does, so the view
243
+ * is read-only; `Tether.init(options)` is how a site changes the config.
244
+ */
245
+ export const configView = readOnlyView(cfg);
246
+
247
+ /** The prefix becomes part of attribute names, CSS selectors and event names, so it is kept to those characters. */
248
+ const PREFIX = /^[a-z][a-z0-9-]*$/;
249
+
250
+ type CountOption =
251
+ | 'imagesFirst'
252
+ | 'variantsFirst'
253
+ | 'sellingPlanGroupsFirst'
254
+ | 'sellingPlansFirst'
255
+ | 'collectionFirst'
256
+ | 'retries'
257
+ | 'maxAge'
258
+ | 'lowStock'
259
+ | 'searchDelay';
260
+ type HookOption =
261
+ | 'warningText'
262
+ | 'formatMoney'
263
+ | 'onLayoutChange'
264
+ | 'onCartOpen'
265
+ | 'onCartClose'
266
+ | 'onLineEnter'
267
+ | 'onLineLeave'
268
+ | 'onValueChange'
269
+ | 'onError';
270
+ type SwitchOption =
271
+ 'revalidate' | 'openCartOnAdd' | 'cartFocus' | 'variantUrl' | 'autoMount' | 'observe' | 'astro' | 'debug';
272
+ type TextOption = 'token' | 'productPath' | 'collectionPath' | 'cartDrawerOpenClass';
273
+
274
+ const SWITCH_OPTIONS: readonly SwitchOption[] = [
275
+ 'revalidate',
276
+ 'openCartOnAdd',
277
+ 'cartFocus',
278
+ 'variantUrl',
279
+ 'autoMount',
280
+ 'observe',
281
+ 'astro',
282
+ 'debug',
283
+ ];
284
+ const TEXT_OPTIONS: readonly TextOption[] = ['token', 'productPath', 'collectionPath', 'cartDrawerOpenClass'];
285
+
286
+ /** Options that are written into queries as list sizes or used as counts. */
287
+ const COUNT_OPTIONS: readonly CountOption[] = [
288
+ 'imagesFirst',
289
+ 'variantsFirst',
290
+ 'sellingPlanGroupsFirst',
291
+ 'sellingPlansFirst',
292
+ 'collectionFirst',
293
+ 'retries',
294
+ 'maxAge',
295
+ 'lowStock',
296
+ 'searchDelay',
297
+ ];
298
+ const HOOK_OPTIONS: readonly HookOption[] = [
299
+ 'warningText',
300
+ 'formatMoney',
301
+ 'onLayoutChange',
302
+ 'onCartOpen',
303
+ 'onCartClose',
304
+ 'onLineEnter',
305
+ 'onLineLeave',
306
+ 'onValueChange',
307
+ 'onError',
308
+ ];
309
+
310
+ /** @returns `fallback`, with a warning unless `value` was left out. */
311
+ function invalidOption<T>(name: string, value: unknown, fallback: T): T {
312
+ if (value !== undefined) warn(`invalid ${name} ${JSON.stringify(value)}: using ${JSON.stringify(fallback)}`);
313
+ return fallback;
314
+ }
315
+
316
+ /** Warns about an option without a default to fall back to, which is left out (`config-sections.ts` too). */
317
+ function dropped(name: string, value: unknown): void {
318
+ warn(`invalid ${name} ${JSON.stringify(value)}: ignored`);
319
+ }
320
+
321
+ /**
322
+ * Warns about each key that is no option, so a misspelt one (`openCartOnAd`) is seen; none of them reaches `cfg`.
323
+ */
324
+ function warnUnknownKeys(options: Readonly<Record<string, unknown>>): void {
325
+ for (const key of Object.keys(options)) {
326
+ if (!(key in DEFAULTS)) warn(`unknown option ${JSON.stringify(key)}: ignored`);
327
+ }
328
+ }
329
+
330
+ /** @returns A site's hook, `null` for anything that isn't a function. */
331
+ function hookOption(value: unknown): ((...args: never[]) => unknown) | null {
332
+ return typeof value === 'function' ? (value as (...args: never[]) => unknown) : null;
333
+ }
334
+
335
+ function hookOptions(options: Readonly<Record<string, unknown>>): Pick<TetherConfig, HookOption> {
336
+ const hooks = Object.fromEntries(HOOK_OPTIONS.map((name) => [name, hookOption(options[name])]));
337
+ // A function from the site can only be checked for being callable; how it is called is the documented signature
338
+ // of its option.
339
+ return hooks as Pick<TetherConfig, HookOption>;
340
+ }
341
+
342
+ /** @returns `value` when it is a non-negative integer, else the default. */
343
+ function countOption(name: CountOption, value: unknown): number {
344
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) return value;
345
+ return invalidOption(name, value, DEFAULTS[name]);
346
+ }
347
+
348
+ function switchOption(name: SwitchOption, value: unknown): boolean {
349
+ return typeof value === 'boolean' ? value : invalidOption(name, value, DEFAULTS[name]);
350
+ }
351
+
352
+ function textOption(name: TextOption, value: unknown): string {
353
+ return typeof value === 'string' ? value : invalidOption(name, value, DEFAULTS[name]);
354
+ }
355
+
356
+ /** @returns An `Intl` locale as written, `undefined` for one `Intl` doesn't accept. */
357
+ function localeOption(value: unknown): string | undefined {
358
+ if (value === undefined || value === null || value === '') return undefined;
359
+ try {
360
+ if (typeof value === 'string' && Intl.getCanonicalLocales(value).length) return value;
361
+ } catch {
362
+ // RangeError: not a locale (falls through to the warning).
363
+ }
364
+ dropped('locale', value);
365
+ return undefined;
366
+ }
367
+
368
+ /** @param choices The first one is the default. */
369
+ function choiceOption<Choice extends string>(
370
+ name: string,
371
+ value: unknown,
372
+ choices: readonly [Choice, ...Choice[]],
373
+ ): Choice {
374
+ return choices.find((choice) => choice === value) ?? invalidOption(name, value, choices[0]);
375
+ }
376
+
377
+ /**
378
+ * @returns A quarterly version, or `unstable` with a warning; the default for anything else, which would be written
379
+ * into the endpoint's path.
380
+ */
381
+ function apiVersionOption(value: unknown): ApiVersion | 'unstable' {
382
+ if (value === 'unstable') {
383
+ warn('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).');
384
+ return value;
385
+ }
386
+ return isQuarterlyVersion(value) ? value : invalidOption('apiVersion', value, DEFAULTS.apiVersion);
387
+ }
388
+
389
+ function codeOption<Code>(name: string, value: unknown, parse: (value: unknown) => Code | null): Code | undefined {
390
+ if (value === undefined || value === null || value === '') return undefined;
391
+ const code = parse(value);
392
+ if (code !== null) return code;
393
+ dropped(name, value);
394
+ return undefined;
395
+ }
396
+
397
+ /**
398
+ * The configuration `options` describe: defaults filled in, every value validated (an invalid one warns and falls
399
+ * back to its default), hooks that aren't functions dropped so callers only check for null.
400
+ *
401
+ * @param options What the site set, or any object (the server helpers take their options from code and `.env`).
402
+ */
403
+ export function resolveConfig(options: TetherOptions | Readonly<Record<string, unknown>>): TetherConfig {
404
+ warnUnknownKeys(options);
405
+ const domain = domainOption(options.domain, dropped);
406
+ const prefix =
407
+ typeof options.prefix === 'string' && PREFIX.test(options.prefix)
408
+ ? options.prefix
409
+ : invalidOption('prefix', options.prefix, DEFAULTS.prefix);
410
+ const resolved: TetherConfig = {
411
+ ...DEFAULTS,
412
+ ...hookOptions(options),
413
+ domain,
414
+ apiVersion: apiVersionOption(options.apiVersion),
415
+ country: codeOption('country', options.country, toCountry),
416
+ locale: localeOption(options.locale),
417
+ partsUrl: partsUrlOption(options.partsUrl, dropped),
418
+ language: codeOption('language', options.language, toLanguage),
419
+ markets: marketsOption(options.markets, dropped),
420
+ analytics: analyticsOption(options.analytics, dropped),
421
+ account: accountOption(options.account, dropped, warn),
422
+ prefix,
423
+ storageKey:
424
+ typeof options.storageKey === 'string' && options.storageKey ? options.storageKey : `${prefix}:cart:${domain}`,
425
+ metafields: metafieldsOption(options.metafields ?? [], dropped),
426
+ embedOrigins: embedOriginsOption(options.embedOrigins ?? [], dropped),
427
+ checkoutTarget: choiceOption('checkoutTarget', options.checkoutTarget, ['_self', '_blank']),
428
+ defaultPurchase: choiceOption('defaultPurchase', options.defaultPurchase, ['one-time', 'subscription']),
429
+ };
430
+ for (const name of COUNT_OPTIONS) resolved[name] = countOption(name, options[name]);
431
+ for (const name of SWITCH_OPTIONS) resolved[name] = switchOption(name, options[name]);
432
+ for (const name of TEXT_OPTIONS) resolved[name] = textOption(name, options[name]);
433
+ return resolved;
434
+ }
435
+
436
+ /**
437
+ * @param value A `markets` option from code other than the config (`Tether.detectMarket(markets)`).
438
+ * @returns It validated as the config's own is, with the same warnings.
439
+ */
440
+ export const parseMarkets = (value: unknown): MarketsConfig | null => marketsOption(value, dropped);
441
+
442
+ /**
443
+ * Replaces the shared configuration with `options` over the defaults (see `resolveConfig`).
444
+ *
445
+ * @returns The shared `cfg`.
446
+ */
447
+ export function configure(options: TetherOptions | Readonly<Record<string, unknown>> = {}): TetherConfig {
448
+ const resolved = resolveConfig(options);
449
+ for (const key of Object.keys(cfg)) {
450
+ if (!(key in resolved)) Reflect.deleteProperty(cfg, key);
451
+ }
452
+ return Object.assign(cfg, resolved);
453
+ }
454
+
455
+ /** @returns True once a domain and a token are set. */
456
+ export const isConfigured = (): boolean => Boolean(cfg.domain && cfg.token);
457
+
458
+ export function log(...args: unknown[]): void {
459
+ if (cfg.debug) console.log(`[${cfg.prefix}]`, ...args);
460
+ }
461
+
462
+ export function warn(...args: unknown[]): void {
463
+ console.warn(`[${cfg.prefix}]`, ...args);
464
+ }
465
+
466
+ /**
467
+ * Logs a failure that reaches the visitor and hands it to `onError`. A throwing `onError` is logged, never rethrown:
468
+ * the site's hook must not break the render that reported the failure.
469
+ *
470
+ * @param context What failed, e.g. `product coffee-box`.
471
+ */
472
+ export function reportError(error: unknown, context: string): void {
473
+ warn(context, error instanceof Error && error.message ? error.message : error);
474
+ if (!cfg.onError) return;
475
+ try {
476
+ cfg.onError(error, context);
477
+ } catch (hookError) {
478
+ warn('onError threw', hookError);
479
+ }
480
+ }
@@ -3,46 +3,30 @@
3
3
  * clear and checkout buttons, a line's +, - and remove buttons, its quantity input and plan select, the cart note, and
4
4
  * the keyboard of the open drawer (Escape closes it, Tab stays inside it).
5
5
  */
6
- import { cfg, reportError } from '../config.js';
7
- import { Cart, trapFocus } from '../cart.js';
8
- import { sel, busy } from '../dom.js';
6
+ import { cfg, reportError } from '../config.ts';
7
+ import { Cart, trapFocus } from '../cart.ts';
8
+ import { sel, busy } from '../dom.ts';
9
9
 
10
- /** @returns {string} The attribute a rendered cart line carries its line id in. */
11
- const lineKeyAttribute = () => `data-${cfg.prefix}-key`;
10
+ /** @returns The attribute a rendered cart line carries its line id in. */
11
+ const lineKeyAttribute = (): string => `data-${cfg.prefix}-key`;
12
12
 
13
- /**
14
- * @param {EventTarget | null} target
15
- * @returns {target is Element}
16
- */
17
- const isElement = (target) => target instanceof window.Element;
13
+ const isElement = (target: EventTarget | null): target is Element => target instanceof window.Element;
18
14
 
19
- /**
20
- * @param {Element} target
21
- * @returns {{ line: Element, id: string } | null} The rendered cart line `target` is in, with its line id.
22
- */
23
- function lineOf(target) {
15
+ /** @returns The rendered cart line `target` is in, with its line id. */
16
+ function lineOf(target: Element): { line: Element; id: string } | null {
24
17
  const line = target.closest(`[${lineKeyAttribute()}]`);
25
18
  const id = line?.getAttribute(lineKeyAttribute());
26
19
  return line && id ? { line, id } : null;
27
20
  }
28
21
 
29
- /**
30
- * @param {Promise<unknown>} change
31
- * @param {string} context
32
- */
33
- function reportFailure(change, context) {
34
- change.catch((/** @type {unknown} */ error) => {
22
+ function reportFailure(change: Promise<unknown>, context: string): void {
23
+ change.catch((error: unknown) => {
35
24
  reportError(error, context);
36
25
  });
37
26
  }
38
27
 
39
- /**
40
- * A line's remove, + and - buttons.
41
- *
42
- * @param {MouseEvent} event
43
- * @param {Element} target
44
- */
45
- function onLineClick(event, target) {
28
+ /** A line's remove, + and - buttons. */
29
+ function onLineClick(event: MouseEvent, target: Element): void {
46
30
  const found = lineOf(target);
47
31
  if (!found?.line.closest(sel('cart-lines'))) return;
48
32
  const { line, id } = found;
@@ -58,18 +42,14 @@ function onLineClick(event, target) {
58
42
  reportFailure(Cart.changeQuantity(id, delta), 'cart line');
59
43
  }
60
44
 
61
- /**
62
- * @param {Element} target
63
- * @returns {1 | -1 | 0} The quantity change of a + or - button, 0 for anything else.
64
- */
65
- function lineDelta(target) {
45
+ /** @returns The quantity change of a + or - button, 0 for anything else. */
46
+ function lineDelta(target: Element): 1 | -1 | 0 {
66
47
  if (target.closest(sel('line-plus'))) return 1;
67
48
  if (target.closest(sel('line-minus'))) return -1;
68
49
  return 0;
69
50
  }
70
51
 
71
- /** @param {MouseEvent} event */
72
- function onClick(event) {
52
+ function onClick(event: MouseEvent): void {
73
53
  const { target } = event;
74
54
  if (!isElement(target)) return;
75
55
  if (target.closest(sel('cart-open'))) {
@@ -86,7 +66,7 @@ function onClick(event) {
86
66
  if (clear) {
87
67
  event.preventDefault();
88
68
  void busy(clear, () =>
89
- Cart.clear().catch((/** @type {unknown} */ error) => {
69
+ Cart.clear().catch((error: unknown) => {
90
70
  reportError(error, 'cart clear');
91
71
  }),
92
72
  );
@@ -101,21 +81,14 @@ function onClick(event) {
101
81
  onLineClick(event, target);
102
82
  }
103
83
 
104
- /**
105
- * The line shows `is-busy` and `aria-busy` until Shopify has answered; the next render removes them.
106
- *
107
- * @param {Element} line
108
- * @param {Promise<unknown>} change
109
- * @param {string} context
110
- */
111
- function showLineBusy(line, change, context) {
84
+ /** The line shows `is-busy` and `aria-busy` until Shopify has answered; the next render removes them. */
85
+ function showLineBusy(line: Element, change: Promise<unknown>, context: string): void {
112
86
  line.classList.add('is-busy');
113
87
  line.setAttribute('aria-busy', 'true');
114
88
  reportFailure(change, context);
115
89
  }
116
90
 
117
- /** @param {Event} event */
118
- function onChange(event) {
91
+ function onChange(event: Event): void {
119
92
  const { target } = event;
120
93
  if (!isElement(target)) return;
121
94
  const found = lineOf(target);
@@ -128,17 +101,12 @@ function onChange(event) {
128
101
  }
129
102
  }
130
103
 
131
- /**
132
- * @param {Element} target
133
- * @returns {target is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement}
134
- */
135
- const hasValue = (target) =>
104
+ const hasValue = (target: Element): target is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement =>
136
105
  target instanceof window.HTMLInputElement ||
137
106
  target instanceof window.HTMLTextAreaElement ||
138
107
  target instanceof window.HTMLSelectElement;
139
108
 
140
- /** @param {KeyboardEvent} event */
141
- function onKeydown(event) {
109
+ function onKeydown(event: KeyboardEvent): void {
142
110
  if (!Cart.isOpen()) return;
143
111
  if (event.key === 'Escape') {
144
112
  Cart.close();
@@ -148,7 +116,7 @@ function onKeydown(event) {
148
116
  }
149
117
 
150
118
  /** Binds the document-wide listeners, once per page load of the script. */
151
- export function bindCartControls() {
119
+ export function bindCartControls(): void {
152
120
  document.addEventListener('click', onClick);
153
121
  document.addEventListener('change', onChange);
154
122
  document.addEventListener('keydown', onKeydown);