@ape-egg/vibe 4.0.1 → 4.1.1

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.
package/runtime/index.js CHANGED
@@ -38,22 +38,15 @@ import {
38
38
  restoreMarkersFromManifest,
39
39
  } from './pre-compiled-manifest.js';
40
40
 
41
- // Wire up cross-module dependency after all modules are loaded
42
41
  setRenderAllConditionals(renderAllConditionals);
43
42
 
44
- // Stamped at module load, not boot — a console can read __vibe.version even
45
- // on a page whose boot died, which is exactly when a bug report needs it.
46
43
  ((globalThis.__vibe ??= {}).version = VERSION);
47
44
 
48
- // Check if node should be processed by Vibe
49
45
  const shouldProcessNode = (node) => {
50
- // Only process element nodes
51
46
  if (node.nodeType !== 1) return false;
52
47
 
53
- // Skip nodes already managed by mountBranch or renderIteration
54
48
  if (managedNodes.has(node)) return false;
55
49
 
56
- // Fast check first: skip nodes without Vibe syntax (cheapest check)
57
50
  const html = node.outerHTML;
58
51
  if (
59
52
  !html.includes('@[') &&
@@ -64,7 +57,6 @@ const shouldProcessNode = (node) => {
64
57
  return false;
65
58
  }
66
59
 
67
- // Node has Vibe syntax - now check if it's in a non-reactive context
68
60
  let current = node;
69
61
  while (current && current !== document.body) {
70
62
  if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
@@ -82,22 +74,11 @@ const shouldProcessNode = (node) => {
82
74
  return true;
83
75
  };
84
76
 
85
- // Navigate tree using dot notation (handles .children at each level)
86
77
  const navigateTree = (tree, path) => {
87
78
  if (!path) return tree;
88
79
  return path.split('.').filter(k => k).reduce((node, key) => node?.children?.[key], tree);
89
80
  };
90
81
 
91
- // Locate the tree node whose `.element` is `target`, searching the full tree:
92
- // plain `children`, plus conditional branch trees (`runtime.activeInstance`)
93
- // and iteration instance trees (`runtime.instances`). navigateTree only walks
94
- // `children`, so it can't reach content projected across a <slot> boundary — a
95
- // conditional branch's slotted content is registered in the flat manifest by
96
- // path, but that path isn't navigable through `children` (the slot node's
97
- // children don't include the projected subtree). When a <component src> resolves
98
- // inside such content, navigateTree returns null and the resolved subtree would
99
- // be orphaned from the reactive tree. This identity search recovers the real
100
- // parent node so the subtree links in regardless of slot/branch projection.
101
82
  const findNodeByElement = (tree, target) => {
102
83
  if (!target) return null;
103
84
  const seen = new Set();
@@ -130,7 +111,6 @@ const findNodeByElement = (tree, target) => {
130
111
  return walk(tree);
131
112
  };
132
113
 
133
- // Get or create a node in the tree at the given path
134
114
  const ensureNode = (tree, path) => {
135
115
  const keys = path.split('.').filter(k => k);
136
116
  return keys.reduce((node, key) => {
@@ -141,7 +121,6 @@ const ensureNode = (tree, path) => {
141
121
  }, tree);
142
122
  };
143
123
 
144
- // Recursively add parsed tree nodes to manifest (like createManifest does)
145
124
  const addToManifest = (tree, manifest, dotPath) => {
146
125
  setManifestEntry(manifest, dotPath, tree.element);
147
126
 
@@ -152,16 +131,6 @@ const addToManifest = (tree, manifest, dotPath) => {
152
131
  }
153
132
  };
154
133
 
155
- /**
156
- * Core loop: processes a node through parse → hydrate → conditionals → iterate
157
- * @param {Node} node - DOM node to process (unused in current implementation, parsedNode is primary)
158
- * @param {Object} parsedNode - Parsed tree node
159
- * @param {Object} state - Current global state
160
- * @param {Object} manifest - DOM manifest
161
- * @param {Boolean} isNewNode - Whether this is a newly added node (affects hydration)
162
- * @param {Boolean} debug - Debug mode
163
- * @returns {Object} - Counts of hydrated/iterated/evaluated elements
164
- */
165
134
  const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
166
135
  let hydratedCount = 0;
167
136
  let iteratedCount = 0;
@@ -169,13 +138,6 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
169
138
 
170
139
  if (!parsedNode) return { hydratedCount, iteratedCount, evaluatedCount };
171
140
 
172
- // 1. Parse - already done before calling core loop (in processMutations)
173
-
174
- // 2. Hydrate (replace @[...] bindings)
175
- // For new nodes, use {} so all bindings are found. But filter out iterations
176
- // and conditionals — those should only go through renderAllIterations/renderAllConditionals
177
- // (initial render path), not updateIteration/updateConditional (which would diff against
178
- // stale oldState and produce false adds/removes).
179
141
  const oldStateForAffected = isNewNode ? {} : previousState;
180
142
  let affectedElements = affected(parsedNode, oldStateForAffected, state);
181
143
  if (isNewNode) {
@@ -186,33 +148,24 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
186
148
  hydrate(affectedElements, state, manifest, oldStateForAffected);
187
149
  }
188
150
 
189
- // 3. Conditionals (evaluate <!-- if -->)
190
151
  const conditionalCount = renderAllConditionals(parsedNode, state, manifest);
191
152
  if (conditionalCount > 0) {
192
153
  evaluatedCount = conditionalCount;
193
154
  }
194
155
 
195
- // 4. Iterate (render <!-- each -->)
196
156
  const iterationCount = renderAllIterations(parsedNode, state, manifest);
197
157
  if (iterationCount > 0) {
198
158
  iteratedCount = iterationCount;
199
159
  }
200
160
 
201
- // Note: Any DOM changes from steps 3-4 will trigger MutationObserver
202
- // which will recursively call processMutations for nested content
203
-
204
161
  return { hydratedCount, iteratedCount, evaluatedCount };
205
162
  };
206
163
 
207
- /**
208
- * Deep clone a tree node (to avoid mutating hyperspeed template)
209
- */
210
164
  const deepCloneNode = (node) => {
211
165
  if (!node || typeof node !== 'object') return node;
212
166
 
213
167
  const cloned = { ...node };
214
168
 
215
- // Clone children recursively
216
169
  if (node.children && typeof node.children === 'object') {
217
170
  cloned.children = {};
218
171
  for (const key in node.children) {
@@ -220,7 +173,6 @@ const deepCloneNode = (node) => {
220
173
  }
221
174
  }
222
175
 
223
- // Clone meta if it exists
224
176
  if (node.meta && typeof node.meta === 'object') {
225
177
  cloned.meta = { ...node.meta };
226
178
  if (node.meta.template) {
@@ -231,7 +183,6 @@ const deepCloneNode = (node) => {
231
183
  }
232
184
  }
233
185
 
234
- // Clone runtime if it exists
235
186
  if (node.runtime && typeof node.runtime === 'object') {
236
187
  cloned.runtime = { ...node.runtime };
237
188
  if (Array.isArray(node.runtime.instances)) {
@@ -242,121 +193,87 @@ const deepCloneNode = (node) => {
242
193
  return cloned;
243
194
  };
244
195
 
245
- /**
246
- * Merge hyperspeed manifest with runtime manifest
247
- * Hyperspeed provides structure/metadata, runtime populates DOM references
248
- */
249
196
  const mergeManifests = (hyperspeedTree, runtimeTree) => {
250
197
  if (!hyperspeedTree) return runtimeTree;
251
198
  if (!runtimeTree) return hyperspeedTree;
252
199
 
253
- // hyperspeedTree is already a clone (done before restoration to avoid mutation)
254
200
  const merged = hyperspeedTree;
255
201
 
256
- // Helper to recursively augment hyperspeed with runtime data
257
202
  const augmentWithRuntime = (mergedNode, runtimeNode) => {
258
203
  if (!runtimeNode) return;
259
204
 
260
- // Debug: log node types
261
- // Populate DOM references from runtime
262
205
  if (runtimeNode.element) {
263
206
  mergedNode.element = runtimeNode.element;
264
207
  }
265
208
 
266
- // Populate parsed data from runtime
267
209
  if (runtimeNode.parsed) {
268
210
  mergedNode.parsed = runtimeNode.parsed;
269
211
  }
270
212
 
271
- // Populate name bindings from runtime (detected after restoration)
272
213
  if (runtimeNode.nameBindings) {
273
214
  mergedNode.nameBindings = runtimeNode.nameBindings;
274
215
  }
275
216
 
276
- // Populate attributes from runtime (detected after restoration)
277
217
  if (runtimeNode.attributes) {
278
218
  mergedNode.attributes = runtimeNode.attributes;
279
219
  }
280
220
 
281
- // Populate textNode reference from runtime (for text node children)
282
221
  if (runtimeNode.textNode) {
283
222
  mergedNode.textNode = runtimeNode.textNode;
284
223
  }
285
224
 
286
- // For iterations: populate all meta and runtime from runtime
287
225
  if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
288
- // Preserve compiled data from manifest (has compiled batch function)
289
226
  const compiledData = mergedNode.compiled;
290
227
 
291
- // Copy entire meta object from runtime (all properties needed)
292
228
  mergedNode.meta = runtimeNode.meta;
293
- // Copy runtime object (instances, etc.)
294
229
  mergedNode.runtime = runtimeNode.runtime;
295
230
 
296
- // Restore compiled data if it was present
297
231
  if (compiledData) {
298
232
  mergedNode.compiled = compiledData;
299
233
  }
300
234
  }
301
235
 
302
- // For conditionals: populate all meta and runtime from runtime
303
236
  if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
304
- // Copy entire meta object from runtime (all properties needed)
305
237
  mergedNode.meta = runtimeNode.meta;
306
- // Copy runtime object (activeBranch, activeInstance, templateRemoved)
307
238
  mergedNode.runtime = runtimeNode.runtime;
308
239
  }
309
240
 
310
- // The runtime parse of the live DOM is the ground truth for what exists.
311
- // A manifest child with no runtime counterpart sits at an index the DOM
312
- // no longer agrees with (a content script prepending into <body> shifts
313
- // every sibling) — it can never bind an element, and hydrating its
314
- // bindings would dereference null. Drop it; the runtime-discovered
315
- // sibling added below carries the real element.
316
241
  if (mergedNode.children) {
317
242
  for (const key in mergedNode.children) {
318
243
  if (!runtimeNode.children?.[key]) delete mergedNode.children[key];
319
244
  }
320
245
  }
321
246
 
322
- // Augment children recursively
323
247
  if (runtimeNode.children) {
324
248
  if (!mergedNode.children) mergedNode.children = {};
325
249
 
326
250
  for (const key in runtimeNode.children) {
327
251
  const runtimeChild = runtimeNode.children[key];
328
252
 
329
- // For iterations: augment existing node, don't replace
330
253
  if (runtimeChild.type === 'iteration') {
331
254
  const hyperspeedChild = mergedNode.children[key];
332
255
 
333
256
  if (hyperspeedChild) {
334
- // Preserve compiled data from hyperspeed
335
257
  const compiledData = hyperspeedChild.compiled;
336
258
 
337
- // Update meta and runtime from runtime node
338
259
  mergedNode.children[key].meta = runtimeChild.meta;
339
260
  mergedNode.children[key].runtime = runtimeChild.runtime;
340
261
 
341
- // Keep compiled data from hyperspeed (it has batchFn)
342
262
  if (compiledData) {
343
263
  mergedNode.children[key].compiled = compiledData;
344
264
  }
345
265
  } else {
346
- // No hyperspeed node, just use runtime
347
266
  mergedNode.children[key] = runtimeChild;
348
267
  }
349
268
  continue;
350
269
  }
351
270
 
352
- // For conditionals: use runtime node but preserve compiled data
353
271
  if (runtimeChild.type === 'conditional') {
354
272
  const hyperspeedChild = mergedNode.children[key];
355
273
  const compiledData = hyperspeedChild?.compiled;
356
274
 
357
275
  mergedNode.children[key] = runtimeChild;
358
276
 
359
- // Restore compiled data if it existed
360
277
  if (compiledData) {
361
278
  mergedNode.children[key].compiled = compiledData;
362
279
  }
@@ -364,20 +281,16 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
364
281
  }
365
282
 
366
283
  if (mergedNode.children[key]) {
367
- // Child exists in both - augment it
368
284
  augmentWithRuntime(mergedNode.children[key], runtimeNode.children[key]);
369
285
  } else {
370
- // Child only in runtime - add it (dynamic element discovered at runtime)
371
286
  mergedNode.children[key] = runtimeNode.children[key];
372
287
  }
373
288
  }
374
289
  }
375
290
  };
376
291
 
377
- // Augment hyperspeed foundation with runtime data
378
292
  augmentWithRuntime(merged, runtimeTree);
379
293
 
380
- // Preserve stats from runtime (hyperspeed won't have stats)
381
294
  if (runtimeTree.stats) {
382
295
  merged.stats = runtimeTree.stats;
383
296
  }
@@ -385,7 +298,6 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
385
298
  return merged;
386
299
  };
387
300
 
388
- // Store previous state for comparison (needs to be accessible by core loop)
389
301
  let previousState = {};
390
302
 
391
303
  const main = (s, config = {}, stringSelector = '') => {
@@ -393,26 +305,19 @@ const main = (s, config = {}, stringSelector = '') => {
393
305
  const verbose = !!config?.verbose;
394
306
  ((globalThis.__vibe ??= {}).debug = debug);
395
307
 
396
- // Enable/disable the component template cache from config (`{ noCache }`).
397
308
  configureComponentCache(config);
398
309
 
399
- // Expose the global `$scope` resolver used by loop-scoped `on*` handlers.
400
310
  installScopeResolver();
401
311
 
402
- // Detect if running in compiler's headless browser
403
- // When true: skip cleanup to preserve [vibe] attribute in compiled HTML
404
312
  const isCompiling = typeof window !== 'undefined' && window.__vibe?.compiling === true;
405
313
 
406
- // Reset previous state for each new instance
407
314
  previousState = {};
408
315
 
409
- // Use page-specific hyperspeed manifest (detected at module load)
410
316
  const hyperspeedTree = hyperspeedManifest;
411
317
 
412
318
  let rootElement = document.body;
413
319
 
414
320
  if (stringSelector) {
415
- // Find element(s) with the specified attribute
416
321
  const elements = document.querySelectorAll(`${stringSelector}`);
417
322
  if (elements?.[0]) {
418
323
  rootElement = elements[0];
@@ -421,16 +326,10 @@ const main = (s, config = {}, stringSelector = '') => {
421
326
 
422
327
  debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
423
328
 
424
- // Component tagging is now handled by component.js before boot
425
- // Component state is merged into s by boot.js
426
-
427
- // RESTORATION PHASE: If hyperspeed detected, restore @[...] markers in DOM
428
- // This allows pre-rendered values to be visible (no FOUC) but makes DOM reactive
429
329
  let hyperspeedSubtree = null;
430
330
  if (hyperspeedTree) {
431
331
  const manifestName = hyperspeedPath ? hyperspeedPath.split('/').pop() : 'manifest.js';
432
332
 
433
- // Count compiled features
434
333
  const countCompiledIterations = (tree) => {
435
334
  let count = 0;
436
335
  if (tree.type === 'iteration' && tree.compiled?.iterations?.batchFn) count++;
@@ -444,7 +343,6 @@ const main = (s, config = {}, stringSelector = '') => {
444
343
 
445
344
  const compiledIterationCount = countCompiledIterations(hyperspeedTree);
446
345
 
447
- // Log features that are enabled
448
346
  debugLog(PHASE_HYPERSPEED, `Loaded ${manifestName}, page is pre-compiled`, debug);
449
347
 
450
348
  if (compiledIterationCount > 0) {
@@ -461,17 +359,13 @@ const main = (s, config = {}, stringSelector = '') => {
461
359
  );
462
360
  }
463
361
 
464
- // Find matching subtree by DOM path (not just tag name)
465
- // Build path from document to rootElement
466
362
  const buildDomPath = (element) => {
467
363
  const path = [];
468
364
  let current = element;
469
365
 
470
- // Walk up to document itself (include html in the path)
471
366
  while (current && current.parentNode && current !== document) {
472
367
  const parent = current.parentNode;
473
368
 
474
- // Skip document node itself
475
369
  if (parent === document) {
476
370
  const siblings = Array.from(document.childNodes);
477
371
  const index = siblings.indexOf(current);
@@ -491,19 +385,16 @@ const main = (s, config = {}, stringSelector = '') => {
491
385
  return path;
492
386
  };
493
387
 
494
- // Walk manifest tree following DOM path
495
388
  const findManifestNodeByPath = (manifest, path) => {
496
389
  let current = manifest;
497
390
 
498
391
  for (const { tag, index } of path) {
499
392
  if (!current || !current.children) return null;
500
393
 
501
- // Look for matching child by tag_index pattern
502
394
  const key = `${tag}_${index}`;
503
395
  if (current.children[key]) {
504
396
  current = current.children[key];
505
397
  } else {
506
- // Fallback: search all children for matching tag at this level
507
398
  let found = false;
508
399
  for (const childKey in current.children) {
509
400
  if (childKey.startsWith(tag + '_')) {
@@ -523,36 +414,27 @@ const main = (s, config = {}, stringSelector = '') => {
523
414
  let subtreeFromManifest = findManifestNodeByPath(hyperspeedTree, domPath);
524
415
 
525
416
  if (!subtreeFromManifest) {
526
- // Fallback to root if path matching fails
527
417
  subtreeFromManifest = hyperspeedTree;
528
418
  }
529
419
 
530
- // IMPORTANT: Clone BEFORE restoration because restoration mutates the tree
531
- // We need TWO clones: one for restoration (gets mutated), one for merging (stays intact)
532
- hyperspeedSubtree = deepCloneNode(subtreeFromManifest); // For merging
533
- const cloneForRestoration = deepCloneNode(subtreeFromManifest); // For restoration
420
+ hyperspeedSubtree = deepCloneNode(subtreeFromManifest);
421
+ const cloneForRestoration = deepCloneNode(subtreeFromManifest);
534
422
 
535
423
  restoreMarkersFromManifest(rootElement, cloneForRestoration, hyperspeedTree);
536
424
  }
537
425
 
538
- // Save raw slot content of fetched components before hydration replaces @[...] markers.
539
- // component.js captures el.innerHTML when resolving — if hydration already ran, the
540
- // binding syntax is gone and the resolved component won't be reactive.
541
426
  rootElement.querySelectorAll('component[src], div.component[src]').forEach(el => {
542
427
  el._vibeSlotContent = el.innerHTML;
543
428
  });
544
429
 
545
- // Runtime parses DOM (which now has restored markers if hyperspeed was used)
546
430
  let parsedTree = parse(rootElement);
547
431
 
548
- // Merge with hyperspeed if available (hyperspeed as foundation, runtime augments)
549
432
  if (hyperspeedSubtree) {
550
433
  parsedTree = mergeManifests(hyperspeedSubtree, parsedTree);
551
434
  }
552
435
 
553
436
  let manifest = createManifest(parsedTree);
554
437
 
555
- // Build hyperspeed manifest (before hydration, extract markers from parsed strings)
556
438
  const hyperspeedManifestData = buildHyperspeedManifest(parsedTree);
557
439
 
558
440
  const manifestEntries = Object.entries(manifest);
@@ -582,7 +464,6 @@ const main = (s, config = {}, stringSelector = '') => {
582
464
  debug,
583
465
  );
584
466
 
585
- // Lifecycle hooks that users can subscribe to
586
467
  const hooks = {
587
468
  afterUpdate: [],
588
469
  afterDomMutation: [],
@@ -590,25 +471,14 @@ const main = (s, config = {}, stringSelector = '') => {
590
471
  unmount: [],
591
472
  };
592
473
 
593
- // The ready phase happens once. A listener registered after it fires
594
- // immediately (parity with the late-safe $.ready promise) — late
595
- // registration is the SPA norm, where fragment scripts run on mount,
596
- // long after the shell booted.
597
474
  let readyFired = false;
598
475
 
599
- // Page-scope 'unmount': the visitor actually leaving — pagehide (navigation
600
- // away, tab close). Deliberately NOT visibilitychange: a tab switch is not
601
- // an unmount, the visitor comes back. Inside a component script the same
602
- // event name resolves to that component's unmount instead (the scoped `$`
603
- // proxy in component.js intercepts it before it reaches this hook).
604
476
  if (typeof window !== 'undefined') {
605
477
  window.addEventListener('pagehide', () => {
606
478
  hooks.unmount.forEach((callback) => callback());
607
479
  });
608
480
  }
609
481
 
610
- // Extract plain values from proxy (removes proxy wrappers)
611
- // Optimized: indexed loops, Object.keys (no prototype walk), inline primitive check
612
482
  const extractPlainValue = (obj) => {
613
483
  if (obj === null || typeof obj !== 'object') return obj;
614
484
  if (Array.isArray(obj)) {
@@ -631,14 +501,7 @@ const main = (s, config = {}, stringSelector = '') => {
631
501
  return plain;
632
502
  };
633
503
 
634
- // Observer callback will be defined below (already declared above before processComponent)
635
-
636
- // Pause the observer around an engine patch phase (its DOM writes are the
637
- // engine's own), then replay anything that queued while paused and resolve
638
- // fresh <component src> mounts. Shared by the walk flush and the
639
- // subscription dispatch flush.
640
504
  const patchWithObserverPaused = (patch) => {
641
- // Capture pending mutations before disconnecting (takeRecords clears the queue)
642
505
  let pendingMutations = [];
643
506
  if (observer) {
644
507
  pendingMutations = observer.takeRecords();
@@ -655,14 +518,11 @@ const main = (s, config = {}, stringSelector = '') => {
655
518
  subtree: true,
656
519
  });
657
520
 
658
- // Process mutations that were pending before we disconnected
659
521
  if (pendingMutations.length > 0 && processMutations) {
660
522
  processMutations(pendingMutations);
661
523
  }
662
524
  }
663
525
 
664
- // The patch may have mounted new DOM while the observer was disconnected.
665
- // Scan for unresolved <component src=""> elements that need fetching.
666
526
  if (componentProcessingStarted) {
667
527
  const componentConfig = {
668
528
  ...config,
@@ -675,17 +535,8 @@ const main = (s, config = {}, stringSelector = '') => {
675
535
  };
676
536
 
677
537
  const $ = state(s, (changedProps) => {
678
- // Selective extraction: only extract changed props, preserve references for unchanged.
679
- // This ensures affected() correctly skips iterations whose arrays didn't change,
680
- // while still detecting binding changes inside iteration instances.
681
538
  const currentState = { ...previousState };
682
539
  for (const prop of changedProps) {
683
- // Distinguish "set to undefined" (key still present in $) from "deleted"
684
- // (key absent from $). For deletions, removing from currentState matches
685
- // the live proxy's shape — otherwise downstream consumers that pass
686
- // currentState as `$` to evalInScope (e.g. iterations that re-render via
687
- // bindings reading the root state) would see the deleted key as a phantom
688
- // own-property with value `undefined`.
689
540
  if (prop in $) {
690
541
  currentState[prop] = extractPlainValue($[prop]);
691
542
  } else {
@@ -693,11 +544,6 @@ const main = (s, config = {}, stringSelector = '') => {
693
544
  }
694
545
  }
695
546
 
696
- // THE update path: the flush is served from the subscription reverse
697
- // index — a write notifies exactly its subscribers, O(change). The walk
698
- // (`affected()`) exists only as the MOUNT path now: first hydration,
699
- // processMutations, fresh branches and rows, where it registers new
700
- // subscribers via their first evaluation.
701
547
  const dirty = beginFlush(changedProps);
702
548
  if (dirty.size > 0) {
703
549
  debugLog(PHASE_UPDATE, 'state changed', debug);
@@ -706,30 +552,13 @@ const main = (s, config = {}, stringSelector = '') => {
706
552
  });
707
553
  }
708
554
 
709
- // Store previous state for hooks (currentState is already plain, no need to clone)
710
555
  const prev = previousState;
711
556
  previousState = currentState;
712
557
  hooks.afterUpdate.forEach((callback) => callback(currentState, prev));
713
558
  });
714
559
 
715
- // Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
716
- // `configurable: true` lets a component script's scoped `$` Proxy (built in
717
- // component.js) legally return a wrapped `.on` that auto-registers cleanup
718
- // — the Proxy invariant rejects overriding non-configurable + non-writable
719
- // data properties. Enumerable stays false so `on` doesn't leak into state
720
- // snapshots or Object.keys($).
721
- // Late ready — a `$.on('ready')` AFTER boot (a fetched fragment's script,
722
- // an SPA mount's app-boot module) defers until the registering mount
723
- // SETTLES: content inserted, attributes hydrated, staging committed, fouc
724
- // gates released. That makes `$.on('ready')` ≡ `await $.ready` genuinely
725
- // true — DOM-touching ready work (querySelector, scroll-spy) sees the
726
- // mounted subtree in both forms. With nothing in flight the microtask
727
- // flush fires on the same tick `await $.ready` would resume on.
728
560
  const lateReady = [];
729
561
  let lateReadyWatch = null;
730
- // Settlement is judged against the MANAGED root, same as boot ready —
731
- // content outside a config.target root is never scanned, so its literal
732
- // @[...] text must not starve the late-ready queue.
733
562
  const mountsSettled = () =>
734
563
  !document.querySelector('[vibe-staged], [vibe-fouc]') && shouldCleanup(rootElement);
735
564
  const flushLateReady = () => {
@@ -770,46 +599,23 @@ const main = (s, config = {}, stringSelector = '') => {
770
599
  configurable: true,
771
600
  });
772
601
 
773
- // Promise that resolves after the ready hook fires and all ready callbacks
774
- // have run. Lets late subscribers await readiness without missing the event:
775
- // `await $.ready`. Non-enumerable so it won't leak into state snapshots.
776
602
  let resolveReady;
777
603
  Object.defineProperty($, 'ready', {
778
604
  value: new Promise((resolve) => { resolveReady = resolve; }),
779
605
  enumerable: false,
780
606
  });
781
607
 
782
- // Reconcile a managed subtree against new source HTML. Opt-in entry point;
783
- // dormant unless called (so hot paths and benchmarks are unaffected).
784
608
  Object.defineProperty($, 'reconcile', {
785
609
  value: reconcile,
786
610
  enumerable: false,
787
611
  });
788
612
 
789
- // Register a component's state bucket under its id (the fetched-mount path).
790
- // A FRESH id is a key nothing in the live tree can bind yet — its subtree
791
- // enters the reactive tree only after this write — so the write lands
792
- // silently (raw target, no global flush) and previousState is deliberately
793
- // NOT seeded: the mount's grouped notify must see undefined → state as a
794
- // real diff (see the inline comment below — the load-bearing invariant).
795
- // Re-registering an EXISTING id (an HMR re-run) is a real value change for
796
- // live bindings and flushes normally.
797
613
  Object.defineProperty($, '_register', {
798
614
  value: (componentId, componentState) => {
799
615
  if (componentId in $) {
800
616
  $[componentId] = componentState;
801
617
  return componentId;
802
618
  }
803
- // Raw write only — previousState is deliberately NOT seeded. The mount
804
- // commits all its registrations in one notifyChanged batch when its
805
- // scripts settle, and that flush must see each fresh id as a real
806
- // change (undefined -> state): the subtree may have hydrated BEFORE the
807
- // script ran (scripts execute in module order behind earlier imports),
808
- // so this correction flush is what renders bindings, conditionals and
809
- // iterations that read the component's state. A seeded previousState
810
- // made the diff vacuous whenever the script filled its state object
811
- // before calling component() — the game's fill-then-register pages
812
- // stayed frozen at their pre-registration (empty) render.
813
619
  silentSet($, componentId, componentState);
814
620
  return componentId;
815
621
  },
@@ -817,80 +623,35 @@ const main = (s, config = {}, stringSelector = '') => {
817
623
  configurable: true,
818
624
  });
819
625
 
820
- // Mark a trusted string as raw HTML. A binding that is the sole content of
821
- // its element — `<p>@[$.unsafe(desc)]</p>` — sets innerHTML from the string
822
- // instead of escaping it via textContent. Trusted input only (no sanitizing,
823
- // like Svelte {@html}). Non-enumerable so it never leaks into state snapshots.
824
626
  Object.defineProperty($, 'unsafe', {
825
627
  value: unsafe,
826
628
  enumerable: false,
827
629
  configurable: true,
828
630
  });
829
631
 
830
- // Pure-render path for surgical component HMR. Given raw component template
831
- // HTML, callsite props, slot HTML, and existing componentIds, returns the
832
- // processed HTML string the plugin's HMR handler can hand to $.reconcile.
833
- // Scripts are NOT executed — callers use this only when they've verified
834
- // script contents haven't changed (so registered state is still valid).
835
632
  Object.defineProperty($, '_renderComponent', {
836
633
  value: renderComponentTemplate,
837
634
  enumerable: false,
838
635
  });
839
636
 
840
- // Invalidate cached component templates. `$.clearComponentCache(path)` drops
841
- // one entry, `$.clearComponentCache()` drops all. Templates are immutable in
842
- // production (nothing to clear), so this exists for tooling that swaps a
843
- // template under a live session — e.g. the dev server busts the changed file
844
- // on hot update. Non-enumerable so it never leaks into state snapshots.
845
637
  Object.defineProperty($, 'clearComponentCache', {
846
638
  value: clearComponentCache,
847
639
  enumerable: false,
848
640
  configurable: true,
849
641
  });
850
642
 
851
- // Expose the live reactive proxy to the iteration stamper so loop-scoped
852
- // `on*` handlers (`$scope`) resolve the SAME object identity the app sees via
853
- // `$`, instead of the plain diff-snapshot clones iterations render against
854
- // (see extractPlainValue below). Non-enumerable so it never shows up in the
855
- // manifest's node-path entry iteration.
856
643
  Object.defineProperty(manifest, '__live', { value: $, enumerable: false, configurable: true });
857
644
 
858
- // Pair the flat manifest (dotPath -> element) with its parsed tree so removal
859
- // paths can prune both views together. The page MutationObserver is
860
- // disconnected while Vibe renders (iteration/conditional teardown removes DOM
861
- // unobserved), so those paths must prune the manifest + tree themselves; this
862
- // gives them the tree root without threading it through every call. Same
863
- // non-enumerable contract as __live.
864
645
  Object.defineProperty(manifest, '__tree', { value: parsedTree, enumerable: false, configurable: true });
865
646
 
866
- // Bind `$` inside every expression to this live root proxy (see setRootState
867
- // in utils.js). Done before the first hydration pass so `$.unsafe` and the
868
- // other reserved methods are reachable from the initial render onward.
869
647
  setRootState($);
870
648
 
871
- // Publish the real proxy on `window.$` BEFORE the first hydration pass.
872
- // boot.js sets `window.$ = main(...)`, but until main returns it's the
873
- // pre-boot placeholder (vibeInstance with no state keys). User helpers
874
- // defined on `window` that close over `$` — e.g. a global `brawlerActivity`
875
- // function reading `$.combat?.duration` — would then read the placeholder
876
- // during initial render and treat the whole world as empty. Assigning the
877
- // live proxy here lets those closures see the right `$` from the very
878
- // first conditional/binding eval.
879
649
  if (typeof window !== 'undefined') window.$ = $;
880
650
 
881
- // Compiled pages: execute build-inlined component scripts (neutered to
882
- // type="vibe-module" by the compiler) through the runtime's component-script
883
- // pipeline — same injected component(), same scoped `$`, same import
884
- // rewriting as fetched scripts. Runs after `window.$` is live so
885
- // `const id = component(state); $[id].x = ...` captures the reactive proxy,
886
- // and before initial hydration so synchronous scripts' state is already
887
- // registered when `this.` bindings first evaluate. Async scripts gate
888
- // `ready` via compiledScriptsDone below.
889
651
  let compiledScriptsDone = true;
890
652
  const compiledScriptsPending = executeCompiledComponentScripts();
891
653
  if (compiledScriptsPending) compiledScriptsDone = false;
892
654
 
893
- // Initial hydration - pass plain values so iteration can do reference comparison
894
655
  const initialState = extractPlainValue($);
895
656
  const affectedElements = affected(parsedTree, initialState, initialState);
896
657
 
@@ -907,12 +668,8 @@ const main = (s, config = {}, stringSelector = '') => {
907
668
  debug,
908
669
  );
909
670
 
910
- // Hydrate with proxy $ so DOM bindings work
911
- // Pass initialState as oldState for iterations (won't actually update, just initial render)
912
671
  hydrate(affectedElements, $, manifest, initialState);
913
672
 
914
- // Render all iterations and conditionals after initial hydration
915
- // Use initialState (plain values) for iteration rendering so reference comparison works
916
673
  const iterationCount = renderAllIterations(parsedTree, initialState, manifest);
917
674
  if (iterationCount > 0)
918
675
  debugLog(
@@ -943,35 +700,28 @@ const main = (s, config = {}, stringSelector = '') => {
943
700
  debug,
944
701
  );
945
702
 
946
- // After all initial rendering, capture a clean snapshot for comparison
947
703
  previousState = extractPlainValue($);
948
704
 
949
- // Observer reference and processMutations
950
705
  let observer = null;
951
706
  let processMutations = null;
952
707
 
953
- // Define observer callback as named function so we can call it manually for pending mutations
954
708
  processMutations = (mutations) => {
955
- // Early exit if no mutations to process (common case)
956
709
  if (mutations.length === 0) return;
957
710
 
958
711
  const manifestSizeBefore = debug ? Object.keys(manifest).length : 0;
959
712
  let hadChanges = false;
960
- let parsedParents = null; // Lazy init - only create Set when needed
713
+ let parsedParents = null;
961
714
  let addedElements = 0;
962
715
  let addedNodes = 0;
963
716
  let removedElements = 0;
964
717
  let removedNodes = 0;
965
- let singleElement = null; // Track the single element when count is 1
966
- let totalSkipped = 0; // Track skipped elements from parse
718
+ let singleElement = null;
719
+ let totalSkipped = 0;
967
720
  let hydratedCount = 0;
968
721
  let iteratedCount = 0;
969
722
  let evaluatedCount = 0;
970
- let addedElementsList = []; // Track all added elements for verbose output
723
+ let addedElementsList = [];
971
724
 
972
- // Collect data-vibe-component-id values across ALL removed subtrees in this
973
- // batch before doing any per-node work, so we can evict their state after
974
- // the DOM mutations have been applied.
975
725
  const removedComponentIds = new Set();
976
726
  mutations.forEach(({ removedNodes: removedNodesList }) => {
977
727
  removedNodesList.forEach((node) => collectComponentIds(node, removedComponentIds));
@@ -979,21 +729,12 @@ const main = (s, config = {}, stringSelector = '') => {
979
729
 
980
730
  mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
981
731
  removedNodesList.forEach((node) => {
982
- // If this is a component element with pending fetch, abort it
983
732
  if (node.nodeName === 'COMPONENT') {
984
733
  abortComponentFetch(node);
985
734
  }
986
735
 
987
- // If this node is tracked by a conditional branch (e.g. a <component src>
988
- // that was replaced by processComponent via el.replaceWith), update the
989
- // conditional's tracked reference to point to the replacement node.
990
736
  const branchRef = branchNodeRegistry.get(node);
991
737
  if (branchRef) {
992
- // Find the replacement: an added node in the same mutation at the same
993
- // parent. A STAGED remount splits add and remove into different
994
- // batches (the incoming wrapper is inserted early, the outgoing one
995
- // removed at commit) — the wrapper's replacement back-pointer covers
996
- // that case.
997
738
  const replacement =
998
739
  Array.from(addedNodesList).find(n => n.parentNode === target) ||
999
740
  (node._vibeReplacedBy?.isConnected ? node._vibeReplacedBy : null);
@@ -1006,30 +747,19 @@ const main = (s, config = {}, stringSelector = '') => {
1006
747
 
1007
748
  const dotAnnotation = manifestPathOf(manifest, node);
1008
749
 
1009
- // Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
1010
750
  if (dotAnnotation === null) return;
1011
751
 
1012
- // Prune the node's ENTIRE manifest scope — root entry plus every
1013
- // descendant and branch-alias path under it. Deleting just the root
1014
- // entry leaked the rest, pinning each swapped-out page's detached
1015
- // DOM forever (the SPA navigation leak).
1016
752
  removeManifestSubtree(manifest, dotAnnotation);
1017
753
 
1018
754
  const dotPath = dotAnnotation.split('.');
1019
755
  const name = dotPath.pop();
1020
756
  const parentDotAnnotation = dotPath.join('.');
1021
757
 
1022
- // Identity fallback mirrors the added-node path: a parent inside
1023
- // slot-projected branch content isn't reachable via navigateTree's
1024
- // `children` walk, so resolve it by element identity instead — otherwise
1025
- // the removed node's stale tree entry lingers alongside its replacement.
1026
758
  const picked =
1027
759
  navigateTree(parsedTree, parentDotAnnotation) || findNodeByElement(parsedTree, target);
1028
760
 
1029
- // If we can't navigate to the parent, skip
1030
761
  if (!picked || !picked.element) return;
1031
762
 
1032
- // Update parent's parsed HTML (only once per parent)
1033
763
  if (!parsedParents) parsedParents = new Set();
1034
764
  if (!parsedParents.has(picked)) {
1035
765
  const { parsed } = parse(picked.element);
@@ -1037,10 +767,8 @@ const main = (s, config = {}, stringSelector = '') => {
1037
767
  parsedParents.add(picked);
1038
768
  }
1039
769
 
1040
- // Remove the node from parent's children
1041
770
  delete picked.children[name];
1042
771
 
1043
- // Count elements vs nodes separately
1044
772
  if (node.nodeName.startsWith('#')) {
1045
773
  removedNodes++;
1046
774
  } else {
@@ -1051,16 +779,10 @@ const main = (s, config = {}, stringSelector = '') => {
1051
779
  });
1052
780
 
1053
781
  addedNodesList.forEach((node) => {
1054
- // Skip nodes that aren't element nodes, are non-reactive, or lack Vibe syntax
1055
782
  if (!shouldProcessNode(node)) {
1056
783
  return;
1057
784
  }
1058
785
 
1059
- // Capture raw slot content of nested <component src> elements BEFORE parse runs.
1060
- // Parse creates conditional nodes from <!-- if --> comments, and renderConditional
1061
- // later removes the template nodes between the comments. Without capturing slot
1062
- // content first, conditionals inside a component's slot content lose their branch
1063
- // templates, breaking reactive updates.
1064
786
  if (node.nodeType === 1) {
1065
787
  node.querySelectorAll('component[src], div.component[src]').forEach((el) => {
1066
788
  if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
@@ -1069,23 +791,13 @@ const main = (s, config = {}, stringSelector = '') => {
1069
791
 
1070
792
  const dotAnnotation = manifestPathOf(manifest, target);
1071
793
 
1072
- // Parse the newly added node
1073
794
  const parsedNode = parse(node);
1074
795
 
1075
- // Accumulate skipped stats
1076
796
  if (parsedNode.stats?.skipped) {
1077
797
  totalSkipped += parsedNode.stats.skipped;
1078
798
  }
1079
799
 
1080
- // If parent is tracked in the manifest, register this new node in the parsed tree.
1081
- // (If not — e.g. mutations inside an iteration instance whose rows aren't in the
1082
- // global manifest — we still hydrate the node below; we just skip tree/manifest
1083
- // registration since there's no tree branch to attach to.)
1084
800
  if (dotAnnotation !== null) {
1085
- // navigateTree walks `children` only; when the parent lives in a
1086
- // conditional branch's slot-projected content its manifest path isn't
1087
- // navigable that way (see findNodeByElement). Fall back to an identity
1088
- // search so the resolved subtree still links into the reactive tree.
1089
801
  const picked =
1090
802
  navigateTree(parsedTree, dotAnnotation) || findNodeByElement(parsedTree, target);
1091
803
 
@@ -1094,15 +806,12 @@ const main = (s, config = {}, stringSelector = '') => {
1094
806
  picked.element = target;
1095
807
  }
1096
808
 
1097
- // Parse the newly added node (use monotonically increasing counter for deterministic key)
1098
- // Initialize counter if it doesn't exist
1099
809
  if (!picked._nextChildIndex) {
1100
810
  picked._nextChildIndex = Object.keys(picked.children).length;
1101
811
  }
1102
812
  const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
1103
- picked._nextChildIndex++; // Always increment, never decrement
813
+ picked._nextChildIndex++;
1104
814
 
1105
- // Update parent's parsed HTML (only once per parent)
1106
815
  if (!parsedParents) parsedParents = new Set();
1107
816
  if (!parsedParents.has(picked)) {
1108
817
  const { parsed } = parse(picked.element);
@@ -1110,34 +819,21 @@ const main = (s, config = {}, stringSelector = '') => {
1110
819
  parsedParents.add(picked);
1111
820
  }
1112
821
 
1113
- // Add the parsed node to parent's children
1114
822
  picked.children[name] = parsedNode;
1115
823
 
1116
- // Recursively add node and all descendants to manifest
1117
824
  addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
1118
825
  }
1119
826
  }
1120
827
 
1121
- // Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
1122
828
  const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
1123
829
  hydratedCount += counts.hydratedCount;
1124
830
  iteratedCount += counts.iteratedCount;
1125
831
  evaluatedCount += counts.evaluatedCount;
1126
832
 
1127
- // For inlined `<component>` wrappers belonging to an iteration row
1128
- // (marked by component.js's resolveIterationComponentProps transfer),
1129
- // stash the parsed tree on the wrapper. processCoreLoop has just run
1130
- // hydrate + renderAllConditionals + renderAllIterations on it, so
1131
- // `parsedNode` carries live `runtime.activeInstance` /
1132
- // `runtime.instances` data — exactly what `affected.js` needs to walk
1133
- // into branch / row content. Iterate.js's update path consumes this
1134
- // tree to re-hydrate bindings inside the inlined component on each
1135
- // row-scope change without rebuilding the wrapper's DOM.
1136
833
  if (node.nodeType === 1 && node._vibeIterPropExprs) {
1137
834
  node._vibeIterTree = parsedNode;
1138
835
  }
1139
836
 
1140
- // Count elements vs nodes separately
1141
837
  if (node.nodeName.startsWith('#')) {
1142
838
  addedNodes++;
1143
839
  } else {
@@ -1149,23 +845,18 @@ const main = (s, config = {}, stringSelector = '') => {
1149
845
  });
1150
846
  });
1151
847
 
1152
- // CLEANUP OF CURRENT STATE
1153
848
  releaseOrphanedComponentState(removedComponentIds);
1154
849
  let hadRemovals = false;
1155
850
  mutations.forEach(({ removedNodes: removedNodesList }) => {
1156
851
  if (removedNodesList.length > 0) hadRemovals = true;
1157
852
  releaseOrphanedIterationProps(removedNodesList);
1158
853
  });
1159
- // Subscribers anchored in the removed subtrees are dead — drop them from
1160
- // the reverse index (observed removals: page swaps, reconcile, app code).
1161
854
  if (hadRemovals) pruneDisconnected();
1162
855
 
1163
- // Fire hooks once after all mutations are processed (not per-node)
1164
856
  if (hadChanges) {
1165
857
  if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
1166
858
  const segments = [];
1167
859
 
1168
- // Added
1169
860
  if (addedElements > 0) {
1170
861
  segments.push({ text: `+${addedElements}`, color: 'green' });
1171
862
  segments.push({
@@ -1179,7 +870,6 @@ const main = (s, config = {}, stringSelector = '') => {
1179
870
  segments.push({ text: ` ${addedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
1180
871
  }
1181
872
 
1182
- // Removed
1183
873
  if (removedElements > 0) {
1184
874
  if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
1185
875
  segments.push({ text: `-${removedElements}`, color: 'red' });
@@ -1195,25 +885,15 @@ const main = (s, config = {}, stringSelector = '') => {
1195
885
  segments.push({ text: ` ${removedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
1196
886
  }
1197
887
 
1198
- // Pass element reference if exactly one element was mutated (not counting nodes)
1199
- // const totalElementCount = addedElements + removedElements;
1200
- // const elementToLog = totalElementCount === 1 ? singleElement : null;
1201
- // debugLog(PHASE_MUTATE, segments, debug, 0, elementToLog);
1202
888
  debugLog(PHASE_MUTATE, segments, debug);
1203
889
 
1204
- // Verbose: log the added element (or topmost parent if multiple)
1205
890
  if (verbose && addedElementsList.length > 0) {
1206
- // Find the topmost parent among added elements
1207
891
  const topmostParent = addedElementsList.find((el) => {
1208
- // Check if this element is NOT a descendant of any other element in the list
1209
892
  return !addedElementsList.some((other) => other !== el && other.contains(el));
1210
893
  });
1211
894
  debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
1212
895
  }
1213
896
 
1214
- // Log parsed elements after mutation — debug-only: the entries
1215
- // snapshot allocates the full manifest per batch, so skip it entirely
1216
- // when nothing will be printed.
1217
897
  const manifestSizeAfter = debug ? Object.keys(manifest).length : manifestSizeBefore;
1218
898
  const totalNodes = manifestSizeAfter - manifestSizeBefore;
1219
899
 
@@ -1240,7 +920,6 @@ const main = (s, config = {}, stringSelector = '') => {
1240
920
 
1241
921
  debugLog(PHASE_PARSE, parseSegments, debug);
1242
922
 
1243
- // Log hydration after parse
1244
923
  if (hydratedCount > 0) {
1245
924
  debugLog(
1246
925
  PHASE_HYDRATE,
@@ -1256,7 +935,6 @@ const main = (s, config = {}, stringSelector = '') => {
1256
935
  );
1257
936
  }
1258
937
 
1259
- // Log iterations after hydration
1260
938
  if (iteratedCount > 0) {
1261
939
  debugLog(
1262
940
  PHASE_ITERATE,
@@ -1272,7 +950,6 @@ const main = (s, config = {}, stringSelector = '') => {
1272
950
  );
1273
951
  }
1274
952
 
1275
- // Log conditionals after iterations
1276
953
  if (evaluatedCount > 0) {
1277
954
  debugLog(
1278
955
  PHASE_CONDITION,
@@ -1291,12 +968,8 @@ const main = (s, config = {}, stringSelector = '') => {
1291
968
  }
1292
969
  }
1293
970
 
1294
- // Always call hooks, even if no changes detected (cleanup needs to check)
1295
971
  hooks.afterDomMutation.forEach((callback) => callback());
1296
972
 
1297
- // Check for new <component> elements after DOM mutations
1298
- // Process component elements if we've started (initial call happened)
1299
- // Note: Also process after cleanup — conditionals may reveal new components
1300
973
  if (componentProcessingStarted) {
1301
974
  const componentConfig = {
1302
975
  ...config,
@@ -1307,7 +980,6 @@ const main = (s, config = {}, stringSelector = '') => {
1307
980
  processComponent(
1308
981
  rootElement,
1309
982
  () => {
1310
- // When all components are done, run cleanup check
1311
983
  checkCleanup();
1312
984
  },
1313
985
  componentConfig,
@@ -1328,34 +1000,21 @@ const main = (s, config = {}, stringSelector = '') => {
1328
1000
 
1329
1001
  debugLog(PHASE_OBSERVE, `MutationObserver listening to DOM changes`, debug);
1330
1002
 
1331
- // Track cleanup state
1332
1003
  let componentProcessingStarted = false;
1333
1004
  let cleanupExecuted = false;
1334
1005
  let bootSettleDone = false;
1335
1006
 
1336
- // Check if cleanup should run
1337
1007
  const checkCleanup = () => {
1338
1008
  if (cleanupExecuted || isCompiling || !compiledScriptsDone) return;
1339
1009
 
1340
- // Check for pending mutations first
1341
1010
  const pendingMutations = observer ? observer.takeRecords() : [];
1342
1011
 
1343
1012
  if (pendingMutations.length > 0) {
1344
- // More mutations to process
1345
1013
  processMutations(pendingMutations);
1346
1014
  return;
1347
1015
  }
1348
1016
 
1349
- // Check if all processing is complete
1350
1017
  if (shouldCleanup(rootElement)) {
1351
- // One-shot settle before ready: directives whose expressions read
1352
- // globals provided by component scripts (window helpers) carry no
1353
- // reactive dependency for those globals, so a directive that rendered
1354
- // before the defining script settled — boot renders race async imports,
1355
- // runtime fetch mounts land after initial hydration — would stay empty
1356
- // forever. All scripts and mounts have settled here; re-render once
1357
- // against the fully-scripted world. Idempotent: rendered iterations
1358
- // early-return, value-unchanged conditionals are a no-op.
1359
1018
  if (!bootSettleDone) {
1360
1019
  bootSettleDone = true;
1361
1020
  renderAllIterations(parsedTree, extractPlainValue($), manifest);
@@ -1364,7 +1023,6 @@ const main = (s, config = {}, stringSelector = '') => {
1364
1023
  cleanup(rootElement, debug);
1365
1024
  cleanupExecuted = true;
1366
1025
 
1367
- // Fire ready hook after cleanup completes
1368
1026
  readyFired = true;
1369
1027
  hooks.ready.forEach((callback) => {
1370
1028
  try {
@@ -1373,35 +1031,22 @@ const main = (s, config = {}, stringSelector = '') => {
1373
1031
  console.error('[vibe] Error in ready hook:', error);
1374
1032
  }
1375
1033
  });
1376
- // Resolve $.ready promise after all ready callbacks have run
1377
1034
  resolveReady();
1378
1035
  }
1379
1036
  };
1380
1037
 
1381
- // Register hook to check for cleanup readiness after each mutation batch
1382
1038
  hooks.afterDomMutation.push(checkCleanup);
1383
1039
 
1384
- // Async compiled component scripts (imports) finish after boot — unlock the
1385
- // ready gate and re-check once their state has merged into `$`.
1386
1040
  if (compiledScriptsPending) {
1387
1041
  compiledScriptsPending.then(() => {
1388
1042
  compiledScriptsDone = true;
1389
- // The initial conditional pass ran before these module scripts settled
1390
- // — native MPA ordering had page modules evaluate BEFORE vibe booted,
1391
- // so a gate on a module-provided global (`<!-- if window.isDev -->`)
1392
- // saw the booted world. A gate like that carries no reactive
1393
- // dependency to re-check it later, so settle conditionals once against
1394
- // the post-module world before ready fires. Value-unchanged branches
1395
- // are a no-op.
1396
1043
  settleConditionals(parsedTree, $, manifest);
1397
1044
  checkCleanup();
1398
1045
  });
1399
1046
  }
1400
1047
 
1401
- // Process component elements after initialization - MutationObserver will handle hydration
1402
1048
  componentProcessingStarted = true;
1403
1049
 
1404
- // Pass observer and processMutations to component for sync processing
1405
1050
  const componentConfig = {
1406
1051
  ...config,
1407
1052
  _forceSync: true,
@@ -1412,14 +1057,11 @@ const main = (s, config = {}, stringSelector = '') => {
1412
1057
  processComponent(
1413
1058
  rootElement,
1414
1059
  () => {
1415
- // When all components are done, run cleanup check
1416
1060
  checkCleanup();
1417
1061
  },
1418
1062
  componentConfig,
1419
1063
  );
1420
1064
 
1421
- // Export hyperspeed manifest globally for compiler extraction and optimizations
1422
- // Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
1423
1065
  if (typeof window !== 'undefined') {
1424
1066
  const manifestToExport = hyperspeedTree || hyperspeedManifestData;
1425
1067
  (window.__vibe ??= {}).manifest = manifestToExport;