assign-gingerly 0.0.37 → 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/object-extension.js +2 -2
- package/object-extension.ts +3 -3
- package/package.json +14 -2
- package/resolveValues.js +62 -2
- package/resolveValues.ts +71 -2
- package/types/assign-gingerly/types.d.ts +146 -0
package/object-extension.js
CHANGED
|
@@ -138,7 +138,7 @@ class ElementEnhancementContainer {
|
|
|
138
138
|
}
|
|
139
139
|
// Check if there's an enhKey
|
|
140
140
|
if (registryItem.enhKey) {
|
|
141
|
-
const ctx = { config: registryItem, mountCtx };
|
|
141
|
+
const ctx = { config: registryItem, mountCtx, emc: mountCtx?.emc };
|
|
142
142
|
const self = this;
|
|
143
143
|
// Get existing initVals from enhKey
|
|
144
144
|
const existingInitVals = self[registryItem.enhKey] &&
|
|
@@ -155,7 +155,7 @@ class ElementEnhancementContainer {
|
|
|
155
155
|
}
|
|
156
156
|
else {
|
|
157
157
|
// No enhKey, still pass attrInitVals
|
|
158
|
-
const ctx = { config: registryItem, mountCtx };
|
|
158
|
+
const ctx = { config: registryItem, mountCtx, emc: mountCtx?.emc };
|
|
159
159
|
instance = new SpawnClass(element, ctx, attrInitVals);
|
|
160
160
|
}
|
|
161
161
|
// Store in global instance map
|
package/object-extension.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assignGingerly, { EnhancementRegistry, ItemscopeRegistry, IAssignGingerlyOptions, getInstanceMap, INSTANCE_MAP_GUID } from './assignGingerly.js';
|
|
2
|
-
import { EnhancementConfig } from './types/assign-gingerly/types.js';
|
|
2
|
+
import { EnhancementConfig, SpawnContext } from './types/assign-gingerly/types.js';
|
|
3
3
|
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -225,7 +225,7 @@ class ElementEnhancementContainer {
|
|
|
225
225
|
|
|
226
226
|
// Check if there's an enhKey
|
|
227
227
|
if (registryItem.enhKey) {
|
|
228
|
-
const ctx = { config: registryItem, mountCtx };
|
|
228
|
+
const ctx: SpawnContext = { config: registryItem, mountCtx, emc: (mountCtx as any)?.emc };
|
|
229
229
|
const self = this as any;
|
|
230
230
|
|
|
231
231
|
// Get existing initVals from enhKey
|
|
@@ -245,7 +245,7 @@ class ElementEnhancementContainer {
|
|
|
245
245
|
self[registryItem.enhKey] = instance;
|
|
246
246
|
} else {
|
|
247
247
|
// No enhKey, still pass attrInitVals
|
|
248
|
-
const ctx = { config: registryItem, mountCtx };
|
|
248
|
+
const ctx: SpawnContext = { config: registryItem, mountCtx, emc: (mountCtx as any)?.emc };
|
|
249
249
|
instance = new SpawnClass(element, ctx, attrInitVals);
|
|
250
250
|
}
|
|
251
251
|
|
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
|
+
}
|
|
@@ -212,6 +212,13 @@ export interface SpawnContext<T = any, TMountContext = any> {
|
|
|
212
212
|
* Used for scoped parser registry access during attribute parsing.
|
|
213
213
|
*/
|
|
214
214
|
synthesizerElement?: Element;
|
|
215
|
+
/**
|
|
216
|
+
* The full EMC configuration object that triggered this spawn.
|
|
217
|
+
* Passed through so enhancement classes can access their full configuration
|
|
218
|
+
* (including customData) without needing to separately import the JSON file.
|
|
219
|
+
* This avoids duplicate JSON imports when using emoji shorthand aliases.
|
|
220
|
+
*/
|
|
221
|
+
emc?: any;
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
/**
|
|
@@ -234,6 +241,18 @@ export interface IAssignGingerlyOptions {
|
|
|
234
241
|
* When the signal is aborted, all event listeners are automatically removed
|
|
235
242
|
*/
|
|
236
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>;
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
/**
|
|
@@ -314,3 +333,130 @@ export interface ElementEnhancement{
|
|
|
314
333
|
dispose(registryItem: EnhancementConfig | string | symbol): void;
|
|
315
334
|
whenResolved(registryItem: EnhancementConfig | string | symbol, mountCtx?: any): Promise<any>;
|
|
316
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;
|