@ape-egg/vibe 1.0.5 → 1.1.2

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 (35) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +228 -23
  3. package/compiler/bin/vibe-compile.js +109 -0
  4. package/compiler/native/.gitkeep +0 -0
  5. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  6. package/compiler/src/Cargo.lock +1885 -0
  7. package/compiler/src/Cargo.toml +29 -0
  8. package/compiler/src/compiler/compile.rs +1209 -0
  9. package/compiler/src/compiler/mod.rs +5 -0
  10. package/compiler/src/config.rs +184 -0
  11. package/compiler/src/main.rs +284 -0
  12. package/compiler/src/parser/element.rs +96 -0
  13. package/compiler/src/parser/html.rs +335 -0
  14. package/compiler/src/parser/mod.rs +8 -0
  15. package/index.js +2 -248
  16. package/package.json +26 -3
  17. package/{affected.js → runtime/affected.js} +64 -4
  18. package/runtime/cleanup.js +59 -0
  19. package/runtime/component.js +116 -0
  20. package/{conditionals.js → runtime/conditionals.js} +25 -11
  21. package/{constants.js → runtime/constants.js} +23 -3
  22. package/runtime/debug.js +91 -0
  23. package/{hydrate.js → runtime/hydrate.js} +57 -7
  24. package/runtime/index.js +614 -0
  25. package/{iterate.js → runtime/iterate.js} +53 -45
  26. package/{iteration-utils.js → runtime/iteration-utils.js} +11 -1
  27. package/{parse.js → runtime/parse.js} +37 -7
  28. package/runtime/state.js +52 -0
  29. package/ROADMAP.md +0 -289
  30. package/llms.txt +0 -279
  31. package/state.js +0 -26
  32. /package/{_vibe-compiled-iteration-batch.js → runtime/_vibe-compiled-iteration-batch.js} +0 -0
  33. /package/{link.js → runtime/manifest.js} +0 -0
  34. /package/{utils.js → runtime/utils.js} +0 -0
  35. /package/{vibe.css → runtime/vibe.css} +0 -0
