@ape-egg/vibe 1.8.0 → 1.9.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
@@ -4,7 +4,7 @@ import createManifest from './manifest.js';
4
4
  import hydrate from './hydrate.js';
5
5
  import affected from './affected.js';
6
6
  import { deepMerge, hash } from './utils.js';
7
- import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
7
+ import { renderAllIterations, setRenderAllConditionals, releaseOrphanedIterationProps } from './iterate.js';
8
8
  import { renderAllConditionals, branchNodeRegistry, managedNodes } from './conditionals.js';
9
9
  import {
10
10
  NON_REACTIVE_ELEMENTS,
@@ -21,9 +21,10 @@ import {
21
21
  PHASE_READY,
22
22
  DEHYDRATE_CLASS_OR_ATTR,
23
23
  } from './constants.js';
24
- import { processComponent, abortComponentFetch } from './component.js';
24
+ import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate } from './component.js';
25
25
  import { debugLog } from './debug.js';
26
26
  import { shouldCleanup, cleanup } from './cleanup.js';
27
+ import { reconcile } from './reconcile.js';
27
28
  import {
28
29
  buildHyperspeedManifest,
29
30
  hyperspeedManifest,
@@ -617,6 +618,32 @@ const main = (s, config = {}, stringSelector = '') => {
617
618
  enumerable: false,
618
619
  });
619
620
 
621
+ // Promise that resolves after the ready hook fires and all ready callbacks
622
+ // have run. Lets late subscribers await readiness without missing the event:
623
+ // `await $.ready`. Non-enumerable so it won't leak into state snapshots.
624
+ let resolveReady;
625
+ Object.defineProperty($, 'ready', {
626
+ value: new Promise((resolve) => { resolveReady = resolve; }),
627
+ enumerable: false,
628
+ });
629
+
630
+ // Reconcile a managed subtree against new source HTML. Opt-in entry point;
631
+ // dormant unless called (so hot paths and benchmarks are unaffected).
632
+ Object.defineProperty($, 'reconcile', {
633
+ value: reconcile,
634
+ enumerable: false,
635
+ });
636
+
637
+ // Pure-render path for surgical component HMR. Given raw component template
638
+ // HTML, callsite props, slot HTML, and existing componentIds, returns the
639
+ // processed HTML string the plugin's HMR handler can hand to $.reconcile.
640
+ // Scripts are NOT executed — callers use this only when they've verified
641
+ // script contents haven't changed (so registered state is still valid).
642
+ Object.defineProperty($, 'renderComponent', {
643
+ value: renderComponentTemplate,
644
+ enumerable: false,
645
+ });
646
+
620
647
  // Initial hydration - pass plain values so iteration can do reference comparison
621
648
  const initialState = extractPlainValue($);
622
649
  const affectedElements = affected(parsedTree, initialState, initialState);
