@occluze/tether 0.6.8 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +28 -28
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
  4. package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
  5. package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
  6. package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
  7. package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
  8. package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
  9. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  10. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  11. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  12. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  13. package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
  14. package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
  15. package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
  16. package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
  17. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  18. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  19. package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
  20. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  21. package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
  22. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/presets/barba.esm.js +19 -23
  26. package/dist/tether.account-pages.js +332 -367
  27. package/dist/tether.account-pages.min.js +1 -1
  28. package/dist/tether.account.js +253 -492
  29. package/dist/tether.account.min.js +1 -1
  30. package/dist/tether.analytics.js +82 -89
  31. package/dist/tether.analytics.min.js +1 -1
  32. package/dist/tether.diagnose.js +52 -34
  33. package/dist/tether.diagnose.min.js +2 -2
  34. package/dist/tether.extras.js +42 -37
  35. package/dist/tether.extras.min.js +1 -1
  36. package/dist/tether.js +1361 -935
  37. package/dist/tether.markets.js +89 -91
  38. package/dist/tether.markets.min.js +1 -1
  39. package/dist/tether.min.js +0 -0
  40. package/dist/tether.min.js.map +4 -4
  41. package/dist/tether.search.js +60 -69
  42. package/dist/tether.search.min.js +1 -1
  43. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  44. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  45. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  46. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  47. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  48. package/dist/types/server/src/shared/json.d.ts +25 -0
  49. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  50. package/dist/types/src/account/addresses-view.d.ts +5 -0
  51. package/dist/types/src/account/answers.d.ts +40 -0
  52. package/dist/types/src/account/markup.d.ts +19 -0
  53. package/dist/types/src/account/operations.d.ts +41 -0
  54. package/dist/types/src/account/order-lines.d.ts +13 -0
  55. package/dist/types/src/account/order-list.d.ts +12 -0
  56. package/dist/types/src/account/order-page.d.ts +6 -0
  57. package/dist/types/src/account/page-answers.d.ts +182 -0
  58. package/dist/types/src/account/page-events.d.ts +2 -0
  59. package/dist/types/src/account/server-client.d.ts +60 -0
  60. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  61. package/dist/types/src/account-api.d.ts +18 -0
  62. package/dist/types/src/account-marks.d.ts +15 -0
  63. package/dist/types/src/account-pages.d.ts +8 -0
  64. package/dist/types/src/account.d.ts +64 -0
  65. package/dist/types/src/amounts.d.ts +12 -0
  66. package/dist/types/src/analytics-shopify.d.ts +41 -0
  67. package/dist/types/src/analytics.d.ts +2 -0
  68. package/dist/types/src/api-version.d.ts +28 -0
  69. package/dist/types/src/astro.d.ts +42 -0
  70. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  71. package/dist/types/src/cart/checkout.d.ts +16 -0
  72. package/dist/types/src/cart/drawer.d.ts +18 -0
  73. package/dist/types/src/cart/mutations.d.ts +85 -0
  74. package/dist/types/src/cart/render.d.ts +17 -0
  75. package/dist/types/src/cart/store.d.ts +185 -0
  76. package/dist/types/src/cart.d.ts +106 -0
  77. package/dist/types/src/client.d.ts +134 -0
  78. package/dist/types/src/collection.d.ts +25 -0
  79. package/dist/types/src/config-sections.d.ts +40 -0
  80. package/dist/types/src/config.d.ts +190 -0
  81. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  82. package/dist/types/src/controls/discount.d.ts +2 -0
  83. package/dist/types/src/controls/extras.d.ts +5 -0
  84. package/dist/types/src/controls/navigation.d.ts +12 -0
  85. package/dist/types/src/diagnose-markup.d.ts +19 -0
  86. package/dist/types/src/diagnose.d.ts +60 -0
  87. package/dist/types/src/dom.d.ts +75 -0
  88. package/dist/types/src/extras.d.ts +21 -0
  89. package/dist/types/src/fragments.d.ts +158 -0
  90. package/dist/types/src/globals.d.ts +15 -0
  91. package/dist/types/src/images.d.ts +20 -0
  92. package/dist/types/src/index.d.ts +167 -0
  93. package/dist/types/src/initial.d.ts +56 -0
  94. package/dist/types/src/lazy.d.ts +28 -0
  95. package/dist/types/src/loader.d.ts +84 -0
  96. package/dist/types/src/market-codes.d.ts +6 -0
  97. package/dist/types/src/markets-ui.d.ts +32 -0
  98. package/dist/types/src/markets.d.ts +72 -0
  99. package/dist/types/src/markup.d.ts +131 -0
  100. package/dist/types/src/mounting.d.ts +46 -0
  101. package/dist/types/src/page-market.d.ts +27 -0
  102. package/dist/types/src/presets/barba.d.ts +56 -0
  103. package/dist/types/src/product-events.d.ts +2 -0
  104. package/dist/types/src/product-render.d.ts +17 -0
  105. package/dist/types/src/product-shape.d.ts +90 -0
  106. package/dist/types/src/product-state.d.ts +110 -0
  107. package/dist/types/src/product.d.ts +66 -0
  108. package/dist/types/src/repeaters.d.ts +16 -0
  109. package/dist/types/src/search.d.ts +33 -0
  110. package/dist/types/src/selling-plans.d.ts +64 -0
  111. package/dist/types/src/server.d.ts +88 -0
  112. package/dist/types/src/shopify-values.d.ts +46 -0
  113. package/dist/types/src/status.d.ts +33 -0
  114. package/dist/types/src/storage.d.ts +12 -0
  115. package/dist/types/src/store-html.d.ts +39 -0
  116. package/dist/types/src/storefront/catalog.d.ts +38 -0
  117. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  118. package/dist/types/src/variant-render.d.ts +14 -0
  119. package/package.json +24 -24
  120. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  121. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  122. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  123. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  124. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  125. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  126. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  127. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  128. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  129. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  130. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  131. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  132. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  133. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  134. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  135. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  136. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  137. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  138. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  139. package/src/account/addresses-view.js +0 -92
  140. package/src/account/browser-session.js +0 -359
  141. package/src/account/markup.js +0 -48
  142. package/src/account/operations.js +0 -180
  143. package/src/account/order-lines.js +0 -96
  144. package/src/account/order-list.js +0 -91
  145. package/src/account/order-page.js +0 -192
  146. package/src/account/page-events.js +0 -288
  147. package/src/account/server-client.js +0 -172
  148. package/src/account/shared.js +0 -111
  149. package/src/account/shopify-auth.js +0 -150
  150. package/src/account/subscriptions-view.js +0 -158
  151. package/src/account-api.js +0 -118
  152. package/src/account-links.js +0 -35
  153. package/src/account-pages.js +0 -89
  154. package/src/account.js +0 -342
  155. package/src/amounts.js +0 -36
  156. package/src/analytics-shopify.js +0 -241
  157. package/src/analytics.js +0 -352
  158. package/src/api-version.js +0 -85
  159. package/src/astro.js +0 -266
  160. package/src/cart/checkout.js +0 -97
  161. package/src/cart/drawer.js +0 -138
  162. package/src/cart/mutations.js +0 -234
  163. package/src/cart/render.js +0 -294
  164. package/src/cart/store.js +0 -326
  165. package/src/cart.js +0 -391
  166. package/src/client.js +0 -267
  167. package/src/collection.js +0 -183
  168. package/src/config.js +0 -349
  169. package/src/controls/cart-controls.js +0 -155
  170. package/src/controls/discount.js +0 -96
  171. package/src/controls/extras.js +0 -63
  172. package/src/controls/navigation.js +0 -83
  173. package/src/diagnose-markup.js +0 -333
  174. package/src/diagnose.js +0 -295
  175. package/src/dom.js +0 -287
  176. package/src/extras.js +0 -145
  177. package/src/fragments.js +0 -237
  178. package/src/images.js +0 -104
  179. package/src/index.js +0 -353
  180. package/src/initial.js +0 -87
  181. package/src/lazy-iife.js +0 -116
  182. package/src/lazy.js +0 -39
  183. package/src/loader.js +0 -337
  184. package/src/market-codes.js +0 -35
  185. package/src/markets-ui.js +0 -204
  186. package/src/markets.js +0 -334
  187. package/src/markup.js +0 -64
  188. package/src/mounting.js +0 -277
  189. package/src/page-market.js +0 -116
  190. package/src/parts/account-pages.js +0 -6
  191. package/src/parts/account.js +0 -6
  192. package/src/parts/analytics.js +0 -6
  193. package/src/parts/diagnose.js +0 -6
  194. package/src/parts/extras.js +0 -6
  195. package/src/parts/markets.js +0 -6
  196. package/src/parts/search.js +0 -6
  197. package/src/presets/barba.js +0 -190
  198. package/src/product-events.js +0 -130
  199. package/src/product-render.js +0 -257
  200. package/src/product-state.js +0 -318
  201. package/src/product.js +0 -344
  202. package/src/repeaters.js +0 -173
  203. package/src/search.js +0 -323
  204. package/src/selling-plans.js +0 -154
  205. package/src/server.js +0 -215
  206. package/src/shopify-values.js +0 -89
  207. package/src/status.js +0 -126
  208. package/src/store-html.js +0 -0
  209. package/src/storefront/catalog.js +0 -199
  210. package/src/storefront/json-ld.js +0 -167
  211. package/src/variant-render.js +0 -165
  212. package/types/astro.d.ts +0 -28
  213. package/types/barba.d.ts +0 -16
  214. package/types/index.d.ts +0 -577
  215. package/types/server.d.ts +0 -182
