assign-gingerly 0.0.54 → 0.0.55

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 (110) hide show
  1. package/README.md +143 -2
  2. package/assignFrom.js +37 -18
  3. package/assignFrom.ts +62 -3
  4. package/assignGingerly.js +34 -1
  5. package/assignGingerly.ts +54 -1
  6. package/beVigilant.js +73 -0
  7. package/beVigilant.ts +85 -0
  8. package/enhanceAll.js +106 -0
  9. package/enhanceAll.ts +138 -0
  10. package/handlers/join.js +74 -0
  11. package/handlers/join.ts +80 -0
  12. package/handlers/lazyLoad.js +212 -0
  13. package/handlers/lazyLoad.ts +307 -0
  14. package/handlers/lazyLoadSwitch.js +58 -0
  15. package/handlers/lazyLoadSwitch.ts +63 -0
  16. package/handlers/microDataJoin.js +184 -0
  17. package/handlers/microDataJoin.ts +270 -0
  18. package/inferencer/.gitmodules +3 -0
  19. package/inferencer/.vscode/settings.json +2 -0
  20. package/inferencer/InferencedPropagator.js +230 -0
  21. package/inferencer/InferencedPropagator.ts +269 -0
  22. package/inferencer/LICENSE +21 -0
  23. package/inferencer/README.md +524 -0
  24. package/inferencer/Requirements/SupportForPropagator.md +368 -0
  25. package/inferencer/imports.html +7 -0
  26. package/inferencer/inferencer.js +254 -0
  27. package/inferencer/inferencer.ts +292 -0
  28. package/inferencer/package-lock.json +129 -0
  29. package/inferencer/package.json +60 -0
  30. package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
  31. package/inferencer/playwright-report/index.html +90 -0
  32. package/inferencer/playwright.config.ts +54 -0
  33. package/inferencer/test-results/.last-run.json +6 -0
  34. package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
  35. package/inferencer/tests/inferencedPropagator.html +428 -0
  36. package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
  37. package/inferencer/tests/inferencer.html +355 -0
  38. package/inferencer/tests/inferencer.spec.ts +19 -0
  39. package/inferencer/tsconfig.json +19 -0
  40. package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
  41. package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
  42. package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
  43. package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
  44. package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
  45. package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
  46. package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
  47. package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
  48. package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
  49. package/inferencer/types/LICENSE +21 -0
  50. package/inferencer/types/NewCustomElement.md +388 -0
  51. package/inferencer/types/NewCustomElementFeature.md +683 -0
  52. package/inferencer/types/NewEnhancementInstructions.md +705 -0
  53. package/inferencer/types/README.md +2 -0
  54. package/inferencer/types/agrace/types.d.ts +11 -0
  55. package/inferencer/types/assign-gingerly/types.d.ts +572 -0
  56. package/inferencer/types/be-a-beacon/types.d.ts +17 -0
  57. package/inferencer/types/be-bound/types.d.ts +66 -0
  58. package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
  59. package/inferencer/types/be-calculating/types.d.ts +54 -0
  60. package/inferencer/types/be-clonable/types.d.ts +38 -0
  61. package/inferencer/types/be-committed/types.d.ts +22 -0
  62. package/inferencer/types/be-consoling/types.d.ts +24 -0
  63. package/inferencer/types/be-decked-with/types.d.ts +26 -0
  64. package/inferencer/types/be-delible/types.d.ts +27 -0
  65. package/inferencer/types/be-dispatching/types.d.ts +34 -0
  66. package/inferencer/types/be-evanescent/types.d.ts +20 -0
  67. package/inferencer/types/be-flashy/types.d.ts +21 -0
  68. package/inferencer/types/be-gone/types.d.ts +25 -0
  69. package/inferencer/types/be-observing/types.d.ts +55 -0
  70. package/inferencer/types/be-reflective/types.d.ts +78 -0
  71. package/inferencer/types/be-reformable/types.d.ts +49 -0
  72. package/inferencer/types/be-render-neutral/types.d.ts +32 -0
  73. package/inferencer/types/be-switched/types.d.ts +146 -0
  74. package/inferencer/types/be-typed/types.d.ts +32 -0
  75. package/inferencer/types/be-valued/types.d.ts +22 -0
  76. package/inferencer/types/data-props/types.d.ts +34 -0
  77. package/inferencer/types/do-inc/types.d.ts +56 -0
  78. package/inferencer/types/do-invoke/types.d.ts +38 -0
  79. package/inferencer/types/do-merge/types.d.ts +28 -0
  80. package/inferencer/types/do-toggle/types.d.ts +31 -0
  81. package/inferencer/types/face-up/types.d.ts +100 -0
  82. package/inferencer/types/fetch-for/types.d.ts +36 -0
  83. package/inferencer/types/folder-picker/types.d.ts +21 -0
  84. package/inferencer/types/global.d.ts +29 -0
  85. package/inferencer/types/id-generation/types.d.ts +26 -0
  86. package/inferencer/types/inferencer/types.d.ts +46 -0
  87. package/inferencer/types/mount-observer/types.d.ts +363 -0
  88. package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
  89. package/inferencer/types/pipe-in/types.d.ts +52 -0
  90. package/inferencer/types/roundabout/types.d.ts +268 -0
  91. package/inferencer/types/soak-up/types.d.ts +40 -0
  92. package/inferencer/types/templ-maker/types.d.ts +43 -0
  93. package/inferencer/types/time-ticker/types.d.ts +62 -0
  94. package/inferencer/types/truth-sourcer/types.d.ts +44 -0
  95. package/inferencer/upSearch.js +27 -0
  96. package/inferencer/upSearch.ts +26 -0
  97. package/inferencer/withScopePerimeter.js +27 -0
  98. package/inferencer/withScopePerimeter.ts +33 -0
  99. package/inferredAssignments.js +38 -0
  100. package/inferredAssignments.ts +65 -0
  101. package/isAllowedImportPath.js +42 -0
  102. package/isAllowedImportPath.ts +53 -0
  103. package/package.json +27 -1
  104. package/paths.js +49 -1
  105. package/paths.ts +80 -1
  106. package/processHandlerCommands.js +19 -19
  107. package/processHandlerCommands.ts +10 -13
  108. package/resolveIdRef.js +144 -125
  109. package/resolveIdRef.ts +30 -0
  110. package/types/assign-gingerly/types.d.ts +12 -0
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## Introduction
11
11
 
