@occluze/tether 0.7.0 → 0.7.2

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 (139) hide show
  1. package/README.md +51 -109
  2. package/dist/esm/astro.js +1 -1
  3. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-W4FBRKXA.js} +1 -1
  4. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IM3OQTOP.js} +1 -1
  5. package/dist/esm/chunks/analytics-QDVOLZY3.js +1 -0
  6. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-AUVAN3KZ.js} +1 -1
  7. package/dist/esm/chunks/chunk-BMDYNMRC.js +1 -0
  8. package/dist/esm/chunks/chunk-CUL66KRA.js +1 -0
  9. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-CVNRODH3.js} +1 -1
  10. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-CXHPBT7N.js} +1 -1
  11. package/dist/esm/chunks/chunk-GF6JTI65.js +1 -0
  12. package/dist/esm/chunks/chunk-MRSDJPBC.js +1 -0
  13. package/dist/esm/chunks/chunk-NYDZPTLP.js +1 -0
  14. package/dist/esm/chunks/chunk-QDHLXXAZ.js +1 -0
  15. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-QUECUXHD.js} +1 -1
  16. package/dist/esm/chunks/chunk-RJY76ESL.js +1 -0
  17. package/dist/esm/chunks/chunk-RPI5C3EJ.js +1 -0
  18. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-YC4JXWRG.js} +2 -2
  19. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-3XHPZZNP.js} +1 -1
  20. package/dist/esm/chunks/extras-C724KKG6.js +1 -0
  21. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-WBWWQAGU.js} +1 -1
  22. package/dist/esm/chunks/{search-UJ73YBQ5.js → search-5NFIZBPJ.js} +1 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/tether.account-pages.js +13 -3
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +13 -3
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +18 -3
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +20 -5
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +52 -28
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +353 -288
  36. package/dist/tether.markets.js +31 -5
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +2 -2
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +18 -3
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  43. package/dist/types/src/cart/mutations.d.ts +6 -0
  44. package/dist/types/src/cart/store.d.ts +5 -2
  45. package/dist/types/src/cart.d.ts +3 -2
  46. package/dist/types/src/client.d.ts +61 -22
  47. package/dist/types/src/config.d.ts +6 -1
  48. package/dist/types/src/index.d.ts +2 -2
  49. package/dist/types/src/markets.d.ts +9 -2
  50. package/dist/types/src/product-shape.d.ts +3 -1
  51. package/dist/types/src/uri.d.ts +6 -0
  52. package/package.json +54 -6
  53. package/dist/esm/chunks/analytics-2V632IRV.js +0 -1
  54. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  55. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  56. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  57. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  58. package/dist/esm/chunks/chunk-E5YO6274.js +0 -1
  59. package/dist/esm/chunks/chunk-MPDWM2NN.js +0 -1
  60. package/dist/esm/chunks/chunk-YLUULATY.js +0 -1
  61. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  62. package/src/account/addresses-view.ts +0 -58
  63. package/src/account/answers.ts +0 -79
  64. package/src/account/markup.ts +0 -36
  65. package/src/account/operations.ts +0 -145
  66. package/src/account/order-lines.ts +0 -75
  67. package/src/account/order-list.ts +0 -107
  68. package/src/account/order-page.ts +0 -137
  69. package/src/account/page-answers.ts +0 -317
  70. package/src/account/page-events.ts +0 -253
  71. package/src/account/server-client.ts +0 -183
  72. package/src/account/subscriptions-view.ts +0 -115
  73. package/src/account-api.ts +0 -98
  74. package/src/account-marks.ts +0 -35
  75. package/src/account-pages.ts +0 -81
  76. package/src/account.ts +0 -284
  77. package/src/amounts.ts +0 -64
  78. package/src/analytics-shopify.ts +0 -246
  79. package/src/analytics.ts +0 -315
  80. package/src/api-version.ts +0 -79
  81. package/src/astro.ts +0 -272
  82. package/src/cart/cart-shape.ts +0 -25
  83. package/src/cart/checkout.ts +0 -101
  84. package/src/cart/drawer.ts +0 -125
  85. package/src/cart/mutations.ts +0 -237
  86. package/src/cart/render.ts +0 -266
  87. package/src/cart/store.ts +0 -279
  88. package/src/cart.ts +0 -345
  89. package/src/client.ts +0 -391
  90. package/src/collection.ts +0 -175
  91. package/src/config-sections.ts +0 -284
  92. package/src/config.ts +0 -480
  93. package/src/controls/cart-controls.ts +0 -123
  94. package/src/controls/discount.ts +0 -75
  95. package/src/controls/extras.ts +0 -54
  96. package/src/controls/navigation.ts +0 -77
  97. package/src/diagnose-markup.ts +0 -298
  98. package/src/diagnose.ts +0 -294
  99. package/src/dom.ts +0 -268
  100. package/src/extras.ts +0 -136
  101. package/src/fragments.ts +0 -261
  102. package/src/globals.d.ts +0 -15
  103. package/src/images.ts +0 -91
  104. package/src/index.ts +0 -415
  105. package/src/initial.ts +0 -101
  106. package/src/lazy-iife.ts +0 -197
  107. package/src/lazy.ts +0 -42
  108. package/src/loader.ts +0 -363
  109. package/src/market-codes.ts +0 -6
  110. package/src/markets-ui.ts +0 -227
  111. package/src/markets.ts +0 -246
  112. package/src/markup.ts +0 -127
  113. package/src/mounting.ts +0 -264
  114. package/src/page-market.ts +0 -110
  115. package/src/part-integrity.ts +0 -7
  116. package/src/parts/account-pages.ts +0 -7
  117. package/src/parts/account.ts +0 -7
  118. package/src/parts/analytics.ts +0 -7
  119. package/src/parts/diagnose.ts +0 -7
  120. package/src/parts/extras.ts +0 -7
  121. package/src/parts/markets.ts +0 -7
  122. package/src/parts/search.ts +0 -7
  123. package/src/presets/barba.ts +0 -194
  124. package/src/product-events.ts +0 -115
  125. package/src/product-render.ts +0 -218
  126. package/src/product-shape.ts +0 -49
  127. package/src/product-state.ts +0 -318
  128. package/src/product.ts +0 -308
  129. package/src/repeaters.ts +0 -161
  130. package/src/search.ts +0 -305
  131. package/src/selling-plans.ts +0 -145
  132. package/src/server.ts +0 -211
  133. package/src/shopify-values.ts +0 -91
  134. package/src/status.ts +0 -106
  135. package/src/storage.ts +0 -34
  136. package/src/store-html.ts +0 -359
  137. package/src/storefront/catalog.ts +0 -196
  138. package/src/storefront/json-ld.ts +0 -144
  139. package/src/variant-render.ts +0 -139
