assign-gingerly 0.0.58 → 0.0.60

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.
Files changed (42) hide show
  1. package/README.md +511 -142
  2. package/assignFrom-extension.js +25 -0
  3. package/assignFrom-extension.ts +53 -0
  4. package/assignFrom.js +306 -133
  5. package/assignFrom.ts +318 -232
  6. package/assignFromAsync-extension.js +28 -0
  7. package/assignFromAsync-extension.ts +58 -0
  8. package/assignFromAsync.js +120 -0
  9. package/assignFromAsync.ts +256 -0
  10. package/assignGingerly.js +50 -0
  11. package/assignGingerly.ts +51 -0
  12. package/builtInEmoji.js +25 -0
  13. package/builtInEmoji.ts +33 -0
  14. package/getValues.js +223 -0
  15. package/getValues.ts +255 -0
  16. package/handlers/join.ts +1 -1
  17. package/handlers/lazyLoad.js +33 -2
  18. package/handlers/lazyLoad.ts +47 -2
  19. package/handlers/lazyLoadSwitch.ts +1 -1
  20. package/handlers/manageTemplateList.js +226 -0
  21. package/handlers/manageTemplateList.ts +263 -0
  22. package/handlers/microDataJoin.ts +1 -1
  23. package/index.js +1 -0
  24. package/index.ts +2 -0
  25. package/inferencer/inferencer.js +9 -21
  26. package/inferencer/inferencer.ts +10 -21
  27. package/inferredAssignments.js +35 -5
  28. package/inferredAssignments.ts +58 -8
  29. package/markerUtils.js +136 -127
  30. package/package.json +30 -1
  31. package/playwright.config.ts +3 -2
  32. package/processHandlerCommands.js +36 -4
  33. package/processHandlerCommands.ts +38 -8
  34. package/resolveIdRef.js +70 -19
  35. package/resolveIdRef.ts +73 -21
  36. package/resolveValues.js +41 -125
  37. package/resolveValues.ts +131 -255
  38. package/types/assign-gingerly/types.d.ts +61 -0
  39. package/waitForSettled.js +57 -0
  40. package/waitForSettled.ts +65 -0
  41. package/withIdsCorrector.js +47 -0
  42. package/withIdsCorrector.ts +59 -0
package/assignFrom.ts CHANGED
@@ -1,140 +1,157 @@
1
1
  /**
2
- * Resolve RHS path strings against a source object, then assign the
3
- * resolved values into a target using assignGingerly.
2
+ * assignFrom.ts — Synchronous assign-from-source function.
4
3
  *
5
- * Combines resolveValues + assignGingerly into a single call.
6
- * Inherits all assignGingerly options (withMethods, aka, signal, etc.).
4
+ * Resolves RHS path strings synchronously via getValues, then assigns into the target.
5
+ * Supports looped substitution, #[x] refs, inferredAssignments, and handleSpreads — all sync.
7
6
  *
8
- * @param target - Object to merge resolved values into
9
- * @param pattern - Object whose RHS values may contain `?.` path strings
10
- * @param options - Options including `from` (source object) and any assignGingerly options
11
- * @returns The target object after merging
7
+ * For async protocol handlers or awaitable handler execution, use assignFromAsync.
12
8
  *
13
- * @example
14
- * const source = { theme: { color: 'red' }, label: 'Hello' };
15
- * const target = { color: 'blue', text: '' };
16
- * assignFrom(target, {
17
- * color: '?.theme?.color',
18
- * text: '?.label'
19
- * }, { from: source });
20
- * // target is now { color: 'red', text: 'Hello' }
9
+ * Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
21
10
  */
22
- import { resolveValues, ResolveValuesOptions } from './resolveValues.js';
11
+
12
+ import { getValues, getValue } from './getValues.js';
23
13
  import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
14
+ import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
15
+ import { processInferredAssignments } from './inferredAssignments.js';
24
16
  import type { AssignPermissions } from './isAllowedImportPath.js';
25
17
 