12
- This package starts out innocently enough -- it provides three utility functions for carefully merging one object into another. This is a primitive sorely lacking in the web, and this package is a polyfill for what we (me with a lot of help from AI) would like to see built into the platform. We make no apologies about adding these features directly to the underlying API's, as it is part of a proposal which is sitting there gathering dust, with no apparent alternatives under consideration. In particular the reference:
12
+ This package starts out innocently enough -- it provides the ability to carefully merge one object into another. This is a primitive sorely lacking in the web, and this package is a polyfill for what we (me with a lot of help from AI) would like to see built into the platform. We make no apologies about adding these features directly to the underlying API's, as it is part of a proposal which is sitting there gathering dust, with no apparent alternatives under consideration. In particular the reference:
13
13
 
14
14
  ```JavaScript
15
15
  import 'assign-gingerly/object-extension.js';
@@ -1131,7 +1131,7 @@ const result = assignGingerly({}, {
1131
1131
  "[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
1132
1132
  style: {
1133
1133
  height: '40px'
1134
- }
1134
+ },
1135
1135
  enh: {
1136
1136
  mellowYellow?.madAboutFourteen': true
1137
1137
  }
@@ -4126,6 +4126,104 @@ The `#[x]` + `withIds` pattern gives you the Map+WeakRef speed tier automaticall
4126
4126
  - **Scoped to rootNode** — works correctly inside Shadow DOM (IDs are scoped per shadow root).
4127
4127
  - **`assignFrom` only** — this feature is not available in `assignGingerly` or `assignTentatively`.
4128
4128
 
