assign-gingerly 0.0.95 → 0.0.97

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 CHANGED
@@ -82,7 +82,7 @@ The third fundamental utility function is:
82
82
  **`assignFrom` is synchronous** — it resolves paths, expands substitutions, handles spreads, processes `#[x]` refs, and runs inferred assignments all without yielding to the event loop. Handler commands (` =>`), `beVigilant`, and `enhance` are fire-and-forget (kicked off asynchronously in the background).
83
83
 
84
84
  **`assignFromAsync`** is the awaitable variant for when you need to:
85
- - Use async protocol handlers (e.g., `fetch`-based resolution)
85
+ - Use async protocol handlers (e.g., `fetch`-based resolution — protocols are introduced just below, and covered in full in [Protocol Resolution](#protocol-resolution-in-getvalues-and-assignfrom) later in this doc)
86
86
  - `await` handler completion before proceeding
87
87
  - Wait for `enhance` (EMC JSON imports) to finish
88
88
 
@@ -97,7 +97,7 @@ assignFrom adds support for:
97
97
 
98
98
  1. Resolving RHS path strings against a source object (`from`).
99
99
  2. Target-relative root references via `$0` so paths can resolve from the first argument passed to `assignFrom`/`assignFromAsync` (the target object) instead of the `from` object.
100
- 3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
100
+ 3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols) — don't worry if this is unfamiliar yet, it's explained in full in [Protocol Resolution in `getValues` and `assignFrom`](#protocol-resolution-in-getvalues-and-assignfrom) later in this doc.
101
101
  4. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
102
102
  5. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
103
103
  6. Dynamic substitutions via the `substitutions` option for injecting runtime string values into path segments. See [docs/substitutions.md](docs/substitutions.md).
@@ -1594,7 +1594,83 @@ When you access `element.enh.set.enhKey.property`, the proxy:
1594
1594
  - If a non-matching object already exists at `element.enh[enhKey]`, it's passed as `initVals`
1595
1595
  - Stores the spawned instance at `element.enh[enhKey]`
1596
1596
  3. **Reuses existing instances**: If the enhancement already exists and is the correct type, it reuses it
1597
- 4. **Falls back to plain objects**: If no registry item is found, creates a plain object at `element.enh[enhKey]`
1597
+ 4. **Falls back to plain objects, and waits for registration**: If no registry item is found yet, creates a plain object at `element.enh[enhKey]` and calls `registry.whenDefined(enhKey)` to spawn the real instance automatically once the registry item eventually arrives — see [Setting Properties Before the Registry Item Exists](#setting-properties-before-the-registry-item-exists) below
1598
+
1599
+ </details>
1600
+
1601
+ ### Setting Properties Before the Registry Item Exists
1602
+
1603
+ Code doesn't always run in the order you'd like — a module might set
1604
+ properties on an enhancement before whatever registers that enhancement's
1605
+ config has had a chance to run. `enh.set` handles this automatically, the
1606
+ same way `element.enh.set.enhKey.property = value` does when the registry
1607
+ item is already there:
1608
+
1609
+ ```TypeScript
1610
+ import 'assign-gingerly/object-extension.js';
1611
+
1612
+ const myElement = document.createElement('div');
1613
+
1614
+ // Nothing has called `registry.push(...)` for 'myEnh' yet.
1615
+ myElement.enh.set.myEnh.myProp = 'hello'; // plain placeholder, collected as pending initVals
1616
+ myElement.enh.myEnh.anotherProp = 'world'; // direct property access works too, same placeholder
1617
+
1618
+ console.log(myElement.enh.myEnh instanceof MyEnhancement); // false -- still a plain object
1619
+
1620
+ // ...later, whenever the registry item shows up...
1621
+ const registry = myElement.customElementRegistry.enhancementRegistry;
1622
+ registry.push({ spawn: MyEnhancement, enhKey: 'myEnh' });
1623
+
1624
+ // The spawn happens automatically once `whenDefined` resolves -- no need to
1625
+ // touch `myElement.enh.myEnh` again yourself.
1626
+ await registry.whenDefined('myEnh');
1627
+ console.log(myElement.enh.myEnh instanceof MyEnhancement); // true
1628
+ console.log(myElement.enh.myEnh.myProp); // 'hello' -- both properties
1629
+ console.log(myElement.enh.myEnh.anotherProp); // 'world' -- survived as initVals
1630
+ ```
1631
+
1632
+ <details>
1633
+ <summary>How it works</summary>
1634
+
1635
+ 1. The first `enh.set.enhKey.property = value` assignment for an
1636
+ unregistered `enhKey` creates a plain object at `element.enh[enhKey]` (as
1637
+ always) and, in addition, calls
1638
+ `registry.whenDefined(enhKey).then(() => element.enh.get(enhKey))`.
1639
+ 2. Because that call only happens inside the `if (self[prop] === undefined)`
1640
+ branch, it fires exactly once per element/`enhKey` pair — further property
1641
+ assignments before registration just add to the same placeholder object,
1642
+ they don't queue additional spawn attempts.
1643
+ 3. `registry.whenDefined(enhKey)` resolves once a matching `enhKey` is
1644
+ `push`ed into the registry (see below).
1645
+ 4. Once it resolves, `enh.get(enhKey)` runs — the same method used everywhere
1646
+ else in this library. It finds the plain placeholder already sitting at
1647
+ `element.enh[enhKey]`, treats it as `initVals` (this is the same
1648
+ "non-matching object passed as `initVals`" behavior described in step 2 of
1649
+ [How It Works](#basic-usage) above — nothing new here), constructs the real
1650
+ instance, and overwrites the placeholder with it.
1651
+
1652
+ **`EnhancementRegistry.whenDefined(enhKey)`** — mirrors
1653
+ [`customElements.whenDefined(name)`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined):
1654
+
1655
+ - If `enhKey` is already registered, resolves once any of its pending
1656
+ [Custom Element Features](#custom-element-features) setup has settled (see
1657
+ `features` on `EnhancementConfig`).
1658
+ - If `enhKey` isn't registered yet, first waits for a future `registry.push(...)`
1659
+ call whose config (a single item, or an array) includes a matching `enhKey`,
1660
+ then does the same features-setup wait.
1661
+ - **If `enhKey` is never registered, this promise never resolves** — same
1662
+ tradeoff `customElements.whenDefined` makes. Don't `await` it unconditionally
1663
+ in code paths where registration isn't guaranteed to eventually happen.
1664
+
1665
+ **Limitations while waiting:**
1666
+ - The placeholder is a plain object — no reactivity, no defaults, no
1667
+ validation. Reading a property other than the ones you've explicitly set
1668
+ (e.g. checking `resolved`) just returns `undefined`, since nothing has
1669
+ spawned yet.
1670
+ - `mountCtx` still isn't available through this path (same restriction as
1671
+ `enh.set` generally — see the note in
1672
+ [Passing Custom Context](#constructor-signature) above); the eventual
1673
+ `enh.get(enhKey)` call is made with no `mountCtx`.
1598
1674
 
1599
1675
  </details>
1600
1676
 
package/assignGingerly.js CHANGED
@@ -63,10 +63,27 @@ export class EnhancementRegistry extends EventTarget {
63
63
  }
64
64
  }
65
65
  /**
66
- * Wait for all pending setups for a given enhancement key to complete.
66
+ * Resolves once `enhKey` is defined -- i.e. registered via `push` -- and
67
+ * ready (any pending `features` setup for it has settled). Mirrors
68
+ * `customElements.whenDefined(name)`: if the key isn't registered yet, waits
69
+ * for a future `push` that registers it; if it's never registered, this
70
+ * never resolves (same tradeoff as the platform method).
67
71
  * @param enhKey - Enhancement key to wait for
68
72
  */
69
73
  async whenDefined(enhKey) {
74
+ if (!this.findByEnhKey(enhKey)) {
75
+ await new Promise((resolve) => {
76
+ const handler = (event) => {
77
+ const config = event.config;
78
+ const items = Array.isArray(config) ? config : [config];
79
+ if (items.some(item => item.enhKey === enhKey)) {
80
+ this.removeEventListener(EnhancementRegisteredEvent.eventName, handler);
81
+ resolve();
82
+ }
83
+ };
84
+ this.addEventListener(EnhancementRegisteredEvent.eventName, handler);
85
+ });
86
+ }
70
87
  const pending = this.#pendingSetups.get(enhKey);
71
88
  if (pending && pending.length > 0) {
72
89
  await Promise.all(pending);
package/assignGingerly.ts CHANGED
@@ -106,10 +106,27 @@ export class EnhancementRegistry extends EventTarget {
106
106
  }
107
107
 
108
108
  /**
109
- * Wait for all pending setups for a given enhancement key to complete.
109
+ * Resolves once `enhKey` is defined -- i.e. registered via `push` -- and
110
+ * ready (any pending `features` setup for it has settled). Mirrors
111
+ * `customElements.whenDefined(name)`: if the key isn't registered yet, waits
112
+ * for a future `push` that registers it; if it's never registered, this
113
+ * never resolves (same tradeoff as the platform method).
110
114
  * @param enhKey - Enhancement key to wait for
111
115
  */
112
116
  async whenDefined(enhKey: EnhKey): Promise<void> {
117
+ if (!this.findByEnhKey(enhKey)) {
118
+ await new Promise<void>((resolve) => {
119
+ const handler = (event: Event) => {
120
+ const config = (event as EnhancementRegisteredEvent).config;
121
+ const items = Array.isArray(config) ? config : [config];
122
+ if (items.some(item => item.enhKey === enhKey)) {
123
+ this.removeEventListener(EnhancementRegisteredEvent.eventName, handler);
124
+ resolve();
125
+ }
126
+ };
127
+ this.addEventListener(EnhancementRegisteredEvent.eventName, handler);
128
+ });
129
+ }
113
130
  const pending = this.#pendingSetups.get(enhKey);
114
131
  if (pending && pending.length > 0) {
115
132
  await Promise.all(pending);
package/index.js CHANGED
@@ -8,6 +8,7 @@ export { buildCSSQuery } from './buildCSSQuery.js';
8
8
  export { resolveTemplate } from './resolve/resolveTemplate.js';
9
9
  export { getHost } from './getHost.js';
10
10
  export { resolveValues, resolveValue } from './resolve/resolveValues.js';
11
+ export { getValues, getValue, hasProtocol, parseProtocolRef, isPlainObject } from './resolve/getValues.js';
11
12
  export { assignFromAsync } from './assignFromAsync.js';
12
13
  export { assignFrom } from './assignFrom.js';
13
14
  export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
package/index.ts CHANGED
@@ -8,6 +8,7 @@ export {buildCSSQuery} from './buildCSSQuery.js';
8
8
  export {resolveTemplate} from './resolve/resolveTemplate.js';
9
9
  export {getHost} from './getHost.js';
10
10
  export {resolveValues, resolveValue} from './resolve/resolveValues.js';
11
+ export {getValues, getValue, hasProtocol, parseProtocolRef, isPlainObject} from './resolve/getValues.js';
11
12
  export {assignFromAsync} from './assignFromAsync.js';
12
13
  export {assignFrom} from './assignFrom.js';
13
14
  export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
@@ -107,7 +107,7 @@ Update the package.json to use the modern architecture's dependencies and build
107
107
  "nested-regex-groups": "*"
108
108
  }
109
109
  ```
110
- - Drop the legacy dependencies (`be-enhanced`, `trans-render`, etc.). Keep a legacy dependency **only** if a converted action still imports from it and the modern stack has no replacement (e.g. `trans-render/XV/set.js` for the Uniform Storage Path protocol) — note any such carry-over in your conversion notes.
110
+ - Drop the legacy dependencies (`be-enhanced`, `trans-render`, etc.). There is a modern replacement for most legacy utilities — e.g. Uniform Storage Path writes move from `trans-render/XV/set.js` to [`fifteenth`](https://github.com/bahrus/fifteenth) (`fifteenth/set.js`, see Step 9). Keep a legacy dependency **only** if a converted action still imports from it and no replacement exists — and note any such carry-over in your conversion notes.
111
111
  - `mount-observer` is listed as a direct dependency so it lands at the root `node_modules/` level, accessible via the import map. `nested-regex-groups` is optional but recommended if your enhancement needs custom attribute parsing (Step 7a).
112
112
  - The exact version strings do not matter here — the next step replaces them all with the latest published versions.
113
113
 
@@ -969,6 +969,14 @@ export interface Actions{
969
969
  7. **Update utility imports**:
970
970
  - Replace `trans-render/lib/findAdjacentElement.js` with `be-hive/findAdjacentElement.js`
971
971
  - The be-hive package provides common utilities that were previously in trans-render
972
+ - **Uniform Storage Path (USL) writes** — if a legacy action wrote values to `indexedDB://…`, `localStorage://…`, etc. via `trans-render/XV/set.js`, use [`fifteenth`](https://github.com/bahrus/fifteenth) instead:
973
+ ```javascript
974
+ const { set } = await import('fifteenth/set.js');
975
+ await set('indexedDB://myDB/myStore/myKey', value); // same (usl, val, ctx?) signature as XV/set
976
+ ```
977
+ - Add `"fifteenth"` to `dependencies` and a `"fifteenth/": "/node_modules/fifteenth/"` entry to the import map.
978
+ - `fifteenth` imports `parseProtocolRef` from `assign-gingerly@0.0.96`+. `roundabout-lib` / `mount-observer` may still pin an older `assign-gingerly` exactly, and the flat import map can only serve one copy — so also add `"assign-gingerly": "<the version fifteenth needs>"` to `dependencies` to hoist a single root copy. Verify the be-hive/mount-observer/roundabout read path still works against that version.
979
+ - **Behavior change to note in your conversion notes:** `fifteenth`'s `set` broadcasts `window.postMessage([usp])` (or `[usp, usl]` when the USL has a `?.` accessor chain) — a plain `Array` of path strings, *not* the `Set` (protocol + store-root + full-path) that `trans-render/XV` posted. Update any `message` listeners accordingly.
972
980
 
973
981
  **CRITICAL - Avoid Compact/Action Conflicts:**
974
982
 
@@ -0,0 +1,46 @@
1
+ # Clarify Programmatic Way to Add Enhancements
2
+
3
+ When we define an enhancement, it is easiest to demonstrate what it does and how it works by providing simple HTML examples with the attributes used to trigger the enhancement.
4
+
5
+ These examples are skewed towards an important use case -- progressive enhancement of server rendered content.
6
+
7
+ However, currently, only a small fraction of web development centers around that paradigm. Rather, it tends to revolve around client-side rendering, using a framework.
8
+
9
+ In such a context, putting so much emphasis on the attribute way of hooking things up is problematic.
10
+
11
+ 1. Such frameworks are clumsy when it comes to setting attributes.
12
+ 2. The amount of object stringifying and parsing is inefficient.
13
+
14
+ So we should make an effort to showcase how to integrate with these enhancements efficiently and ergonomically in such settings. Basically programmatically without the use of attributes.
15
+
16
+ I think we should showcase two ways to do so.
17
+
18
+ So for example, currently the README.md for be-persistent shows the example:
19
+
20
+ ```html
21
+ <input be-persistent="of value@input via sessionStorage://{autoGenId}.">
22
+ ```
23
+
24
+ We should also show:
25
+
26
+ ## Imperative Enhancement Attachment:
27
+
28
+ ```JS
29
+ import {emc} from 'be-persistent/emc.json' with {type: 'json'};
30
+ import {BePersistent} from 'be-persistent/be-persistent.js';
31
+ emc.spawn = BePersistent;
32
+ const persistenceEnhancement = oInput.enh.get(emc);
33
+ persistenceEnhancement.store =
34
+ {
35
+ localProp: 'value', //default
36
+ localEvent: 'input', //default
37
+ usl: 'sessionStorage://{autoGenId}'
38
+ };
39
+ ```
40
+
41
+ ## Declarative Enhancement Attachment:
42
+
43
+ ```JS
44
+ import 'be-persistent/.js';
45
+
46
+ ```
@@ -72,6 +72,8 @@ export interface EnhancementConfig<T = any, Obj = Element> extends EnhancementCo
72
72
  * Calls assignFeatures(spawn, features) automatically on registration.
73
73
  */
74
74
  features?: FeatureConfigsMap;
75
+
76
+ customData?: any;
75
77
 
76
78
  }
77
79
 
@@ -406,7 +408,7 @@ export interface AssignFromOptions {
406
408
  from: any;
407
409
 
408
410
  /** Protocol handlers (sync or async) */
409
- protocols?: Record<string, (key: string) => any | Promise<any>>;
411
+ protocols?: ProtocolHandlers;
410
412
 
411
413
  /** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
412
414
  withMethods?: string[] | Set<string>;
@@ -480,6 +482,37 @@ export interface AssignFromOptions {
480
482
  [key: string]: any;
481
483
  }
482
484
 
485
+ /**
486
+ * A protocol handler resolves the key portion of a protocol-prefixed value
487
+ * (the text between `://` and the first `?.`) to a value. May be sync or async.
488
+ */
489
+ export type ProtocolHandler = (key: string) => any | Promise<any>;
490
+
491
+ /**
492
+ * A synchronous-only protocol handler, as accepted by getValues / getValue /
493
+ * assignFrom. For handlers that may return a Promise, use ProtocolHandler.
494
+ */
495
+ export type SyncProtocolHandler = (key: string) => any;
496
+
497
+ /** Map of protocol name (the text before `://`) to a handler (sync or async). */
498
+ export type ProtocolHandlers = Record<string, ProtocolHandler>;
499
+
500
+ /** Map of protocol name to a synchronous handler. */
501
+ export type SyncProtocolHandlers = Record<string, SyncProtocolHandler>;
502
+
503
+ /**
504
+ * The outer grammar of a protocol-prefixed value string,
505
+ * `‹protocol›://‹key›?.‹path›`, as produced by `parseProtocolRef`.
506
+ */
507
+ export interface ParsedProtocolRef {
508
+ /** Text before `://`; `''` when the value contains no `://`. */
509
+ protocol: string;
510
+ /** Text between `://` and the first `?.` (or the end of the string). */
511
+ key: string;
512
+ /** Text from the first `?.` onward (always starts with `?.`), or `null`. */
513
+ path: string | null;
514
+ }
515
+
483
516
  /**
484
517
  * Options for synchronous value resolution (getValues / getValue).
485
518
  * Extends IAssignGingerlyOptions with synchronous protocol handlers.
@@ -507,7 +540,7 @@ export interface GetValuesOptions extends IAssignGingerlyOptions {
507
540
  * localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
508
541
  * }
509
542
  */
510
- protocols?: Record<string, (key: string) => any>;
543
+ protocols?: SyncProtocolHandlers;
511
544
  }
512
545
 
513
546
  /**
@@ -530,7 +563,7 @@ export interface ResolveValuesOptions extends IAssignGingerlyOptions {
530
563
  * Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
531
564
  * Each handler receives the key portion and returns the resolved value (sync or async).
532
565
  */
533
- protocols?: Record<string, (key: string) => any | Promise<any>>;
566
+ protocols?: ProtocolHandlers;
534
567
  }
535
568
 
536
569
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Uniform Storage Path string (e.g. "indexedDB://myDB/myFiles/{file.name}").
3
- * Inlined here (was imported from trans-render/XV/types) to keep this file standalone.
3
+ * Consumed by `fifteenth`'s `set`. Kept as a local alias so this file stays standalone.
4
4
  */
5
5
  export type USL = string;
6
6
 
@@ -0,0 +1,81 @@
1
+ import { ElementEnhancementGateway, SpawnContext } from "../assign-gingerly/types";
2
+ import { StatementsResult } from "../nested-regex-groups/types";
3
+
4
+ /**
5
+ * The programmatic shape of `store`: a bare USL string (shorthand for
6
+ * `{usl}`, i.e. `localProp`/`localEvent` take their defaults), a single
7
+ * {@link PersistenceRule}, or an array of them (one binder per entry).
8
+ * This is the property JS callers set directly — e.g.
9
+ * `persistenceEnhancement.store = 'sessionStorage://{autoGenId}'` — without
10
+ * ever going through attribute parsing.
11
+ */
12
+ export type PersistenceRuleConfig = string | PersistenceRule | PersistenceRule[];
13
+
14
+ export interface EndUserProps {
15
+ /**
16
+ * What to persist and where. Accepts a USL string, a single
17
+ * {@link PersistenceRule}, or an array of rules — see
18
+ * {@link PersistenceRuleConfig}. This is the property `hydrate` reads and
19
+ * the one programmatic (attribute-free) callers should assign.
20
+ */
21
+ store?: PersistenceRuleConfig;
22
+ /**
23
+ * Opt-in flag set by the boolean `be-persistent-nudge` / `💾-nudge`
24
+ * attribute. When present, `hydrate` waits for every rule to finish its
25
+ * initial storage reconciliation and then calls `assign-gingerly`'s
26
+ * `nudge` on the enhanced element — decrementing its `disabled` counter so
27
+ * an element that was disabled only to block edits before its persisted
28
+ * value loaded becomes interactive.
29
+ */
30
+ nudge?: boolean;
31
+ }
32
+
33
+ export interface AllProps extends EndUserProps {
34
+ enhancedElement: Element & ElementEnhancementGateway;
35
+ resolved?: boolean;
36
+ /**
37
+ * Flipped to `true` at the end of `init`, once `roundabout` has finished
38
+ * assigning every attribute-derived prop. `hydrate` is gated on it so it
39
+ * never runs against a half-populated view model.
40
+ */
41
+ initialized?: boolean;
42
+ /**
43
+ * Raw parse result of the base attribute (`be-persistent` / `💾`), via the
44
+ * `parse-grouped-capture-statements` built-in parser. Exists **only** to be
45
+ * transferred into `store` by `onPersistenceRulesChange` — nothing else
46
+ * reads it. Programmatic callers should assign `store` directly instead of
47
+ * this property.
48
+ */
49
+ persistenceRules?: StatementsResult<PersistenceRule>;
50
+ }
51
+
52
+ export type AP = AllProps;
53
+
54
+ export type PAP = Partial<AP>;
55
+
56
+ export type ProPAP = Promise<PAP>;
57
+
58
+ export interface Actions {
59
+ init(self: AP, enhancedElement: Element & ElementEnhancementGateway, ctx: SpawnContext, initVals: PAP): Promise<void>;
60
+ /**
61
+ * Compact-invoked (`when_persistenceRules_changes_call_onPersistenceRulesChange`),
62
+ * not an action — normalizes `persistenceRules.statements` into `store`.
63
+ */
64
+ onPersistenceRulesChange(self: AP): PAP;
65
+ hydrate(self: AP): ProPAP;
66
+ }
67
+
68
+ export interface PersistenceRule {
69
+ /** Element property to read/write (e.g. `value`, `checked`, `innerHTML`). Defaults to `value`. */
70
+ localProp?: string;
71
+ /** DOM event that triggers a save. Defaults to `input`. */
72
+ localEvent?: string;
73
+ /**
74
+ * Uniform Storage Locator describing where the value lives, e.g.
75
+ * `sessionStorage://{autoGenId}`, `indexedDB://myDB/myStore/{autoGenId}`,
76
+ * `cookie://{autoGenId}`, `locationHash://{autoGenId}`.
77
+ * `{autoGenId}` is expanded at runtime to a location-independent DOM path.
78
+ * Defaults to `sessionStorage://{autoGenId}`.
79
+ */
80
+ usl?: string;
81
+ }
@@ -0,0 +1,59 @@
1
+ import { ElementEnhancementGateway, SpawnContext } from "../assign-gingerly/types";
2
+
3
+ export interface EndUserProps {
4
+ /**
5
+ * Where to fetch the replacement content from — either a literal URL
6
+ * (typically a `gist.githubusercontent.com/.../raw/.../<file>` URL) or a
7
+ * `fifteenth` `gist://` USL (`gist://<owner>/<id>/raw[/<sha>]/<file>`,
8
+ * or an alias/`=<id>` form once `configureGist()` has registered the
9
+ * `gist` protocol).
10
+ */
11
+ url: string;
12
+ /** The `<?marker name="…">` this template's content replaces. */
13
+ markerName: string;
14
+ /**
15
+ * CSS selector naming the parent(s) of the marker(s), so the search
16
+ * doesn't have to `TreeWalker` the whole subtree. Optional.
17
+ */
18
+ forHint: string | undefined;
19
+ /**
20
+ * Show a link to edit the gist on GitHub right after the template.
21
+ * Also triggered page-wide by `?gist-in-show-edit-link=true` in the URL.
22
+ */
23
+ showEditLink: boolean;
24
+ /**
25
+ * Custom Sanitizer configuration (JSON object, e.g.
26
+ * `{"elements":["option","optgroup"]}`), same shape as pipe-in's
27
+ * `[base]-sanitizer`. The platform's *default* sanitizer strips elements
28
+ * like `<option>` entirely — this is the escape hatch for patching them.
29
+ * Gated the same way pipe-in gates it: only honored when `url` is a
30
+ * same-origin path, an import-map-resolved bare specifier, or a
31
+ * `gist://` USL (whose real destination is always the fixed
32
+ * `gist.githubusercontent.com` host, not attacker-steerable).
33
+ */
34
+ sanitizer: object | undefined;
35
+ /**
36
+ * `'setHTML'` (default, sanitized) or `'setHTMLUnsafe'` (no sanitizing at
37
+ * all) — named after, and selecting between, the two real underlying
38
+ * methods, the same way pipe-in's `[base]-method` selects one of its own
39
+ * `streamHTML`/`streamHTMLUnsafe`/etc. `'setHTMLUnsafe'` is gated the same
40
+ * as a custom `sanitizer` (see above). Never runs embedded `<script>`s
41
+ * either way — `setHTML`/`setHTMLUnsafe` parse, they don't execute
42
+ * (verified; same as plain `innerHTML`).
43
+ */
44
+ method: 'setHTML' | 'setHTMLUnsafe';
45
+ }
46
+
47
+ export interface AllProps extends EndUserProps {
48
+ enhancedElement: Element & ElementEnhancementGateway;
49
+ resolved: boolean;
50
+ }
51
+
52
+ export type AP = AllProps;
53
+ export type PAP = Partial<AP>;
54
+ export type ProPAP = Promise<PAP>;
55
+
56
+ export interface Actions {
57
+ init(self: AP, enhancedElement: Element, ctx: SpawnContext, initVals: PAP): Promise<void>;
58
+ hydrate(self: AP): ProPAP;
59
+ }
@@ -317,6 +317,16 @@ class ElementEnhancementContainer {
317
317
  // No registry item found - create plain object if needed
318
318
  if (self[prop] === undefined) {
319
319
  self[prop] = {};
320
+ // Declarative-out-of-sequence: `prop`'s enhancement config hasn't
321
+ // been registered yet. Once it is, hand the plain object we just
322
+ // created off to `get()` as pending initVals and let it spawn
323
+ // normally -- `get()` already treats a non-instance value sitting
324
+ // in this slot as initVals to merge in (see the `existingInitVals`
325
+ // check above). If `prop` is never registered, this never
326
+ // resolves (same tradeoff as `customElements.whenDefined`).
327
+ const laterRegistry = registry
328
+ ?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
329
+ laterRegistry?.whenDefined(prop).then(() => self.get(prop));
320
330
  }
321
331
  return self[prop];
322
332
  }
@@ -443,8 +443,19 @@ class ElementEnhancementContainer {
443
443
  // No registry item found - create plain object if needed
444
444
  if (self[prop] === undefined) {
445
445
  self[prop] = {};
446
+
447
+ // Declarative-out-of-sequence: `prop`'s enhancement config hasn't
448
+ // been registered yet. Once it is, hand the plain object we just
449
+ // created off to `get()` as pending initVals and let it spawn
450
+ // normally -- `get()` already treats a non-instance value sitting
451
+ // in this slot as initVals to merge in (see the `existingInitVals`
452
+ // check above). If `prop` is never registered, this never
453
+ // resolves (same tradeoff as `customElements.whenDefined`).
454
+ const laterRegistry = registry
455
+ ?? (typeof customElements !== 'undefined' ? (customElements as any).enhancementRegistry : undefined);
456
+ laterRegistry?.whenDefined(prop).then(() => self.get(prop));
446
457
  }
447
-
458
+
448
459
  return self[prop];
449
460
  }
450
461
  }) as any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.95",
3
+ "version": "0.0.97",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -228,7 +228,7 @@
228
228
  "chrome": "npx playwright cr http://localhost:8000"
229
229
  },
230
230
  "devDependencies": {
231
- "@playwright/test": "1.62.1",
232
- "spa-ssi": "0.0.27"
231
+ "@playwright/test": "1.63.0",
232
+ "spa-ssi": "0.0.28"
233
233
  }
234
234
  }
@@ -183,11 +183,47 @@ function navigatePath(source, parts, withMethods, permissionProcessor) {
183
183
  return current;
184
184
  }
185
185
  /**
186
- * Checks if a string value looks like a protocol reference.
186
+ * Whether a string carries a protocol prefix (contains `://`).
187
+ *
188
+ * Exported as the single source of truth for the outer USL grammar, so
189
+ * downstream packages (and resolveValues) don't re-implement the check.
187
190
  */
188
- function hasProtocol(value) {
191
+ export function hasProtocol(value) {
189
192
  return value.includes('://');
190
193
  }
194
+ /**
195
+ * Split a protocol-prefixed value string into its outer-grammar parts:
196
+ * `‹protocol›://‹key›?.‹path›`.
197
+ *
198
+ * - `protocol` is the text before `://` (`''` when there is no `://`).
199
+ * - `key` is the text between `://` and the first `?.` (or end of string).
200
+ * - `path` is the `?.`-onward remainder (starting with `?.`), or `null`.
201
+ *
202
+ * Pure string parsing — no handler lookup, no resolution. Shared by the sync
203
+ * (`getProtocolValue`) and async (`resolveProtocolValue`) resolvers.
204
+ */
205
+ export function parseProtocolRef(value) {
206
+ const protoEnd = value.indexOf('://');
207
+ if (protoEnd === -1)
208
+ return { protocol: '', key: value, path: null };
209
+ const protocol = value.substring(0, protoEnd);
210
+ const rest = value.substring(protoEnd + 3);
211
+ const pathStart = rest.indexOf('?.');
212
+ const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
213
+ const path = pathStart === -1 ? null : rest.substring(pathStart);
214
+ return { protocol, key, path };
215
+ }
216
+ /**
217
+ * Whether a value is a plain object (prototype is `Object.prototype` or `null`),
218
+ * as opposed to an array, a class instance, or a primitive. Used to decide
219
+ * whether a nested value should be recursed into as a pattern.
220
+ */
221
+ export function isPlainObject(value) {
222
+ if (!value || typeof value !== 'object')
223
+ return false;
224
+ const proto = Object.getPrototypeOf(value);
225
+ return proto === Object.prototype || proto === null;
226
+ }
191
227
  /**
192
228
  * Detect a leading run of `!` characters used as a boolean coercion / negation
193
229
  * marker (`!` = negate, `!!` = coerce to boolean, `!!!` = negate, …). Returns the
@@ -266,15 +302,10 @@ function resolveStringValue(value, source, aliasMap, withMethods, protocols, opt
266
302
  * Resolve a protocol-prefixed value synchronously.
267
303
  */
268
304
  function getProtocolValue(value, protocols, options) {
269
- const protoEnd = value.indexOf('://');
270
- const protocol = value.substring(0, protoEnd);
305
+ const { protocol, key, path } = parseProtocolRef(value);
271
306
  const handler = protocols[protocol];
272
307
  if (!handler)
273
308
  return value; // not a recognized protocol
274
- const rest = value.substring(protoEnd + 3);
275
- const pathStart = rest.indexOf('?.');
276
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
277
- const path = pathStart === -1 ? null : rest.substring(pathStart);
278
309
  const resolved = handler(key);
279
310
  if (path) {
280
311
  return getValue(path, resolved, options);
@@ -295,13 +326,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options, substi
295
326
  result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
296
327
  }
297
328
  else if (item && typeof item === 'object') {
298
- const proto = Object.getPrototypeOf(item);
299
- if (proto === Object.prototype || proto === null) {
300
- result.push(getValues(item, source, options));
301
- }
302
- else {
303
- result.push(item);
304
- }
329
+ result.push(isPlainObject(item) ? getValues(item, source, options) : item);
305
330
  }
306
331
  else {
307
332
  result.push(item);
@@ -334,13 +359,7 @@ export function getValues(pattern, source, options) {
334
359
  result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
335
360
  }
336
361
  else if (typeof value === 'object' && value !== null) {
337
- const proto = Object.getPrototypeOf(value);
338
- if (proto === Object.prototype || proto === null) {
339
- result[key] = getValues(value, source, options);
340
- }
341
- else {
342
- result[key] = value;
343
- }
362
+ result[key] = isPlainObject(value) ? getValues(value, source, options) : value;
344
363
  }
345
364
  else {
346
365
  result[key] = value;
@@ -17,7 +17,7 @@
17
17
  * }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
18
18
  */
19
19
 
20
- import type { GetValuesOptions, PermissionProcessor } from '../types/assign-gingerly/types.js';
20
+ import type { GetValuesOptions, ParsedProtocolRef, PermissionProcessor, SyncProtocolHandlers } from '../types/assign-gingerly/types.js';
21
21
 
22
22
  export function normalizeAliasOptions(options?: {
23
23
  aka?: Record<string, string>;
@@ -221,12 +221,48 @@ function navigatePath(
221
221
  }
222
222
 
223
223
  /**
224
- * Checks if a string value looks like a protocol reference.
224
+ * Whether a string carries a protocol prefix (contains `://`).
225
+ *
226
+ * Exported as the single source of truth for the outer USL grammar, so
227
+ * downstream packages (and resolveValues) don't re-implement the check.
225
228
  */
226
- function hasProtocol(value: string): boolean {
229
+ export function hasProtocol(value: string): boolean {
227
230
  return value.includes('://');
228
231
  }
229
232
 
233
+ /**
234
+ * Split a protocol-prefixed value string into its outer-grammar parts:
235
+ * `‹protocol›://‹key›?.‹path›`.
236
+ *
237
+ * - `protocol` is the text before `://` (`''` when there is no `://`).
238
+ * - `key` is the text between `://` and the first `?.` (or end of string).
239
+ * - `path` is the `?.`-onward remainder (starting with `?.`), or `null`.
240
+ *
241
+ * Pure string parsing — no handler lookup, no resolution. Shared by the sync
242
+ * (`getProtocolValue`) and async (`resolveProtocolValue`) resolvers.
243
+ */
244
+ export function parseProtocolRef(value: string): ParsedProtocolRef {
245
+ const protoEnd = value.indexOf('://');
246
+ if (protoEnd === -1) return { protocol: '', key: value, path: null };
247
+ const protocol = value.substring(0, protoEnd);
248
+ const rest = value.substring(protoEnd + 3);
249
+ const pathStart = rest.indexOf('?.');
250
+ const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
251
+ const path = pathStart === -1 ? null : rest.substring(pathStart);
252
+ return { protocol, key, path };
253
+ }
254
+
255
+ /**
256
+ * Whether a value is a plain object (prototype is `Object.prototype` or `null`),
257
+ * as opposed to an array, a class instance, or a primitive. Used to decide
258
+ * whether a nested value should be recursed into as a pattern.
259
+ */
260
+ export function isPlainObject(value: any): boolean {
261
+ if (!value || typeof value !== 'object') return false;
262
+ const proto = Object.getPrototypeOf(value);
263
+ return proto === Object.prototype || proto === null;
264
+ }
265
+
230
266
  /**
231
267
  * Detect a leading run of `!` characters used as a boolean coercion / negation
232
268
  * marker (`!` = negate, `!!` = coerce to boolean, `!!!` = negate, …). Returns the
@@ -249,7 +285,7 @@ function parseNegationMarker(value: string): { count: number; rest: string } | n
249
285
  */
250
286
  function looksLikeReference(
251
287
  value: string,
252
- protocols: Record<string, (key: string) => any> | undefined
288
+ protocols: SyncProtocolHandlers | undefined
253
289
  ): boolean {
254
290
  return value.startsWith('?.')
255
291
  || value.startsWith('$0')
@@ -265,7 +301,7 @@ function resolveReferenceString(
265
301
  source: any,
266
302
  aliasMap: Map<string, string>,
267
303
  withMethods: Set<string> | undefined,
268
- protocols: Record<string, (key: string) => any> | undefined,
304
+ protocols: SyncProtocolHandlers | undefined,
269
305
  options: GetValuesOptions | undefined,
270
306
  substitutionMap: Map<string, string> | undefined
271
307
  ): any {
@@ -298,7 +334,7 @@ function resolveStringValue(
298
334
  source: any,
299
335
  aliasMap: Map<string, string>,
300
336
  withMethods: Set<string> | undefined,
301
- protocols: Record<string, (key: string) => any> | undefined,
337
+ protocols: SyncProtocolHandlers | undefined,
302
338
  options: GetValuesOptions | undefined,
303
339
  substitutionMap: Map<string, string> | undefined
304
340
  ): any {
@@ -325,21 +361,14 @@ function resolveStringValue(
325
361
  */
326
362
  function getProtocolValue(
327
363
  value: string,
328
- protocols: Record<string, (key: string) => any>,
364
+ protocols: SyncProtocolHandlers,
329
365
  options?: GetValuesOptions
330
366
  ): any {
331
- const protoEnd = value.indexOf('://');
332
- const protocol = value.substring(0, protoEnd);
367
+ const { protocol, key, path } = parseProtocolRef(value);
333
368
 
334
369
  const handler = protocols[protocol];
335
370
  if (!handler) return value; // not a recognized protocol
336
371
 
337
- const rest = value.substring(protoEnd + 3);
338
-
339
- const pathStart = rest.indexOf('?.');
340
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
341
- const path = pathStart === -1 ? null : rest.substring(pathStart);
342
-
343
372
  const resolved = handler(key);
344
373
 
345
374
  if (path) {
@@ -357,7 +386,7 @@ function getArray(
357
386
  source: any,
358
387
  aliasMap: Map<string, string>,
359
388
  withMethods: Set<string> | undefined,
360
- protocols: Record<string, (key: string) => any> | undefined,
389
+ protocols: SyncProtocolHandlers | undefined,
361
390
  options?: GetValuesOptions,
362
391
  substitutionMap?: Map<string, string>
363
392
  ): any[] {
@@ -368,12 +397,7 @@ function getArray(
368
397
  } else if (Array.isArray(item)) {
369
398
  result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
370
399
  } else if (item && typeof item === 'object') {
371
- const proto = Object.getPrototypeOf(item);
372
- if (proto === Object.prototype || proto === null) {
373
- result.push(getValues(item, source, options));
374
- } else {
375
- result.push(item);
376
- }
400
+ result.push(isPlainObject(item) ? getValues(item, source, options) : item);
377
401
  } else {
378
402
  result.push(item);
379
403
  }
@@ -410,12 +434,7 @@ export function getValues(
410
434
  } else if (Array.isArray(value)) {
411
435
  result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
412
436
  } else if (typeof value === 'object' && value !== null) {
413
- const proto = Object.getPrototypeOf(value);
414
- if (proto === Object.prototype || proto === null) {
415
- result[key] = getValues(value, source, options);
416
- } else {
417
- result[key] = value;
418
- }
437
+ result[key] = isPlainObject(value) ? getValues(value, source, options) : value;
419
438
  } else {
420
439
  result[key] = value;
421
440
  }
@@ -4,30 +4,24 @@
4
4
  * Thin async wrapper around getValues that adds support for async protocol handlers.
5
5
  * For synchronous-only use cases, import getValues/getValue directly for better performance.
6
6
  *
7
+ * The outer-grammar primitives (`hasProtocol`, `parseProtocolRef`, `isPlainObject`)
8
+ * live in getValues.js and are re-exported here for convenience.
9
+ *
7
10
  * Re-exports ResolveValuesOptions for backward compatibility.
8
11
  */
9
- import { getValue, getValues } from './getValues.js';
12
+ import { getValue, getValues, hasProtocol, isPlainObject, parseProtocolRef } from './getValues.js';
10
13
  // Re-export getValue as resolveValue for backward compatibility
11
14
  export { getValue as resolveValue };
12
- /**
13
- * Checks if a string value looks like a protocol reference.
14
- */
15
- function hasProtocol(value) {
16
- return value.includes('://');
17
- }
15
+ // Re-export the shared outer-grammar primitives (defined in getValues.js)
16
+ export { hasProtocol, parseProtocolRef, isPlainObject };
18
17
  /**
19
18
  * Resolves a protocol-prefixed value asynchronously.
20
19
  */
21
20
  async function resolveProtocolValue(value, protocols, options) {
22
- const protoEnd = value.indexOf('://');
23
- const protocol = value.substring(0, protoEnd);
21
+ const { protocol, key, path } = parseProtocolRef(value);
24
22
  const handler = protocols[protocol];
25
23
  if (!handler)
26
24
  return value;
27
- const rest = value.substring(protoEnd + 3);
28
- const pathStart = rest.indexOf('?.');
29
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
30
- const path = pathStart === -1 ? null : rest.substring(pathStart);
31
25
  const resolved = await handler(key);
32
26
  if (path) {
33
27
  return getValue(path, resolved, options);
@@ -52,8 +46,7 @@ async function resolveArray(arr, source, protocols, options) {
52
46
  result.push(await resolveArray(item, source, protocols, options));
53
47
  }
54
48
  else if (item && typeof item === 'object') {
55
- const proto = Object.getPrototypeOf(item);
56
- if (proto === Object.prototype || proto === null) {
49
+ if (isPlainObject(item)) {
57
50
  result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
58
51
  }
59
52
  else {
@@ -93,8 +86,7 @@ export async function resolveValues(pattern, source, options) {
93
86
  result[key] = await resolveArray(value, source, protocols, options);
94
87
  }
95
88
  else if (typeof value === 'object' && value !== null) {
96
- const proto = Object.getPrototypeOf(value);
97
- if (proto === Object.prototype || proto === null) {
89
+ if (isPlainObject(value)) {
98
90
  result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
99
91
  }
100
92
  else {
@@ -1,126 +1,116 @@
1
- /**
2
- * resolveValues.ts — Async value resolution for path strings.
3
- *
4
- * Thin async wrapper around getValues that adds support for async protocol handlers.
5
- * For synchronous-only use cases, import getValues/getValue directly for better performance.
6
- *
7
- * Re-exports ResolveValuesOptions for backward compatibility.
8
- */
9
-
10
- import { getValue, getValues } from './getValues.js';
11
- import type { ResolveValuesOptions } from '../types/assign-gingerly/types.js';
12
-
13
- export type { ResolveValuesOptions };
14
-
15
- // Re-export getValue as resolveValue for backward compatibility
16
- export { getValue as resolveValue };
17
-
18
- /**
19
- * Checks if a string value looks like a protocol reference.
20
- */
21
- function hasProtocol(value: string): boolean {
22
- return value.includes('://');
23
- }
24
-
25
-
26
- /**
27
- * Resolves a protocol-prefixed value asynchronously.
28
- */
29
- async function resolveProtocolValue(
30
- value: string,
31
- protocols: Record<string, (key: string) => any | Promise<any>>,
32
- options?: ResolveValuesOptions
33
- ): Promise<any> {
34
- const protoEnd = value.indexOf('://');
35
- const protocol = value.substring(0, protoEnd);
36
-
37
- const handler = protocols[protocol];
38
- if (!handler) return value;
39
-
40
- const rest = value.substring(protoEnd + 3);
41
- const pathStart = rest.indexOf('?.');
42
- const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
43
- const path = pathStart === -1 ? null : rest.substring(pathStart);
44
-
45
- const resolved = await handler(key);
46
-
47
- if (path) {
48
- return getValue(path, resolved, options);
49
- }
50
- return resolved;
51
- }
52
-
53
- /**
54
- * Resolve path strings and protocol references within an array (async).
55
- */
56
- async function resolveArray(
57
- arr: any[],
58
- source: any,
59
- protocols: Record<string, (key: string) => any | Promise<any>> | undefined,
60
- options?: ResolveValuesOptions
61
- ): Promise<any[]> {
62
- const result: any[] = [];
63
- for (const item of arr) {
64
- if (typeof item === 'string') {
65
- if (protocols && hasProtocol(item)) {
66
- result.push(await resolveProtocolValue(item, protocols, options));
67
- } else {
68
- result.push(getValue(item, source, options));
69
- }
70
- } else if (Array.isArray(item)) {
71
- result.push(await resolveArray(item, source, protocols, options));
72
- } else if (item && typeof item === 'object') {
73
- const proto = Object.getPrototypeOf(item);
74
- if (proto === Object.prototype || proto === null) {
75
- result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
76
- } else {
77
- result.push(item);
78
- }
79
- } else {
80
- result.push(item);
81
- }
82
- }
83
- return result;
84
- }
85
-
86
- /**
87
- * Async resolve RHS path strings in a pattern object against a source object.
88
- *
89
- * Supports async protocol handlers (e.g., fetch, IndexedDB).
90
- * For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
91
- *
92
- * @param pattern - Object whose RHS values may contain `?.` path strings
93
- * @param source - Object to resolve paths against
94
- * @param options - Optional withMethods, aka, and protocol handlers
95
- * @returns New object with path strings replaced by resolved values
96
- */
97
- export async function resolveValues(
98
- pattern: Record<string, any>,
99
- source: any,
100
- options?: ResolveValuesOptions
101
- ): Promise<Record<string, any>> {
102
- const protocols = options?.protocols;
103
-
104
- const result: Record<string, any> = {};
105
- for (const [key, value] of Object.entries(pattern)) {
106
- if (typeof value === 'string') {
107
- if (protocols && hasProtocol(value)) {
108
- result[key] = await resolveProtocolValue(value, protocols, options);
109
- } else {
110
- result[key] = getValue(value, source, options);
111
- }
112
- } else if (Array.isArray(value)) {
113
- result[key] = await resolveArray(value, source, protocols, options);
114
- } else if (typeof value === 'object' && value !== null) {
115
- const proto = Object.getPrototypeOf(value);
116
- if (proto === Object.prototype || proto === null) {
117
- result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
118
- } else {
119
- result[key] = value;
120
- }
121
- } else {
122
- result[key] = value;
123
- }
124
- }
125
- return result;
126
- }
1
+ /**
2
+ * resolveValues.ts — Async value resolution for path strings.
3
+ *
4
+ * Thin async wrapper around getValues that adds support for async protocol handlers.
5
+ * For synchronous-only use cases, import getValues/getValue directly for better performance.
6
+ *
7
+ * The outer-grammar primitives (`hasProtocol`, `parseProtocolRef`, `isPlainObject`)
8
+ * live in getValues.js and are re-exported here for convenience.
9
+ *
10
+ * Re-exports ResolveValuesOptions for backward compatibility.
11
+ */
12
+
13
+ import { getValue, getValues, hasProtocol, isPlainObject, parseProtocolRef } from './getValues.js';
14
+ import type { ProtocolHandlers, ResolveValuesOptions } from '../types/assign-gingerly/types.js';
15
+
16
+ export type { ResolveValuesOptions };
17
+
18
+ // Re-export getValue as resolveValue for backward compatibility
19
+ export { getValue as resolveValue };
20
+
21
+ // Re-export the shared outer-grammar primitives (defined in getValues.js)
22
+ export { hasProtocol, parseProtocolRef, isPlainObject };
23
+
24
+ /**
25
+ * Resolves a protocol-prefixed value asynchronously.
26
+ */
27
+ async function resolveProtocolValue(
28
+ value: string,
29
+ protocols: ProtocolHandlers,
30
+ options?: ResolveValuesOptions
31
+ ): Promise<any> {
32
+ const { protocol, key, path } = parseProtocolRef(value);
33
+
34
+ const handler = protocols[protocol];
35
+ if (!handler) return value;
36
+
37
+ const resolved = await handler(key);
38
+
39
+ if (path) {
40
+ return getValue(path, resolved, options);
41
+ }
42
+ return resolved;
43
+ }
44
+
45
+ /**
46
+ * Resolve path strings and protocol references within an array (async).
47
+ */
48
+ async function resolveArray(
49
+ arr: any[],
50
+ source: any,
51
+ protocols: ProtocolHandlers | undefined,
52
+ options?: ResolveValuesOptions
53
+ ): Promise<any[]> {
54
+ const result: any[] = [];
55
+ for (const item of arr) {
56
+ if (typeof item === 'string') {
57
+ if (protocols && hasProtocol(item)) {
58
+ result.push(await resolveProtocolValue(item, protocols, options));
59
+ } else {
60
+ result.push(getValue(item, source, options));
61
+ }
62
+ } else if (Array.isArray(item)) {
63
+ result.push(await resolveArray(item, source, protocols, options));
64
+ } else if (item && typeof item === 'object') {
65
+ if (isPlainObject(item)) {
66
+ result.push(options?.protocols ? await resolveValues(item, source, options) : getValues(item, source, options));
67
+ } else {
68
+ result.push(item);
69
+ }
70
+ } else {
71
+ result.push(item);
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Async resolve RHS path strings in a pattern object against a source object.
79
+ *
80
+ * Supports async protocol handlers (e.g., fetch, IndexedDB).
81
+ * For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
82
+ *
83
+ * @param pattern - Object whose RHS values may contain `?.` path strings
84
+ * @param source - Object to resolve paths against
85
+ * @param options - Optional withMethods, aka, and protocol handlers
86
+ * @returns New object with path strings replaced by resolved values
87
+ */
88
+ export async function resolveValues(
89
+ pattern: Record<string, any>,
90
+ source: any,
91
+ options?: ResolveValuesOptions
92
+ ): Promise<Record<string, any>> {
93
+ const protocols = options?.protocols;
94
+
95
+ const result: Record<string, any> = {};
96
+ for (const [key, value] of Object.entries(pattern)) {
97
+ if (typeof value === 'string') {
98
+ if (protocols && hasProtocol(value)) {
99
+ result[key] = await resolveProtocolValue(value, protocols, options);
100
+ } else {
101
+ result[key] = getValue(value, source, options);
102
+ }
103
+ } else if (Array.isArray(value)) {
104
+ result[key] = await resolveArray(value, source, protocols, options);
105
+ } else if (typeof value === 'object' && value !== null) {
106
+ if (isPlainObject(value)) {
107
+ result[key] = options?.protocols ? await resolveValues(value, source, options) : getValues(value, source, options);
108
+ } else {
109
+ result[key] = value;
110
+ }
111
+ } else {
112
+ result[key] = value;
113
+ }
114
+ }
115
+ return result;
116
+ }
@@ -72,6 +72,8 @@ export interface EnhancementConfig<T = any, Obj = Element> extends EnhancementCo
72
72
  * Calls assignFeatures(spawn, features) automatically on registration.
73
73
  */
74
74
  features?: FeatureConfigsMap;
75
+
76
+ customData?: any;
75
77
 
76
78
  }
77
79
 
@@ -406,7 +408,7 @@ export interface AssignFromOptions {
406
408
  from: any;
407
409
 
408
410
  /** Protocol handlers (sync or async) */
409
- protocols?: Record<string, (key: string) => any | Promise<any>>;
411
+ protocols?: ProtocolHandlers;
410
412
 
411
413
  /** Method names to call during path evaluation (append `|` to a path segment for a zero-argument call) */
412
414
  withMethods?: string[] | Set<string>;
@@ -480,6 +482,37 @@ export interface AssignFromOptions {
480
482
  [key: string]: any;
481
483
  }
482
484
 
485
+ /**
486
+ * A protocol handler resolves the key portion of a protocol-prefixed value
487
+ * (the text between `://` and the first `?.`) to a value. May be sync or async.
488
+ */
489
+ export type ProtocolHandler = (key: string) => any | Promise<any>;
490
+
491
+ /**
492
+ * A synchronous-only protocol handler, as accepted by getValues / getValue /
493
+ * assignFrom. For handlers that may return a Promise, use ProtocolHandler.
494
+ */
495
+ export type SyncProtocolHandler = (key: string) => any;
496
+
497
+ /** Map of protocol name (the text before `://`) to a handler (sync or async). */
498
+ export type ProtocolHandlers = Record<string, ProtocolHandler>;
499
+
500
+ /** Map of protocol name to a synchronous handler. */
501
+ export type SyncProtocolHandlers = Record<string, SyncProtocolHandler>;
502
+
503
+ /**
504
+ * The outer grammar of a protocol-prefixed value string,
505
+ * `‹protocol›://‹key›?.‹path›`, as produced by `parseProtocolRef`.
506
+ */
507
+ export interface ParsedProtocolRef {
508
+ /** Text before `://`; `''` when the value contains no `://`. */
509
+ protocol: string;
510
+ /** Text between `://` and the first `?.` (or the end of the string). */
511
+ key: string;
512
+ /** Text from the first `?.` onward (always starts with `?.`), or `null`. */
513
+ path: string | null;
514
+ }
515
+
483
516
  /**
484
517
  * Options for synchronous value resolution (getValues / getValue).
485
518
  * Extends IAssignGingerlyOptions with synchronous protocol handlers.
@@ -507,7 +540,7 @@ export interface GetValuesOptions extends IAssignGingerlyOptions {
507
540
  * localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
508
541
  * }
509
542
  */
510
- protocols?: Record<string, (key: string) => any>;
543
+ protocols?: SyncProtocolHandlers;
511
544
  }
512
545
 
513
546
  /**
@@ -530,7 +563,7 @@ export interface ResolveValuesOptions extends IAssignGingerlyOptions {
530
563
  * Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
531
564
  * Each handler receives the key portion and returns the resolved value (sync or async).
532
565
  */
533
- protocols?: Record<string, (key: string) => any | Promise<any>>;
566
+ protocols?: ProtocolHandlers;
534
567
  }
535
568
 
536
569
  /**