@occluze/tether 0.7.0 → 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.
Files changed (132) hide show
  1. package/README.md +21 -21
  2. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-CXMA5RPZ.js} +1 -1
  3. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IZO7FEOZ.js} +1 -1
  4. package/dist/esm/chunks/{analytics-2V632IRV.js → analytics-PG5FPRDN.js} +1 -1
  5. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-4HD4J5JC.js} +1 -1
  6. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-6TFMLT4A.js} +2 -2
  7. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-BBVUVPMO.js} +1 -1
  8. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  9. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  10. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  11. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-N3V45JPS.js} +1 -1
  12. package/dist/esm/chunks/{chunk-YLUULATY.js → chunk-RPMBCAGS.js} +1 -1
  13. package/dist/esm/chunks/{chunk-MPDWM2NN.js → chunk-VFNHIGCS.js} +1 -1
  14. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-WT7RGGJU.js} +1 -1
  15. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  16. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  17. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-XCEBQKBO.js} +1 -1
  18. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  19. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-M4IQ62CT.js} +1 -1
  20. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/server.js +2 -2
  23. package/dist/tether.account-pages.js +13 -3
  24. package/dist/tether.account-pages.min.js +1 -1
  25. package/dist/tether.account.js +13 -3
  26. package/dist/tether.account.min.js +1 -1
  27. package/dist/tether.analytics.js +18 -3
  28. package/dist/tether.analytics.min.js +1 -1
  29. package/dist/tether.diagnose.js +18 -3
  30. package/dist/tether.diagnose.min.js +2 -2
  31. package/dist/tether.extras.js +19 -6
  32. package/dist/tether.extras.min.js +1 -1
  33. package/dist/tether.js +162 -112
  34. package/dist/tether.markets.js +18 -3
  35. package/dist/tether.markets.min.js +1 -1
  36. package/dist/tether.min.js +3 -3
  37. package/dist/tether.min.js.map +4 -4
  38. package/dist/tether.search.js +18 -3
  39. package/dist/tether.search.min.js +1 -1
  40. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  41. package/dist/types/src/cart/mutations.d.ts +6 -0
  42. package/dist/types/src/cart/store.d.ts +5 -2
  43. package/dist/types/src/cart.d.ts +2 -2
  44. package/dist/types/src/client.d.ts +53 -19
  45. package/dist/types/src/config.d.ts +6 -1
  46. package/dist/types/src/index.d.ts +2 -2
  47. package/dist/types/src/product-shape.d.ts +3 -1
  48. package/package.json +1 -2
  49. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  50. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  51. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  52. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  53. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  54. package/dist/esm/chunks/search-UJ73YBQ5.js +0 -1
  55. package/src/account/addresses-view.ts +0 -58
  56. package/src/account/answers.ts +0 -79
  57. package/src/account/markup.ts +0 -36
  58. package/src/account/operations.ts +0 -145
  59. package/src/account/order-lines.ts +0 -75
  60. package/src/account/order-list.ts +0 -107
  61. package/src/account/order-page.ts +0 -137
  62. package/src/account/page-answers.ts +0 -317
  63. package/src/account/page-events.ts +0 -253
  64. package/src/account/server-client.ts +0 -183
  65. package/src/account/subscriptions-view.ts +0 -115
  66. package/src/account-api.ts +0 -98
  67. package/src/account-marks.ts +0 -35
  68. package/src/account-pages.ts +0 -81
  69. package/src/account.ts +0 -284
  70. package/src/amounts.ts +0 -64
  71. package/src/analytics-shopify.ts +0 -246
  72. package/src/analytics.ts +0 -315
  73. package/src/api-version.ts +0 -79
  74. package/src/astro.ts +0 -272
  75. package/src/cart/cart-shape.ts +0 -25
  76. package/src/cart/checkout.ts +0 -101
  77. package/src/cart/drawer.ts +0 -125
  78. package/src/cart/mutations.ts +0 -237
  79. package/src/cart/render.ts +0 -266
  80. package/src/cart/store.ts +0 -279
  81. package/src/cart.ts +0 -345
  82. package/src/client.ts +0 -391
  83. package/src/collection.ts +0 -175
  84. package/src/config-sections.ts +0 -284
  85. package/src/config.ts +0 -480
  86. package/src/controls/cart-controls.ts +0 -123
  87. package/src/controls/discount.ts +0 -75
  88. package/src/controls/extras.ts +0 -54
  89. package/src/controls/navigation.ts +0 -77
  90. package/src/diagnose-markup.ts +0 -298
  91. package/src/diagnose.ts +0 -294
  92. package/src/dom.ts +0 -268
  93. package/src/extras.ts +0 -136
  94. package/src/fragments.ts +0 -261
  95. package/src/globals.d.ts +0 -15
  96. package/src/images.ts +0 -91
  97. package/src/index.ts +0 -415
  98. package/src/initial.ts +0 -101
  99. package/src/lazy-iife.ts +0 -197
  100. package/src/lazy.ts +0 -42
  101. package/src/loader.ts +0 -363
  102. package/src/market-codes.ts +0 -6
  103. package/src/markets-ui.ts +0 -227
  104. package/src/markets.ts +0 -246
  105. package/src/markup.ts +0 -127
  106. package/src/mounting.ts +0 -264
  107. package/src/page-market.ts +0 -110
  108. package/src/part-integrity.ts +0 -7
  109. package/src/parts/account-pages.ts +0 -7
  110. package/src/parts/account.ts +0 -7
  111. package/src/parts/analytics.ts +0 -7
  112. package/src/parts/diagnose.ts +0 -7
  113. package/src/parts/extras.ts +0 -7
  114. package/src/parts/markets.ts +0 -7
  115. package/src/parts/search.ts +0 -7
  116. package/src/presets/barba.ts +0 -194
  117. package/src/product-events.ts +0 -115
  118. package/src/product-render.ts +0 -218
  119. package/src/product-shape.ts +0 -49
  120. package/src/product-state.ts +0 -318
  121. package/src/product.ts +0 -308
  122. package/src/repeaters.ts +0 -161
  123. package/src/search.ts +0 -305
  124. package/src/selling-plans.ts +0 -145
  125. package/src/server.ts +0 -211
  126. package/src/shopify-values.ts +0 -91
  127. package/src/status.ts +0 -106
  128. package/src/storage.ts +0 -34
  129. package/src/store-html.ts +0 -359
  130. package/src/storefront/catalog.ts +0 -196
  131. package/src/storefront/json-ld.ts +0 -144
  132. package/src/variant-render.ts +0 -139