4129
+ ## Inferred Assignments
4130
+
4131
+ `assignFrom` can automatically distribute source values to matching DOM elements based on structural conventions — no manual path strings needed. The [inferencer](./inferencer/) submodule determines the correct property for each element type.
4132
+
4133
+ ```TypeScript
4134
+ const vm = {
4135
+ name: 'Alice',
4136
+ email: 'alice@example.com',
4137
+ user: { role: 'admin', avatar: '/img/alice.png' }
4138
+ };
4139
+
4140
+ await assignFrom(outerDiv, {}, {
4141
+ from: vm,
4142
+ inferredAssignments: {
4143
+ byItemprop: ['name', 'email', 'user']
4144
+ }
4145
+ });
4146
+ ```
4147
+
4148
+ For each key in `byItemprop`, this finds `[itemprop="${key}"]` elements within the target (respecting itemscope boundaries) and sets the value using the inferred property:
4149
+
4150
+ | Element | Inferred Property |
4151
+ |---------|------------------|
4152
+ | `<span itemprop="name">` | `textContent` |
4153
+ | `<input itemprop="email">` | `value` |
4154
+ | `<time itemprop="date">` | `dateTime` |
4155
+ | `<data itemprop="count">` | `value` |
4156
+ | `<div itemprop="user" itemscope="user-card">` | `ish` (manager routing) |
4157
+
4158
+ **Scope perimeter:** Queries respect itemscope boundaries — `[itemprop="name"]` inside a nested `[itemscope]` won't be matched from the outer scope (donut-hole scoping).
4159
+
4160
+ **Pass `true` to infer all source keys:**
4161
+
4162
+ ```TypeScript
4163
+ inferredAssignments: { byItemprop: true }
4164
+ ```
4165
+
4166
+ For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.md).
4167
+
4168
+ ## Bulk Enhancement with `enhance` and `enhanceAll`
4169
+
4170
+ Apply enhancements in bulk to matching elements using EMC (Element Mount Configuration) JSON files that enhancement packages already publish. No manual registration step needed — enhancements are auto-loaded and registered on demand.
4171
+
4172
+ ```TypeScript
4173
+ import { assignFrom } from 'assign-gingerly/assignFrom.js';
4174
+
4175
+ await assignFrom(shadowRoot, { /* normal assignments */ }, {
4176
+ from: vm,
4177
+ enhance: [
4178
+ { emc: 'be-bound/emc.json', matching: '[name]' },
4179
+ { emc: 'be-observant/emc.json', matching: '[itemprop]' },
4180
+ ]
4181
+ });
4182
+ ```
4183
+
4184
+ **How it works:**
4185
+
4186
+ 1. Dynamically imports each EMC JSON file
4187
+ 2. Extracts the `enhConfig` (spawn path, enhKey)
4188
+ 3. Finds matching elements via `querySelectorAll` (using `matching` override, or the EMC's own selector)
4189
+ 4. Registers the enhancement in the element's `enhancementRegistry` if not already present
4190
+ 5. Spawns the enhancement on each matched element via `element.enh.get(registryItem)`
4191
+
4192
+ **Standalone usage (without `assignFrom`):**
4193
+
4194
+ ```TypeScript
4195
+ import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
4196
+
4197
+ await enhanceAll(shadowRoot, [
4198
+ { emc: 'be-bound/emc.json', matching: '[name]' },
4199
+ ]);
4200
+ ```
4201
+
4202
+ **With `assignGingerly` (fire-and-forget):**
4203
+
4204
+ ```TypeScript
4205
+ assignGingerly(shadowRoot, {
4206
+ '?.someProperty': 'value'
4207
+ }, {
4208
+ enhance: [{ emc: 'be-bound/emc.json', matching: '[name]' }]
4209
+ });
4210
+ // Enhancements apply asynchronously in the background
4211
+ ```
4212
+
4213
+ **Configuration:**
4214
+
4215
+ | Field | Type | Description |
4216
+ |-------|------|-------------|
4217
+ | `emc` | `string` | Path to EMC JSON file (dynamically imported) |
4218
+ | `matching` | `string?` | CSS selector override (defaults to EMC's own selector) |
4219
+ | `parse` | `boolean?` | Parse attributes via withAttrs (default: `false`, Phase II) |
4220
+
4221
+ **Notes:**
4222
+
4223
+ - No scope perimeter is applied — the selector does all filtering. For reactive observation of new elements, use [mount-observer](https://github.com/bahrus/mount-observer) instead.
4224
+ - Requires JSON module import support (`import ... with { type: 'json' }`).
4225
+ - Enhancement modules are loaded lazily — zero cost until `enhance` is used.
4226
+
4129
4227
  ## Custom Assignment with `static assignTo` Protocol
4130
4228
 
4131
4229
  Classes can opt into custom assignment behavior by defining a `static assignTo` method. When `assignGingerly` encounters a property whose current value is an instance of such a class, it delegates the assignment to `assignTo` instead of performing the default merge/replace logic.
@@ -4788,6 +4886,33 @@ This is useful for:
4788
4886
  4. On first access, the getter spawns the feature instance, validates it (optionally), caches it, and returns it.
4789
4887
  5. Because the property is getter-only (no setter), `assignGingerly` automatically merges into the spawned instance when assigning object values to that property.
4790
4888
 
4889
+ ### Features at Registration Time
4890
+
4891
+ Features can also be bundled directly into enhancement and itemscope manager registration configs — no separate `assignFeatures` call needed:
4892
+
4893
+ ```TypeScript
4894
+ // Enhancement with features
4895
+ enhancementRegistry.push({
4896
+ spawn: MyEnhancement,
4897
+ enhKey: 'myEnh',
4898
+ features: {
4899
+ photoTaker: { spawn: PhotoTakerImpl },
4900
+ badgeMaker: { spawn: BadgeMakerImpl },
4901
+ }
4902
+ });
4903
+
4904
+ // Itemscope manager with features
4905
+ customElements.itemscopeRegistry.define('my-manager', {
4906
+ manager: MyManagerClass,
4907
+ lifecycleKeys: { dispose: 'cleanup', resolved: 'isReady' },
4908
+ features: {
4909
+ roundabout: { customData: { /* ... */ } },
4910
+ }
4911
+ });
4912
+ ```
4913
+
4914
+ This calls `assignFeatures` automatically under the hood using the shared `customElements.featuresRegistry`. The standalone `assignFeatures` call remains available for cases where you need to associate features with a class you don't own or after initial registration.
4915
+
4791
4916
  ### Basic example
4792
4917
 
4793
4918
  ```JavaScript
@@ -5660,3 +5785,19 @@ See [time-ticker/wireFeatures.js](https://github.com/bahrus/time-ticker/blob/bas
5660
5785
 
5661
5786
  - **Nested features**: Support `?.path?.notation` keys directly in `assignFeatures` (without requiring `PropertyBag`).
5662
5787
  - **`@each` + async interaction**: Combine async methods with iteration.
5788
+
5789
+
5790
+ ## Viewing Locally
5791
+
5792
+ Any web server that serves static files with server-side includes will do but...
5793
+
5794
+ 1. Install git
5795
+ 2. Fork/clone this repo
5796
+ 3. Install node.js
5797
+ 4. Open command window to folder where you cloned this repo
5798
+ 5. > git submodule add https://github.com/bahrus/types.git types
5799
+ 6. > git submodule add https://github.com/bahrus/inferencer.git inferencer
5800
+ 7. > git submodule update --init --recursive
5801
+ 8. > npm install
5802
+ 9. > npm run serve
5803
+ 10. Open http://localhost:8000/demo/ in a modern browser
package/assignFrom.js CHANGED
@@ -140,7 +140,7 @@ function mergeHandlerDuplicates(entries) {
140
140
  }
141
141
  return result;
142
142
  }
143
- export async function assignFrom(target, pattern, options) {
143
+ export async function assignFrom(target, pattern, options, permissions) {
144
144
  // First: expand looped substitution variables (${x}, ${y}, ${z})
145
145
  const expandedPattern = expandSubstitutions(pattern, options);
146
146
  // Separate handler commands ( =>), #[x] keys, and normal keys
@@ -180,27 +180,25 @@ export async function assignFrom(target, pattern, options) {
180
180
  const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
181
181
  for (const key of idRefNormalKeys) {
182
182
  const parsed = parseIdRef(key);
183
- if (!parsed) continue;
183
+ if (!parsed)
184
+ continue;
184
185
  const el = resolveIdVariable(parsed.varName, target, options.withIds);
185
- if (!el) continue;
186
+ if (!el)
187
+ continue;
186
188
  const value = expandedPattern[key];
187
189
  if (parsed.remainingPath) {
188
190
  // Resolve the RHS value
189
- const resolvedValue = await resolveValues(
190
- { __v: value }, options.from,
191
- { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
192
- );
191
+ const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
193
192
  // Apply remaining path on the resolved element
194
193
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
195
194
  }
196
195
  else {
197
196
  // No remaining path — resolve and assign directly to the element
198
- const resolvedValue = await resolveValues(
199
- typeof value === 'object' && value !== null ? value : { __v: value },
200
- options.from,
201
- { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
202
- );
203
- if (!('__v' in resolvedValue)) {
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 {
204
202
  assignGingerly(el, resolvedValue, options);
205
203
  }
206
204
  }
@@ -209,7 +207,7 @@ export async function assignFrom(target, pattern, options) {
209
207
  // Process handler commands ( =>) — dynamically imported only when needed
210
208
  if (handlerKeys.length > 0) {
211
209
  const { processHandlerCommands } = await import('./processHandlerCommands.js');
212
- await processHandlerCommands(target, handlerKeys, expandedPattern, options);
210
+ await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
213
211
  }
214
212
  // Process #[x] handler keys — resolve element, then pass to handler processing
215
213
  if (idRefHandlerKeys.length > 0 && options.withIds) {
@@ -217,19 +215,40 @@ export async function assignFrom(target, pattern, options) {
217
215
  const { processHandlerCommands } = await import('./processHandlerCommands.js');
218
216
  for (const key of idRefHandlerKeys) {
219
217
  const parsed = parseIdRef(key);
220
- if (!parsed) continue;
218
+ if (!parsed)
219
+ continue;
221
220
  const el = resolveIdVariable(parsed.varName, target, options.withIds);
222
- if (!el) continue;
223
- // Build a synthetic key for processHandlerCommands
221
+ if (!el)
222
+ continue;
223
+ // Build a synthetic key for processHandlerCommands:
224
+ // The resolved element becomes the target, remaining path is the LHS
224
225
  const syntheticKey = parsed.remainingPath
225
226
  ? `${parsed.remainingPath} =>`
226
227
  : ' =>';
227
228
  const syntheticPattern = {
228
229
  [syntheticKey]: expandedPattern[key]
229
230
  };
230
- await processHandlerCommands(el, [syntheticKey], syntheticPattern, options);
231
+ await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
231
232
  }
232
233
  }
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) {
240
+ if (!options.signal) {
241
+ throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
242
+ }
243
+ const { setupVigilantObserver } = await import('./beVigilant.js');
244
+ setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
245
+ }
246
+ }
247
+ // Process bulk enhancements — dynamically imported only when option is present
248
+ if (options.enhance && options.enhance.length > 0) {
249
+ const { enhanceAll } = await import('./enhanceAll.js');
250
+ await enhanceAll(target, options.enhance, permissions);
251
+ }
233
252
  return target;
234
253
  }
235
254
  /**
package/assignFrom.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { resolveValues, ResolveValuesOptions } from './resolveValues.js';
23
23
  import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
24
+ import type { AssignPermissions } from './isAllowedImportPath.js';
24
25
 
25
26
  export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValuesOptions {
26
27
  /** Source object to resolve RHS path strings against */
@@ -63,6 +64,42 @@ export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValues
63
64
  * }
64
65
  */
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 }>;
66
103
  }
67
104
 
68
105
  /**
@@ -205,7 +242,8 @@ function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
205
242
  export async function assignFrom(
206
243
  target: any,
207
244
  pattern: Record<string, any>,
208
- options: AssignFromOptions
245
+ options: AssignFromOptions,
246
+ permissions?: AssignPermissions
209
247
  ): Promise<any> {
210
248
  // First: expand looped substitution variables (${x}, ${y}, ${z})
211
249
  const expandedPattern = expandSubstitutions(pattern, options);
@@ -282,7 +320,7 @@ export async function assignFrom(
282
320
  // Process handler commands ( =>) — dynamically imported only when needed
283
321
  if (handlerKeys.length > 0) {
284
322
  const { processHandlerCommands } = await import('./processHandlerCommands.js');
285
- await processHandlerCommands(target, handlerKeys, expandedPattern, options);
323
+ await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
286
324
  }
287
325
 
288
326
  // Process #[x] handler keys — resolve element, then pass to handler processing
@@ -307,10 +345,31 @@ export async function assignFrom(
307
345
  [syntheticKey]: expandedPattern[key]
308
346
  };
309
347
 
310
- await processHandlerCommands(el, [syntheticKey], syntheticPattern, options);
348
+ await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
311
349
  }
312
350
  }
313
351
 
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);
356
+
357
+ // Set up MutationObserver for new matching elements if beVigilant
358
+ if (options.inferredAssignments.beVigilant) {
359
+ if (!options.signal) {
360
+ throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
361
+ }
362
+ const { setupVigilantObserver } = await import('./beVigilant.js');
363
+ setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
364
+ }
365
+ }
366
+
367
+ // Process bulk enhancements — dynamically imported only when option is present
368
+ if (options.enhance && options.enhance.length > 0) {
369
+ const { enhanceAll } = await import('./enhanceAll.js');
370
+ await enhanceAll(target, options.enhance, permissions);
371
+ }
372
+
314
373
  return target;
315
374
  }
316
375
 
package/assignGingerly.js CHANGED
@@ -42,6 +42,21 @@ export class EnhancementRegistry extends EventTarget {
42
42
  }
43
43
  // Dispatch event after adding items
44
44
  this.dispatchEvent(new EnhancementRegisteredEvent(items));
45
+ // Process features if present (fire-and-forget, uses shared featuresRegistry)
46
+ const itemsArr = Array.isArray(items) ? items : [items];
47
+ for (const item of itemsArr) {
48
+ if (item.features) {
49
+ this.#assignFeatures(item.spawn, item.features);
50
+ }
51
+ }
52
+ }
53
+ async #assignFeatures(spawn, features) {
54
+ const { assignFeatures } = await import('./assignFeatures.js');
55
+ const featuresRegistry = this._featuresRegistry
56
+ ?? (typeof customElements !== 'undefined' ? customElements.featuresRegistry : undefined);
57
+ if (featuresRegistry) {
58
+ assignFeatures(spawn, features, featuresRegistry);
59
+ }
45
60
  }
46
61
  getItems() {
47
62
  return Array.from(this.#items);
@@ -89,6 +104,18 @@ export class ItemscopeRegistry extends EventTarget {
89
104
  }
90
105
  this.#configs.set(name, config);
91
106
  this.dispatchEvent(new Event(name));
107
+ // Process features if present (fire-and-forget, uses shared featuresRegistry)
108
+ if (config.features) {
109
+ this.#assignFeatures(config.manager, config.features);
110
+ }
111
+ }
112
+ async #assignFeatures(manager, features) {
113
+ const { assignFeatures } = await import('./assignFeatures.js');
114
+ const featuresRegistry = this._featuresRegistry
115
+ ?? (typeof customElements !== 'undefined' ? customElements.featuresRegistry : undefined);
116
+ if (featuresRegistry) {
117
+ assignFeatures(manager, features, featuresRegistry);
118
+ }
92
119
  }
93
120
  /**
94
121
  * Get a manager configuration by name
@@ -515,7 +542,7 @@ function applyAliases(key, aliasMap) {
515
542
  /**
516
543
  * Main assignGingerly function
517
544
  */
518
- export function assignGingerly(target, source, options) {
545
+ export function assignGingerly(target, source, options, permissions) {
519
546
  if (!target || typeof target !== 'object') {
520
547
  return target;
521
548
  }
@@ -1059,6 +1086,12 @@ export function assignGingerly(target, source, options) {
1059
1086
  configurable: true,
1060
1087
  });
1061
1088
  }
1089
+ // Fire-and-forget bulk enhancements (async, non-blocking)
1090
+ if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
1091
+ import('./enhanceAll.js').then(({ enhanceAll }) => {
1092
+ enhanceAll(target, options.enhance, permissions);
1093
+ });
1094
+ }
1062
1095
  return target;
1063
1096
  }
1064
1097
  export default assignGingerly;
package/assignGingerly.ts CHANGED
@@ -1,6 +1,8 @@
1
1
 
2
2
 
3
3
  import { EnhancementConfig } from "./types/assign-gingerly/types";
4
+ import type { FeatureConfigsMap } from "./types/assign-gingerly/types";
5
+ import type { AssignPermissions } from "./isAllowedImportPath.js";
4
6
 
5
7
  /**
6
8
  * Constructor signature for ItemScope Manager classes
@@ -116,6 +118,18 @@ export interface IAssignGingerlyOptions {
116
118
  * // Calls: (await el.whenFeatureReady('photoTaker')).someProp = 'hello'
117
119
  */
118
120
  withAsyncMethods?: string[] | Set<string>;
121
+
122
+ /**
123
+ * Bulk enhancement application via EMC JSON configs.
124
+ * Finds matching elements and spawns enhancements on them.
125
+ * Fire-and-forget (async) — assignGingerly remains synchronous.
126
+ *
127
+ * @example
128
+ * enhance: [
129
+ * { emc: 'be-bound/emc.json', matching: '[name]' },
130
+ * ]
131
+ */
132
+ enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
119
133
  }
120
134
 
121
135
  /**
@@ -167,6 +181,23 @@ export class EnhancementRegistry extends EventTarget {
167
181
 
168
182
  // Dispatch event after adding items
169
183
  this.dispatchEvent(new EnhancementRegisteredEvent(items));
184
+
185
+ // Process features if present (fire-and-forget, uses shared featuresRegistry)
186
+ const itemsArr = Array.isArray(items) ? items : [items];
187
+ for (const item of itemsArr) {
188
+ if (item.features) {
189
+ this.#assignFeatures(item.spawn, item.features);
190
+ }
191
+ }
192
+ }
193
+
194
+ async #assignFeatures(spawn: any, features: FeatureConfigsMap): Promise<void> {
195
+ const { assignFeatures } = await import('./assignFeatures.js');
196
+ const featuresRegistry = (this as any)._featuresRegistry
197
+ ?? (typeof customElements !== 'undefined' ? (customElements as any).featuresRegistry : undefined);
198
+ if (featuresRegistry) {
199
+ assignFeatures(spawn, features, featuresRegistry);
200
+ }
170
201
  }
171
202
 
172
203
  getItems(): EnhancementConfig[] {
@@ -218,6 +249,20 @@ export class ItemscopeRegistry extends EventTarget {
218
249
  }
219
250
  this.#configs.set(name, config);
220
251
  this.dispatchEvent(new Event(name));
252
+
253
+ // Process features if present (fire-and-forget, uses shared featuresRegistry)
254
+ if ((config as any).features) {
255
+ this.#assignFeatures(config.manager, (config as any).features);
256
+ }
257
+ }
258
+
259
+ async #assignFeatures(manager: any, features: FeatureConfigsMap): Promise<void> {
260
+ const { assignFeatures } = await import('./assignFeatures.js');
261
+ const featuresRegistry = (this as any)._featuresRegistry
262
+ ?? (typeof customElements !== 'undefined' ? (customElements as any).featuresRegistry : undefined);
263
+ if (featuresRegistry) {
264
+ assignFeatures(manager, features, featuresRegistry);
265
+ }
221
266
  }
222
267
 
223
268
  /**
@@ -691,7 +736,8 @@ function applyAliases(key: string, aliasMap: Map<string, string>): string {
691
736
  export function assignGingerly(
692
737
  target: any,
693
738
  source: Record<string | symbol, any>,
694
- options?: IAssignGingerlyOptions
739
+ options?: IAssignGingerlyOptions,
740
+ permissions?: AssignPermissions
695
741
  ): any {
696
742
  if (!target || typeof target !== 'object') {
697
743
  return target;
@@ -1274,6 +1320,13 @@ export function assignGingerly(
1274
1320
  });
1275
1321
  }
1276
1322
 
1323
+ // Fire-and-forget bulk enhancements (async, non-blocking)
1324
+ if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
1325
+ import('./enhanceAll.js').then(({ enhanceAll }) => {
1326
+ enhanceAll(target, options!.enhance!, permissions);
1327
+ });
1328
+ }
1329
+
1277
1330
  return target;
1278
1331
  }
1279
1332
 
package/beVigilant.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * beVigilant.ts — MutationObserver-based reactive inference for newly added elements.
3
+ *
4
+ * Dynamically imported when `inferredAssignments.beVigilant: true` is set.
5
+ * Watches for new [itemprop] elements and attribute changes, applying inferred
6
+ * assignments from the live `from` object.
7
+ *
8
+ * Requires an AbortSignal for cleanup (disconnects the observer on abort).
9
+ */
10
+ import { Infer } from './inferencer/inferencer.js';
11
+ import { withScopePerimeter } from './inferencer/withScopePerimeter.js';
12
+ import { registerInCache } from './resolveIdRef.js';
13
+ /**
14
+ * Set up a MutationObserver that watches for new [itemprop] elements
15
+ * and applies inferred assignments from the live `from` object.
16
+ *
17
+ * @param target - The DOM element to observe
18
+ * @param from - The live source object (read at observation time for current values)
19
+ * @param config - The inferredAssignments config (byItemprop keys)
20
+ * @param signal - AbortSignal for cleanup (required)
21
+ */
22
+ export function setupVigilantObserver(target, from, config, signal) {
23
+ const keys = config.byItemprop === true ? null : new Set(config.byItemprop);
24
+ const observer = new MutationObserver((mutations) => {
25
+ for (const mutation of mutations) {
26
+ if (mutation.type === 'childList') {
27
+ // Process added nodes and their descendants
28
+ for (const node of mutation.addedNodes) {
29
+ if (!(node instanceof Element))
30
+ continue;
31
+ // Check the added element itself
32
+ processElement(node, from, keys, target);
33
+ // Check descendants (respecting scope perimeter)
34
+ const nested = node.querySelectorAll('[itemprop]');
35
+ for (const el of nested) {
36
+ processElement(el, from, keys, target);
37
+ }
38
+ }
39
+ }
40
+ else if (mutation.type === 'attributes' && mutation.target instanceof Element) {
41
+ // itemprop attribute was added or changed on an existing element
42
+ processElement(mutation.target, from, keys, target);
43
+ }
44
+ }
45
+ });
46
+ observer.observe(target, {
47
+ childList: true,
48
+ subtree: true,
49
+ attributes: true,
50
+ attributeFilter: ['itemprop']
51
+ });
52
+ // Cleanup on abort
53
+ signal.addEventListener('abort', () => observer.disconnect(), { once: true });
54
+ }
55
+ /**
56
+ * Process a single element: check if it matches, apply inferred value, cache it.
57
+ */
58
+ function processElement(element, from, keys, scopeRoot) {
59
+ const itemprop = element.getAttribute('itemprop');
60
+ if (!itemprop)
61
+ return;
62
+ if (keys && !keys.has(itemprop))
63
+ return;
64
+ if (!(itemprop in from))
65
+ return;
66
+ if (!withScopePerimeter(scopeRoot, element, '[itemscope]'))
67
+ return;
68
+ // Set value via inferencer
69
+ const infer = new Infer(element, itemprop);
70
+ infer.value = from[itemprop];
71
+ // Cache for future fast access
72
+ registerInCache(scopeRoot, itemprop, element);
73
+ }