assign-gingerly 0.0.51 → 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 +96 -0
- package/assignFrom.js +12 -3
- package/assignFrom.ts +17 -6
- package/package.json +2 -2
- 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
|
|
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/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": {
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@playwright/test": "1.60.0",
|
|
99
99
|
"spa-ssi": "0.0.27",
|
|
100
|
-
"@types/node": "25.
|
|
100
|
+
"@types/node": "25.9.3",
|
|
101
101
|
"typescript": "6.0.3"
|
|
102
102
|
}
|
|
103
103
|
}
|
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> = {
|