@@ -696,6 +723,14 @@ const main = (s, config = {}, stringSelector = '') => {
696
723
  let evaluatedCount = 0;
697
724
  let addedElementsList = []; // Track all added elements for verbose output
698
725
 
726
+ // Collect data-vibe-component-id values across ALL removed subtrees in this
727
+ // batch before doing any per-node work, so we can evict their state after
728
+ // the DOM mutations have been applied.
729
+ const removedComponentIds = new Set();
730
+ mutations.forEach(({ removedNodes: removedNodesList }) => {
731
+ removedNodesList.forEach((node) => collectComponentIds(node, removedComponentIds));
732
+ });
733
+
699
734
  mutations.forEach(({ addedNodes: addedNodesList, removedNodes: removedNodesList, target }) => {
700
735
  removedNodesList.forEach((node) => {
701
736
  // If this is a component element with pending fetch, abort it
@@ -761,29 +796,20 @@ const main = (s, config = {}, stringSelector = '') => {
761
796
  return;
762
797
  }
763
798
 
764
- const entry = Object.entries(manifest).find(([_, element]) => element === target);
765
-
766
- // If parent isn't tracked, this node is outside the reactive scope
767
- if (!entry) return;
768
-
769
- const [dotAnnotation] = entry;
770
- const picked = navigateTree(parsedTree, dotAnnotation);
771
-
772
- // If we can't navigate to the parent in the tree, skip
773
- if (!picked) return;
774
-
775
- // If parent has no element reference, re-parse from the actual DOM element
776
- if (!picked.element) {
777
- picked.element = target;
799
+ // Capture raw slot content of nested <component src> elements BEFORE parse runs.
800
+ // Parse creates conditional nodes from <!-- if --> comments, and renderConditional
801
+ // later removes the template nodes between the comments. Without capturing slot
802
+ // content first, conditionals inside a component's slot content lose their branch
803
+ // templates, breaking reactive updates.
804
+ if (node.nodeType === 1) {
805
+ node.querySelectorAll('component[src], div.component[src]').forEach((el) => {
806
+ if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
807
+ });
778
808
  }
779
809
 
780
- // Parse the newly added node (use monotonically increasing counter for deterministic key)
781
- // Initialize counter if it doesn't exist
782
- if (!picked._nextChildIndex) {
783
- picked._nextChildIndex = Object.keys(picked.children).length;
784
- }
785
- const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
786
- picked._nextChildIndex++; // Always increment, never decrement
810
+ const entry = Object.entries(manifest).find(([_, element]) => element === target);
811
+
812
+ // Parse the newly added node
787
813
  const parsedNode = parse(node);
788
814
 
789
815
  // Accumulate skipped stats
@@ -791,19 +817,42 @@ const main = (s, config = {}, stringSelector = '') => {
791
817
  totalSkipped += parsedNode.stats.skipped;
792
818
  }
793
819
 
794
- // Update parent's parsed HTML (only once per parent)
795
- if (!parsedParents) parsedParents = new Set();
796
- if (!parsedParents.has(picked)) {
797
- const { parsed } = parse(picked.element);
798
- picked.parsed = parsed;
799
- parsedParents.add(picked);
800
- }
820
+ // If parent is tracked in the manifest, register this new node in the parsed tree.
821
+ // (If not e.g. mutations inside an iteration instance whose rows aren't in the
822
+ // global manifest — we still hydrate the node below; we just skip tree/manifest
823
+ // registration since there's no tree branch to attach to.)
824
+ if (entry) {
825
+ const [dotAnnotation] = entry;
826
+ const picked = navigateTree(parsedTree, dotAnnotation);
827
+
828
+ if (picked) {
829
+ if (!picked.element) {
830
+ picked.element = target;
831
+ }
801
832
 
802
- // Add the parsed node to parent's children
803
- picked.children[name] = parsedNode;
833
+ // Parse the newly added node (use monotonically increasing counter for deterministic key)
834
+ // Initialize counter if it doesn't exist
835
+ if (!picked._nextChildIndex) {
836
+ picked._nextChildIndex = Object.keys(picked.children).length;
837
+ }
838
+ const name = `${node.nodeName.toLowerCase()}_${picked._nextChildIndex}`;
839
+ picked._nextChildIndex++; // Always increment, never decrement
840
+
841
+ // Update parent's parsed HTML (only once per parent)
842
+ if (!parsedParents) parsedParents = new Set();
843
+ if (!parsedParents.has(picked)) {
844
+ const { parsed } = parse(picked.element);
845
+ picked.parsed = parsed;
846
+ parsedParents.add(picked);
847
+ }
804
848
 
805
- // Recursively add node and all descendants to manifest
806
- addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
849
+ // Add the parsed node to parent's children
850
+ picked.children[name] = parsedNode;
851
+
852
+ // Recursively add node and all descendants to manifest
853
+ addToManifest(parsedNode, manifest, `${dotAnnotation}.${name}`);
854
+ }
855
+ }
807
856
 
808
857
  // Run core loop for the new node (parse already done, hydrate → conditionals → iterate)
809
858
  const counts = processCoreLoop(node, parsedNode, $, manifest, true, debug);
@@ -823,6 +872,12 @@ const main = (s, config = {}, stringSelector = '') => {
823
872
  });
824
873
  });
825
874
 
875
+ // CLEANUP OF CURRENT STATE
876
+ releaseOrphanedComponentState(removedComponentIds);
877
+ mutations.forEach(({ removedNodes: removedNodesList }) => {
878
+ releaseOrphanedIterationProps(removedNodesList);
879
+ });
880
+
826
881
  // Fire hooks once after all mutations are processed (not per-node)
827
882
  if (hadChanges) {
828
883
  if (addedElements > 0 || addedNodes > 0 || removedElements > 0 || removedNodes > 0) {
@@ -1019,6 +1074,8 @@ const main = (s, config = {}, stringSelector = '') => {
1019
1074
  console.error('[vibe] Error in ready hook:', error);
1020
1075
  }
1021
1076
  });
1077
+ // Resolve $.ready promise after all ready callbacks have run
1078
+ resolveReady();
1022
1079
  }
1023
1080
  };
1024
1081