@occluze/tether 0.7.0 → 0.7.2

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.
Files changed (139) hide show
  1. package/README.md +51 -109
  2. package/dist/esm/astro.js +1 -1
  3. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-W4FBRKXA.js} +1 -1
  4. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IM3OQTOP.js} +1 -1
  5. package/dist/esm/chunks/analytics-QDVOLZY3.js +1 -0
  6. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-AUVAN3KZ.js} +1 -1
  7. package/dist/esm/chunks/chunk-BMDYNMRC.js +1 -0
  8. package/dist/esm/chunks/chunk-CUL66KRA.js +1 -0
  9. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-CVNRODH3.js} +1 -1
  10. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-CXHPBT7N.js} +1 -1
  11. package/dist/esm/chunks/chunk-GF6JTI65.js +1 -0
  12. package/dist/esm/chunks/chunk-MRSDJPBC.js +1 -0
  13. package/dist/esm/chunks/chunk-NYDZPTLP.js +1 -0
  14. package/dist/esm/chunks/chunk-QDHLXXAZ.js +1 -0
  15. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-QUECUXHD.js} +1 -1
  16. package/dist/esm/chunks/chunk-RJY76ESL.js +1 -0
  17. package/dist/esm/chunks/chunk-RPI5C3EJ.js +1 -0
  18. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-YC4JXWRG.js} +2 -2
  19. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-3XHPZZNP.js} +1 -1
  20. package/dist/esm/chunks/extras-C724KKG6.js +1 -0
  21. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-WBWWQAGU.js} +1 -1
  22. package/dist/esm/chunks/{search-UJ73YBQ5.js → search-5NFIZBPJ.js} +1 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/tether.account-pages.js +13 -3
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +13 -3
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +18 -3
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +20 -5
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +52 -28
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +353 -288
  36. package/dist/tether.markets.js +31 -5
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +2 -2
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +18 -3
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  43. package/dist/types/src/cart/mutations.d.ts +6 -0
  44. package/dist/types/src/cart/store.d.ts +5 -2
  45. package/dist/types/src/cart.d.ts +3 -2
  46. package/dist/types/src/client.d.ts +61 -22
  47. package/dist/types/src/config.d.ts +6 -1
  48. package/dist/types/src/index.d.ts +2 -2
  49. package/dist/types/src/markets.d.ts +9 -2
  50. package/dist/types/src/product-shape.d.ts +3 -1
  51. package/dist/types/src/uri.d.ts +6 -0
  52. package/package.json +54 -6
  53. package/dist/esm/chunks/analytics-2V632IRV.js +0 -1
  54. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  55. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  56. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  57. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  58. package/dist/esm/chunks/chunk-E5YO6274.js +0 -1
  59. package/dist/esm/chunks/chunk-MPDWM2NN.js +0 -1
  60. package/dist/esm/chunks/chunk-YLUULATY.js +0 -1
  61. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  62. package/src/account/addresses-view.ts +0 -58
  63. package/src/account/answers.ts +0 -79
  64. package/src/account/markup.ts +0 -36
  65. package/src/account/operations.ts +0 -145
  66. package/src/account/order-lines.ts +0 -75
  67. package/src/account/order-list.ts +0 -107
  68. package/src/account/order-page.ts +0 -137
  69. package/src/account/page-answers.ts +0 -317
  70. package/src/account/page-events.ts +0 -253
  71. package/src/account/server-client.ts +0 -183
  72. package/src/account/subscriptions-view.ts +0 -115
  73. package/src/account-api.ts +0 -98
  74. package/src/account-marks.ts +0 -35
  75. package/src/account-pages.ts +0 -81
  76. package/src/account.ts +0 -284
  77. package/src/amounts.ts +0 -64
  78. package/src/analytics-shopify.ts +0 -246
  79. package/src/analytics.ts +0 -315
  80. package/src/api-version.ts +0 -79
  81. package/src/astro.ts +0 -272
  82. package/src/cart/cart-shape.ts +0 -25
  83. package/src/cart/checkout.ts +0 -101
  84. package/src/cart/drawer.ts +0 -125
  85. package/src/cart/mutations.ts +0 -237
  86. package/src/cart/render.ts +0 -266
  87. package/src/cart/store.ts +0 -279
  88. package/src/cart.ts +0 -345
  89. package/src/client.ts +0 -391
  90. package/src/collection.ts +0 -175
  91. package/src/config-sections.ts +0 -284
  92. package/src/config.ts +0 -480
  93. package/src/controls/cart-controls.ts +0 -123
  94. package/src/controls/discount.ts +0 -75
  95. package/src/controls/extras.ts +0 -54
  96. package/src/controls/navigation.ts +0 -77
  97. package/src/diagnose-markup.ts +0 -298
  98. package/src/diagnose.ts +0 -294
  99. package/src/dom.ts +0 -268
  100. package/src/extras.ts +0 -136
  101. package/src/fragments.ts +0 -261
  102. package/src/globals.d.ts +0 -15
  103. package/src/images.ts +0 -91
  104. package/src/index.ts +0 -415
  105. package/src/initial.ts +0 -101
  106. package/src/lazy-iife.ts +0 -197
  107. package/src/lazy.ts +0 -42
  108. package/src/loader.ts +0 -363
  109. package/src/market-codes.ts +0 -6
  110. package/src/markets-ui.ts +0 -227
  111. package/src/markets.ts +0 -246
  112. package/src/markup.ts +0 -127
  113. package/src/mounting.ts +0 -264
  114. package/src/page-market.ts +0 -110
  115. package/src/part-integrity.ts +0 -7
  116. package/src/parts/account-pages.ts +0 -7
  117. package/src/parts/account.ts +0 -7
  118. package/src/parts/analytics.ts +0 -7
  119. package/src/parts/diagnose.ts +0 -7
  120. package/src/parts/extras.ts +0 -7
  121. package/src/parts/markets.ts +0 -7
  122. package/src/parts/search.ts +0 -7
  123. package/src/presets/barba.ts +0 -194
  124. package/src/product-events.ts +0 -115
  125. package/src/product-render.ts +0 -218
  126. package/src/product-shape.ts +0 -49
  127. package/src/product-state.ts +0 -318
  128. package/src/product.ts +0 -308
  129. package/src/repeaters.ts +0 -161
  130. package/src/search.ts +0 -305
  131. package/src/selling-plans.ts +0 -145
  132. package/src/server.ts +0 -211
  133. package/src/shopify-values.ts +0 -91
  134. package/src/status.ts +0 -106
  135. package/src/storage.ts +0 -34
  136. package/src/store-html.ts +0 -359
  137. package/src/storefront/catalog.ts +0 -196
  138. package/src/storefront/json-ld.ts +0 -144
  139. package/src/variant-render.ts +0 -139
@@ -1,284 +0,0 @@
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);