@ape-egg/vibe 4.0.1 → 4.1.3

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
@@ -8,9 +8,9 @@ import { unsafe } from './raw-html.js';
8
8
  import { renderAllIterations, setRenderAllConditionals, releaseOrphanedIterationProps } from './iterate.js';
9
9
  import { renderAllConditionals, branchNodeRegistry, managedNodes , settleConditionals} from './conditionals.js';
10
10
  import { installScopeResolver } from './loop-scope.js';
11
+ import { isInert } from './inert.js';
11
12
  import {
12
13
  VERSION,
13
- NON_REACTIVE_ELEMENTS,
14
14
  PHASE_ATTACH,
15
15
  PHASE_PARSE,
16
16
  PHASE_MANIFEST,
@@ -22,7 +22,6 @@ import {
22
22
  PHASE_MUTATE,
23
23
  PHASE_HYPERSPEED,
24
24
  PHASE_READY,
25
- DEHYDRATE_CLASS_OR_ATTR,
26
25
  } from './constants.js';
27
26
  import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate, executeCompiledComponentScripts } from './component.js';
28
27
  import { configureComponentCache, clearComponentCache } from './component-cache.js';
@@ -38,22 +37,15 @@ import {
38
37
  restoreMarkersFromManifest,
39
38
  } from './pre-compiled-manifest.js';
40
39
 
41
- // Wire up cross-module dependency after all modules are loaded
42
40
  setRenderAllConditionals(renderAllConditionals);
43
41
 
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
42
  ((globalThis.__vibe ??= {}).version = VERSION);
47
43
 
48
- // Check if node should be processed by Vibe
49
44
  const shouldProcessNode = (node) => {
50
- // Only process element nodes
51
45
  if (node.nodeType !== 1) return false;
52
46
 
53
- // Skip nodes already managed by mountBranch or renderIteration
54
47
  if (managedNodes.has(node)) return false;
55
48
 
56
- // Fast check first: skip nodes without Vibe syntax (cheapest check)
57
49
  const html = node.outerHTML;
58
50
  if (
59
51
  !html.includes('@[') &&
@@ -64,40 +56,14 @@ const shouldProcessNode = (node) => {
64
56
  return false;
65
57
  }
66
58
 
67
- // Node has Vibe syntax - now check if it's in a non-reactive context
68
- let current = node;
69
- while (current && current !== document.body) {
70
- if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
71
- return false;
72
- }
73
- if (
74
- current.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) ||
75
- current.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)
76
- ) {
77
- return false;
78
- }
79
- current = current.parentElement;
80
- }
81
-
82
- return true;
59
+ return !isInert(node);
83
60
  };
84
61
 
85
- // Navigate tree using dot notation (handles .children at each level)
86
62
  const navigateTree = (tree, path) => {
87
63
  if (!path) return tree;
88
64
  return path.split('.').filter(k => k).reduce((node, key) => node?.children?.[key], tree);
89
65
  };
