@occluze/tether 0.6.7 → 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 -1
- package/dist/presets/barba.esm.js +55 -39
- package/dist/tether.account-pages.js +781 -495
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +462 -381
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +299 -194
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +354 -262
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +82 -45
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3653 -2116
- package/dist/tether.markets.js +238 -150
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +3 -3
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +207 -136
- 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 +42 -22
- 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.ts +75 -0
- 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.ts +253 -0
- package/src/account/server-client.ts +183 -0
- package/src/account/subscriptions-view.ts +115 -0
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/account-pages.ts +81 -0
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/analytics-shopify.ts +246 -0
- package/src/analytics.ts +315 -0
- package/src/api-version.ts +79 -0
- package/src/astro.ts +272 -0
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/drawer.ts +125 -0
- package/src/cart/mutations.ts +237 -0
- package/src/cart/render.ts +266 -0
- package/src/cart/store.ts +279 -0
- package/src/cart.ts +345 -0
- package/src/client.ts +391 -0
- package/src/collection.ts +175 -0
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/cart-controls.ts +123 -0
- package/src/controls/discount.ts +75 -0
- package/src/controls/extras.ts +54 -0
- package/src/controls/navigation.ts +77 -0
- package/src/diagnose-markup.ts +298 -0
- package/src/diagnose.ts +294 -0
- package/src/dom.ts +268 -0
- package/src/extras.ts +136 -0
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/images.ts +91 -0
- 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.ts +363 -0
- package/src/market-codes.ts +6 -0
- package/src/markets-ui.ts +227 -0
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/mounting.ts +264 -0
- package/src/page-market.ts +110 -0
- 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.ts +115 -0
- package/src/product-render.ts +218 -0
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/product.ts +308 -0
- package/src/repeaters.ts +161 -0
- package/src/search.ts +305 -0
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/shopify-values.ts +91 -0
- package/src/status.ts +106 -0
- 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.ts +144 -0
- package/src/variant-render.ts +139 -0
- package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
- package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
- package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
- package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
- package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
- package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
- package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
- package/src/account-pages.js +0 -318
- package/src/account.js +0 -385
- package/src/analytics.js +0 -232
- package/src/api-version.js +0 -35
- package/src/astro.js +0 -101
- package/src/cart.js +0 -559
- package/src/client.js +0 -131
- package/src/collection.js +0 -114
- package/src/config.js +0 -85
- package/src/diagnose.js +0 -246
- package/src/dom.js +0 -288
- package/src/extras.js +0 -63
- package/src/fragments.js +0 -144
- package/src/index.js +0 -395
- package/src/initial.js +0 -75
- package/src/lazy-iife.js +0 -46
- package/src/lazy.js +0 -18
- package/src/loader.js +0 -198
- package/src/markets-ui.js +0 -90
- package/src/markets.js +0 -133
- package/src/mounting.js +0 -154
- package/src/parts/account-pages.js +0 -3
- package/src/parts/account.js +0 -3
- package/src/parts/analytics.js +0 -3
- package/src/parts/diagnose.js +0 -3
- package/src/parts/extras.js +0 -3
- package/src/parts/markets.js +0 -3
- package/src/parts/search.js +0 -3
- package/src/presets/barba.js +0 -100
- package/src/product.js +0 -570
- package/src/search.js +0 -153
- package/src/selling-plans.js +0 -94
- package/src/server.js +0 -252
- package/src/status.js +0 -66
- package/types/astro.d.ts +0 -17
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -426
- package/types/server.d.ts +0 -110
package/src/loader.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data loading. Products are batched per tick into one request and cached for
|
|
3
|
-
* the lifetime of the page (survives Barba/Swup page swaps).
|
|
4
|
-
*/
|
|
5
|
-
import { cfg, log } from './config.js';
|
|
6
|
-
import { gql, TetherError } from './client.js';
|
|
7
|
-
import { productFragment, collectionFields } from './fragments.js';
|
|
8
|
-
|
|
9
|
-
const productCache = new Map(); // key -> Promise<product>
|
|
10
|
-
let pending = [];
|
|
11
|
-
const resolvers = new Map();
|
|
12
|
-
let scheduled = false;
|
|
13
|
-
// Bumped by clearProductCache(): responses to requests sent before that (e.g. in the old
|
|
14
|
-
// market after a country switch) still reach their callers but never refill the cache.
|
|
15
|
-
let generation = 0;
|
|
16
|
-
|
|
17
|
-
const isGid = (v) => typeof v === 'string' && v.startsWith('gid://');
|
|
18
|
-
|
|
19
|
-
/** Accepts a handle, numeric id, gid or a full product URL. */
|
|
20
|
-
export function normalizeProductKey(raw) {
|
|
21
|
-
const v = String(raw ?? '').trim();
|
|
22
|
-
if (!v) return null;
|
|
23
|
-
if (isGid(v)) return v;
|
|
24
|
-
if (/^\d+$/.test(v)) return `gid://shopify/Product/${v}`;
|
|
25
|
-
const m = v.match(/\/products\/([^/?#]+)/);
|
|
26
|
-
return m ? m[1] : v;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Put a product we already have (from a collection query) in the cache. */
|
|
30
|
-
export function primeProduct(product) {
|
|
31
|
-
if (!product) return;
|
|
32
|
-
const p = Promise.resolve(product);
|
|
33
|
-
productCache.set(product.handle, p);
|
|
34
|
-
productCache.set(product.id, p);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getProduct(rawKey) {
|
|
38
|
-
const key = normalizeProductKey(rawKey);
|
|
39
|
-
if (!key) return Promise.reject(new TetherError('empty product key', { code: 'NOT_FOUND' }));
|
|
40
|
-
if (productCache.has(key)) return productCache.get(key);
|
|
41
|
-
|
|
42
|
-
const p = new Promise((resolve, reject) => {
|
|
43
|
-
resolvers.set(key, { resolve, reject });
|
|
44
|
-
pending.push(key);
|
|
45
|
-
if (!scheduled) {
|
|
46
|
-
scheduled = true;
|
|
47
|
-
setTimeout(flush, 0);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
productCache.set(key, p);
|
|
51
|
-
return p;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function flush() {
|
|
55
|
-
scheduled = false;
|
|
56
|
-
const gen = generation;
|
|
57
|
-
const keys = pending.splice(0);
|
|
58
|
-
if (!keys.length) return;
|
|
59
|
-
|
|
60
|
-
const ids = keys.filter(isGid);
|
|
61
|
-
const handles = keys.filter((k) => !isGid(k));
|
|
62
|
-
const defs = [];
|
|
63
|
-
const parts = [];
|
|
64
|
-
const vars = {};
|
|
65
|
-
|
|
66
|
-
if (ids.length) {
|
|
67
|
-
defs.push('$ids: [ID!]!');
|
|
68
|
-
vars.ids = ids;
|
|
69
|
-
parts.push('byId: nodes(ids: $ids) { ... on Product { ...TetherProduct } }');
|
|
70
|
-
}
|
|
71
|
-
handles.forEach((h, i) => {
|
|
72
|
-
defs.push(`$h${i}: String!`);
|
|
73
|
-
vars[`h${i}`] = h;
|
|
74
|
-
parts.push(`h${i}: product(handle: $h${i}) { ...TetherProduct }`);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const query = `query TetherProducts(${defs.join(',')}) { ${parts.join(' ')} } ${productFragment()}`;
|
|
78
|
-
log('fetch products', keys);
|
|
79
|
-
|
|
80
|
-
const settle = (k, fn) => {
|
|
81
|
-
const r = resolvers.get(k);
|
|
82
|
-
resolvers.delete(k);
|
|
83
|
-
if (r) fn(r);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const data = await gql(query, vars);
|
|
88
|
-
const got = new Map();
|
|
89
|
-
(data.byId || []).forEach((p) => { if (p) got.set(p.id, p); });
|
|
90
|
-
handles.forEach((h, i) => { if (data[`h${i}`]) got.set(h, data[`h${i}`]); });
|
|
91
|
-
|
|
92
|
-
keys.forEach((k) => settle(k, (r) => {
|
|
93
|
-
if (got.has(k)) {
|
|
94
|
-
const p = got.get(k);
|
|
95
|
-
if (gen === generation) { primeProduct(p); freshAt.set(p.handle, Date.now()); freshAt.set(p.id, Date.now()); }
|
|
96
|
-
r.resolve(p);
|
|
97
|
-
} else {
|
|
98
|
-
// A miss is remembered for a few seconds (one page load asking twice makes one request),
|
|
99
|
-
// not longer: a product published in Shopify shows up on the next visit.
|
|
100
|
-
const miss = productCache.get(k);
|
|
101
|
-
if (gen === generation) setTimeout(() => { if (productCache.get(k) === miss) productCache.delete(k); }, 10000);
|
|
102
|
-
r.reject(new TetherError(`Product not found: ${k}`, { code: 'NOT_FOUND' }));
|
|
103
|
-
}
|
|
104
|
-
}));
|
|
105
|
-
} catch (err) {
|
|
106
|
-
keys.forEach((k) => settle(k, (r) => { if (gen === generation) productCache.delete(k); r.reject(err); }));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function clearProductCache() { productCache.clear(); generation++; stale.clear(); freshAt.clear(); }
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Hydration: products rendered on the server (tether/server hydrate()) are used at once and
|
|
114
|
-
// refreshed once in the background, so prices and stock are live without a loading state.
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
const stale = new Set(); // keys of products that came from the server and weren't refreshed yet
|
|
118
|
-
const freshAt = new Map(); // key -> when it was last loaded live (this page session)
|
|
119
|
-
const FRESH_MS = 60000;
|
|
120
|
-
|
|
121
|
-
export function hydrateProducts(products) {
|
|
122
|
-
(products || []).forEach((p) => {
|
|
123
|
-
if (!p?.id || !p.handle) return;
|
|
124
|
-
// Loaded live in the last minute (an earlier page of a View Transition visit): that copy is newer.
|
|
125
|
-
if (Date.now() - (freshAt.get(p.handle) || 0) < FRESH_MS && productCache.has(p.handle)) return;
|
|
126
|
-
primeProduct(p);
|
|
127
|
-
stale.add(p.handle);
|
|
128
|
-
stale.add(p.id);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** True once per server-provided product: the caller refreshes it. */
|
|
133
|
-
export function takeStale(key) {
|
|
134
|
-
const k = normalizeProductKey(key);
|
|
135
|
-
if (!stale.has(k)) return false;
|
|
136
|
-
const p = productCache.get(k);
|
|
137
|
-
stale.delete(k);
|
|
138
|
-
// Both keys point to the same product: refresh it once.
|
|
139
|
-
p?.then?.((x) => { stale.delete(x.handle); stale.delete(x.id); });
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Fetch these products again (bypassing the cache). */
|
|
144
|
-
export function refreshProducts(keys) {
|
|
145
|
-
keys.forEach((k) => productCache.delete(normalizeProductKey(k)));
|
|
146
|
-
return keys.map((k) => getProduct(k));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* A page of products from a collection (by handle) or from a search query.
|
|
151
|
-
* Returns { collection, products, pageInfo }.
|
|
152
|
-
*/
|
|
153
|
-
export async function getProducts({
|
|
154
|
-
collection, query, first = cfg.collectionFirst, after = null, sortKey, reverse = false, filters,
|
|
155
|
-
} = {}) {
|
|
156
|
-
const fragment = productFragment();
|
|
157
|
-
|
|
158
|
-
if (collection) {
|
|
159
|
-
const data = await gql(`query TetherCollection($handle: String!, $first: Int!, $after: String, $sortKey: ProductCollectionSortKeys, $reverse: Boolean, $filters: [ProductFilter!]) {
|
|
160
|
-
collection(handle: $handle) {
|
|
161
|
-
${collectionFields()}
|
|
162
|
-
products(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, filters: $filters) {
|
|
163
|
-
nodes { ...TetherProduct }
|
|
164
|
-
pageInfo { hasNextPage endCursor }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
${fragment}`, {
|
|
169
|
-
handle: collection, first, after, sortKey: sortKey || undefined, reverse, filters: filters || undefined,
|
|
170
|
-
});
|
|
171
|
-
if (!data.collection) throw new TetherError(`Collection not found: ${collection}`, { code: 'NOT_FOUND' });
|
|
172
|
-
const { products, ...rest } = data.collection;
|
|
173
|
-
products.nodes.forEach(primeProduct);
|
|
174
|
-
return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const data = await gql(`query TetherSearch($first: Int!, $after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
|
|
178
|
-
products(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
|
|
179
|
-
nodes { ...TetherProduct }
|
|
180
|
-
pageInfo { hasNextPage endCursor }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
${fragment}`, { first, after, query: query || undefined, sortKey: sortKey || undefined, reverse });
|
|
184
|
-
data.products.nodes.forEach(primeProduct);
|
|
185
|
-
return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function getRecommendations(product, intent = 'RELATED') {
|
|
189
|
-
const key = normalizeProductKey(typeof product === 'object' ? product?.id : product);
|
|
190
|
-
const byId = key?.startsWith('gid://');
|
|
191
|
-
const data = await gql(`query TetherRecs(${byId ? '$id: ID!' : '$handle: String!'}, $intent: ProductRecommendationIntent) {
|
|
192
|
-
productRecommendations(${byId ? 'productId: $id' : 'productHandle: $handle'}, intent: $intent) { ...TetherProduct }
|
|
193
|
-
}
|
|
194
|
-
${productFragment()}`, byId ? { id: key, intent } : { handle: key, intent });
|
|
195
|
-
const list = data.productRecommendations || [];
|
|
196
|
-
list.forEach(primeProduct);
|
|
197
|
-
return list;
|
|
198
|
-
}
|
package/src/markets-ui.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional part, loaded on first use: market detection, select[tether-country-select],
|
|
3
|
-
* tether-country-bind and Tether.setCountry(). See markets.js for the detection rules.
|
|
4
|
-
*/
|
|
5
|
-
import { cfg, reportError } from './config.js';
|
|
6
|
-
import { TetherError } from './client.js';
|
|
7
|
-
import { Cart } from './cart.js';
|
|
8
|
-
import { listSelector } from './collection.js';
|
|
9
|
-
import { clearProductCache } from './loader.js';
|
|
10
|
-
import { attr, sel, get, emit } from './dom.js';
|
|
11
|
-
import { within, pruneLive, liveLists, liveProducts, mountedProducts, mountProduct } from './mounting.js';
|
|
12
|
-
import { rememberMarket, getLocalization, clearLocalization, toCountry, toLanguage } from './markets.js';
|
|
13
|
-
|
|
14
|
-
export { detectMarket, getLocalization } from './markets.js';
|
|
15
|
-
|
|
16
|
-
export /**
|
|
17
|
-
* select[tether-country-select]: countries the store sells to (Shopify Markets).
|
|
18
|
-
* tether-country-label="{name} ({currency})" sets the option text ({name}, {code}, {currency}, {symbol}).
|
|
19
|
-
* tether-country-bind="name|code|currency|symbol" shows the current country anywhere.
|
|
20
|
-
*/
|
|
21
|
-
function renderCountrySelects(root = document) {
|
|
22
|
-
const selects = within(root, `select${sel('country-select')}`);
|
|
23
|
-
const binds = within(root, sel('country-bind'));
|
|
24
|
-
if (!selects.length && !binds.length) return Promise.resolve();
|
|
25
|
-
return getLocalization().then((loc) => {
|
|
26
|
-
const current = (cfg.country || loc.country?.isoCode || '').toUpperCase();
|
|
27
|
-
const countries = [...(loc.availableCountries || [])].sort((a, b) => a.name.localeCompare(b.name, cfg.locale || undefined));
|
|
28
|
-
const info = countries.find((c) => c.isoCode === current) || loc.country;
|
|
29
|
-
const fill = (tpl, c) => tpl.replace(/\{name\}/g, c.name).replace(/\{code\}/g, c.isoCode)
|
|
30
|
-
.replace(/\{currency\}/g, c.currency?.isoCode || '').replace(/\{symbol\}/g, c.currency?.symbol || '');
|
|
31
|
-
selects.forEach((s) => {
|
|
32
|
-
const tpl = s.getAttribute(attr('country-label')) || '{name} ({currency})';
|
|
33
|
-
// tether-countries="NL,DE,BE": only these (in your order); otherwise every country of the store.
|
|
34
|
-
const only = (s.getAttribute(attr('countries')) || '').toUpperCase().split(/[\s,]+/).filter(Boolean);
|
|
35
|
-
const list = only.length ? only.map((c) => countries.find((x) => x.isoCode === c)).filter(Boolean) : countries;
|
|
36
|
-
s.innerHTML = '';
|
|
37
|
-
list.forEach((c) => s.appendChild(new Option(fill(tpl, c), c.isoCode)));
|
|
38
|
-
s.value = current;
|
|
39
|
-
if (s.value !== current) s.selectedIndex = -1; // a country the store doesn't sell to
|
|
40
|
-
s.disabled = list.length < 2 && !s.hasAttribute(attr('always-enabled'));
|
|
41
|
-
});
|
|
42
|
-
binds.forEach((el) => {
|
|
43
|
-
const k = get(el, 'country-bind') || 'name';
|
|
44
|
-
const v = { name: info?.name, code: info?.isoCode, currency: info?.currency?.isoCode, symbol: info?.currency?.symbol }[k];
|
|
45
|
-
el.textContent = v || '';
|
|
46
|
-
});
|
|
47
|
-
}).catch((err) => reportError(err, 'country select'));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Switches the market: prices, currency and translations follow (@inContext), the cart's
|
|
52
|
-
* buyerIdentity.countryCode is updated, and every product and list on the page re-renders.
|
|
53
|
-
* Tether.setCountry('DE') Tether.setCountry('BE', { language: 'FR' })
|
|
54
|
-
*/
|
|
55
|
-
export async function setCountry(code, { language, locale, remember = true } = {}) {
|
|
56
|
-
const country = toCountry(code);
|
|
57
|
-
if (!country) throw new TetherError(`Not a country code: ${code}`, { code: 'INVALID_COUNTRY' });
|
|
58
|
-
// Money format: the option, else a markets.locales entry for this country, else unchanged.
|
|
59
|
-
const entry = Object.values(cfg.markets?.locales || {}).find((m) => toCountry(m?.country) === country);
|
|
60
|
-
const own = locale || entry?.locale;
|
|
61
|
-
const loc = own || cfg.locale;
|
|
62
|
-
// Language: the option, else the markets.locales entry for this country (which may have none), else unchanged.
|
|
63
|
-
const lang = language !== undefined ? toLanguage(language) : entry ? (entry.language ? toLanguage(entry.language) : undefined) : cfg.language;
|
|
64
|
-
const changed = country !== String(cfg.country || '').toUpperCase() || lang !== cfg.language || loc !== cfg.locale;
|
|
65
|
-
cfg.country = country;
|
|
66
|
-
cfg.language = lang || undefined;
|
|
67
|
-
cfg.locale = loc;
|
|
68
|
-
if (remember) rememberMarket({ country, language: lang, locale: own });
|
|
69
|
-
if (!changed) { renderCountrySelects(); return Cart.state; }
|
|
70
|
-
|
|
71
|
-
clearProductCache();
|
|
72
|
-
const jobs = [];
|
|
73
|
-
if (language !== undefined) clearLocalization(); // country names follow the language
|
|
74
|
-
// Only an existing cart moves; switching country never creates one.
|
|
75
|
-
if (Cart.id()) jobs.push(Cart.setBuyerIdentity({ countryCode: country }, { create: false }).catch((err) => reportError(err, 'cart country')));
|
|
76
|
-
|
|
77
|
-
pruneLive();
|
|
78
|
-
liveLists.forEach((list) => jobs.push(list.load()));
|
|
79
|
-
liveProducts.forEach((el) => {
|
|
80
|
-
if (el.closest(listSelector())) return; // re-rendered by its list
|
|
81
|
-
mountedProducts.delete(el);
|
|
82
|
-
mountProduct(el);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
renderCountrySelects();
|
|
86
|
-
emit('country-changed', { country, language: cfg.language });
|
|
87
|
-
await Promise.all(jobs);
|
|
88
|
-
return Cart.state;
|
|
89
|
-
}
|
|
90
|
-
|
package/src/markets.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markets: which country/language prices, currency and translations follow.
|
|
3
|
-
*
|
|
4
|
-
* markets: {
|
|
5
|
-
* detect: true, // or a list: ['saved', 'server', 'webflow']
|
|
6
|
-
* locales: { // Webflow Localization: locale subdirectory or <html lang>
|
|
7
|
-
* de: { country: 'DE', language: 'DE', locale: 'de-DE' },
|
|
8
|
-
* 'fr-be': { country: 'BE', language: 'FR' },
|
|
9
|
-
* },
|
|
10
|
-
* cookie: 'tether_country', // a cookie your server/Worker sets (e.g. from cf-ipcountry)
|
|
11
|
-
* remember: true, // keep the visitor's tether-country-select choice
|
|
12
|
-
* }
|
|
13
|
-
*
|
|
14
|
-
* Sources, first match wins per field:
|
|
15
|
-
* saved the visitor's own choice (localStorage), from tether-country-select / Tether.setCountry()
|
|
16
|
-
* server window.TETHER_COUNTRY, <meta name="tether-country" content="DE">, or the cookie
|
|
17
|
-
* webflow locale subdirectory (/de/...) or <html lang="de-DE">, through `locales`
|
|
18
|
-
* then the static `country` / `language` config. Without `detect`, only `saved` applies
|
|
19
|
-
* (so a country select keeps working across pages); `detect: false` turns even that off. Tether never calls an IP lookup service:
|
|
20
|
-
* a country from the visitor's IP only comes from your own server (see README).
|
|
21
|
-
*/
|
|
22
|
-
import { cfg } from './config.js';
|
|
23
|
-
import { gql } from './client.js';
|
|
24
|
-
|
|
25
|
-
const COUNTRY = /^[A-Z]{2}$/;
|
|
26
|
-
// Shopify LanguageCode values with a region; others are the plain ISO 639-1 code.
|
|
27
|
-
const REGIONAL_LANGUAGES = new Set(['PT_BR', 'PT_PT', 'ZH_CN', 'ZH_TW']);
|
|
28
|
-
|
|
29
|
-
export const storageKey = () => `${cfg.prefix}:market:${cfg.domain}`;
|
|
30
|
-
|
|
31
|
-
const defaultEnv = () => ({
|
|
32
|
-
document: typeof document !== 'undefined' ? document : null,
|
|
33
|
-
location: typeof window !== 'undefined' ? window.location : null,
|
|
34
|
-
storage: (() => { try { return window.localStorage; } catch { return null; } })(),
|
|
35
|
-
global: typeof window !== 'undefined' ? window : {},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export function toCountry(v) {
|
|
39
|
-
const c = String(v || '').trim().toUpperCase();
|
|
40
|
-
return COUNTRY.test(c) && c !== 'XX' && c !== 'T1' ? c : null; // XX/T1: Cloudflare's unknown/Tor
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 'de-DE' -> 'DE', 'pt-BR' -> 'PT_BR', 'en' -> 'EN'. */
|
|
44
|
-
export function toLanguage(v) {
|
|
45
|
-
const raw = String(v || '').trim().replace('-', '_').toUpperCase();
|
|
46
|
-
if (!raw) return null;
|
|
47
|
-
if (REGIONAL_LANGUAGES.has(raw)) return raw;
|
|
48
|
-
const primary = raw.split('_')[0];
|
|
49
|
-
return /^[A-Z]{2,3}$/.test(primary) ? primary : null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function readCookie(doc, name) {
|
|
53
|
-
if (!doc || !name) return null;
|
|
54
|
-
const hit = String(doc.cookie || '').split(';').map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
|
|
55
|
-
return hit ? decodeURIComponent(hit.slice(name.length + 1)) : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const lookup = (locales, key) => {
|
|
59
|
-
if (!locales || !key) return null;
|
|
60
|
-
const k = String(key).toLowerCase();
|
|
61
|
-
const found = Object.keys(locales).find((x) => x.toLowerCase() === k);
|
|
62
|
-
return found ? locales[found] : null;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const readers = {
|
|
66
|
-
saved(m, env) {
|
|
67
|
-
if (m.remember === false || !env.storage) return null;
|
|
68
|
-
try { return JSON.parse(env.storage.getItem(storageKey()) || 'null'); } catch { return null; }
|
|
69
|
-
},
|
|
70
|
-
server(m, env) {
|
|
71
|
-
const meta = (name) => env.document?.querySelector?.(`meta[name="${name}"]`)?.getAttribute('content');
|
|
72
|
-
const country = env.global.TETHER_COUNTRY || meta('tether-country') || readCookie(env.document, m.cookie || 'tether_country');
|
|
73
|
-
const language = env.global.TETHER_LANGUAGE || meta('tether-language');
|
|
74
|
-
return { country, language };
|
|
75
|
-
},
|
|
76
|
-
webflow(m, env) {
|
|
77
|
-
const segment = (env.location?.pathname || '/').split('/').filter(Boolean)[0];
|
|
78
|
-
const fromPath = lookup(m.locales, segment);
|
|
79
|
-
if (fromPath) return fromPath;
|
|
80
|
-
const lang = env.document?.documentElement?.getAttribute('lang');
|
|
81
|
-
if (!lang) return null;
|
|
82
|
-
const mapped = lookup(m.locales, lang) || lookup(m.locales, lang.split('-')[0]);
|
|
83
|
-
if (mapped) return { locale: lang, ...mapped };
|
|
84
|
-
if (m.locales && m.strict) return null;
|
|
85
|
-
// Unmapped: language from <html lang>, country only from a region subtag (de-CH -> CH).
|
|
86
|
-
const region = lang.split('-')[1];
|
|
87
|
-
return { language: lang, country: region && region.length === 2 ? region : null, locale: lang };
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Returns { country, language, locale, sources } with only the fields found.
|
|
93
|
-
* Pure apart from reading the DOM/storage: easy to test.
|
|
94
|
-
*/
|
|
95
|
-
export function detectMarket(markets = cfg.markets, env = defaultEnv()) {
|
|
96
|
-
const m = markets || {};
|
|
97
|
-
if (m.detect === false) return {};
|
|
98
|
-
// Without detect, only the visitor's own choice (from a country select) is applied.
|
|
99
|
-
const list = !m.detect ? ['saved'] : m.detect === true ? ['saved', 'server', 'webflow'] : [].concat(m.detect);
|
|
100
|
-
markets = m;
|
|
101
|
-
const out = { sources: {} };
|
|
102
|
-
list.forEach((name) => {
|
|
103
|
-
const r = readers[name]?.(markets, env);
|
|
104
|
-
if (!r) return;
|
|
105
|
-
const vals = { country: toCountry(r.country), language: toLanguage(r.language), locale: r.locale || null };
|
|
106
|
-
Object.entries(vals).forEach(([k, v]) => {
|
|
107
|
-
if (v && !out[k]) { out[k] = v; out.sources[k] = name; }
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function rememberMarket(market) {
|
|
114
|
-
if (cfg.markets?.remember === false) return;
|
|
115
|
-
try { localStorage.setItem(storageKey(), JSON.stringify({ country: market.country, language: market.language || undefined, locale: market.locale || undefined })); } catch { /* private mode */ }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let localizationPromise = null;
|
|
119
|
-
export function clearLocalization() { localizationPromise = null; }
|
|
120
|
-
|
|
121
|
-
/** Countries the store sells to (Shopify Markets), with currency. Cached per page. */
|
|
122
|
-
export function getLocalization() {
|
|
123
|
-
if (!localizationPromise) {
|
|
124
|
-
localizationPromise = gql(`query TetherLocalization {
|
|
125
|
-
localization {
|
|
126
|
-
country { isoCode name currency { isoCode symbol } }
|
|
127
|
-
language { isoCode name }
|
|
128
|
-
availableCountries { isoCode name currency { isoCode symbol } availableLanguages { isoCode name } }
|
|
129
|
-
}
|
|
130
|
-
}`).then((d) => d.localization).catch((err) => { localizationPromise = null; throw err; });
|
|
131
|
-
}
|
|
132
|
-
return localizationPromise;
|
|
133
|
-
}
|
package/src/mounting.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/** Product scopes and lists that are mounted, shared by index.js and the optional markets part. */
|
|
2
|
-
import { cfg, log, warn, reportError } from './config.js';
|
|
3
|
-
import { ProductScope, scopes, rememberOptionNames } from './product.js';
|
|
4
|
-
import { listSelector } from './collection.js';
|
|
5
|
-
import { getProduct, takeStale, refreshProducts, hydrateProducts } from './loader.js';
|
|
6
|
-
import { resolveProductKey, setScopeStatus, isNotFound } from './status.js';
|
|
7
|
-
import { attr, get, qsa, emit } from './dom.js';
|
|
8
|
-
|
|
9
|
-
export const mountedProducts = new WeakSet();
|
|
10
|
-
export const mountedLists = new WeakSet();
|
|
11
|
-
// Live roots, for re-rendering after a country switch (pruned when disconnected).
|
|
12
|
-
export const liveProducts = new Set();
|
|
13
|
-
export const liveLists = new Set();
|
|
14
|
-
const requestToken = new WeakMap(); // product root -> latest request, so a slow old response can't win
|
|
15
|
-
|
|
16
|
-
export function pruneLive() {
|
|
17
|
-
liveProducts.forEach((el) => { if (!el.isConnected) liveProducts.delete(el); });
|
|
18
|
-
liveLists.forEach((l) => { if (!l.root.isConnected) liveLists.delete(l); });
|
|
19
|
-
}
|
|
20
|
-
export function within(root, selector) {
|
|
21
|
-
return [...(root.matches?.(selector) ? [root] : []), ...qsa(root, selector)];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export /** Mounts one product root. Returns true when a product request was started. */
|
|
25
|
-
function mountProduct(el) {
|
|
26
|
-
if (mountedProducts.has(el)) return false;
|
|
27
|
-
const { key, fromUrl, raw } = resolveProductKey(el);
|
|
28
|
-
if (!key) {
|
|
29
|
-
if (fromUrl) {
|
|
30
|
-
// Dynamic product page opened without ?handle= (or an empty path).
|
|
31
|
-
mountedProducts.add(el);
|
|
32
|
-
setScopeStatus(el, 'not-found');
|
|
33
|
-
emit('product-not-found', { key: null, source: raw }, el);
|
|
34
|
-
} else if (!el.closest(listSelector())) {
|
|
35
|
-
// List cards get their handle when the list renders.
|
|
36
|
-
warn(`[${attr('product')}] without a value. Bind it to the Shopify handle field, or use "url:handle".`, el);
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
mountedProducts.add(el);
|
|
41
|
-
liveProducts.add(el);
|
|
42
|
-
const token = {};
|
|
43
|
-
requestToken.set(el, token);
|
|
44
|
-
const current = () => requestToken.get(el) === token;
|
|
45
|
-
// A rendered scope keeps showing its content while it refreshes (e.g. country switch).
|
|
46
|
-
if (!scopes.get(el)) setScopeStatus(el, 'loading');
|
|
47
|
-
else el.setAttribute(attr('state'), 'updating');
|
|
48
|
-
const loading = getProduct(key);
|
|
49
|
-
marketPending?.add(loading);
|
|
50
|
-
loading
|
|
51
|
-
.then((p) => {
|
|
52
|
-
if (!current()) return;
|
|
53
|
-
const existing = scopes.get(el);
|
|
54
|
-
if (existing) existing.update(p);
|
|
55
|
-
// eslint-disable-next-line no-new
|
|
56
|
-
else new ProductScope(el, p);
|
|
57
|
-
applyPageTitle(el, p);
|
|
58
|
-
if (cfg.revalidate !== false && takeStale(key)) queueRefresh(key);
|
|
59
|
-
})
|
|
60
|
-
.catch((err) => {
|
|
61
|
-
if (!current()) return;
|
|
62
|
-
const notFound = isNotFound(err);
|
|
63
|
-
setScopeStatus(el, notFound ? 'not-found' : 'error', err);
|
|
64
|
-
if (notFound) {
|
|
65
|
-
emit('product-not-found', { key, source: raw }, el);
|
|
66
|
-
// A typo in a URL isn't worth an alert; a CMS item pointing at a deleted product is.
|
|
67
|
-
if (fromUrl) log(`product not found: ${key}`); else reportError(err, `product ${key}`);
|
|
68
|
-
} else {
|
|
69
|
-
reportError(err, `product ${key}`);
|
|
70
|
-
emit('error', { error: err, context: 'product', key }, el);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** tether-page-title="{title} | My shop" on a product root sets document.title (dynamic product pages). */
|
|
77
|
-
function applyPageTitle(el, p) {
|
|
78
|
-
const tpl = get(el, 'page-title');
|
|
79
|
-
if (!tpl || typeof document === 'undefined') return;
|
|
80
|
-
document.title = tpl.replace(/\{title\}/g, p.title).replace(/\{vendor\}/g, p.vendor || '').replace(/\{type\}/g, p.productType || '');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/** Product scopes that failed on a network/API error: loaded again when the browser is back online. */
|
|
85
|
-
export function retryFailed() {
|
|
86
|
-
pruneLive();
|
|
87
|
-
liveProducts.forEach((el) => {
|
|
88
|
-
if (el.getAttribute(attr('state')) !== 'error' || el.getAttribute(attr('error')) === 'not-found') return;
|
|
89
|
-
mountedProducts.delete(el);
|
|
90
|
-
mountProduct(el);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Server-rendered products: one batched background request refreshes them all.
|
|
95
|
-
let refreshQueue = new Set();
|
|
96
|
-
function queueRefresh(key) {
|
|
97
|
-
if (!refreshQueue.size) setTimeout(flushRefresh, 0);
|
|
98
|
-
refreshQueue.add(key);
|
|
99
|
-
}
|
|
100
|
-
function flushRefresh() {
|
|
101
|
-
const keys = [...refreshQueue];
|
|
102
|
-
refreshQueue = new Set();
|
|
103
|
-
refreshProducts(keys).forEach((promise, i) => promise.then((fresh) => {
|
|
104
|
-
liveProducts.forEach((el) => {
|
|
105
|
-
const scope = scopes.get(el);
|
|
106
|
-
if (!scope || !el.isConnected) return;
|
|
107
|
-
const k = resolveProductKey(el).key;
|
|
108
|
-
if (k !== keys[i] && k !== fresh.handle && k !== fresh.id) return;
|
|
109
|
-
if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
|
|
110
|
-
});
|
|
111
|
-
}).catch((err) => reportError(err, `refresh ${keys[i]}`)));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* <script type="application/json" data-tether-hydrate> tags written by tether/server's hydrate():
|
|
116
|
-
* their products are used without a request. Ignored when they were made for another market.
|
|
117
|
-
*/
|
|
118
|
-
const readScripts = new WeakSet();
|
|
119
|
-
let marketPending = null; // product loads to wait for before html.tether-market-pending goes
|
|
120
|
-
|
|
121
|
-
/** Lists (and anything else that loads prices) wait too. */
|
|
122
|
-
export const trackMarket = (p) => { marketPending?.add(p); return p; };
|
|
123
|
-
|
|
124
|
-
/** After a mount: drop html.tether-market-pending once this page's products are live. */
|
|
125
|
-
export function settleMarket() {
|
|
126
|
-
if (!marketPending) return;
|
|
127
|
-
const waits = [...marketPending];
|
|
128
|
-
Promise.allSettled(waits).then(() => {
|
|
129
|
-
if (!marketPending || waits.some((w) => !marketPending.has(w))) return;
|
|
130
|
-
if (waits.length === marketPending.size) {
|
|
131
|
-
marketPending = null;
|
|
132
|
-
document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
|
|
133
|
-
} else settleMarket(); // more products came in meanwhile
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function hydrateFromPage(root = document) {
|
|
138
|
-
qsa(root.ownerDocument || root, 'script[type="application/json"][data-tether-hydrate]').forEach((s) => {
|
|
139
|
-
if (readScripts.has(s)) return;
|
|
140
|
-
readScripts.add(s);
|
|
141
|
-
try {
|
|
142
|
-
const data = JSON.parse(s.textContent);
|
|
143
|
-
rememberOptionNames(data.products); // the build's option names, also for other languages
|
|
144
|
-
const same = (a, b) => String(a || '').toUpperCase() === String(b || '').toUpperCase();
|
|
145
|
-
if (same(data.country, cfg.country) && same(data.language, cfg.language)) hydrateProducts(data.products);
|
|
146
|
-
else {
|
|
147
|
-
// The page was built for another market: its prices aren't this visitor's. Mark the page
|
|
148
|
-
// (html.tether-market-pending) until the live ones are in, so CSS can hide prices meanwhile.
|
|
149
|
-
log('hydration skipped: other market');
|
|
150
|
-
if (!marketPending) { marketPending = new Set(); document.documentElement.classList.add(`${cfg.prefix}-market-pending`); }
|
|
151
|
-
}
|
|
152
|
-
} catch (err) { warn('bad data-tether-hydrate', err); }
|
|
153
|
-
});
|
|
154
|
-
}
|
package/src/parts/account.js
DELETED
package/src/parts/analytics.js
DELETED
package/src/parts/diagnose.js
DELETED
package/src/parts/extras.js
DELETED
package/src/parts/markets.js
DELETED
package/src/parts/search.js
DELETED