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
@@ -0,0 +1,25 @@
1
+ /**
2
+ * assignFrom-extension.js — Adds assignFrom to Object.prototype.
3
+ *
4
+ * Import this module for the side effect of extending all objects with
5
+ * the assignFrom method, enabling fluent method chaining:
6
+ *
7
+ * @example
8
+ * import 'assign-gingerly/assignFrom-extension.js';
9
+ *
10
+ * oElement
11
+ * .assignFrom({ '?.textContent': '?.greeting' }, { from: vm1 })
12
+ * .assignFrom({ '?.style Y=': { color: '?.themeColor' } }, { from: vm2 });
13
+ */
14
+
15
+ import { assignFrom } from './assignFrom.js';
16
+
17
+ Object.defineProperty(Object.prototype, 'assignFrom', {
18
+ value: function (pattern, options) {
19
+ assignFrom(this, pattern, options);
20
+ return this;
21
+ },
22
+ writable: true,
23
+ enumerable: false,
24
+ configurable: true,
25
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * assignFrom-extension.ts — Adds assignFrom to Object.prototype.
3
+ *
4
+ * Import this module for the side effect of extending all objects with
5
+ * the assignFrom method, enabling fluent method chaining:
6
+ *
7
+ * @example
8
+ * import 'assign-gingerly/assignFrom-extension.js';
9
+ *
10
+ * oElement
11
+ * .assignFrom({ '?.textContent': '?.greeting' }, { from: vm1 })
12
+ * .assignFrom({ '?.style Y=': { color: '?.themeColor' } }, { from: vm2 });
13
+ */
14
+
15
+ import { assignFrom } from './assignFrom.js';
16
+ import type { AssignFromOptions } from './assignFromAsync.js';
17
+
18
+ declare global {
19
+ interface Object {
20
+ /**
21
+ * Resolve RHS path strings from a source object and assign into this object.
22
+ * Synchronous — handlers are fire-and-forget.
23
+ *
24
+ * @param pattern - Object with LHS paths as keys and RHS path strings (or literals) as values
25
+ * @param options - Configuration including `from` (source object), protocols, withMethods, etc.
26
+ * @returns This object after assignment
27
+ *
28
+ * @example
29
+ * oElement.assignFrom({
30
+ * '?.textContent': '?.greeting',
31
+ * '?.style Y=': { width: '?.width' }
32
+ * }, { from: viewModel });
33
+ */
34
+ assignFrom(
35
+ pattern: Record<string, any>,
36
+ options: AssignFromOptions
37
+ ): this;
38
+ }
39
+ }
40
+
41
+ Object.defineProperty(Object.prototype, 'assignFrom', {
42
+ value: function <T extends object>(
43
+ this: T,
44
+ pattern: Record<string, any>,
45
+ options: AssignFromOptions
46
+ ): T {
47
+ assignFrom(this, pattern, options);
48
+ return this;
49
+ },
50
+ writable: true,
51
+ enumerable: false,
52
+ configurable: true,
53
+ });
package/assignFrom.js CHANGED
@@ -1,45 +1,149 @@
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 } from './resolveValues.js';
11
+ import { getValues, getValue } from './getValues.js';
23
12
  import assignGingerly from './assignGingerly.js';
24
- /**
25
- * Check if a key ends with the handler operator ' =>'.
26
- */
27
- function isHandlerCommand(key) {
28
- return key.endsWith(' =>');
29
- }
13
+ import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
14
+ import { processInferredAssignments } from './inferredAssignments.js';
30
15
  /**
31
16
  * Supported substitution variables and their option keys.
32
17
  */
33
- const SUBSTITUTION_VARS = [
18
+ export const SUBSTITUTION_VARS = [
34
19
  { placeholder: '${x}', optionKey: 'where_x_in' },
35
20
  { placeholder: '${y}', optionKey: 'where_y_in' },
36
21
  { placeholder: '${z}', optionKey: 'where_z_in' },
37
22
  ];
23
+ /**
24
+ * Check if a key ends with the handler operator ' =>'.
25
+ */
26
+ export function isHandlerCommand(key) {
27
+ return key.endsWith(' =>');
28
+ }
29
+ /**
30
+ * Check if a key ends with the ternary operator ' ?='.
31
+ */
32
+ export function isTernaryCommand(key) {
33
+ return key.endsWith(' ?=');
34
+ }
35
+ /**
36
+ * Parse a ?= ternary command and extract the LHS path.
37
+ */
38
+ export function parseTernaryCommand(key) {
39
+ if (!isTernaryCommand(key))
40
+ return null;
41
+ return key.substring(0, key.length - 3); // Remove ' ?=' suffix
42
+ }
43
+ /**
44
+ * Resolve a single value — if it's a `?.` path string, resolve against source.
45
+ * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
46
+ */
47
+ function resolveTernaryValue(value, source, options) {
48
+ if (typeof value === 'string' && value.startsWith('?.')) {
49
+ return getValue(value, source, {
50
+ withMethods: options.withMethods,
51
+ aka: options.aka,
52
+ protocols: options.protocols
53
+ });
54
+ }
55
+ if (typeof value === 'string' && value.includes('://') && options.protocols) {
56
+ // Check if it matches a known protocol
57
+ const protoEnd = value.indexOf('://');
58
+ const protocol = value.substring(0, protoEnd);
59
+ if (options.protocols[protocol]) {
60
+ return getValue(value, source, {
61
+ withMethods: options.withMethods,
62
+ aka: options.aka,
63
+ protocols: options.protocols
64
+ });
65
+ }
66
+ }
67
+ return value;
68
+ }
69
+ /**
70
+ * Evaluate a ?= ternary expression.
71
+ *
72
+ * Supported forms:
73
+ * - [ifTruthy, thenResult] — guard (skip if falsy)
74
+ * - [ifTruthy, thenResult, elseResult] — ternary
75
+ * - [ifTrue, trueResult, falseResult, neither] — three-state (true/false/nullish)
76
+ * - [[lhs, rhs], ifEqual, ifNotEqual?] — equality comparison
77
+ * - [[lhs, rhs], ifEqual] — equality guard
78
+ *
79
+ * Returns undefined to signal "skip assignment" (guard forms when condition not met).
80
+ */
81
+ const TERNARY_SKIP = Symbol('ternary-skip');
82
+ function evaluateTernary(arr, source, options) {
83
+ const condition = arr[0];
84
+ if (Array.isArray(condition)) {
85
+ // Comparison mode: [[lhs, rhs], ...] or [[lhs, op, rhs], ...]
86
+ const lhs = resolveTernaryValue(condition[0], source, options);
87
+ if (condition.length === 2) {
88
+ // Equality: [[lhs, rhs], result, elseResult?]
89
+ const rhs = resolveTernaryValue(condition[1], source, options);
90
+ if (lhs === rhs) {
91
+ return resolveTernaryValue(arr[1], source, options);
92
+ }
93
+ else {
94
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
95
+ }
96
+ }
97
+ else {
98
+ // Operator: [[lhs, op, rhs], result, elseResult?]
99
+ const op = condition[1];
100
+ const rhs = resolveTernaryValue(condition[2], source, options);
101
+ const satisfied = compareWithOp(lhs, op, rhs);
102
+ if (satisfied) {
103
+ return resolveTernaryValue(arr[1], source, options);
104
+ }
105
+ else {
106
+ return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ // Truthiness mode
112
+ const resolved = resolveTernaryValue(condition, source, options);
113
+ if (arr.length === 4) {
114
+ // [ifTrue, trueResult, falseResult, neitherResult]
115
+ if (resolved == null)
116
+ return resolveTernaryValue(arr[3], source, options);
117
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
118
+ }
119
+ else if (arr.length === 3) {
120
+ // [ifTruthy, thenResult, elseResult]
121
+ return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
122
+ }
123
+ else {
124
+ // [ifTruthy, thenResult] — guard, skip if falsy
125
+ return resolved ? resolveTernaryValue(arr[1], source, options) : TERNARY_SKIP;
126
+ }
127
+ }
128
+ }
129
+ /**
130
+ * Compare two values with a given operator.
131
+ */
132
+ function compareWithOp(lhs, op, rhs) {
133
+ switch (op) {
134
+ case '===': return lhs === rhs;
135
+ case '!==': return lhs !== rhs;
136
+ case '>': return lhs > rhs;
137
+ case '>=': return lhs >= rhs;
138
+ case '<': return lhs < rhs;
139
+ case '<=': return lhs <= rhs;
140
+ default: return lhs === rhs; // fallback to equality
141
+ }
142
+ }
38
143
  /**
39
144
  * Recursively substitute a placeholder in all string values of an object.
40
- * Returns a new object (shallow clone at each level) with substitutions applied.
41
145
  */
42
- function substituteInValue(value, placeholder, replacement) {
146
+ export function substituteInValue(value, placeholder, replacement) {
43
147
  if (typeof value === 'string') {
44
148
  return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
45
149
  }
@@ -61,7 +165,7 @@ function substituteInValue(value, placeholder, replacement) {
61
165
  /**
62
166
  * Check if a pattern entry (key + value) contains a given placeholder.
63
167
  */
64
- function entryContainsPlaceholder(key, value, placeholder) {
168
+ export function entryContainsPlaceholder(key, value, placeholder) {
65
169
  if (key.includes(placeholder))
66
170
  return true;
67
171
  return valueContainsPlaceholder(value, placeholder);
@@ -69,7 +173,7 @@ function entryContainsPlaceholder(key, value, placeholder) {
69
173
  /**
70
174
  * Check if a value (string, object, or array) contains a placeholder.
71
175
  */
72
- function valueContainsPlaceholder(value, placeholder) {
176
+ export function valueContainsPlaceholder(value, placeholder) {
73
177
  if (typeof value === 'string')
74
178
  return value.includes(placeholder);
75
179
  if (Array.isArray(value))
@@ -84,12 +188,8 @@ function valueContainsPlaceholder(value, placeholder) {
84
188
  }
85
189
  /**
86
190
  * Expand looped substitution variables in a pattern.
87
- * Applies cartesian expansion: x values are expanded first, then y, then z.
88
- * Each variable multiplies the entries — result count = x.length × y.length × z.length.
89
- *
90
- * Returns the expanded pattern (or the original if no substitutions apply).
91
191
  */
92
- function expandSubstitutions(pattern, options) {
192
+ export function expandSubstitutions(pattern, options) {
93
193
  let entries = Object.entries(pattern);
94
194
  for (const { placeholder, optionKey } of SUBSTITUTION_VARS) {
95
195
  const values = options[optionKey];
@@ -98,7 +198,6 @@ function expandSubstitutions(pattern, options) {
98
198
  const expanded = [];
99
199
  for (const [key, value] of entries) {
100
200
  if (entryContainsPlaceholder(key, value, placeholder)) {
101
- // Expand this entry for each value in the variable array
102
201
  for (const replacement of values) {
103
202
  const newKey = key.includes(placeholder)
104
203
  ? key.replaceAll(placeholder, replacement)
@@ -108,7 +207,6 @@ function expandSubstitutions(pattern, options) {
108
207
  }
109
208
  }
110
209
  else {
111
- // No placeholder in this entry — pass through
112
210
  expanded.push([key, value]);
113
211
  }
114
212
  }
@@ -118,14 +216,11 @@ function expandSubstitutions(pattern, options) {
118
216
  }
119
217
  /**
120
218
  * Convert entries to an object, merging duplicate handler (` =>`) keys into arrays.
121
- * For normal (non-handler) keys, later entries overwrite earlier ones (standard object behavior).
122
- * For handler keys, duplicate entries are combined into an array (Multiple Handlers pattern).
123
219
  */
124
- function mergeHandlerDuplicates(entries) {
220
+ export function mergeHandlerDuplicates(entries) {
125
221
  const result = {};
126
222
  for (const [key, value] of entries) {
127
223
  if (key.endsWith(' =>') && key in result) {
128
- // Duplicate handler key — merge into array
129
224
  const existing = result[key];
130
225
  if (Array.isArray(existing)) {
131
226
  existing.push(value);
@@ -140,14 +235,36 @@ function mergeHandlerDuplicates(entries) {
140
235
  }
141
236
  return result;
142
237
  }
143
- export async function assignFrom(target, pattern, options, permissions) {
144
- // First: expand looped substitution variables (${x}, ${y}, ${z})
145
- const expandedPattern = expandSubstitutions(pattern, options);
146
- // Separate handler commands ( =>), #[x] keys, and normal keys
238
+ /**
239
+ * Recursively walk an object and handle "..." spread keys.
240
+ */
241
+ export function handleSpreads(obj) {
242
+ for (const [key, value] of Object.entries(obj)) {
243
+ if (key !== '...' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
244
+ const proto = Object.getPrototypeOf(value);
245
+ if (proto === Object.prototype || proto === null) {
246
+ obj[key] = handleSpreads(value);
247
+ }
248
+ }
249
+ }
250
+ if ('...' in obj) {
251
+ const spreadValue = obj['...'];
252
+ delete obj['...'];
253
+ if (spreadValue && typeof spreadValue === 'object') {
254
+ Object.assign(obj, spreadValue);
255
+ }
256
+ }
257
+ return obj;
258
+ }
259
+ /**
260
+ * Categorize pattern keys into handler keys, #[x] keys, and normal keys.
261
+ */
262
+ export function categorizeKeys(expandedPattern) {
147
263
  const handlerKeys = [];
148
264
  const normalPattern = {};
149
265
  const idRefNormalKeys = [];
150
266
  const idRefHandlerKeys = [];
267
+ const ternaryKeys = [];
151
268
  for (const key of Object.keys(expandedPattern)) {
152
269
  if (isHandlerCommand(key)) {
153
270
  if (key.startsWith('#[')) {
@@ -157,6 +274,9 @@ export async function assignFrom(target, pattern, options, permissions) {
157
274
  handlerKeys.push(key);
158
275
  }
159
276
  }
277
+ else if (isTernaryCommand(key)) {
278
+ ternaryKeys.push(key);
279
+ }
160
280
  else if (key.startsWith('#[')) {
161
281
  idRefNormalKeys.push(key);
162
282
  }
@@ -164,113 +284,166 @@ export async function assignFrom(target, pattern, options, permissions) {
164
284
  normalPattern[key] = expandedPattern[key];
165
285
  }
166
286
  }
167
- // Process normal keys via resolveValues + assignGingerly
287
+ return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
288
+ }
289
+ /**
290
+ * Merge withIds and at into a single lookup map for resolveIdVariable.
291
+ */
292
+ function getEffectiveIds(options) {
293
+ if (!options.withIds && !options.at)
294
+ return undefined;
295
+ if (options.withIds && !options.at)
296
+ return options.withIds;
297
+ if (!options.withIds && options.at)
298
+ return options.at;
299
+ return { ...options.withIds, ...options.at };
300
+ }
301
+ /**
302
+ * Process #[x] normal keys synchronously.
303
+ */
304
+ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, options) {
305
+ const ids = getEffectiveIds(options);
306
+ if (!ids)
307
+ return;
308
+ for (const key of idRefNormalKeys) {
309
+ const parsed = parseIdRef(key);
310
+ if (!parsed)
311
+ continue;
312
+ const el = resolveIdVariable(parsed.varName, target, ids);
313
+ if (!el)
314
+ continue;
315
+ const value = expandedPattern[key];
316
+ if (parsed.remainingPath) {
317
+ const resolvedValue = getValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
318
+ assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
319
+ }
320
+ else {
321
+ const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
322
+ if (!('__v' in resolvedValue)) {
323
+ assignGingerly(el, resolvedValue, options);
324
+ }
325
+ }
326
+ }
327
+ }
328
+ /**
329
+ * Synchronous assignFrom — resolves values, assigns to target, all without awaiting.
330
+ *
331
+ * Handler commands (` =>`), beVigilant, and enhance are fire-and-forget (async, non-blocking).
332
+ * For awaitable handler execution, use assignFromAsync.
333
+ *
334
+ * @param target - Object to merge resolved values into
335
+ * @param pattern - Object whose RHS values may contain `?.` path strings
336
+ * @param options - Options including `from` (source object)
337
+ * @param permissions - Optional security permissions
338
+ * @returns The target object after merging
339
+ */
340
+ export function assignFrom(target, pattern, options, permissions) {
341
+ // Expand looped substitution variables
342
+ const expandedPattern = expandSubstitutions(pattern, options);
343
+ // Categorize keys
344
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
345
+ // Process ?= ternary keys (sync)
346
+ if (ternaryKeys.length > 0) {
347
+ const ternaryResolved = {};
348
+ for (const key of ternaryKeys) {
349
+ const lhsPath = parseTernaryCommand(key);
350
+ if (!lhsPath)
351
+ continue;
352
+ const arr = expandedPattern[key];
353
+ if (!Array.isArray(arr) || arr.length < 2)
354
+ continue;
355
+ const result = evaluateTernary(arr, options.from, options);
356
+ if (result !== TERNARY_SKIP) {
357
+ ternaryResolved[lhsPath] = result;
358
+ }
359
+ }
360
+ if (Object.keys(ternaryResolved).length > 0) {
361
+ assignGingerly(target, ternaryResolved, options);
362
+ }
363
+ }
364
+ // Process normal keys via getValues (sync) + assignGingerly
168
365
  if (Object.keys(normalPattern).length > 0) {
169
- const resolved = await resolveValues(normalPattern, options.from, {
366
+ // Resolve #[x] references on RHS values before getValues
367
+ if (options.withIds || options.at) {
368
+ const ids = getEffectiveIds(options);
369
+ for (const key of Object.keys(normalPattern)) {
370
+ const value = normalPattern[key];
371
+ if (typeof value === 'string' && value.startsWith('#[')) {
372
+ const closeIdx = value.indexOf(']');
373
+ if (closeIdx !== -1) {
374
+ const varName = value.substring(2, closeIdx);
375
+ const el = resolveIdVariable(varName, target, ids);
376
+ if (el) {
377
+ const remainingPath = value.substring(closeIdx + 1);
378
+ if (remainingPath) {
379
+ normalPattern[key] = getValue(remainingPath, el, {
380
+ withMethods: options.withMethods,
381
+ aka: options.aka,
382
+ protocols: options.protocols
383
+ });
384
+ }
385
+ else {
386
+ normalPattern[key] = el.id; // bare #[x] → ID string
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ }
393
+ const resolved = getValues(normalPattern, options.from, {
170
394
  withMethods: options.withMethods,
171
395
  aka: options.aka,
172
396
  protocols: options.protocols
173
397
  });
174
- // Recursively handle "..." spread keys at all nesting levels
175
398
  handleSpreads(resolved);
176
399
  assignGingerly(target, resolved, options);
177
400
  }
178
- // Process #[x] normal keys — resolve element, then apply remaining path + value
179
- if (idRefNormalKeys.length > 0 && options.withIds) {
180
- const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
181
- for (const key of idRefNormalKeys) {
182
- const parsed = parseIdRef(key);
183
- if (!parsed)
184
- continue;
185
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
186
- if (!el)
187
- continue;
188
- const value = expandedPattern[key];
189
- if (parsed.remainingPath) {
190
- // Resolve the RHS value
191
- const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
192
- // Apply remaining path on the resolved element
193
- assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
194
- }
195
- else {
196
- // No remaining path — resolve and assign directly to the element
197
- const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
198
- if ('__v' in resolvedValue) {
199
- // Single value — can't assign to element root without a path
200
- }
201
- else {
202
- assignGingerly(el, resolvedValue, options);
203
- }
204
- }
205
- }
401
+ // Process #[x] normal keys (sync)
402
+ if (idRefNormalKeys.length > 0) {
403
+ processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, options);
206
404
  }
207
- // Process handler commands ( =>) — dynamically imported only when needed
405
+ // Process handler commands — fire-and-forget (async)
208
406
  if (handlerKeys.length > 0) {
209
- const { processHandlerCommands } = await import('./processHandlerCommands.js');
210
- await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
407
+ import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
408
+ processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
409
+ });
211
410
  }
212
- // Process #[x] handler keys — resolve element, then pass to handler processing
213
- if (idRefHandlerKeys.length > 0 && options.withIds) {
214
- const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
215
- const { processHandlerCommands } = await import('./processHandlerCommands.js');
216
- for (const key of idRefHandlerKeys) {
217
- const parsed = parseIdRef(key);
218
- if (!parsed)
219
- continue;
220
- const el = resolveIdVariable(parsed.varName, target, options.withIds);
221
- if (!el)
222
- continue;
223
- // Build a synthetic key for processHandlerCommands:
224
- // The resolved element becomes the target, remaining path is the LHS
225
- const syntheticKey = parsed.remainingPath
226
- ? `${parsed.remainingPath} =>`
227
- : ' =>';
228
- const syntheticPattern = {
229
- [syntheticKey]: expandedPattern[key]
230
- };
231
- await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
232
- }
411
+ // Process #[x] handler keys — fire-and-forget (async)
412
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
413
+ const ids = getEffectiveIds(options);
414
+ import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
415
+ for (const key of idRefHandlerKeys) {
416
+ const parsed = parseIdRef(key);
417
+ if (!parsed)
418
+ continue;
419
+ const el = resolveIdVariable(parsed.varName, target, ids);
420
+ if (!el)
421
+ continue;
422
+ const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
423
+ const syntheticPattern = { [syntheticKey]: expandedPattern[key] };
424
+ processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
425
+ }
426
+ });
233
427
  }
234
- // Process inferred assignments — dynamically imported only when option is present
235
- if (options.inferredAssignments) {
236
- const { processInferredAssignments } = await import('./inferredAssignments.js');
237
- await processInferredAssignments(target, options.from, options.inferredAssignments);
238
- // Set up MutationObserver for new matching elements if beVigilant
239
- if (options.inferredAssignments.beVigilant) {
428
+ // Process inferred assignments (sync)
429
+ if (options.infer) {
430
+ processInferredAssignments(target, options.from, options.infer);
431
+ // beVigilant — fire-and-forget (async)
432
+ if (options.infer.beVigilant) {
240
433
  if (!options.signal) {
241
- throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
434
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
242
435
  }
243
- const { setupVigilantObserver } = await import('./beVigilant.js');
244
- setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
436
+ import('./beVigilant.js').then(({ setupVigilantObserver }) => {
437
+ setupVigilantObserver(target, options.from, options.infer, options.signal);
438
+ });
245
439
  }
246
440
  }
247
- // Process bulk enhancements — dynamically imported only when option is present
441
+ // Process bulk enhancements — fire-and-forget (async)
248
442
  if (options.enhance && options.enhance.length > 0) {
249
- const { enhanceAll } = await import('./enhanceAll.js');
250
- await enhanceAll(target, options.enhance, permissions);
443
+ import('./enhanceAll.js').then(({ enhanceAll }) => {
444
+ enhanceAll(target, options.enhance, permissions);
445
+ });
251
446
  }
252
447
  return target;
253
448
  }
254
- /**
255
- * Recursively walk an object and handle "..." spread keys.
256
- * When a "..." key is found, its value (which should be an object after protocol resolution)
257
- * is spread into the parent, replacing the "..." entry.
258
- */
259
- function handleSpreads(obj) {
260
- for (const [key, value] of Object.entries(obj)) {
261
- if (key !== '...' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
262
- const proto = Object.getPrototypeOf(value);
263
- if (proto === Object.prototype || proto === null) {
264
- obj[key] = handleSpreads(value);
265
- }
266
- }
267
- }
268
- if ('...' in obj) {
269
- const spreadValue = obj['...'];
270
- delete obj['...'];
271
- if (spreadValue && typeof spreadValue === 'object') {
272
- Object.assign(obj, spreadValue);
273
- }
274
- }
275
- return obj;
276
- }
449
+ export default assignFrom;