90
66
 
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
67
  const findNodeByElement = (tree, target) => {
102
68
  if (!target) return null;
103
69
  const seen = new Set();
@@ -130,7 +96,6 @@ const findNodeByElement = (tree, target) => {
130
96
  return walk(tree);
131
97
  };
132
98
 
133
- // Get or create a node in the tree at the given path
134
99
  const ensureNode = (tree, path) => {
135
100
  const keys = path.split('.').filter(k => k);
136
101
  return keys.reduce((node, key) => {
@@ -141,7 +106,6 @@ const ensureNode = (tree, path) => {
141
106
  }, tree);
142
107
  };
143
108
 
144
- // Recursively add parsed tree nodes to manifest (like createManifest does)
145
109
  const addToManifest = (tree, manifest, dotPath) => {
146
110
  setManifestEntry(manifest, dotPath, tree.element);
147
111
 
@@ -152,16 +116,6 @@ const addToManifest = (tree, manifest, dotPath) => {
152
116
  }
153
117
  };
154
118
 
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
119
  const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) => {
166
120
  let hydratedCount = 0;
167
121
  let iteratedCount = 0;
@@ -169,13 +123,6 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
169
123
 
170
124
  if (!parsedNode) return { hydratedCount, iteratedCount, evaluatedCount };
171
125
 
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
126
  const oldStateForAffected = isNewNode ? {} : previousState;
180
127
  let affectedElements = affected(parsedNode, oldStateForAffected, state);
181
128
  if (isNewNode) {
@@ -186,33 +133,24 @@ const processCoreLoop = (node, parsedNode, state, manifest, isNewNode, debug) =>
186
133
  hydrate(affectedElements, state, manifest, oldStateForAffected);
187
134
  }
188
135
 
189
- // 3. Conditionals (evaluate <!-- if -->)
190
136
  const conditionalCount = renderAllConditionals(parsedNode, state, manifest);
191
137
  if (conditionalCount > 0) {
192
138
  evaluatedCount = conditionalCount;
193
139
  }
194
140
 
195
- // 4. Iterate (render <!-- each -->)
196
141
  const iterationCount = renderAllIterations(parsedNode, state, manifest);
197
142
  if (iterationCount > 0) {
198
143
  iteratedCount = iterationCount;
199
144
  }
200
145
 
201
- // Note: Any DOM changes from steps 3-4 will trigger MutationObserver
202
- // which will recursively call processMutations for nested content
203
-
204
146
  return { hydratedCount, iteratedCount, evaluatedCount };
205
147
  };
206
148
 
207
- /**
208
- * Deep clone a tree node (to avoid mutating hyperspeed template)
209
- */
210
149
  const deepCloneNode = (node) => {
211
150
  if (!node || typeof node !== 'object') return node;
212
151
 
213
152
  const cloned = { ...node };
214
153
 
215
- // Clone children recursively
216
154
  if (node.children && typeof node.children === 'object') {
217
155
  cloned.children = {};
218
156
  for (const key in node.children) {
@@ -220,7 +158,6 @@ const deepCloneNode = (node) => {
220
158
  }
221
159
  }
222
160
 
223
- // Clone meta if it exists
224
161
  if (node.meta && typeof node.meta === 'object') {
225
162
  cloned.meta = { ...node.meta };
226
163
  if (node.meta.template) {
@@ -231,7 +168,6 @@ const deepCloneNode = (node) => {
231
168
  }
232
169
  }
233
170
 
234
- // Clone runtime if it exists
235
171
  if (node.runtime && typeof node.runtime === 'object') {
236
172
  cloned.runtime = { ...node.runtime };
237
173
  if (Array.isArray(node.runtime.instances)) {
@@ -242,121 +178,87 @@ const deepCloneNode = (node) => {
242
178
  return cloned;
243
179
  };
244
180
 
245
- /**
246
- * Merge hyperspeed manifest with runtime manifest
247
- * Hyperspeed provides structure/metadata, runtime populates DOM references
248
- */
249
181
  const mergeManifests = (hyperspeedTree, runtimeTree) => {
250
182
  if (!hyperspeedTree) return runtimeTree;
251
183
  if (!runtimeTree) return hyperspeedTree;
252
184
 
253
- // hyperspeedTree is already a clone (done before restoration to avoid mutation)
254
185
  const merged = hyperspeedTree;
255
186
 
256
- // Helper to recursively augment hyperspeed with runtime data
257
187
  const augmentWithRuntime = (mergedNode, runtimeNode) => {
258
188
  if (!runtimeNode) return;
259
189
 
260
- // Debug: log node types
261
- // Populate DOM references from runtime
262
190
  if (runtimeNode.element) {
263
191
  mergedNode.element = runtimeNode.element;
264
192
  }
265
193
 
266
- // Populate parsed data from runtime
267
194
  if (runtimeNode.parsed) {
268
195
  mergedNode.parsed = runtimeNode.parsed;
269
196
  }
270
197
 
271
- // Populate name bindings from runtime (detected after restoration)
272
198
  if (runtimeNode.nameBindings) {
273
199
  mergedNode.nameBindings = runtimeNode.nameBindings;
274
200
  }
275
201
 
276
- // Populate attributes from runtime (detected after restoration)
277
202
  if (runtimeNode.attributes) {
278
203
  mergedNode.attributes = runtimeNode.attributes;
279
204
  }
280
205
 
281
- // Populate textNode reference from runtime (for text node children)
282
206
  if (runtimeNode.textNode) {
283
207
  mergedNode.textNode = runtimeNode.textNode;
284
208
  }
285
209
 
286
- // For iterations: populate all meta and runtime from runtime
287
210
  if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
288
- // Preserve compiled data from manifest (has compiled batch function)
289
211
  const compiledData = mergedNode.compiled;
290
212
 
291
- // Copy entire meta object from runtime (all properties needed)
292
213
  mergedNode.meta = runtimeNode.meta;
293
- // Copy runtime object (instances, etc.)
294
214
  mergedNode.runtime = runtimeNode.runtime;
295
215
 
296
- // Restore compiled data if it was present
297
216
  if (compiledData) {
298
217
  mergedNode.compiled = compiledData;
299
218
  }
300
219
  }
301
220
 
302
- // For conditionals: populate all meta and runtime from runtime
303
221
  if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
304
- // Copy entire meta object from runtime (all properties needed)
305
222
  mergedNode.meta = runtimeNode.meta;
306
- // Copy runtime object (activeBranch, activeInstance, templateRemoved)
307
223
  mergedNode.runtime = runtimeNode.runtime;
308
224
  }
309
225
 
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
226
  if (mergedNode.children) {
317
227
  for (const key in mergedNode.children) {
318
228
  if (!runtimeNode.children?.[key]) delete mergedNode.children[key];
319
229
  }
320
230
  }
321
231
 
322
- // Augment children recursively
323
232
  if (runtimeNode.children) {
324
233
  if (!mergedNode.children) mergedNode.children = {};
325
234
 
326
235
  for (const key in runtimeNode.children) {
327
236
  const runtimeChild = runtimeNode.children[key];
328
237
 
329
- // For iterations: augment existing node, don't replace
330
238
  if (runtimeChild.type === 'iteration') {
331
239
  const hyperspeedChild = mergedNode.children[key];
332
240
 
333
241
  if (hyperspeedChild) {
334
- // Preserve compiled data from hyperspeed
335
242
  const compiledData = hyperspeedChild.compiled;
336
243
 
337
- // Update meta and runtime from runtime node
338
244
  mergedNode.children[key].meta = runtimeChild.meta;
339
245
  mergedNode.children[key].runtime = runtimeChild.runtime;
340
246
 
341
- // Keep compiled data from hyperspeed (it has batchFn)
342
247
  if (compiledData) {
343
248
  mergedNode.children[key].compiled = compiledData;
344
249
  }
345
250
  } else {
346
- // No hyperspeed node, just use runtime
347
251
  mergedNode.children[key] = runtimeChild;
348
252
  }
349
253
  continue;
350
254
  }
351
255
 
352
- // For conditionals: use runtime node but preserve compiled data
353
256
  if (runtimeChild.type === 'conditional') {
354
257
  const hyperspeedChild = mergedNode.children[key];
355
258
  const compiledData = hyperspeedChild?.compiled;
356
259
 
357
260
  mergedNode.children[key] = runtimeChild;
358
261
 
359
- // Restore compiled data if it existed
360
262
  if (compiledData) {
361
263
  mergedNode.children[key].compiled = compiledData;
362
264
  }
@@ -364,20 +266,16 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
364
266
  }
365
267
 
366
268
  if (mergedNode.children[key]) {
367
- // Child exists in both - augment it
368
269
  augmentWithRuntime(mergedNode.children[key], runtimeNode.children[key]);
369
270
  } else {
370
- // Child only in runtime - add it (dynamic element discovered at runtime)
371
271
  mergedNode.children[key] = runtimeNode.children[key];
372
272
  }
373
273
  }
374
274
  }
375
275
  };
376
276
 
377
- // Augment hyperspeed foundation with runtime data
378
277
  augmentWithRuntime(merged, runtimeTree);
379
278
 
380
- // Preserve stats from runtime (hyperspeed won't have stats)
381
279
  if (runtimeTree.stats) {
382
280
  merged.stats = runtimeTree.stats;
383
281
  }
@@ -385,7 +283,6 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
385
283
  return merged;
386
284
  };
387
285
 
388
- // Store previous state for comparison (needs to be accessible by core loop)
389
286
  let previousState = {};
390
287
 
391
288
  const main = (s, config = {}, stringSelector = '') => {
@@ -393,26 +290,19 @@ const main = (s, config = {}, stringSelector = '') => {
393
290
  const verbose = !!config?.verbose;
394
291
  ((globalThis.__vibe ??= {}).debug = debug);
395
292
 
396
- // Enable/disable the component template cache from config (`{ noCache }`).
397
293
  configureComponentCache(config);
398
294
 
399
- // Expose the global `$scope` resolver used by loop-scoped `on*` handlers.
400
295
  installScopeResolver();
401
296
 
402
- // Detect if running in compiler's headless browser
403
- // When true: skip cleanup to preserve [vibe] attribute in compiled HTML
404
297
  const isCompiling = typeof window !== 'undefined' && window.__vibe?.compiling === true;
405
298
 
406
- // Reset previous state for each new instance
407
299
  previousState = {};
408
300
 
409
- // Use page-specific hyperspeed manifest (detected at module load)
410
301
  const hyperspeedTree = hyperspeedManifest;
411
302
 
412
303
  let rootElement = document.body;
413
304
 
414
305
  if (stringSelector) {
415
- // Find element(s) with the specified attribute
416
306
  const elements = document.querySelectorAll(`${stringSelector}`);
417
307
  if (elements?.[0]) {
418
308
  rootElement = elements[0];
@@ -421,16 +311,10 @@ const main = (s, config = {}, stringSelector = '') => {
421
311
 
422
312
  debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
423
313
 
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
314
  let hyperspeedSubtree = null;
430
315
  if (hyperspeedTree) {
431
316
  const manifestName = hyperspeedPath ? hyperspeedPath.split('/').pop() : 'manifest.js';
432
317
 
433
- // Count compiled features
434
318
  const countCompiledIterations = (tree) => {
435
319
  let count = 0;
436
320
  if (tree.type === 'iteration' && tree.compiled?.iterations?.batchFn) count++;
@@ -444,7 +328,6 @@ const main = (s, config = {}, stringSelector = '') => {
444
328
 
445
329
  const compiledIterationCount = countCompiledIterations(hyperspeedTree);
446
330
 
447
- // Log features that are enabled
448
331
  debugLog(PHASE_HYPERSPEED, `Loaded ${manifestName}, page is pre-compiled`, debug);
449
332
 
450
333
  if (compiledIterationCount > 0) {
@@ -461,17 +344,13 @@ const main = (s, config = {}, stringSelector = '') => {
461
344
  );
462
345
  }
463
346
 
464
- // Find matching subtree by DOM path (not just tag name)
465
- // Build path from document to rootElement
466
347
  const buildDomPath = (element) => {
467
348
  const path = [];
468
349
  let current = element;
469
350
 
470
- // Walk up to document itself (include html in the path)
471
351
  while (current && current.parentNode && current !== document) {
472
352
  const parent = current.parentNode;
473
353
 
474
- // Skip document node itself
475
354
  if (parent === document) {
476
355
  const siblings = Array.from(document.childNodes);
477
356
  const index = siblings.indexOf(current);
@@ -491,19 +370,16 @@ const main = (s, config = {}, stringSelector = '') => {
491
370
  return path;
492
371
  };
493
372
 
494
- // Walk manifest tree following DOM path
495
373
  const findManifestNodeByPath = (manifest, path) => {
496
374
  let current = manifest;
497
375
 
498
376
  for (const { tag, index } of path) {
499
377
  if (!current || !current.children) return null;
500
378
 
501
- // Look for matching child by tag_index pattern
502
379
  const key = `${tag}_${index}`;
503
380
  if (current.children[key]) {
504
381
  current = current.children[key];
505
382
  } else {
506
- // Fallback: search all children for matching tag at this level
507
383
  let found = false;
508
384
  for (const childKey in current.children) {
509
385
  if (childKey.startsWith(tag + '_')) {
@@ -523,36 +399,27 @@ const main = (s, config = {}, stringSelector = '') => {
523
399
  let subtreeFromManifest = findManifestNodeByPath(hyperspeedTree, domPath);
524
400
 
525
401
  if (!subtreeFromManifest) {
526
- // Fallback to root if path matching fails
527
402
  subtreeFromManifest = hyperspeedTree;
528
403
  }
529
404
 
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
405
+ hyperspeedSubtree = deepCloneNode(subtreeFromManifest);
406
+ const cloneForRestoration = deepCloneNode(subtreeFromManifest);
534
407
 
535
408
  restoreMarkersFromManifest(rootElement, cloneForRestoration, hyperspeedTree);
536
409
  }
537
410
 
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
411
  rootElement.querySelectorAll('component[src], div.component[src]').forEach(el => {
542
412
  el._vibeSlotContent = el.innerHTML;
543
413
  });
544
414
 
545
- // Runtime parses DOM (which now has restored markers if hyperspeed was used)
546
415
  let parsedTree = parse(rootElement);
547
416
 
548
- // Merge with hyperspeed if available (hyperspeed as foundation, runtime augments)
549
417
  if (hyperspeedSubtree) {
550
418
  parsedTree = mergeManifests(hyperspeedSubtree, parsedTree);
551
419
  }
552
420
 
553
421
  let manifest = createManifest(parsedTree);
554
422
 
555
- // Build hyperspeed manifest (before hydration, extract markers from parsed strings)
556
423
  const hyperspeedManifestData = buildHyperspeedManifest(parsedTree);
557
424
 
558
425
  const manifestEntries = Object.entries(manifest);
@@ -582,7 +449,6 @@ const main = (s, config = {}, stringSelector = '') => {
582
449
  debug,
583
450
  );
584
451
 
585
- // Lifecycle hooks that users can subscribe to
586
452
  const hooks = {
587
453
  afterUpdate: [],
588
454
  afterDomMutation: [],
@@ -590,25 +456,14 @@ const main = (s, config = {}, stringSelector = '') => {
590
456
  unmount: [],
591
457
  };
592
458
 
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
459
  let readyFired = false;
598
460
 
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
461
  if (typeof window !== 'undefined') {
605
462
  window.addEventListener('pagehide', () => {
606
463
  hooks.unmount.forEach((callback) => callback());
607
464
  });
608
465
  }
609
466
 
610
- // Extract plain values from proxy (removes proxy wrappers)
611
- // Optimized: indexed loops, Object.keys (no prototype walk), inline primitive check
612
467
  const extractPlainValue = (obj) => {
613
468
  if (obj === null || typeof obj !== 'object') return obj;
614
469
  if (Array.isArray(obj)) {
@@ -631,14 +486,7 @@ const main = (s, config = {}, stringSelector = '') => {
631
486
  return plain;
632
487
  };
633
488
 
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
489
  const patchWithObserverPaused = (patch) => {
641
- // Capture pending mutations before disconnecting (takeRecords clears the queue)
642
490
  let pendingMutations = [];
643
491
  if (observer) {
644
492
  pendingMutations = observer.takeRecords();
@@ -655,14 +503,11 @@ const main = (s, config = {}, stringSelector = '') => {
655
503
  subtree: true,
656
504
  });
657
505
 
658
- // Process mutations that were pending before we disconnected
659
506
  if (pendingMutations.length > 0 && processMutations) {
660
507
  processMutations(pendingMutations);
661
508
  }
662
509
  }
663
510
 
664
- // The patch may have mounted new DOM while the observer was disconnected.
665
- // Scan for unresolved <component src=""> elements that need fetching.
666
511
  if (componentProcessingStarted) {
667
512
  const componentConfig = {
668
513
  ...config,
@@ -675,17 +520,8 @@ const main = (s, config = {}, stringSelector = '') => {
675
520
  };
676
521
 
677
522
  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
523
  const currentState = { ...previousState };
682
524
  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
525
  if (prop in $) {
690
526
  currentState[prop] = extractPlainValue($[prop]);
691
527
  } else {
@@ -693,11 +529,6 @@ const main = (s, config = {}, stringSelector = '') => {
693
529
  }
694
530
  }
695
531
 
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
532
  const dirty = beginFlush(changedProps);
702
533
  if (dirty.size > 0) {
703
534
  debugLog(PHASE_UPDATE, 'state changed', debug);
@@ -706,30 +537,13 @@ const main = (s, config = {}, stringSelector = '') => {
706
537
  });
707
538
  }
708
539
 
709
- // Store previous state for hooks (currentState is already plain, no need to clone)
710
540
  const prev = previousState;
711
541
  previousState = currentState;
712
542
  hooks.afterUpdate.forEach((callback) => callback(currentState, prev));
713
543
  });
714
544
 
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
545
  const lateReady = [];
729
546
  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
547
  const mountsSettled = () =>
734
548
  !document.querySelector('[vibe-staged], [vibe-fouc]') && shouldCleanup(rootElement);
735
549
  const flushLateReady = () => {
@@ -770,46 +584,23 @@ const main = (s, config = {}, stringSelector = '') => {
770
584
  configurable: true,
771
585
  });
772
586
 
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
587
  let resolveReady;
777
588
  Object.defineProperty($, 'ready', {
778
589
  value: new Promise((resolve) => { resolveReady = resolve; }),
779
590
  enumerable: false,
780
591
  });
781
592
 
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
593
  Object.defineProperty($, 'reconcile', {
785
594
  value: reconcile,
786
595
  enumerable: false,
787
596
  });
788
597
 
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
598
  Object.defineProperty($, '_register', {
798
599
  value: (componentId, componentState) => {
799
600
  if (componentId in $) {
800
601
  $[componentId] = componentState;
801
602
  return componentId;
802
603
  }
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
604
  silentSet($, componentId, componentState);
814
605
  return componentId;
815
606
  },
@@ -817,80 +608,35 @@ const main = (s, config = {}, stringSelector = '') => {
817
608
  configurable: true,
818
609
  });
819
610
 
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
611
  Object.defineProperty($, 'unsafe', {
825
612
  value: unsafe,
826
613
  enumerable: false,
827
614
  configurable: true,
828
615
  });
829
616
 
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
617
  Object.defineProperty($, '_renderComponent', {
836
618
  value: renderComponentTemplate,
837
619
  enumerable: false,
838
620
  });
839
621
 
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
622
  Object.defineProperty($, 'clearComponentCache', {
846
623
  value: clearComponentCache,
847
624
  enumerable: false,
848
625
  configurable: true,
849
626
  });
850
627
 
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
628
  Object.defineProperty(manifest, '__live', { value: $, enumerable: false, configurable: true });
857
629
 
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
630
  Object.defineProperty(manifest, '__tree', { value: parsedTree, enumerable: false, configurable: true });
865
631
 
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
632
  setRootState($);
870
633
 
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
634
  if (typeof window !== 'undefined') window.$ = $;
880
635
 
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
636
  let compiledScriptsDone = true;
890
637
  const compiledScriptsPending = executeCompiledComponentScripts();
891
638
  if (compiledScriptsPending) compiledScriptsDone = false;
892
639
 
893
- // Initial hydration - pass plain values so iteration can do reference comparison
894
640
  const initialState = extractPlainValue($);
895
641
  const affectedElements = affected(parsedTree, initialState, initialState);
896
642
 
@@ -907,12 +653,8 @@ const main = (s, config = {}, stringSelector = '') => {
907
653
  debug,
908
654
  );
909
655
 
910
- // Hydrate with proxy $ so DOM bindings work
911
- // Pass initialState as oldState for iterations (won't actually update, just initial render)
912
656
  hydrate(affectedElements, $, manifest, initialState);
913
657
 
914
- // Render all iterations and conditionals after initial hydration
915
- // Use initialState (plain values) for iteration rendering so reference comparison works
916
658
  const iterationCount = renderAllIterations(parsedTree, initialState, manifest);
917
659
  if (iterationCount > 0)
918
660
  debugLog(
@@ -943,35 +685,28 @@ const main = (s, config = {}, stringSelector = '') => {
943
685
  debug,
944
686
  );
945
687
 
946
- // After all initial rendering, capture a clean snapshot for comparison
947
688
  previousState = extractPlainValue($);
948
689
 
949
- // Observer reference and processMutations
950
690
  let observer = null;
951
691
  let processMutations = null;
952
692
 
953
- // Define observer callback as named function so we can call it manually for pending mutations
954
693
  processMutations = (mutations) => {
955
- // Early exit if no mutations to process (common case)
956
694
  if (mutations.length === 0) return;
957
695
 
958
696
  const manifestSizeBefore = debug ? Object.keys(manifest).length : 0;
959
697
  let hadChanges = false;
960
- let parsedParents = null; // Lazy init - only create Set when needed
698
+ let parsedParents = null;
961
699
  let addedElements = 0;
962
700
  let addedNodes = 0;
963
701
  let removedElements = 0;
964
702
  let removedNodes = 0;
965
- let singleElement = null; // Track the single element when count is 1
966
- let totalSkipped = 0; // Track skipped elements from parse
703
+ let singleElement = null;
704
+ let totalSkipped = 0;
967
705
  let hydratedCount = 0;
968
706
  let iteratedCount = 0;
969
707
  let evaluatedCount = 0;
970
- let addedElementsList = []; // Track all added elements for verbose output
708
+ let addedElementsList = [];
971
709
 
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
710
  const removedComponentIds = new Set();
976
711
  mutations.forEach(({ removedNodes: removedNodesList }) => {
977
712
  removedNodesList.forEach((node) => collectComponentIds(node, removedComponentIds));
@@ -979,21 +714,12 @@ const main = (s, config = {}, stringSelector = '') => {
979
714
 
980
715
  mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
981
716
  removedNodesList.forEach((node) => {
982
- // If this is a component element with pending fetch, abort it
983
717
  if (node.nodeName === 'COMPONENT') {
984
718
  abortComponentFetch(node);
985
719
  }
986
720
 
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
721
  const branchRef = branchNodeRegistry.get(node);
991
722
  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
723
  const replacement =
998
724
  Array.from(addedNodesList).find(n => n.parentNode === target) ||
999
725
  (node._vibeReplacedBy?.isConnected ? node._vibeReplacedBy : null);
@@ -1006,30 +732,19 @@ const main = (s, config = {}, stringSelector = '') => {
1006
732
 
1007
733
  const dotAnnotation = manifestPathOf(manifest, node);
1008
734
 
1009
- // Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
1010
735
  if (dotAnnotation === null) return;
1011
736
 
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
737
  removeManifestSubtree(manifest, dotAnnotation);
1017
738
 
1018
739
  const dotPath = dotAnnotation.split('.');
1019
740
  const name = dotPath.pop();
1020
741
  const parentDotAnnotation = dotPath.join('.');
1021
742
 
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
743
  const picked =
1027
744
  navigateTree(parsedTree, parentDotAnnotation) || findNodeByElement(parsedTree, target);
1028
745
 
1029
- // If we can't navigate to the parent, skip
1030
746
  if (!picked || !picked.element) return;
1031
747
 
1032
- // Update parent's parsed HTML (only once per parent)
1033
748
  if (!parsedParents) parsedParents = new Set();
1034
749
  if (!parsedParents.has(picked)) {
1035
750
  const { parsed } = parse(picked.element);
@@ -1037,10 +752,8 @@ const main = (s, config = {}, stringSelector = '') => {
1037
752
  parsedParents.add(picked);
1038
753
  }
1039
754
 
1040
- // Remove the node from parent's children
1041
755
  delete picked.children[name];
1042
756
 
1043
- // Count elements vs nodes separately
1044
757
  if (node.nodeName.startsWith('#')) {
1045
758
  removedNodes++;
1046
759
  } else {
@@ -1051,16 +764,10 @@ const main = (s, config = {}, stringSelector = '') => {
1051
764
  });
1052
765
 
1053
766
  addedNodesList.forEach((node) => {
1054
- // Skip nodes that aren't element nodes, are non-reactive, or lack Vibe syntax
1055
767
  if (!shouldProcessNode(node)) {
1056
768
  return;
1057
769
  }
1058
770
 
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
771
  if (node.nodeType === 1) {
1065
772
  node.querySelectorAll('component[src], div.component[src]').forEach((el) => {
1066
773
  if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
@@ -1069,23 +776,13 @@ const main = (s, config = {}, stringSelector = '') => {
1069
776
 
1070
777
  const dotAnnotation = manifestPathOf(manifest, target);
1071
778
 
1072
- // Parse the newly added node
1073
779
  const parsedNode = parse(node);
1074
780
 
1075
- // Accumulate skipped stats
1076
781
  if (parsedNode.stats?.skipped) {
1077
782
  totalSkipped += parsedNode.stats.skipped;
1078
783
  }
1079
784
 
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
785
  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
786
  const picked =
1090
787
  navigateTree(parsedTree, dotAnnotation) || findNodeByElement(parsedTree, target);
1091
788
 
@@ -1094,15 +791,12 @@ const main = (s, config = {}, stringSelector = '') => {
1094
791
  picked.element = target;
1095
792
  }
1096
793
 
1097
- // Parse the newly added node (use monotonically increasing counter for deterministic key)
1098
- // Initialize counter if it doesn't exist
1099
794
  if (!picked._nextChildIndex) {
1100
795
  picked._nextChildIndex = Object.keys(picked.children).length;
1101
796
  }
1102
797
  const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
1103
- picked._nextChildIndex++; // Always increment, never decrement
798
+ picked._nextChildIndex++;
1104
799
 
1105
- // Update parent's parsed HTML (only once per parent)
1106
800
  if (!parsedParents) parsedParents = new Set();
1107
801
  if (!parsedParents.has(picked)) {
1108
802
  const { parsed } = parse(picked.element);
@@ -1110,34 +804,21 @@ const main = (s, config = {}, stringSelector = '') => {
1110
804
  parsedParents.add(picked);
1111
805
  }
1112
806
 
1113
- // Add the parsed node to parent's children
1114
807
  picked.children[name] = parsedNode;
1115
808
 
1116
- // Recursively add node and all descendants to manifest
1117
809
  addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
1118
810
  }
1119
811
  }
1120
812
 
1121
- // Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
1122
813
  const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
1123
814
  hydratedCount += counts.hydratedCount;
1124
815
  iteratedCount += counts.iteratedCount;
1125
816
  evaluatedCount += counts.evaluatedCount;
1126
817
 
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
818
  if (node.nodeType === 1 && node._vibeIterPropExprs) {
1137
819
  node._vibeIterTree = parsedNode;
1138
820
  }
1139
821
 
1140
- // Count elements vs nodes separately
1141
822
  if (node.nodeName.startsWith('#')) {
1142
823
  addedNodes++;
1143
824
  } else {
@@ -1149,23 +830,18 @@ const main = (s, config = {}, stringSelector = '') => {
1149
830
  });
1150
831
  });
1151
832
 
1152
- // CLEANUP OF CURRENT STATE
1153
833
  releaseOrphanedComponentState(removedComponentIds);
1154
834
  let hadRemovals = false;
1155
835
  mutations.forEach(({ removedNodes: removedNodesList }) => {
1156
836
  if (removedNodesList.length > 0) hadRemovals = true;
1157
837
  releaseOrphanedIterationProps(removedNodesList);
1158
838
  });
1159
- // Subscribers anchored in the removed subtrees are dead — drop them from
1160
- // the reverse index (observed removals: page swaps, reconcile, app code).
1161
839
  if (hadRemovals) pruneDisconnected();
1162
840
 
1163
- // Fire hooks once after all mutations are processed (not per-node)
1164
841
  if (hadChanges) {
1165
842
  if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
1166
843
  const segments = [];
1167
844
 
1168
- // Added
1169
845
  if (addedElements > 0) {
1170
846
  segments.push({ text: `+${addedElements}`, color: 'green' });
1171
847
  segments.push({
@@ -1179,7 +855,6 @@ const main = (s, config = {}, stringSelector = '') => {
1179
855
  segments.push({ text: ` ${addedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
1180
856
  }
1181
857
 
1182
- // Removed
1183
858
  if (removedElements > 0) {
1184
859
  if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
1185
860
  segments.push({ text: `-${removedElements}`, color: 'red' });
@@ -1195,25 +870,15 @@ const main = (s, config = {}, stringSelector = '') => {
1195
870
  segments.push({ text: ` ${removedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
1196
871
  }
1197
872
 
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
873
  debugLog(PHASE_MUTATE, segments, debug);
1203
874
 
1204
- // Verbose: log the added element (or topmost parent if multiple)
1205
875
  if (verbose && addedElementsList.length > 0) {
1206
- // Find the topmost parent among added elements
1207
876
  const topmostParent = addedElementsList.find((el) => {
1208
- // Check if this element is NOT a descendant of any other element in the list
1209
877
  return !addedElementsList.some((other) => other !== el && other.contains(el));
1210
878
  });
1211
879
  debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
1212
880
  }
1213
881
 
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
882
  const manifestSizeAfter = debug ? Object.keys(manifest).length : manifestSizeBefore;
1218
883
  const totalNodes = manifestSizeAfter - manifestSizeBefore;
1219
884
 
@@ -1240,7 +905,6 @@ const main = (s, config = {}, stringSelector = '') => {
1240
905
 
1241
906
  debugLog(PHASE_PARSE, parseSegments, debug);
1242
907
 
1243
- // Log hydration after parse
1244
908
  if (hydratedCount > 0) {
1245
909
  debugLog(
1246
910
  PHASE_HYDRATE,
@@ -1256,7 +920,6 @@ const main = (s, config = {}, stringSelector = '') => {
1256
920
  );
1257
921
  }
1258
922
 
1259
- // Log iterations after hydration
1260
923
  if (iteratedCount > 0) {
1261
924
  debugLog(
1262
925
  PHASE_ITERATE,
@@ -1272,7 +935,6 @@ const main = (s, config = {}, stringSelector = '') => {
1272
935
  );
1273
936
  }
1274
937
 
1275
- // Log conditionals after iterations
1276
938
  if (evaluatedCount > 0) {
1277
939
  debugLog(
1278
940
  PHASE_CONDITION,
@@ -1291,12 +953,8 @@ const main = (s, config = {}, stringSelector = '') => {
1291
953
  }
1292
954
  }
1293
955
 
1294
- // Always call hooks, even if no changes detected (cleanup needs to check)
1295
956
  hooks.afterDomMutation.forEach((callback) => callback());
1296
957
 
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
958
  if (componentProcessingStarted) {
1301
959
  const componentConfig = {
1302
960
  ...config,
@@ -1307,7 +965,6 @@ const main = (s, config = {}, stringSelector = '') => {
1307
965
  processComponent(
1308
966
  rootElement,
1309
967
  () => {
1310
- // When all components are done, run cleanup check
1311
968
  checkCleanup();
1312
969
  },
1313
970
  componentConfig,
@@ -1328,34 +985,21 @@ const main = (s, config = {}, stringSelector = '') => {
1328
985
 
1329
986
  debugLog(PHASE_OBSERVE, `MutationObserver listening to DOM changes`, debug);
1330
987
 
1331
- // Track cleanup state
1332
988
  let componentProcessingStarted = false;
1333
989
  let cleanupExecuted = false;
1334
990
  let bootSettleDone = false;
1335
991
 
1336
- // Check if cleanup should run
1337
992
  const checkCleanup = () => {
1338
993
  if (cleanupExecuted || isCompiling || !compiledScriptsDone) return;
1339
994
 
1340
- // Check for pending mutations first
1341
995
  const pendingMutations = observer ? observer.takeRecords() : [];
1342
996
 
1343
997
  if (pendingMutations.length > 0) {
1344
- // More mutations to process
1345
998
  processMutations(pendingMutations);
1346
999
  return;
1347
1000
  }
1348
1001
 
1349
- // Check if all processing is complete
1350
1002
  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
1003
  if (!bootSettleDone) {
1360
1004
  bootSettleDone = true;
1361
1005
  renderAllIterations(parsedTree, extractPlainValue($), manifest);
@@ -1364,7 +1008,6 @@ const main = (s, config = {}, stringSelector = '') => {
1364
1008
  cleanup(rootElement, debug);
1365
1009
  cleanupExecuted = true;
1366
1010
 
1367
- // Fire ready hook after cleanup completes
1368
1011
  readyFired = true;
1369
1012
  hooks.ready.forEach((callback) => {
1370
1013
  try {
@@ -1373,35 +1016,22 @@ const main = (s, config = {}, stringSelector = '') => {
1373
1016
  console.error('[vibe] Error in ready hook:', error);
1374
1017
  }
1375
1018
  });
1376
- // Resolve $.ready promise after all ready callbacks have run
1377
1019
  resolveReady();
1378
1020
  }
1379
1021
  };
1380
1022
 
1381
- // Register hook to check for cleanup readiness after each mutation batch
1382
1023
  hooks.afterDomMutation.push(checkCleanup);
1383
1024
 
1384
- // Async compiled component scripts (imports) finish after boot — unlock the
1385
- // ready gate and re-check once their state has merged into `$`.
1386
1025
  if (compiledScriptsPending) {
1387
1026
  compiledScriptsPending.then(() => {
1388
1027
  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
1028
  settleConditionals(parsedTree, $, manifest);
1397
1029
  checkCleanup();
1398
1030
  });
1399
1031
  }
1400
1032
 
1401
- // Process component elements after initialization - MutationObserver will handle hydration
1402
1033
  componentProcessingStarted = true;
1403
1034
 
1404
- // Pass observer and processMutations to component for sync processing
1405
1035
  const componentConfig = {
1406
1036
  ...config,
1407
1037
  _forceSync: true,
@@ -1412,14 +1042,11 @@ const main = (s, config = {}, stringSelector = '') => {
1412
1042
  processComponent(
1413
1043
  rootElement,
1414
1044
  () => {
1415
- // When all components are done, run cleanup check
1416
1045
  checkCleanup();
1417
1046
  },
1418
1047
  componentConfig,
1419
1048
  );
1420
1049
 
1421
- // Export hyperspeed manifest globally for compiler extraction and optimizations
1422
- // Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
1423
1050
  if (typeof window !== 'undefined') {
1424
1051
  const manifestToExport = hyperspeedTree || hyperspeedManifestData;
1425
1052
  (window.__vibe ??= {}).manifest = manifestToExport;