assign-gingerly 0.0.50 → 0.0.52
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 +97 -0
- package/assignFrom.js +12 -3
- package/assignFrom.ts +17 -6
- package/index.js +1 -0
- package/index.ts +1 -0
- package/package.json +6 -2
- package/resolveAndAssignFeatures.js +67 -0
- package/resolveAndAssignFeatures.ts +79 -0
- package/resolveValues.js +41 -1
- package/resolveValues.ts +67 -2
- package/types/assign-gingerly/types.d.ts +7 -0
package/README.md
CHANGED
|
@@ -3366,6 +3366,65 @@ assignFrom(target, {
|
|
|
3366
3366
|
|
|
3367
3367
|
For full documentation, see [docs/assignFrom.md](docs/assignFrom.md).
|
|
3368
3368
|
|
|
3369
|
+
## Protocol Resolution in `resolveValues` and `assignFrom`
|
|
3370
|
+
|
|
3371
|
+
`resolveValues` (and by extension `assignFrom`) supports resolving values from external sources via protocol-prefixed strings. This enables declarative references to `globalThis`, `localStorage`, `sessionStorage`, or custom stores.
|
|
3372
|
+
|
|
3373
|
+
```JavaScript
|
|
3374
|
+
import { resolveValues } from 'assign-gingerly/resolveValues.js';
|
|
3375
|
+
|
|
3376
|
+
const result = await resolveValues({
|
|
3377
|
+
baseURL: 'globalThis://myAppConfig?.apiBaseUrl',
|
|
3378
|
+
authToken: 'localStorage://auth?.token',
|
|
3379
|
+
label: '?.title' // normal path resolution still works
|
|
3380
|
+
}, source, {
|
|
3381
|
+
protocols: {
|
|
3382
|
+
globalThis: (key) => globalThis[key],
|
|
3383
|
+
localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
```
|
|
3387
|
+
|
|
3388
|
+
**How it works:**
|
|
3389
|
+
|
|
3390
|
+
1. If a value contains `://` and the part before it matches a key in `protocols`, it's treated as a protocol reference.
|
|
3391
|
+
2. The protocol handler is called with the key portion (between `://` and the first `?.`, or end of string).
|
|
3392
|
+
3. If a `?.` path follows the key, it's resolved against the handler's result using `resolveValue`.
|
|
3393
|
+
4. If the protocol isn't found in the map, the value passes through unchanged (no error).
|
|
3394
|
+
|
|
3395
|
+
**Path after protocol key:**
|
|
3396
|
+
|
|
3397
|
+
```JavaScript
|
|
3398
|
+
// 'globalThis://myConfig?.database?.host'
|
|
3399
|
+
// 1. Protocol: 'globalThis'
|
|
3400
|
+
// 2. Key: 'myConfig'
|
|
3401
|
+
// 3. Handler returns: globalThis['myConfig'] → { database: { host: 'localhost' } }
|
|
3402
|
+
// 4. Remaining path: '?.database?.host' → resolves to 'localhost'
|
|
3403
|
+
```
|
|
3404
|
+
|
|
3405
|
+
**With `assignFrom` and the `"..."` spread key:**
|
|
3406
|
+
|
|
3407
|
+
`assignFrom` supports a special `"..."` key that spreads the resolved value into the parent object:
|
|
3408
|
+
|
|
3409
|
+
```JavaScript
|
|
3410
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
3411
|
+
|
|
3412
|
+
await assignFrom(myForm, {
|
|
3413
|
+
"...": "globalThis://qmywdO1vr0SwyuIe4fvzxQ",
|
|
3414
|
+
path: "api/v2/:operation/:expression",
|
|
3415
|
+
headers: {
|
|
3416
|
+
"...": "globalThis://rPpwNLcYsUOjFcg+N8lmOA"
|
|
3417
|
+
}
|
|
3418
|
+
}, {
|
|
3419
|
+
from: source,
|
|
3420
|
+
protocols: { globalThis: (key) => globalThis[key] }
|
|
3421
|
+
});
|
|
3422
|
+
```
|
|
3423
|
+
|
|
3424
|
+
The `"..."` key causes the resolved object to be merged (spread) into the result before passing to `assignGingerly`, rather than being assigned to a property named `"..."`.
|
|
3425
|
+
|
|
3426
|
+
**Note:** Both `resolveValues` and `assignFrom` are async (return Promises) to support async protocol handlers (e.g., IndexedDB, fetch). For patterns without protocols, the async overhead is negligible.
|
|
3427
|
+
|
|
3369
3428
|
## Custom Assignment with `static assignTo` Protocol
|
|
3370
3429
|
|
|
3371
3430
|
Classes can opt into custom assignment behavior by defining a `static assignTo` method. When `assignGingerly` encounters a property whose current value is an instance of such a class, it delegates the assignment to `assignTo` instead of performing the default merge/replace logic.
|
|
@@ -4845,6 +4904,43 @@ It resolves async `fallbackSpawn` implementations from the base class, creates a
|
|
|
4845
4904
|
|
|
4846
4905
|
For full documentation, see [docs/defineWithFeatures.md](docs/defineWithFeatures.md).
|
|
4847
4906
|
|
|
4907
|
+
### Resolving async spawns with `resolveAndAssignFeatures`
|
|
4908
|
+
|
|
4909
|
+
When defining a custom element via a traditional JS module (rather than declaratively via cede scripts), `resolveAndAssignFeatures` handles the boilerplate of resolving async `fallbackSpawn` implementations before calling `assignFeatures`:
|
|
4910
|
+
|
|
4911
|
+
```JavaScript
|
|
4912
|
+
import { resolveAndAssignFeatures } from 'assign-gingerly/resolveAndAssignFeatures.js';
|
|
4913
|
+
|
|
4914
|
+
export async function wireFeatures(ElementClass, cfg) {
|
|
4915
|
+
const { roundabout } = cfg.features;
|
|
4916
|
+
const { customData, withAttrs } = roundabout;
|
|
4917
|
+
|
|
4918
|
+
await resolveAndAssignFeatures(ElementClass, {
|
|
4919
|
+
timeTicker: { spawn: TimeTicker }, // explicit spawn — used as-is
|
|
4920
|
+
faceUp: { // no spawn — resolved from fallbackSpawn
|
|
4921
|
+
callbackForwarding: ['connectedCallback', 'disconnectedCallback']
|
|
4922
|
+
},
|
|
4923
|
+
roundabout: { // no spawn — resolved from fallbackSpawn
|
|
4924
|
+
customData,
|
|
4925
|
+
withAttrs,
|
|
4926
|
+
callbackForwarding: ['connectedCallback']
|
|
4927
|
+
}
|
|
4928
|
+
});
|
|
4929
|
+
}
|
|
4930
|
+
```
|
|
4931
|
+
|
|
4932
|
+
**What it does:**
|
|
4933
|
+
|
|
4934
|
+
For each feature in the config that doesn't have an explicit `spawn`, it resolves the async `fallbackSpawn` from the class's `static supportedFeatures`, sets it as the spawn, then calls `assignFeatures`. Features with an explicit `spawn` are left untouched.
|
|
4935
|
+
|
|
4936
|
+
**When to use it:**
|
|
4937
|
+
|
|
4938
|
+
- Defining custom elements via JS modules (the traditional `import` + `define` pattern)
|
|
4939
|
+
- When the base class uses async `fallbackSpawn` for lazy loading but you want synchronous feature access after registration
|
|
4940
|
+
- As a reusable `wireFeatures` function that multiple element definitions can share
|
|
4941
|
+
|
|
4942
|
+
See [time-ticker/wireFeatures.js](https://github.com/bahrus/time-ticker/blob/baseline/wireFeatures.js) for a real-world example.
|
|
4943
|
+
|
|
4848
4944
|
<details>
|
|
4849
4945
|
<summary>Catalog of Published Custom Element Features</summary>
|
|
4850
4946
|
|
|
@@ -4855,6 +4951,7 @@ For full documentation, see [docs/defineWithFeatures.md](docs/defineWithFeatures
|
|
|
4855
4951
|
| [face-up](https://www.npmjs.com/package/face-up) | Form Associated Custom Element behavior via ElementInternals | [GitHub](https://github.com/bahrus/face-up) |
|
|
4856
4952
|
| [roundabout](https://www.npmjs.com/package/roundabout) | Reactive view-model binding with template rendering and computed property orchestration | [GitHub](https://github.com/bahrus/roundabout#using-roundaboutfeature-with-assignfeatures) |
|
|
4857
4953
|
| [time-ticker](https://www.npmjs.com/package/time-ticker) | Web component that fires events periodically (example of a feature-based component with no code in the class) | [GitHub](https://github.com/bahrus/time-ticker) |
|
|
4954
|
+
| [templ-maker](https://www.npmjs.com/package/templ-maker) | Extracts a DOM fragment into a reusable template and clones it per instance (works with cede scripts) | [GitHub](https://github.com/bahrus/templ-maker) |
|
|
4858
4955
|
|
|
4859
4956
|
</details>
|
|
4860
4957
|
|
package/assignFrom.js
CHANGED
|
@@ -21,10 +21,19 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { resolveValues } from './resolveValues.js';
|
|
23
23
|
import assignGingerly from './assignGingerly.js';
|
|
24
|
-
export function assignFrom(target, pattern, options) {
|
|
25
|
-
const resolved = resolveValues(pattern, options.from, {
|
|
24
|
+
export async function assignFrom(target, pattern, options) {
|
|
25
|
+
const resolved = await resolveValues(pattern, options.from, {
|
|
26
26
|
withMethods: options.withMethods,
|
|
27
|
-
aka: options.aka
|
|
27
|
+
aka: options.aka,
|
|
28
|
+
protocols: options.protocols
|
|
28
29
|
});
|
|
30
|
+
// Handle "..." spread key — merge resolved value into parent
|
|
31
|
+
if ('...' in resolved) {
|
|
32
|
+
const spreadValue = resolved['...'];
|
|
33
|
+
if (spreadValue && typeof spreadValue === 'object') {
|
|
34
|
+
Object.assign(resolved, spreadValue);
|
|
35
|
+
}
|
|
36
|
+
delete resolved['...'];
|
|
37
|
+
}
|
|
29
38
|
return assignGingerly(target, resolved, options);
|
|
30
39
|
}
|
package/assignFrom.ts
CHANGED
|
@@ -19,22 +19,33 @@
|
|
|
19
19
|
* }, { from: source });
|
|
20
20
|
* // target is now { color: 'red', text: 'Hello' }
|
|
21
21
|
*/
|
|
22
|
-
import { resolveValues } from './resolveValues.js';
|
|
22
|
+
import { resolveValues, ResolveValuesOptions } from './resolveValues.js';
|
|
23
23
|
import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
|
|
24
24
|
|
|
25
|
-
export interface AssignFromOptions extends IAssignGingerlyOptions {
|
|
25
|
+
export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValuesOptions {
|
|
26
26
|
/** Source object to resolve RHS path strings against */
|
|
27
27
|
from: any;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function assignFrom(
|
|
30
|
+
export async function assignFrom(
|
|
31
31
|
target: any,
|
|
32
32
|
pattern: Record<string, any>,
|
|
33
33
|
options: AssignFromOptions
|
|
34
|
-
): any {
|
|
35
|
-
const resolved = resolveValues(pattern, options.from, {
|
|
34
|
+
): Promise<any> {
|
|
35
|
+
const resolved = await resolveValues(pattern, options.from, {
|
|
36
36
|
withMethods: options.withMethods,
|
|
37
|
-
aka: options.aka
|
|
37
|
+
aka: options.aka,
|
|
38
|
+
protocols: options.protocols
|
|
38
39
|
});
|
|
40
|
+
|
|
41
|
+
// Handle "..." spread key — merge resolved value into parent
|
|
42
|
+
if ('...' in resolved) {
|
|
43
|
+
const spreadValue = resolved['...'];
|
|
44
|
+
if (spreadValue && typeof spreadValue === 'object') {
|
|
45
|
+
Object.assign(resolved, spreadValue);
|
|
46
|
+
}
|
|
47
|
+
delete resolved['...'];
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
return assignGingerly(target, resolved, options);
|
|
40
51
|
}
|
package/index.js
CHANGED
|
@@ -12,4 +12,5 @@ export { assignFrom } from './assignFrom.js';
|
|
|
12
12
|
export { assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions } from './assignFeatures.js';
|
|
13
13
|
export { installForwarding } from './installForwarding.js';
|
|
14
14
|
export { defineWithFeatures } from './defineWithFeatures.js';
|
|
15
|
+
export { resolveAndAssignFeatures } from './resolveAndAssignFeatures.js';
|
|
15
16
|
import './object-extension.js';
|
package/index.ts
CHANGED
|
@@ -12,4 +12,5 @@ export {assignFrom} from './assignFrom.js';
|
|
|
12
12
|
export {assignFeatures, FeaturesRegistry, captureFeatureInitVals, PropertyBag, suggestFeatureInfo, getFeatureInfoSuggestions} from './assignFeatures.js';
|
|
13
13
|
export {installForwarding} from './installForwarding.js';
|
|
14
14
|
export {defineWithFeatures} from './defineWithFeatures.js';
|
|
15
|
+
export {resolveAndAssignFeatures} from './resolveAndAssignFeatures.js';
|
|
15
16
|
import './object-extension.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
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": {
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"default": "./defineWithFeatures.js",
|
|
69
69
|
"types": "./defineWithFeatures.ts"
|
|
70
70
|
},
|
|
71
|
+
"./resolveAndAssignFeatures.js": {
|
|
72
|
+
"default": "./resolveAndAssignFeatures.js",
|
|
73
|
+
"types": "./resolveAndAssignFeatures.ts"
|
|
74
|
+
},
|
|
71
75
|
"./assignFrom.js": {
|
|
72
76
|
"default": "./assignFrom.js",
|
|
73
77
|
"types": "./assignFrom.ts"
|
|
@@ -93,7 +97,7 @@
|
|
|
93
97
|
"devDependencies": {
|
|
94
98
|
"@playwright/test": "1.60.0",
|
|
95
99
|
"spa-ssi": "0.0.27",
|
|
96
|
-
"@types/node": "25.
|
|
100
|
+
"@types/node": "25.9.3",
|
|
97
101
|
"typescript": "6.0.3"
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveAndAssignFeatures - Resolves async fallback spawns then calls assignFeatures.
|
|
3
|
+
*
|
|
4
|
+
* For each feature in the config that doesn't have an explicit `spawn`, resolves
|
|
5
|
+
* the async `fallbackSpawn` from the class's `static supportedFeatures` and sets
|
|
6
|
+
* it as the spawn. Then calls `assignFeatures` with the fully resolved config.
|
|
7
|
+
*
|
|
8
|
+
* This eliminates the boilerplate of manually resolving async spawns before
|
|
9
|
+
* calling assignFeatures.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import { resolveAndAssignFeatures } from 'assign-gingerly/resolveAndAssignFeatures.js';
|
|
13
|
+
*
|
|
14
|
+
* await resolveAndAssignFeatures(MyElement, {
|
|
15
|
+
* roundabout: {
|
|
16
|
+
* customData: {...},
|
|
17
|
+
* withAttrs: {...},
|
|
18
|
+
* callbackForwarding: ['connectedCallback']
|
|
19
|
+
* },
|
|
20
|
+
* faceUp: {
|
|
21
|
+
* callbackForwarding: ['connectedCallback', 'disconnectedCallback']
|
|
22
|
+
* }
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Determines if a function is an async spawner.
|
|
27
|
+
*/
|
|
28
|
+
function isAsyncSpawn(fn) {
|
|
29
|
+
if (typeof fn !== 'function')
|
|
30
|
+
return false;
|
|
31
|
+
if (fn.constructor.name === 'AsyncFunction')
|
|
32
|
+
return true;
|
|
33
|
+
if (fn.prototype === undefined)
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves async fallback spawns for features that don't have an explicit spawn,
|
|
39
|
+
* then calls assignFeatures on the registry.
|
|
40
|
+
*
|
|
41
|
+
* @param ElementClass - The custom element class (must have static supportedFeatures)
|
|
42
|
+
* @param featuresConfig - Feature configurations (spawn will be resolved from fallbackSpawn if missing)
|
|
43
|
+
* @param registry - Optional CustomElementRegistry (defaults to global customElements)
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveAndAssignFeatures(ElementClass, featuresConfig, registry) {
|
|
46
|
+
const supportedFeatures = ElementClass.supportedFeatures;
|
|
47
|
+
if (!supportedFeatures) {
|
|
48
|
+
throw new Error(`resolveAndAssignFeatures: ${ElementClass.name || 'constructor'} does not define static supportedFeatures`);
|
|
49
|
+
}
|
|
50
|
+
// Resolve async fallback spawns in parallel for features without explicit spawn
|
|
51
|
+
await Promise.all(Object.entries(featuresConfig).map(async ([key, featureConfig]) => {
|
|
52
|
+
// Skip if spawn is already provided
|
|
53
|
+
if (featureConfig.spawn)
|
|
54
|
+
return;
|
|
55
|
+
const optIn = supportedFeatures[key];
|
|
56
|
+
if (!optIn?.fallbackSpawn)
|
|
57
|
+
return;
|
|
58
|
+
let spawn = optIn.fallbackSpawn;
|
|
59
|
+
if (isAsyncSpawn(spawn)) {
|
|
60
|
+
spawn = await spawn();
|
|
61
|
+
}
|
|
62
|
+
featureConfig.spawn = spawn;
|
|
63
|
+
}));
|
|
64
|
+
// Call assignFeatures on the registry
|
|
65
|
+
const reg = registry || customElements;
|
|
66
|
+
await reg.assignFeatures(ElementClass, featuresConfig);
|
|
67
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveAndAssignFeatures - Resolves async fallback spawns then calls assignFeatures.
|
|
3
|
+
*
|
|
4
|
+
* For each feature in the config that doesn't have an explicit `spawn`, resolves
|
|
5
|
+
* the async `fallbackSpawn` from the class's `static supportedFeatures` and sets
|
|
6
|
+
* it as the spawn. Then calls `assignFeatures` with the fully resolved config.
|
|
7
|
+
*
|
|
8
|
+
* This eliminates the boilerplate of manually resolving async spawns before
|
|
9
|
+
* calling assignFeatures.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import { resolveAndAssignFeatures } from 'assign-gingerly/resolveAndAssignFeatures.js';
|
|
13
|
+
*
|
|
14
|
+
* await resolveAndAssignFeatures(MyElement, {
|
|
15
|
+
* roundabout: {
|
|
16
|
+
* customData: {...},
|
|
17
|
+
* withAttrs: {...},
|
|
18
|
+
* callbackForwarding: ['connectedCallback']
|
|
19
|
+
* },
|
|
20
|
+
* faceUp: {
|
|
21
|
+
* callbackForwarding: ['connectedCallback', 'disconnectedCallback']
|
|
22
|
+
* }
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { FeatureConfigsMap, SupportedFeaturesMap } from './types/assign-gingerly/types.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determines if a function is an async spawner.
|
|
30
|
+
*/
|
|
31
|
+
function isAsyncSpawn(fn: any): boolean {
|
|
32
|
+
if (typeof fn !== 'function') return false;
|
|
33
|
+
if (fn.constructor.name === 'AsyncFunction') return true;
|
|
34
|
+
if (fn.prototype === undefined) return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves async fallback spawns for features that don't have an explicit spawn,
|
|
40
|
+
* then calls assignFeatures on the registry.
|
|
41
|
+
*
|
|
42
|
+
* @param ElementClass - The custom element class (must have static supportedFeatures)
|
|
43
|
+
* @param featuresConfig - Feature configurations (spawn will be resolved from fallbackSpawn if missing)
|
|
44
|
+
* @param registry - Optional CustomElementRegistry (defaults to global customElements)
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveAndAssignFeatures(
|
|
47
|
+
ElementClass: Function,
|
|
48
|
+
featuresConfig: FeatureConfigsMap,
|
|
49
|
+
registry?: any
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const supportedFeatures: SupportedFeaturesMap | undefined = (ElementClass as any).supportedFeatures;
|
|
52
|
+
|
|
53
|
+
if (!supportedFeatures) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`resolveAndAssignFeatures: ${(ElementClass as any).name || 'constructor'} does not define static supportedFeatures`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Resolve async fallback spawns in parallel for features without explicit spawn
|
|
60
|
+
await Promise.all(
|
|
61
|
+
Object.entries(featuresConfig).map(async ([key, featureConfig]) => {
|
|
62
|
+
// Skip if spawn is already provided
|
|
63
|
+
if (featureConfig.spawn) return;
|
|
64
|
+
|
|
65
|
+
const optIn = supportedFeatures[key];
|
|
66
|
+
if (!optIn?.fallbackSpawn) return;
|
|
67
|
+
|
|
68
|
+
let spawn = optIn.fallbackSpawn;
|
|
69
|
+
if (isAsyncSpawn(spawn)) {
|
|
70
|
+
spawn = await (spawn as () => Promise<any>)();
|
|
71
|
+
}
|
|
72
|
+
(featureConfig as any).spawn = spawn;
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Call assignFeatures on the registry
|
|
77
|
+
const reg = registry || customElements;
|
|
78
|
+
await reg.assignFeatures(ElementClass, featuresConfig);
|
|
79
|
+
}
|
package/resolveValues.js
CHANGED
|
@@ -25,6 +25,41 @@ function parseCachedPath(path) {
|
|
|
25
25
|
}
|
|
26
26
|
return parts;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolves a protocol-prefixed value (e.g., 'globalThis://key?.path').
|
|
30
|
+
*
|
|
31
|
+
* 1. Extracts the protocol name (before '://')
|
|
32
|
+
* 2. If the protocol isn't in the protocols map, returns the value unchanged (false positive)
|
|
33
|
+
* 3. Extracts the key (between '://' and first '?.' or end of string)
|
|
34
|
+
* 4. Calls the protocol handler with the key
|
|
35
|
+
* 5. If there's a remaining '?.' path, resolves it against the handler's result
|
|
36
|
+
*/
|
|
37
|
+
async function resolveProtocolValue(value, protocols, options) {
|
|
38
|
+
// Extract protocol name (before ://)
|
|
39
|
+
const protoEnd = value.indexOf('://');
|
|
40
|
+
const protocol = value.substring(0, protoEnd);
|
|
41
|
+
// Resolve via protocol handler
|
|
42
|
+
const handler = protocols[protocol];
|
|
43
|
+
if (!handler)
|
|
44
|
+
return value; // false flag — coincidentally looks like a protocol
|
|
45
|
+
const rest = value.substring(protoEnd + 3);
|
|
46
|
+
// Split at first ?. to separate key from path
|
|
47
|
+
const pathStart = rest.indexOf('?.');
|
|
48
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
49
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
50
|
+
const resolved = await handler(key);
|
|
51
|
+
// If there's a remaining path, resolve it against the result
|
|
52
|
+
if (path) {
|
|
53
|
+
return resolveValue(path, resolved, options);
|
|
54
|
+
}
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Checks if a string value looks like a protocol reference.
|
|
59
|
+
*/
|
|
60
|
+
function hasProtocol(value) {
|
|
61
|
+
return value.includes('://');
|
|
62
|
+
}
|
|
28
63
|
/**
|
|
29
64
|
* Navigate a path against a source object, optionally calling methods.
|
|
30
65
|
* Returns the resolved value at the end of the path.
|
|
@@ -96,7 +131,7 @@ function navigatePath(source, parts, withMethods) {
|
|
|
96
131
|
* aka: { 'q': 'querySelector' }
|
|
97
132
|
* });
|
|
98
133
|
*/
|
|
99
|
-
export function resolveValues(pattern, source, options) {
|
|
134
|
+
export async function resolveValues(pattern, source, options) {
|
|
100
135
|
// Build alias map
|
|
101
136
|
const aliasMap = new Map();
|
|
102
137
|
if (options?.aka) {
|
|
@@ -110,6 +145,7 @@ export function resolveValues(pattern, source, options) {
|
|
|
110
145
|
? options.withMethods
|
|
111
146
|
: new Set(options.withMethods)
|
|
112
147
|
: undefined;
|
|
148
|
+
const protocols = options?.protocols;
|
|
113
149
|
const result = {};
|
|
114
150
|
for (const [key, value] of Object.entries(pattern)) {
|
|
115
151
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
@@ -120,6 +156,10 @@ export function resolveValues(pattern, source, options) {
|
|
|
120
156
|
// Navigate with method support
|
|
121
157
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
122
158
|
}
|
|
159
|
+
else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
160
|
+
// Protocol-prefixed value — resolve asynchronously
|
|
161
|
+
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
162
|
+
}
|
|
123
163
|
else {
|
|
124
164
|
result[key] = value;
|
|
125
165
|
}
|
package/resolveValues.ts
CHANGED
|
@@ -13,6 +13,21 @@ export interface ResolveValuesOptions {
|
|
|
13
13
|
* Substituted before path resolution, matching complete tokens between `?.` delimiters.
|
|
14
14
|
*/
|
|
15
15
|
aka?: Record<string, string>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Protocol handlers for resolving protocol-prefixed values (e.g., 'globalThis://key').
|
|
19
|
+
* Each handler receives the key portion and returns the resolved value (sync or async).
|
|
20
|
+
*
|
|
21
|
+
* If a value contains '://' but the protocol isn't in this map, the value passes through unchanged.
|
|
22
|
+
* If a '?.' appears after the protocol key, the remaining path is resolved against the handler's result.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* protocols: {
|
|
26
|
+
* globalThis: (key) => globalThis[key],
|
|
27
|
+
* localStorage: (key) => JSON.parse(localStorage.getItem(key) || 'null')
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
protocols?: Record<string, (key: string) => any | Promise<any>>;
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
/**
|
|
@@ -44,6 +59,51 @@ function parseCachedPath(path: string): string[] {
|
|
|
44
59
|
return parts;
|
|
45
60
|
}
|
|
46
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Resolves a protocol-prefixed value (e.g., 'globalThis://key?.path').
|
|
64
|
+
*
|
|
65
|
+
* 1. Extracts the protocol name (before '://')
|
|
66
|
+
* 2. If the protocol isn't in the protocols map, returns the value unchanged (false positive)
|
|
67
|
+
* 3. Extracts the key (between '://' and first '?.' or end of string)
|
|
68
|
+
* 4. Calls the protocol handler with the key
|
|
69
|
+
* 5. If there's a remaining '?.' path, resolves it against the handler's result
|
|
70
|
+
*/
|
|
71
|
+
async function resolveProtocolValue(
|
|
72
|
+
value: string,
|
|
73
|
+
protocols: Record<string, (key: string) => any | Promise<any>>,
|
|
74
|
+
options?: ResolveValuesOptions
|
|
75
|
+
): Promise<any> {
|
|
76
|
+
// Extract protocol name (before ://)
|
|
77
|
+
const protoEnd = value.indexOf('://');
|
|
78
|
+
const protocol = value.substring(0, protoEnd);
|
|
79
|
+
|
|
80
|
+
// Resolve via protocol handler
|
|
81
|
+
const handler = protocols[protocol];
|
|
82
|
+
if (!handler) return value; // false flag — coincidentally looks like a protocol
|
|
83
|
+
|
|
84
|
+
const rest = value.substring(protoEnd + 3);
|
|
85
|
+
|
|
86
|
+
// Split at first ?. to separate key from path
|
|
87
|
+
const pathStart = rest.indexOf('?.');
|
|
88
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
89
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
90
|
+
|
|
91
|
+
const resolved = await handler(key);
|
|
92
|
+
|
|
93
|
+
// If there's a remaining path, resolve it against the result
|
|
94
|
+
if (path) {
|
|
95
|
+
return resolveValue(path, resolved, options);
|
|
96
|
+
}
|
|
97
|
+
return resolved;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Checks if a string value looks like a protocol reference.
|
|
102
|
+
*/
|
|
103
|
+
function hasProtocol(value: string): boolean {
|
|
104
|
+
return value.includes('://');
|
|
105
|
+
}
|
|
106
|
+
|
|
47
107
|
/**
|
|
48
108
|
* Navigate a path against a source object, optionally calling methods.
|
|
49
109
|
* Returns the resolved value at the end of the path.
|
|
@@ -120,11 +180,11 @@ function navigatePath(
|
|
|
120
180
|
* aka: { 'q': 'querySelector' }
|
|
121
181
|
* });
|
|
122
182
|
*/
|
|
123
|
-
export function resolveValues(
|
|
183
|
+
export async function resolveValues(
|
|
124
184
|
pattern: Record<string, any>,
|
|
125
185
|
source: any,
|
|
126
186
|
options?: ResolveValuesOptions
|
|
127
|
-
): Record<string, any
|
|
187
|
+
): Promise<Record<string, any>> {
|
|
128
188
|
// Build alias map
|
|
129
189
|
const aliasMap = new Map<string, string>();
|
|
130
190
|
if (options?.aka) {
|
|
@@ -139,6 +199,8 @@ export function resolveValues(
|
|
|
139
199
|
? options.withMethods
|
|
140
200
|
: new Set(options.withMethods)
|
|
141
201
|
: undefined;
|
|
202
|
+
|
|
203
|
+
const protocols = options?.protocols;
|
|
142
204
|
|
|
143
205
|
const result: Record<string, any> = {};
|
|
144
206
|
for (const [key, value] of Object.entries(pattern)) {
|
|
@@ -151,6 +213,9 @@ export function resolveValues(
|
|
|
151
213
|
|
|
152
214
|
// Navigate with method support
|
|
153
215
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
216
|
+
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
217
|
+
// Protocol-prefixed value — resolve asynchronously
|
|
218
|
+
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
154
219
|
} else {
|
|
155
220
|
result[key] = value;
|
|
156
221
|
}
|
|
@@ -184,6 +184,13 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
|
184
184
|
* Should make sure it is added to static observedAttribrutes
|
|
185
185
|
*/
|
|
186
186
|
sourceOfTruth?: boolean;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Options to pass to the parser function (e.g., splitStatements behavior).
|
|
190
|
+
* For named parsers like 'parse-pattern-statements', this is forwarded
|
|
191
|
+
* as the options argument to the underlying parse function.
|
|
192
|
+
*/
|
|
193
|
+
parserOptions?: any;
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
export type AttrPatterns<T = any> = {
|