@occluze/tether 0.6.7 → 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.
Files changed (135) hide show
  1. package/README.md +3 -3
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
  4. package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
  5. package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
  6. package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
  7. package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
  8. package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
  9. package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
  10. package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
  11. package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
  12. package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
  13. package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
  14. package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
  15. package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
  16. package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
  17. package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
  18. package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
  19. package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
  20. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
  21. package/dist/esm/chunks/search-OU75RNQL.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -1
  24. package/dist/presets/barba.esm.js +56 -36
  25. package/dist/tether.account-pages.js +773 -442
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +648 -318
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +318 -191
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +331 -242
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +78 -33
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +3176 -2015
  36. package/dist/tether.markets.js +231 -126
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +221 -126
  41. package/dist/tether.search.min.js +1 -1
  42. package/package.json +23 -4
  43. package/src/account/addresses-view.js +92 -0
  44. package/src/account/browser-session.js +359 -0
  45. package/src/account/markup.js +48 -0
  46. package/src/account/operations.js +180 -0
  47. package/src/account/order-lines.js +96 -0
  48. package/src/account/order-list.js +91 -0
  49. package/src/account/order-page.js +192 -0
  50. package/src/account/page-events.js +288 -0
  51. package/src/account/server-client.js +172 -0
  52. package/src/account/shared.js +111 -0
  53. package/src/account/shopify-auth.js +150 -0
  54. package/src/account/subscriptions-view.js +158 -0
  55. package/src/account-api.js +118 -0
  56. package/src/account-links.js +35 -0
  57. package/src/account-pages.js +69 -298
  58. package/src/account.js +280 -323
  59. package/src/amounts.js +36 -0
  60. package/src/analytics-shopify.js +241 -0
  61. package/src/analytics.js +304 -184
  62. package/src/api-version.js +75 -25
  63. package/src/astro.js +233 -68
  64. package/src/cart/checkout.js +97 -0
  65. package/src/cart/drawer.js +138 -0
  66. package/src/cart/mutations.js +234 -0
  67. package/src/cart/render.js +294 -0
  68. package/src/cart/store.js +326 -0
  69. package/src/cart.js +320 -488
  70. package/src/client.js +214 -78
  71. package/src/collection.js +126 -57
  72. package/src/config.js +323 -59
  73. package/src/controls/cart-controls.js +155 -0
  74. package/src/controls/discount.js +96 -0
  75. package/src/controls/extras.js +63 -0
  76. package/src/controls/navigation.js +83 -0
  77. package/src/diagnose-markup.js +333 -0
  78. package/src/diagnose.js +251 -202
  79. package/src/dom.js +219 -220
  80. package/src/extras.js +119 -37
  81. package/src/fragments.js +115 -22
  82. package/src/images.js +104 -0
  83. package/src/index.js +245 -287
  84. package/src/initial.js +78 -66
  85. package/src/lazy-iife.js +92 -22
  86. package/src/lazy.js +25 -4
  87. package/src/loader.js +263 -124
  88. package/src/market-codes.js +35 -0
  89. package/src/markets-ui.js +169 -55
  90. package/src/markets.js +286 -85
  91. package/src/markup.js +64 -0
  92. package/src/mounting.js +214 -91
  93. package/src/page-market.js +116 -0
  94. package/src/parts/account-pages.js +4 -1
  95. package/src/parts/account.js +4 -1
  96. package/src/parts/analytics.js +4 -1
  97. package/src/parts/diagnose.js +4 -1
  98. package/src/parts/extras.js +4 -1
  99. package/src/parts/markets.js +4 -1
  100. package/src/parts/search.js +4 -1
  101. package/src/presets/barba.js +120 -30
  102. package/src/product-events.js +130 -0
  103. package/src/product-render.js +257 -0
  104. package/src/product-state.js +318 -0
  105. package/src/product.js +233 -459
  106. package/src/repeaters.js +173 -0
  107. package/src/search.js +277 -107
  108. package/src/selling-plans.js +107 -47
  109. package/src/server.js +168 -205
  110. package/src/shopify-values.js +89 -0
  111. package/src/status.js +99 -39
  112. package/src/store-html.js +0 -0
  113. package/src/storefront/catalog.js +199 -0
  114. package/src/storefront/json-ld.js +167 -0
  115. package/src/variant-render.js +165 -0
  116. package/types/astro.d.ts +13 -2
  117. package/types/index.d.ts +178 -27
  118. package/types/server.d.ts +89 -17
  119. package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
  120. package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
  121. package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
  122. package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
  123. package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
  124. package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
  125. package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
  126. package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
  127. package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
  128. package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
  129. package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
  130. package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
  131. package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
  132. package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
  133. package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
  134. package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
  135. package/dist/esm/chunks/search-NILMGYXG.js +0 -1