@@ -0,0 +1,614 @@
1
+ import state from './state.js';
2
+ import parse from './parse.js';
3
+ import createManifest from './manifest.js';
4
+ import hydrate, { setPreviousState } from './hydrate.js';
5
+ import affected from './affected.js';
6
+ import { deepMerge, hash } from './utils.js';
7
+ import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
8
+ import { renderAllConditionals } from './conditionals.js';
9
+ import {
10
+ NON_REACTIVE_ELEMENTS,
11
+ PHASE_ATTACH,
12
+ PHASE_PARSE,
13
+ PHASE_MANIFEST,
14
+ PHASE_HYDRATE,
15
+ PHASE_ITERATE,
16
+ PHASE_CONDITION,
17
+ PHASE_OBSERVE,
18
+ PHASE_UPDATE,
19
+ PHASE_MUTATE,
20
+ PHASE_COMPLETE,
21
+ } from './constants.js';
22
+ import { processComponent, abortComponentFetch } from './component.js';
23
+ import { debugLog } from './debug.js';
24
+ import { shouldCleanup, cleanup } from './cleanup.js';
25
+
26
+ // Wire up cross-module dependency after all modules are loaded
27
+ setRenderAllConditionals(renderAllConditionals);
28
+
29
+ // Check if node itself or any ancestor is non-reactive or dehydrated
30
+ const isNonReactiveOrInside = (node) => {
31
+ let current = node;
32
+ while (current && current !== document.body) {
33
+ if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
34
+ return true;
35
+ }
36
+ if (current.hasAttribute?.('dehydrate')) {
37
+ return true;
38
+ }
39
+ current = current.parentElement;
40
+ }
41
+ return false;
42
+ };
43
+
44
+ // Navigate tree using dot notation (handles .children at each level)
45
+ const navigateTree = (tree, path) => {
46
+ if (!path) return tree;
47
+ return path.split('.').reduce((node, key) => node?.children?.[key], tree);
48
+ };
49
+
50
+ // Get or create a node in the tree at the given path
51
+ const ensureNode = (tree, path) => {
52
+ const keys = path.split('.');
53
+ return keys.reduce((node, key) => {
54
+ if (!node.children[key]) {
55
+ node.children[key] = { children: {} };
56
+ }
57
+ return node.children[key];
58
+ }, tree);
59
+ };
60
+
61
+ // Recursively add parsed tree nodes to manifest (like createManifest does)
62
+ const addToManifest = (tree, manifest, dotPath) => {
63
+ manifest[dotPath] = tree.element;
64
+
65
+ if (tree.children && Object.keys(tree.children).length > 0) {
66
+ Object.keys(tree.children).forEach((childKey) => {
67
+ addToManifest(tree.children[childKey], manifest, `${dotPath}.${childKey}`);
68
+ });
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Core loop: processes a node through parse → hydrate → conditionals → iterate
74
+ * @param {Node} node - DOM node to process (unused in current implementation, parsedNode is primary)
75
+ * @param {Object} parsedNode - Parsed tree node
76
+ * @param {Object} state - Current global state
77
+ * @param {Object} manifest - DOM manifest
78
+ * @param {Boolean} isNewNode - Whether this is a newly added node (affects hydration)
79
+ * @param {Boolean} debug - Debug mode
80
+ * @returns {Object} - Counts of hydrated/iterated/evaluated elements
81
+ */
82
+ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
83
+ let hydratedCount = 0;
84
+ let iteratedCount = 0;
85
+ let evaluatedCount = 0;
86
+
87
+ if (!parsedNode) return { hydratedCount, iteratedCount, evaluatedCount };
88
+
89
+ // 1. Parse - already done before calling core loop (in processMutations)
90
+
91
+ // 2. Hydrate (replace @[...] bindings)
92
+ // Use empty object as "previous state" for new nodes so all bindings are affected
93
+ const affectedElements = affected(parsedNode, isNewNode ? {} : previousState, state);
94
+ if (affectedElements.length > 0) {
95
+ hydratedCount = affectedElements.length;
96
+ hydrate(affectedElements, state, manifest);
97
+ }
98
+
99
+ // 3. Conditionals (evaluate <!-- if -->)
100
+ const conditionalCount = renderAllConditionals(parsedNode, state, manifest);
101
+ if (conditionalCount > 0) {
102
+ evaluatedCount = conditionalCount;
103
+ }
104
+
105
+ // 4. Iterate (render <!-- each -->)
106
+ const iterationCount = renderAllIterations(parsedNode, state, manifest);
107
+ if (iterationCount > 0) {
108
+ iteratedCount = iterationCount;
109
+ }
110
+
111
+ // Note: Any DOM changes from steps 3-4 will trigger MutationObserver
112
+ // which will recursively call processMutations for nested content
113
+
114
+ return { hydratedCount, iteratedCount, evaluatedCount };
115
+ };
116
+
117
+ // Store previous state for comparison (needs to be accessible by core loop)
118
+ let previousState = {};
119
+
120
+ const main = (s, attrName = 'vibe', config = {}) => {
121
+ const debug = !!config?.debug;
122
+ const verbose = !!config?.verbose;
123
+
124
+ // Reset previous state for each new instance
125
+ previousState = {};
126
+
127
+ // Find element(s) with the specified attribute
128
+ const elements = document.querySelectorAll(`[${attrName}]`);
129
+
130
+ if (elements.length === 0) {
131
+ console.info(`[vibe] No element found with attribute "${attrName}". Falling back to body.`);
132
+ } else if (elements.length > 1) {
133
+ console.info(
134
+ `[vibe] Multiple elements (${elements.length}) found with attribute "${attrName}". Hydrating the first one.`,
135
+ );
136
+ }
137
+
138
+ const rootElement = elements[0] || document.body;
139
+
140
+ debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
141
+
142
+ let parsedTree = parse(rootElement);
143
+ let manifest = createManifest(parsedTree);
144
+
145
+ const manifestEntries = Object.entries(manifest);
146
+ const totalCount = manifestEntries.length;
147
+ const elementsOnly = manifestEntries.filter(
148
+ ([_, el]) => el && !el.nodeName.startsWith('#'),
149
+ ).length;
150
+ const nodesOnly = totalCount - elementsOnly;
151
+
152
+ const segments = [
153
+ { text: `${elementsOnly}`, colored: true },
154
+ { text: ' elements (', colored: false },
155
+ { text: `${totalCount}`, color: 'slate' },
156
+ { text: ' total nodes)', colored: false },
157
+ ];
158
+ if (parsedTree.stats?.skipped > 0) {
159
+ segments.push(
160
+ { text: ' (', colored: false },
161
+ { text: `${parsedTree.stats.skipped}`, color: 'yellow' },
162
+ { text: ' skipped)', colored: false },
163
+ );
164
+ }
165
+ debugLog(PHASE_PARSE, segments, debug);
166
+ debugLog(PHASE_MANIFEST, `DOM manifest created`, debug);
167
+
168
+ // Lifecycle hooks that users can subscribe to
169
+ const hooks = {
170
+ afterUpdate: [],
171
+ afterDomMutation: [],
172
+ };
173
+
174
+ // Observer reference and callback - defined here so state handler can access processMutations
175
+ let observer = null;
176
+ let processMutations = null;
177
+
178
+ const $ = state(s, (newState) => {
179
+ const mergedState = deepMerge($, newState);
180
+
181
+ // Find what changed
182
+ const affectedElements = affected(parsedTree, previousState, mergedState);
183
+
184
+ if (affectedElements.length > 0) {
185
+ const changedKeys = Object.keys(newState).join(', ');
186
+ debugLog(PHASE_UPDATE, `state changed (${changedKeys})`, debug);
187
+
188
+ // Capture pending mutations before disconnecting (takeRecords clears the queue)
189
+ let pendingMutations = [];
190
+ if (observer) {
191
+ pendingMutations = observer.takeRecords();
192
+ observer.disconnect();
193
+ }
194
+
195
+ // Run core loop on the full tree (hydrate will use affected list)
196
+ // isNewNode = false because this is a state update, not a DOM mutation
197
+ hydrate(affectedElements, mergedState, manifest);
198
+
199
+ // After hydrate, check for conditional/iteration changes
200
+ const conditionalCount = renderAllConditionals(parsedTree, mergedState, manifest);
201
+ const iterationCount = renderAllIterations(parsedTree, mergedState, manifest);
202
+
203
+ if (observer) {
204
+ observer.observe(rootElement, {
205
+ attributes: false,
206
+ characterData: false,
207
+ childList: true,
208
+ subtree: true,
209
+ });
210
+
211
+ // Process mutations that were pending before we disconnected
212
+ if (pendingMutations.length > 0 && processMutations) {
213
+ processMutations(pendingMutations);
214
+ }
215
+ }
216
+ }
217
+
218
+ // Deep clone that handles proxies (structuredClone fails on proxy objects)
219
+ const deepClone = (obj) => {
220
+ if (obj === null || typeof obj !== 'object') return obj;
221
+ if (Array.isArray(obj)) return obj.map(deepClone);
222
+ const cloned = {};
223
+ for (const key in obj) {
224
+ if (obj.hasOwnProperty(key)) {
225
+ cloned[key] = deepClone(obj[key]);
226
+ }
227
+ }
228
+ return cloned;
229
+ };
230
+
231
+ const prev = deepClone(previousState);
232
+ previousState = { ...$, ...newState };
233
+ hooks.afterUpdate.forEach((callback) => callback(deepClone({ ...$ }), prev));
234
+ });
235
+
236
+ // Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
237
+ Object.defineProperty($, 'on', {
238
+ value: (event, callback) => {
239
+ if (hooks[event]) {
240
+ hooks[event].push(callback);
241
+ }
242
+ return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
243
+ },
244
+ enumerable: false,
245
+ });
246
+
247
+ // Initial hydration
248
+ const affectedElements = affected(parsedTree, $, $);
249
+
250
+ debugLog(
251
+ PHASE_HYDRATE,
252
+ [
253
+ { text: `${affectedElements.length} bindings (`, colored: false },
254
+ { text: '@[...]', color: 'pink' },
255
+ { text: ')', colored: false },
256
+ ],
257
+ debug,
258
+ );
259
+
260
+ hydrate(affectedElements, $, manifest);
261
+
262
+
263
+ // Render all iterations and conditionals after initial hydration
264
+ setPreviousState($);
265
+ previousState = { ...$ };
266
+
267
+ const iterationCount = renderAllIterations(parsedTree, $, manifest);
268
+ if (iterationCount > 0)
269
+ debugLog(
270
+ PHASE_ITERATE,
271
+ [
272
+ { text: `${iterationCount} iterations (`, colored: false },
273
+ { text: '<!-- each -->', color: 'commentGreen' },
274
+ { text: ')', colored: false },
275
+ ],
276
+ debug,
277
+ );
278
+
279
+ const conditionalCount = renderAllConditionals(parsedTree, $, manifest);
280
+ if (conditionalCount > 0)
281
+ debugLog(
282
+ PHASE_CONDITION,
283
+ [
284
+ { text: `${conditionalCount} conditionals (`, colored: false },
285
+ { text: '<!-- if -->', color: 'commentGreen' },
286
+ { text: ')', colored: false },
287
+ ],
288
+ debug,
289
+ );
290
+
291
+ // Define observer callback as named function so we can call it manually for pending mutations
292
+ processMutations = (mutations) => {
293
+ // Early exit if no mutations to process (common case)
294
+ if (mutations.length === 0) return;
295
+
296
+ const manifestSizeBefore = Object.keys(manifest).length;
297
+ let hadChanges = false;
298
+ let parsedParents = null; // Lazy init - only create Set when needed
299
+ let addedElements = 0;
300
+ let addedNodes = 0;
301
+ let removedElements = 0;
302
+ let removedNodes = 0;
303
+ let singleElement = null; // Track the single element when count is 1
304
+ let totalSkipped = 0; // Track skipped elements from parse
305
+ let hydratedCount = 0;
306
+ let iteratedCount = 0;
307
+ let evaluatedCount = 0;
308
+ let addedElementsList = []; // Track all added elements for verbose output
309
+
310
+ mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
311
+ removedNodesList.forEach((node) => {
312
+ // If this is a component element with pending fetch, abort it
313
+ if (node.nodeName === 'COMPONENT') {
314
+ abortComponentFetch(node);
315
+ }
316
+
317
+ const entry = Object.entries(manifest).find(([_, element]) => element === node);
318
+
319
+ // Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
320
+ if (!entry) return;
321
+
322
+ const [dotAnnotation] = entry;
323
+ delete manifest[dotAnnotation];
324
+
325
+ const dotPath = dotAnnotation.split('.');
326
+ const name = dotPath.pop();
327
+ const parentDotAnnotation = dotPath.join('.');
328
+
329
+ const picked = navigateTree(parsedTree, parentDotAnnotation);
330
+
331
+ // If we can't navigate to the parent, skip
332
+ if (!picked || !picked.element) return;
333
+
334
+ // Update parent's parsed HTML (only once per parent)
335
+ if (!parsedParents) parsedParents = new Set();
336
+ if (!parsedParents.has(picked)) {
337
+ const { parsed } = parse(picked.element);
338
+ picked.parsed = parsed;
339
+ parsedParents.add(picked);
340
+ }
341
+
342
+ // Remove the node from parent's children
343
+ delete picked.children[name];
344
+
345
+ // Count elements vs nodes separately
346
+ if (node.nodeName.startsWith('#')) {
347
+ removedNodes++;
348
+ } else {
349
+ removedElements++;
350
+ singleElement = node;
351
+ }
352
+ hadChanges = true;
353
+ });
354
+
355
+ addedNodesList.forEach((node) => {
356
+ // Skip if node itself or any ancestor is non-reactive
357
+ if (isNonReactiveOrInside(node)) {
358
+ return;
359
+ }
360
+
361
+ const entry = Object.entries(manifest).find(([_, element]) => element === target);
362
+
363
+ // If parent isn't tracked, this node is outside the reactive scope
364
+ if (!entry) return;
365
+
366
+ const [dotAnnotation] = entry;
367
+ const picked = navigateTree(parsedTree, dotAnnotation);
368
+
369
+ // If we can't navigate to the parent in the tree, skip
370
+ if (!picked) return;
371
+
372
+ // If parent has no element reference, re-parse from the actual DOM element
373
+ if (!picked.element) {
374
+ picked.element = target;
375
+ }
376
+
377
+ // Parse the newly added node
378
+ const name = `${node.nodeName.toLowerCase()}_${hash()}`;
379
+ const parsedNode = parse(node);
380
+
381
+ // Accumulate skipped stats
382
+ if (parsedNode.stats?.skipped) {
383
+ totalSkipped += parsedNode.stats.skipped;
384
+ }
385
+
386
+ // Update parent's parsed HTML (only once per parent)
387
+ if (!parsedParents) parsedParents = new Set();
388
+ if (!parsedParents.has(picked)) {
389
+ const { parsed } = parse(picked.element);
390
+ picked.parsed = parsed;
391
+ parsedParents.add(picked);
392
+ }
393
+
394
+ // Add the parsed node to parent's children
395
+ picked.children[name] = parsedNode;
396
+
397
+ // Recursively add node and all descendants to manifest
398
+ addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
399
+
400
+ // Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
401
+ const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
402
+ hydratedCount += counts.hydratedCount;
403
+ iteratedCount += counts.iteratedCount;
404
+ evaluatedCount += counts.evaluatedCount;
405
+
406
+ // Count elements vs nodes separately
407
+ if (node.nodeName.startsWith('#')) {
408
+ addedNodes++;
409
+ } else {
410
+ addedElements++;
411
+ singleElement = node;
412
+ addedElementsList.push(node);
413
+ }
414
+ hadChanges = true;
415
+ });
416
+ });
417
+
418
+ // Fire hooks once after all mutations are processed (not per-node)
419
+ if (hadChanges) {
420
+ if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
421
+ const segments = [];
422
+
423
+ // Added
424
+ if (addedElements > 0) {
425
+ segments.push({ text: `+${addedElements}`, color: 'green' });
426
+ segments.push({ text: ' elements', colored: false });
427
+ }
428
+ if (addedNodes > 0) {
429
+ if (addedElements > 0) segments.push({ text: ', ', colored: false });
430
+ segments.push({ text: `+${addedNodes}`, color: 'slate' });
431
+ segments.push({ text: ' nodes', colored: false });
432
+ }
433
+
434
+ // Removed
435
+ if (removedElements > 0) {
436
+ if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
437
+ segments.push({ text: `-${removedElements}`, color: 'red' });
438
+ segments.push({ text: ' elements', colored: false });
439
+ }
440
+ if (removedNodes > 0) {
441
+ if (addedElements > 0 || addedNodes > 0 || removedElements > 0)
442
+ segments.push({ text: ', ', colored: false });
443
+ segments.push({ text: `-${removedNodes}`, color: 'slate' });
444
+ segments.push({ text: ' nodes', colored: false });
445
+ }
446
+
447
+ // Pass element reference if exactly one element was mutated (not counting nodes)
448
+ // const totalElementCount = addedElements + removedElements;
449
+ // const elementToLog = totalElementCount === 1 ? singleElement : null;
450
+ // debugLog(PHASE_MUTATE, segments, debug, 0, elementToLog);
451
+ debugLog(PHASE_MUTATE, segments, debug);
452
+
453
+ // Verbose: log the added element (or topmost parent if multiple)
454
+ if (verbose && addedElementsList.length > 0) {
455
+ // Find the topmost parent among added elements
456
+ const topmostParent = addedElementsList.find((el) => {
457
+ // Check if this element is NOT a descendant of any other element in the list
458
+ return !addedElementsList.some((other) => other !== el && other.contains(el));
459
+ });
460
+ debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
461
+ }
462
+
463
+ // Log parsed elements after mutation
464
+ const manifestSizeAfter = Object.keys(manifest).length;
465
+ const totalNodes = manifestSizeAfter - manifestSizeBefore;
466
+
467
+ if (totalNodes > 0) {
468
+ const newManifestEntries = Object.entries(manifest).slice(-totalNodes);
469
+ const elementsOnly = newManifestEntries.filter(
470
+ ([_, el]) => el && !el.nodeName.startsWith('#'),
471
+ ).length;
472
+
473
+ const parseSegments = [
474
+ { text: `${elementsOnly}`, colored: true },
475
+ { text: ' elements (', colored: false },
476
+ { text: `${totalNodes}`, color: 'slate' },
477
+ { text: ' total nodes)', colored: false },
478
+ ];
479
+
480
+ if (totalSkipped > 0) {
481
+ parseSegments.push(
482
+ { text: ' (', colored: false },
483
+ { text: `${totalSkipped}`, color: 'yellow' },
484
+ { text: ' skipped)', colored: false },
485
+ );
486
+ }
487
+
488
+ debugLog(PHASE_PARSE, parseSegments, debug);
489
+
490
+ // Log hydration after parse
491
+ if (hydratedCount > 0) {
492
+ debugLog(
493
+ PHASE_HYDRATE,
494
+ [
495
+ { text: `${hydratedCount} bindings (`, colored: false },
496
+ { text: '@[...]', color: 'pink' },
497
+ { text: ')', colored: false },
498
+ ],
499
+ debug,
500
+ );
501
+ }
502
+
503
+ // Log iterations after hydration
504
+ if (iteratedCount > 0) {
505
+ debugLog(
506
+ PHASE_ITERATE,
507
+ [
508
+ { text: `${iteratedCount} iterations (`, colored: false },
509
+ { text: '<!-- each -->', color: 'commentGreen' },
510
+ { text: ')', colored: false },
511
+ ],
512
+ debug,
513
+ );
514
+ }
515
+
516
+ // Log conditionals after iterations
517
+ if (evaluatedCount > 0) {
518
+ debugLog(
519
+ PHASE_CONDITION,
520
+ [
521
+ { text: `${evaluatedCount} conditionals (`, colored: false },
522
+ { text: '<!-- if -->', color: 'commentGreen' },
523
+ { text: ')', colored: false },
524
+ ],
525
+ debug,
526
+ );
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ // Always call hooks, even if no changes detected (cleanup needs to check)
533
+ hooks.afterDomMutation.forEach((callback) => callback());
534
+
535
+ // Check for new <component> elements after DOM mutations
536
+ // Process component elements if we've started (initial call happened)
537
+ if (componentProcessingStarted && !cleanupExecuted) {
538
+ const componentConfig = {
539
+ ...config,
540
+ _forceSync: true,
541
+ _observer: observer,
542
+ _processMutations: processMutations
543
+ };
544
+ processComponent(rootElement, () => {
545
+ // When all components are done, run cleanup check
546
+ checkCleanup();
547
+ }, componentConfig);
548
+ }
549
+ };
550
+
551
+ observer = new MutationObserver(processMutations);
552
+
553
+ observer.observe(rootElement, {
554
+ attributes: false,
555
+ characterData: false,
556
+ childList: true,
557
+ subtree: true,
558
+ attributeOldValue: false,
559
+ characterDataOldValue: false,
560
+ });
561
+
562
+ debugLog(PHASE_OBSERVE, `MutationObserver listening to DOM changes`, debug);
563
+
564
+ // Track cleanup state
565
+ let componentProcessingStarted = false;
566
+ let cleanupExecuted = false;
567
+
568
+ // Check if cleanup should run
569
+ const checkCleanup = () => {
570
+ if (cleanupExecuted) return;
571
+
572
+ // Check for pending mutations first
573
+ const pendingMutations = observer ? observer.takeRecords() : [];
574
+
575
+ if (pendingMutations.length > 0) {
576
+ // More mutations to process
577
+ processMutations(pendingMutations);
578
+ return;
579
+ }
580
+
581
+ // Check if all processing is complete
582
+ if (shouldCleanup(rootElement)) {
583
+ cleanup(rootElement, attrName, debug);
584
+ cleanupExecuted = true;
585
+ }
586
+ };
587
+
588
+ // Register hook to check for cleanup readiness after each mutation batch
589
+ hooks.afterDomMutation.push(checkCleanup);
590
+
591
+ // Process component elements after initialization - MutationObserver will handle hydration
592
+ componentProcessingStarted = true;
593
+
594
+ // Pass observer and processMutations to component for sync processing
595
+ const componentConfig = {
596
+ ...config,
597
+ _forceSync: true,
598
+ _observer: observer,
599
+ _processMutations: processMutations
600
+ };
601
+
602
+ processComponent(
603
+ rootElement,
604
+ () => {
605
+ // When all components are done, run cleanup check
606
+ checkCleanup();
607
+ },
608
+ componentConfig,
609
+ );
610
+
611
+ return $;
612
+ };
613
+
614
+ export default main;