package/src/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
- }
package/src/mounting.js DELETED
@@ -1,277 +0,0 @@
1
- /**
2
- * The product scopes and lists on the page: mounting a product root, the live roots a country switch re-renders, and
3
- * the products the server rendered into the page. Shared by `index.js` and the optional markets part.
4
- */
5
- import { cfg, log, warn, reportError } from './config.js';
6
- import { ProductScope, scopes, rememberOptionNames } from './product.js';
7
- import { listSelector } from './collection.js';
8
- import { getProduct, takeStale, refreshProducts, hydrateProducts } from './loader.js';
9
- import { resolveProductKey, setScopeStatus, isNotFound } from './status.js';
10
- import { attr, get, qsa, emit } from './dom.js';
11
- /** @import { Product } from './fragments.js' */
12
-
13
- /** @typedef {{ root: Element, load: (more?: boolean) => Promise<unknown> }} MountedList A mounted `ListScope`. */
14
-
15
- /**
16
- * The marker on the `<script type="application/json">` tags `tether/server`'s `hydrate()` writes. It is fixed, not
17
- * `<prefix>-hydrate`, because the server writes it without knowing the browser's prefix.
18
- */
19
- const HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
20
-
21
- /** @type {WeakSet<Element>} */
22
- export const mountedProducts = new WeakSet();
23
- /** @type {WeakSet<Element>} */
24
- export const mountedLists = new WeakSet();
25
- /** @type {Set<HTMLElement>} Product roots to re-render after a country switch; pruned once disconnected. */
26
- export const liveProducts = new Set();
27
- /** @type {Set<MountedList>} Lists to reload after a country switch; pruned once disconnected. */
28
- export const liveLists = new Set();
29
- /** @type {WeakMap<Element, object>} Each product root's latest request, so a slow older response can't win. */
30
- const latestRequest = new WeakMap();
31
-
32
- /** Forgets roots a page transition removed, so they aren't kept alive. */
33
- export function pruneLive() {
34
- liveProducts.forEach((el) => {
35
- if (!el.isConnected) liveProducts.delete(el);
36
- });
37
- liveLists.forEach((list) => {
38
- if (!list.root.isConnected) liveLists.delete(list);
39
- });
40
- }
41
-
42
- /**
43
- * @param {ParentNode & Partial<Pick<Element, 'matches'>>} root
44
- * @param {string} selector
45
- * @returns {HTMLElement[]} `root` itself when it matches, then the matches inside it.
46
- */
47
- export function within(root, selector) {
48
- const self = root.matches?.(selector) ? [/** @type {HTMLElement} */ (root)] : [];
49
- return [...self, ...qsa(root, selector)];
50
- }
51
-
52
- /**
53
- * `tether-page-title="{title} | My shop"` on a product root sets `document.title`, for dynamic product pages.
54
- *
55
- * @param {Element} el
56
- * @param {Product} product
57
- */
58
- function applyPageTitle(el, product) {
59
- const template = get(el, 'page-title');
60
- if (!template || typeof document === 'undefined') return;
61
- document.title = template
62
- .replace(/\{title\}/g, product.title)
63
- .replace(/\{vendor\}/g, product.vendor || '')
64
- .replace(/\{type\}/g, product.productType || '');
65
- }
66
-
67
- /**
68
- * A product root without a key: on a dynamic page opened without `?handle=` (or with an empty path) it is a product
69
- * that isn't there; inside a list it gets its handle when the list renders.
70
- *
71
- * @param {HTMLElement} el
72
- * @param {{ fromUrl: boolean, raw: string }} source
73
- */
74
- function mountWithoutKey(el, { fromUrl, raw }) {
75
- if (fromUrl) {
76
- mountedProducts.add(el);
77
- setScopeStatus(el, 'not-found');
78
- emit('product-not-found', { key: null, source: raw }, el);
79
- } else if (!el.closest(listSelector())) {
80
- warn(`[${attr('product')}] without a value. Bind it to the Shopify handle field, or use "url:handle".`, el);
81
- }
82
- }
83
-
84
- /**
85
- * @param {HTMLElement} el
86
- * @param {string} key
87
- * @param {{ fromUrl: boolean, raw: string }} source
88
- * @param {unknown} error
89
- */
90
- function showLoadFailure(el, key, { fromUrl, raw }, error) {
91
- const notFound = isNotFound(error);
92
- setScopeStatus(el, notFound ? 'not-found' : 'error', error);
93
- if (!notFound) {
94
- reportError(error, `product ${key}`);
95
- emit('error', { error, context: 'product', key }, el);
96
- return;
97
- }
98
- emit('product-not-found', { key, source: raw }, el);
99
- // A typo in a URL isn't worth an alert; a CMS item pointing at a deleted product is.
100
- if (fromUrl) log(`product not found: ${key}`);
101
- else reportError(error, `product ${key}`);
102
- }
103
-
104
- /**
105
- * Mounts one product root.
106
- *
107
- * @param {HTMLElement} el
108
- * @returns {boolean} True when a product request was started.
109
- */
110
- export function mountProduct(el) {
111
- if (mountedProducts.has(el)) return false;
112
- const { key, ...source } = resolveProductKey(el);
113
- if (!key) {
114
- mountWithoutKey(el, source);
115
- return false;
116
- }
117
- mountedProducts.add(el);
118
- liveProducts.add(el);
119
- const request = {};
120
- latestRequest.set(el, request);
121
- const isLatest = () => latestRequest.get(el) === request;
122
- // A rendered scope keeps showing its content while it refreshes (a country switch).
123
- if (scopes.get(el)) el.setAttribute(attr('state'), 'updating');
124
- else setScopeStatus(el, 'loading');
125
- const loading = trackMarket(getProduct(key));
126
- loading
127
- .then((product) => {
128
- if (!isLatest()) return;
129
- const existing = scopes.get(el);
130
- if (existing) existing.update(product);
131
- else new ProductScope(el, product);
132
- applyPageTitle(el, product);
133
- if (cfg.revalidate && takeStale(key)) queueRefresh(key);
134
- })
135
- .catch((/** @type {unknown} */ error) => {
136
- if (isLatest()) showLoadFailure(el, key, source, error);
137
- });
138
- return true;
139
- }
140
-
141
- /** Loads again the product scopes that failed on a network or API error, for when the browser is back online. */
142
- export function retryFailed() {
143
- pruneLive();
144
- liveProducts.forEach((el) => {
145
- if (el.getAttribute(attr('state')) !== 'error' || el.getAttribute(attr('error')) === 'not-found') return;
146
- mountedProducts.delete(el);
147
- mountProduct(el);
148
- });
149
- }
150
-
151
- /** @type {Set<string>} Server-rendered products to refresh in the next batch. */
152
- let refreshQueue = new Set();
153
-
154
- /** @param {string} key */
155
- function queueRefresh(key) {
156
- if (!refreshQueue.size) setTimeout(flushRefresh, 0);
157
- refreshQueue.add(key);
158
- }
159
-
160
- /**
161
- * @param {string} key The key the refresh was asked for.
162
- * @param {Product} fresh
163
- */
164
- function applyRefresh(key, fresh) {
165
- liveProducts.forEach((el) => {
166
- const scope = scopes.get(el);
167
- if (!scope || !el.isConnected) return;
168
- const rootKey = resolveProductKey(el).key;
169
- if (rootKey !== key && rootKey !== fresh.handle && rootKey !== fresh.id) return;
170
- if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
171
- });
172
- }
173
-
174
- /** One batched background request refreshes every server-rendered product queued. */
175
- function flushRefresh() {
176
- const keys = [...refreshQueue];
177
- refreshQueue = new Set();
178
- refreshProducts(keys).forEach((promise, index) => {
179
- const key = /** @type {string} */ (keys[index]);
180
- promise
181
- .then((fresh) => {
182
- applyRefresh(key, fresh);
183
- })
184
- .catch((/** @type {unknown} */ error) => {
185
- reportError(error, `refresh ${key}`);
186
- });
187
- });
188
- }
189
-
190
- /**
191
- * @type {Set<Promise<unknown>> | null} While the page's server-rendered prices are another market's: the loads to
192
- * wait for before `html.tether-market-pending` goes. `null` when no market is pending.
193
- */
194
- let marketPending = null;
195
-
196
- /**
197
- * Lists, and anything else that loads prices, hold `html.tether-market-pending` until they are in.
198
- *
199
- * @template T
200
- * @param {Promise<T>} loading
201
- * @returns {Promise<T>} `loading` itself.
202
- */
203
- export const trackMarket = (loading) => {
204
- marketPending?.add(loading);
205
- return loading;
206
- };
207
-
208
- /** After a mount: drops `html.tether-market-pending` once this page's products are live. */
209
- export function settleMarket() {
210
- if (!marketPending) return;
211
- const waits = [...marketPending];
212
- void Promise.allSettled(waits).then(() => {
213
- if (!marketPending || waits.some((wait) => !marketPending?.has(wait))) return;
214
- if (waits.length !== marketPending.size) {
215
- settleMarket(); // more loads came in meanwhile
216
- return;
217
- }
218
- marketPending = null;
219
- document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
220
- });
221
- }
222
-
223
- /**
224
- * What `tether/server`'s `hydrate()` writes. The tag is the site's own markup, so its shape is trusted; a product
225
- * without an id or a handle is skipped.
226
- *
227
- * @typedef {{ country?: string | null, language?: string | null, products?: (Product | null)[] }} HydratePayload
228
- */
229
-
230
- /**
231
- * @param {Product | null} product
232
- * @returns {product is Product}
233
- */
234
- const isUsable = (product) => Boolean(product?.id && product.handle);
235
-
236
- /**
237
- * @param {string | null | undefined} a
238
- * @param {string | null | undefined} b
239
- */
240
- const sameCode = (a, b) => (a ?? '').toUpperCase() === (b ?? '').toUpperCase();
241
-
242
- /** @type {WeakSet<Element>} Hydrate tags already read. */
243
- const readScripts = new WeakSet();
244
-
245
- /**
246
- * Uses the products in the page's hydrate tags (`tether/server` `hydrate()`) without a request. A tag made for another
247
- * market is skipped, and marks the page (`html.tether-market-pending`) until the live prices are in, so CSS can hide
248
- * prices meanwhile.
249
- *
250
- * @param {Document | Element} [root]
251
- */
252
- export function hydrateFromPage(root = document) {
253
- qsa(root.ownerDocument ?? root, HYDRATE_SELECTOR).forEach((script) => {
254
- if (readScripts.has(script)) return;
255
- readScripts.add(script);
256
- /** @type {unknown} */
257
- let json;
258
- try {
259
- json = JSON.parse(script.textContent);
260
- } catch (error) {
261
- warn('bad data-tether-hydrate', error);
262
- return;
263
- }
264
- const { country, language, products } = /** @type {HydratePayload | null} */ (json) ?? {};
265
- const usable = Array.isArray(products) ? products.filter(isUsable) : [];
266
- rememberOptionNames(usable); // the build's option names, also for other languages
267
- if (sameCode(country, cfg.country) && sameCode(language, cfg.language)) {
268
- hydrateProducts(usable);
269
- return;
270
- }
271
- log('hydration skipped: other market');
272
- if (!marketPending) {
273
- marketPending = new Set();
274
- document.documentElement.classList.add(`${cfg.prefix}-market-pending`);
275
- }
276
- });
277
- }