package/src/initial.js CHANGED
@@ -1,75 +1,87 @@
1
1
  /**
2
- * What the browser script will show first for a product, so a server can render the same
3
- * (no flash when Tether takes over): the selected variant, price, purchase mode and conditions.
4
- * Same rules as src/product.js: first available variant (or the one in ?variant=), subscription
5
- * when the product requires a plan or defaultPurchase is 'subscription', low stock at 5 or fewer.
2
+ * What the browser script shows first for a product, as plain data, so a server renders the same and nothing flashes
3
+ * when Tether takes over. Public as `initialState()` of `@occluze/tether/server`; the rules are `product-state.js`'s,
4
+ * the ones the browser scope uses.
6
5
  */
7
- import { GID, allPlans, allocationFor, findPlan, planPrice, planSavings } from './selling-plans.js';
6
+ import { DEFAULTS } from './config.js';
7
+ import { initialChoice, isValueAvailable, productView } from './product-state.js';
8
+ import { allPlans, allocationFor, planPrice, planSavings } from './selling-plans.js';
9
+ /** @import { Money } from './shopify-values.js' */
10
+ /** @import { Product, ProductVariant } from './fragments.js' */
11
+ /** @import { PlanInGroup } from './selling-plans.js' */
12
+ /** @import { Selection, ShowCondition } from './product-state.js' */
8
13
 
9
- export function initialState(product, { variant, sellingPlan, defaultPurchase = 'one-time', lowStock = 5 } = {}) {
10
- const vs = product?.variants?.nodes || [];
11
- const gid = variant ? GID.variant(variant) : null;
12
- const v = (gid && vs.find((x) => x.id === gid)) || vs.find((x) => x.availableForSale) || vs[0] || null;
13
- if (!v) return null;
14
- const selected = Object.fromEntries((v.selectedOptions || []).map((o) => [o.name, o.value]));
15
-
16
- const plans = allPlans(product);
17
- const fromUrl = findPlan(product, sellingPlan);
18
- let plan = plans.length ? (fromUrl || plans[0]) : null;
19
- if (plan && !allocationFor(v, plan.id)) plan = plans.find((p) => allocationFor(v, p.id)) || plan;
20
- const wantsSub = product.requiresSellingPlan || fromUrl || defaultPurchase === 'subscription';
21
- const subscriptionAvailable = plans.some((p) => allocationFor(v, p.id));
22
- const purchase = plans.length && wantsSub ? 'subscription' : 'one-time';
23
- const sub = purchase === 'subscription' && subscriptionAvailable;
24
-
25
- const price = sub ? planPrice(v, plan.id) : v.price;
26
- const compareAt = sub ? (allocationFor(v, plan.id)?.priceAdjustments?.[0]?.compareAtPrice || v.compareAtPrice) : v.compareAtPrice;
27
- const onSale = Boolean(compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount));
28
- const q = v.quantityAvailable;
29
- const low = q != null && q > 0 && q <= lowStock;
30
- const savingsNow = sub ? planSavings(v, plan.id).percentage : 0;
14
+ /**
15
+ * @typedef {object} InitialOptions
16
+ * @property {string | null | undefined} [variant] The page's `?variant=`; the first variant for sale without.
17
+ * @property {string | null | undefined} [sellingPlan] The page's `?selling_plan=`: subscribes on that plan.
18
+ * @property {string | null | undefined} [defaultPurchase] `subscription` subscribes on the first plan.
19
+ * @property {number | undefined} [lowStock] `low-stock` at this many or fewer.
20
+ */
21
+ /**
22
+ * @typedef {object} InitialPlan A plan in the plan list (`tether-plan-list`).
23
+ * @property {string} id
24
+ * @property {string} name
25
+ * @property {Money | null} price On this plan, for the variant.
26
+ * @property {number} savings In %, 0 without savings.
27
+ * @property {boolean} selected `is-selected`: the line is bought on this plan.
28
+ * @property {boolean} available The variant is on this plan; `is-unavailable` when not.
29
+ */
30
+ /**
31
+ * @typedef {object} InitialState
32
+ * @property {ProductVariant} variant
33
+ * @property {Selection} selected Option name → value of the variant: `is-selected` on those buttons.
34
+ * @property {'one-time' | 'subscription'} purchase
35
+ * @property {PlanInGroup | null} plan The plan the line is bought on, `null` when bought once.
36
+ * @property {InitialPlan[]} plans
37
+ * @property {number} savings The offered plan's savings in % (`tether-bind="subscription-savings"`), 0 without.
38
+ * @property {Money} price
39
+ * @property {Money | null} compareAtPrice Only when on sale.
40
+ * @property {boolean} available
41
+ * @property {Record<ShowCondition, boolean>} show The `tether-show` conditions: `hidden={!state.show['out-of-stock']}`.
42
+ * @property {Record<string, string[]>} unavailable Per option, the values without a variant for sale (given the other
43
+ * selected values), as plain data for frameworks that serialise props.
44
+ * @property {(name: string, value: string) => boolean} isUnavailable The same as a function: `is-unavailable`.
45
+ */
31
46
 