package/src/index.ts DELETED
@@ -1,415 +0,0 @@
1
- /**
2
- * Tether's entry: `window.Tether`, `init()`, `mount()` and the start of the plain `<script>` setup.
3
- *
4
- * <script>window.TETHER_CONFIG = { domain: 'x.myshopify.com', token: '...' }</script>
5
- * <script src="tether.min.js" defer></script>
6
- *
7
- * With page transitions (Barba, Swup): set `autoMount: false`, call `Tether.init()` once and
8
- * `Tether.mount(container)` after every page enter.
9
- */
10
- import { cfg, configView, configure, isConfigured, log, warn, reportError } from './config.ts';
11
- import { query, TetherError, withContext } from './client.ts';
12
- import { Cart } from './cart.ts';
13
- import { scopes } from './product.ts';
14
- import { ListScope, listSelector } from './collection.ts';
15
- import {
16
- getProduct,
17
- getProducts,
18
- getRecommendations,
19
- clearProductCache,
20
- primeProduct,
21
- normalizeProductKey,
22
- } from './loader.ts';
23
- import * as plans from './selling-plans.ts';
24
- import { resolveProductKey } from './status.ts';
25
- import { sel, emit, isOwnEvent, ANALYTICS_EVENTS, formatMoney, imageUrl, pageGlobal } from './dom.ts';
26
- import { loadPart, coreModules } from './lazy.ts';
27
- import {
28
- mountedLists,
29
- liveLists,
30
- pruneLive,
31
- within,
32
- mountProduct,
33
- hydrateFromPage,
34
- settleMarket,
35
- trackMarket,
36
- } from './mounting.ts';
37
- import { applyStartMarket, setCountry } from './page-market.ts';
38
- import { accountApi, mountAccountIfUsed } from './account-api.ts';
39
- import { forgetBrowserModeTokens } from './account-marks.ts';
40
- import { bindCartControls } from './controls/cart-controls.ts';
41
- import { bindDiscountControls } from './controls/discount.ts';
42
- import { addGroup, bindExtrasControls } from './controls/extras.ts';
43
- import { bindNavigation, rememberDocumentMount } from './controls/navigation.ts';
44
- import { field } from '../server/src/shared/json.ts';
45
- import type { TetherConfig, TetherOptions, MarketsConfig } from './config.ts';
46
- import type { ShopifyCart } from './cart/store.ts';
47
- import type { ProductScope } from './product.ts';
48
- import type { DiagnoseOptions } from './diagnose.ts';
49
- import type { TetherEvents } from './markup.ts';
50
- import type { TetherInternals } from './lazy.ts';
51
-
52
- export { addGroup, setCountry };
53
-
54
- // The types a site uses by name, under the names the docs give them.
55
- export type { TetherOptions as TetherConfig, TetherConfig as ResolvedTetherConfig } from './config.ts';
56
- export type { MarketsConfig, CartWarning } from './config.ts';
57
- export type { Money } from './shopify-values.ts';
58
- export type {
59
- Image as ShopifyImage,
60
- Metafield as ShopifyMetafield,
61
- Product as ShopifyProduct,
62
- ProductVariant as ShopifyVariant,
63
- SellingPlan as ShopifySellingPlanNode,
64
- SellingPlanAdjustmentValue,
65
- SellingPlanAllocation as ShopifySellingPlanAllocation,
66
- SellingPlanGroup as ShopifySellingPlanGroup,
67
- } from './fragments.ts';
68
- export type {
69
- PlanInGroup as ShopifySellingPlan,
70
- PlanAdjustment,
71
- ProductWithPlans,
72
- VariantWithPlans,
73
- } from './selling-plans.ts';
74
- export type { CartStatus, CartVariant as ShopifyCartVariant, ShopifyCart, ShopifyCartLine } from './cart/store.ts';
75
- export type { Attributes, LineSpec as LineInput } from './cart/mutations.ts';
76
- export type { TetherError, TetherErrorCode, GraphQLError, UserError } from './client.ts';
77
- export type { ProductScope } from './product.ts';
78
- export type { Purchase } from './product-state.ts';
79
- export type { ProductPage, ProductQuery } from './loader.ts';
80
- export type { DetectedMarket, Localization, MarketSource } from './markets.ts';
81
- export type { SetCountryOptions } from './page-market.ts';
82
- export type { DiagnoseOptions, DiagnoseReport } from './diagnose.ts';
83
- export type { TetherEvents } from './markup.ts';
84
- export type { AccountCustomer } from './account/answers.ts';
85
- export type { AccountImage as ImageRef, AccountOrder, AccountOrderLine } from '../server/src/shared/account-orders.ts';
86
- export type { AddressFields as CustomerAddressInput } from './account/operations.ts';
87
- export type {
88
- CustomerAddress,
89
- Fulfillment,
90
- MailingAddress,
91
- OrderDetail,
92
- OrderDetailLine,
93
- SubscriptionContract,
94
- SubscriptionLine,
95
- } from './account/page-answers.ts';
96
- /** The helpers for selling plans, `Tether.plans`. */
97
- export type SellingPlanHelpers = typeof plans;
98
-
99
- export const VERSION = '0.7.0';
100
-
101
- /** Milliseconds `observe` waits after the last added node, so a burst of additions mounts once. */
102
- const OBSERVE_DEBOUNCE_MS = 50;
103
-
104
- /** @returns The keys of `window.TETHER_CONFIG`, none when it is no object. */
105
- function pageConfig(): Readonly<Record<string, unknown>> {
106
- const config = pageGlobal('TETHER_CONFIG');
107
- return typeof config === 'object' && config !== null ? Object.fromEntries(Object.entries(config)) : {};
108
- }
109
-
110
- let initPromise: Promise<Tether | null> | null = null;
111
-
112
- let observer: MutationObserver | null = null;
113
-
114
- /** The analytics part's queue, created once by the first `init()` with `analytics`. */
115
- let analyticsQueue: Event[] | null = null;
116
-
117
- /** The document-wide listeners are bound once, by the first configured `init()`. */
118
- let controlsBound = false;
119
-
120
- /**
121
- * Mounts the lists of `root` and the product scopes in them as they render.
122
- *
123
- * @returns The lists mounted.
124
- */
125
- function mountLists(root: ParentNode): number {
126
- let count = 0;
127
- within(root, listSelector()).forEach((el) => {
128
- if (mountedLists.has(el)) return;
129
- mountedLists.add(el);
130
- count++;
131
- const list = new ListScope(el);
132
- liveLists.add(list);
133
- el.addEventListener(`${cfg.prefix}:list-rendered`, (event) => {
134
- // The list emits this once its products are in the page, with the element they are in.
135
- if (isOwnEvent(event, 'list-rendered')) mount(event.detail.container);
136
- });
137
- void trackMarket(list.load());
138
- });
139
- return count;
140
- }
141
-
142
- /**
143
- * Loads each optional part `root` uses: the country select, customer accounts and search as you type.
144
- */
145
- function mountParts(root: ParentNode): void {
146
- if (within(root, `${sel('country-select')},${sel('country-bind')}`).length) {
147
- loadPart('markets')
148
- .then((part) => part.renderCountrySelects(root))
149
- .catch((error: unknown) => {
150
- reportError(error, 'markets');
151
- });
152
- }
153
- mountAccountIfUsed(root);
154
- if (within(root, sel('search')).length) {
155
- loadPart('search')
156
- .then((part) => {
157
- part.mountSearch(root);
158
- })
159
- .catch((error: unknown) => {
160
- reportError(error, 'search');
161
- });
162
- }
163
- }
164
-
165
- /**
166
- * Mounts the lists, product scopes and parts inside `root`, after its hydrate tags were read (products the server
167
- * rendered into the page need no request), without the page-wide work that follows a mount.
168
- *
169
- * @returns The scopes and lists newly mounted.
170
- */
171
- function mountWithin(root: Document | Element): number {
172
- let count = mountLists(root);
173
- within(root, sel('product')).forEach((el) => {
174
- if (mountProduct(el)) count++;
175
- });
176
- mountParts(root);
177
- return count;
178
- }
179
-
180
- /**
181
- * Mounts every product scope and product list inside `root`. Safe to call repeatedly.
182
- *
183
- * @returns The scopes and lists newly mounted.
184
- */
185
- export function mount(root: Document | Element = document): number {
186
- if (!isConfigured()) return 0;
187
- if (root === document) rememberDocumentMount();
188
- // Page transitions detach old roots: they aren't kept alive.
189
- pruneLive();
190
- hydrateFromPage(root.ownerDocument ?? document);
191
- const count = mountWithin(root);
192
- settleMarket();
193
- Cart.render();
194
- log(`mounted ${count} scope(s)`, root);
195
- return count;
196
- }
197
-
198
- /**
199
- * `observe`: mounts a burst of added elements at once. Each root is scanned on its own (an element inside another
200
- * added one is covered by it), and the page-wide work (market state, the cart's markup) runs once for the burst.
201
- */
202
- function mountAdded(added: readonly Element[]): void {
203
- const roots = added.filter(
204
- (root) => root.isConnected && !added.some((other) => other !== root && other.contains(root)),
205
- );
206
- if (!roots.length || !isConfigured()) return;
207
- pruneLive();
208
- // Only the added elements are read for hydrate tags: the rest of the page was read when it was mounted. A load-more
209
- // fragment may bring its tag after the cards it is for, so every tag is read before any card mounts.
210
- roots.forEach((root) => {
211
- hydrateFromPage(root);
212
- });
213
- const count = roots.reduce((sum, root) => sum + mountWithin(root), 0);
214
- settleMarket();
215
- Cart.render();
216
- log(`mounted ${count} scope(s) in ${roots.length} added element(s)`);
217
- }
218
-
219
- /** Call before a page transition removes the page: closes the cart drawer. */
220
- export function unmount(): void {
221
- Cart.close();
222
- }
223
-
224
- /** @returns The product scope `el` is in. */
225
- export function getScope(el: Element | null | undefined): ProductScope | null {
226
- const root = el?.closest(sel('product'));
227
- return root ? (scopes.get(root) ?? null) : null;
228
- }
229
-
230
- function bindControls() {
231
- if (controlsBound) return;
232
- controlsBound = true;
233
- bindCartControls();
234
- bindDiscountControls();
235
- bindExtrasControls();
236
- bindNavigation({ ready: () => initPromise ?? Promise.resolve(null), mount, unmount });
237
- }
238
-
239
- /** Buffers Tether's events until the analytics part has loaded, so none are missed. */
240
- function startAnalyticsQueue() {
241
- const queue: Event[] = [];
242
- analyticsQueue = queue;
243
- ANALYTICS_EVENTS.forEach((name) => {
244
- document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event));
245
- });
246
- document.addEventListener('astro:page-load', (event) => queue.push(event));
247
- loadPart('analytics')
248
- .then((part) => part.startAnalytics(queue))
249
- .catch((error: unknown) => {
250
- reportError(error, 'analytics');
251
- });
252
- }
253
-
254
- /**
255
- * A saved cart from another market moves to this one, so its prices match the page's.
256
- */
257
- async function cartInPageMarket(cart: ShopifyCart | null): Promise<ShopifyCart | null> {
258
- const country = cfg.country;
259
- const cartCountry = cart?.buyerIdentity.countryCode;
260
- if (!cart || !country || !cartCountry || cartCountry === country) return cart;
261
- try {
262
- return await Cart.setBuyerIdentity({ countryCode: country }, { create: false });
263
- } catch (error) {
264
- reportError(error, 'cart country');
265
- return cart;
266
- }
267
- }
268
-
269
- /**
270
- * Configures Tether (`window.TETHER_CONFIG` with `overrides` over it), applies the visitor's market and loads the
271
- * cart. A second call without overrides returns the first one's promise.
272
- *
273
- * @returns `null` without a domain and token, when Tether stays idle.
274
- */
275
- export function init(overrides: TetherOptions = {}): Promise<Tether | null> {
276
- if (initPromise && !Object.keys(overrides).length) return initPromise;
277
- configure({ ...pageConfig(), ...overrides });
278
- if (!isConfigured()) {
279
- warn('TETHER_CONFIG is missing domain or token. Tether is idle.');
280
- initPromise = Promise.resolve(null);
281
- return initPromise;
282
- }
283
- forgetBrowserModeTokens();
284
- bindControls();
285
- if (cfg.observe) startObserver();
286
- if (cfg.analytics && !analyticsQueue) startAnalyticsQueue();
287
- initPromise = applyStartMarket(overrides)
288
- .then(() => Cart.init())
289
- .then(cartInPageMarket)
290
- .then((cart) => {
291
- emit('ready', { cart });
292
- return api;
293
- });
294
- return initPromise;
295
- }
296
-
297
- /** `observe`: mounts the elements added to the page later. */
298
- function startObserver() {
299
- if (observer || typeof MutationObserver === 'undefined') return;
300
- let timer: ReturnType<typeof setTimeout> | undefined;
301
- const queued = new Set<Element>();
302
- observer = new MutationObserver((mutations) => {
303
- mutations.forEach((mutation) => {
304
- mutation.addedNodes.forEach((node) => {
305
- if (node instanceof window.Element) queued.add(node);
306
- });
307
- });
308
- clearTimeout(timer);
309
- timer = setTimeout(() => {
310
- const added = [...queued];
311
- queued.clear();
312
- mountAdded(added);
313
- }, OBSERVE_DEBOUNCE_MS);
314
- });
315
- observer.observe(document.body, { childList: true, subtree: true });
316
- }
317
-
318
- /** @param markets Default `markets` of the config. */
319
- const detectMarket = (markets?: MarketsConfig | null) =>
320
- loadPart('markets').then((part) => part.detectMarket(markets ?? cfg.markets));
321
-
322
- const getLocalization = () => loadPart('markets').then((part) => part.getLocalization());
323
-
324
- const diagnose = (options?: Omit<DiagnoseOptions, 'version'>) =>
325
- loadPart('diagnose').then((part) => part.diagnose({ ...options, version: VERSION }));
326
-
327
- /**
328
- * Listens to the Tether event `<prefix>:<name>`. Only the events Tether dispatched reach the listener: one another
329
- * script dispatches under the same name is ignored, so the detail is always the one `TetherEvents` gives.
330
- *
331
- * @returns Stops listening.
332
- */
333
- function on<Name extends keyof TetherEvents>(
334
- name: Name,
335
- listener: (detail: TetherEvents[Name], event: CustomEvent<TetherEvents[Name]>) => void,
336
- ): () => void {
337
- const type = `${cfg.prefix}:${name}`;
338
- const handler = (event: Event) => {
339
- if (isOwnEvent(event, name)) listener(event.detail, event);
340
- };
341
- document.addEventListener(type, handler);
342
- return () => {
343
- document.removeEventListener(type, handler);
344
- };
345
- }
346
-
347
- const publicApi = {
348
- VERSION,
349
- init,
350
- mount,
351
- unmount,
352
- /** The live config, read-only. */
353
- get config(): Readonly<TetherConfig> {
354
- return configView;
355
- },
356
- get ready(): Promise<Tether | null> {
357
- return initPromise ?? Promise.resolve(null);
358
- },
359
- query,
360
- withContext,
361
- cart: Cart,
362
- getProduct,
363
- getProducts,
364
- getRecommendations,
365
- getScope,
366
- addGroup,
367
- account: accountApi,
368
- primeProduct,
369
- normalizeProductKey,
370
- clearProductCache,
371
- formatMoney,
372
- imageUrl,
373
- plans,
374
- TetherError,
375
- resolveProductKey,
376
- setCountry,
377
- detectMarket,
378
- getLocalization,
379
- diagnose,
380
- on,
381
- };
382
-
383
- /** `window.Tether`, and what `import Tether from '@occluze/tether'` gives. */
384
- export type Tether = typeof publicApi;
385
-
386
- const internals: TetherInternals = { __core: coreModules, __parts: {} };
387
-
388
- /** `Tether`, also carrying `TetherInternals`, which the published type leaves out. */
389
- export const api: Tether = Object.assign(publicApi, internals);
390
-
391
- /** @alias `api`, for `import Tether from '@occluze/tether'`. */
392
- export default api;
393
-
394
- /**
395
- * The plain `<script>` setup starts by itself, unless `autoMount: false` or a framework integration (Astro), which
396
- * starts Tether itself.
397
- */
398
- function autoStart() {
399
- // `window.Tether` is declared by the published types, which describe this object to sites; the source doesn't
400
- // depend on them.
401
- Reflect.set(window, 'Tether', api);
402
- const config = pageGlobal('TETHER_CONFIG');
403
- if (!config || field(config, 'autoMount') === false || pageGlobal('TETHER_INTEGRATION')) return;
404
- const start = () => {
405
- init()
406
- .then(() => mount(document))
407
- .catch((error: unknown) => {
408
- reportError(error, 'init');
409
- });
410
- };
411
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
412
- else start();
413
- }
414
-
415
- if (typeof window !== 'undefined' && typeof document !== 'undefined') autoStart();
package/src/initial.ts DELETED
@@ -1,101 +0,0 @@
1
- /**
2
- * What the browser script shows first for a product, as plain data, so a server renders the same and nothing flashes
3
- * when Tether takes over. Public as `initialState()` of `@occluze/tether/server`; the rules are `product-state.ts`'s,
4
- * the ones the browser scope uses.
5
- */
6
- import { DEFAULTS } from './config.ts';
7
- import { TetherError } from './client.ts';
8
- import { initialChoice, isValueAvailable, productView } from './product-state.ts';
9
- import { allPlans, allocationFor, planPrice, planSavings } from './selling-plans.ts';
10
- import type { Money } from './shopify-values.ts';
11
- import type { Product, ProductVariant } from './fragments.ts';
12
- import type { PlanInGroup } from './selling-plans.ts';
13
- import type { Selection, ShowCondition } from './product-state.ts';
14
-
15
- export interface InitialOptions {
16
- /** The page's `?variant=`; the first variant for sale without. */
17
- variant?: string | null | undefined;
18
- /** The page's `?selling_plan=`: subscribes on that plan. */
19
- sellingPlan?: string | null | undefined;
20
- /** `subscription` subscribes on the first plan. */
21
- defaultPurchase?: string | null | undefined;
22
- /** `low-stock` at this many or fewer. */
23
- lowStock?: number | undefined;
24
- }
25
- /** A plan in the plan list (`tether-plan-list`). */
26
- interface InitialPlan {
27
- id: string;
28
- name: string;
29
- /** On this plan, for the variant. */
30
- price: Money | null;
31
- /** In %, 0 without savings. */
32
- savings: number;
33
- /** `is-selected`: the line is bought on this plan. */
34
- selected: boolean;
35
- /** The variant is on this plan; `is-unavailable` when not. */
36
- available: boolean;
37
- }
38
- export interface InitialState {
39
- variant: ProductVariant;
40
- /** Option name → value of the variant: `is-selected` on those buttons. */
41
- selected: Selection;
42
- purchase: 'one-time' | 'subscription';
43
- /** The plan the line is bought on, `null` when bought once. */
44
- plan: PlanInGroup | null;
45
- plans: InitialPlan[];
46
- /** The offered plan's savings in % (`tether-bind="subscription-savings"`), 0 without. */
47
- savings: number;
48
- price: Money;
49
- /** Only when on sale. */
50
- compareAtPrice: Money | null;
51
- available: boolean;
52
- /** The `tether-show` conditions: `hidden={!state.show['out-of-stock']}`. */
53
- show: Record<ShowCondition, boolean>;
54
- /**
55
- * Per option, the values without a variant for sale (given the other selected values), as plain data for
56
- * frameworks that serialise props.
57
- */
58
- unavailable: Record<string, string[]>;
59
- /** The same as a function: `is-unavailable`. */
60
- isUnavailable: (name: string, value: string) => boolean;
61
- }
62
-
63
- /** @throws TetherError For a product without variants, which Shopify never sends: every product has one. */
64
- export function initialState(product: Product, options?: InitialOptions): InitialState;
65
- /** @returns `null` without a product. */
66
- export function initialState(product: Product | null | undefined, options?: InitialOptions): InitialState | null;
67
- export function initialState(product: Product | null | undefined, options: InitialOptions = {}): InitialState | null {
68
- if (!product) return null;
69
- const { variant, sellingPlan, defaultPurchase = DEFAULTS.defaultPurchase, lowStock = DEFAULTS.lowStock } = options;
70
- const choice = initialChoice(product, { variant, sellingPlan, defaultPurchase });
71
- const view = productView(product, { ...choice, quantity: 1 }, lowStock);
72
- if (!view) throw new TetherError(`Product ${product.handle} has no variants`);
73
- const { selected, line } = view;
74
- const isUnavailable = (name: string, value: string) => !isValueAvailable(product, selected, name, value);
75
- return {
76
- variant: view.variant,
77
- selected,
78
- purchase: view.purchase.kind,
79
- plan: line.kind === 'subscription' ? view.plan : null,
80
- plans: allPlans(product).map((plan) => ({
81
- id: plan.id,
82
- name: plan.name,
83
- price: planPrice(view.variant, plan.id),
84
- savings: planSavings(view.variant, plan.id).percentage,
85
- selected: line.kind === 'subscription' && plan.id === line.planId,
86
- available: Boolean(allocationFor(view.variant, plan.id)),
87
- })),
88
- savings: view.savings.percentage,
89
- price: view.price,
90
- compareAtPrice: view.compareAtPrice,
91
- available: view.variant.availableForSale,
92
- show: view.show,
93
- unavailable: Object.fromEntries(
94
- product.options.map((option) => [
95
- option.name,
96
- option.optionValues.map((value) => value.name).filter((value) => isUnavailable(option.name, value)),
97
- ]),
98
- ),
99
- isUnavailable,
100
- };
101
- }