@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
@@ -0,0 +1,197 @@
1
+ /**
2
+ * `<script>` build: the optional parts are separate files next to tether(.min).js (tether.markets.min.js), loaded on
3
+ * first use and checked against the hashes the core was built with, from that folder or from `partsUrl`, which must
4
+ * serve the files of the same release. A part registers itself in `window.Tether.__parts` and uses the core modules in
5
+ * `window.Tether.__core` instead of bundling copies.
6
+ */
7
+ import { cfg, isConfigured, log, MARKET_SOURCES, parseMarkets, reportError, warn } from './config.ts';
8
+ import { endpoint, gql, orNull, shaped, TetherError } from './client.ts';
9
+ import {
10
+ ANALYTICS_EVENTS,
11
+ attr,
12
+ attributesFromJson,
13
+ emit,
14
+ formatDate,
15
+ formatMoney,
16
+ get,
17
+ has,
18
+ hasTemplate,
19
+ isOwnEvent,
20
+ parseJSONAttr,
21
+ productUrl,
22
+ qsa,
23
+ repeat,
24
+ sel,
25
+ setImage,
26
+ setText,
27
+ show,
28
+ } from './dom.ts';
29
+ import { clearProductCache, getProduct } from './loader.ts';
30
+ import { allPlans } from './selling-plans.ts';
31
+ import { boundary, resolveProductKey } from './status.ts';
32
+ import { Cart } from './cart.ts';
33
+ import { scopes } from './product.ts';
34
+ import { listSelector } from './collection.ts';
35
+ import { liveLists, liveProducts, mountedProducts, mountProduct, pruneLive, within } from './mounting.ts';
36
+ import { toCountry, toLanguage } from './market-codes.ts';
37
+ import { isHttpsUrl } from './shopify-values.ts';
38
+ import { PART_INTEGRITY } from './part-integrity.ts';
39
+ import type { Parts, PartName, TetherInternals } from './lazy.ts';
40
+
41
+ /**
42
+ * The core exports the parts use, shared instead of bundled twice, by the file name scripts/build.ts maps a part's
43
+ * imports to. Each file shares a list of values without their names: the build finds each value's export name in its
44
+ * module and hands a part's import of that name the value at its position. The core so carries no export names and no
45
+ * export a part doesn't use, and a part that imports one not listed here fails its build. A part reads its entries
46
+ * when it loads, once every module has run; read while the core runs, an import cycle through this file could leave
47
+ * a value unset.
48
+ */
49
+ export const coreModules = {
50
+ get 'config.ts'() {
51
+ return [cfg, isConfigured, log, MARKET_SOURCES, parseMarkets, reportError, warn];
52
+ },
53
+ get 'client.ts'() {
54
+ return [endpoint, gql, orNull, shaped, TetherError];
55
+ },
56
+ get 'dom.ts'() {
57
+ return [
58
+ ANALYTICS_EVENTS,
59
+ attr,
60
+ attributesFromJson,
61
+ emit,
62
+ formatDate,
63
+ formatMoney,
64
+ get,
65
+ has,
66
+ hasTemplate,
67
+ isOwnEvent,
68
+ parseJSONAttr,
69
+ productUrl,
70
+ qsa,
71
+ repeat,
72
+ sel,
73
+ setImage,
74
+ setText,
75
+ show,
76
+ ];
77
+ },
78
+ get 'loader.ts'() {
79
+ return [clearProductCache, getProduct];
80
+ },
81
+ get 'selling-plans.ts'() {
82
+ return [allPlans];
83
+ },
84
+ get 'status.ts'() {
85
+ return [boundary, resolveProductKey];
86
+ },
87
+ get 'cart.ts'() {
88
+ return [Cart];
89
+ },
90
+ get 'product.ts'() {
91
+ return [scopes];
92
+ },
93
+ get 'collection.ts'() {
94
+ return [listSelector];
95
+ },
96
+ get 'mounting.ts'() {
97
+ return [liveLists, liveProducts, mountedProducts, mountProduct, pruneLive, within];
98
+ },
99
+ get 'market-codes.ts'() {
100
+ return [toCountry, toLanguage];
101
+ },
102
+ get 'shopify-values.ts'() {
103
+ return [isHttpsUrl];
104
+ },
105
+ // Parts that load other parts (account → account-pages) use this same loader.
106
+ get 'lazy.ts'() {
107
+ return [loadPart];
108
+ },
109
+ };
110
+
111
+ const MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
112
+ const coreScript = typeof document === 'undefined' ? '' : (document.currentScript?.getAttribute('src') ?? '');
113
+ const coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : '';
114
+
115
+ /** Parts loaded or loading. */
116
+ const parts = new Map<PartName, Promise<unknown>>();
117
+
118
+ /**
119
+ * The folder parts load from: the `partsUrl` (checked to be https or on this site when the config was resolved), else
120
+ * the core script's own folder. `null` when the core was not loaded by a `<script src>` (a module, a loader) and no
121
+ * `partsUrl` is set, so nothing says where parts are.
122
+ */
123
+ function partsFolder(): string | null {
124
+ if (cfg.partsUrl) return new URL(cfg.partsUrl, document.baseURI).href;
125
+ if (!coreScriptUrl) return null;
126
+ return coreScriptUrl.replace(/[^/]*$/, '');
127
+ }
128
+
129
+ /**
130
+ * Pins the script to the part file built with this core, wherever it loads from: a page that pins the core with
131
+ * `integrity` then runs only parts of that build, and a `partsUrl` must serve the same release's files.
132
+ */
133
+ function applyIntegrity(script: HTMLScriptElement, integrity: string): void {
134
+ script.integrity = integrity;
135
+ // A cross-origin script is only checked when fetched with CORS (CDNs such as jsDelivr allow it).
136
+ if (new URL(script.src).origin !== window.location.origin) script.crossOrigin = 'anonymous';
137
+ }
138
+
139
+ /**
140
+ * Loads a part's file. A failed load leaves the cache, so the next use tries again (the network may be back).
141
+ *
142
+ * @param integrity The file's hash from this core's build.
143
+ */
144
+ function loadScript(name: PartName, folder: string, file: string, integrity: string): Promise<unknown> {
145
+ return new Promise((resolve, reject) => {
146
+ const url = `${folder.replace(/\/?$/, '/')}${file}`;
147
+ const script = document.createElement('script');
148
+ script.src = url;
149
+ script.async = true;
150
+ applyIntegrity(script, integrity);
151
+ script.onload = () => {
152
+ const { Tether } = window as { Tether?: TetherInternals };
153
+ const part = Tether?.__parts[name];
154
+ if (part) {
155
+ resolve(part);
156
+ return;
157
+ }
158
+ parts.delete(name);
159
+ reject(new Error(`${url} did not register`));
160
+ };
161
+ script.onerror = () => {
162
+ parts.delete(name);
163
+ reject(
164
+ new Error(
165
+ `Could not load ${url}, or it is not the file built with this Tether script. Host the same release's files ` +
166
+ 'next to the Tether script or at partsUrl.',
167
+ ),
168
+ );
169
+ };
170
+ document.head.appendChild(script);
171
+ });
172
+ }
173
+
174
+ /**
175
+ * @throws {Error} (rejected) When the part's file can't be loaded, fails its integrity check or doesn't register.
176
+ */
177
+ export function loadPart<Name extends PartName>(name: Name): Promise<Parts[Name]> {
178
+ let loading = parts.get(name);
179
+ if (!loading) {
180
+ const folder = partsFolder();
181
+ if (folder === null) {
182
+ return Promise.reject(
183
+ new Error(`The Tether script's own location is unknown, so its ${name} part can't be found: set partsUrl.`),
184
+ );
185
+ }
186
+ const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? '.min' : '';
187
+ const file = `tether.${name}${suffix}.js`;
188
+ const integrity = PART_INTEGRITY[file];
189
+ if (integrity === undefined) {
190
+ return Promise.reject(new Error(`This Tether script was built without ${file}, so it can't load that part.`));
191
+ }
192
+ loading = loadScript(name, folder, file, integrity);
193
+ parts.set(name, loading);
194
+ }
195
+ // A part's file is built from the module `Parts[Name]` names (scripts/build.ts), and registers that module.
196
+ return loading as Promise<Parts[Name]>;
197
+ }
package/src/lazy.ts ADDED
@@ -0,0 +1,42 @@
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.ts`
4
+ * (scripts/build.ts).
5
+ */
6
+
7
+ export interface Parts {
8
+ diagnose: typeof import('./diagnose.ts');
9
+ markets: typeof import('./markets-ui.ts');
10
+ search: typeof import('./search.ts');
11
+ extras: typeof import('./extras.ts');
12
+ account: typeof import('./account.ts');
13
+ 'account-pages': typeof import('./account-pages.ts');
14
+ analytics: typeof import('./analytics.ts');
15
+ }
16
+ export type PartName = keyof Parts;
17
+
18
+ /**
19
+ * What `window.Tether` carries for the `<script>` build's separately loaded parts, besides the API sites use. Left out
20
+ * of the published `Tether` type.
21
+ */
22
+ export interface TetherInternals {
23
+ /** `coreModules`: the modules the parts use instead of bundling copies, in the `<script>` build only. */
24
+ __core: unknown;
25
+ /** Each part registers its module here when its file has loaded. */
26
+ __parts: Partial<Parts>;
27
+ }
28
+
29
+ const PARTS: { [Name in PartName]: () => Promise<Parts[Name]> } = {
30
+ diagnose: () => import('./diagnose.ts'),
31
+ markets: () => import('./markets-ui.ts'),
32
+ search: () => import('./search.ts'),
33
+ extras: () => import('./extras.ts'),
34
+ account: () => import('./account.ts'),
35
+ 'account-pages': () => import('./account-pages.ts'),
36
+ analytics: () => import('./analytics.ts'),
37
+ };
38
+
39
+ export const loadPart = <Name extends PartName>(name: Name): Promise<Parts[Name]> => PARTS[name]();
40
+
41
+ /** Only the `<script>` build shares its modules with separately loaded parts. */
42
+ export const coreModules = undefined;
@@ -3,11 +3,15 @@
3
3
  * is cached for the life of the page, which survives Barba/Swup page swaps. Products the server rendered
4
4
  * (`tether/server` `hydrate()`) are used at once and refreshed once in the background.
5
5
  */
6
- import { cfg, log } from './config.js';
7
- import { gql, TetherError } from './client.js';
8
- import { productFragment, collectionFields } from './fragments.js';
9
- import { productKey } from './shopify-values.js';
10
- /** @import { Product, Image } from './fragments.js' */
6
+ import { cfg, log } from './config.ts';
7
+ import { gql, matches, orNull, shaped, TetherError } from './client.ts';
8
+ import { productFragment, collectionFields } from './fragments.ts';
9
+ import { productKey } from './shopify-values.ts';
10
+ import { isRecord } from '../server/src/shared/json.ts';
11
+ import { PRODUCT, PRODUCT_CONNECTION, PRODUCT_NODE } from './product-shape.ts';
12
+ import type { Product, Image } from './fragments.ts';
13
+ import type { QueryText } from './client.ts';
14
+ import type { ProductConnection } from './product-shape.ts';
11
15
 
12
16
  /**
13
17
  * A product request that missed is remembered this long, so one page asking twice sends one request, and a product
@@ -17,16 +21,20 @@ const MISS_CACHE_MS = 10_000;
17
21
  /** A product loaded live this recently is newer than a server-rendered copy of it, which is then ignored. */
18
22
  const FRESH_MS = 60_000;
19
23
 
20
- /** @type {Map<string, Promise<Product>>} Handle or gid to the product, loaded or loading. */
21
- const productCache = new Map();
22
- /** @type {Map<string, { resolve: (product: Product) => void, reject: (error: unknown) => void }>} */
23
- const waiting = new Map();
24
- /** @type {Set<string>} Keys of products that came from the server and weren't refreshed yet. */
25
- const stale = new Set();
26
- /** @type {Map<string, number>} Handle or gid to when it last loaded live, in ms since the epoch. */
27
- const freshAt = new Map();
28
- /** @type {string[]} Keys for the next batch. */
29
- const pending = [];
24
+ interface Waiter {
25
+ resolve: (product: Product) => void;
26
+ reject: (error: unknown) => void;
27
+ }
28
+
29
+ /** Handle or gid to the product, loaded or loading. */
30
+ const productCache = new Map<string, Promise<Product>>();
31
+ const waiting = new Map<string, Waiter>();
32
+ /** Keys of products that came from the server and weren't refreshed yet. */
33
+ const stale = new Set<string>();
34
+ /** Handle or gid to when it last loaded live, in ms since the epoch. */
35
+ const freshAt = new Map<string, number>();
36
+ /** Keys for the next batch. */
37
+ const pending: string[] = [];
30
38
  let flushScheduled = false;
31
39
  /**
32
40
  * Bumped by `clearProductCache()`: responses to requests sent before that (in the old market, after a country
@@ -34,18 +42,13 @@ let flushScheduled = false;
34
42
  */
35
43
  let generation = 0;
36
44
 
37
- /** @param {string} key */
38
- const isGid = (key) => key.startsWith('gid://');
45
+ const isGid = (key: string) => key.startsWith('gid://');
39
46
 
40
47
  /** Accepts a handle, numeric id, gid or a full product URL. */
41
48
  export const normalizeProductKey = productKey;
42
49
 
43
- /**
44
- * Puts a product the page already has (from a collection query) in the cache, under its handle and its id.
45
- *
46
- * @param {Product | null | undefined} product
47
- */
48
- export function primeProduct(product) {
50
+ /** Puts a product the page already has (from a collection query) in the cache, under its handle and its id. */
51
+ export function primeProduct(product: Product | null | undefined): void {
49
52
  if (!product) return;
50
53
  const loaded = Promise.resolve(product);
51
54
  productCache.set(product.handle, loaded);
@@ -53,18 +56,16 @@ export function primeProduct(product) {
53
56
  }
54
57
 
55
58
  /**
56
- * @param {string | number | null | undefined} rawKey A handle, numeric id, gid or product URL.
57
- * @returns {Promise<Product>}
59
+ * @param rawKey A handle, numeric id, gid or product URL.
58
60
  * @throws {TetherError} `NOT_FOUND` (rejected) for an empty key or a product the store doesn't have.
59
61
  */
60
- export function getProduct(rawKey) {
62
+ export function getProduct(rawKey: string | number | null | undefined): Promise<Product> {
61
63
  const key = normalizeProductKey(rawKey);
62
64
  if (!key) return Promise.reject(new TetherError('empty product key', { code: 'NOT_FOUND' }));
63
65
  const cached = productCache.get(key);
64
66
  if (cached) return cached;
65
67
 
66
- /** @type {Promise<Product>} */
67
- const loading = new Promise((resolve, reject) => {
68
+ const loading = new Promise<Product>((resolve, reject) => {
68
69
  waiting.set(key, { resolve, reject });
69
70
  pending.push(key);
70
71
  if (!flushScheduled) {
@@ -77,17 +78,25 @@ export function getProduct(rawKey) {
77
78
  }
78
79
 
79
80
  /**
80
- * @param {string[]} keys Handles and gids.
81
- * @returns {{ query: string, variables: Record<string, unknown>, handles: string[] }} One query for all of them: gids
82
- * through `nodes(ids:)` as `byId`, each handle as its own alias `h<index>`.
81
+ * @param operation An operation that spreads `...TetherProduct`.
82
+ * @returns The operation with the product fragment (which starts on its own line), written for what the token may
83
+ * read.
83
84
  */
84
- function batchQuery(keys) {
85
+ const withProductFragment =
86
+ (operation: string): QueryText =>
87
+ (access) =>
88
+ `${operation}${productFragment(cfg, access)}`;
89
+
90
+ /**
91
+ * @param keys Handles and gids.
92
+ * @returns One query for all of them: gids through `nodes(ids:)` as `byId`, each handle as its own alias `h<index>`.
93
+ */
94
+ function batchQuery(keys: string[]): { query: QueryText; variables: Record<string, unknown>; handles: string[] } {
85
95
  const ids = keys.filter(isGid);
86
96
  const handles = keys.filter((key) => !isGid(key));
87
- const definitions = [];
88
- const selections = [];
89
- /** @type {Record<string, unknown>} */
90
- const variables = {};
97
+ const definitions: string[] = [];
98
+ const selections: string[] = [];
99
+ const variables: Record<string, unknown> = {};
91
100
  if (ids.length) {
92
101
  definitions.push('$ids: [ID!]!');
93
102
  variables['ids'] = ids;
@@ -98,25 +107,17 @@ function batchQuery(keys) {
98
107
  variables[`h${index}`] = handle;
99
108
  selections.push(`h${index}: product(handle: $h${index}) { ...TetherProduct }`);
100
109
  });
101
- const query = `query TetherProducts(${definitions.join(',')}) { ${selections.join(' ')} } ${productFragment()}`;
110
+ const query = withProductFragment(`query TetherProducts(${definitions.join(',')}) { ${selections.join(' ')} }`);
102
111
  return { query, variables, handles };
103
112
  }
104
113
 
105
- /**
106
- * @param {string} key
107
- * @param {(waiter: { resolve: (product: Product) => void, reject: (error: unknown) => void }) => void} settle
108
- */
109
- function settleWaiting(key, settle) {
114
+ function settleWaiting(key: string, settle: (waiter: Waiter) => void): void {
110
115
  const waiter = waiting.get(key);
111
116
  waiting.delete(key);
112
117
  if (waiter) settle(waiter);
113
118
  }
114
119
 
115
- /**
116
- * @param {string} key
117
- * @param {number} batchGeneration
118
- */
119
- function forgetMissLater(key, batchGeneration) {
120
+ function forgetMissLater(key: string, batchGeneration: number): void {
120
121
  if (batchGeneration !== generation) return;
121
122
  const miss = productCache.get(key);
122
123
  setTimeout(() => {
@@ -124,6 +125,19 @@ function forgetMissLater(key, batchGeneration) {
124
125
  }, MISS_CACHE_MS);
125
126
  }
126
127
 
128
+ /** A node of a type other than Product, for which `... on Product` selects no field. */
129
+ interface OtherNode {
130
+ id?: undefined;
131
+ }
132
+ /** The answer to `batchQuery`: `byId` for the gids, `h<index>` for each handle. */
133
+ type BatchData = Partial<Record<`h${number}`, Product | null>> & { byId?: (Product | OtherNode | null)[] };
134
+
135
+ function isBatchData(data: unknown): data is BatchData {
136
+ if (!isRecord(data)) return false;
137
+ const { byId = [], ...byHandle } = data;
138
+ return matches(byId, [orNull(PRODUCT_NODE)]) && Object.values(byHandle).every(orNull(PRODUCT));
139
+ }
140
+
127
141
  async function flush() {
128
142
  flushScheduled = false;
129
143
  const batchGeneration = generation;
@@ -132,10 +146,9 @@ async function flush() {
132
146
  const { query, variables, handles } = batchQuery(keys);
133
147
  log('fetch products', keys);
134
148
 
135
- /** @type {Record<string, Product | null> & { byId?: (Product | null)[] }} */
136
149
  let data;
137
150
  try {
138
- data = await gql(query, variables);
151
+ data = await gql(query, variables, isBatchData);
139
152
  } catch (error) {
140
153
  keys.forEach((key) => {
141
154
  settleWaiting(key, (waiter) => {
@@ -146,9 +159,8 @@ async function flush() {
146
159
  return;
147
160
  }
148
161
 
149
- /** @type {Map<string, Product>} */
150
- const found = new Map();
151
- for (const product of data.byId ?? []) if (product) found.set(product.id, product);
162
+ const found = new Map<string, Product>();
163
+ for (const product of data.byId ?? []) if (product?.id) found.set(product.id, product);
152
164
  handles.forEach((handle, index) => {
153
165
  const product = data[`h${index}`];
154
166
  if (product) found.set(handle, product);
@@ -174,7 +186,7 @@ async function flush() {
174
186
  }
175
187
 
176
188
  /** Forgets every product, for a new market: requests already sent no longer fill the cache. */
177
- export function clearProductCache() {
189
+ export function clearProductCache(): void {
178
190
  productCache.clear();
179
191
  generation += 1;
180
192
  stale.clear();
@@ -184,10 +196,8 @@ export function clearProductCache() {
184
196
  /**
185
197
  * Uses products the server rendered, unless the page loaded the same product live in the last `FRESH_MS` (an earlier
186
198
  * page of a View Transition visit), whose copy is newer.
187
- *
188
- * @param {readonly Product[] | null | undefined} products
189
199
  */
190
- export function hydrateProducts(products) {
200
+ export function hydrateProducts(products: readonly Product[] | null | undefined): void {
191
201
  for (const product of products ?? []) {
192
202
  if (!product.id || !product.handle) continue;
193
203
  const loadedAgo = Date.now() - (freshAt.get(product.handle) ?? 0);
@@ -198,11 +208,8 @@ export function hydrateProducts(products) {
198
208
  }
199
209
  }
200
210
 
201
- /**
202
- * @param {string | number | null | undefined} rawKey
203
- * @returns {boolean} True once per server-rendered product: the caller then refreshes it.
204
- */
205
- export function takeStale(rawKey) {
211
+ /** @returns True once per server-rendered product: the caller then refreshes it. */
212
+ export function takeStale(rawKey: string | number | null | undefined): boolean {
206
213
  const key = normalizeProductKey(rawKey);
207
214
  if (!key || !stale.has(key)) return false;
208
215
  stale.delete(key);
@@ -210,46 +217,66 @@ export function takeStale(rawKey) {
210
217
  void productCache.get(key)?.then((product) => {
211
218
  stale.delete(product.handle);
212
219
  stale.delete(product.id);
213
- });
220
+ }, failureIsReportedByTheRender);
214
221
  return true;
215
222
  }
216
223
 
217
224
  /**
218
- * Loads these products again, past the cache.
219
- *
220
- * @param {readonly (string | number)[]} keys
221
- * @returns {Promise<Product>[]}
225
+ * The cached promise is the one the product's render awaits, and the render reports its failure: the stale cleanup
226
+ * above has nothing left to do for a product that did not load.
222
227
  */
223
- export function refreshProducts(keys) {
224
- for (const key of keys) {
225
- const normalized = normalizeProductKey(key);
226
- if (normalized) productCache.delete(normalized);
227
- }
228
- return keys.map((key) => getProduct(key));
228
+ function failureIsReportedByTheRender() {
229
+ // Nothing to clean: see above.
229
230
  }
230
231
 
231
- /** @typedef {{ hasNextPage: boolean, endCursor: string | null }} PageInfo */
232
- /**
233
- * @typedef {object} Collection
234
- * @property {string} id
235
- * @property {string} handle
236
- * @property {string} title
237
- * @property {string} description
238
- * @property {string} descriptionHtml
239
- * @property {Image | null} image
240
- * @property {{ title: string | null, description: string | null }} seo
241
- */
242
- /** @typedef {{ collection: Collection | null, products: Product[], pageInfo: PageInfo }} ProductPage */
243
- /**
244
- * @typedef {object} ProductQuery
245
- * @property {string} [collection] A collection handle; without one, a search.
246
- * @property {string} [query] Storefront search syntax, for a search.
247
- * @property {number} [first]
248
- * @property {string | null} [after] The cursor of the page before.
249
- * @property {string} [sortKey] `ProductCollectionSortKeys` for a collection, `ProductSortKeys` for a search.
250
- * @property {boolean} [reverse]
251
- * @property {unknown[]} [filters] `ProductFilter` values, for a collection.
252
- */
232
+ /** Loads this product again, past the cache. Products refreshed in the same tick go out as one request. */
233
+ export function refreshProduct(key: string | number): Promise<Product> {
234
+ const normalized = normalizeProductKey(key);
235
+ if (normalized) productCache.delete(normalized);
236
+ return getProduct(key);
237
+ }
238
+
239
+ export interface PageInfo {
240
+ hasNextPage: boolean;
241
+ endCursor: string | null;
242
+ }
243
+ export interface Collection {
244
+ id: string;
245
+ handle: string;
246
+ title: string;
247
+ description: string;
248
+ descriptionHtml: string;
249
+ image: Image | null;
250
+ seo: { title: string | null; description: string | null };
251
+ }
252
+ export interface ProductPage {
253
+ collection: Collection | null;
254
+ products: Product[];
255
+ pageInfo: PageInfo;
256
+ }
257
+ export interface ProductQuery {
258
+ /** A collection handle; without one, a search. */
259
+ collection?: string;
260
+ /** Storefront search syntax, for a search. */
261
+ query?: string;
262
+ first?: number;
263
+ /** The cursor of the page before. */
264
+ after?: string | null;
265
+ /** `ProductCollectionSortKeys` for a collection, `ProductSortKeys` for a search. */
266
+ sortKey?: string;
267
+ reverse?: boolean;
268
+ /** `ProductFilter` values, for a collection. */
269
+ filters?: unknown[];
270
+ }
271
+
272
+ /** A collection's answer: `null` for a handle the store doesn't have. Its other fields are written as text. */
273
+ export const isCollectionAnswer = shaped<{ collection: (Collection & { products: ProductConnection }) | null }>({
274
+ collection: orNull({ handle: 'string', products: PRODUCT_CONNECTION }),
275
+ });
276
+ export const isSearchAnswer = shaped<{ products: ProductConnection }>({ products: PRODUCT_CONNECTION });
277
+ export const isRecommendationsAnswer = shaped<{ productRecommendations: Product[] | null }>({
278
+ productRecommendations: orNull([PRODUCT]),
279
+ });
253
280
 
254
281
  const COLLECTION_QUERY = `query TetherCollection($handle: String!, $first: Int!, $after: String, $sortKey: ProductCollectionSortKeys, $reverse: Boolean, $filters: [ProductFilter!]) {
255
282
  collection(handle: $handle) {
@@ -271,8 +298,6 @@ const SEARCH_QUERY = `query TetherSearch($first: Int!, $after: String, $query: S
271
298
  /**
272
299
  * A page of products from a collection (by handle) or from a search. Every product on it is cached.
273
300
  *
274
- * @param {ProductQuery} [options]
275
- * @returns {Promise<ProductPage>}
276
301
  * @throws {TetherError} `NOT_FOUND` for a collection the store doesn't have.
277
302
  */
278
303
  export async function getProducts({
@@ -283,32 +308,31 @@ export async function getProducts({
283
308
  sortKey,
284
309
  reverse = false,
285
310
  filters,
286
- } = {}) {
287
- const fragment = productFragment();
311
+ }: ProductQuery = {}): Promise<ProductPage> {
288
312
  if (collection) {
289
- /** @type {{ collection: (Collection & { products: { nodes: Product[], pageInfo: PageInfo } }) | null }} */
290
- const data = await gql(`${COLLECTION_QUERY}\n${fragment}`, {
291
- handle: collection,
292
- first,
293
- after,
294
- sortKey: sortKey || undefined,
295
- reverse,
296
- filters: filters ?? undefined,
297
- });
313
+ const data = await gql(
314
+ withProductFragment(COLLECTION_QUERY),
315
+ {
316
+ handle: collection,
317
+ first,
318
+ after,
319
+ sortKey: sortKey || undefined,
320
+ reverse,
321
+ filters: filters ?? undefined,
322
+ },
323
+ isCollectionAnswer,
324
+ );
298
325
  if (!data.collection) throw new TetherError(`Collection not found: ${collection}`, { code: 'NOT_FOUND' });
299
326
  const { products, ...rest } = data.collection;
300
327
  products.nodes.forEach(primeProduct);
301
328
  return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
302
329
  }
303
330
 
304
- /** @type {{ products: { nodes: Product[], pageInfo: PageInfo } }} */
305
- const data = await gql(`${SEARCH_QUERY}\n${fragment}`, {
306
- first,
307
- after,
308
- query: query || undefined,
309
- sortKey: sortKey || undefined,
310
- reverse,
311
- });
331
+ const data = await gql(
332
+ withProductFragment(SEARCH_QUERY),
333
+ { first, after, query: query || undefined, sortKey: sortKey || undefined, reverse },
334
+ isSearchAnswer,
335
+ );
312
336
  data.products.nodes.forEach(primeProduct);
313
337
  return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
314
338
  }
@@ -316,20 +340,22 @@ export async function getProducts({
316
340
  /**
317
341
  * Shopify's recommendations for a product. Every product in them is cached.
318
342
  *
319
- * @param {string | number | { id: string } | null | undefined} product A product, or its handle, id or gid.
320
- * @param {'RELATED' | 'COMPLEMENTARY'} [intent]
321
- * @returns {Promise<Product[]>}
343
+ * @param product A product, or its handle, id or gid.
322
344
  */
323
- export async function getRecommendations(product, intent = 'RELATED') {
345
+ export async function getRecommendations(
346
+ product: string | number | { id: string } | null | undefined,
347
+ intent: 'RELATED' | 'COMPLEMENTARY' = 'RELATED',
348
+ ): Promise<Product[]> {
324
349
  const key = normalizeProductKey(typeof product === 'object' ? product?.id : product);
325
350
  const byId = key?.startsWith('gid://') ?? false;
326
- /** @type {{ productRecommendations: Product[] | null }} */
327
351
  const data = await gql(
328
- `query TetherRecs(${byId ? '$id: ID!' : '$handle: String!'}, $intent: ProductRecommendationIntent) {
352
+ withProductFragment(
353
+ `query TetherRecs(${byId ? '$id: ID!' : '$handle: String!'}, $intent: ProductRecommendationIntent) {
329
354
  productRecommendations(${byId ? 'productId: $id' : 'productHandle: $handle'}, intent: $intent) { ...TetherProduct }
330
- }
331
- ${productFragment()}`,
355
+ }`,
356
+ ),
332
357
  byId ? { id: key, intent } : { handle: key, intent },
358
+ isRecommendationsAnswer,
333
359
  );
334
360
  const list = data.productRecommendations ?? [];
335
361
  list.forEach(primeProduct);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Country and language codes as Shopify's `@inContext` takes them, parsed by the module the Worker uses for the
3
+ * login's market, so the browser and the Worker accept and refuse the same values.
4
+ */
5
+ export { toCountry, toLanguage } from '../server/src/shared/market-codes.ts';
6
+ export type { CountryCode, LanguageCode } from '../server/src/shared/market-codes.ts';