@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.
- package/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +320 -365
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +242 -491
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +66 -88
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +36 -33
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +28 -36
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1270 -894
- package/dist/tether.markets.js +73 -90
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +44 -68
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -23
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/{order-lines.js → order-lines.ts} +20 -41
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/{page-events.js → page-events.ts} +55 -90
- package/src/account/server-client.ts +183 -0
- package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/{account-pages.js → account-pages.ts} +29 -37
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
- package/src/analytics.ts +315 -0
- package/src/{api-version.js → api-version.ts} +25 -31
- package/src/{astro.js → astro.ts} +110 -104
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/{drawer.js → drawer.ts} +18 -31
- package/src/cart/mutations.ts +237 -0
- package/src/cart/{render.js → render.ts} +68 -96
- package/src/cart/store.ts +279 -0
- package/src/{cart.js → cart.ts} +82 -128
- package/src/client.ts +391 -0
- package/src/{collection.js → collection.ts} +30 -38
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
- package/src/controls/{discount.js → discount.ts} +13 -34
- package/src/controls/{extras.js → extras.ts} +15 -24
- package/src/controls/{navigation.js → navigation.ts} +24 -30
- package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
- package/src/{diagnose.js → diagnose.ts} +95 -96
- package/src/dom.ts +268 -0
- package/src/{extras.js → extras.ts} +43 -52
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/{images.js → images.ts} +30 -43
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/{loader.js → loader.ts} +149 -123
- package/src/market-codes.ts +6 -0
- package/src/{markets-ui.js → markets-ui.ts} +91 -68
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/{mounting.js → mounting.ts} +88 -101
- package/src/{page-market.js → page-market.ts} +32 -38
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/{product-events.js → product-events.ts} +33 -48
- package/src/{product-render.js → product-render.ts} +38 -77
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/{product.js → product.ts} +90 -126
- package/src/{repeaters.js → repeaters.ts} +40 -52
- package/src/{search.js → search.ts} +85 -103
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/{shopify-values.js → shopify-values.ts} +24 -22
- package/src/{status.js → status.ts} +34 -54
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
- package/src/{variant-render.js → variant-render.ts} +24 -50
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics.js +0 -352
- package/src/cart/checkout.js +0 -97
- package/src/cart/mutations.js +0 -234
- package/src/cart/store.js +0 -326
- package/src/client.js +0 -267
- package/src/config.js +0 -349
- package/src/dom.js +0 -287
- package/src/fragments.js +0 -237
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/market-codes.js +0 -35
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-state.js +0 -318
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- package/types/server.d.ts +0 -182
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
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.
|
|
3
|
+
* the products the server rendered into the page. Shared by `index.ts` and the optional markets part.
|
|
4
4
|
*/
|
|
5
|
-
import { cfg, log, warn, reportError } from './config.
|
|
6
|
-
import { ProductScope, scopes, rememberOptionNames } from './product.
|
|
7
|
-
import { listSelector } from './collection.
|
|
8
|
-
import { getProduct, takeStale,
|
|
9
|
-
import { resolveProductKey, setScopeStatus, isNotFound } from './status.
|
|
10
|
-
import { attr, get, qsa, emit } from './dom.
|
|
11
|
-
|
|
5
|
+
import { cfg, log, warn, reportError } from './config.ts';
|
|
6
|
+
import { ProductScope, scopes, rememberOptionNames } from './product.ts';
|
|
7
|
+
import { listSelector } from './collection.ts';
|
|
8
|
+
import { getProduct, takeStale, refreshProduct, hydrateProducts } from './loader.ts';
|
|
9
|
+
import { resolveProductKey, setScopeStatus, isNotFound } from './status.ts';
|
|
10
|
+
import { attr, get, qsa, emit } from './dom.ts';
|
|
11
|
+
import { toCountry, toLanguage } from './market-codes.ts';
|
|
12
|
+
import { isRecord } from '../server/src/shared/json.ts';
|
|
13
|
+
import { isProductShape } from './product-shape.ts';
|
|
14
|
+
import type { Product } from './fragments.ts';
|
|
15
|
+
import type { CountryCode, LanguageCode } from './market-codes.ts';
|
|
12
16
|
|
|
13
|
-
/**
|
|
17
|
+
/** A mounted `ListScope`. */
|
|
18
|
+
interface MountedList {
|
|
19
|
+
root: Element;
|
|
20
|
+
load: (more?: boolean) => Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type KeySource = Pick<ReturnType<typeof resolveProductKey>, 'fromUrl' | 'raw'>;
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* The marker on the `<script type="application/json">` tags `tether/server`'s `hydrate()` writes. It is fixed, not
|
|
@@ -18,19 +28,17 @@ import { attr, get, qsa, emit } from './dom.js';
|
|
|
18
28
|
*/
|
|
19
29
|
const HYDRATE_SELECTOR = 'script[type="application/json"][data-tether-hydrate]';
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
export const
|
|
23
|
-
/**
|
|
24
|
-
export const
|
|
25
|
-
/**
|
|
26
|
-
export const
|
|
27
|
-
/**
|
|
28
|
-
|
|
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
|
+
export const mountedProducts = new WeakSet<Element>();
|
|
32
|
+
export const mountedLists = new WeakSet<Element>();
|
|
33
|
+
/** Product roots to re-render after a country switch; pruned once disconnected. */
|
|
34
|
+
export const liveProducts = new Set<HTMLElement>();
|
|
35
|
+
/** Lists to reload after a country switch; pruned once disconnected. */
|
|
36
|
+
export const liveLists = new Set<MountedList>();
|
|
37
|
+
/** Each product root's latest request, so a slow older response can't win. */
|
|
38
|
+
const latestRequest = new WeakMap<Element, object>();
|
|
31
39
|
|
|
32
40
|
/** Forgets roots a page transition removed, so they aren't kept alive. */
|
|
33
|
-
export function pruneLive() {
|
|
41
|
+
export function pruneLive(): void {
|
|
34
42
|
liveProducts.forEach((el) => {
|
|
35
43
|
if (!el.isConnected) liveProducts.delete(el);
|
|
36
44
|
});
|
|
@@ -39,23 +47,15 @@ export function pruneLive() {
|
|
|
39
47
|
});
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
export function within(root, selector) {
|
|
48
|
-
const self = root.matches?.(selector) ? [/** @type {HTMLElement} */ (root)] : [];
|
|
50
|
+
/** @returns `root` itself when it matches, then the matches inside it. */
|
|
51
|
+
export function within(root: ParentNode & Partial<Pick<Element, 'matches'>>, selector: string): HTMLElement[] {
|
|
52
|
+
// A root that matches carries Tether's markup, which goes on HTML elements (see `qsa`).
|
|
53
|
+
const self = root.matches?.(selector) ? [root as HTMLElement] : [];
|
|
49
54
|
return [...self, ...qsa(root, selector)];
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* @param {Element} el
|
|
56
|
-
* @param {Product} product
|
|
57
|
-
*/
|
|
58
|
-
function applyPageTitle(el, product) {
|
|
57
|
+
/** `tether-page-title="{title} | My shop"` on a product root sets `document.title`, for dynamic product pages. */
|
|
58
|
+
function applyPageTitle(el: Element, product: Product): void {
|
|
59
59
|
const template = get(el, 'page-title');
|
|
60
60
|
if (!template || typeof document === 'undefined') return;
|
|
61
61
|
document.title = template
|
|
@@ -67,11 +67,8 @@ function applyPageTitle(el, product) {
|
|
|
67
67
|
/**
|
|
68
68
|
* A product root without a key: on a dynamic page opened without `?handle=` (or with an empty path) it is a product
|
|
69
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
70
|
*/
|
|
74
|
-
function mountWithoutKey(el, { fromUrl, raw }) {
|
|
71
|
+
function mountWithoutKey(el: HTMLElement, { fromUrl, raw }: KeySource): void {
|
|
75
72
|
if (fromUrl) {
|
|
76
73
|
mountedProducts.add(el);
|
|
77
74
|
setScopeStatus(el, 'not-found');
|
|
@@ -81,13 +78,7 @@ function mountWithoutKey(el, { fromUrl, raw }) {
|
|
|
81
78
|
}
|
|
82
79
|
}
|
|
83
80
|
|
|
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) {
|
|
81
|
+
function showLoadFailure(el: HTMLElement, key: string, { fromUrl, raw }: KeySource, error: unknown): void {
|
|
91
82
|
const notFound = isNotFound(error);
|
|
92
83
|
setScopeStatus(el, notFound ? 'not-found' : 'error', error);
|
|
93
84
|
if (!notFound) {
|
|
@@ -104,10 +95,9 @@ function showLoadFailure(el, key, { fromUrl, raw }, error) {
|
|
|
104
95
|
/**
|
|
105
96
|
* Mounts one product root.
|
|
106
97
|
*
|
|
107
|
-
* @
|
|
108
|
-
* @returns {boolean} True when a product request was started.
|
|
98
|
+
* @returns True when a product request was started.
|
|
109
99
|
*/
|
|
110
|
-
export function mountProduct(el) {
|
|
100
|
+
export function mountProduct(el: HTMLElement): boolean {
|
|
111
101
|
if (mountedProducts.has(el)) return false;
|
|
112
102
|
const { key, ...source } = resolveProductKey(el);
|
|
113
103
|
if (!key) {
|
|
@@ -132,14 +122,14 @@ export function mountProduct(el) {
|
|
|
132
122
|
applyPageTitle(el, product);
|
|
133
123
|
if (cfg.revalidate && takeStale(key)) queueRefresh(key);
|
|
134
124
|
})
|
|
135
|
-
.catch((
|
|
125
|
+
.catch((error: unknown) => {
|
|
136
126
|
if (isLatest()) showLoadFailure(el, key, source, error);
|
|
137
127
|
});
|
|
138
128
|
return true;
|
|
139
129
|
}
|
|
140
130
|
|
|
141
131
|
/** Loads again the product scopes that failed on a network or API error, for when the browser is back online. */
|
|
142
|
-
export function retryFailed() {
|
|
132
|
+
export function retryFailed(): void {
|
|
143
133
|
pruneLive();
|
|
144
134
|
liveProducts.forEach((el) => {
|
|
145
135
|
if (el.getAttribute(attr('state')) !== 'error' || el.getAttribute(attr('error')) === 'not-found') return;
|
|
@@ -148,20 +138,16 @@ export function retryFailed() {
|
|
|
148
138
|
});
|
|
149
139
|
}
|
|
150
140
|
|
|
151
|
-
/**
|
|
152
|
-
let refreshQueue = new Set();
|
|
141
|
+
/** Server-rendered products to refresh in the next batch. */
|
|
142
|
+
let refreshQueue = new Set<string>();
|
|
153
143
|
|
|
154
|
-
|
|
155
|
-
function queueRefresh(key) {
|
|
144
|
+
function queueRefresh(key: string): void {
|
|
156
145
|
if (!refreshQueue.size) setTimeout(flushRefresh, 0);
|
|
157
146
|
refreshQueue.add(key);
|
|
158
147
|
}
|
|
159
148
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
* @param {Product} fresh
|
|
163
|
-
*/
|
|
164
|
-
function applyRefresh(key, fresh) {
|
|
149
|
+
/** @param key The key the refresh was asked for. */
|
|
150
|
+
function applyRefresh(key: string, fresh: Product): void {
|
|
165
151
|
liveProducts.forEach((el) => {
|
|
166
152
|
const scope = scopes.get(el);
|
|
167
153
|
if (!scope || !el.isConnected) return;
|
|
@@ -175,38 +161,35 @@ function applyRefresh(key, fresh) {
|
|
|
175
161
|
function flushRefresh() {
|
|
176
162
|
const keys = [...refreshQueue];
|
|
177
163
|
refreshQueue = new Set();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
promise
|
|
164
|
+
keys.forEach((key) => {
|
|
165
|
+
refreshProduct(key)
|
|
181
166
|
.then((fresh) => {
|
|
182
167
|
applyRefresh(key, fresh);
|
|
183
168
|
})
|
|
184
|
-
.catch((
|
|
169
|
+
.catch((error: unknown) => {
|
|
185
170
|
reportError(error, `refresh ${key}`);
|
|
186
171
|
});
|
|
187
172
|
});
|
|
188
173
|
}
|
|
189
174
|
|
|
190
175
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
176
|
+
* While the page's server-rendered prices are another market's: the loads to wait for before
|
|
177
|
+
* `html.tether-market-pending` goes. `null` when no market is pending.
|
|
193
178
|
*/
|
|
194
|
-
let marketPending = null;
|
|
179
|
+
let marketPending: Set<Promise<unknown>> | null = null;
|
|
195
180
|
|
|
196
181
|
/**
|
|
197
182
|
* Lists, and anything else that loads prices, hold `html.tether-market-pending` until they are in.
|
|
198
183
|
*
|
|
199
|
-
* @
|
|
200
|
-
* @param {Promise<T>} loading
|
|
201
|
-
* @returns {Promise<T>} `loading` itself.
|
|
184
|
+
* @returns `loading` itself.
|
|
202
185
|
*/
|
|
203
|
-
export const trackMarket = (loading) => {
|
|
186
|
+
export const trackMarket = <T>(loading: Promise<T>): Promise<T> => {
|
|
204
187
|
marketPending?.add(loading);
|
|
205
188
|
return loading;
|
|
206
189
|
};
|
|
207
190
|
|
|
208
191
|
/** After a mount: drops `html.tether-market-pending` once this page's products are live. */
|
|
209
|
-
export function settleMarket() {
|
|
192
|
+
export function settleMarket(): void {
|
|
210
193
|
if (!marketPending) return;
|
|
211
194
|
const waits = [...marketPending];
|
|
212
195
|
void Promise.allSettled(waits).then(() => {
|
|
@@ -220,52 +203,56 @@ export function settleMarket() {
|
|
|
220
203
|
});
|
|
221
204
|
}
|
|
222
205
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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();
|
|
206
|
+
/** What `tether/server`'s `hydrate()` writes: the market it fetched in, and the products. */
|
|
207
|
+
interface HydratePayload {
|
|
208
|
+
country: CountryCode | undefined;
|
|
209
|
+
language: LanguageCode | undefined;
|
|
210
|
+
products: Product[];
|
|
211
|
+
}
|
|
241
212
|
|
|
242
|
-
/** @
|
|
243
|
-
|
|
213
|
+
/** @param json A hydrate tag's JSON. */
|
|
214
|
+
function toHydratePayload(json: unknown): HydratePayload {
|
|
215
|
+
if (!isRecord(json)) return { country: undefined, language: undefined, products: [] };
|
|
216
|
+
const { products } = json;
|
|
217
|
+
const entries: unknown[] = Array.isArray(products) ? products : [];
|
|
218
|
+
return {
|
|
219
|
+
country: toCountry(json['country']) ?? undefined,
|
|
220
|
+
language: toLanguage(json['language']) ?? undefined,
|
|
221
|
+
products: entries.filter(isProductShape),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
244
224
|
|
|
225
|
+
/** Hydrate tags already read. */
|
|
226
|
+
const readScripts = new WeakSet<Element>();
|
|
245
227
|
/**
|
|
246
228
|
* Uses the products in the page's hydrate tags (`tether/server` `hydrate()`) without a request. A tag made for another
|
|
247
229
|
* market is skipped, and marks the page (`html.tether-market-pending`) until the live prices are in, so CSS can hide
|
|
248
230
|
* prices meanwhile.
|
|
249
231
|
*
|
|
250
|
-
*
|
|
232
|
+
* Trust: a hydrate tag is part of the page, written by the site's own build like the rest of its HTML and scripts, so
|
|
233
|
+
* its prices and products are shown until the background refresh replaces them. Whoever can write a tag into the page
|
|
234
|
+
* can already write a script there; the shape check (`isProductShape`) is for a truncated or hand-written tag, which
|
|
235
|
+
* is skipped instead of failing a render.
|
|
236
|
+
*
|
|
237
|
+
* @param root Where to look: the document for a mount (a tag may sit in `<head>` or anywhere else on the page), the
|
|
238
|
+
* added elements for `observe`.
|
|
251
239
|
*/
|
|
252
|
-
export function hydrateFromPage(root = document) {
|
|
253
|
-
|
|
240
|
+
export function hydrateFromPage(root: Document | Element = document): void {
|
|
241
|
+
within(root, HYDRATE_SELECTOR).forEach((script) => {
|
|
254
242
|
if (readScripts.has(script)) return;
|
|
255
243
|
readScripts.add(script);
|
|
256
|
-
|
|
257
|
-
let json;
|
|
244
|
+
let json: unknown;
|
|
258
245
|
try {
|
|
259
246
|
json = JSON.parse(script.textContent);
|
|
260
247
|
} catch (error) {
|
|
261
248
|
warn('bad data-tether-hydrate', error);
|
|
262
249
|
return;
|
|
263
250
|
}
|
|
264
|
-
const { country, language, products } =
|
|
265
|
-
|
|
266
|
-
rememberOptionNames(
|
|
267
|
-
if (
|
|
268
|
-
hydrateProducts(
|
|
251
|
+
const { country, language, products } = toHydratePayload(json);
|
|
252
|
+
// The build's option names, also for other languages.
|
|
253
|
+
rememberOptionNames(products);
|
|
254
|
+
if (country === cfg.country && language === cfg.language) {
|
|
255
|
+
hydrateProducts(products);
|
|
269
256
|
return;
|
|
270
257
|
}
|
|
271
258
|
log('hydration skipped: other market');
|
|
@@ -3,24 +3,25 @@
|
|
|
3
3
|
* saved or detected market, then the configured one. Init applies it to `cfg` once. Astro's ClientRouter keeps the
|
|
4
4
|
* script running across pages, so each arrival asks again: the next page may be built for another market, or for none.
|
|
5
5
|
*/
|
|
6
|
-
import { cfg, log, reportError } from './config.
|
|
7
|
-
import { loadPart } from './lazy.
|
|
8
|
-
import { toCountry, toLanguage } from './market-codes.
|
|
9
|
-
import { readSavedMarket } from './markets.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { cfg, log, reportError } from './config.ts';
|
|
7
|
+
import { loadPart } from './lazy.ts';
|
|
8
|
+
import { toCountry, toLanguage } from './market-codes.ts';
|
|
9
|
+
import { readSavedMarket } from './markets.ts';
|
|
10
|
+
import type { TetherOptions } from './config.ts';
|
|
11
|
+
import type { Market, DetectedMarket } from './markets.ts';
|
|
12
|
+
import type { Parts } from './lazy.ts';
|
|
13
|
+
import type { ShopifyCart } from './cart/store.ts';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export type SetCountryOptions = Parameters<Parts['markets']['setCountry']>[1];
|
|
16
|
+
|
|
17
|
+
/** The market of the config, before a page's or visitor's market applied. */
|
|
18
|
+
interface ConfiguredMarket {
|
|
19
|
+
country: string | undefined;
|
|
20
|
+
language: string | undefined;
|
|
21
|
+
locale: string | undefined;
|
|
22
|
+
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
let configuredMarket = { country: undefined, language: undefined, locale: undefined };
|
|
24
|
+
let configuredMarket: ConfiguredMarket = { country: undefined, language: undefined, locale: undefined };
|
|
24
25
|
|
|
25
26
|
/** Whether the page arrived at last was built for its own market, which the next page without one switches back from. */
|
|
26
27
|
let lastPageHadOwnMarket = false;
|
|
@@ -29,11 +30,10 @@ let lastPageHadOwnMarket = false;
|
|
|
29
30
|
* `<meta name="tether-page-country" content="DE">`, with `tether-page-language` and `tether-page-locale`. Never
|
|
30
31
|
* remembered: it belongs to the page, not to the visitor.
|
|
31
32
|
*
|
|
32
|
-
* @returns
|
|
33
|
+
* @returns `null` when the page has no valid country of its own.
|
|
33
34
|
*/
|
|
34
|
-
function pageMarket() {
|
|
35
|
-
|
|
36
|
-
const meta = (name) =>
|
|
35
|
+
function pageMarket(): Market | null {
|
|
36
|
+
const meta = (name: string) =>
|
|
37
37
|
document.querySelector(`meta[name="${cfg.prefix}-page-${name}"]`)?.getAttribute('content') || undefined;
|
|
38
38
|
const country = toCountry(meta('country'));
|
|
39
39
|
if (!country) return null;
|
|
@@ -42,10 +42,8 @@ function pageMarket() {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Without `markets.detect` only the visitor's saved choice applies, read without loading the markets part.
|
|
45
|
-
*
|
|
46
|
-
* @returns {Promise<Market | DetectedMarket | null>}
|
|
47
45
|
*/
|
|
48
|
-
async function visitorMarket() {
|
|
46
|
+
async function visitorMarket(): Promise<Market | DetectedMarket | null> {
|
|
49
47
|
const markets = cfg.markets ?? {};
|
|
50
48
|
if (markets.detect === false) return null;
|
|
51
49
|
if (!markets.detect) return markets.remember === false ? null : readSavedMarket();
|
|
@@ -60,10 +58,9 @@ async function visitorMarket() {
|
|
|
60
58
|
/**
|
|
61
59
|
* Sets `cfg`'s market for the first page, over the configured one.
|
|
62
60
|
*
|
|
63
|
-
* @param
|
|
64
|
-
* @returns {Promise<void>}
|
|
61
|
+
* @param overrides What `init()` was called with: a `locale` there wins over the visitor's.
|
|
65
62
|
*/
|
|
66
|
-
export async function applyStartMarket(overrides) {
|
|
63
|
+
export async function applyStartMarket(overrides: TetherOptions): Promise<void> {
|
|
67
64
|
configuredMarket = { country: cfg.country, language: cfg.language, locale: cfg.locale };
|
|
68
65
|
const page = pageMarket();
|
|
69
66
|
lastPageHadOwnMarket = page !== null;
|
|
@@ -81,20 +78,19 @@ export async function applyStartMarket(overrides) {
|
|
|
81
78
|
log('market', market);
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
* @param {string | undefined} a
|
|
86
|
-
* @param {string | undefined} b
|
|
87
|
-
*/
|
|
88
|
-
const sameCode = (a, b) => (a ?? '').toUpperCase() === (b ?? '').toUpperCase();
|
|
81
|
+
const sameCode = (a: string | undefined, b: string | undefined) => (a ?? '').toUpperCase() === (b ?? '').toUpperCase();
|
|
89
82
|
|
|
90
83
|
/**
|
|
91
84
|
* The market to switch to on arriving at the current page. A page built for a market switches to it; the first page
|
|
92
85
|
* without one after such a page switches back to the visitor's own choice, else to the configured market.
|
|
93
86
|
*
|
|
94
|
-
* @returns
|
|
95
|
-
* stays in the market in use.
|
|
87
|
+
* @returns `null` when the page stays in the market in use.
|
|
96
88
|
*/
|
|
97
|
-
export function marketForArrival() {
|
|
89
|
+
export function marketForArrival(): {
|
|
90
|
+
country: string;
|
|
91
|
+
language: string | undefined;
|
|
92
|
+
locale: string | undefined;
|
|
93
|
+
} | null {
|
|
98
94
|
const page = pageMarket();
|
|
99
95
|
const leftOwnMarket = page === null && lastPageHadOwnMarket;
|
|
100
96
|
lastPageHadOwnMarket = page !== null;
|
|
@@ -107,10 +103,8 @@ export function marketForArrival() {
|
|
|
107
103
|
/**
|
|
108
104
|
* `Tether.setCountry('DE')`: loads the markets part on first use.
|
|
109
105
|
*
|
|
110
|
-
* @
|
|
111
|
-
* @param {SetCountryOptions} [options]
|
|
112
|
-
* @returns {Promise<ShopifyCart | null>} The cart once everything is in the new market.
|
|
106
|
+
* @returns The cart once everything is in the new market.
|
|
113
107
|
*/
|
|
114
|
-
export function setCountry(code, options) {
|
|
108
|
+
export function setCountry(code: string, options?: SetCountryOptions): Promise<ShopifyCart | null> {
|
|
115
109
|
return loadPart('markets').then((part) => part.setCountry(code, options));
|
|
116
110
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Subresource Integrity hash of each optional part's file, by file name, so the core loads only the parts built
|
|
3
|
+
* with it (src/lazy-iife.ts). Every `<script>` build, watch mode included, puts the hashes of the parts it built into
|
|
4
|
+
* the core in place of this module (scripts/build.ts); the empty map here only lets the source type-check, and a part
|
|
5
|
+
* missing from the map is not loaded.
|
|
6
|
+
*/
|
|
7
|
+
export const PART_INTEGRITY: Readonly<Record<string, string>> = {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.account-pages.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../account-pages.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.account-pages.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts['account-pages'] = part;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.account.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../account.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.account.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts.account = part;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.analytics.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../analytics.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.analytics.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts.analytics = part;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.diagnose.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../diagnose.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.diagnose.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts.diagnose = part;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.extras.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../extras.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.extras.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts.extras = part;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.markets.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../markets-ui.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.markets.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts.markets = part;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The <script> build's tether.search.js: registers the part where the core script's loader (lazy-iife.ts) finds it.
|
|
2
|
+
import * as part from '../search.ts';
|
|
3
|
+
import type { TetherInternals } from '../lazy.ts';
|
|
4
|
+
|
|
5
|
+
const { Tether } = window as { Tether?: TetherInternals };
|
|
6
|
+
if (!Tether) throw new Error('tether.search.js runs after the Tether script, which loads it');
|
|
7
|
+
Tether.__parts.search = part;
|