@occluze/tether 0.6.8 → 0.7.1
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 +28 -28
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
- package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
- package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
- package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
- package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
- package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
- package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
- package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
- package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
- package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
- package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
- package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
- package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
- package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
- package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
- package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
- package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
- package/dist/esm/chunks/search-A5DS2IOS.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 +332 -367
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +253 -492
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +82 -89
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +52 -34
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +42 -37
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1361 -935
- package/dist/tether.markets.js +89 -91
- 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 +60 -69
- 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/cart-id.d.ts +2 -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 +85 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +185 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +134 -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 +190 -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 +90 -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 -24
- 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-lines.js +0 -96
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/page-events.js +0 -288
- 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/subscriptions-view.js +0 -158
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account-pages.js +0 -89
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics-shopify.js +0 -241
- package/src/analytics.js +0 -352
- package/src/api-version.js +0 -85
- package/src/astro.js +0 -266
- package/src/cart/checkout.js +0 -97
- package/src/cart/drawer.js +0 -138
- package/src/cart/mutations.js +0 -234
- package/src/cart/render.js +0 -294
- package/src/cart/store.js +0 -326
- package/src/cart.js +0 -391
- package/src/client.js +0 -267
- package/src/collection.js +0 -183
- package/src/config.js +0 -349
- package/src/controls/cart-controls.js +0 -155
- package/src/controls/discount.js +0 -96
- package/src/controls/extras.js +0 -63
- package/src/controls/navigation.js +0 -83
- package/src/diagnose-markup.js +0 -333
- package/src/diagnose.js +0 -295
- package/src/dom.js +0 -287
- package/src/extras.js +0 -145
- package/src/fragments.js +0 -237
- package/src/images.js +0 -104
- 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/loader.js +0 -337
- package/src/market-codes.js +0 -35
- package/src/markets-ui.js +0 -204
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/mounting.js +0 -277
- package/src/page-market.js +0 -116
- 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-events.js +0 -130
- package/src/product-render.js +0 -257
- package/src/product-state.js +0 -318
- package/src/product.js +0 -344
- package/src/repeaters.js +0 -173
- package/src/search.js +0 -323
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/shopify-values.js +0 -89
- package/src/status.js +0 -126
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/src/storefront/json-ld.js +0 -167
- package/src/variant-render.js +0 -165
- 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/diagnose.js
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
/**
|
|
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.js.
|
|
4
|
-
*
|
|
5
|
-
* await Tether.diagnose()
|
|
6
|
-
* await Tether.diagnose({ silent: true }) // just the object
|
|
7
|
-
*/
|
|
8
|
-
import { cfg, isConfigured } from './config.js';
|
|
9
|
-
import { gql, endpoint } from './client.js';
|
|
10
|
-
import { sel, get, qsa } from './dom.js';
|
|
11
|
-
import { getProduct } from './loader.js';
|
|
12
|
-
import { allPlans } from './selling-plans.js';
|
|
13
|
-
import { resolveProductKey } from './status.js';
|
|
14
|
-
import { apiVersionStatus } from './api-version.js';
|
|
15
|
-
import { loadPart } from './lazy.js';
|
|
16
|
-
import { checkMarkup, didYouMean } from './diagnose-markup.js';
|
|
17
|
-
/** @import { ApiVersionStatus } from './api-version.js' */
|
|
18
|
-
/** @import { DetectedMarket } from './markets.js' */
|
|
19
|
-
/** @import { Problem } from './diagnose-markup.js' */
|
|
20
|
-
/** @import { Product } from './fragments.js' */
|
|
21
|
-
|
|
22
|
-
export { apiVersionStatus, checkMarkup, didYouMean };
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {object} MarketReport The market in use (a visitor's choice included), and what detection would pick.
|
|
26
|
-
* @property {string | null} country
|
|
27
|
-
* @property {string | null} language
|
|
28
|
-
* @property {string | null} locale
|
|
29
|
-
* @property {DetectedMarket | null} detected `null` without `markets.detect`.
|
|
30
|
-
*/
|
|
31
|
-
/** @typedef {{ name: string, url: string, currency: string }} ShopReport */
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {{ key: string, handle: string, title: string, plans: number, requiresSellingPlan: boolean }
|
|
34
|
-
* | { key: string, error: string | undefined }} ProductReport
|
|
35
|
-
*/
|
|
36
|
-
/** @typedef {{ product: string, group: string, plan: string, id: string }} SellingPlanReport */
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {object} DiagnoseReport
|
|
39
|
-
* @property {string | undefined} version
|
|
40
|
-
* @property {Record<string, unknown>} config The config, with the token masked.
|
|
41
|
-
* @property {ApiVersionStatus} apiVersion
|
|
42
|
-
* @property {MarketReport} market
|
|
43
|
-
* @property {boolean} reachable
|
|
44
|
-
* @property {ShopReport | null} shop
|
|
45
|
-
* @property {number | null} latencyMs
|
|
46
|
-
* @property {ProductReport[]} products
|
|
47
|
-
* @property {SellingPlanReport[]} sellingPlans
|
|
48
|
-
* @property {Problem[]} problems
|
|
49
|
-
*/
|
|
50
|
-
/**
|
|
51
|
-
* @typedef {{ kind: 'not-configured' }
|
|
52
|
-
* | { kind: 'reachable', shop: ShopReport, latencyMs: number }
|
|
53
|
-
* | { kind: 'unreachable', error: unknown, latencyMs: number }} StorefrontCheck
|
|
54
|
-
*/
|
|
55
|
-
/**
|
|
56
|
-
* @typedef {object} DiagnoseOptions
|
|
57
|
-
* @property {ParentNode} [root]
|
|
58
|
-
* @property {boolean} [silent] Return the report without logging it.
|
|
59
|
-
* @property {string} [version] Tether's version, for the report.
|
|
60
|
-
* @property {Date} [now] The date the API version's support is measured against.
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/** Products checked at most, so a long CMS page doesn't send a request per card. */
|
|
64
|
-
const MAX_PRODUCTS_CHECKED = 20;
|
|
65
|
-
/** Matches the warning window of `apiVersionStatus`. */
|
|
66
|
-
const API_UPGRADE_WARNING_MONTHS = 3;
|
|
67
|
-
const TOKEN_EDGE_CHARACTERS = 4;
|
|
68
|
-
|
|
69
|
-
/** @param {string} token */
|
|
70
|
-
function maskToken(token) {
|
|
71
|
-
if (!token) return '(missing)';
|
|
72
|
-
return `${token.slice(0, TOKEN_EDGE_CHARACTERS)}…${token.slice(-TOKEN_EDGE_CHARACTERS)} (${token.length} chars)`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* @param {unknown} error
|
|
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) {
|
|
81
|
-
if (error instanceof Error && error.message) return error.message;
|
|
82
|
-
/** @type {unknown} */
|
|
83
|
-
const code = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
84
|
-
return typeof code === 'string' && code ? code : fallback;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** @returns {Record<string, unknown>} */
|
|
88
|
-
function configSummary() {
|
|
89
|
-
return {
|
|
90
|
-
domain: cfg.domain || '(missing)',
|
|
91
|
-
token: maskToken(cfg.token),
|
|
92
|
-
apiVersion: cfg.apiVersion,
|
|
93
|
-
endpoint: cfg.domain ? endpoint() : null,
|
|
94
|
-
country: cfg.country,
|
|
95
|
-
language: cfg.language,
|
|
96
|
-
locale: cfg.locale,
|
|
97
|
-
markets: cfg.markets,
|
|
98
|
-
prefix: cfg.prefix,
|
|
99
|
-
productPath: cfg.productPath,
|
|
100
|
-
metafields: cfg.metafields,
|
|
101
|
-
autoMount: cfg.autoMount,
|
|
102
|
-
observe: cfg.observe,
|
|
103
|
-
onError: typeof cfg.onError === 'function',
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Detection runs in the markets part, which init has already loaded whenever `markets.detect` is set.
|
|
109
|
-
*
|
|
110
|
-
* @returns {Promise<MarketReport>}
|
|
111
|
-
*/
|
|
112
|
-
async function marketSummary() {
|
|
113
|
-
return {
|
|
114
|
-
country: cfg.country || null,
|
|
115
|
-
language: cfg.language || null,
|
|
116
|
-
locale: cfg.locale || null,
|
|
117
|
-
detected: cfg.markets?.detect ? (await loadPart('markets')).detectMarket(cfg.markets) : null,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {ApiVersionStatus} status
|
|
123
|
-
* @returns {Problem | null} An error for an unsupported version, a warning when support ends soon.
|
|
124
|
-
*/
|
|
125
|
-
function apiVersionProblem({ ok, monthsLeft, note }) {
|
|
126
|
-
if (ok && (monthsLeft === undefined || monthsLeft >= API_UPGRADE_WARNING_MONTHS)) return null;
|
|
127
|
-
return { level: ok ? 'warn' : 'error', message: `apiVersion ${cfg.apiVersion}: ${note}` };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** @returns {Promise<StorefrontCheck>} */
|
|
131
|
-
async function checkStorefront() {
|
|
132
|
-
if (!isConfigured()) return { kind: 'not-configured' };
|
|
133
|
-
const startedAt = Date.now();
|
|
134
|
-
try {
|
|
135
|
-
/** @type {{ shop: { name: string, primaryDomain: { url: string }, paymentSettings: { currencyCode: string } } }} */
|
|
136
|
-
const data = await gql(
|
|
137
|
-
'query TetherDiagnose { shop { name primaryDomain { url } paymentSettings { currencyCode } } }',
|
|
138
|
-
{},
|
|
139
|
-
{ context: false },
|
|
140
|
-
);
|
|
141
|
-
const shop = {
|
|
142
|
-
name: data.shop.name,
|
|
143
|
-
url: data.shop.primaryDomain.url,
|
|
144
|
-
currency: data.shop.paymentSettings.currencyCode,
|
|
145
|
-
};
|
|
146
|
-
return { kind: 'reachable', shop, latencyMs: Date.now() - startedAt };
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return { kind: 'unreachable', error, latencyMs: Date.now() - startedAt };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* 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
|
-
*/
|
|
160
|
-
function checkOptions(root, key, product) {
|
|
161
|
-
/** @type {Problem[]} */
|
|
162
|
-
const problems = [];
|
|
163
|
-
const names = product.options.map((option) => option.name);
|
|
164
|
-
const roots = qsa(root, sel('product')).filter(
|
|
165
|
-
(el) => resolveProductKey(el).key === key && el.querySelector(sel('add-to-cart')),
|
|
166
|
-
);
|
|
167
|
-
for (const el of roots) {
|
|
168
|
-
const shown = new Set(
|
|
169
|
-
qsa(el, `${sel('option-list')},select${sel('option')}`).map(
|
|
170
|
-
(list) => get(list, 'option-list') || get(list, 'option'),
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
if (!shown.size && qsa(el, `select${sel('variant-select')}`).length) continue;
|
|
174
|
-
// Translated option names (a language in the market) don't match the names in the markup.
|
|
175
|
-
if (!cfg.language) {
|
|
176
|
-
for (const name of shown) {
|
|
177
|
-
if (!name || names.includes(name)) continue;
|
|
178
|
-
problems.push({
|
|
179
|
-
level: 'warn',
|
|
180
|
-
message: `Product "${product.handle}" has no option "${name}" (its options: ${names.join(', ') || 'none'}).${didYouMean(name, names)}`,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
for (const option of product.options) {
|
|
185
|
-
if (option.optionValues.length <= 1 || shown.has(option.name)) continue;
|
|
186
|
-
problems.push({
|
|
187
|
-
level: 'warn',
|
|
188
|
-
message: `Product "${product.handle}": option "${option.name}" (${option.optionValues.length} values) has no tether-option-list, so visitors can't choose it.`,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return problems;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Loads the page's products and adds what it finds about each to `report`.
|
|
197
|
-
*
|
|
198
|
-
* @param {ParentNode} root
|
|
199
|
-
* @param {DiagnoseReport} report
|
|
200
|
-
*/
|
|
201
|
-
async function checkProducts(root, report) {
|
|
202
|
-
const allKeys = qsa(root, sel('product')).map((el) => resolveProductKey(el).key);
|
|
203
|
-
const keys = [...new Set(allKeys.filter((key) => key !== null && key !== ''))].slice(0, MAX_PRODUCTS_CHECKED);
|
|
204
|
-
const results = await Promise.allSettled(keys.map((key) => getProduct(key)));
|
|
205
|
-
results.forEach((result, index) => {
|
|
206
|
-
const key = keys[index] ?? '';
|
|
207
|
-
if (result.status === 'rejected') {
|
|
208
|
-
const reason = errorText(result.reason, 'not loaded');
|
|
209
|
-
report.products.push({ key, error: result.reason instanceof Error ? result.reason.message : undefined });
|
|
210
|
-
report.problems.push({
|
|
211
|
-
level: 'error',
|
|
212
|
-
message: `Product "${key}": ${reason}. Is it published to the Headless channel?`,
|
|
213
|
-
});
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const product = result.value;
|
|
217
|
-
const plans = allPlans(product);
|
|
218
|
-
report.products.push({
|
|
219
|
-
key,
|
|
220
|
-
handle: product.handle,
|
|
221
|
-
title: product.title,
|
|
222
|
-
plans: plans.length,
|
|
223
|
-
requiresSellingPlan: product.requiresSellingPlan,
|
|
224
|
-
});
|
|
225
|
-
for (const plan of plans) {
|
|
226
|
-
report.sellingPlans.push({ product: product.handle, group: plan.group.name, plan: plan.name, id: plan.id });
|
|
227
|
-
}
|
|
228
|
-
report.problems.push(...checkOptions(root, key, product));
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* @param {DiagnoseOptions} [options]
|
|
234
|
-
* @returns {Promise<DiagnoseReport>} What's wrong on the page: config, token, API version, products, plans, markup.
|
|
235
|
-
*/
|
|
236
|
-
export async function diagnose({ root = document, silent = false, version, now = new Date() } = {}) {
|
|
237
|
-
const storefront = await checkStorefront();
|
|
238
|
-
/** @type {DiagnoseReport} */
|
|
239
|
-
const report = {
|
|
240
|
-
version,
|
|
241
|
-
config: configSummary(),
|
|
242
|
-
apiVersion: apiVersionStatus(cfg.apiVersion, now),
|
|
243
|
-
market: await marketSummary(),
|
|
244
|
-
reachable: storefront.kind === 'reachable',
|
|
245
|
-
shop: storefront.kind === 'reachable' ? storefront.shop : null,
|
|
246
|
-
latencyMs: storefront.kind === 'not-configured' ? null : storefront.latencyMs,
|
|
247
|
-
products: [],
|
|
248
|
-
sellingPlans: [],
|
|
249
|
-
problems: [],
|
|
250
|
-
};
|
|
251
|
-
switch (storefront.kind) {
|
|
252
|
-
case 'not-configured':
|
|
253
|
-
report.problems.push({ level: 'error', message: 'TETHER_CONFIG needs domain and token.' });
|
|
254
|
-
break;
|
|
255
|
-
case 'unreachable':
|
|
256
|
-
report.problems.push({
|
|
257
|
-
level: 'error',
|
|
258
|
-
message: `Storefront API not reachable: ${errorText(storefront.error, 'unknown error')}`,
|
|
259
|
-
});
|
|
260
|
-
break;
|
|
261
|
-
case 'reachable':
|
|
262
|
-
await checkProducts(root, report);
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
const versionProblem = apiVersionProblem(report.apiVersion);
|
|
266
|
-
if (versionProblem) report.problems.push(versionProblem);
|
|
267
|
-
if (typeof document !== 'undefined') report.problems.push(...checkMarkup(root));
|
|
268
|
-
if (!silent) print(report);
|
|
269
|
-
return report;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** @param {DiagnoseReport} report */
|
|
273
|
-
function print(report) {
|
|
274
|
-
const errors = report.problems.filter((problem) => problem.level === 'error').length;
|
|
275
|
-
const warnings = report.problems.length - errors;
|
|
276
|
-
console.group(`[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : 'no errors'}, ${warnings} warning(s)`);
|
|
277
|
-
console.log('version', report.version, '| API', report.apiVersion.version, `(${report.apiVersion.note})`);
|
|
278
|
-
console.log('config', report.config);
|
|
279
|
-
const { country, language, detected } = report.market;
|
|
280
|
-
const inUse = [country, language].filter(Boolean).join(' / ') || 'the store default';
|
|
281
|
-
console.log('market', `in use: ${inUse}`, detected ? `(detection picks ${JSON.stringify(detected)})` : '');
|
|
282
|
-
console.log(
|
|
283
|
-
report.shop
|
|
284
|
-
? `Storefront API reachable: ${report.shop.name} (${report.latencyMs ?? 0} ms)`
|
|
285
|
-
: 'Storefront API NOT reachable',
|
|
286
|
-
);
|
|
287
|
-
if (report.products.length) console.table(report.products);
|
|
288
|
-
if (report.sellingPlans.length) console.table(report.sellingPlans);
|
|
289
|
-
else if (report.reachable) console.log('No selling plans on the products of this page.');
|
|
290
|
-
for (const problem of report.problems) {
|
|
291
|
-
if (problem.level === 'error') console.error(problem.message, problem.el ?? '');
|
|
292
|
-
else console.warn(problem.message, problem.el ?? '');
|
|
293
|
-
}
|
|
294
|
-
console.groupEnd();
|
|
295
|
-
}
|
package/src/dom.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
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.js']`).
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, reportError, warn } from './config.js';
|
|
7
|
-
import { attr, emit, get } from './markup.js';
|
|
8
|
-
import { storeHtmlFragment } from './store-html.js';
|
|
9
|
-
import { formatMoney as formatMoneyIn, productUrl as productUrlFor } from './shopify-values.js';
|
|
10
|
-
/** @import { Money } from './shopify-values.js' */
|
|
11
|
-
|
|
12
|
-
export { attr, sel, get, has, qsa, scoped, emit } from './markup.js';
|
|
13
|
-
export { imageUrl, cdnUrl, sameImage, setImage } from './images.js';
|
|
14
|
-
export { hasTemplate, repeat, repeatKeyed } from './repeaters.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* State Tether keeps on elements, as `data-tether-*` attributes whatever the prefix: sites style and test against
|
|
18
|
-
* these names, so they stay fixed where the `tether-*` markup attributes follow `prefix`.
|
|
19
|
-
*/
|
|
20
|
-
const SEEN = 'data-tether-seen';
|
|
21
|
-
const SHOWN = 'data-tether-shown';
|
|
22
|
-
const PREVIOUS_DISPLAY = 'data-tether-prev-display';
|
|
23
|
-
|
|
24
|
-
/** Async renders change the page height in bursts; one notification per burst. */
|
|
25
|
-
const LAYOUT_DEBOUNCE_MS = 120;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The product page of a product (`productPath`).
|
|
29
|
-
*
|
|
30
|
-
* @param {string | null | undefined} handle
|
|
31
|
-
* @param {string | null} [id]
|
|
32
|
-
*/
|
|
33
|
-
export function productUrl(handle, id) {
|
|
34
|
-
return productUrlFor(cfg.productPath || '/product', handle, id);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* `Intl` throws a RangeError for a locale or currency it doesn't know; the caller then shows a plain fallback.
|
|
39
|
-
*
|
|
40
|
-
* @template T
|
|
41
|
-
* @param {() => T} format
|
|
42
|
-
* @param {T} fallback
|
|
43
|
-
* @returns {T}
|
|
44
|
-
*/
|
|
45
|
-
function formatOr(format, fallback) {
|
|
46
|
-
try {
|
|
47
|
-
return format();
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if (error instanceof RangeError) return fallback;
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* `30` → `30%` (en, nl) or `30 %` (de, fr), in the configured locale.
|
|
56
|
-
*
|
|
57
|
-
* @param {number | null | undefined} percentage
|
|
58
|
-
* @param {string | undefined} [locale]
|
|
59
|
-
* @returns {string} `''` for no or a zero percentage.
|
|
60
|
-
*/
|
|
61
|
-
export function formatPercent(percentage, locale = cfg.locale) {
|
|
62
|
-
if (!percentage) return '';
|
|
63
|
-
const format = () =>
|
|
64
|
-
new Intl.NumberFormat(locale || undefined, { style: 'percent', maximumFractionDigits: 0 }).format(percentage / 100);
|
|
65
|
-
return formatOr(format, `${percentage}%`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Money in the configured locale, through the site's `formatMoney` when it has one.
|
|
70
|
-
*
|
|
71
|
-
* @param {Money | null | undefined} money
|
|
72
|
-
*/
|
|
73
|
-
export function formatMoney(money) {
|
|
74
|
-
if (!money) return '';
|
|
75
|
-
if (cfg.formatMoney) return cfg.formatMoney(money, cfg.locale);
|
|
76
|
-
return formatMoneyIn(money, cfg.locale);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @param {string | null | undefined} iso
|
|
81
|
-
* @returns {string} The date in the configured locale, `''` without one.
|
|
82
|
-
*/
|
|
83
|
-
export function formatDate(iso) {
|
|
84
|
-
if (!iso) return '';
|
|
85
|
-
return formatOr(() => new Date(iso).toLocaleDateString(cfg.locale || undefined), '');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Writes HTML from the Shopify admin (product and collection descriptions) into `el`, with script removed
|
|
90
|
-
* (`store-html.js`). Everything else from Shopify is written as text (`setText`).
|
|
91
|
-
*
|
|
92
|
-
* @param {Element} el
|
|
93
|
-
* @param {string | null | undefined} html
|
|
94
|
-
*/
|
|
95
|
-
export function setStoreHtml(el, html) {
|
|
96
|
-
el.replaceChildren(storeHtmlFragment(html, el.ownerDocument));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {Element} el
|
|
101
|
-
* @returns {el is HTMLInputElement | HTMLTextAreaElement}
|
|
102
|
-
*/
|
|
103
|
-
const isTextField = (el) => el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* @param {Element} el
|
|
107
|
-
* @returns {el is HTMLInputElement}
|
|
108
|
-
*/
|
|
109
|
-
const isInput = (el) => el.tagName === 'INPUT';
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Sets the text of `el`, or its value when it is a form field.
|
|
113
|
-
*
|
|
114
|
-
* @param {Element} el
|
|
115
|
-
* @param {string | number | boolean | null | undefined} value `null` and `undefined` clear it.
|
|
116
|
-
*/
|
|
117
|
-
export function setText(el, value) {
|
|
118
|
-
const text = value == null ? '' : String(value);
|
|
119
|
-
if (isTextField(el)) el.value = text;
|
|
120
|
-
else el.textContent = text;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** @param {Element} el */
|
|
124
|
-
const readText = (el) => (isTextField(el) ? el.value : el.textContent);
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* `setText` that hands a change after the first render to `onValueChange`, for scramble and roll effects.
|
|
128
|
-
*
|
|
129
|
-
* @param {HTMLElement} el
|
|
130
|
-
* @param {string | number | boolean | null | undefined} value
|
|
131
|
-
*/
|
|
132
|
-
export function setTextTracked(el, value) {
|
|
133
|
-
const before = readText(el);
|
|
134
|
-
setText(el, value);
|
|
135
|
-
if (before !== readText(el) && el.hasAttribute(SEEN) && cfg.onValueChange) {
|
|
136
|
-
try {
|
|
137
|
-
cfg.onValueChange(el, before);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
reportError(error, 'onValueChange');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
el.setAttribute(SEEN, '1');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Shows an element hidden before the script ran (a class, an inline `display: none`, a `hidden` attribute), as
|
|
147
|
-
* `block` or the value of `tether-display`.
|
|
148
|
-
*
|
|
149
|
-
* @param {HTMLElement} el
|
|
150
|
-
*/
|
|
151
|
-
function reveal(el) {
|
|
152
|
-
if (el.hasAttribute(SHOWN)) return;
|
|
153
|
-
el.setAttribute(SHOWN, '');
|
|
154
|
-
el.removeAttribute('hidden');
|
|
155
|
-
const previous = el.getAttribute(PREVIOUS_DISPLAY);
|
|
156
|
-
if (previous !== null) {
|
|
157
|
-
el.style.display = previous;
|
|
158
|
-
el.removeAttribute(PREVIOUS_DISPLAY);
|
|
159
|
-
} else if (el.style.display === 'none') {
|
|
160
|
-
el.style.display = '';
|
|
161
|
-
}
|
|
162
|
-
if (typeof getComputedStyle === 'function' && getComputedStyle(el).display === 'none') {
|
|
163
|
-
el.style.display = el.getAttribute(attr('display')) || 'block';
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Shows or hides `el`, restoring its own display value when shown again.
|
|
169
|
-
*
|
|
170
|
-
* @param {HTMLElement} el
|
|
171
|
-
* @param {boolean} visible
|
|
172
|
-
*/
|
|
173
|
-
export function show(el, visible) {
|
|
174
|
-
if (visible) {
|
|
175
|
-
reveal(el);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
el.removeAttribute(SHOWN);
|
|
179
|
-
// The display value to restore is a real one, never 'none'.
|
|
180
|
-
if (!el.hasAttribute(PREVIOUS_DISPLAY) && el.style.display !== 'none') {
|
|
181
|
-
el.setAttribute(PREVIOUS_DISPLAY, el.style.display);
|
|
182
|
-
}
|
|
183
|
-
el.style.display = 'none';
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Marks an option button or input as chosen: `is-selected`, `aria-pressed`, and its own or inner radio/checkbox.
|
|
188
|
-
*
|
|
189
|
-
* @param {Element} el
|
|
190
|
-
* @param {boolean} selected
|
|
191
|
-
*/
|
|
192
|
-
export function setSelected(el, selected) {
|
|
193
|
-
el.classList.toggle('is-selected', selected);
|
|
194
|
-
el.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|
195
|
-
if (isInput(el) && (el.type === 'radio' || el.type === 'checkbox')) el.checked = selected;
|
|
196
|
-
const input = el.querySelector('input[type="radio"],input[type="checkbox"]');
|
|
197
|
-
if (input && isInput(input)) input.checked = selected;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
201
|
-
let layoutTimer;
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Async renders change the page height after the site's page-transition hooks ran. After a burst of them this calls
|
|
205
|
-
* `onLayoutChange` (else refreshes ScrollTrigger) and emits `tether:layout-changed`.
|
|
206
|
-
*
|
|
207
|
-
* @param {string} reason
|
|
208
|
-
*/
|
|
209
|
-
export function notifyLayoutChanged(reason) {
|
|
210
|
-
clearTimeout(layoutTimer);
|
|
211
|
-
layoutTimer = setTimeout(() => {
|
|
212
|
-
// GSAP's ScrollTrigger, when the site has it, measures positions that a height change makes stale.
|
|
213
|
-
const { ScrollTrigger } = /** @type {Window & { ScrollTrigger?: { refresh?: () => void } }} */ (window);
|
|
214
|
-
if (cfg.onLayoutChange) cfg.onLayoutChange(reason);
|
|
215
|
-
else ScrollTrigger?.refresh?.();
|
|
216
|
-
emit('layout-changed', { reason });
|
|
217
|
-
}, LAYOUT_DEBOUNCE_MS);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Notifies a layout change (`<reason>:images`) once every image in `root` that is still loading has loaded or failed.
|
|
222
|
-
*
|
|
223
|
-
* @param {ParentNode} root
|
|
224
|
-
* @param {string} reason
|
|
225
|
-
*/
|
|
226
|
-
export function watchImages(root, reason) {
|
|
227
|
-
const loading = [...root.querySelectorAll('img')].filter((img) => !img.complete);
|
|
228
|
-
let left = loading.length;
|
|
229
|
-
const done = () => {
|
|
230
|
-
left -= 1;
|
|
231
|
-
if (left === 0) notifyLayoutChanged(`${reason}:images`);
|
|
232
|
-
};
|
|
233
|
-
loading.forEach((img) => {
|
|
234
|
-
img.addEventListener('load', done, { once: true });
|
|
235
|
-
img.addEventListener('error', done, { once: true });
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Runs `task` with `aria-busy` on `el`; does nothing while `el` is already busy.
|
|
241
|
-
*
|
|
242
|
-
* @template T
|
|
243
|
-
* @param {Element} el
|
|
244
|
-
* @param {() => T | Promise<T>} task
|
|
245
|
-
* @returns {Promise<T | undefined>} The task's result, `undefined` when it didn't run.
|
|
246
|
-
*/
|
|
247
|
-
export function busy(el, task) {
|
|
248
|
-
if (el.getAttribute('aria-busy') === 'true') return Promise.resolve(undefined);
|
|
249
|
-
el.setAttribute('aria-busy', 'true');
|
|
250
|
-
return Promise.resolve()
|
|
251
|
-
.then(task)
|
|
252
|
-
.finally(() => {
|
|
253
|
-
el.removeAttribute('aria-busy');
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* @param {Element | null | undefined} el
|
|
259
|
-
* @param {string} name
|
|
260
|
-
* @returns {unknown} The attribute's JSON, `null` when it is missing or not JSON (with a warning).
|
|
261
|
-
*/
|
|
262
|
-
export function parseJSONAttr(el, name) {
|
|
263
|
-
const raw = el ? get(el, name) : null;
|
|
264
|
-
if (!raw) return null;
|
|
265
|
-
try {
|
|
266
|
-
return /** @type {unknown} */ (JSON.parse(raw));
|
|
267
|
-
} catch {
|
|
268
|
-
warn(`invalid JSON in ${attr(name)}`, el);
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** @typedef {string | number | boolean | null | undefined} AttributeValue */
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* `{ a: 1 }` or `[{ key, value }]` → `[{ key, value }]` as Shopify wants cart and line attributes.
|
|
277
|
-
*
|
|
278
|
-
* @param {Record<string, AttributeValue> | { key: string | number, value?: AttributeValue }[] | null | undefined} attributes
|
|
279
|
-
* @returns {{ key: string, value: string }[]}
|
|
280
|
-
*/
|
|
281
|
-
export function toAttributes(attributes) {
|
|
282
|
-
if (!attributes) return [];
|
|
283
|
-
if (Array.isArray(attributes)) {
|
|
284
|
-
return attributes.map((attribute) => ({ key: String(attribute.key), value: String(attribute.value ?? '') }));
|
|
285
|
-
}
|
|
286
|
-
return Object.entries(attributes).map(([key, value]) => ({ key, value: String(value ?? '') }));
|
|
287
|
-
}
|