26
- export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValuesOptions {
27
- /** Source object to resolve RHS path strings against */
28
- from: any;
29
-
30
- /** Loop variable bindings — expand pattern entries containing ${x} */
31
- where_x_in?: string[];
32
- /** Loop variable bindings — expand pattern entries containing ${y} */
33
- where_y_in?: string[];
34
- /** Loop variable bindings — expand pattern entries containing ${z} */
35
- where_z_in?: string[];
36
-
37
- /**
38
- * Cached element references by variable name.
39
- * Used with `#[varName]` syntax in LHS keys for fast repeated element access.
40
- *
41
- * - String value: existing element ID (uses getElementById)
42
- * - Object value: { qry: 'selector' } — finds element via querySelector on target, auto-assigns an ID
43
- *
44
- * Elements are cached via WeakRef with getElementById fallback on cache miss.
45
- */
46
- withIds?: Record<string, string | { qry: string }>;
47
-
48
- /**
49
- * Handler implementations scoped to this call.
50
- * Key: the `do` name referenced in handler configs.
51
- * Value: a class constructor, or an import path to dynamically load one.
52
- *
53
- * Import paths must be local (relative, absolute, or bare specifier — no cross-domain URLs).
54
- * The module's default export is checked first; otherwise the first exported class
55
- * with an `assign` method on its prototype is used.
56
- *
57
- * Built-in handlers (builtIns.*) auto-load without needing to be listed here.
58
- *
59
- * @example
60
- * handlers: {
61
- * 'my-list': MyListHandler, // class constructor
62
- * 'my-chart': './handlers/chart.js', // dynamic import path
63
- * 'vendor-widget': 'some-package/handler.js', // bare specifier (import map)
64
- * }
65
- */
66
- handlers?: Record<string, AssignFromHandlerConstructor | string>;
67
-
68
- /**
69
- * Inferred assignments — automatically distribute source values to matching
70
- * DOM elements based on structural conventions (itemprop, name, etc.).
71
- *
72
- * Uses the inferencer submodule to determine the correct property for each
73
- * matched element (textContent, value, checked, dateTime, ish, etc.).
74
- *
75
- * @example
76
- * inferredAssignments: {
77
- * byItemprop: ['user', 'name', 'email'], // or true for all source keys
78
- * beVigilant: true, // watch for new matching elements (requires signal)
79
- * }
80
- */
81
- inferredAssignments?: {
82
- byItemprop?: string[] | true;
83
- /** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
84
- beVigilant?: boolean;
85
- };
86
-
87
- /**
88
- * Bulk enhancement application via EMC JSON configs.
89
- * Finds matching elements and spawns enhancements on them.
90
- *
91
- * Each entry specifies an EMC JSON path and optionally overrides the matching selector.
92
- * Enhancements are auto-registered if not already present in the enhancement registry.
93
- *
94
- * No scope perimeter is applied — use mount-observer for reactive/scoped enhancement.
95
- *
96
- * @example
97
- * enhance: [
98
- * { emc: 'be-bound/emc.json', matching: '[name]' },
99
- * { emc: 'be-observant/emc.json', matching: '[itemprop]' },
100
- * ]
101
- */
102
- enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
18
+ // Re-export types and interfaces for consumers
19
+ export type { AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './assignFromAsync.js';
20
+ import type { AssignFromOptions } from './assignFromAsync.js';
21
+
22
+ /**
23
+ * Supported substitution variables and their option keys.
24
+ */
25
+ export const SUBSTITUTION_VARS = [
26
+ { placeholder: '${x}', optionKey: 'where_x_in' },
27
+ { placeholder: '${y}', optionKey: 'where_y_in' },
28
+ { placeholder: '${z}', optionKey: 'where_z_in' },
29
+ ] as const;
30
+
31
+ /**
32
+ * Check if a key ends with the handler operator ' =>'.
33
+ */
34
+ export function isHandlerCommand(key: string): boolean {
35
+ return key.endsWith(' =>');
103
36
  }
104
37
 
105
38
  /**
106
- * Interface for assignFrom handler classes.
107
- * Handlers are invoked when a LHS key ends with ' =>'.
39
+ * Check if a key ends with the ternary operator ' ?='.
108
40
  */
109
- export interface AssignFromHandler {
110
- assign(lhsTarget: any, resolvedParams: Record<string, any>, options: AssignFromOptions): Promise<void> | void;
41
+ export function isTernaryCommand(key: string): boolean {
42
+ return key.endsWith(' ?=');
111
43
  }
112
44
 
113
- export interface AssignFromHandlerConstructor {
114
- new (config: any): AssignFromHandler;
45
+ /**
46
+ * Parse a ?= ternary command and extract the LHS path.
47
+ */
48
+ export function parseTernaryCommand(key: string): string | null {
49
+ if (!isTernaryCommand(key)) return null;
50
+ return key.substring(0, key.length - 3); // Remove ' ?=' suffix
115
51
  }
116
52
 
117
53
  /**
118
- * Check if a key ends with the handler operator ' =>'.
54
+ * Resolve a single value — if it's a `?.` path string, resolve against source.
55
+ * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
119
56
  */
120
- function isHandlerCommand(key: string): boolean {
121
- return key.endsWith(' =>');
57
+ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions): any {
58
+ if (typeof value === 'string' && value.startsWith('?.')) {
59
+ return getValue(value, source, {
60
+ withMethods: options.withMethods,
61
+ aka: options.aka,
62
+ protocols: options.protocols
63
+ });
64
+ }
65
+ if (typeof value === 'string' && value.includes('://') && options.protocols) {
66
+ // Check if it matches a known protocol
67
+ const protoEnd = value.indexOf('://');
68
+ const protocol = value.substring(0, protoEnd);
69
+ if (options.protocols[protocol]) {
70
+ return getValue(value, source, {
71
+ withMethods: options.withMethods,
72
+ aka: options.aka,
73
+ protocols: options.protocols
74
+ });
75
+ }
76
+ }
77
+ return value;
122
78
  }
123
79
 
124
80
  /**
125
- * Supported substitution variables and their option keys.
81
+ * Evaluate a ?= ternary expression.
82
+ *
83
+ * Supported forms:
84
+ * - [ifTruthy, thenResult] — guard (skip if falsy)
85
+ * - [ifTruthy, thenResult, elseResult] — ternary
86
+ * - [ifTrue, trueResult, falseResult, neither] — three-state (true/false/nullish)
87
+ * - [[lhs, rhs], ifEqual, ifNotEqual?] — equality comparison
88
+ * - [[lhs, rhs], ifEqual] — equality guard
89
+ *
90
+ * Returns undefined to signal "skip assignment" (guard forms when condition not met).
126
91
  */
127
- const SUBSTITUTION_VARS = [
128
- { placeholder: '${x}', optionKey: 'where_x_in' },
129
- { placeholder: '${y}', optionKey: 'where_y_in' },
130
- { placeholder: '${z}', optionKey: 'where_z_in' },
131
- ] as const;
92
+ const TERNARY_SKIP = Symbol('ternary-skip');
93
+
94
+ function evaluateTernary(arr: any[], source: any, options: AssignFromOptions): any {
95
+ const condition = arr[0];
96
+
97
+ if (Array.isArray(condition)) {
98
+ // Comparison mode: [[lhs, rhs], ...] or [[lhs, op, rhs], ...]
99
+ const lhs = resolveTernaryValue(condition[0], source, options);
100
+ if (condition.length === 2) {
101
+ // Equality: [[lhs, rhs], result, elseResult?]
102
+ const rhs = resolveTernaryValue(condition[1], source, options);
103
+ if (lhs === rhs) {
104
+ return resolveTernaryValue(arr[1], source, options);
105
+ } else {
106
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
107
+ }
108
+ } else {
109
+ // Operator: [[lhs, op, rhs], result, elseResult?]
110
+ const op = condition[1] as string;
111
+ const rhs = resolveTernaryValue(condition[2], source, options);
112
+ const satisfied = compareWithOp(lhs, op, rhs);
113
+ if (satisfied) {
114
+ return resolveTernaryValue(arr[1], source, options);
115
+ } else {
116
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
117
+ }
118
+ }
119
+ } else {
120
+ // Truthiness mode
121
+ const resolved = resolveTernaryValue(condition, source, options);
122
+ if (arr.length === 4) {
123
+ // [ifTrue, trueResult, falseResult, neitherResult]
124
+ if (resolved == null) return resolveTernaryValue(arr[3], source, options);
125
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
126
+ } else if (arr.length === 3) {
127
+ // [ifTruthy, thenResult, elseResult]
128
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
129
+ } else {
130
+ // [ifTruthy, thenResult] — guard, skip if falsy
131
+ return resolved ? resolveTernaryValue(arr[1], source, options) : TERNARY_SKIP;
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Compare two values with a given operator.
138
+ */
139
+ function compareWithOp(lhs: any, op: string, rhs: any): boolean {
140
+ switch (op) {
141
+ case '===': return lhs === rhs;
142
+ case '!==': return lhs !== rhs;
143
+ case '>': return lhs > rhs;
144
+ case '>=': return lhs >= rhs;
145
+ case '<': return lhs < rhs;
146
+ case '<=': return lhs <= rhs;
147
+ default: return lhs === rhs; // fallback to equality
148
+ }
149
+ }
132
150
 
133
151
  /**
134
152
  * Recursively substitute a placeholder in all string values of an object.
135
- * Returns a new object (shallow clone at each level) with substitutions applied.
136
153
  */
137
- function substituteInValue(value: any, placeholder: string, replacement: string): any {
154
+ export function substituteInValue(value: any, placeholder: string, replacement: string): any {
138
155
  if (typeof value === 'string') {
139
156
  return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
140
157
  }
@@ -157,7 +174,7 @@ function substituteInValue(value: any, placeholder: string, replacement: string)
157
174
  /**
158
175
  * Check if a pattern entry (key + value) contains a given placeholder.
159
176
  */
160
- function entryContainsPlaceholder(key: string, value: any, placeholder: string): boolean {
177
+ export function entryContainsPlaceholder(key: string, value: any, placeholder: string): boolean {
161
178
  if (key.includes(placeholder)) return true;
162
179
  return valueContainsPlaceholder(value, placeholder);
163
180
  }
@@ -165,7 +182,7 @@ function entryContainsPlaceholder(key: string, value: any, placeholder: string):
165
182
  /**
166
183
  * Check if a value (string, object, or array) contains a placeholder.
167
184
  */
168
- function valueContainsPlaceholder(value: any, placeholder: string): boolean {
185
+ export function valueContainsPlaceholder(value: any, placeholder: string): boolean {
169
186
  if (typeof value === 'string') return value.includes(placeholder);
170
187
  if (Array.isArray(value)) return value.some(item => valueContainsPlaceholder(item, placeholder));
171
188
  if (value && typeof value === 'object') {
@@ -179,12 +196,8 @@ function valueContainsPlaceholder(value: any, placeholder: string): boolean {
179
196
 
180
197
  /**
181
198
  * Expand looped substitution variables in a pattern.
182
- * Applies cartesian expansion: x values are expanded first, then y, then z.
183
- * Each variable multiplies the entries — result count = x.length × y.length × z.length.
184
- *
185
- * Returns the expanded pattern (or the original if no substitutions apply).
186
199
  */
187
- function expandSubstitutions(
200
+ export function expandSubstitutions(
188
201
  pattern: Record<string, any>,
189
202
  options: AssignFromOptions
190
203
  ): Record<string, any> {
@@ -197,7 +210,6 @@ function expandSubstitutions(
197
210
  const expanded: [string, any][] = [];
198
211
  for (const [key, value] of entries) {
199
212
  if (entryContainsPlaceholder(key, value, placeholder)) {
200
- // Expand this entry for each value in the variable array
201
213
  for (const replacement of values) {
202
214
  const newKey = key.includes(placeholder)
203
215
  ? key.replaceAll(placeholder, replacement)
@@ -206,7 +218,6 @@ function expandSubstitutions(
206
218
  expanded.push([newKey, newValue]);
207
219
  }
208
220
  } else {
209
- // No placeholder in this entry — pass through
210
221
  expanded.push([key, value]);
211
222
  }
212
223
  }
@@ -218,14 +229,11 @@ function expandSubstitutions(
218
229
 
219
230
  /**
220
231
  * Convert entries to an object, merging duplicate handler (` =>`) keys into arrays.
221
- * For normal (non-handler) keys, later entries overwrite earlier ones (standard object behavior).
222
- * For handler keys, duplicate entries are combined into an array (Multiple Handlers pattern).
223
232
  */
224
- function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
233
+ export function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
225
234
  const result: Record<string, any> = {};
226
235
  for (const [key, value] of entries) {
227
236
  if (key.endsWith(' =>') && key in result) {
228
- // Duplicate handler key — merge into array
229
237
  const existing = result[key];
230
238
  if (Array.isArray(existing)) {
231
239
  existing.push(value);
@@ -239,20 +247,37 @@ function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
239
247
  return result;
240
248
  }
241
249
 
242
- export async function assignFrom(
243
- target: any,
244
- pattern: Record<string, any>,
245
- options: AssignFromOptions,
246
- permissions?: AssignPermissions
247
- ): Promise<any> {
248
- // First: expand looped substitution variables (${x}, ${y}, ${z})
249
- const expandedPattern = expandSubstitutions(pattern, options);
250
+ /**
251
+ * Recursively walk an object and handle "..." spread keys.
252
+ */
253
+ export function handleSpreads(obj: Record<string, any>): Record<string, any> {
254
+ for (const [key, value] of Object.entries(obj)) {
255
+ if (key !== '...' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
256
+ const proto = Object.getPrototypeOf(value);
257
+ if (proto === Object.prototype || proto === null) {
258
+ obj[key] = handleSpreads(value);
259
+ }
260
+ }
261
+ }
262
+ if ('...' in obj) {
263
+ const spreadValue = obj['...'];
264
+ delete obj['...'];
265
+ if (spreadValue && typeof spreadValue === 'object') {
266
+ Object.assign(obj, spreadValue);
267
+ }
268
+ }
269
+ return obj;
270
+ }
250
271
 
251
- // Separate handler commands ( =>), #[x] keys, and normal keys
272
+ /**
273
+ * Categorize pattern keys into handler keys, #[x] keys, and normal keys.
274
+ */
275
+ export function categorizeKeys(expandedPattern: Record<string, any>) {
252
276
  const handlerKeys: string[] = [];
253
277
  const normalPattern: Record<string, any> = {};
254
278
  const idRefNormalKeys: string[] = [];
255
279
  const idRefHandlerKeys: string[] = [];
280
+ const ternaryKeys: string[] = [];
256
281
 
257
282
  for (const key of Object.keys(expandedPattern)) {
258
283
  if (isHandlerCommand(key)) {
@@ -261,6 +286,8 @@ export async function assignFrom(
261
286
  } else {
262
287
  handlerKeys.push(key);
263
288
  }
289
+ } else if (isTernaryCommand(key)) {
290
+ ternaryKeys.push(key);
264
291
  } else if (key.startsWith('#[')) {
265
292
  idRefNormalKeys.push(key);
266
293
  } else {
@@ -268,131 +295,190 @@ export async function assignFrom(
268
295
  }
269
296
  }
270
297
 
271
- // Process normal keys via resolveValues + assignGingerly
298
+ return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
299
+ }
300
+
301
+ /**
302
+ * Merge withIds and at into a single lookup map for resolveIdVariable.
303
+ */
304
+ function getEffectiveIds(options: AssignFromOptions): Record<string, any> | undefined {
305
+ if (!options.withIds && !options.at) return undefined;
306
+ if (options.withIds && !options.at) return options.withIds;
307
+ if (!options.withIds && options.at) return options.at;
308
+ return { ...options.withIds, ...options.at };
309
+ }
310
+
311
+ /**
312
+ * Process #[x] normal keys synchronously.
313
+ */
314
+ function processIdRefNormalKeys(
315
+ idRefNormalKeys: string[],
316
+ expandedPattern: Record<string, any>,
317
+ target: any,
318
+ options: AssignFromOptions
319
+ ): void {
320
+ const ids = getEffectiveIds(options);
321
+ if (!ids) return;
322
+
323
+ for (const key of idRefNormalKeys) {
324
+ const parsed = parseIdRef(key);
325
+ if (!parsed) continue;
326
+
327
+ const el = resolveIdVariable(parsed.varName, target, ids);
328
+ if (!el) continue;
329
+
330
+ const value = expandedPattern[key];
331
+ if (parsed.remainingPath) {
332
+ const resolvedValue = getValues(
333
+ { __v: value }, options.from,
334
+ { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
335
+ );
336
+ assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
337
+ } else {
338
+ const resolvedValue = getValues(
339
+ typeof value === 'object' && value !== null ? value : { __v: value },
340
+ options.from,
341
+ { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
342
+ );
343
+ if (!('__v' in resolvedValue)) {
344
+ assignGingerly(el, resolvedValue, options);
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Synchronous assignFrom — resolves values, assigns to target, all without awaiting.
352
+ *
353
+ * Handler commands (` =>`), beVigilant, and enhance are fire-and-forget (async, non-blocking).
354
+ * For awaitable handler execution, use assignFromAsync.
355
+ *
356
+ * @param target - Object to merge resolved values into
357
+ * @param pattern - Object whose RHS values may contain `?.` path strings
358
+ * @param options - Options including `from` (source object)
359
+ * @param permissions - Optional security permissions
360
+ * @returns The target object after merging
361
+ */
362
+ export function assignFrom(
363
+ target: any,
364
+ pattern: Record<string, any>,
365
+ options: AssignFromOptions,
366
+ permissions?: AssignPermissions
367
+ ): any {
368
+ // Expand looped substitution variables
369
+ const expandedPattern = expandSubstitutions(pattern, options);
370
+
371
+ // Categorize keys
372
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
373
+
374
+ // Process ?= ternary keys (sync)
375
+ if (ternaryKeys.length > 0) {
376
+ const ternaryResolved: Record<string, any> = {};
377
+ for (const key of ternaryKeys) {
378
+ const lhsPath = parseTernaryCommand(key);
379
+ if (!lhsPath) continue;
380
+ const arr = expandedPattern[key];
381
+ if (!Array.isArray(arr) || arr.length < 2) continue;
382
+ const result = evaluateTernary(arr, options.from, options);
383
+ if (result !== TERNARY_SKIP) {
384
+ ternaryResolved[lhsPath] = result;
385
+ }
386
+ }
387
+ if (Object.keys(ternaryResolved).length > 0) {
388
+ assignGingerly(target, ternaryResolved, options);
389
+ }
390
+ }
391
+
392
+ // Process normal keys via getValues (sync) + assignGingerly
272
393
  if (Object.keys(normalPattern).length > 0) {
273
- const resolved = await resolveValues(normalPattern, options.from, {
394
+ // Resolve #[x] references on RHS values before getValues
395
+ if (options.withIds || options.at) {
396
+ const ids = getEffectiveIds(options)!;
397
+ for (const key of Object.keys(normalPattern)) {
398
+ const value = normalPattern[key];
399
+ if (typeof value === 'string' && value.startsWith('#[')) {
400
+ const closeIdx = value.indexOf(']');
401
+ if (closeIdx !== -1) {
402
+ const varName = value.substring(2, closeIdx);
403
+ const el = resolveIdVariable(varName, target, ids);
404
+ if (el) {
405
+ const remainingPath = value.substring(closeIdx + 1);
406
+ if (remainingPath) {
407
+ normalPattern[key] = getValue(remainingPath, el, {
408
+ withMethods: options.withMethods,
409
+ aka: options.aka,
410
+ protocols: options.protocols
411
+ });
412
+ } else {
413
+ normalPattern[key] = el.id; // bare #[x] → ID string
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ const resolved = getValues(normalPattern, options.from, {
274
422
  withMethods: options.withMethods,
275
423
  aka: options.aka,
276
424
  protocols: options.protocols
277
425
  });
278
426
 
279
- // Recursively handle "..." spread keys at all nesting levels
280
427
  handleSpreads(resolved);
281
-
282
428
  assignGingerly(target, resolved, options);
283
429
  }
284
430
 
285
- // Process #[x] normal keys — resolve element, then apply remaining path + value
286
- if (idRefNormalKeys.length > 0 && options.withIds) {
287
- const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
288
- for (const key of idRefNormalKeys) {
289
- const parsed = parseIdRef(key);
290
- if (!parsed) continue;
291
-
292
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
293
- if (!el) continue;
294
-
295
- const value = expandedPattern[key];
296
- if (parsed.remainingPath) {
297
- // Resolve the RHS value
298
- const resolvedValue = await resolveValues(
299
- { __v: value }, options.from,
300
- { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
301
- );
302
- // Apply remaining path on the resolved element
303
- assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
304
- } else {
305
- // No remaining path — resolve and assign directly to the element
306
- const resolvedValue = await resolveValues(
307
- typeof value === 'object' && value !== null ? value : { __v: value },
308
- options.from,
309
- { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
310
- );
311
- if ('__v' in resolvedValue) {
312
- // Single value — can't assign to element root without a path
313
- } else {
314
- assignGingerly(el, resolvedValue, options);
315
- }
316
- }
317
- }
431
+ // Process #[x] normal keys (sync)
432
+ if (idRefNormalKeys.length > 0) {
433
+ processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, options);
318
434
  }
319
435
 
320
- // Process handler commands ( =>) — dynamically imported only when needed
436
+ // Process handler commands — fire-and-forget (async)
321
437
  if (handlerKeys.length > 0) {
322
- const { processHandlerCommands } = await import('./processHandlerCommands.js');
323
- await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
438
+ import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
439
+ processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
440
+ });
324
441
  }
325
442
 
326
- // Process #[x] handler keys — resolve element, then pass to handler processing
327
- if (idRefHandlerKeys.length > 0 && options.withIds) {
328
- const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
329
- const { processHandlerCommands } = await import('./processHandlerCommands.js');
330
-
331
- for (const key of idRefHandlerKeys) {
332
- const parsed = parseIdRef(key);
333
- if (!parsed) continue;
334
-
335
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
336
- if (!el) continue;
337
-
338
- // Build a synthetic key for processHandlerCommands:
339
- // The resolved element becomes the target, remaining path is the LHS
340
- const syntheticKey = parsed.remainingPath
341
- ? `${parsed.remainingPath} =>`
342
- : ' =>';
343
-
344
- const syntheticPattern: Record<string, any> = {
345
- [syntheticKey]: expandedPattern[key]
346
- };
347
-
348
- await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
349
- }
443
+ // Process #[x] handler keys — fire-and-forget (async)
444
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
445
+ const ids = getEffectiveIds(options)!;
446
+ import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
447
+ for (const key of idRefHandlerKeys) {
448
+ const parsed = parseIdRef(key);
449
+ if (!parsed) continue;
450
+ const el = resolveIdVariable(parsed.varName, target, ids);
451
+ if (!el) continue;
452
+ const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
453
+ const syntheticPattern = { [syntheticKey]: expandedPattern[key] };
454
+ processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
455
+ }
456
+ });
350
457
  }
351
458
 
352
- // Process inferred assignments — dynamically imported only when option is present
353
- if (options.inferredAssignments) {
354
- const { processInferredAssignments } = await import('./inferredAssignments.js');
355
- await processInferredAssignments(target, options.from, options.inferredAssignments);
459
+ // Process inferred assignments (sync)
460
+ if (options.infer) {
461
+ processInferredAssignments(target, options.from, options.infer);
356
462
 
357
- // Set up MutationObserver for new matching elements if beVigilant
358
- if (options.inferredAssignments.beVigilant) {
463
+ // beVigilant — fire-and-forget (async)
464
+ if (options.infer.beVigilant) {
359
465
  if (!options.signal) {
360
- throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
466
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
361
467
  }
362
- const { setupVigilantObserver } = await import('./beVigilant.js');
363
- setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
468
+ import('./beVigilant.js').then(({ setupVigilantObserver }) => {
469
+ setupVigilantObserver(target, options.from, options.infer!, options.signal!);
470
+ });
364
471
  }
365
472
  }
366
473
 
367
- // Process bulk enhancements — dynamically imported only when option is present
474
+ // Process bulk enhancements — fire-and-forget (async)
368
475
  if (options.enhance && options.enhance.length > 0) {
369
- const { enhanceAll } = await import('./enhanceAll.js');
370
- await enhanceAll(target, options.enhance, permissions);
476
+ import('./enhanceAll.js').then(({ enhanceAll }) => {
477
+ enhanceAll(target, options.enhance!, permissions);
478
+ });
371
479
  }
372
480
 
373
481
  return target;
374
482
  }
375
483
 
376
- /**
377
- * Recursively walk an object and handle "..." spread keys.
378
- * When a "..." key is found, its value (which should be an object after protocol resolution)
379
- * is spread into the parent, replacing the "..." entry.
380
- */
381
- function handleSpreads(obj: Record<string, any>): Record<string, any> {
382
- for (const [key, value] of Object.entries(obj)) {
383
- if (key !== '...' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
384
- const proto = Object.getPrototypeOf(value);
385
- if (proto === Object.prototype || proto === null) {
386
- obj[key] = handleSpreads(value);
387
- }
388
- }
389
- }
390
- if ('...' in obj) {
391
- const spreadValue = obj['...'];
392
- delete obj['...'];
393
- if (spreadValue && typeof spreadValue === 'object') {
394
- Object.assign(obj, spreadValue);
395
- }
396
- }
397
- return obj;
398
- }
484
+ export default assignFrom;