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 +79 -3
- package/assignGingerly.js +18 -1
- package/assignGingerly.ts +18 -1
- package/index.js +1 -0
- package/index.ts +1 -0
- package/inferencer/types/EnhancementConversionInstructions.md +9 -1
- package/inferencer/types/ImportantEnhancementAddendum.md +46 -0
- package/inferencer/types/assign-gingerly/types.d.ts +36 -3
- package/inferencer/types/be-literate/types.d.ts +1 -1
- package/inferencer/types/be-persistent/types.d.ts +81 -0
- package/inferencer/types/gist-in/types.d.ts +59 -0
- package/object-extension.js +10 -0
- package/object-extension.ts +12 -1
- package/package.json +3 -3
- package/resolve/getValues.js +41 -22
- package/resolve/getValues.ts +47 -28
- package/resolve/resolveValues.js +9 -17
- package/resolve/resolveValues.ts +116 -126
- package/types/assign-gingerly/types.d.ts +36 -3
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
|
-
*
|
|
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
|
-
*
|
|
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.).
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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
|
-
*
|
|
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
|
+
}
|
package/object-extension.js
CHANGED
|
@@ -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
|
}
|
package/object-extension.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
232
|
-
"spa-ssi": "0.0.
|
|
231
|
+
"@playwright/test": "1.63.0",
|
|
232
|
+
"spa-ssi": "0.0.28"
|
|
233
233
|
}
|
|
234
234
|
}
|
package/resolve/getValues.js
CHANGED
|
@@ -183,11 +183,47 @@ function navigatePath(source, parts, withMethods, permissionProcessor) {
|
|
|
183
183
|
return current;
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/resolve/getValues.ts
CHANGED
|
@@ -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
|
-
*
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
364
|
+
protocols: SyncProtocolHandlers,
|
|
329
365
|
options?: GetValuesOptions
|
|
330
366
|
): any {
|
|
331
|
-
const
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/resolve/resolveValues.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/resolve/resolveValues.ts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (typeof item === '
|
|
65
|
-
if (
|
|
66
|
-
result.push(await
|
|
67
|
-
} else {
|
|
68
|
-
result.push(
|
|
69
|
-
}
|
|
70
|
-
} else
|
|
71
|
-
result.push(
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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?:
|
|
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?:
|
|
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?:
|
|
566
|
+
protocols?: ProtocolHandlers;
|
|
534
567
|
}
|
|
535
568
|
|
|
536
569
|
/**
|