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.
@@ -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
@@ -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.37",
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
+ }
@@ -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;