@occluze/tether 0.6.6 → 0.6.8
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 +3 -3
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
- package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
- package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
- package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
- package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
- package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
- package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
- package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
- package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
- package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
- package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
- package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
- package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
- package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
- package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
- package/dist/esm/chunks/search-OU75RNQL.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -1
- package/dist/presets/barba.esm.js +56 -36
- package/dist/tether.account-pages.js +776 -432
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +655 -321
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +318 -191
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +334 -243
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +78 -33
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3171 -1998
- package/dist/tether.markets.js +231 -126
- 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 +221 -126
- package/dist/tether.search.min.js +1 -1
- package/package.json +23 -4
- package/src/account/addresses-view.js +92 -0
- package/src/account/browser-session.js +359 -0
- package/src/account/markup.js +48 -0
- package/src/account/operations.js +180 -0
- package/src/account/order-lines.js +96 -0
- package/src/account/order-list.js +91 -0
- package/src/account/order-page.js +192 -0
- package/src/account/page-events.js +288 -0
- package/src/account/server-client.js +172 -0
- package/src/account/shared.js +111 -0
- package/src/account/shopify-auth.js +150 -0
- package/src/account/subscriptions-view.js +158 -0
- package/src/account-api.js +118 -0
- package/src/account-links.js +35 -0
- package/src/account-pages.js +68 -284
- package/src/account.js +281 -317
- package/src/amounts.js +36 -0
- package/src/analytics-shopify.js +241 -0
- package/src/analytics.js +304 -184
- package/src/api-version.js +75 -25
- package/src/astro.js +233 -68
- package/src/cart/checkout.js +97 -0
- package/src/cart/drawer.js +138 -0
- package/src/cart/mutations.js +234 -0
- package/src/cart/render.js +294 -0
- package/src/cart/store.js +326 -0
- package/src/cart.js +322 -481
- package/src/client.js +214 -78
- package/src/collection.js +126 -57
- package/src/config.js +323 -59
- package/src/controls/cart-controls.js +155 -0
- package/src/controls/discount.js +96 -0
- package/src/controls/extras.js +63 -0
- package/src/controls/navigation.js +83 -0
- package/src/diagnose-markup.js +333 -0
- package/src/diagnose.js +251 -202
- package/src/dom.js +219 -220
- package/src/extras.js +119 -37
- package/src/fragments.js +115 -22
- package/src/images.js +104 -0
- package/src/index.js +245 -285
- package/src/initial.js +78 -66
- package/src/lazy-iife.js +92 -22
- package/src/lazy.js +25 -4
- package/src/loader.js +263 -124
- package/src/market-codes.js +35 -0
- package/src/markets-ui.js +169 -55
- package/src/markets.js +286 -85
- package/src/markup.js +64 -0
- package/src/mounting.js +214 -91
- package/src/page-market.js +116 -0
- package/src/parts/account-pages.js +4 -1
- package/src/parts/account.js +4 -1
- package/src/parts/analytics.js +4 -1
- package/src/parts/diagnose.js +4 -1
- package/src/parts/extras.js +4 -1
- package/src/parts/markets.js +4 -1
- package/src/parts/search.js +4 -1
- package/src/presets/barba.js +120 -30
- package/src/product-events.js +130 -0
- package/src/product-render.js +257 -0
- package/src/product-state.js +318 -0
- package/src/product.js +233 -459
- package/src/repeaters.js +173 -0
- package/src/search.js +277 -107
- package/src/selling-plans.js +107 -47
- package/src/server.js +168 -205
- package/src/shopify-values.js +89 -0
- package/src/status.js +99 -39
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +199 -0
- package/src/storefront/json-ld.js +167 -0
- package/src/variant-render.js +165 -0
- package/types/astro.d.ts +13 -2
- package/types/index.d.ts +178 -27
- package/types/server.d.ts +89 -17
- package/dist/esm/chunks/account-QUBHQG3C.js +0 -1
- package/dist/esm/chunks/account-pages-VWDHF6WA.js +0 -1
- package/dist/esm/chunks/analytics-SLATWPLC.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-L2TRRKYV.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/chunk-VCPI7MJ2.js +0 -1
- package/dist/esm/chunks/chunk-VPVJPNB4.js +0 -1
- package/dist/esm/chunks/chunk-ZIUIWEON.js +0 -1
- package/dist/esm/chunks/diagnose-VXQBSD3A.js +0 -14
- package/dist/esm/chunks/extras-R4X34VQL.js +0 -1
- package/dist/esm/chunks/markets-ui-F3R5T5EL.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
package/src/diagnose.js
CHANGED
|
@@ -1,246 +1,295 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tether.diagnose()
|
|
3
|
-
*
|
|
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
4
|
*
|
|
5
5
|
* await Tether.diagnose()
|
|
6
6
|
* await Tether.diagnose({ silent: true }) // just the object
|
|
7
7
|
*/
|
|
8
8
|
import { cfg, isConfigured } from './config.js';
|
|
9
9
|
import { gql, endpoint } from './client.js';
|
|
10
|
-
import {
|
|
10
|
+
import { sel, get, qsa } from './dom.js';
|
|
11
11
|
import { getProduct } from './loader.js';
|
|
12
12
|
import { allPlans } from './selling-plans.js';
|
|
13
|
-
import { resolveProductKey
|
|
13
|
+
import { resolveProductKey } from './status.js';
|
|
14
14
|
import { apiVersionStatus } from './api-version.js';
|
|
15
|
-
import {
|
|
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' */
|
|
16
21
|
|
|
17
|
-
export { apiVersionStatus };
|
|
22
|
+
export { apiVersionStatus, checkMarkup, didYouMean };
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
+
*/
|
|
49
62
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
56
|
-
}
|
|
57
|
-
return d[a.length][b.length];
|
|
58
|
-
}
|
|
59
|
-
export function didYouMean(value, options, prefix = '') {
|
|
60
|
-
let best = null; let score = Infinity;
|
|
61
|
-
for (const o of options) {
|
|
62
|
-
const n = distance(String(value), o);
|
|
63
|
-
if (n < score) { score = n; best = o; }
|
|
64
|
-
}
|
|
65
|
-
return best && score <= Math.max(2, Math.floor(String(value).length / 4)) ? ` Did you mean "${prefix}${best}"?` : '';
|
|
66
|
-
}
|
|
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;
|
|
67
68
|
|
|
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
|
+
}
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
106
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
}
|
|
89
120
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
129
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
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
|
+
}
|
|
102
151
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
106
183
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
qsa(root, sel('show')).forEach((el) => {
|
|
114
|
-
const k = get(el, 'show');
|
|
115
|
-
if (!SHOW.has(k)) add('warn', `Unknown ${attr('show')}="${k}".${didYouMean(k, SHOW)}`, el);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
qsa(root, sel('bind')).forEach((el) => {
|
|
119
|
-
const k = get(el, 'bind') || '';
|
|
120
|
-
if (!k || BIND.has(k) || /^image:\d+$/.test(k) || k.startsWith('metafield:')) return;
|
|
121
|
-
add('warn', `Unknown ${attr('bind')}="${k}": it stays empty.${didYouMean(k, BIND)}`, el);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Typos in attribute names (tether-bnd, tether-add-to-card) do nothing, silently.
|
|
125
|
-
const prefix = `${cfg.prefix}-`;
|
|
126
|
-
const seen = new Set();
|
|
127
|
-
qsa(root, '*').forEach((el) => {
|
|
128
|
-
for (const a of el.attributes) {
|
|
129
|
-
if (!a.name.startsWith(prefix)) continue;
|
|
130
|
-
const name = a.name.slice(prefix.length);
|
|
131
|
-
if (ATTRS.has(name) || seen.has(name)) continue;
|
|
132
|
-
seen.add(name);
|
|
133
|
-
add('warn', `Unknown attribute ${a.name}: Tether ignores it.${didYouMean(name, ATTRS, prefix)}`, el);
|
|
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
|
+
});
|
|
134
190
|
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
qsa(root, sel('bind')).forEach((el) => {
|
|
138
|
-
const k = get(el, 'bind') || '';
|
|
139
|
-
if (!k.startsWith('metafield:')) return;
|
|
140
|
-
const ref = k.slice(10);
|
|
141
|
-
const listed = (cfg.metafields || []).some((m) => (typeof m === 'string' ? m : `${m.namespace}.${m.key}`) === ref);
|
|
142
|
-
if (!listed) add('error', `${attr('bind')}="${k}" but "${ref}" is not in config.metafields, so it stays empty.`, el);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
if (qsa(root, sel('cart-open')).length && !qsa(root, sel('cart-drawer')).length) {
|
|
146
|
-
add('warn', `${attr('cart-open')} exists but there is no ${attr('cart-drawer')} on this page.`);
|
|
147
191
|
}
|
|
148
|
-
|
|
192
|
+
return problems;
|
|
193
|
+
}
|
|
149
194
|
|
|
150
|
-
|
|
151
|
-
|
|
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));
|
|
152
229
|
});
|
|
153
|
-
|
|
154
|
-
return problems;
|
|
155
230
|
}
|
|
156
231
|
|
|
232
|
+
/**
|
|
233
|
+
* @param {DiagnoseOptions} [options]
|
|
234
|
+
* @returns {Promise<DiagnoseReport>} What's wrong on the page: config, token, API version, products, plans, markup.
|
|
235
|
+
*/
|
|
157
236
|
export async function diagnose({ root = document, silent = false, version, now = new Date() } = {}) {
|
|
237
|
+
const storefront = await checkStorefront();
|
|
238
|
+
/** @type {DiagnoseReport} */
|
|
158
239
|
const report = {
|
|
159
240
|
version,
|
|
160
|
-
config:
|
|
161
|
-
domain: cfg.domain || '(missing)',
|
|
162
|
-
token: mask(cfg.token),
|
|
163
|
-
apiVersion: cfg.apiVersion,
|
|
164
|
-
endpoint: cfg.domain ? endpoint() : null,
|
|
165
|
-
country: cfg.country, language: cfg.language, locale: cfg.locale, markets: cfg.markets || null,
|
|
166
|
-
prefix: cfg.prefix, productPath: cfg.productPath, metafields: cfg.metafields,
|
|
167
|
-
autoMount: cfg.autoMount, observe: cfg.observe, onError: typeof cfg.onError === 'function',
|
|
168
|
-
},
|
|
241
|
+
config: configSummary(),
|
|
169
242
|
apiVersion: apiVersionStatus(cfg.apiVersion, now),
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
reachable:
|
|
173
|
-
|
|
174
|
-
latencyMs: null,
|
|
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,
|
|
175
247
|
products: [],
|
|
176
248
|
sellingPlans: [],
|
|
177
249
|
problems: [],
|
|
178
250
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
report.shop = { name: data.shop?.name, url: data.shop?.primaryDomain?.url, currency: data.shop?.paymentSettings?.currencyCode };
|
|
188
|
-
} catch (err) {
|
|
189
|
-
report.problems.push({ level: 'error', message: `Storefront API not reachable: ${err.message || err.code || 'unknown error'}` });
|
|
190
|
-
}
|
|
191
|
-
report.latencyMs = Date.now() - t0;
|
|
192
|
-
|
|
193
|
-
if (report.reachable) {
|
|
194
|
-
const keys = [...new Set(qsa(root, sel('product')).map((el) => resolveProductKey(el).key).filter(Boolean))].slice(0, 20);
|
|
195
|
-
const results = await Promise.allSettled(keys.map((k) => getProduct(k)));
|
|
196
|
-
results.forEach((r, i) => {
|
|
197
|
-
if (r.status === 'fulfilled') {
|
|
198
|
-
const p = r.value;
|
|
199
|
-
const plans = allPlans(p);
|
|
200
|
-
report.products.push({ key: keys[i], handle: p.handle, title: p.title, plans: plans.length, requiresSellingPlan: p.requiresSellingPlan });
|
|
201
|
-
plans.forEach((pl) => report.sellingPlans.push({ product: p.handle, group: pl.group?.name, plan: pl.name, id: pl.id }));
|
|
202
|
-
// Options a visitor can't choose on this page (they get the first available variant's value).
|
|
203
|
-
qsa(root, sel('product')).filter((el) => resolveProductKey(el).key === keys[i] && el.querySelector(sel('add-to-cart'))).forEach((el) => {
|
|
204
|
-
const shown = new Set(qsa(el, `${sel('option-list')},select${sel('option')}`).map((x) => get(x, 'option-list') || get(x, 'option')));
|
|
205
|
-
if (!shown.size && qsa(el, `select${sel('variant-select')}`).length) return;
|
|
206
|
-
const names = (p.options || []).map((o) => o.name);
|
|
207
|
-
if (!cfg.language) {
|
|
208
|
-
[...shown].filter((n) => n && !names.includes(n)).forEach((n) => {
|
|
209
|
-
report.problems.push({ level: 'warn', message: `Product "${p.handle}" has no option "${n}" (its options: ${names.join(', ') || 'none'}).${didYouMean(n, names)}` });
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
(p.options || []).filter((o) => o.optionValues.length > 1 && !shown.has(o.name)).forEach((o) => {
|
|
213
|
-
report.problems.push({ level: 'warn', message: `Product "${p.handle}": option "${o.name}" (${o.optionValues.length} values) has no tether-option-list, so visitors can't choose it.` });
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
} else {
|
|
217
|
-
report.products.push({ key: keys[i], error: r.reason?.message });
|
|
218
|
-
report.problems.push({ level: 'error', message: `Product "${keys[i]}": ${r.reason?.message || r.reason?.code || 'not loaded'}. Is it published to the Headless channel?` });
|
|
219
|
-
}
|
|
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')}`,
|
|
220
259
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
report.problems.push({ level: report.apiVersion.ok ? 'warn' : 'error', message: `apiVersion ${cfg.apiVersion}: ${report.apiVersion.note}` });
|
|
260
|
+
break;
|
|
261
|
+
case 'reachable':
|
|
262
|
+
await checkProducts(root, report);
|
|
263
|
+
break;
|
|
226
264
|
}
|
|
265
|
+
const versionProblem = apiVersionProblem(report.apiVersion);
|
|
266
|
+
if (versionProblem) report.problems.push(versionProblem);
|
|
227
267
|
if (typeof document !== 'undefined') report.problems.push(...checkMarkup(root));
|
|
228
|
-
|
|
229
268
|
if (!silent) print(report);
|
|
230
269
|
return report;
|
|
231
270
|
}
|
|
232
271
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const errors =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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();
|
|
246
295
|
}
|