assign-gingerly 0.0.82 → 0.0.84
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 +2 -1
- package/assignFrom.js +6 -3
- package/assignFrom.ts +6 -3
- package/assignFromAsync.js +4 -3
- package/assignFromAsync.ts +4 -3
- package/inferencer/types/NewHTMLFirstCustomElement.md +27 -1
- package/inferencer/types/assign-gingerly/types.d.ts +21 -0
- package/inferencer/types/el-maker/types.d.ts +2 -0
- package/inferencer/types/roundabout/types.d.ts +15 -1
- package/inferencer/types/swipe-dismiss/types.d.ts +43 -0
- package/package.json +1 -1
- package/processHandlerCommands.js +2 -0
- package/processHandlerCommands.ts +2 -0
- package/resolve/getValues.js +57 -9
- package/resolve/getValues.ts +71 -9
- package/types/assign-gingerly/types.d.ts +21 -0
package/README.md
CHANGED
|
@@ -100,7 +100,8 @@ assignFrom adds support for:
|
|
|
100
100
|
3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
|
|
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
|
-
6.
|
|
103
|
+
6. Dynamic substitutions via the `substitutions` option for injecting runtime string values into path segments. See [docs/substitutions.md](docs/substitutions.md).
|
|
104
|
+
7. Spread merging via the `"..."` key.
|
|
104
105
|
|
|
105
106
|
Example:
|
|
106
107
|
|
package/assignFrom.js
CHANGED
|
@@ -49,6 +49,7 @@ function resolveTernaryValue(value, source, options) {
|
|
|
49
49
|
return getValue(value, source, {
|
|
50
50
|
withMethods: options.withMethods,
|
|
51
51
|
aka: options.aka,
|
|
52
|
+
substitutions: options.substitutions,
|
|
52
53
|
protocols: options.protocols,
|
|
53
54
|
root: options.root
|
|
54
55
|
});
|
|
@@ -61,6 +62,7 @@ function resolveTernaryValue(value, source, options) {
|
|
|
61
62
|
return getValue(value, source, {
|
|
62
63
|
withMethods: options.withMethods,
|
|
63
64
|
aka: options.aka,
|
|
65
|
+
substitutions: options.substitutions,
|
|
64
66
|
protocols: options.protocols,
|
|
65
67
|
root: options.root
|
|
66
68
|
});
|
|
@@ -381,7 +383,7 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
|
|
|
381
383
|
const ids = getEffectiveIds(options);
|
|
382
384
|
if (!ids)
|
|
383
385
|
return;
|
|
384
|
-
const { withMethods, aka, akaMethods, protocols, from } = options;
|
|
386
|
+
const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
|
|
385
387
|
for (const key of idRefNormalKeys) {
|
|
386
388
|
const parsed = parseIdRef(key);
|
|
387
389
|
if (!parsed)
|
|
@@ -391,11 +393,11 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
|
|
|
391
393
|
continue;
|
|
392
394
|
const value = expandedPattern[key];
|
|
393
395
|
if (parsed.remainingPath) {
|
|
394
|
-
const resolvedValue = getValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target, permissionProcessor });
|
|
396
|
+
const resolvedValue = getValues({ __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor });
|
|
395
397
|
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
|
|
396
398
|
}
|
|
397
399
|
else {
|
|
398
|
-
const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target, permissionProcessor });
|
|
400
|
+
const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor });
|
|
399
401
|
if (!('__v' in resolvedValue)) {
|
|
400
402
|
assignGingerly(el, resolvedValue, options, permissionProcessor);
|
|
401
403
|
}
|
|
@@ -458,6 +460,7 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
|
|
|
458
460
|
withMethods: options.withMethods,
|
|
459
461
|
aka: options.aka,
|
|
460
462
|
akaMethods: options.akaMethods,
|
|
463
|
+
substitutions: options.substitutions,
|
|
461
464
|
protocols: options.protocols,
|
|
462
465
|
root: target,
|
|
463
466
|
permissionProcessor
|
package/assignFrom.ts
CHANGED
|
@@ -58,6 +58,7 @@ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions
|
|
|
58
58
|
return getValue(value, source, {
|
|
59
59
|
withMethods: options.withMethods,
|
|
60
60
|
aka: options.aka,
|
|
61
|
+
substitutions: options.substitutions,
|
|
61
62
|
protocols: options.protocols,
|
|
62
63
|
root: options.root
|
|
63
64
|
});
|
|
@@ -70,6 +71,7 @@ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions
|
|
|
70
71
|
return getValue(value, source, {
|
|
71
72
|
withMethods: options.withMethods,
|
|
72
73
|
aka: options.aka,
|
|
74
|
+
substitutions: options.substitutions,
|
|
73
75
|
protocols: options.protocols,
|
|
74
76
|
root: options.root
|
|
75
77
|
});
|
|
@@ -395,7 +397,7 @@ function processIdRefNormalKeys(
|
|
|
395
397
|
const ids = getEffectiveIds(options);
|
|
396
398
|
if (!ids) return;
|
|
397
399
|
|
|
398
|
-
const { withMethods, aka, akaMethods, protocols, from } = options;
|
|
400
|
+
const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
|
|
399
401
|
for (const key of idRefNormalKeys) {
|
|
400
402
|
const parsed = parseIdRef(key);
|
|
401
403
|
if (!parsed) continue;
|
|
@@ -407,14 +409,14 @@ function processIdRefNormalKeys(
|
|
|
407
409
|
if (parsed.remainingPath) {
|
|
408
410
|
const resolvedValue = getValues(
|
|
409
411
|
{ __v: value }, from,
|
|
410
|
-
{ withMethods, aka, akaMethods, protocols, root: target, permissionProcessor }
|
|
412
|
+
{ withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor }
|
|
411
413
|
);
|
|
412
414
|
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
|
|
413
415
|
} else {
|
|
414
416
|
const resolvedValue = getValues(
|
|
415
417
|
typeof value === 'object' && value !== null ? value : { __v: value },
|
|
416
418
|
from,
|
|
417
|
-
{ withMethods, aka, akaMethods, protocols, root: target, permissionProcessor }
|
|
419
|
+
{ withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor }
|
|
418
420
|
);
|
|
419
421
|
if (!('__v' in resolvedValue)) {
|
|
420
422
|
assignGingerly(el, resolvedValue, options, permissionProcessor);
|
|
@@ -486,6 +488,7 @@ export function assignFrom(
|
|
|
486
488
|
withMethods: options.withMethods,
|
|
487
489
|
aka: options.aka,
|
|
488
490
|
akaMethods: options.akaMethods,
|
|
491
|
+
substitutions: options.substitutions,
|
|
489
492
|
protocols: options.protocols,
|
|
490
493
|
root: target,
|
|
491
494
|
permissionProcessor
|
package/assignFromAsync.js
CHANGED
|
@@ -35,6 +35,7 @@ export async function assignFromAsync(target, pattern, options, permissionProces
|
|
|
35
35
|
withMethods: options.withMethods,
|
|
36
36
|
aka: options.aka,
|
|
37
37
|
akaMethods: options.akaMethods,
|
|
38
|
+
substitutions: options.substitutions,
|
|
38
39
|
protocols: options.protocols,
|
|
39
40
|
root: target,
|
|
40
41
|
permissionProcessor
|
|
@@ -47,7 +48,7 @@ export async function assignFromAsync(target, pattern, options, permissionProces
|
|
|
47
48
|
if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
|
|
48
49
|
const ids = { ...options.pin, ...options.at };
|
|
49
50
|
const { resolveIdVariable, parseIdRef } = await import('./resolve/resolveIdRef.js');
|
|
50
|
-
const { withMethods, aka, akaMethods, protocols, from } = options;
|
|
51
|
+
const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
|
|
51
52
|
for (const key of idRefNormalKeys) {
|
|
52
53
|
const parsed = parseIdRef(key);
|
|
53
54
|
if (!parsed)
|
|
@@ -58,13 +59,13 @@ export async function assignFromAsync(target, pattern, options, permissionProces
|
|
|
58
59
|
const value = expandedPattern[key];
|
|
59
60
|
if (parsed.remainingPath) {
|
|
60
61
|
// Resolve the RHS value
|
|
61
|
-
const resolvedValue = await resolveValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el, permissionProcessor });
|
|
62
|
+
const resolvedValue = await resolveValues({ __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor });
|
|
62
63
|
// Apply remaining path on the resolved element
|
|
63
64
|
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
|
|
64
65
|
}
|
|
65
66
|
else {
|
|
66
67
|
// No remaining path — resolve and assign directly to the element
|
|
67
|
-
const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el, permissionProcessor });
|
|
68
|
+
const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor });
|
|
68
69
|
if ('__v' in resolvedValue) {
|
|
69
70
|
// Single value — can't assign to element root without a path
|
|
70
71
|
}
|
package/assignFromAsync.ts
CHANGED
|
@@ -69,6 +69,7 @@ export async function assignFromAsync(
|
|
|
69
69
|
withMethods: options.withMethods,
|
|
70
70
|
aka: options.aka,
|
|
71
71
|
akaMethods: options.akaMethods,
|
|
72
|
+
substitutions: options.substitutions,
|
|
72
73
|
protocols: options.protocols,
|
|
73
74
|
root: target,
|
|
74
75
|
permissionProcessor
|
|
@@ -84,7 +85,7 @@ export async function assignFromAsync(
|
|
|
84
85
|
if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
|
|
85
86
|
const ids = { ...options.pin, ...options.at };
|
|
86
87
|
const { resolveIdVariable, parseIdRef } = await import('./resolve/resolveIdRef.js');
|
|
87
|
-
const { withMethods, aka, akaMethods, protocols, from } = options;
|
|
88
|
+
const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
|
|
88
89
|
for (const key of idRefNormalKeys) {
|
|
89
90
|
const parsed = parseIdRef(key);
|
|
90
91
|
if (!parsed) continue;
|
|
@@ -97,7 +98,7 @@ export async function assignFromAsync(
|
|
|
97
98
|
// Resolve the RHS value
|
|
98
99
|
const resolvedValue = await resolveValues(
|
|
99
100
|
{ __v: value }, from,
|
|
100
|
-
{ withMethods, aka, akaMethods, protocols, root: el, permissionProcessor }
|
|
101
|
+
{ withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor }
|
|
101
102
|
);
|
|
102
103
|
// Apply remaining path on the resolved element
|
|
103
104
|
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
|
|
@@ -106,7 +107,7 @@ export async function assignFromAsync(
|
|
|
106
107
|
const resolvedValue = await resolveValues(
|
|
107
108
|
typeof value === 'object' && value !== null ? value : { __v: value },
|
|
108
109
|
from,
|
|
109
|
-
{ withMethods, aka, akaMethods, protocols, root: el, permissionProcessor }
|
|
110
|
+
{ withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor }
|
|
110
111
|
);
|
|
111
112
|
if ('__v' in resolvedValue) {
|
|
112
113
|
// Single value — can't assign to element root without a path
|
|
@@ -303,13 +303,25 @@ const raConfig = {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// withAttrs configuration for parsing element attributes
|
|
307
|
+
const withAttrs = {
|
|
308
|
+
base: 'user-counter',
|
|
309
|
+
count: '${base}-count',
|
|
310
|
+
_count: {
|
|
311
|
+
instanceOf: 'Number',
|
|
312
|
+
valIfNull: 0,
|
|
313
|
+
},
|
|
314
|
+
username: '${base}-username',
|
|
315
|
+
};
|
|
316
|
+
|
|
306
317
|
/** @type {ElMakerConfig<AP>} */
|
|
307
318
|
const features = {
|
|
308
319
|
assignFeatures: {
|
|
309
320
|
roundabout: {
|
|
310
321
|
customData: {
|
|
311
322
|
raConfig,
|
|
312
|
-
}
|
|
323
|
+
},
|
|
324
|
+
withAttrs
|
|
313
325
|
},
|
|
314
326
|
templateMaker: {}
|
|
315
327
|
}
|
|
@@ -361,6 +373,20 @@ const raConfig = {
|
|
|
361
373
|
};
|
|
362
374
|
```
|
|
363
375
|
|
|
376
|
+
## How can I set focus after a delay?
|
|
377
|
+
|
|
378
|
+
Work is underway to improve the DX a bit, but for now:
|
|
379
|
+
|
|
380
|
+
```JS
|
|
381
|
+
{
|
|
382
|
+
delay:10, //milliseconds
|
|
383
|
+
ifAllOf: ['expanded'],
|
|
384
|
+
assign: {
|
|
385
|
+
set($.querySelector('a').focus()).to({}),
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
```
|
|
389
|
+
|
|
364
390
|
## Step 8
|
|
365
391
|
|
|
366
392
|
Run `node el-maker.mjs` (or `npm run build-el-maker` if your `package.json` includes a watch script) to regenerate `el-maker.json`.
|
|
@@ -267,6 +267,17 @@ export interface IAssignGingerlyOptions {
|
|
|
267
267
|
*/
|
|
268
268
|
aka?: Record<string, string>;
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Value substitutions for path segments.
|
|
272
|
+
* Each key names a placeholder; the value is a `?.`-delimited path resolved
|
|
273
|
+
* against the source (`from`) object. The resolved string value replaces any
|
|
274
|
+
* matching whole path segment in RHS path strings before path evaluation.
|
|
275
|
+
*
|
|
276
|
+
* Substitution values must be strings and must not contain the `?.` sequence,
|
|
277
|
+
* otherwise an error is thrown.
|
|
278
|
+
*/
|
|
279
|
+
substitutions?: Record<string, string>;
|
|
280
|
+
|
|
270
281
|
/**
|
|
271
282
|
* Shorthand for binding method aliases from the source object.
|
|
272
283
|
* Each entry maps an alias to a method name and is normalized into
|
|
@@ -357,6 +368,16 @@ export interface AssignFromOptions {
|
|
|
357
368
|
/** Alias mappings for path segments */
|
|
358
369
|
aka?: Record<string, string>;
|
|
359
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Value substitutions for path segments.
|
|
373
|
+
* Each key names a placeholder; the value is a `?.`-delimited path resolved
|
|
374
|
+
* against `from`. The resolved string value replaces any matching whole path
|
|
375
|
+
* segment in RHS path strings before path evaluation.
|
|
376
|
+
*
|
|
377
|
+
* Substitution values must be strings and must not contain `?.`.
|
|
378
|
+
*/
|
|
379
|
+
substitutions?: Record<string, string>;
|
|
380
|
+
|
|
360
381
|
/** AbortSignal for cleanup */
|
|
361
382
|
signal?: AbortSignal;
|
|
362
383
|
|
|
@@ -2,6 +2,7 @@ import {RAConfig} from '../roundabout/types.js';
|
|
|
2
2
|
import {FontFaceFeatureConfig} from '../font-face-feature/types.js';
|
|
3
3
|
import {CustomData as TSCD} from '../truth-sourcer/types.js';
|
|
4
4
|
import {CustomData as FUCD} from '../face-up/types.js';
|
|
5
|
+
import {AttrPatterns} from '../assign-gingerly/types.js';
|
|
5
6
|
|
|
6
7
|
export interface ElMakerConfig<AllProps = any, TActions = AllProps> {
|
|
7
8
|
assignFeatures: {
|
|
@@ -10,6 +11,7 @@ export interface ElMakerConfig<AllProps = any, TActions = AllProps> {
|
|
|
10
11
|
customData: {
|
|
11
12
|
raConfig: RAConfig<AllProps, TActions, TActions>
|
|
12
13
|
}
|
|
14
|
+
withAttrs: AttrPatterns<AllProps>
|
|
13
15
|
},
|
|
14
16
|
fontMgr?: {
|
|
15
17
|
spawn?: string,
|
|
@@ -161,6 +161,13 @@ export interface RAConfig<
|
|
|
161
161
|
initialPropVals?: Partial<{[key in keyof TProps & string]: unknown}>,
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// export interface RoundaboutFeatureConfig<
|
|
165
|
+
// TProps = unknown, TActions = TProps, ETProps = TProps,
|
|
166
|
+
// TCustomData = unknown, TEvents extends string = string>{
|
|
167
|
+
// RAConfig: RAConfig<TProps, TActions, ETProps, TCustomData, TEvents>,
|
|
168
|
+
|
|
169
|
+
// }
|
|
170
|
+
|
|
164
171
|
export interface RoundaboutOptions<TProps = unknown, TActions = TProps, ETProps = TProps, EventTypes extends string = string> extends RAConfig<TProps, TActions, ETProps, unknown, EventTypes> {
|
|
165
172
|
vm?: TProps & TActions & RoundaboutReady,
|
|
166
173
|
//for enhanced elements, pass in the container, referenced via $0.
|
|
@@ -217,7 +224,14 @@ export interface WeakRefConfig<TProps = any> {
|
|
|
217
224
|
/**
|
|
218
225
|
* Properties to automatically wrap in WeakRef
|
|
219
226
|
*/
|
|
220
|
-
properties
|
|
227
|
+
properties?: Array<keyof TProps & string>;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Array-valued properties whose elements should be stored as WeakRefs.
|
|
231
|
+
* When accessed, the getter returns a new array with each WeakRef dereferenced.
|
|
232
|
+
* Collected elements appear as `undefined` in their original positions.
|
|
233
|
+
*/
|
|
234
|
+
listProperties?: Array<keyof TProps & string>;
|
|
221
235
|
|
|
222
236
|
/**
|
|
223
237
|
* Logging behavior when deref returns null/undefined
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { FeatureSpawnContext } from "../assign-gingerly/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public properties of the SwipeDismissFeature.
|
|
5
|
+
* These can be set directly on the feature instance or initialized via attributes.
|
|
6
|
+
*/
|
|
7
|
+
export interface SwipeDismissProps {
|
|
8
|
+
/** Axis along which the dismiss gesture is measured. */
|
|
9
|
+
axis: 'x' | 'y';
|
|
10
|
+
/**
|
|
11
|
+
* Direction that counts toward dismissal.
|
|
12
|
+
* 1 = right/down, -1 = left/up.
|
|
13
|
+
* Set to 'both' to allow swiping in either direction (e.g. toasts/snackbars).
|
|
14
|
+
*/
|
|
15
|
+
direction: 1 | -1 | 'both';
|
|
16
|
+
/** Fraction of the panel size that triggers commit. */
|
|
17
|
+
distanceThreshold: number;
|
|
18
|
+
/** Velocity threshold in px/ms; a fast flick commits even under distanceThreshold. */
|
|
19
|
+
velocityThreshold: number;
|
|
20
|
+
/** CSS selector for the drag handle. Defaults to the host element. */
|
|
21
|
+
handleSelector: string | null;
|
|
22
|
+
/** CSS selector for the panel that visually follows the drag. Defaults to the handle. */
|
|
23
|
+
panelSelector: string | null;
|
|
24
|
+
/** Called on every pointermove with the current delta and fraction of the threshold. */
|
|
25
|
+
onProgress: ((deltaPx: number, fraction: number) => void) | null;
|
|
26
|
+
/** Called when the gesture crosses the commit threshold. */
|
|
27
|
+
onCommit: (() => void) | null;
|
|
28
|
+
/** Called when the gesture is released before the commit threshold. */
|
|
29
|
+
onCancel: (() => void) | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Internal state of the feature.
|
|
34
|
+
*/
|
|
35
|
+
export interface AllProps extends SwipeDismissProps {
|
|
36
|
+
/** WeakRef to the host custom element. */
|
|
37
|
+
hostRef: WeakRef<Element>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type AP = AllProps;
|
|
41
|
+
export type PAP = Partial<AllProps>;
|
|
42
|
+
|
|
43
|
+
export { FeatureSpawnContext };
|
package/package.json
CHANGED
|
@@ -171,6 +171,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
|
|
|
171
171
|
resolvedParams = getValues(config.get, options.from, {
|
|
172
172
|
withMethods: options.withMethods,
|
|
173
173
|
aka: options.aka,
|
|
174
|
+
substitutions: options.substitutions,
|
|
174
175
|
protocols: options.protocols,
|
|
175
176
|
root: target
|
|
176
177
|
});
|
|
@@ -180,6 +181,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
|
|
|
180
181
|
const asyncResolved = await resolveValues(config.resolve, options.from, {
|
|
181
182
|
withMethods: options.withMethods,
|
|
182
183
|
aka: options.aka,
|
|
184
|
+
substitutions: options.substitutions,
|
|
183
185
|
protocols: options.protocols,
|
|
184
186
|
root: target
|
|
185
187
|
});
|
|
@@ -192,6 +192,7 @@ export async function processHandlerCommands(
|
|
|
192
192
|
resolvedParams = getValues(config.get, options.from, {
|
|
193
193
|
withMethods: options.withMethods,
|
|
194
194
|
aka: options.aka,
|
|
195
|
+
substitutions: options.substitutions,
|
|
195
196
|
protocols: options.protocols,
|
|
196
197
|
root: target
|
|
197
198
|
});
|
|
@@ -201,6 +202,7 @@ export async function processHandlerCommands(
|
|
|
201
202
|
const asyncResolved = await resolveValues(config.resolve, options.from, {
|
|
202
203
|
withMethods: options.withMethods,
|
|
203
204
|
aka: options.aka,
|
|
205
|
+
substitutions: options.substitutions,
|
|
204
206
|
protocols: options.protocols,
|
|
205
207
|
root: target
|
|
206
208
|
});
|
package/resolve/getValues.js
CHANGED
|
@@ -67,6 +67,47 @@ function applyAliases(path, aliasMap) {
|
|
|
67
67
|
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
68
68
|
return substituted.join('?.');
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Apply value substitutions to a path string.
|
|
72
|
+
* Replaces complete tokens between `?.` delimiters with their resolved values.
|
|
73
|
+
* Substitutions are applied before aliases.
|
|
74
|
+
*/
|
|
75
|
+
function applySubstitutions(path, substitutionMap) {
|
|
76
|
+
if (!substitutionMap || substitutionMap.size === 0)
|
|
77
|
+
return path;
|
|
78
|
+
const parts = path.split('?.');
|
|
79
|
+
const substituted = parts.map(part => substitutionMap.get(part) ?? part);
|
|
80
|
+
return substituted.join('?.');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve substitution values declared in options.substitutions.
|
|
84
|
+
* Each substitution path is resolved against the source object without
|
|
85
|
+
* applying substitutions itself, to avoid infinite recursion.
|
|
86
|
+
* Resolved values must be strings and must not contain `?.`.
|
|
87
|
+
*/
|
|
88
|
+
function resolveSubstitutions(substitutions, source, options) {
|
|
89
|
+
const map = new Map();
|
|
90
|
+
if (!substitutions)
|
|
91
|
+
return map;
|
|
92
|
+
for (const [name, path] of Object.entries(substitutions)) {
|
|
93
|
+
// Resolve the substitution path against the source, but do not apply
|
|
94
|
+
// substitutions to that path. Root references ($0) are also disabled
|
|
95
|
+
// for substitution paths so values are sourced from `from` only.
|
|
96
|
+
const resolved = getValue(path, source, options
|
|
97
|
+
? { ...options, substitutions: undefined, root: undefined }
|
|
98
|
+
: undefined);
|
|
99
|
+
if (resolved === null || typeof resolved === 'undefined')
|
|
100
|
+
continue;
|
|
101
|
+
if (typeof resolved !== 'string') {
|
|
102
|
+
throw new Error(`Substitution '${name}' must resolve to a string, got ${typeof resolved}`);
|
|
103
|
+
}
|
|
104
|
+
if (resolved.includes('?.')) {
|
|
105
|
+
throw new Error(`Substitution '${name}' resolved to a string containing '?.', which would alter the path structure: '${resolved}'`);
|
|
106
|
+
}
|
|
107
|
+
map.set(name, resolved);
|
|
108
|
+
}
|
|
109
|
+
return map;
|
|
110
|
+
}
|
|
70
111
|
/**
|
|
71
112
|
* Resolve a special root-reference token at the start of a string.
|
|
72
113
|
* '$0' refers to the first argument passed to assignFrom / resolveValues.
|
|
@@ -159,12 +200,13 @@ function getProtocolValue(value, protocols, options) {
|
|
|
159
200
|
* Resolve path strings and protocols within an array (synchronous).
|
|
160
201
|
* Recurses into nested arrays and plain objects.
|
|
161
202
|
*/
|
|
162
|
-
function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
203
|
+
function getArray(arr, source, aliasMap, withMethods, protocols, options, substitutionMap) {
|
|
163
204
|
const permissionProcessor = options?.permissionProcessor;
|
|
164
205
|
const result = [];
|
|
165
206
|
for (const item of arr) {
|
|
166
207
|
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
167
|
-
const
|
|
208
|
+
const substituted = applySubstitutions(item, substitutionMap);
|
|
209
|
+
const aliased = applyAliases(substituted, aliasMap);
|
|
168
210
|
const parts = parseCachedPath(aliased);
|
|
169
211
|
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor));
|
|
170
212
|
}
|
|
@@ -174,7 +216,8 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
174
216
|
result.push(source);
|
|
175
217
|
}
|
|
176
218
|
else {
|
|
177
|
-
const
|
|
219
|
+
const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
|
|
220
|
+
const aliased = applyAliases(substitutedPath, aliasMap);
|
|
178
221
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
179
222
|
const parts = parseCachedPath(normalizedPath);
|
|
180
223
|
result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor));
|
|
@@ -184,7 +227,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
184
227
|
result.push(getProtocolValue(item, protocols, options));
|
|
185
228
|
}
|
|
186
229
|
else if (Array.isArray(item)) {
|
|
187
|
-
result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
|
|
230
|
+
result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
|
|
188
231
|
}
|
|
189
232
|
else if (item && typeof item === 'object') {
|
|
190
233
|
const proto = Object.getPrototypeOf(item);
|
|
@@ -215,12 +258,14 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
215
258
|
*/
|
|
216
259
|
export function getValues(pattern, source, options) {
|
|
217
260
|
const { aliasMap, withMethods } = normalizeAliasOptions(options);
|
|
261
|
+
const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
|
|
218
262
|
const protocols = options?.protocols;
|
|
219
263
|
const permissionProcessor = options?.permissionProcessor;
|
|
220
264
|
const result = {};
|
|
221
265
|
for (const [key, value] of Object.entries(pattern)) {
|
|
222
266
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
223
|
-
const
|
|
267
|
+
const substituted = applySubstitutions(value, substitutionMap);
|
|
268
|
+
const aliased = applyAliases(substituted, aliasMap);
|
|
224
269
|
const parts = parseCachedPath(aliased);
|
|
225
270
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor);
|
|
226
271
|
}
|
|
@@ -230,7 +275,8 @@ export function getValues(pattern, source, options) {
|
|
|
230
275
|
result[key] = source;
|
|
231
276
|
}
|
|
232
277
|
else {
|
|
233
|
-
const
|
|
278
|
+
const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
|
|
279
|
+
const aliased = applyAliases(substitutedPath, aliasMap);
|
|
234
280
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
235
281
|
const parts = parseCachedPath(normalizedPath);
|
|
236
282
|
result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor);
|
|
@@ -240,7 +286,7 @@ export function getValues(pattern, source, options) {
|
|
|
240
286
|
result[key] = getProtocolValue(value, protocols, options);
|
|
241
287
|
}
|
|
242
288
|
else if (Array.isArray(value)) {
|
|
243
|
-
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
|
|
289
|
+
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
|
|
244
290
|
}
|
|
245
291
|
else if (typeof value === 'object' && value !== null) {
|
|
246
292
|
const proto = Object.getPrototypeOf(value);
|
|
@@ -274,10 +320,12 @@ export function getValue(path, source, options) {
|
|
|
274
320
|
else if (!path.startsWith('?.')) {
|
|
275
321
|
return path;
|
|
276
322
|
}
|
|
277
|
-
|
|
323
|
+
const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
|
|
324
|
+
const substituted = applySubstitutions(path, substitutionMap);
|
|
325
|
+
let aliased = substituted;
|
|
278
326
|
const { aliasMap } = normalizeAliasOptions(options);
|
|
279
327
|
if (aliasMap.size > 0) {
|
|
280
|
-
aliased = applyAliases(
|
|
328
|
+
aliased = applyAliases(substituted, aliasMap);
|
|
281
329
|
}
|
|
282
330
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
283
331
|
const parts = parseCachedPath(normalizedPath);
|
package/resolve/getValues.ts
CHANGED
|
@@ -85,6 +85,59 @@ function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
|
85
85
|
return substituted.join('?.');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Apply value substitutions to a path string.
|
|
90
|
+
* Replaces complete tokens between `?.` delimiters with their resolved values.
|
|
91
|
+
* Substitutions are applied before aliases.
|
|
92
|
+
*/
|
|
93
|
+
function applySubstitutions(path: string, substitutionMap?: Map<string, string>): string {
|
|
94
|
+
if (!substitutionMap || substitutionMap.size === 0) return path;
|
|
95
|
+
const parts = path.split('?.');
|
|
96
|
+
const substituted = parts.map(part => substitutionMap.get(part) ?? part);
|
|
97
|
+
return substituted.join('?.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve substitution values declared in options.substitutions.
|
|
102
|
+
* Each substitution path is resolved against the source object without
|
|
103
|
+
* applying substitutions itself, to avoid infinite recursion.
|
|
104
|
+
* Resolved values must be strings and must not contain `?.`.
|
|
105
|
+
*/
|
|
106
|
+
function resolveSubstitutions(
|
|
107
|
+
substitutions: Record<string, string> | undefined,
|
|
108
|
+
source: any,
|
|
109
|
+
options: GetValuesOptions | undefined
|
|
110
|
+
): Map<string, string> {
|
|
111
|
+
const map = new Map<string, string>();
|
|
112
|
+
if (!substitutions) return map;
|
|
113
|
+
|
|
114
|
+
for (const [name, path] of Object.entries(substitutions)) {
|
|
115
|
+
// Resolve the substitution path against the source, but do not apply
|
|
116
|
+
// substitutions to that path. Root references ($0) are also disabled
|
|
117
|
+
// for substitution paths so values are sourced from `from` only.
|
|
118
|
+
const resolved = getValue(
|
|
119
|
+
path,
|
|
120
|
+
source,
|
|
121
|
+
options
|
|
122
|
+
? { ...options, substitutions: undefined, root: undefined }
|
|
123
|
+
: undefined
|
|
124
|
+
);
|
|
125
|
+
if(resolved === null || typeof resolved === 'undefined') continue;
|
|
126
|
+
if (typeof resolved !== 'string') {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Substitution '${name}' must resolve to a string, got ${typeof resolved}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (resolved.includes('?.')) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Substitution '${name}' resolved to a string containing '?.', which would alter the path structure: '${resolved}'`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
map.set(name, resolved);
|
|
137
|
+
}
|
|
138
|
+
return map;
|
|
139
|
+
}
|
|
140
|
+
|
|
88
141
|
/**
|
|
89
142
|
* Resolve a special root-reference token at the start of a string.
|
|
90
143
|
* '$0' refers to the first argument passed to assignFrom / resolveValues.
|
|
@@ -200,13 +253,15 @@ function getArray(
|
|
|
200
253
|
aliasMap: Map<string, string>,
|
|
201
254
|
withMethods: Set<string> | undefined,
|
|
202
255
|
protocols: Record<string, (key: string) => any> | undefined,
|
|
203
|
-
options?: GetValuesOptions
|
|
256
|
+
options?: GetValuesOptions,
|
|
257
|
+
substitutionMap?: Map<string, string>
|
|
204
258
|
): any[] {
|
|
205
259
|
const permissionProcessor = options?.permissionProcessor;
|
|
206
260
|
const result: any[] = [];
|
|
207
261
|
for (const item of arr) {
|
|
208
262
|
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
209
|
-
const
|
|
263
|
+
const substituted = applySubstitutions(item, substitutionMap);
|
|
264
|
+
const aliased = applyAliases(substituted, aliasMap);
|
|
210
265
|
const parts = parseCachedPath(aliased);
|
|
211
266
|
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor));
|
|
212
267
|
} else if (typeof item === 'string' && item.startsWith('$0')) {
|
|
@@ -214,7 +269,8 @@ function getArray(
|
|
|
214
269
|
if (rootRef === null) {
|
|
215
270
|
result.push(source);
|
|
216
271
|
} else {
|
|
217
|
-
const
|
|
272
|
+
const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
|
|
273
|
+
const aliased = applyAliases(substitutedPath, aliasMap);
|
|
218
274
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
219
275
|
const parts = parseCachedPath(normalizedPath);
|
|
220
276
|
result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor));
|
|
@@ -222,7 +278,7 @@ function getArray(
|
|
|
222
278
|
} else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
223
279
|
result.push(getProtocolValue(item, protocols, options));
|
|
224
280
|
} else if (Array.isArray(item)) {
|
|
225
|
-
result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
|
|
281
|
+
result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
|
|
226
282
|
} else if (item && typeof item === 'object') {
|
|
227
283
|
const proto = Object.getPrototypeOf(item);
|
|
228
284
|
if (proto === Object.prototype || proto === null) {
|
|
@@ -255,6 +311,7 @@ export function getValues(
|
|
|
255
311
|
options?: GetValuesOptions
|
|
256
312
|
): Record<string, any> {
|
|
257
313
|
const { aliasMap, withMethods } = normalizeAliasOptions(options);
|
|
314
|
+
const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
|
|
258
315
|
|
|
259
316
|
const protocols = options?.protocols;
|
|
260
317
|
const permissionProcessor = options?.permissionProcessor;
|
|
@@ -262,7 +319,8 @@ export function getValues(
|
|
|
262
319
|
const result: Record<string, any> = {};
|
|
263
320
|
for (const [key, value] of Object.entries(pattern)) {
|
|
264
321
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
265
|
-
const
|
|
322
|
+
const substituted = applySubstitutions(value, substitutionMap);
|
|
323
|
+
const aliased = applyAliases(substituted, aliasMap);
|
|
266
324
|
const parts = parseCachedPath(aliased);
|
|
267
325
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor);
|
|
268
326
|
} else if (typeof value === 'string' && value.startsWith('$0')) {
|
|
@@ -270,7 +328,8 @@ export function getValues(
|
|
|
270
328
|
if (rootRef === null) {
|
|
271
329
|
result[key] = source;
|
|
272
330
|
} else {
|
|
273
|
-
const
|
|
331
|
+
const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
|
|
332
|
+
const aliased = applyAliases(substitutedPath, aliasMap);
|
|
274
333
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
275
334
|
const parts = parseCachedPath(normalizedPath);
|
|
276
335
|
result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor);
|
|
@@ -278,7 +337,7 @@ export function getValues(
|
|
|
278
337
|
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
279
338
|
result[key] = getProtocolValue(value, protocols, options);
|
|
280
339
|
} else if (Array.isArray(value)) {
|
|
281
|
-
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
|
|
340
|
+
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
|
|
282
341
|
} else if (typeof value === 'object' && value !== null) {
|
|
283
342
|
const proto = Object.getPrototypeOf(value);
|
|
284
343
|
if (proto === Object.prototype || proto === null) {
|
|
@@ -314,10 +373,13 @@ export function getValue(
|
|
|
314
373
|
return path;
|
|
315
374
|
}
|
|
316
375
|
|
|
317
|
-
|
|
376
|
+
const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
|
|
377
|
+
|
|
378
|
+
const substituted = applySubstitutions(path, substitutionMap);
|
|
379
|
+
let aliased = substituted;
|
|
318
380
|
const { aliasMap } = normalizeAliasOptions(options);
|
|
319
381
|
if (aliasMap.size > 0) {
|
|
320
|
-
aliased = applyAliases(
|
|
382
|
+
aliased = applyAliases(substituted, aliasMap);
|
|
321
383
|
}
|
|
322
384
|
|
|
323
385
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
@@ -267,6 +267,17 @@ export interface IAssignGingerlyOptions {
|
|
|
267
267
|
*/
|
|
268
268
|
aka?: Record<string, string>;
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Value substitutions for path segments.
|
|
272
|
+
* Each key names a placeholder; the value is a `?.`-delimited path resolved
|
|
273
|
+
* against the source (`from`) object. The resolved string value replaces any
|
|
274
|
+
* matching whole path segment in RHS path strings before path evaluation.
|
|
275
|
+
*
|
|
276
|
+
* Substitution values must be strings and must not contain the `?.` sequence,
|
|
277
|
+
* otherwise an error is thrown.
|
|
278
|
+
*/
|
|
279
|
+
substitutions?: Record<string, string>;
|
|
280
|
+
|
|
270
281
|
/**
|
|
271
282
|
* Shorthand for binding method aliases from the source object.
|
|
272
283
|
* Each entry maps an alias to a method name and is normalized into
|
|
@@ -357,6 +368,16 @@ export interface AssignFromOptions {
|
|
|
357
368
|
/** Alias mappings for path segments */
|
|
358
369
|
aka?: Record<string, string>;
|
|
359
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Value substitutions for path segments.
|
|
373
|
+
* Each key names a placeholder; the value is a `?.`-delimited path resolved
|
|
374
|
+
* against `from`. The resolved string value replaces any matching whole path
|
|
375
|
+
* segment in RHS path strings before path evaluation.
|
|
376
|
+
*
|
|
377
|
+
* Substitution values must be strings and must not contain `?.`.
|
|
378
|
+
*/
|
|
379
|
+
substitutions?: Record<string, string>;
|
|
380
|
+
|
|
360
381
|
/** AbortSignal for cleanup */
|
|
361
382
|
signal?: AbortSignal;
|
|
362
383
|
|