@occluze/tether 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -1
- package/dist/presets/barba.esm.js +55 -39
- package/dist/tether.account-pages.js +781 -495
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +462 -381
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +299 -194
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +354 -262
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +82 -45
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3653 -2116
- package/dist/tether.markets.js +238 -150
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +3 -3
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +207 -136
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +42 -22
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/order-lines.ts +75 -0
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/page-events.ts +253 -0
- package/src/account/server-client.ts +183 -0
- package/src/account/subscriptions-view.ts +115 -0
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/account-pages.ts +81 -0
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/analytics-shopify.ts +246 -0
- package/src/analytics.ts +315 -0
- package/src/api-version.ts +79 -0
- package/src/astro.ts +272 -0
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/drawer.ts +125 -0
- package/src/cart/mutations.ts +237 -0
- package/src/cart/render.ts +266 -0
- package/src/cart/store.ts +279 -0
- package/src/cart.ts +345 -0
- package/src/client.ts +391 -0
- package/src/collection.ts +175 -0
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/cart-controls.ts +123 -0
- package/src/controls/discount.ts +75 -0
- package/src/controls/extras.ts +54 -0
- package/src/controls/navigation.ts +77 -0
- package/src/diagnose-markup.ts +298 -0
- package/src/diagnose.ts +294 -0
- package/src/dom.ts +268 -0
- package/src/extras.ts +136 -0
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/images.ts +91 -0
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/loader.ts +363 -0
- package/src/market-codes.ts +6 -0
- package/src/markets-ui.ts +227 -0
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/mounting.ts +264 -0
- package/src/page-market.ts +110 -0
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/product-events.ts +115 -0
- package/src/product-render.ts +218 -0
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/product.ts +308 -0
- package/src/repeaters.ts +161 -0
- package/src/search.ts +305 -0
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/shopify-values.ts +91 -0
- package/src/status.ts +106 -0
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/json-ld.ts +144 -0
- package/src/variant-render.ts +139 -0
- package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
- package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
- package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
- package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
- package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
- package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
- package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
- package/src/account-pages.js +0 -318
- package/src/account.js +0 -385
- package/src/analytics.js +0 -232
- package/src/api-version.js +0 -35
- package/src/astro.js +0 -101
- package/src/cart.js +0 -559
- package/src/client.js +0 -131
- package/src/collection.js +0 -114
- package/src/config.js +0 -85
- package/src/diagnose.js +0 -246
- package/src/dom.js +0 -288
- package/src/extras.js +0 -63
- package/src/fragments.js +0 -144
- package/src/index.js +0 -395
- package/src/initial.js +0 -75
- package/src/lazy-iife.js +0 -46
- package/src/lazy.js +0 -18
- package/src/loader.js +0 -198
- package/src/markets-ui.js +0 -90
- package/src/markets.js +0 -133
- package/src/mounting.js +0 -154
- package/src/parts/account-pages.js +0 -3
- package/src/parts/account.js +0 -3
- package/src/parts/analytics.js +0 -3
- package/src/parts/diagnose.js +0 -3
- package/src/parts/extras.js +0 -3
- package/src/parts/markets.js +0 -3
- package/src/parts/search.js +0 -3
- package/src/presets/barba.js +0 -100
- package/src/product.js +0 -570
- package/src/search.js +0 -153
- package/src/selling-plans.js +0 -94
- package/src/server.js +0 -252
- package/src/status.js +0 -66
- package/types/astro.d.ts +0 -17
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -426
- package/types/server.d.ts +0 -110
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsers for the options with structure, whose values become URLs, credentials, storage names or query text: the
|
|
3
|
+
* store's domain, the metafields, the embed origins and the nested `account`, `markets` and `analytics` objects.
|
|
4
|
+
* `account.server` becomes the base of credentialed requests, so a value of the wrong kind is dropped (with a warning
|
|
5
|
+
* through `invalid`), never used. `config.ts` resolves the rest.
|
|
6
|
+
*/
|
|
7
|
+
import { isRecord } from '../server/src/shared/json.ts';
|
|
8
|
+
import type { AccountConfig, AnalyticsConfig, MarketsConfig, MetafieldIdentifier, MetafieldRef } from './config.ts';
|
|
9
|
+
|
|
10
|
+
/** Warns that an option was dropped. */
|
|
11
|
+
type Invalid = (name: string, value: unknown) => void;
|
|
12
|
+
|
|
13
|
+
/** A host name, without protocol or path: what `domainOption` keeps of the `domain` option. */
|
|
14
|
+
export type ShopDomain = string & { readonly __brand: 'ShopDomain' };
|
|
15
|
+
type LocaleMarket = NonNullable<MarketsConfig['locales']>[string];
|
|
16
|
+
type ShopifyAnalyticsOptions = Exclude<NonNullable<AnalyticsConfig['shopify']>, boolean>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metafield identifiers are written into the product fragment's text: a GraphQL fragment can't declare variables, so
|
|
20
|
+
* they can't be sent as variables. Only these characters are let through, which covers Shopify's namespaces and keys.
|
|
21
|
+
*/
|
|
22
|
+
const METAFIELD_PART = /^[A-Za-z0-9_.-]+$/;
|
|
23
|
+
/** A namespace is the same, or an app-reserved one: `$app` or `$app:<name>`. */
|
|
24
|
+
const METAFIELD_NAMESPACE = /^(\$app(:[A-Za-z0-9_-]+)?|[A-Za-z0-9_.-]+)$/;
|
|
25
|
+
/** A DNS name: labels of letters, digits and inner hyphens. No port, user or path. */
|
|
26
|
+
const HOSTNAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
27
|
+
/** A cookie name, as RFC 6265 allows it. */
|
|
28
|
+
const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
29
|
+
/** A global variable's name, which `analytics.dataLayer` names. */
|
|
30
|
+
const GLOBAL_NAME = /^[A-Za-z_$][\w$]*$/;
|
|
31
|
+
/** Stands for the site's own origin, to tell a path on the site from a URL elsewhere. */
|
|
32
|
+
const SITE_ORIGIN = 'https://site.invalid';
|
|
33
|
+
/** Hosts a developer's own machine answers on, where plain http is the norm (`wrangler dev`). */
|
|
34
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
35
|
+
/** Every source `markets.detect` reads. */
|
|
36
|
+
export const MARKET_SOURCES: readonly ('saved' | 'server' | 'webflow')[] = ['saved', 'server', 'webflow'];
|
|
37
|
+
const CONSENT_MODES: readonly NonNullable<ShopifyAnalyticsOptions['consent']>[] = [
|
|
38
|
+
'no-banner',
|
|
39
|
+
'default-banner',
|
|
40
|
+
'custom-banner',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const isText = (value: unknown): value is string => typeof value === 'string' && value !== '';
|
|
44
|
+
|
|
45
|
+
/** A hook the site passes. What it is called with is up to Tether, and what it returns is not used. */
|
|
46
|
+
const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => typeof value === 'function';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @returns The store's domain without protocol or path (`https://shop.example/x` → `shop.example`), `''` for anything
|
|
50
|
+
* that isn't a host name.
|
|
51
|
+
*/
|
|
52
|
+
export function domainOption(value: unknown, invalid: Invalid): ShopDomain | '' {
|
|
53
|
+
if (typeof value !== 'string' || !value) return '';
|
|
54
|
+
const host = value.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
55
|
+
if (isHostname(host)) return host;
|
|
56
|
+
invalid('domain', value);
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @returns True for a path on this site, an https URL, or an http one on the developer's machine. */
|
|
61
|
+
function isSiteOrHttpsUrl(value: unknown): value is string {
|
|
62
|
+
if (!isText(value)) return false;
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(value, `${SITE_ORIGIN}/`);
|
|
65
|
+
if (url.origin === SITE_ORIGIN) return true;
|
|
66
|
+
return url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname));
|
|
67
|
+
} catch {
|
|
68
|
+
// Not a URL at all.
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The folder of the optional parts. It must be https or on this site, which keeps parts off plain http (mixed content,
|
|
75
|
+
* rewritable on the way); it doesn't vouch for the host, which the site chose.
|
|
76
|
+
*
|
|
77
|
+
* @returns The folder as written, `null` for none or one elsewhere (the parts then load beside the core).
|
|
78
|
+
*/
|
|
79
|
+
export function partsUrlOption(value: unknown, invalid: Invalid): string | null {
|
|
80
|
+
if (value === undefined || value === null || value === '') return null;
|
|
81
|
+
if (typeof value === 'string' && (isSiteOrHttpsUrl(value) || isPageOrigin(value))) return value;
|
|
82
|
+
invalid('partsUrl', value);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @returns True for an absolute URL on the page's own origin (a site served over plain http). */
|
|
87
|
+
function isPageOrigin(value: string): boolean {
|
|
88
|
+
if (typeof location === 'undefined') return false;
|
|
89
|
+
try {
|
|
90
|
+
return new URL(value).origin === location.origin;
|
|
91
|
+
} catch {
|
|
92
|
+
// Not an absolute URL: `isSiteOrHttpsUrl` has judged it.
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @returns An https origin written as just that (`https://video.example`), `null` for anything else. */
|
|
98
|
+
function toHttpsOrigin(value: unknown): string | null {
|
|
99
|
+
if (typeof value !== 'string') return null;
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(value);
|
|
102
|
+
return url.protocol === 'https:' && url.href === new URL(url.origin).href ? url.origin : null;
|
|
103
|
+
} catch {
|
|
104
|
+
// Not an absolute URL.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The origins a site lets description iframes load from. Each one is trusted to show only what the site expects in a
|
|
111
|
+
* description, so only a bare https origin is taken: a path would suggest a narrower trust than is given.
|
|
112
|
+
*
|
|
113
|
+
* @returns The valid origins; each other entry is skipped with a warning.
|
|
114
|
+
*/
|
|
115
|
+
export function embedOriginsOption(value: unknown, invalid: Invalid): string[] {
|
|
116
|
+
if (!Array.isArray(value)) {
|
|
117
|
+
invalid('embedOrigins', value);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const origins: string[] = [];
|
|
121
|
+
const entries: unknown[] = value;
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const origin = toHttpsOrigin(entry);
|
|
124
|
+
if (origin) origins.push(origin);
|
|
125
|
+
else invalid('embedOrigin', entry);
|
|
126
|
+
}
|
|
127
|
+
return origins;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @returns The identifier of `'namespace.key'` or `{ namespace, key }`, when both parts are valid identifiers. */
|
|
131
|
+
export function toMetafieldIdentifier(ref: unknown): MetafieldIdentifier | null {
|
|
132
|
+
let namespace: unknown;
|
|
133
|
+
let key: unknown;
|
|
134
|
+
if (typeof ref === 'string') {
|
|
135
|
+
const dot = ref.indexOf('.');
|
|
136
|
+
namespace = dot > 0 ? ref.slice(0, dot) : '';
|
|
137
|
+
key = ref.slice(dot + 1);
|
|
138
|
+
} else if (typeof ref === 'object' && ref !== null) {
|
|
139
|
+
namespace = Reflect.get(ref, 'namespace');
|
|
140
|
+
key = Reflect.get(ref, 'key');
|
|
141
|
+
}
|
|
142
|
+
if (typeof namespace !== 'string' || typeof key !== 'string') return null;
|
|
143
|
+
return METAFIELD_NAMESPACE.test(namespace) && METAFIELD_PART.test(key) ? { namespace, key } : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @returns The refs that name a valid identifier, as written; each other one is skipped with a warning. */
|
|
147
|
+
export function metafieldsOption(value: unknown, invalid: Invalid): MetafieldRef[] {
|
|
148
|
+
if (!Array.isArray(value)) {
|
|
149
|
+
invalid('metafields', value);
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const refs: MetafieldRef[] = [];
|
|
153
|
+
const entries: unknown[] = value;
|
|
154
|
+
for (const ref of entries) {
|
|
155
|
+
const identifier = toMetafieldIdentifier(ref);
|
|
156
|
+
if (identifier) refs.push(typeof ref === 'string' ? ref : identifier);
|
|
157
|
+
else invalid('metafield', ref);
|
|
158
|
+
}
|
|
159
|
+
return refs;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** How each field of a nested option is read: the field's value when it is of the right kind, else `undefined`. */
|
|
163
|
+
type Schema<Section extends object> = { [Key in keyof Section]-?: (value: unknown) => Section[Key] | undefined };
|
|
164
|
+
|
|
165
|
+
const when =
|
|
166
|
+
<T>(check: (value: unknown) => value is T) =>
|
|
167
|
+
(value: unknown): T | undefined =>
|
|
168
|
+
check(value) ? value : undefined;
|
|
169
|
+
|
|
170
|
+
/** The pattern is what makes a string a `ShopDomain`. */
|
|
171
|
+
const isHostname = (value: unknown): value is ShopDomain => isText(value) && HOSTNAME.test(value);
|
|
172
|
+
|
|
173
|
+
const isCookieName = (value: unknown): value is string => isText(value) && COOKIE_NAME.test(value);
|
|
174
|
+
|
|
175
|
+
/** `true` for `window.dataLayer`, or the name of another global array. */
|
|
176
|
+
const isDataLayer = (value: unknown): value is boolean | string =>
|
|
177
|
+
typeof value === 'boolean' || (isText(value) && GLOBAL_NAME.test(value));
|
|
178
|
+
|
|
179
|
+
const url = when(isSiteOrHttpsUrl);
|
|
180
|
+
const text = when(isText);
|
|
181
|
+
const flag = when((value): value is boolean => typeof value === 'boolean');
|
|
182
|
+
const hook = when(isFunction);
|
|
183
|
+
|
|
184
|
+
/** @returns The fields of the right kind; each other one is dropped with a warning. */
|
|
185
|
+
function sectionOption<Section extends object>(
|
|
186
|
+
name: string,
|
|
187
|
+
value: unknown,
|
|
188
|
+
schema: Schema<Section>,
|
|
189
|
+
invalid: Invalid,
|
|
190
|
+
): Section | null {
|
|
191
|
+
if (value === null || value === undefined) return null;
|
|
192
|
+
if (!isRecord(value)) {
|
|
193
|
+
invalid(name, value);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const section: Record<string, unknown> = {};
|
|
197
|
+
const readers: Readonly<Record<string, (value: unknown) => unknown>> = schema;
|
|
198
|
+
for (const [key, read] of Object.entries(readers)) {
|
|
199
|
+
const field = value[key];
|
|
200
|
+
if (field === undefined) continue;
|
|
201
|
+
const parsed = read(field);
|
|
202
|
+
if (parsed === undefined) invalid(`${name}.${key}`, field);
|
|
203
|
+
else section[key] = parsed;
|
|
204
|
+
}
|
|
205
|
+
// Each field went through the reader its schema entry gives it, which returns that field's type in `Section`.
|
|
206
|
+
return section as Section;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const ACCOUNT: Schema<Partial<AccountConfig>> = {
|
|
210
|
+
server: url,
|
|
211
|
+
orderUrl: url,
|
|
212
|
+
subscriptionsUrl: url,
|
|
213
|
+
paymentUrl: url,
|
|
214
|
+
logoutUrl: url,
|
|
215
|
+
locale: text,
|
|
216
|
+
navigate: hook,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Customer tokens are never kept in the browser, so an `account` without the Worker is off. */
|
|
220
|
+
const ACCOUNT_NEEDS_SERVER = 'customer accounts need the Worker since 0.7.0: set account.server';
|
|
221
|
+
|
|
222
|
+
const LOCALE_MARKET: Schema<LocaleMarket> = { country: text, language: text, locale: text };
|
|
223
|
+
|
|
224
|
+
const MARKETS: Schema<MarketsConfig> = {
|
|
225
|
+
// A single source name counts as a list of one.
|
|
226
|
+
detect: (value) => {
|
|
227
|
+
if (typeof value === 'boolean') return value;
|
|
228
|
+
const names: unknown[] = Array.isArray(value) ? value : [value];
|
|
229
|
+
return typeof value === 'string' || Array.isArray(value)
|
|
230
|
+
? MARKET_SOURCES.filter((source) => names.includes(source))
|
|
231
|
+
: undefined;
|
|
232
|
+
},
|
|
233
|
+
locales: (value) => {
|
|
234
|
+
if (!isRecord(value)) return undefined;
|
|
235
|
+
const locales: Record<string, LocaleMarket> = {};
|
|
236
|
+
for (const [locale, entry] of Object.entries(value)) {
|
|
237
|
+
const market = sectionOption(`markets.locales.${locale}`, entry, LOCALE_MARKET, () => undefined);
|
|
238
|
+
if (market) locales[locale] = market;
|
|
239
|
+
}
|
|
240
|
+
return locales;
|
|
241
|
+
},
|
|
242
|
+
cookie: when(isCookieName),
|
|
243
|
+
remember: flag,
|
|
244
|
+
strict: flag,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* `proxy` says the site serves the Storefront API on its own domain. Shopify's script only needs to know that it
|
|
249
|
+
* does, not where, so any truthy value (including a path such as `'/api'`) turns it on.
|
|
250
|
+
*/
|
|
251
|
+
const SHOPIFY_ANALYTICS: Schema<ShopifyAnalyticsOptions> = {
|
|
252
|
+
consent: (value) => CONSENT_MODES.find((mode) => mode === value),
|
|
253
|
+
proxy: (value) => value !== undefined && Boolean(value),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const ANALYTICS: Schema<AnalyticsConfig> = {
|
|
257
|
+
dataLayer: when(isDataLayer),
|
|
258
|
+
shopify: (value) =>
|
|
259
|
+
typeof value === 'boolean'
|
|
260
|
+
? value
|
|
261
|
+
: (sectionOption('analytics.shopify', value, SHOPIFY_ANALYTICS, () => undefined) ?? undefined),
|
|
262
|
+
onEvent: hook,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param warn Says why an `account` without a usable `server` is off.
|
|
267
|
+
* @returns `null` without a usable `account.server`: nothing of the account part loads.
|
|
268
|
+
*/
|
|
269
|
+
export function accountOption(value: unknown, invalid: Invalid, warn: (message: string) => void): AccountConfig | null {
|
|
270
|
+
const account = sectionOption('account', value, ACCOUNT, invalid);
|
|
271
|
+
if (account === null) return null;
|
|
272
|
+
const { server } = account;
|
|
273
|
+
if (server === undefined) {
|
|
274
|
+
warn(ACCOUNT_NEEDS_SERVER);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return { ...account, server };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const marketsOption = (value: unknown, invalid: Invalid): MarketsConfig | null =>
|
|
281
|
+
sectionOption('markets', value, MARKETS, invalid);
|
|
282
|
+
|
|
283
|
+
export const analyticsOption = (value: unknown, invalid: Invalid): AnalyticsConfig | null =>
|
|
284
|
+
sectionOption('analytics', value, ANALYTICS, invalid);
|