@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/lazy-iife.js DELETED
@@ -1,116 +0,0 @@
1
- /**
2
- * `<script>` build: the optional parts are separate files next to tether(.min).js (tether.markets.min.js), loaded on
3
- * first use. `partsUrl` sets another folder. A part registers itself in `window.Tether.__parts` and uses the core
4
- * modules in `window.Tether.__core` instead of bundling copies of them.
5
- */
6
- import * as config from './config.js';
7
- import * as client from './client.js';
8
- import * as dom from './dom.js';
9
- import * as fragments from './fragments.js';
10
- import * as loader from './loader.js';
11
- import * as sellingPlans from './selling-plans.js';
12
- import * as status from './status.js';
13
- import * as cart from './cart.js';
14
- import * as product from './product.js';
15
- import * as collection from './collection.js';
16
- import * as mounting from './mounting.js';
17
- import * as marketCodes from './market-codes.js';
18
- import * as shopifyValues from './shopify-values.js';
19
- import * as storeHtml from './store-html.js';
20
- /** @import { Parts, PartName } from './lazy.js' */
21
-
22
- const { cfg } = config;
23
-
24
- /**
25
- * The modules a part may use, shared instead of bundled twice (scripts/build.mjs maps a part's imports to these).
26
- *
27
- * @type {Record<string, object>}
28
- */
29
- export const coreModules = {
30
- 'config.js': config,
31
- 'client.js': client,
32
- 'dom.js': dom,
33
- 'fragments.js': fragments,
34
- 'loader.js': loader,
35
- 'selling-plans.js': sellingPlans,
36
- 'status.js': status,
37
- 'cart.js': cart,
38
- 'product.js': product,
39
- 'collection.js': collection,
40
- 'mounting.js': mounting,
41
- 'market-codes.js': marketCodes,
42
- 'shopify-values.js': shopifyValues,
43
- 'store-html.js': storeHtml,
44
- };
45
-
46
- const MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
47
- const coreScript = typeof document === 'undefined' ? '' : (document.currentScript?.getAttribute('src') ?? '');
48
- const coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : '';
49
-
50
- /** @type {Map<PartName, Promise<unknown>>} Parts loaded or loading. */
51
- const parts = new Map();
52
-
53
- /**
54
- * The folder parts load from: `partsUrl`, else the core script's own folder. A `partsUrl` must be https or on this
55
- * site, because the parts run with the same access as the core (the account part holds customer sessions).
56
- *
57
- * @throws {Error} For a `partsUrl` that is neither.
58
- */
59
- function partsFolder() {
60
- if (!cfg.partsUrl) return coreScriptUrl.replace(/[^/]*$/, '');
61
- const folder = new URL(cfg.partsUrl, document.baseURI);
62
- if (folder.protocol !== 'https:' && folder.origin !== window.location.origin) {
63
- throw new Error(`partsUrl must be https or on this site: ${folder.href}`);
64
- }
65
- return folder.href;
66
- }
67
-
68
- /**
69
- * @param {PartName} name
70
- * @param {string} url
71
- * @returns {Promise<unknown>}
72
- */
73
- function loadScript(name, url) {
74
- return new Promise((resolve, reject) => {
75
- const script = document.createElement('script');
76
- script.src = url;
77
- script.async = true;
78
- script.onload = () => {
79
- const { Tether } = /** @type {Window & { Tether?: { __parts?: Record<string, unknown> } }} */ (window);
80
- const part = Tether?.__parts?.[name];
81
- if (part) resolve(part);
82
- else reject(new Error(`${url} did not register`));
83
- };
84
- script.onerror = () => {
85
- parts.delete(name);
86
- reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`));
87
- };
88
- document.head.appendChild(script);
89
- });
90
- }
91
-
92
- /**
93
- * @template {PartName} Name
94
- * @param {Name} name
95
- * @returns {Promise<Parts[Name]>}
96
- * @throws {Error} (rejected) When the part's file can't be loaded or doesn't register.
97
- */
98
- export function loadPart(name) {
99
- let loading = parts.get(name);
100
- if (!loading) {
101
- let folder;
102
- try {
103
- folder = partsFolder();
104
- } catch (error) {
105
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
106
- }
107
- const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? '.min' : '';
108
- loading = loadScript(name, `${folder.replace(/\/?$/, '/')}tether.${name}${suffix}.js`);
109
- parts.set(name, loading);
110
- }
111
- // A part's file is built from the module `Parts[Name]` names (scripts/build.mjs), and registers that module.
112
- return /** @type {Promise<Parts[Name]>} */ (loading);
113
- }
114
-
115
- // Parts that load other parts (account → account-pages) use this same loader.
116
- coreModules['lazy.js'] = { loadPart, coreModules };
package/src/lazy.js DELETED
@@ -1,39 +0,0 @@
1
- /**
2
- * Optional parts that most pages never need, loaded on first use. In the ESM and Node builds they are dynamic
3
- * imports, which bundlers split into their own chunks; the `<script>` build swaps this file for `lazy-iife.js`
4
- * (scripts/build.mjs).
5
- */
6
-
7
- /**
8
- * @typedef {{
9
- * diagnose: typeof import('./diagnose.js'),
10
- * markets: typeof import('./markets-ui.js'),
11
- * search: typeof import('./search.js'),
12
- * extras: typeof import('./extras.js'),
13
- * account: typeof import('./account.js'),
14
- * 'account-pages': typeof import('./account-pages.js'),
15
- * analytics: typeof import('./analytics.js'),
16
- * }} Parts
17
- */
18
- /** @typedef {keyof Parts} PartName */
19
-
20
- /** @type {{ [Name in PartName]: () => Promise<Parts[Name]> }} */
21
- const PARTS = {
22
- diagnose: () => import('./diagnose.js'),
23
- markets: () => import('./markets-ui.js'),
24
- search: () => import('./search.js'),
25
- extras: () => import('./extras.js'),
26
- account: () => import('./account.js'),
27
- 'account-pages': () => import('./account-pages.js'),
28
- analytics: () => import('./analytics.js'),
29
- };
30
-
31
- /**
32
- * @template {PartName} Name
33
- * @param {Name} name
34
- * @returns {Promise<Parts[Name]>}
35
- */
36
- export const loadPart = (name) => PARTS[name]();
37
-
38
- /** Only the `<script>` build shares its modules with separately loaded parts. */
39
- export const coreModules = undefined;
@@ -1,35 +0,0 @@
1
- /**
2
- * Country and language codes as Shopify's `@inContext` takes them. Every market value that reaches a query passes
3
- * through here: they are written into the query text, so anything else is dropped rather than sent.
4
- */
5
-
6
- /** @typedef {string & { readonly __brand: 'CountryCode' }} CountryCode An ISO 3166-1 alpha-2 code, upper case. */
7
- /** @typedef {string & { readonly __brand: 'LanguageCode' }} LanguageCode A Shopify `LanguageCode` value. */
8
-
9
- const COUNTRY = /^[A-Z]{2}$/;
10
- /** Cloudflare's codes for an unknown country (XX) and Tor (T1), which a `cf-ipcountry` cookie can carry. */
11
- const NOT_A_COUNTRY = new Set(['XX', 'T1']);
12
- /** Shopify `LanguageCode` values with a region. Every other language is its plain ISO 639 code. */
13
- const REGIONAL_LANGUAGES = new Set(['PT_BR', 'PT_PT', 'ZH_CN', 'ZH_TW']);
14
- const LANGUAGE = /^[A-Z]{2,3}$/;
15
-
16
- /**
17
- * @param {unknown} value
18
- * @returns {CountryCode | null} `'de'` → `'DE'`; `null` for anything that is not a country code.
19
- */
20
- export function toCountry(value) {
21
- const code = (typeof value === 'string' ? value : '').trim().toUpperCase();
22
- if (!COUNTRY.test(code) || NOT_A_COUNTRY.has(code)) return null;
23
- return /** @type {CountryCode} */ (code);
24
- }
25
-
26
- /**
27
- * @param {unknown} value
28
- * @returns {LanguageCode | null} `'de-DE'` → `'DE'`, `'pt-BR'` → `'PT_BR'`, `'en'` → `'EN'`; `null` otherwise.
29
- */
30
- export function toLanguage(value) {
31
- const raw = (typeof value === 'string' ? value : '').trim().replace('-', '_').toUpperCase();
32
- if (REGIONAL_LANGUAGES.has(raw)) return /** @type {LanguageCode} */ (raw);
33
- const primary = raw.split('_')[0] ?? '';
34
- return LANGUAGE.test(primary) ? /** @type {LanguageCode} */ (primary) : null;
35
- }
package/src/markets.js DELETED
@@ -1,334 +0,0 @@
1
- /**
2
- * Markets: which country and language prices, currency and translations follow.
3
- *
4
- * markets: {
5
- * detect: true, // or a list: ['saved', 'server', 'webflow']
6
- * locales: { // Webflow Localization: locale subdirectory or <html lang>
7
- * de: { country: 'DE', language: 'DE', locale: 'de-DE' },
8
- * 'fr-be': { country: 'BE', language: 'FR' },
9
- * },
10
- * cookie: 'tether_country', // a cookie your server or Worker sets (from cf-ipcountry)
11
- * remember: true, // keep the visitor's tether-country-select choice
12
- * }
13
- *
14
- * Sources, first match wins per field:
15
- * saved the visitor's own choice (localStorage), from tether-country-select / Tether.setCountry()
16
- * server window.TETHER_COUNTRY, <meta name="tether-country" content="DE">, or the cookie
17
- * webflow locale subdirectory (/de/...) or <html lang="de-DE">, through `locales`
18
- * then the static `country` / `language` config. Without `detect`, only `saved` applies, so a country select keeps
19
- * working across pages; `detect: false` turns even that off. Tether never calls an IP lookup service: a country from
20
- * the visitor's IP only comes from the site's own server (see README).
21
- */
22
- import { cfg } from './config.js';
23
- import { gql } from './client.js';
24
- import { toCountry, toLanguage } from './market-codes.js';
25
- /** @import { MarketsConfig } from './config.js' */
26
- /** @import { CountryCode, LanguageCode } from './market-codes.js' */
27
-
28
- /**
29
- * @typedef {object} Market A market a visitor chose, as `setCountry()` stores it.
30
- * @property {CountryCode} country
31
- * @property {LanguageCode | undefined} language
32
- * @property {string | undefined} locale An `Intl` locale for money and dates.
33
- */
34
-
35
- /** @typedef {'saved' | 'server' | 'webflow'} MarketSource */
36
- /** @typedef {'country' | 'language' | 'locale'} MarketField */
37
- /** @typedef {Partial<Record<MarketField, MarketSource>>} MarketSources */
38
-
39
- /**
40
- * @typedef {object} DetectedMarket The fields detection found, and the source each came from.
41
- * @property {CountryCode} [country]
42
- * @property {LanguageCode} [language]
43
- * @property {string} [locale]
44
- * @property {MarketSources} [sources] Absent when `detect: false` turns detection off.
45
- */
46
-
47
- /**
48
- * @typedef {object} MarketEnv What detection reads, passed in so it runs on any document.
49
- * @property {Pick<Document, 'cookie' | 'querySelector' | 'documentElement'> | null} document
50
- * @property {Pick<Location, 'pathname'> | null} location
51
- * @property {Pick<Storage, 'getItem'> | null} storage
52
- * @property {object} global Where `TETHER_COUNTRY` and `TETHER_LANGUAGE` are read from (`window`).
53
- */
54
-
55
- /** @typedef {{ country?: unknown, language?: unknown, locale?: unknown }} MarketHint A source's raw values. */
56
-
57
- /** @typedef {{ isoCode: string, name: string }} NamedCode */
58
- /** @typedef {{ isoCode: string, symbol: string }} Currency */
59
- /** @typedef {NamedCode & { currency: Currency }} LocalizedCountry */
60
- /**
61
- * @typedef {object} Localization The store's markets, from the Storefront API's `localization`.
62
- * @property {LocalizedCountry} country The country of the current `@inContext`.
63
- * @property {NamedCode} language
64
- * @property {(LocalizedCountry & { availableLanguages: NamedCode[] })[]} availableCountries
65
- */
66
-
67
- /** @type {readonly MarketSource[]} */
68
- const ALL_SOURCES = ['saved', 'server', 'webflow'];
69
- const DEFAULT_COOKIE = 'tether_country';
70
-
71
- /** @returns {string} The localStorage key of the visitor's market: `<prefix>:market:<domain>`. */
72
- export const storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
73
-
74
- /**
75
- * Accessing `localStorage` throws where the browser blocks storage (sandboxed frames, cookies turned off); the visitor
76
- * then simply has no saved market.
77
- *
78
- * @returns {Storage | null}
79
- */
80
- function localStorageIfAllowed() {
81
- if (typeof window === 'undefined') return null;
82
- try {
83
- return window.localStorage;
84
- } catch {
85
- return null;
86
- }
87
- }
88
-
89
- /** @returns {MarketEnv} The page this script runs on. */
90
- function pageEnv() {
91
- const hasWindow = typeof window !== 'undefined';
92
- return {
93
- document: typeof document !== 'undefined' ? document : null,
94
- location: hasWindow ? window.location : null,
95
- storage: localStorageIfAllowed(),
96
- global: hasWindow ? window : {},
97
- };
98
- }
99
-
100
- /**
101
- * @param {string} text
102
- * @returns {unknown} The JSON value, `null` for text that isn't JSON (a key another script wrote).
103
- */
104
- function parseJsonOrNull(text) {
105
- try {
106
- return /** @type {unknown} */ (JSON.parse(text));
107
- } catch (error) {
108
- if (error instanceof SyntaxError) return null;
109
- throw error;
110
- }
111
- }
112
-
113
- /**
114
- * @param {unknown} value
115
- * @returns {Market | null} `value` as a market, when it has a valid country. A language or locale that isn't valid is
116
- * left out.
117
- */
118
- function toMarket(value) {
119
- if (typeof value !== 'object' || value === null) return null;
120
- const country = toCountry(Reflect.get(value, 'country'));
121
- if (!country) return null;
122
- const locale = /** @type {unknown} */ (Reflect.get(value, 'locale'));
123
- return {
124
- country,
125
- language: toLanguage(Reflect.get(value, 'language')) ?? undefined,
126
- locale: typeof locale === 'string' && locale ? locale : undefined,
127
- };
128
- }
129
-
130
- /**
131
- * The visitor's saved market (`setCountry()`, `tether-country-select`), validated: storage is written by other tabs,
132
- * older versions and other scripts too.
133
- *
134
- * @param {Pick<Storage, 'getItem'> | null} [storage] Default `localStorage`, `null` where the browser blocks it.
135
- * @returns {Market | null} `null` when nothing valid is saved.
136
- */
137
- export function readSavedMarket(storage = localStorageIfAllowed()) {
138
- const text = storage?.getItem(storageKey());
139
- return text ? toMarket(parseJsonOrNull(text)) : null;
140
- }
141
-
142
- /**
143
- * @param {Pick<Document, 'cookie'> | null} doc
144
- * @param {string} name
145
- * @returns {string | null}
146
- */
147
- function readCookie(doc, name) {
148
- if (!doc || !name) return null;
149
- const cookie = doc.cookie
150
- .split(';')
151
- .map((part) => part.trim())
152
- .find((part) => part.startsWith(`${name}=`));
153
- return cookie ? decodeURIComponent(cookie.slice(name.length + 1)) : null;
154
- }
155
-
156
- /**
157
- * @param {MarketsConfig['locales']} locales
158
- * @param {string | undefined} key
159
- * @returns {MarketHint | null} The entry for `key`, ignoring case.
160
- */
161
- function lookupLocale(locales, key) {
162
- if (!locales || !key) return null;
163
- const wanted = key.toLowerCase();
164
- const found = Object.keys(locales).find((name) => name.toLowerCase() === wanted);
165
- return found ? (locales[found] ?? null) : null;
166
- }
167
-
168
- /**
169
- * @param {MarketsConfig} markets
170
- * @param {MarketEnv} env
171
- * @returns {MarketHint}
172
- */
173
- function readServerHint(markets, env) {
174
- /** @param {string} name */
175
- const meta = (name) => env.document?.querySelector(`meta[name="${name}"]`)?.getAttribute('content');
176
- return {
177
- country:
178
- Reflect.get(env.global, 'TETHER_COUNTRY') ||
179
- meta('tether-country') ||
180
- readCookie(env.document, markets.cookie || DEFAULT_COOKIE),
181
- language: Reflect.get(env.global, 'TETHER_LANGUAGE') || meta('tether-language'),
182
- };
183
- }
184
-
185
- /**
186
- * @param {MarketsConfig} markets
187
- * @param {MarketEnv} env
188
- * @returns {MarketHint | null}
189
- */
190
- function readWebflowLocale(markets, env) {
191
- const segment = (env.location?.pathname ?? '/').split('/').find((part) => part !== '');
192
- const fromPath = lookupLocale(markets.locales, segment);
193
- if (fromPath) return fromPath;
194
- const lang = env.document?.documentElement.getAttribute('lang');
195
- if (!lang) return null;
196
- const mapped = lookupLocale(markets.locales, lang) ?? lookupLocale(markets.locales, lang.split('-')[0]);
197
- if (mapped) return { locale: lang, ...mapped };
198
- if (markets.locales && markets.strict) return null;
199
- // Unmapped: the language from <html lang>, a country only from a region subtag (de-CH -> CH).
200
- const region = lang.split('-')[1];
201
- return { language: lang, country: region?.length === 2 ? region : null, locale: lang };
202
- }
203
-
204
- /**
205
- * @param {MarketSource} source
206
- * @param {MarketsConfig} markets
207
- * @param {MarketEnv} env
208
- * @returns {MarketHint | null}
209
- */
210
- function readSource(source, markets, env) {
211
- switch (source) {
212
- case 'saved':
213
- return markets.remember === false ? null : readSavedMarket(env.storage);
214
- case 'server':
215
- return readServerHint(markets, env);
216
- case 'webflow':
217
- return readWebflowLocale(markets, env);
218
- }
219
- }
220
-
221
- /**
222
- * @param {unknown} name
223
- * @returns {name is MarketSource}
224
- */
225
- const isMarketSource = (name) => ALL_SOURCES.some((source) => source === name);
226
-
227
- /**
228
- * @param {MarketsConfig['detect']} detect
229
- * @returns {readonly MarketSource[]} Without `detect`, only the visitor's own choice from a country select.
230
- */
231
- function sourcesFor(detect) {
232
- if (!detect) return ['saved'];
233
- if (detect === true) return ALL_SOURCES;
234
- // The config comes from the page unvalidated, so a single name or an unknown one can arrive here.
235
- return /** @type {unknown[]} */ ([detect].flat()).filter(isMarketSource);
236
- }
237
-
238
- /**
239
- * The market the page's sources point to. Reads the DOM and storage only, so it can run on any `env`.
240
- *
241
- * @param {MarketsConfig | null | undefined} [markets]
242
- * @param {MarketEnv} [env]
243
- * @returns {DetectedMarket} Only the fields found; `{}` with `detect: false`.
244
- */
245
- export function detectMarket(markets = cfg.markets, env = pageEnv()) {
246
- const config = markets ?? {};
247
- if (config.detect === false) return {};
248
- /** @type {MarketSources} */
249
- const sources = {};
250
- /** @type {DetectedMarket} */
251
- const found = { sources };
252
- for (const source of sourcesFor(config.detect)) {
253
- const hint = readSource(source, config, env);
254
- if (!hint) continue;
255
- const country = toCountry(hint.country);
256
- const language = toLanguage(hint.language);
257
- const locale = typeof hint.locale === 'string' && hint.locale ? hint.locale : null;
258
- if (country && !found.country) {
259
- found.country = country;
260
- sources.country = source;
261
- }
262
- if (language && !found.language) {
263
- found.language = language;
264
- sources.language = source;
265
- }
266
- if (locale && !found.locale) {
267
- found.locale = locale;
268
- sources.locale = source;
269
- }
270
- }
271
- return found;
272
- }
273
-
274
- /**
275
- * `setItem` throws when storage is full or blocked (Safari's private mode); the choice then lasts for this page only.
276
- *
277
- * @param {string} key
278
- * @param {string} value
279
- */
280
- function storeIfAllowed(key, value) {
281
- try {
282
- localStorage.setItem(key, value);
283
- } catch {
284
- return;
285
- }
286
- }
287
-
288
- /**
289
- * Saves the visitor's choice for the next pages, unless `markets.remember` is false.
290
- *
291
- * @param {{ country: CountryCode, language?: string | null | undefined, locale?: string | null | undefined }} market
292
- */
293
- export function rememberMarket(market) {
294
- if (cfg.markets?.remember === false) return;
295
- const saved = { country: market.country, language: market.language || undefined, locale: market.locale || undefined };
296
- storeIfAllowed(storageKey(), JSON.stringify(saved));
297
- }
298
-
299
- /** @type {Promise<Localization> | null} */
300
- let localizationPromise = null;
301
-
302
- /** Forgets the cached localization, for when country names change language. */
303
- export function clearLocalization() {
304
- localizationPromise = null;
305
- }
306
-
307
- const LOCALIZATION_QUERY = `query TetherLocalization {
308
- localization {
309
- country { isoCode name currency { isoCode symbol } }
310
- language { isoCode name }
311
- availableCountries { isoCode name currency { isoCode symbol } availableLanguages { isoCode name } }
312
- }
313
- }`;
314
-
315
- /**
316
- * The countries the store sells to (Shopify Markets), with their currency. Cached per page; a failed request is not
317
- * cached, so the next call tries again.
318
- *
319
- * @returns {Promise<Localization>}
320
- */
321
- export function getLocalization() {
322
- if (!localizationPromise) {
323
- /** @type {Promise<{ localization: Localization }>} */
324
- const loading = gql(LOCALIZATION_QUERY);
325
- localizationPromise = loading.then(
326
- (data) => data.localization,
327
- (/** @type {unknown} */ error) => {
328
- localizationPromise = null;
329
- throw error;
330
- },
331
- );
332
- }
333
- return localizationPromise;
334
- }
package/src/markup.js DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * How Tether finds its markup: attribute names carry the configured prefix (`tether-product`, or `wfs-product` with
3
- * `prefix: 'wfs'`), and so do the selectors and the DOM events built from them.
4
- */
5
- import { cfg } from './config.js';
6
-
7
- /**
8
- * @param {string} name
9
- * @returns {string} The prefixed attribute name: `product` → `tether-product`.
10
- */
11
- export const attr = (name) => `${cfg.prefix}-${name}`;
12
-
13
- /**
14
- * @param {string} name
15
- * @param {string} [value]
16
- * @returns {string} A selector for the attribute, with that exact value when given.
17
- */
18
- export const sel = (name, value) => (value === undefined ? `[${attr(name)}]` : `[${attr(name)}="${value}"]`);
19
-
20
- /**
21
- * @param {Element} el
22
- * @param {string} name
23
- */
24
- export const get = (el, name) => el.getAttribute(attr(name));
25
-
26
- /**
27
- * @param {Element} el
28
- * @param {string} name
29
- */
30
- export const has = (el, name) => el.hasAttribute(attr(name));
31
-
32
- /**
33
- * Tether's attributes go on HTML elements, so matches are typed as such. An SVG element that carries one still has
34
- * the `style`, `dataset` and attribute methods Tether uses.
35
- *
36
- * @param {ParentNode | null | undefined} root
37
- * @param {string} selector
38
- * @returns {HTMLElement[]} The matches in `root`, none without a root.
39
- */
40
- export function qsa(root, selector) {
41
- if (!root) return [];
42
- return [.../** @type {NodeListOf<HTMLElement>} */ (root.querySelectorAll(selector))];
43
- }
44
-
45
- /**
46
- * @param {Element | null | undefined} root
47
- * @param {string} selector
48
- * @param {string} scopeSelector
49
- * @returns {HTMLElement[]} Matches in `root` whose nearest `scopeSelector` ancestor is `root` itself, not a nested
50
- * scope.
51
- */
52
- export const scoped = (root, selector, scopeSelector) =>
53
- qsa(root, selector).filter((el) => el.closest(scopeSelector) === root);
54
-
55
- /**
56
- * Dispatches the bubbling event `<prefix>:<name>`.
57
- *
58
- * @param {string} name
59
- * @param {unknown} [detail]
60
- * @param {EventTarget} [target]
61
- */
62
- export function emit(name, detail, target = document) {
63
- target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
64
- }
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.account-pages.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../account-pages.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.account-pages.js runs after the Tether script, which loads it');
6
- Tether.__parts['account-pages'] = part;
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.account.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../account.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.account.js runs after the Tether script, which loads it');
6
- Tether.__parts['account'] = part;
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.analytics.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../analytics.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.analytics.js runs after the Tether script, which loads it');
6
- Tether.__parts['analytics'] = part;
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.diagnose.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../diagnose.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.diagnose.js runs after the Tether script, which loads it');
6
- Tether.__parts['diagnose'] = part;
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.extras.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../extras.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.extras.js runs after the Tether script, which loads it');
6
- Tether.__parts['extras'] = part;
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.markets.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../markets-ui.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.markets.js runs after the Tether script, which loads it');
6
- Tether.__parts['markets'] = part;
@@ -1,6 +0,0 @@
1
- // The <script> build's tether.search.js: registers the part where the core script's loader (lazy-iife.js) finds it.
2
- import * as part from '../search.js';
3
-
4
- const { Tether } = /** @type {Window & { Tether?: { __parts: Record<string, unknown> } }} */ (window);
5
- if (!Tether) throw new Error('tether.search.js runs after the Tether script, which loads it');
6
- Tether.__parts['search'] = part;