assign-gingerly 0.0.38 → 0.0.39
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 +897 -11
- package/assignFeatures.js +457 -0
- package/assignFeatures.ts +683 -0
- package/assignGingerly.js +96 -3
- package/assignGingerly.ts +122 -3
- package/evaluatePathWithAsyncMethods.js +90 -0
- package/evaluatePathWithAsyncMethods.ts +102 -0
- package/index.js +3 -1
- package/index.ts +3 -1
- package/installForwarding.js +61 -0
- package/installForwarding.ts +71 -0
- package/package.json +14 -2
- package/resolveValues.js +62 -2
- package/resolveValues.ts +71 -2
- package/types/assign-gingerly/types.d.ts +139 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
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": {
|
|
@@ -60,9 +60,21 @@
|
|
|
60
60
|
"default": "./resolveValues.js",
|
|
61
61
|
"types": "./resolveValues.ts"
|
|
62
62
|
},
|
|
63
|
+
"./installForwarding.js": {
|
|
64
|
+
"default": "./installForwarding.js",
|
|
65
|
+
"types": "./installForwarding.ts"
|
|
66
|
+
},
|
|
63
67
|
"./assignFrom.js": {
|
|
64
68
|
"default": "./assignFrom.js",
|
|
65
69
|
"types": "./assignFrom.ts"
|
|
70
|
+
},
|
|
71
|
+
"./assignFeatures.js": {
|
|
72
|
+
"default": "./assignFeatures.js",
|
|
73
|
+
"types": "./assignFeatures.ts"
|
|
74
|
+
},
|
|
75
|
+
"./evaluatePathWithAsyncMethods.js": {
|
|
76
|
+
"default": "./evaluatePathWithAsyncMethods.js",
|
|
77
|
+
"types": "./evaluatePathWithAsyncMethods.ts"
|
|
66
78
|
}
|
|
67
79
|
},
|
|
68
80
|
"main": "index.js",
|
|
@@ -77,7 +89,7 @@
|
|
|
77
89
|
"devDependencies": {
|
|
78
90
|
"@playwright/test": "1.59.1",
|
|
79
91
|
"spa-ssi": "0.0.27",
|
|
80
|
-
"@types/node": "25.6.
|
|
92
|
+
"@types/node": "25.6.2",
|
|
81
93
|
"typescript": "6.0.3"
|
|
82
94
|
}
|
|
83
95
|
}
|
package/resolveValues.js
CHANGED
|
@@ -9,6 +9,22 @@ function applyAliases(path, aliasMap) {
|
|
|
9
9
|
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
10
10
|
return substituted.join('?.');
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Path cache for parsed path strings.
|
|
14
|
+
* Avoids re-splitting the same path on repeated calls.
|
|
15
|
+
*/
|
|
16
|
+
const pathCache = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Parse a `?.`-delimited path string into segments, with caching.
|
|
19
|
+
*/
|
|
20
|
+
function parseCachedPath(path) {
|
|
21
|
+
let parts = pathCache.get(path);
|
|
22
|
+
if (!parts) {
|
|
23
|
+
parts = path.split('?.').filter(p => p.length > 0);
|
|
24
|
+
pathCache.set(path, parts);
|
|
25
|
+
}
|
|
26
|
+
return parts;
|
|
27
|
+
}
|
|
12
28
|
/**
|
|
13
29
|
* Navigate a path against a source object, optionally calling methods.
|
|
14
30
|
* Returns the resolved value at the end of the path.
|
|
@@ -99,8 +115,8 @@ export function resolveValues(pattern, source, options) {
|
|
|
99
115
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
100
116
|
// Apply aliases to the RHS path
|
|
101
117
|
const aliased = applyAliases(value, aliasMap);
|
|
102
|
-
// Parse path
|
|
103
|
-
const parts = aliased
|
|
118
|
+
// Parse path with caching
|
|
119
|
+
const parts = parseCachedPath(aliased);
|
|
104
120
|
// Navigate with method support
|
|
105
121
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
106
122
|
}
|
|
@@ -110,3 +126,47 @@ export function resolveValues(pattern, source, options) {
|
|
|
110
126
|
}
|
|
111
127
|
return result;
|
|
112
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve a single `?.`-delimited path string against a source object.
|
|
131
|
+
*
|
|
132
|
+
* This is a lighter-weight alternative to `resolveValues` when you only need
|
|
133
|
+
* to resolve one path and don't want the overhead of creating wrapper objects.
|
|
134
|
+
*
|
|
135
|
+
* @param path - A `?.`-delimited path string (e.g., '?.behaviors?.command')
|
|
136
|
+
* @param source - Object to resolve the path against
|
|
137
|
+
* @param options - Optional withMethods and aka for method calls and aliases
|
|
138
|
+
* @returns The resolved value, or undefined if any segment is nullish
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const value = resolveValue('?.behaviors?.commandBehavior?.command', el);
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const value = resolveValue('?.q?.myEl?.textContent', el, {
|
|
145
|
+
* withMethods: ['querySelector'],
|
|
146
|
+
* aka: { 'q': 'querySelector' }
|
|
147
|
+
* });
|
|
148
|
+
*/
|
|
149
|
+
export function resolveValue(path, source, options) {
|
|
150
|
+
if (!path.startsWith('?.'))
|
|
151
|
+
return path;
|
|
152
|
+
// Build alias map
|
|
153
|
+
let aliased = path;
|
|
154
|
+
if (options?.aka) {
|
|
155
|
+
const aliasMap = new Map();
|
|
156
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
157
|
+
aliasMap.set(alias, target);
|
|
158
|
+
}
|
|
159
|
+
aliased = applyAliases(path, aliasMap);
|
|
160
|
+
}
|
|
161
|
+
// Parse path with caching
|
|
162
|
+
const parts = parseCachedPath(aliased);
|
|
163
|
+
if (parts.length === 0)
|
|
164
|
+
return source;
|
|
165
|
+
// Build methods set
|
|
166
|
+
const withMethods = options?.withMethods
|
|
167
|
+
? options.withMethods instanceof Set
|
|
168
|
+
? options.withMethods
|
|
169
|
+
: new Set(options.withMethods)
|
|
170
|
+
: undefined;
|
|
171
|
+
return navigatePath(source, parts, withMethods);
|
|
172
|
+
}
|
package/resolveValues.ts
CHANGED
|
@@ -26,6 +26,24 @@ function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
|
26
26
|
return substituted.join('?.');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Path cache for parsed path strings.
|
|
31
|
+
* Avoids re-splitting the same path on repeated calls.
|
|
32
|
+
*/
|
|
33
|
+
const pathCache = new Map<string, string[]>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a `?.`-delimited path string into segments, with caching.
|
|
37
|
+
*/
|
|
38
|
+
function parseCachedPath(path: string): string[] {
|
|
39
|
+
let parts = pathCache.get(path);
|
|
40
|
+
if (!parts) {
|
|
41
|
+
parts = path.split('?.').filter(p => p.length > 0);
|
|
42
|
+
pathCache.set(path, parts);
|
|
43
|
+
}
|
|
44
|
+
return parts;
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
/**
|
|
30
48
|
* Navigate a path against a source object, optionally calling methods.
|
|
31
49
|
* Returns the resolved value at the end of the path.
|
|
@@ -128,8 +146,8 @@ export function resolveValues(
|
|
|
128
146
|
// Apply aliases to the RHS path
|
|
129
147
|
const aliased = applyAliases(value, aliasMap);
|
|
130
148
|
|
|
131
|
-
// Parse path
|
|
132
|
-
const parts = aliased
|
|
149
|
+
// Parse path with caching
|
|
150
|
+
const parts = parseCachedPath(aliased);
|
|
133
151
|
|
|
134
152
|
// Navigate with method support
|
|
135
153
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
@@ -139,3 +157,54 @@ export function resolveValues(
|
|
|
139
157
|
}
|
|
140
158
|
return result;
|
|
141
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a single `?.`-delimited path string against a source object.
|
|
163
|
+
*
|
|
164
|
+
* This is a lighter-weight alternative to `resolveValues` when you only need
|
|
165
|
+
* to resolve one path and don't want the overhead of creating wrapper objects.
|
|
166
|
+
*
|
|
167
|
+
* @param path - A `?.`-delimited path string (e.g., '?.behaviors?.command')
|
|
168
|
+
* @param source - Object to resolve the path against
|
|
169
|
+
* @param options - Optional withMethods and aka for method calls and aliases
|
|
170
|
+
* @returns The resolved value, or undefined if any segment is nullish
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* const value = resolveValue('?.behaviors?.commandBehavior?.command', el);
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* const value = resolveValue('?.q?.myEl?.textContent', el, {
|
|
177
|
+
* withMethods: ['querySelector'],
|
|
178
|
+
* aka: { 'q': 'querySelector' }
|
|
179
|
+
* });
|
|
180
|
+
*/
|
|
181
|
+
export function resolveValue(
|
|
182
|
+
path: string,
|
|
183
|
+
source: any,
|
|
184
|
+
options?: ResolveValuesOptions
|
|
185
|
+
): any {
|
|
186
|
+
if (!path.startsWith('?.')) return path;
|
|
187
|
+
|
|
188
|
+
// Build alias map
|
|
189
|
+
let aliased = path;
|
|
190
|
+
if (options?.aka) {
|
|
191
|
+
const aliasMap = new Map<string, string>();
|
|
192
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
193
|
+
aliasMap.set(alias, target);
|
|
194
|
+
}
|
|
195
|
+
aliased = applyAliases(path, aliasMap);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Parse path with caching
|
|
199
|
+
const parts = parseCachedPath(aliased);
|
|
200
|
+
if (parts.length === 0) return source;
|
|
201
|
+
|
|
202
|
+
// Build methods set
|
|
203
|
+
const withMethods = options?.withMethods
|
|
204
|
+
? options.withMethods instanceof Set
|
|
205
|
+
? options.withMethods
|
|
206
|
+
: new Set(options.withMethods)
|
|
207
|
+
: undefined;
|
|
208
|
+
|
|
209
|
+
return navigatePath(source, parts, withMethods);
|
|
210
|
+
}
|
|
@@ -241,6 +241,18 @@ export interface IAssignGingerlyOptions {
|
|
|
241
241
|
* When the signal is aborted, all event listeners are automatically removed
|
|
242
242
|
*/
|
|
243
243
|
signal?: AbortSignal;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List of property names that should be treated as async methods.
|
|
247
|
+
* Works together with withMethods — async methods are awaited before
|
|
248
|
+
* continuing the chain.
|
|
249
|
+
*
|
|
250
|
+
* The path evaluation for keys containing async methods is fire-and-forget:
|
|
251
|
+
* assignGingerly remains synchronous and returns immediately.
|
|
252
|
+
*
|
|
253
|
+
* NOTE: Interaction with @each and @eachTime is not yet implemented.
|
|
254
|
+
*/
|
|
255
|
+
withAsyncMethods?: string[] | Set<string>;
|
|
244
256
|
}
|
|
245
257
|
|
|
246
258
|
/**
|
|
@@ -321,3 +333,130 @@ export interface ElementEnhancement{
|
|
|
321
333
|
dispose(registryItem: EnhancementConfig | string | symbol): void;
|
|
322
334
|
whenResolved(registryItem: EnhancementConfig | string | symbol, mountCtx?: any): Promise<any>;
|
|
323
335
|
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Context passed to feature spawn constructors
|
|
339
|
+
*/
|
|
340
|
+
export interface FeatureSpawnContext {
|
|
341
|
+
/** The feature key (e.g., 'photoTaker') */
|
|
342
|
+
key: string;
|
|
343
|
+
/** The SupportedFeatureConfig from static supportedFeatures */
|
|
344
|
+
optIn: SupportedFeatureConfig;
|
|
345
|
+
/** The FeatureConfig from assignFeatures */
|
|
346
|
+
injection: FeatureConfig;
|
|
347
|
+
/** The features registry reference */
|
|
348
|
+
featuresRegistry: FeaturesRegistry;
|
|
349
|
+
/** Shared context from the host element (via getSharedContext callback) */
|
|
350
|
+
shared?: any;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Configuration for a supported feature slot declared via static supportedFeatures
|
|
355
|
+
*/
|
|
356
|
+
export interface SupportedFeatureConfig {
|
|
357
|
+
/**
|
|
358
|
+
* Optional fallback class (or async spawner) to use if no implementation is injected.
|
|
359
|
+
*/
|
|
360
|
+
fallbackSpawn?:
|
|
361
|
+
| { new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }
|
|
362
|
+
| (() => Promise<{ new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }>);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Optional runtime shape validation for the spawned instance.
|
|
366
|
+
* Return true if the instance is valid, false to throw.
|
|
367
|
+
*/
|
|
368
|
+
validateShape?: (spawnedInstance: any) => boolean;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Optional callback to provide shared context (e.g., ElementInternals, private state)
|
|
372
|
+
* to the feature at construction time.
|
|
373
|
+
*
|
|
374
|
+
* Defined in the class body, this callback has access to #private fields.
|
|
375
|
+
* The returned object is passed to the feature constructor as `ctx.shared`.
|
|
376
|
+
*
|
|
377
|
+
* @param instance - The host element instance
|
|
378
|
+
* @returns An object containing shared data for the feature
|
|
379
|
+
*/
|
|
380
|
+
getSharedContext?: (instance: any) => any;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Class-level configuration for the features system.
|
|
385
|
+
* Declared as `static featuresConfig` on the class.
|
|
386
|
+
*/
|
|
387
|
+
export interface FeaturesClassConfig {
|
|
388
|
+
/**
|
|
389
|
+
* Lifecycle method configuration.
|
|
390
|
+
*
|
|
391
|
+
* If set to `true`, installs a method named 'whenFeatureReady' on the prototype.
|
|
392
|
+
* If set to an object, allows customizing the method name.
|
|
393
|
+
*
|
|
394
|
+
* The installed method accepts a feature key and returns a Promise that resolves
|
|
395
|
+
* with the feature instance once it's ready (useful for async spawners).
|
|
396
|
+
* For synchronous spawners, the Promise resolves immediately.
|
|
397
|
+
*
|
|
398
|
+
* Suggested default name: 'whenFeatureReady'
|
|
399
|
+
*/
|
|
400
|
+
lifecycleKeys?: true | {
|
|
401
|
+
/** Method name for awaiting feature readiness. Defaults to 'whenFeatureReady'. */
|
|
402
|
+
whenFeatureReady?: string;
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Configuration for a feature passed to assignFeatures.
|
|
408
|
+
* The feature equivalent of EnhancementConfig.
|
|
409
|
+
*/
|
|
410
|
+
export interface FeatureConfig {
|
|
411
|
+
/**
|
|
412
|
+
* The class to instantiate for this feature, or an async function that
|
|
413
|
+
* resolves to such a class (for lazy-loading).
|
|
414
|
+
*
|
|
415
|
+
* Synchronous: Constructor receives the host element as its first argument,
|
|
416
|
+
* a FeatureSpawnContext as second, and optional initVals as third.
|
|
417
|
+
*
|
|
418
|
+
* Asynchronous: A function (arrow or async) that returns a Promise resolving
|
|
419
|
+
* to a constructor. The getter returns a placeholder object immediately and
|
|
420
|
+
* instantiates the real class once the Promise resolves.
|
|
421
|
+
*/
|
|
422
|
+
spawn?:
|
|
423
|
+
| { new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }
|
|
424
|
+
| (() => Promise<{ new(hostElement: any, ctx: FeatureSpawnContext, initVals?: any): any }>);
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Attribute patterns for parsing element attributes into initVals.
|
|
428
|
+
* Attributes are the "base layer" — programmatic values override them.
|
|
429
|
+
* Always unprefixed for features (no enh- prefix).
|
|
430
|
+
*/
|
|
431
|
+
withAttrs?: AttrPatterns<any>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Reserved field for custom configuration data.
|
|
435
|
+
* Not interpreted by the library — available to the feature class
|
|
436
|
+
* via ctx.injection.customData in the constructor.
|
|
437
|
+
*/
|
|
438
|
+
customData?: any;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export type SupportedFeaturesMap = Record<string, SupportedFeatureConfig>;
|
|
442
|
+
export type FeatureConfigsMap = Record<string, FeatureConfig>;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Registry for feature configs, keyed by constructor
|
|
446
|
+
*/
|
|
447
|
+
export declare class FeaturesRegistry {
|
|
448
|
+
has(ctr: Function): boolean;
|
|
449
|
+
get(ctr: Function): Map<string, FeatureConfig> | undefined;
|
|
450
|
+
set(ctr: Function, key: string, config: FeatureConfig): void;
|
|
451
|
+
hasKey(ctr: Function, key: string): boolean;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Core assignFeatures function.
|
|
456
|
+
* Validates inputs, registers feature configs, and installs lazy getters on the class prototype.
|
|
457
|
+
*/
|
|
458
|
+
export declare function assignFeatures(
|
|
459
|
+
ctr: Function,
|
|
460
|
+
features: FeatureConfigsMap,
|
|
461
|
+
featuresRegistry: FeaturesRegistry
|
|
462
|
+
): void;
|