assign-gingerly 0.0.53 → 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 (114) hide show
  1. package/README.md +847 -5
  2. package/assignFrom.js +229 -9
  3. package/assignFrom.ts +338 -9
  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 +57 -3
  104. package/paths.js +231 -0
  105. package/paths.ts +413 -0
  106. package/processHandlerCommands.js +188 -0
  107. package/processHandlerCommands.ts +217 -0
  108. package/resolveIdRef.js +144 -0
  109. package/resolveIdRef.ts +170 -0
  110. package/resolveValues.js +41 -2
  111. package/resolveValues.ts +41 -1
  112. package/transitionHelper.js +109 -0
  113. package/transitionHelper.ts +132 -0
  114. package/types/assign-gingerly/types.d.ts +89 -0
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
+ }
package/beVigilant.ts ADDED
@@ -0,0 +1,85 @@
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
+
11
+ import { Infer } from './inferencer/inferencer.js';
12
+ import { withScopePerimeter } from './inferencer/withScopePerimeter.js';
13
+ import { registerInCache } from './resolveIdRef.js';
14
+
15
+ /**
16
+ * Set up a MutationObserver that watches for new [itemprop] elements
17
+ * and applies inferred assignments from the live `from` object.
18
+ *
19
+ * @param target - The DOM element to observe
20
+ * @param from - The live source object (read at observation time for current values)
21
+ * @param config - The inferredAssignments config (byItemprop keys)
22
+ * @param signal - AbortSignal for cleanup (required)
23
+ */
24
+ export function setupVigilantObserver(
25
+ target: Element,
26
+ from: any,
27
+ config: { byItemprop?: string[] | true },
28
+ signal: AbortSignal
29
+ ): void {
30
+ const keys = config.byItemprop === true ? null : new Set(config.byItemprop);
31
+
32
+ const observer = new MutationObserver((mutations) => {
33
+ for (const mutation of mutations) {
34
+ if (mutation.type === 'childList') {
35
+ // Process added nodes and their descendants
36
+ for (const node of mutation.addedNodes) {
37
+ if (!(node instanceof Element)) continue;
38
+ // Check the added element itself
39
+ processElement(node, from, keys, target);
40
+ // Check descendants (respecting scope perimeter)
41
+ const nested = node.querySelectorAll('[itemprop]');
42
+ for (const el of nested) {
43
+ processElement(el as Element, from, keys, target);
44
+ }
45
+ }
46
+ } else if (mutation.type === 'attributes' && mutation.target instanceof Element) {
47
+ // itemprop attribute was added or changed on an existing element
48
+ processElement(mutation.target, from, keys, target);
49
+ }
50
+ }
51
+ });
52
+
53
+ observer.observe(target, {
54
+ childList: true,
55
+ subtree: true,
56
+ attributes: true,
57
+ attributeFilter: ['itemprop']
58
+ });
59
+
60
+ // Cleanup on abort
61
+ signal.addEventListener('abort', () => observer.disconnect(), { once: true });
62
+ }
63
+
64
+ /**
65
+ * Process a single element: check if it matches, apply inferred value, cache it.
66
+ */
67
+ function processElement(
68
+ element: Element,
69
+ from: any,
70
+ keys: Set<string> | null,
71
+ scopeRoot: Element
72
+ ): void {
73
+ const itemprop = element.getAttribute('itemprop');
74
+ if (!itemprop) return;
75
+ if (keys && !keys.has(itemprop)) return;
76
+ if (!(itemprop in from)) return;
77
+ if (!withScopePerimeter(scopeRoot, element, '[itemscope]')) return;
78
+
79
+ // Set value via inferencer
80
+ const infer = new Infer(element, itemprop);
81
+ infer.value = from[itemprop];
82
+
83
+ // Cache for future fast access
84
+ registerInCache(scopeRoot, itemprop, element);
85
+ }
package/enhanceAll.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * enhanceAll.ts — Bulk enhancement application via EMC (Element Mount Configuration) JSON.
3
+ *
4
+ * Finds matching elements within a target and spawns enhancements on them
5
+ * using the configuration from EMC JSON files that enhancement packages publish.
6
+ *
7
+ * Dynamically imported by assignFrom when `enhance` option is present,
8
+ * or used standalone.
9
+ *
10
+ * @example
11
+ * import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
12
+ *
13
+ * await enhanceAll(shadowRoot, [
14
+ * { emc: 'be-bound/emc.json', matching: '[name]' },
15
+ * { emc: 'be-observant/emc.json', matching: '[itemprop]' },
16
+ * ]);
17
+ */
18
+ import { isAllowedImportPath } from './isAllowedImportPath.js';
19
+ /**
20
+ * Apply enhancements in bulk to matching elements within a target.
21
+ *
22
+ * For each entry:
23
+ * 1. Dynamically imports the EMC JSON
24
+ * 2. Extracts enhConfig (spawn path, enhKey, withAttrs)
25
+ * 3. Finds matching elements via querySelectorAll
26
+ * 4. Registers the enhancement if not already registered
27
+ * 5. Spawns the enhancement on each matched element via enh.get()
28
+ *
29
+ * Note: No scope perimeter is applied. For reactive observation of new elements,
30
+ * use mount-observer instead.
31
+ *
32
+ * @param target - The DOM element to search within
33
+ * @param configs - Array of enhancement configurations
34
+ */
35
+ export async function enhanceAll(target, configs, permissions) {
36
+ for (const config of configs) {
37
+ // Validate EMC path unless cross-domain imports are explicitly permitted
38
+ if (!permissions?.crossDomainImports && !isAllowedImportPath(config.emc)) {
39
+ throw new Error(`enhanceAll: EMC path "${config.emc}" is a cross-domain URL. ` +
40
+ `Only relative, absolute, or bare specifier paths are allowed by default. ` +
41
+ `Pass { crossDomainImports: true } in permissions to override.`);
42
+ }
43
+ // 1. Import the EMC JSON
44
+ const emcModule = await import(config.emc, { with: { type: 'json' } });
45
+ const emc = emcModule.default ?? emcModule;
46
+ const { enhConfig } = emc;
47
+ if (!enhConfig) {
48
+ throw new Error(`enhanceAll: EMC at "${config.emc}" does not contain an enhConfig field`);
49
+ }
50
+ // 2. Determine the selector
51
+ const selector = config.matching ?? enhConfig.matching ?? '*';
52
+ // 3. Find matching elements
53
+ const elements = target.querySelectorAll(selector);
54
+ if (elements.length === 0)
55
+ continue;
56
+ // 4. Resolve and register the enhancement
57
+ const registryItem = await resolveAndRegister(enhConfig, target);
58
+ if (!registryItem)
59
+ continue;
60
+ // 5. Spawn enhancement on each matched element
61
+ for (const el of elements) {
62
+ try {
63
+ el.enh.get(registryItem);
64
+ }
65
+ catch {
66
+ // If enh gateway isn't available, skip silently
67
+ }
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Resolve the enhancement's registry item from EMC enhConfig.
73
+ * If not already registered, dynamically imports the spawn module and registers it.
74
+ */
75
+ async function resolveAndRegister(enhConfig, target) {
76
+ const { enhKey, spawn: spawnPath } = enhConfig;
77
+ // Get the enhancement registry for this element's scope
78
+ const registry = target.customElementRegistry?.enhancementRegistry
79
+ ?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
80
+ if (!registry)
81
+ return null;
82
+ // Check if already registered
83
+ if (enhKey) {
84
+ const existing = registry.findByEnhKey(enhKey);
85
+ if (existing)
86
+ return existing;
87
+ }
88
+ // Not registered — dynamically import the spawn module
89
+ if (!spawnPath)
90
+ return null;
91
+ const spawnModule = await import(spawnPath);
92
+ const SpawnClass = spawnModule.default ?? Object.values(spawnModule).find((v) => typeof v === 'function' && v.prototype);
93
+ if (!SpawnClass)
94
+ return null;
95
+ // Build and register the registry item
96
+ const registryItem = {
97
+ spawn: SpawnClass,
98
+ enhKey,
99
+ };
100
+ // Include withAttrs if present in enhConfig
101
+ if (enhConfig.withAttrs) {
102
+ registryItem.withAttrs = enhConfig.withAttrs;
103
+ }
104
+ registry.push(registryItem);
105
+ return registryItem;
106
+ }
package/enhanceAll.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * enhanceAll.ts — Bulk enhancement application via EMC (Element Mount Configuration) JSON.
3
+ *
4
+ * Finds matching elements within a target and spawns enhancements on them
5
+ * using the configuration from EMC JSON files that enhancement packages publish.
6
+ *
7
+ * Dynamically imported by assignFrom when `enhance` option is present,
8
+ * or used standalone.
9
+ *
10
+ * @example
11
+ * import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
12
+ *
13
+ * await enhanceAll(shadowRoot, [
14
+ * { emc: 'be-bound/emc.json', matching: '[name]' },
15
+ * { emc: 'be-observant/emc.json', matching: '[itemprop]' },
16
+ * ]);
17
+ */
18
+
19
+ import { isAllowedImportPath } from './isAllowedImportPath.js';
20
+ import type { AssignPermissions } from './isAllowedImportPath.js';
21
+
22
+ /**
23
+ * Configuration for a single enhancement to apply in bulk.
24
+ */
25
+ export interface EnhanceConfig {
26
+ /** Path to the EMC JSON file (dynamically imported) */
27
+ emc: string;
28
+ /** Override the CSS selector from the EMC (optional) */
29
+ matching?: string;
30
+ /** Whether to parse attributes via withAttrs (default: false). Phase II feature. */
31
+ parse?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Apply enhancements in bulk to matching elements within a target.
36
+ *
37
+ * For each entry:
38
+ * 1. Dynamically imports the EMC JSON
39
+ * 2. Extracts enhConfig (spawn path, enhKey, withAttrs)
40
+ * 3. Finds matching elements via querySelectorAll
41
+ * 4. Registers the enhancement if not already registered
42
+ * 5. Spawns the enhancement on each matched element via enh.get()
43
+ *
44
+ * Note: No scope perimeter is applied. For reactive observation of new elements,
45
+ * use mount-observer instead.
46
+ *
47
+ * @param target - The DOM element to search within
48
+ * @param configs - Array of enhancement configurations
49
+ */
50
+ export async function enhanceAll(
51
+ target: Element,
52
+ configs: EnhanceConfig[],
53
+ permissions?: AssignPermissions
54
+ ): Promise<void> {
55
+ for (const config of configs) {
56
+ // Validate EMC path unless cross-domain imports are explicitly permitted
57
+ if (!permissions?.crossDomainImports && !isAllowedImportPath(config.emc)) {
58
+ throw new Error(
59
+ `enhanceAll: EMC path "${config.emc}" is a cross-domain URL. ` +
60
+ `Only relative, absolute, or bare specifier paths are allowed by default. ` +
61
+ `Pass { crossDomainImports: true } in permissions to override.`
62
+ );
63
+ }
64
+
65
+ // 1. Import the EMC JSON
66
+ const emcModule = await import(config.emc, { with: { type: 'json' } });
67
+ const emc = emcModule.default ?? emcModule;
68
+ const { enhConfig } = emc;
69
+
70
+ if (!enhConfig) {
71
+ throw new Error(`enhanceAll: EMC at "${config.emc}" does not contain an enhConfig field`);
72
+ }
73
+
74
+ // 2. Determine the selector
75
+ const selector = config.matching ?? enhConfig.matching ?? '*';
76
+
77
+ // 3. Find matching elements
78
+ const elements = target.querySelectorAll(selector);
79
+ if (elements.length === 0) continue;
80
+
81
+ // 4. Resolve and register the enhancement
82
+ const registryItem = await resolveAndRegister(enhConfig, target);
83
+ if (!registryItem) continue;
84
+
85
+ // 5. Spawn enhancement on each matched element
86
+ for (const el of elements) {
87
+ try {
88
+ (el as any).enh.get(registryItem);
89
+ } catch {
90
+ // If enh gateway isn't available, skip silently
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve the enhancement's registry item from EMC enhConfig.
98
+ * If not already registered, dynamically imports the spawn module and registers it.
99
+ */
100
+ async function resolveAndRegister(enhConfig: any, target: Element): Promise<any> {
101
+ const { enhKey, spawn: spawnPath } = enhConfig;
102
+
103
+ // Get the enhancement registry for this element's scope
104
+ const registry = (target as any).customElementRegistry?.enhancementRegistry
105
+ ?? (typeof customElements !== 'undefined' ? (customElements as any).enhancementRegistry : undefined);
106
+
107
+ if (!registry) return null;
108
+
109
+ // Check if already registered
110
+ if (enhKey) {
111
+ const existing = registry.findByEnhKey(enhKey);
112
+ if (existing) return existing;
113
+ }
114
+
115
+ // Not registered — dynamically import the spawn module
116
+ if (!spawnPath) return null;
117
+
118
+ const spawnModule = await import(spawnPath);
119
+ const SpawnClass = spawnModule.default ?? Object.values(spawnModule).find(
120
+ (v: any) => typeof v === 'function' && v.prototype
121
+ );
122
+
123
+ if (!SpawnClass) return null;
124
+
125
+ // Build and register the registry item
126
+ const registryItem: any = {
127
+ spawn: SpawnClass,
128
+ enhKey,
129
+ };
130
+
131
+ // Include withAttrs if present in enhConfig
132
+ if (enhConfig.withAttrs) {
133
+ registryItem.withAttrs = enhConfig.withAttrs;
134
+ }
135
+
136
+ registry.push(registryItem);
137
+ return registryItem;
138
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * builtIns.join handler for assignFrom.
3
+ *
4
+ * Joins a resolved array into a single string. Supports nested sub-arrays
5
+ * with "all-or-nothing" semantics: if any element in a nested sub-array
6
+ * resolves to null/undefined, the entire sub-array is dropped.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.join'`
9
+ * is encountered — no explicit import is needed.
10
+ *
11
+ * @example
12
+ * assignFrom(oElement, {
13
+ * '?.textContent =>': {
14
+ * do: 'builtIns.join',
15
+ * resolve: {
16
+ * value: ['?.lastName', ', ', '?.firstName']
17
+ * }
18
+ * }
19
+ * }, { from: vm });
20
+ *
21
+ * @example
22
+ * // With optional segment (all-or-nothing):
23
+ * assignFrom(oElement, {
24
+ * '?.textContent =>': {
25
+ * do: 'builtIns.join',
26
+ * resolve: {
27
+ * value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
28
+ * }
29
+ * }
30
+ * }, { from: vm });
31
+ * // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
32
+ */
33
+ /**
34
+ * Process nested arrays with all-or-nothing null semantics.
35
+ * - Top-level null/undefined values are filtered out.
36
+ * - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
37
+ * - Nested sub-arrays that pass are flattened into the result.
38
+ */
39
+ function processValue(value) {
40
+ const result = [];
41
+ for (const item of value) {
42
+ if (Array.isArray(item)) {
43
+ // All-or-nothing: if any element is null/undefined, drop the entire sub-array
44
+ if (item.some(el => el == null)) {
45
+ continue;
46
+ }
47
+ // Sub-array passes — flatten its elements (recursively process nested arrays)
48
+ result.push(...processValue(item));
49
+ }
50
+ else if (item != null) {
51
+ result.push(item);
52
+ }
53
+ // Top-level null/undefined are silently filtered out
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * JoinHandler — built-in handler for composing strings from resolved arrays.
59
+ *
60
+ * Returns the joined string via the return-value protocol, which causes
61
+ * processHandlerCommands to assign it back to the LHS path.
62
+ */
63
+ export class JoinHandler {
64
+ config;
65
+ constructor(config) {
66
+ this.config = config;
67
+ }
68
+ async assign(lhsTarget, resolvedParams) {
69
+ const { value } = resolvedParams;
70
+ const separator = this.config.separator ?? '';
71
+ const items = Array.isArray(value) ? processValue(value) : [value];
72
+ return items.join(separator);
73
+ }
74
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * builtIns.join handler for assignFrom.
3
+ *
4
+ * Joins a resolved array into a single string. Supports nested sub-arrays
5
+ * with "all-or-nothing" semantics: if any element in a nested sub-array
6
+ * resolves to null/undefined, the entire sub-array is dropped.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.join'`
9
+ * is encountered — no explicit import is needed.
10
+ *
11
+ * @example
12
+ * assignFrom(oElement, {
13
+ * '?.textContent =>': {
14
+ * do: 'builtIns.join',
15
+ * resolve: {
16
+ * value: ['?.lastName', ', ', '?.firstName']
17
+ * }
18
+ * }
19
+ * }, { from: vm });
20
+ *
21
+ * @example
22
+ * // With optional segment (all-or-nothing):
23
+ * assignFrom(oElement, {
24
+ * '?.textContent =>': {
25
+ * do: 'builtIns.join',
26
+ * resolve: {
27
+ * value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
28
+ * }
29
+ * }
30
+ * }, { from: vm });
31
+ * // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
32
+ */
33
+
34
+ import type { AssignFromHandler } from '../assignFrom.js';
35
+
36
+ /**
37
+ * Process nested arrays with all-or-nothing null semantics.
38
+ * - Top-level null/undefined values are filtered out.
39
+ * - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
40
+ * - Nested sub-arrays that pass are flattened into the result.
41
+ */
42
+ function processValue(value: any[]): any[] {
43
+ const result: any[] = [];
44
+ for (const item of value) {
45
+ if (Array.isArray(item)) {
46
+ // All-or-nothing: if any element is null/undefined, drop the entire sub-array
47
+ if (item.some(el => el == null)) {
48
+ continue;
49
+ }
50
+ // Sub-array passes — flatten its elements (recursively process nested arrays)
51
+ result.push(...processValue(item));
52
+ } else if (item != null) {
53
+ result.push(item);
54
+ }
55
+ // Top-level null/undefined are silently filtered out
56
+ }
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * JoinHandler — built-in handler for composing strings from resolved arrays.
62
+ *
63
+ * Returns the joined string via the return-value protocol, which causes
64
+ * processHandlerCommands to assign it back to the LHS path.
65
+ */
66
+ export class JoinHandler implements AssignFromHandler {
67
+ config: any;
68
+
69
+ constructor(config: any) {
70
+ this.config = config;
71
+ }
72
+
73
+ async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<string> {
74
+ const { value } = resolvedParams;
75
+ const separator = this.config.separator ?? '';
76
+
77
+ const items = Array.isArray(value) ? processValue(value) : [value];
78
+ return items.join(separator);
79
+ }
80
+ }