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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.38",
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.0",
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: split on '?.' delimiter, filter empties
103
- const parts = aliased.split('?.').filter(p => p.length > 0);
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: split on '?.' delimiter, filter empties
132
- const parts = aliased.split('?.').filter(p => p.length > 0);
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;