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