47
+ /**
48
+ * @param {Product | null | undefined} product
49
+ * @param {InitialOptions} [options]
50
+ * @returns {InitialState | null} `null` without a product or variants.
51
+ */
52
+ export function initialState(product, options = {}) {
53
+ if (!product) return null;
54
+ const { variant, sellingPlan, defaultPurchase = DEFAULTS.defaultPurchase, lowStock = DEFAULTS.lowStock } = options;
55
+ const choice = initialChoice(product, { variant, sellingPlan, defaultPurchase });
56
+ const view = productView(product, { ...choice, quantity: 1 }, lowStock);
57
+ if (!view) return null;
58
+ const { selected, isSubscription } = view;
59
+ const isUnavailable = (/** @type {string} */ name, /** @type {string} */ value) =>
60
+ !isValueAvailable(product, selected, name, value);
32
61
  return {
33
- variant: v,
62
+ variant: view.variant,
34
63
  selected,
35
- purchase,
36
- plan: sub ? plan : null,
37
- /** The plan list (tether-plan-list="all"): selected one gets is-selected, unavailable ones is-unavailable. */
38
- plans: plans.map((p) => ({
39
- id: p.id, name: p.name, price: planPrice(v, p.id), savings: planSavings(v, p.id).percentage,
40
- selected: sub && p.id === plan.id, available: Boolean(allocationFor(v, p.id)),
64
+ purchase: view.purchase.kind,
65
+ plan: isSubscription ? view.plan : null,
66
+ plans: allPlans(product).map((plan) => ({
67
+ id: plan.id,
68
+ name: plan.name,
69
+ price: planPrice(view.variant, plan.id),
70
+ savings: planSavings(view.variant, plan.id).percentage,
71
+ selected: isSubscription && plan.id === view.purchase.planId,
72
+ available: Boolean(allocationFor(view.variant, plan.id)),
41
73
  })),
42
- /** Savings of the selected plan in % (tether-bind="subscription-savings"), 0 without one. */
43
- savings: plan ? planSavings(v, plan.id).percentage : 0,
44
- price,
45
- compareAtPrice: onSale ? compareAt : null,
46
- available: v.availableForSale,
47
- /** The tether-show conditions, e.g. hidden={!state.show['out-of-stock']}. */
48
- show: {
49
- 'in-stock': v.availableForSale,
50
- 'out-of-stock': !v.availableForSale,
51
- backorder: Boolean(v.availableForSale && v.currentlyNotInStock),
52
- 'on-sale': onSale,
53
- 'not-on-sale': !onSale,
54
- 'has-variants': vs.length > 1,
55
- 'single-variant': vs.length === 1,
56
- 'low-stock': low,
57
- 'plenty-in-stock': v.availableForSale && !low,
58
- 'subscription-available': subscriptionAvailable,
59
- 'subscription-unavailable': !subscriptionAvailable,
60
- 'subscription-only': Boolean(product.requiresSellingPlan),
61
- 'one-time-available': !product.requiresSellingPlan,
62
- 'subscription-selected': sub,
63
- 'one-time-selected': !sub,
64
- 'has-savings': sub ? savingsNow > 0 : onSale,
65
- },
66
- /** Option values without an available variant, as plain data (for frameworks that serialise props). */
67
- unavailable: Object.fromEntries((product.options || []).map((o) => [o.name, o.optionValues.map((x) => x.name).filter((n) => isUnavail(o.name, n))])),
68
- /** Option values with no available variant (given the other selected options): class is-unavailable. */
69
- isUnavailable: (name, value) => isUnavail(name, value),
74
+ savings: view.savings.percentage,
75
+ price: view.price,
76
+ compareAtPrice: view.compareAtPrice,
77
+ available: view.variant.availableForSale,
78
+ show: view.show,
79
+ unavailable: Object.fromEntries(
80
+ product.options.map((option) => [
81
+ option.name,
82
+ option.optionValues.map((value) => value.name).filter((value) => isUnavailable(option.name, value)),
83
+ ]),
84
+ ),
85
+ isUnavailable,
70
86
  };
71
- function isUnavail(name, value) {
72
- return !vs.some((x) => x.availableForSale && x.selectedOptions.every((o) =>
73
- (o.name === name ? o.value === value : selected[o.name] === o.value)));
74
- }
75
87
  }
package/src/lazy-iife.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * <script> build: optional parts are separate files next to tether(.min).js, e.g.
3
- * tether.markets.min.js, loaded on first use. Override the folder with config.partsUrl.
2
+ * `<script>` build: the optional parts are separate files next to tether(.min).js (tether.markets.min.js), loaded on
3
+ * first use. `partsUrl` sets another folder. A part registers itself in `window.Tether.__parts` and uses the core
4
+ * modules in `window.Tether.__core` instead of bundling copies of them.
4
5
  */
5
6
  import * as config from './config.js';
6
7
  import * as client from './client.js';
@@ -13,34 +14,103 @@ import * as cart from './cart.js';
13
14
  import * as product from './product.js';
14
15
  import * as collection from './collection.js';
15
16
  import * as mounting from './mounting.js';
17
+ import * as marketCodes from './market-codes.js';
18
+ import * as shopifyValues from './shopify-values.js';
19
+ import * as storeHtml from './store-html.js';
20
+ /** @import { Parts, PartName } from './lazy.js' */
16
21
 
17
22
  const { cfg } = config;
18
23
 
19
- /** The modules a part may use, shared instead of bundled twice (scripts/build.mjs maps imports to these). */
24
+ /**
25
+ * The modules a part may use, shared instead of bundled twice (scripts/build.mjs maps a part's imports to these).
26
+ *
27
+ * @type {Record<string, object>}
28
+ */
20
29
  export const coreModules = {
21
- 'config.js': config, 'client.js': client, 'dom.js': dom, 'fragments.js': fragments, 'loader.js': loader,
22
- 'selling-plans.js': sellingPlans, 'status.js': status, 'cart.js': cart, 'product.js': product,
23
- 'collection.js': collection, 'mounting.js': mounting,
30
+ 'config.js': config,
31
+ 'client.js': client,
32
+ 'dom.js': dom,
33
+ 'fragments.js': fragments,
34
+ 'loader.js': loader,
35
+ 'selling-plans.js': sellingPlans,
36
+ 'status.js': status,
37
+ 'cart.js': cart,
38
+ 'product.js': product,
39
+ 'collection.js': collection,
40
+ 'mounting.js': mounting,
41
+ 'market-codes.js': marketCodes,
42
+ 'shopify-values.js': shopifyValues,
43
+ 'store-html.js': storeHtml,
24
44
  };
25
45
 
26
- const self = typeof document !== 'undefined' ? document.currentScript?.src || '' : '';
27
- const parts = {};
46
+ const MINIFIED_SCRIPT = /\.min\.js(\?|$)/;
47
+ const coreScript = typeof document === 'undefined' ? '' : (document.currentScript?.getAttribute('src') ?? '');
48
+ const coreScriptUrl = coreScript ? new URL(coreScript, document.baseURI).href : '';
28
49
 
29
- export function loadPart(name) {
30
- if (parts[name]) return parts[name];
31
- const base = cfg.partsUrl || self.replace(/[^/]*$/, '');
32
- const min = /\.min\.js(\?|$)/.test(self) || !self ? '.min' : '';
33
- const url = `${base.replace(/\/?$/, '/')}tether.${name}${min}.js`;
34
- parts[name] = new Promise((resolve, reject) => {
35
- const s = document.createElement('script');
36
- s.src = url;
37
- s.async = true;
38
- s.onload = () => (window.Tether?.__parts?.[name] ? resolve(window.Tether.__parts[name]) : reject(new Error(`${url} did not register`)));
39
- s.onerror = () => { delete parts[name]; reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`)); };
40
- document.head.appendChild(s);
50
+ /** @type {Map<PartName, Promise<unknown>>} Parts loaded or loading. */
51
+ const parts = new Map();
52
+
53
+ /**
54
+ * The folder parts load from: `partsUrl`, else the core script's own folder. A `partsUrl` must be https or on this
55
+ * site, because the parts run with the same access as the core (the account part holds customer sessions).
56
+ *
57
+ * @throws {Error} For a `partsUrl` that is neither.
58
+ */
59
+ function partsFolder() {
60
+ if (!cfg.partsUrl) return coreScriptUrl.replace(/[^/]*$/, '');
61
+ const folder = new URL(cfg.partsUrl, document.baseURI);
62
+ if (folder.protocol !== 'https:' && folder.origin !== window.location.origin) {
63
+ throw new Error(`partsUrl must be https or on this site: ${folder.href}`);
64
+ }
65
+ return folder.href;
66
+ }
67
+
68
+ /**
69
+ * @param {PartName} name
70
+ * @param {string} url
71
+ * @returns {Promise<unknown>}
72
+ */
73
+ function loadScript(name, url) {
74
+ return new Promise((resolve, reject) => {
75
+ const script = document.createElement('script');
76
+ script.src = url;
77
+ script.async = true;
78
+ script.onload = () => {
79
+ const { Tether } = /** @type {Window & { Tether?: { __parts?: Record<string, unknown> } }} */ (window);
80
+ const part = Tether?.__parts?.[name];
81
+ if (part) resolve(part);
82
+ else reject(new Error(`${url} did not register`));
83
+ };
84
+ script.onerror = () => {
85
+ parts.delete(name);
86
+ reject(new Error(`Could not load ${url}. Host it next to the Tether script, or set partsUrl.`));
87
+ };
88
+ document.head.appendChild(script);
41
89
  });
42
- return parts[name];
43
90
  }
44
91
 
45
- // Parts that load other parts (account -> account-pages) use this same loader.
92
+ /**
93
+ * @template {PartName} Name
94
+ * @param {Name} name
95
+ * @returns {Promise<Parts[Name]>}
96
+ * @throws {Error} (rejected) When the part's file can't be loaded or doesn't register.
97
+ */
98
+ export function loadPart(name) {
99
+ let loading = parts.get(name);
100
+ if (!loading) {
101
+ let folder;
102
+ try {
103
+ folder = partsFolder();
104
+ } catch (error) {
105
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
106
+ }
107
+ const suffix = MINIFIED_SCRIPT.test(coreScriptUrl) || !coreScriptUrl ? '.min' : '';
108
+ loading = loadScript(name, `${folder.replace(/\/?$/, '/')}tether.${name}${suffix}.js`);
109
+ parts.set(name, loading);
110
+ }
111
+ // A part's file is built from the module `Parts[Name]` names (scripts/build.mjs), and registers that module.
112
+ return /** @type {Promise<Parts[Name]>} */ (loading);
113
+ }
114
+
115
+ // Parts that load other parts (account → account-pages) use this same loader.
46
116
  coreModules['lazy.js'] = { loadPart, coreModules };
package/src/lazy.js CHANGED
@@ -1,8 +1,23 @@
1
1
  /**
2
- * Optional parts that most pages never need, loaded on first use.
3
- * ESM / Node: dynamic import (bundlers split these into their own chunks).
4
- * The <script> build swaps this file for lazy-iife.js (see scripts/build.mjs).
2
+ * Optional parts that most pages never need, loaded on first use. In the ESM and Node builds they are dynamic
3
+ * imports, which bundlers split into their own chunks; the `<script>` build swaps this file for `lazy-iife.js`
4
+ * (scripts/build.mjs).
5
5
  */
6
+
7
+ /**
8
+ * @typedef {{
9
+ * diagnose: typeof import('./diagnose.js'),
10
+ * markets: typeof import('./markets-ui.js'),
11
+ * search: typeof import('./search.js'),
12
+ * extras: typeof import('./extras.js'),
13
+ * account: typeof import('./account.js'),
14
+ * 'account-pages': typeof import('./account-pages.js'),
15
+ * analytics: typeof import('./analytics.js'),
16
+ * }} Parts
17
+ */
18
+ /** @typedef {keyof Parts} PartName */
19
+
20
+ /** @type {{ [Name in PartName]: () => Promise<Parts[Name]> }} */
6
21
  const PARTS = {
7
22
  diagnose: () => import('./diagnose.js'),
8
23
  markets: () => import('./markets-ui.js'),
@@ -12,7 +27,13 @@ const PARTS = {
12
27
  'account-pages': () => import('./account-pages.js'),
13
28
  analytics: () => import('./analytics.js'),
14
29
  };
30
+
31
+ /**
32
+ * @template {PartName} Name
33
+ * @param {Name} name
34
+ * @returns {Promise<Parts[Name]>}
35
+ */
15
36
  export const loadPart = (name) => PARTS[name]();
16
37
 
17
- // Only the <script> build shares its modules with separately loaded parts.
38
+ /** Only the `<script>` build shares its modules with separately loaded parts. */
18
39
  export const coreModules = undefined;