package/src/server.ts DELETED
@@ -1,211 +0,0 @@
1
- /**
2
- * `@occluze/tether/server`: Shopify product data at build time or on the server (Astro, Eleventy, Node, Cloudflare
3
- * Workers). No browser APIs. It sends the browser script's own queries, so products handed over with `hydrate()` are
4
- * exactly what the script expects and need no request of their own.
5
- *
6
- * import { createStorefront } from '@occluze/tether/server';
7
- * const shop = createStorefront({ domain, token, country: 'NL' });
8
- * const product = await shop.getProduct('coffee-box');
9
- * const all = await shop.allProducts();
10
- */
11
- import { DEFAULTS, resolveConfig } from './config.ts';
12
- import { request, hasData, withContext, TetherError } from './client.ts';
13
- import * as plans from './selling-plans.ts';
14
- import { imageUrl, formatPercent } from './dom.ts';
15
- import { initialState } from './initial.ts';
16
- import { STORE_ENV_NAMES, formatMoney, productKey, productUrl as productUrlFor } from './shopify-values.ts';
17
- import { createCatalog } from './storefront/catalog.ts';
18
- import { jsonLdData, scriptJson } from './storefront/json-ld.ts';
19
- import type { TetherConfig, TetherOptions } from './config.ts';
20
- import type { Product } from './fragments.ts';
21
- import type { Money } from './shopify-values.ts';
22
- import type { InitialOptions, InitialState } from './initial.ts';
23
- import type { JsonLdOptions } from './storefront/json-ld.ts';
24
-
25
- export { imageUrl, initialState, formatPercent, formatMoney, productKey };
26
-
27
- export { TetherError, plans, withContext };
28
-
29
- // The types a site uses by name, under the names the docs give them.
30
- export type { StorefrontOptions, HydrateOptions };
31
- export type { Money } from './shopify-values.ts';
32
- export type { Product as ShopifyProduct, ProductVariant as ShopifyVariant } from './fragments.ts';
33
- export type { ShopifyCart, ShopifyCartLine } from './cart/store.ts';
34
- export type { Collection as ShopifyCollection, ProductQuery } from './loader.ts';
35
- export type { InitialOptions, InitialState } from './initial.ts';
36
- export type { ImageSize } from './images.ts';
37
- export type { JsonLdOptions } from './storefront/json-ld.ts';
38
- /** What `createStorefront()` returns. */
39
- export type Storefront = ReturnType<typeof createStorefront>;
40
-
41
- /** The options only a storefront has. */
42
- interface ServerOptions {
43
- /** Replaces the global `fetch` (tests, caching). */
44
- fetch?: typeof fetch;
45
- /** Products per request for `allProducts()` (default 25). */
46
- pageSize?: number;
47
- /** The site's address, for product URLs in `jsonLd()` without `url`. */
48
- siteUrl?: string;
49
- }
50
-
51
- /**
52
- * The config options, or an `import.meta.env` spread into them: its `PUBLIC_SHOPIFY_*` names give the domain and
53
- * token, its other keys are ignored.
54
- */
55
- type StorefrontOptions = TetherOptions & ServerOptions & StoreEnv;
56
-
57
- /** The environment names that give the domain and the token. */
58
- type StoreEnv = Partial<Record<(typeof STORE_ENV_NAMES)['domain' | 'token'][number], string>>;
59
-
60
- type ServerConfig = TetherConfig & ServerOptions;
61
-
62
- interface HydrateOptions {
63
- card?: boolean;
64
- images?: number;
65
- }
66
-
67
- const DEFAULT_PAGE_SIZE = 25;
68
- /** The images a card keeps: the main one and a hover image (`tether-bind="image:2"`). */
69
- const DEFAULT_CARD_IMAGES = 2;
70
- /** The version of the `data-tether-hydrate` payload, for the browser script reading it. */
71
- const HYDRATE_VERSION = 1;
72
-
73
- /**
74
- * @returns `productPath` `/products` → `/products/<handle>`; templates with `{handle}` and `{id}` work too. `null` when
75
- * the path needs a handle or an id that is empty.
76
- */
77
- export function productUrl(
78
- handle: string,
79
- { productPath = DEFAULTS.productPath, id }: { productPath?: string; id?: string } = {},
80
- ): string | null {
81
- return productUrlFor(productPath, handle, id);
82
- }
83
-
84
- /** @returns The first of the `names` set in `options`, `''` without one. */
85
- function fromEnv(options: StoreEnv, names: readonly (keyof StoreEnv)[]): string {
86
- for (const name of names) {
87
- const value = options[name];
88
- if (typeof value === 'string' && value) return value;
89
- }
90
- return '';
91
- }
92
-
93
- /**
94
- * @returns The config options of `options`, resolved and validated as the browser's are, with the domain and token
95
- * from the environment names when they aren't set.
96
- */
97
- function storefrontConfig(options: StorefrontOptions): ServerConfig {
98
- const known: Record<string, unknown> = {};
99
- for (const [key, value] of Object.entries(options)) {
100
- if (key in DEFAULTS) known[key] = value;
101
- }
102
- const domain = typeof options.domain === 'string' && options.domain ? options.domain : '';
103
- const token = typeof options.token === 'string' && options.token ? options.token : '';
104
- const conf: ServerConfig = resolveConfig({
105
- // Only config keys, each parsed by `resolveConfig`.
106
- ...known,
107
- domain: domain || fromEnv(options, STORE_ENV_NAMES.domain),
108
- token: token || fromEnv(options, STORE_ENV_NAMES.token),
109
- });
110
- if (options.fetch) conf.fetch = options.fetch;
111
- if (options.pageSize !== undefined) conf.pageSize = options.pageSize;
112
- if (options.siteUrl !== undefined) conf.siteUrl = options.siteUrl;
113
- return conf;
114
- }
115
-
116
- /** @returns `pageSize` when it is a positive integer, else the default. */
117
- const validPageSize = (pageSize: number | undefined): number =>
118
- typeof pageSize === 'number' && Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE;
119
-
120
- /**
121
- * A product for a list (home, collections, recommendations): without descriptions and variant images, and only its
122
- * first `images` images. Prices, stock and plans per variant stay.
123
- */
124
- function cardOf(product: Product, images: number): Product {
125
- return {
126
- ...product,
127
- description: '',
128
- descriptionHtml: '',
129
- images: { nodes: product.images.nodes.slice(0, Math.max(1, images)) },
130
- variants: { nodes: product.variants.nodes.map((variant) => ({ ...variant, image: null })) },
131
- };
132
- }
133
-
134
- /**
135
- * A storefront: the store's products and collections for the server, and the tags that hand them to the page.
136
- *
137
- * @throws {TetherError} `NOT_CONFIGURED` without a domain and a token.
138
- */
139
- export function createStorefront(options: StorefrontOptions = {}) {
140
- const conf = storefrontConfig(options);
141
- if (!conf.domain || !conf.token) {
142
- throw new TetherError(
143
- 'createStorefront needs domain and token: pass them, or import.meta.env with PUBLIC_SHOPIFY_DOMAIN and PUBLIC_SHOPIFY_STOREFRONT_TOKEN',
144
- { code: 'NOT_CONFIGURED' },
145
- );
146
- }
147
- const catalog = createCatalog(conf, validPageSize(conf.pageSize));
148
- const site = { productPath: conf.productPath, siteUrl: conf.siteUrl };
149
-
150
- /**
151
- * A `<script type="application/json" data-tether-hydrate>` tag that hands products to the browser script: it renders
152
- * them at once, without a request of its own, then refreshes prices and stock once in the background. Astro:
153
- * `<Fragment set:html={shop.hydrate(products)} />`.
154
- *
155
- * @param options `card: true` for lists: about 40% smaller (see `cardOf`). The full product comes with the
156
- * background refresh.
157
- */
158
- function hydrate(
159
- products: Product | null | undefined | (Product | null | undefined)[],
160
- { card = false, images = DEFAULT_CARD_IMAGES }: HydrateOptions = {},
161
- ): string {
162
- const present = (Array.isArray(products) ? products : [products]).filter((product) => product != null);
163
- const payload = {
164
- v: HYDRATE_VERSION,
165
- at: new Date().toISOString(),
166
- country: conf.country || null,
167
- language: conf.language || null,
168
- products: card ? present.map((product) => cardOf(product, images)) : present,
169
- };
170
- return `<script type="application/json" data-tether-hydrate>${scriptJson(payload)}</script>`;
171
- }
172
-
173
- /** @returns The `<script type="application/ld+json">` tag, `''` without a product. */
174
- function jsonLd(product: Product | null | undefined, jsonLdOptions: JsonLdOptions = {}): string {
175
- if (!product) return '';
176
- return `<script type="application/ld+json">${scriptJson(jsonLdData(product, jsonLdOptions, site))}</script>`;
177
- }
178
-
179
- /** `initialState()` with the storefront's `defaultPurchase` and `lowStock`. */
180
- function storefrontInitialState(product: Product, initialOptions?: InitialOptions): InitialState;
181
- function storefrontInitialState(
182
- product: Product | null | undefined,
183
- initialOptions?: InitialOptions,
184
- ): InitialState | null;
185
- function storefrontInitialState(
186
- product: Product | null | undefined,
187
- initialOptions: InitialOptions = {},
188
- ): InitialState | null {
189
- return initialState(product, { defaultPurchase: conf.defaultPurchase, lowStock: conf.lowStock, ...initialOptions });
190
- }
191
-
192
- return {
193
- config: conf,
194
- query: <T>(query: string, variables?: Record<string, unknown>, requestOptions?: { context?: boolean }) =>
195
- // The caller's own query text decides the shape of `data`, and the caller's `T` states it.
196
- request(conf, query, variables ?? {}, hasData, requestOptions) as Promise<T>,
197
- ...catalog,
198
- hydrate,
199
- formatMoney: (money: Money | null | undefined, locale: string | undefined = conf.locale) =>
200
- formatMoney(money, locale),
201
- formatPercent: (percentage: number, locale: string | undefined = conf.locale) => formatPercent(percentage, locale),
202
- imageUrl,
203
- jsonLd,
204
- jsonLdData: (product: Product | null | undefined, jsonLdOptions: JsonLdOptions = {}) =>
205
- jsonLdData(product, jsonLdOptions, site),
206
- initialState: storefrontInitialState,
207
- productUrl: (handle: string, urlOptions: { productPath?: string; id?: string } = {}) =>
208
- productUrl(handle, { productPath: conf.productPath, ...urlOptions }),
209
- plans,
210
- };
211
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Conversions of Shopify values that the browser script and the server helpers (`src/server.ts`, `src/astro.ts`)
3
- * share, so a server-rendered page and the script that takes it over agree. Pure: no config, no DOM.
4
- */
5
-
6
- /** A Storefront API `MoneyV2`. */
7
- export interface Money {
8
- amount: string;
9
- currencyCode: string;
10
- }
11
-
12
- /** The `.env` names a store's domain and public Storefront token are read from, first match wins. */
13
- export const STORE_ENV_NAMES = {
14
- domain: ['PUBLIC_SHOPIFY_DOMAIN', 'PUBLIC_SHOPIFY_STORE_DOMAIN', 'SHOPIFY_DOMAIN'],
15
- token: ['PUBLIC_SHOPIFY_STOREFRONT_TOKEN', 'PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN', 'SHOPIFY_STOREFRONT_TOKEN'],
16
- } as const;
17
-
18
- /**
19
- * True for an absolute https URL. Links and redirects that come from Shopify (checkout, status pages,
20
- * tracking) are only used when this holds.
21
- */
22
- export function isHttpsUrl(url: unknown): url is string {
23
- if (typeof url !== 'string') return false;
24
- try {
25
- return new URL(url).protocol === 'https:';
26
- } catch {
27
- // Not an absolute URL, so not an https one.
28
- return false;
29
- }
30
- }
31
-
32
- /**
33
- * True for a token Shopify issues as a secret (`shpat_` Admin, `shpss_` shared secret, `shpca_` client credentials,
34
- * `shppa_` private app, `shpua_` user). The public Storefront token has no such prefix. A secret token must never
35
- * reach a page.
36
- */
37
- export function isSecretShopifyToken(token: string): boolean {
38
- return /^shp[a-z]{2}_/.test(token.trim());
39
- }
40
-
41
- /**
42
- * Money as text in `locale`, e.g. `{ amount: '30.0', currencyCode: 'EUR' }` in `nl-NL` → `€ 30,00`.
43
- *
44
- * @param locale An `Intl` locale; the runtime's own when absent.
45
- * @returns `''` without money or with an amount that isn't a number.
46
- */
47
- export function formatMoney(money: Money | null | undefined, locale: string | undefined): string {
48
- if (!money) return '';
49
- const amount = parseFloat(money.amount);
50
- if (Number.isNaN(amount)) return '';
51
- try {
52
- return new Intl.NumberFormat(locale || undefined, { style: 'currency', currency: money.currencyCode }).format(
53
- amount,
54
- );
55
- } catch {
56
- // A currency code Intl doesn't know.
57
- return `${money.currencyCode} ${amount.toFixed(2)}`;
58
- }
59
- }
60
-
61
- /**
62
- * A product page's path. `productPath` is a folder (`/product` → `/product/<handle>`) or a template with `{handle}`
63
- * and `{id}` (the numeric product id), e.g. `/product?handle={handle}`.
64
- *
65
- * @param id A product gid or numeric id.
66
- * @returns `null` when the path needs a handle or an id the product lacks: no link beats a wrong one.
67
- */
68
- export function productUrl(productPath: string, handle: string | null | undefined, id?: string | null): string | null {
69
- if (!/\{(handle|id)\}/.test(productPath)) {
70
- return handle ? `${productPath.replace(/\/$/, '')}/${encodeURIComponent(handle)}` : null;
71
- }
72
- const numericId = id ? (id.split('/').at(-1) ?? '') : '';
73
- if (productPath.includes('{handle}') && !handle) return null;
74
- if (productPath.includes('{id}') && !numericId) return null;
75
- return productPath
76
- .replace(/\{handle\}/g, encodeURIComponent(handle ?? ''))
77
- .replace(/\{id\}/g, encodeURIComponent(numericId));
78
- }
79
-
80
- /**
81
- * The key Tether loads a product by: a handle, or a product gid.
82
- *
83
- * @param raw A handle, a numeric id, a gid or a product URL (`…/products/<handle>`).
84
- */
85
- export function productKey(raw: string | number | null | undefined): string | null {
86
- const value = String(raw ?? '').trim();
87
- if (!value) return null;
88
- if (value.startsWith('gid://')) return value;
89
- if (/^\d+$/.test(value)) return `gid://shopify/Product/${value}`;
90
- return /\/products\/([^/?#]+)/.exec(value)?.[1] ?? value;
91
- }
package/src/status.ts DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * Where a product scope gets its key from, and the state of a scope (loading, ready, error, not found), so a failing
3
- * API never leaves a broken page.
4
- */
5
- import { attr, sel, get, scoped, show } from './dom.ts';
6
-
7
- interface ProductKeySource {
8
- /** The product to load; `null` when the attribute or the page URL has none. */
9
- key: string | null;
10
- /** The key comes from the page URL (`url:…`). */
11
- fromUrl: boolean;
12
- /** The attribute's value as written. */
13
- raw: string;
14
- }
15
-
16
- type ScopeStatus = 'loading' | 'ready' | 'error' | 'not-found';
17
-
18
- const URL_SOURCE = 'url:';
19
-
20
- /** A path segment as text. A malformed escape (`%E0%A4%A`) is kept as written, which is how the browser shows it. */
21
- function decodePathSegment(segment: string): string {
22
- try {
23
- return decodeURIComponent(segment);
24
- } catch (error) {
25
- if (error instanceof URIError) return segment;
26
- throw error;
27
- }
28
- }
29
-
30
- /**
31
- * Reads a `tether-product` value:
32
- * - `coffee-box`: a handle, numeric id, gid or product URL;
33
- * - `url:handle`: `?handle=` from the page URL (any parameter: `url:p` reads `?p=`);
34
- * - `url:path`: the last segment of the page path (`/shop/coffee-box` → `coffee-box`).
35
- *
36
- * @param name The attribute, without prefix.
37
- */
38
- export function resolveProductKey(
39
- el: Element,
40
- location: Pick<Location, 'pathname' | 'search'> = window.location,
41
- name = 'product',
42
- ): ProductKeySource {
43
- const raw = (get(el, name) ?? '').trim();
44
- if (!raw.toLowerCase().startsWith(URL_SOURCE)) return { key: raw || null, fromUrl: false, raw };
45
- const source = raw.slice(URL_SOURCE.length).trim();
46
- let key;
47
- if (source.toLowerCase() === 'path') {
48
- const segments = location.pathname.split('/').filter(Boolean);
49
- const segment = segments[segments.length - 1];
50
- key = segment === undefined ? null : decodePathSegment(segment);
51
- } else {
52
- key = new URLSearchParams(location.search).get(source || 'handle');
53
- }
54
- return { key: key?.trim() || null, fromUrl: true, raw };
55
- }
56
-
57
- /** The values of `tether-show` that follow the scope's state. */
58
- export const STATUS_KEYS: readonly ScopeStatus[] = ['loading', 'ready', 'error', 'not-found'];
59
-
60
- const isScopeStatus = (value: unknown): value is ScopeStatus => STATUS_KEYS.some((status) => status === value);
61
-
62
- /** @returns The error's `code` (a `TetherError` has one). */
63
- const codeOf = (error: unknown): unknown =>
64
- typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
65
-
66
- /** @returns True for a `TetherError` with code `NOT_FOUND`. */
67
- export const isNotFound = (error: unknown): boolean => codeOf(error) === 'NOT_FOUND';
68
-
69
- /** @returns The selector of every scope that owns its state elements; the nearest one wins. */
70
- export const boundary = (): string =>
71
- ['product', 'collection', 'products', 'recommendations'].map((name) => sel(name)).join(',');
72
-
73
- /** @returns True when `el` belongs to the scope `root`, not to a scope nested in it. */
74
- export const ownedBy = (el: Element, root: Element): boolean =>
75
- el === root || el.parentElement?.closest(boundary()) === root;
76
-
77
- /** @returns The error's code for `tether-error`, `error` without one. */
78
- function errorCode(error: unknown): string {
79
- const code = codeOf(error);
80
- return typeof code === 'string' && code ? code : 'error';
81
- }
82
-
83
- /**
84
- * Sets `tether-state` (loading | ready | error) and `tether-error` (not-found or an error code) on `root`, toggles
85
- * `tether-show="loading|ready|error|not-found"` inside it, and disables its add-to-cart buttons when there's no
86
- * product.
87
- */
88
- export function setScopeStatus(root: Element, status: ScopeStatus, error?: unknown): void {
89
- const notFound = status === 'not-found';
90
- const failed = status === 'error' || notFound;
91
- root.setAttribute(attr('state'), notFound ? 'error' : status);
92
- if (failed) root.setAttribute(attr('error'), notFound ? 'not-found' : errorCode(error));
93
- else root.removeAttribute(attr('error'));
94
-
95
- scoped(root, sel('show'), boundary()).forEach((el) => {
96
- const shows = get(el, 'show');
97
- if (isScopeStatus(shows)) show(el, shows === status);
98
- });
99
-
100
- if (!failed) return;
101
- scoped(root, sel('add-to-cart'), boundary()).forEach((button) => {
102
- button.classList.add('is-disabled');
103
- button.setAttribute('aria-disabled', 'true');
104
- if ('disabled' in button) button.disabled = true;
105
- });
106
- }
package/src/storage.ts DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * The browser's `localStorage` and `sessionStorage`, which may be blocked: accessing them throws in private mode, in
3
- * sandboxed frames and where the visitor blocks site data, and writing throws when they are full. Tether then keeps
4
- * nothing between pages: a read finds nothing, and a write or removal does nothing. Each call catches on its own, so a
5
- * blocked area never stops a call on the other.
6
- */
7
-
8
- type StorageArea = 'localStorage' | 'sessionStorage';
9
-
10
- /** @returns `null` when nothing is stored under `key`, or the storage is blocked. */
11
- export function getStored(key: string, area: StorageArea = 'localStorage'): string | null {
12
- try {
13
- return window[area].getItem(key);
14
- } catch {
15
- // Blocked storage holds nothing: see the file's comment.
16
- return null;
17
- }
18
- }
19
-
20
- export function setStored(key: string, value: string, area: StorageArea = 'localStorage'): void {
21
- try {
22
- window[area].setItem(key, value);
23
- } catch {
24
- // Blocked or full storage keeps nothing: see the file's comment.
25
- }
26
- }
27
-
28
- export function removeStored(key: string, area: StorageArea = 'localStorage'): void {
29
- try {
30
- window[area].removeItem(key);
31
- } catch {
32
- // Blocked storage has nothing to remove: see the file's comment.
33
- }
34
- }