@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,64 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Tether.diagnose()`: one call in the browser console that explains what's wrong on a page. Logs a readable report
|
|
3
|
-
* and returns it as an object. The markup checks are in diagnose-markup.
|
|
3
|
+
* and returns it as an object. The markup checks are in diagnose-markup.ts.
|
|
4
4
|
*
|
|
5
5
|
* await Tether.diagnose()
|
|
6
6
|
* await Tether.diagnose({ silent: true }) // just the object
|
|
7
7
|
*/
|
|
8
|
-
import { cfg, isConfigured } from './config.
|
|
9
|
-
import { gql, endpoint } from './client.
|
|
10
|
-
import { sel, get, qsa } from './dom.
|
|
11
|
-
import { getProduct } from './loader.
|
|
12
|
-
import { allPlans } from './selling-plans.
|
|
13
|
-
import { resolveProductKey } from './status.
|
|
14
|
-
import { apiVersionStatus } from './api-version.
|
|
15
|
-
import { loadPart } from './lazy.
|
|
16
|
-
import { checkMarkup, didYouMean } from './diagnose-markup.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
import { cfg, isConfigured } from './config.ts';
|
|
9
|
+
import { gql, endpoint, shaped } from './client.ts';
|
|
10
|
+
import { sel, get, qsa } from './dom.ts';
|
|
11
|
+
import { getProduct } from './loader.ts';
|
|
12
|
+
import { allPlans } from './selling-plans.ts';
|
|
13
|
+
import { resolveProductKey } from './status.ts';
|
|
14
|
+
import { apiVersionStatus } from './api-version.ts';
|
|
15
|
+
import { loadPart } from './lazy.ts';
|
|
16
|
+
import { checkMarkup, didYouMean } from './diagnose-markup.ts';
|
|
17
|
+
import type { ApiVersionStatus } from './api-version.ts';
|
|
18
|
+
import type { DetectedMarket } from './markets.ts';
|
|
19
|
+
import type { Problem } from './diagnose-markup.ts';
|
|
20
|
+
import type { Product } from './fragments.ts';
|
|
21
21
|
|
|
22
22
|
export { apiVersionStatus, checkMarkup, didYouMean };
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
24
|
+
/** The market in use (a visitor's choice included), and what detection would pick. */
|
|
25
|
+
interface MarketReport {
|
|
26
|
+
country: string | null;
|
|
27
|
+
language: string | null;
|
|
28
|
+
locale: string | null;
|
|
29
|
+
/** `null` without `markets.detect`. */
|
|
30
|
+
detected: DetectedMarket | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ShopReport {
|
|
34
|
+
name: string;
|
|
35
|
+
url: string;
|
|
36
|
+
currency: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ProductReport =
|
|
40
|
+
| { key: string; handle: string; title: string; plans: number; requiresSellingPlan: boolean }
|
|
41
|
+
| { key: string; error: string | undefined };
|
|
42
|
+
|
|
43
|
+
interface SellingPlanReport {
|
|
44
|
+
product: string;
|
|
45
|
+
group: string;
|
|
46
|
+
plan: string;
|
|
47
|
+
id: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DiagnoseReport {
|
|
51
|
+
/** Tether's version, `''` when the caller didn't say. */
|
|
52
|
+
version: string;
|
|
53
|
+
/** The config, with the token masked. */
|
|
54
|
+
config: Record<string, unknown>;
|
|
55
|
+
apiVersion: ApiVersionStatus;
|
|
56
|
+
market: MarketReport;
|
|
57
|
+
reachable: boolean;
|
|
58
|
+
shop: ShopReport | null;
|
|
59
|
+
latencyMs: number | null;
|
|
60
|
+
products: ProductReport[];
|
|
61
|
+
sellingPlans: SellingPlanReport[];
|
|
62
|
+
problems: Problem[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type StorefrontCheck =
|
|
66
|
+
| { kind: 'not-configured' }
|
|
67
|
+
| { kind: 'reachable'; shop: ShopReport; latencyMs: number }
|
|
68
|
+
| { kind: 'unreachable'; error: unknown; latencyMs: number };
|
|
69
|
+
|
|
70
|
+
export interface DiagnoseOptions {
|
|
71
|
+
root?: ParentNode;
|
|
72
|
+
/** Return the report without logging it. */
|
|
73
|
+
silent?: boolean;
|
|
74
|
+
/** Tether's version, for the report. */
|
|
75
|
+
version?: string;
|
|
76
|
+
/** The date the API version's support is measured against. */
|
|
77
|
+
now?: Date;
|
|
78
|
+
}
|
|
62
79
|
|
|
63
80
|
/** Products checked at most, so a long CMS page doesn't send a request per card. */
|
|
64
81
|
const MAX_PRODUCTS_CHECKED = 20;
|
|
@@ -66,26 +83,19 @@ const MAX_PRODUCTS_CHECKED = 20;
|
|
|
66
83
|
const API_UPGRADE_WARNING_MONTHS = 3;
|
|
67
84
|
const TOKEN_EDGE_CHARACTERS = 4;
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
function maskToken(token) {
|
|
86
|
+
function maskToken(token: string): string {
|
|
71
87
|
if (!token) return '(missing)';
|
|
72
88
|
return `${token.slice(0, TOKEN_EDGE_CHARACTERS)}…${token.slice(-TOKEN_EDGE_CHARACTERS)} (${token.length} chars)`;
|
|
73
89
|
}
|
|
74
90
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
* @param {string} fallback
|
|
78
|
-
* @returns {string} The error's message, else its code (a `TetherError` has one), else `fallback`.
|
|
79
|
-
*/
|
|
80
|
-
function errorText(error, fallback) {
|
|
91
|
+
/** The error's message, else its code (a `TetherError` has one), else `fallback`. */
|
|
92
|
+
function errorText(error: unknown, fallback: string): string {
|
|
81
93
|
if (error instanceof Error && error.message) return error.message;
|
|
82
|
-
|
|
83
|
-
const code = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
94
|
+
const code: unknown = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
84
95
|
return typeof code === 'string' && code ? code : fallback;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
function configSummary() {
|
|
98
|
+
function configSummary(): Record<string, unknown> {
|
|
89
99
|
return {
|
|
90
100
|
domain: cfg.domain || '(missing)',
|
|
91
101
|
token: maskToken(cfg.token),
|
|
@@ -106,10 +116,8 @@ function configSummary() {
|
|
|
106
116
|
|
|
107
117
|
/**
|
|
108
118
|
* Detection runs in the markets part, which init has already loaded whenever `markets.detect` is set.
|
|
109
|
-
*
|
|
110
|
-
* @returns {Promise<MarketReport>}
|
|
111
119
|
*/
|
|
112
|
-
async function marketSummary() {
|
|
120
|
+
async function marketSummary(): Promise<MarketReport> {
|
|
113
121
|
return {
|
|
114
122
|
country: cfg.country || null,
|
|
115
123
|
language: cfg.language || null,
|
|
@@ -118,24 +126,24 @@ async function marketSummary() {
|
|
|
118
126
|
};
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
* @returns {Problem | null} An error for an unsupported version, a warning when support ends soon.
|
|
124
|
-
*/
|
|
125
|
-
function apiVersionProblem({ ok, monthsLeft, note }) {
|
|
129
|
+
/** An error for an unsupported version, a warning when support ends soon. */
|
|
130
|
+
function apiVersionProblem({ ok, monthsLeft, note }: ApiVersionStatus): Problem | null {
|
|
126
131
|
if (ok && (monthsLeft === undefined || monthsLeft >= API_UPGRADE_WARNING_MONTHS)) return null;
|
|
127
132
|
return { level: ok ? 'warn' : 'error', message: `apiVersion ${cfg.apiVersion}: ${note}` };
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
const isShopAnswer = shaped<{
|
|
136
|
+
shop: { name: string; primaryDomain: { url: string }; paymentSettings: { currencyCode: string } };
|
|
137
|
+
}>({ shop: { name: 'string', primaryDomain: { url: 'string' }, paymentSettings: { currencyCode: 'string' } } });
|
|
138
|
+
|
|
139
|
+
async function checkStorefront(): Promise<StorefrontCheck> {
|
|
132
140
|
if (!isConfigured()) return { kind: 'not-configured' };
|
|
133
141
|
const startedAt = Date.now();
|
|
134
142
|
try {
|
|
135
|
-
/** @type {{ shop: { name: string, primaryDomain: { url: string }, paymentSettings: { currencyCode: string } } }} */
|
|
136
143
|
const data = await gql(
|
|
137
144
|
'query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }',
|
|
138
145
|
{},
|
|
146
|
+
isShopAnswer,
|
|
139
147
|
{ context: false },
|
|
140
148
|
);
|
|
141
149
|
const shop = {
|
|
@@ -151,15 +159,9 @@ async function checkStorefront() {
|
|
|
151
159
|
|
|
152
160
|
/**
|
|
153
161
|
* Options a visitor can't choose on this page get the first available variant's value.
|
|
154
|
-
*
|
|
155
|
-
* @param {ParentNode} root
|
|
156
|
-
* @param {string} key
|
|
157
|
-
* @param {Product} product
|
|
158
|
-
* @returns {Problem[]}
|
|
159
162
|
*/
|
|
160
|
-
function checkOptions(root, key, product) {
|
|
161
|
-
|
|
162
|
-
const problems = [];
|
|
163
|
+
function checkOptions(root: ParentNode, key: string, product: Product): Problem[] {
|
|
164
|
+
const problems: Problem[] = [];
|
|
163
165
|
const names = product.options.map((option) => option.name);
|
|
164
166
|
const roots = qsa(root, sel('product')).filter(
|
|
165
167
|
(el) => resolveProductKey(el).key === key && el.querySelector(sel('add-to-cart')),
|
|
@@ -194,11 +196,8 @@ function checkOptions(root, key, product) {
|
|
|
194
196
|
|
|
195
197
|
/**
|
|
196
198
|
* Loads the page's products and adds what it finds about each to `report`.
|
|
197
|
-
*
|
|
198
|
-
* @param {ParentNode} root
|
|
199
|
-
* @param {DiagnoseReport} report
|
|
200
199
|
*/
|
|
201
|
-
async function checkProducts(root, report) {
|
|
200
|
+
async function checkProducts(root: ParentNode, report: DiagnoseReport): Promise<void> {
|
|
202
201
|
const allKeys = qsa(root, sel('product')).map((el) => resolveProductKey(el).key);
|
|
203
202
|
const keys = [...new Set(allKeys.filter((key) => key !== null && key !== ''))].slice(0, MAX_PRODUCTS_CHECKED);
|
|
204
203
|
const results = await Promise.allSettled(keys.map((key) => getProduct(key)));
|
|
@@ -229,14 +228,15 @@ async function checkProducts(root, report) {
|
|
|
229
228
|
});
|
|
230
229
|
}
|
|
231
230
|
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
/** What's wrong on the page: config, token, API version, products, plans, markup. */
|
|
232
|
+
export async function diagnose({
|
|
233
|
+
root = document,
|
|
234
|
+
silent = false,
|
|
235
|
+
version = '',
|
|
236
|
+
now = new Date(),
|
|
237
|
+
}: DiagnoseOptions = {}): Promise<DiagnoseReport> {
|
|
237
238
|
const storefront = await checkStorefront();
|
|
238
|
-
|
|
239
|
-
const report = {
|
|
239
|
+
const report: DiagnoseReport = {
|
|
240
240
|
version,
|
|
241
241
|
config: configSummary(),
|
|
242
242
|
apiVersion: apiVersionStatus(cfg.apiVersion, now),
|
|
@@ -269,8 +269,7 @@ export async function diagnose({ root = document, silent = false, version, now =
|
|
|
269
269
|
return report;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
function print(report) {
|
|
272
|
+
function print(report: DiagnoseReport): void {
|
|
274
273
|
const errors = report.problems.filter((problem) => problem.level === 'error').length;
|
|
275
274
|
const warnings = report.problems.length - errors;
|
|
276
275
|
console.group(`[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : 'no errors'}, ${warnings} warning(s)`);
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser modules' DOM toolkit: what Shopify values look like on the page (money, dates, text, images, lists)
|
|
3
|
+
* and how elements show, hide and report layout changes. Attribute names go through `attr()` so the prefix is
|
|
4
|
+
* configurable. The optional parts import all of it from here (`window.Tether.__core['dom.ts']`).
|
|
5
|
+
*/
|
|
6
|
+
import { cfg, reportError, warn } from './config.ts';
|
|
7
|
+
import { attr, emit, get } from './markup.ts';
|
|
8
|
+
import { storeHtmlFragment } from './store-html.ts';
|
|
9
|
+
import { isRecord } from '../server/src/shared/json.ts';
|
|
10
|
+
import { formatMoney as formatMoneyIn, productUrl as productUrlFor } from './shopify-values.ts';
|
|
11
|
+
import type { Money } from './shopify-values.ts';
|
|
12
|
+
|
|
13
|
+
export { attr, sel, get, has, qsa, scoped, emit, ownEvent, isOwnEvent, ANALYTICS_EVENTS } from './markup.ts';
|
|
14
|
+
export { imageUrl, cdnUrl, sameImage, setImage } from './images.ts';
|
|
15
|
+
export { hasTemplate, repeat, repeatKeyed } from './repeaters.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* State Tether keeps on elements, as `data-tether-*` attributes whatever the prefix: sites style and test against
|
|
19
|
+
* these names, so they stay fixed where the `tether-*` markup attributes follow `prefix`.
|
|
20
|
+
*/
|
|
21
|
+
const SEEN = 'data-tether-seen';
|
|
22
|
+
const SHOWN = 'data-tether-shown';
|
|
23
|
+
const PREVIOUS_DISPLAY = 'data-tether-prev-display';
|
|
24
|
+
|
|
25
|
+
/** Async renders change the page height in bursts; one notification per burst. */
|
|
26
|
+
const LAYOUT_DEBOUNCE_MS = 120;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The product page of a product (`productPath`).
|
|
30
|
+
*
|
|
31
|
+
* @returns `null` without the handle or id the path needs.
|
|
32
|
+
*/
|
|
33
|
+
export function productUrl(handle: string | null | undefined, id?: string | null): string | null {
|
|
34
|
+
return productUrlFor(cfg.productPath || '/product', handle, id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `Intl` throws a RangeError for a locale or currency it doesn't know; the caller then shows a plain fallback. */
|
|
38
|
+
function formatOr<T>(format: () => T, fallback: T): T {
|
|
39
|
+
try {
|
|
40
|
+
return format();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof RangeError) return fallback;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `30` → `30%` (en, nl) or `30 %` (de, fr), in the configured locale.
|
|
49
|
+
*
|
|
50
|
+
* @returns `''` for no or a zero percentage.
|
|
51
|
+
*/
|
|
52
|
+
export function formatPercent(percentage: number | null | undefined, locale: string | undefined = cfg.locale): string {
|
|
53
|
+
if (!percentage) return '';
|
|
54
|
+
const format = () =>
|
|
55
|
+
new Intl.NumberFormat(locale || undefined, { style: 'percent', maximumFractionDigits: 0 }).format(percentage / 100);
|
|
56
|
+
return formatOr(format, `${percentage}%`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Money in the configured locale, through the site's `formatMoney` when it has one. */
|
|
60
|
+
export function formatMoney(money: Money | null | undefined): string {
|
|
61
|
+
if (!money) return '';
|
|
62
|
+
if (cfg.formatMoney) return cfg.formatMoney(money, cfg.locale);
|
|
63
|
+
return formatMoneyIn(money, cfg.locale);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @returns The date in the configured locale, `''` without one. */
|
|
67
|
+
export function formatDate(iso: string | null | undefined): string {
|
|
68
|
+
if (!iso) return '';
|
|
69
|
+
return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || undefined), '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Writes HTML from the Shopify admin (product and collection descriptions) into `el`, with script removed
|
|
74
|
+
* (`store-html.ts`). Everything else from Shopify is written as text (`setText`).
|
|
75
|
+
*/
|
|
76
|
+
export function setStoreHtml(el: Element, html: string | null | undefined): void {
|
|
77
|
+
el.replaceChildren(storeHtmlFragment(html, el.ownerDocument, cfg.embedOrigins));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isTextField = (el: Element): el is HTMLInputElement | HTMLTextAreaElement =>
|
|
81
|
+
el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
|
|
82
|
+
|
|
83
|
+
const isInput = (el: Element): el is HTMLInputElement => el.tagName === 'INPUT';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sets the text of `el`, or its value when it is a form field.
|
|
87
|
+
*
|
|
88
|
+
* @param value `null` and `undefined` clear it.
|
|
89
|
+
*/
|
|
90
|
+
export function setText(el: Element, value: string | number | boolean | null | undefined): void {
|
|
91
|
+
const text = value == null ? '' : String(value);
|
|
92
|
+
if (isTextField(el)) el.value = text;
|
|
93
|
+
else el.textContent = text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const readText = (el: Element) => (isTextField(el) ? el.value : el.textContent);
|
|
97
|
+
|
|
98
|
+
/** `setText` that hands a change after the first render to `onValueChange`, for scramble and roll effects. */
|
|
99
|
+
export function setTextTracked(el: HTMLElement, value: string | number | boolean | null | undefined): void {
|
|
100
|
+
const before = readText(el);
|
|
101
|
+
setText(el, value);
|
|
102
|
+
if (before !== readText(el) && el.hasAttribute(SEEN) && cfg.onValueChange) {
|
|
103
|
+
try {
|
|
104
|
+
cfg.onValueChange(el, before);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
reportError(error, 'onValueChange');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
el.setAttribute(SEEN, '1');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Shows an element hidden before the script ran (a class, an inline `display: none`, a `hidden` attribute), as
|
|
114
|
+
* `block` or the value of `tether-display`.
|
|
115
|
+
*/
|
|
116
|
+
function reveal(el: HTMLElement): void {
|
|
117
|
+
if (el.hasAttribute(SHOWN)) return;
|
|
118
|
+
el.setAttribute(SHOWN, '');
|
|
119
|
+
el.removeAttribute('hidden');
|
|
120
|
+
const previous = el.getAttribute(PREVIOUS_DISPLAY);
|
|
121
|
+
if (previous !== null) {
|
|
122
|
+
el.style.display = previous;
|
|
123
|
+
el.removeAttribute(PREVIOUS_DISPLAY);
|
|
124
|
+
} else if (el.style.display === 'none') {
|
|
125
|
+
el.style.display = '';
|
|
126
|
+
}
|
|
127
|
+
if (typeof getComputedStyle === 'function' && getComputedStyle(el).display === 'none') {
|
|
128
|
+
el.style.display = el.getAttribute(attr('display')) || 'block';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Shows or hides `el`, restoring its own display value when shown again. */
|
|
133
|
+
export function show(el: HTMLElement, visible: boolean): void {
|
|
134
|
+
if (visible) {
|
|
135
|
+
reveal(el);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
el.removeAttribute(SHOWN);
|
|
139
|
+
// The display value to restore is a real one, never 'none'.
|
|
140
|
+
if (!el.hasAttribute(PREVIOUS_DISPLAY) && el.style.display !== 'none') {
|
|
141
|
+
el.setAttribute(PREVIOUS_DISPLAY, el.style.display);
|
|
142
|
+
}
|
|
143
|
+
el.style.display = 'none';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Marks an option button or input as chosen: `is-selected`, `aria-pressed`, and its own or inner radio/checkbox. */
|
|
147
|
+
export function setSelected(el: Element, selected: boolean): void {
|
|
148
|
+
el.classList.toggle('is-selected', selected);
|
|
149
|
+
el.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|
150
|
+
if (isInput(el) && (el.type === 'radio' || el.type === 'checkbox')) el.checked = selected;
|
|
151
|
+
const input = el.querySelector('input[type="radio"],input[type="checkbox"]');
|
|
152
|
+
if (input && isInput(input)) input.checked = selected;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let layoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A global the page's own scripts set on `window`: `TETHER_CONFIG`, the site's config, `TETHER_INTEGRATION`, set by a
|
|
159
|
+
* framework integration (Astro) that starts Tether itself, and GSAP's `ScrollTrigger`. Another script wrote it, so it
|
|
160
|
+
* is read as unknown and checked where it is used.
|
|
161
|
+
*/
|
|
162
|
+
export const pageGlobal = (name: 'TETHER_CONFIG' | 'TETHER_INTEGRATION' | 'ScrollTrigger'): unknown =>
|
|
163
|
+
Reflect.get(window, name);
|
|
164
|
+
|
|
165
|
+
/** Refreshes GSAP's ScrollTrigger, when the site has it: it measures positions that a height change makes stale. */
|
|
166
|
+
function refreshScrollTrigger(): void {
|
|
167
|
+
// ScrollTrigger is a class, and `refresh` a static method of it.
|
|
168
|
+
const scrollTrigger = pageGlobal('ScrollTrigger');
|
|
169
|
+
if (typeof scrollTrigger !== 'function') return;
|
|
170
|
+
const refresh: unknown = Reflect.get(scrollTrigger, 'refresh');
|
|
171
|
+
if (typeof refresh === 'function') Reflect.apply(refresh, scrollTrigger, []);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Async renders change the page height after the site's page-transition hooks ran. After a burst of them this calls
|
|
176
|
+
* `onLayoutChange` (else refreshes ScrollTrigger) and emits `tether:layout-changed`, also when either of those throws.
|
|
177
|
+
*/
|
|
178
|
+
export function notifyLayoutChanged(reason: string): void {
|
|
179
|
+
clearTimeout(layoutTimer);
|
|
180
|
+
layoutTimer = setTimeout(() => {
|
|
181
|
+
try {
|
|
182
|
+
if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
|
|
183
|
+
else refreshScrollTrigger();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
reportError(error, 'layout-changed');
|
|
186
|
+
}
|
|
187
|
+
emit('layout-changed', { reason });
|
|
188
|
+
}, LAYOUT_DEBOUNCE_MS);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Notifies a layout change (`<reason>:images`) once every image in `root` that is still loading has loaded or failed. */
|
|
192
|
+
export function watchImages(root: ParentNode, reason: string): void {
|
|
193
|
+
const loading = [...root.querySelectorAll('img')].filter((img) => !img.complete);
|
|
194
|
+
let left = loading.length;
|
|
195
|
+
const done = () => {
|
|
196
|
+
left -= 1;
|
|
197
|
+
if (left === 0) notifyLayoutChanged(`${reason}:images`);
|
|
198
|
+
};
|
|
199
|
+
loading.forEach((img) => {
|
|
200
|
+
img.addEventListener('load', done, { once: true });
|
|
201
|
+
img.addEventListener('error', done, { once: true });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Runs `task` with `aria-busy` on `el`; does nothing while `el` is already busy.
|
|
207
|
+
*
|
|
208
|
+
* @returns The task's result, `undefined` when it didn't run.
|
|
209
|
+
*/
|
|
210
|
+
export function busy<T>(el: Element, task: () => T | Promise<T>): Promise<T | undefined> {
|
|
211
|
+
if (el.getAttribute('aria-busy') === 'true') return Promise.resolve(undefined);
|
|
212
|
+
el.setAttribute('aria-busy', 'true');
|
|
213
|
+
return Promise.resolve()
|
|
214
|
+
.then(task)
|
|
215
|
+
.finally(() => {
|
|
216
|
+
el.removeAttribute('aria-busy');
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @returns The attribute's JSON, `null` when it is missing or not JSON (with a warning). */
|
|
221
|
+
export function parseJSONAttr(el: Element | null | undefined, name: string): unknown {
|
|
222
|
+
const raw = el ? get(el, name) : null;
|
|
223
|
+
if (!raw) return null;
|
|
224
|
+
try {
|
|
225
|
+
const json: unknown = JSON.parse(raw);
|
|
226
|
+
return json;
|
|
227
|
+
} catch {
|
|
228
|
+
warn(`invalid JSON in ${attr(name)}`, el);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type AttributeValue = string | number | boolean | null | undefined;
|
|
234
|
+
|
|
235
|
+
/** @returns A JSON value as attribute text: `''` for none, an object or array as its JSON. */
|
|
236
|
+
function attributeText(value: unknown): string {
|
|
237
|
+
if (value === null || value === undefined) return '';
|
|
238
|
+
if (typeof value === 'string') return value;
|
|
239
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
240
|
+
// What else parsed JSON holds: objects and arrays.
|
|
241
|
+
return JSON.stringify(value);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `tether-line-attributes`, JSON the site writes: `{ a: 1 }` or `[{ key, value }]`. An array entry that isn't an
|
|
246
|
+
* object is skipped.
|
|
247
|
+
*/
|
|
248
|
+
export function attributesFromJson(json: unknown): { key: string; value: string }[] {
|
|
249
|
+
if (Array.isArray(json)) {
|
|
250
|
+
const entries: unknown[] = json;
|
|
251
|
+
return entries
|
|
252
|
+
.filter(isRecord)
|
|
253
|
+
.map((entry) => ({ key: attributeText(entry['key']), value: attributeText(entry['value']) }));
|
|
254
|
+
}
|
|
255
|
+
if (!isRecord(json)) return [];
|
|
256
|
+
return Object.entries(json).map(([key, value]) => ({ key, value: attributeText(value) }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** `{ a: 1 }` or `[{ key, value }]` → `[{ key, value }]` as Shopify wants cart and line attributes. */
|
|
260
|
+
export function toAttributes(
|
|
261
|
+
attributes: Record<string, AttributeValue> | { key: string | number; value?: AttributeValue }[] | null | undefined,
|
|
262
|
+
): { key: string; value: string }[] {
|
|
263
|
+
if (!attributes) return [];
|
|
264
|
+
if (Array.isArray(attributes)) {
|
|
265
|
+
return attributes.map((attribute) => ({ key: String(attribute.key), value: String(attribute.value ?? '') }));
|
|
266
|
+
}
|
|
267
|
+
return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? '') }));
|
|
268
|
+
}
|