@ape-egg/vibe 2.1.22 → 3.0.0
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/README.md +112 -5
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +26 -17
- package/llms.txt +36 -5
- package/package.json +20 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +360 -98
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +277 -110
- package/runtime/index.js +189 -65
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +81 -11
- package/runtime/pre-compiled-iterations.js +19 -6
- package/runtime/pre-compiled-manifest.js +13 -4
- package/runtime/staging.js +153 -0
- package/runtime/state.js +31 -0
- package/runtime/tracking.js +173 -0
- package/runtime/utils.js +155 -78
- package/spa.js +206 -0
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1159
- package/ROADMAP.md +0 -397
- package/compiler/bin/vibe-compile.js +0 -121
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +0 -2023
- package/compiler/src/Cargo.toml +0 -38
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
- package/compiler/src/compiler/binding_case.rs +0 -88
- package/compiler/src/compiler/compile.rs +0 -2522
- package/compiler/src/compiler/component_tagger.rs +0 -469
- package/compiler/src/compiler/iteration_optimizer.rs +0 -455
- package/compiler/src/compiler/js_analyzer.rs +0 -715
- package/compiler/src/compiler/manifest_builder.rs +0 -693
- package/compiler/src/compiler/mod.rs +0 -15
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/state_extractor.rs +0 -263
- package/compiler/src/compiler/value_stamper.rs +0 -921
- package/compiler/src/compiler/watcher.rs +0 -1147
- package/compiler/src/config.rs +0 -239
- package/compiler/src/main.rs +0 -347
- package/compiler/src/parser/element.rs +0 -96
- package/compiler/src/parser/html.rs +0 -1004
- package/compiler/src/parser/mod.rs +0 -8
- package/runtime/pre-compiled-manifest.test.mjs +0 -58
- package/runtime/scope.js +0 -50
- package/test-results/.last-run.json +0 -4
package/runtime/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import state from './state.js';
|
|
1
|
+
import state, { silentSet } from './state.js';
|
|
2
2
|
import parse from './parse.js';
|
|
3
|
-
import createManifest from './manifest.js';
|
|
3
|
+
import createManifest, { setManifestEntry, manifestPathOf, removeManifestSubtree } from './manifest.js';
|
|
4
4
|
import hydrate from './hydrate.js';
|
|
5
5
|
import affected from './affected.js';
|
|
6
6
|
import { deepMerge, hash, setRootState } from './utils.js';
|
|
7
7
|
import { unsafe } from './raw-html.js';
|
|
8
8
|
import { renderAllIterations, setRenderAllConditionals, releaseOrphanedIterationProps } from './iterate.js';
|
|
9
|
-
import { renderAllConditionals, branchNodeRegistry, managedNodes } from './conditionals.js';
|
|
9
|
+
import { renderAllConditionals, branchNodeRegistry, managedNodes , settleConditionals} from './conditionals.js';
|
|
10
10
|
import { installScopeResolver } from './loop-scope.js';
|
|
11
11
|
import {
|
|
12
12
|
NON_REACTIVE_ELEMENTS,
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate, executeCompiledComponentScripts } from './component.js';
|
|
27
27
|
import { configureComponentCache, clearComponentCache } from './component-cache.js';
|
|
28
28
|
import { debugLog } from './debug.js';
|
|
29
|
+
import { pruneDisconnected, beginFlush } from './tracking.js';
|
|
30
|
+
import dispatchFlush from './dispatch.js';
|
|
29
31
|
import { shouldCleanup, cleanup } from './cleanup.js';
|
|
30
32
|
import { reconcile } from './reconcile.js';
|
|
31
33
|
import {
|
|
@@ -136,7 +138,7 @@ const ensureNode = (tree, path) => {
|
|
|
136
138
|
|
|
137
139
|
// Recursively add parsed tree nodes to manifest (like createManifest does)
|
|
138
140
|
const addToManifest = (tree, manifest, dotPath) => {
|
|
139
|
-
manifest
|
|
141
|
+
setManifestEntry(manifest, dotPath, tree.element);
|
|
140
142
|
|
|
141
143
|
if (tree.children && Object.keys(tree.children).length > 0) {
|
|
142
144
|
Object.keys(tree.children).forEach((childKey) => {
|
|
@@ -372,7 +374,7 @@ let previousState = {};
|
|
|
372
374
|
const main = (s, config = {}, stringSelector = '') => {
|
|
373
375
|
const debug = !!config?.debug;
|
|
374
376
|
const verbose = !!config?.verbose;
|
|
375
|
-
globalThis.
|
|
377
|
+
((globalThis.__vibe ??= {}).debug = debug);
|
|
376
378
|
|
|
377
379
|
// Enable/disable the component template cache from config (`{ noCache }`).
|
|
378
380
|
configureComponentCache(config);
|
|
@@ -382,7 +384,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
382
384
|
|
|
383
385
|
// Detect if running in compiler's headless browser
|
|
384
386
|
// When true: skip cleanup to preserve [vibe] attribute in compiled HTML
|
|
385
|
-
const isCompiling = typeof window !== 'undefined' && window.
|
|
387
|
+
const isCompiling = typeof window !== 'undefined' && window.__vibe?.compiling === true;
|
|
386
388
|
|
|
387
389
|
// Reset previous state for each new instance
|
|
388
390
|
previousState = {};
|
|
@@ -571,6 +573,12 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
571
573
|
unmount: [],
|
|
572
574
|
};
|
|
573
575
|
|
|
576
|
+
// The ready phase happens once. A listener registered after it fires
|
|
577
|
+
// immediately (parity with the late-safe $.ready promise) — late
|
|
578
|
+
// registration is the SPA norm, where fragment scripts run on mount,
|
|
579
|
+
// long after the shell booted.
|
|
580
|
+
let readyFired = false;
|
|
581
|
+
|
|
574
582
|
// Page-scope 'unmount': the visitor actually leaving — pagehide (navigation
|
|
575
583
|
// away, tab close). Deliberately NOT visibilitychange: a tab switch is not
|
|
576
584
|
// an unmount, the visitor comes back. Inside a component script the same
|
|
@@ -608,6 +616,47 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
608
616
|
|
|
609
617
|
// Observer callback will be defined below (already declared above before processComponent)
|
|
610
618
|
|
|
619
|
+
// Pause the observer around an engine patch phase (its DOM writes are the
|
|
620
|
+
// engine's own), then replay anything that queued while paused and resolve
|
|
621
|
+
// fresh <component src> mounts. Shared by the walk flush and the
|
|
622
|
+
// subscription dispatch flush.
|
|
623
|
+
const patchWithObserverPaused = (patch) => {
|
|
624
|
+
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
625
|
+
let pendingMutations = [];
|
|
626
|
+
if (observer) {
|
|
627
|
+
pendingMutations = observer.takeRecords();
|
|
628
|
+
observer.disconnect();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
patch();
|
|
632
|
+
|
|
633
|
+
if (observer) {
|
|
634
|
+
observer.observe(rootElement, {
|
|
635
|
+
attributes: false,
|
|
636
|
+
characterData: false,
|
|
637
|
+
childList: true,
|
|
638
|
+
subtree: true,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Process mutations that were pending before we disconnected
|
|
642
|
+
if (pendingMutations.length > 0 && processMutations) {
|
|
643
|
+
processMutations(pendingMutations);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// The patch may have mounted new DOM while the observer was disconnected.
|
|
648
|
+
// Scan for unresolved <component src=""> elements that need fetching.
|
|
649
|
+
if (componentProcessingStarted) {
|
|
650
|
+
const componentConfig = {
|
|
651
|
+
...config,
|
|
652
|
+
_forceSync: true,
|
|
653
|
+
_observer: observer,
|
|
654
|
+
_processMutations: processMutations,
|
|
655
|
+
};
|
|
656
|
+
processComponent(rootElement, null, componentConfig);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
611
660
|
const $ = state(s, (changedProps) => {
|
|
612
661
|
// Selective extraction: only extract changed props, preserve references for unchanged.
|
|
613
662
|
// This ensures affected() correctly skips iterations whose arrays didn't change,
|
|
@@ -627,52 +676,17 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
627
676
|
}
|
|
628
677
|
}
|
|
629
678
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
679
|
+
// THE update path: the flush is served from the subscription reverse
|
|
680
|
+
// index — a write notifies exactly its subscribers, O(change). The walk
|
|
681
|
+
// (`affected()`) exists only as the MOUNT path now: first hydration,
|
|
682
|
+
// processMutations, fresh branches and rows, where it registers new
|
|
683
|
+
// subscribers via their first evaluation.
|
|
684
|
+
const dirty = beginFlush(changedProps);
|
|
685
|
+
if (dirty.size > 0) {
|
|
634
686
|
debugLog(PHASE_UPDATE, 'state changed', debug);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (observer) {
|
|
639
|
-
pendingMutations = observer.takeRecords();
|
|
640
|
-
observer.disconnect();
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Run core loop on the full tree (hydrate will use affected list)
|
|
644
|
-
// isNewNode = false because this is a state update, not a DOM mutation
|
|
645
|
-
hydrate(affectedElements, currentState, manifest, previousState);
|
|
646
|
-
|
|
647
|
-
// After hydrate, check for conditional/iteration changes
|
|
648
|
-
const conditionalCount = renderAllConditionals(parsedTree, currentState, manifest);
|
|
649
|
-
const iterationCount = renderAllIterations(parsedTree, currentState, manifest);
|
|
650
|
-
|
|
651
|
-
if (observer) {
|
|
652
|
-
observer.observe(rootElement, {
|
|
653
|
-
attributes: false,
|
|
654
|
-
characterData: false,
|
|
655
|
-
childList: true,
|
|
656
|
-
subtree: true,
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// Process mutations that were pending before we disconnected
|
|
660
|
-
if (pendingMutations.length > 0 && processMutations) {
|
|
661
|
-
processMutations(pendingMutations);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Conditionals/iterations may have mounted new DOM while observer was disconnected.
|
|
666
|
-
// Scan for unresolved <component src=""> elements that need fetching.
|
|
667
|
-
if (componentProcessingStarted) {
|
|
668
|
-
const componentConfig = {
|
|
669
|
-
...config,
|
|
670
|
-
_forceSync: true,
|
|
671
|
-
_observer: observer,
|
|
672
|
-
_processMutations: processMutations,
|
|
673
|
-
};
|
|
674
|
-
processComponent(rootElement, null, componentConfig);
|
|
675
|
-
}
|
|
687
|
+
patchWithObserverPaused(() => {
|
|
688
|
+
dispatchFlush(dirty, currentState, previousState, manifest);
|
|
689
|
+
});
|
|
676
690
|
}
|
|
677
691
|
|
|
678
692
|
// Store previous state for hooks (currentState is already plain, no need to clone)
|
|
@@ -687,8 +701,49 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
687
701
|
// — the Proxy invariant rejects overriding non-configurable + non-writable
|
|
688
702
|
// data properties. Enumerable stays false so `on` doesn't leak into state
|
|
689
703
|
// snapshots or Object.keys($).
|
|
704
|
+
// Late ready — a `$.on('ready')` AFTER boot (a fetched fragment's script,
|
|
705
|
+
// an SPA mount's app-boot module) defers until the registering mount
|
|
706
|
+
// SETTLES: content inserted, attributes hydrated, staging committed, fouc
|
|
707
|
+
// gates released. That makes `$.on('ready')` ≡ `await $.ready` genuinely
|
|
708
|
+
// true — DOM-touching ready work (querySelector, scroll-spy) sees the
|
|
709
|
+
// mounted subtree in both forms. With nothing in flight the microtask
|
|
710
|
+
// flush fires on the same tick `await $.ready` would resume on.
|
|
711
|
+
const lateReady = [];
|
|
712
|
+
let lateReadyWatch = null;
|
|
713
|
+
// Settlement is judged against the MANAGED root, same as boot ready —
|
|
714
|
+
// content outside a config.target root is never scanned, so its literal
|
|
715
|
+
// @[...] text must not starve the late-ready queue.
|
|
716
|
+
const mountsSettled = () =>
|
|
717
|
+
!document.querySelector('[vibe-staged], [vibe-fouc]') && shouldCleanup(rootElement);
|
|
718
|
+
const flushLateReady = () => {
|
|
719
|
+
if (lateReady.length === 0 || !mountsSettled()) return;
|
|
720
|
+
if (lateReadyWatch) {
|
|
721
|
+
lateReadyWatch();
|
|
722
|
+
lateReadyWatch = null;
|
|
723
|
+
}
|
|
724
|
+
for (const cb of lateReady.splice(0)) {
|
|
725
|
+
try {
|
|
726
|
+
cb();
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error('[vibe] Error in ready hook:', error);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const queueLateReady = (callback) => {
|
|
733
|
+
lateReady.push(callback);
|
|
734
|
+
lateReadyWatch ??= $.on('afterDomMutation', flushLateReady);
|
|
735
|
+
Promise.resolve().then(flushLateReady);
|
|
736
|
+
return () => {
|
|
737
|
+
const i = lateReady.indexOf(callback);
|
|
738
|
+
if (i >= 0) lateReady.splice(i, 1);
|
|
739
|
+
};
|
|
740
|
+
};
|
|
741
|
+
|
|
690
742
|
Object.defineProperty($, 'on', {
|
|
691
743
|
value: (event, callback) => {
|
|
744
|
+
if (event === 'ready' && readyFired) {
|
|
745
|
+
return queueLateReady(callback);
|
|
746
|
+
}
|
|
692
747
|
if (hooks[event]) {
|
|
693
748
|
hooks[event].push(callback);
|
|
694
749
|
}
|
|
@@ -714,6 +769,37 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
714
769
|
enumerable: false,
|
|
715
770
|
});
|
|
716
771
|
|
|
772
|
+
// Register a component's state bucket under its id (the fetched-mount path).
|
|
773
|
+
// A FRESH id is a key nothing in the live tree can bind yet — its subtree
|
|
774
|
+
// enters the reactive tree only after this write — so the write lands
|
|
775
|
+
// silently (raw target, no global flush) and previousState is deliberately
|
|
776
|
+
// NOT seeded: the mount's grouped notify must see undefined → state as a
|
|
777
|
+
// real diff (see the inline comment below — the load-bearing invariant).
|
|
778
|
+
// Re-registering an EXISTING id (an HMR re-run) is a real value change for
|
|
779
|
+
// live bindings and flushes normally.
|
|
780
|
+
Object.defineProperty($, '_register', {
|
|
781
|
+
value: (componentId, componentState) => {
|
|
782
|
+
if (componentId in $) {
|
|
783
|
+
$[componentId] = componentState;
|
|
784
|
+
return componentId;
|
|
785
|
+
}
|
|
786
|
+
// Raw write only — previousState is deliberately NOT seeded. The mount
|
|
787
|
+
// commits all its registrations in one notifyChanged batch when its
|
|
788
|
+
// scripts settle, and that flush must see each fresh id as a real
|
|
789
|
+
// change (undefined -> state): the subtree may have hydrated BEFORE the
|
|
790
|
+
// script ran (scripts execute in module order behind earlier imports),
|
|
791
|
+
// so this correction flush is what renders bindings, conditionals and
|
|
792
|
+
// iterations that read the component's state. A seeded previousState
|
|
793
|
+
// made the diff vacuous whenever the script filled its state object
|
|
794
|
+
// before calling component() — the game's fill-then-register pages
|
|
795
|
+
// stayed frozen at their pre-registration (empty) render.
|
|
796
|
+
silentSet($, componentId, componentState);
|
|
797
|
+
return componentId;
|
|
798
|
+
},
|
|
799
|
+
enumerable: false,
|
|
800
|
+
configurable: true,
|
|
801
|
+
});
|
|
802
|
+
|
|
717
803
|
// Mark a trusted string as raw HTML. A binding that is the sole content of
|
|
718
804
|
// its element — `<p>@[$.unsafe(desc)]</p>` — sets innerHTML from the string
|
|
719
805
|
// instead of escaping it via textContent. Trusted input only (no sanitizing,
|
|
@@ -729,7 +815,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
729
815
|
// processed HTML string the plugin's HMR handler can hand to $.reconcile.
|
|
730
816
|
// Scripts are NOT executed — callers use this only when they've verified
|
|
731
817
|
// script contents haven't changed (so registered state is still valid).
|
|
732
|
-
Object.defineProperty($, '
|
|
818
|
+
Object.defineProperty($, '_renderComponent', {
|
|
733
819
|
value: renderComponentTemplate,
|
|
734
820
|
enumerable: false,
|
|
735
821
|
});
|
|
@@ -852,7 +938,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
852
938
|
// Early exit if no mutations to process (common case)
|
|
853
939
|
if (mutations.length === 0) return;
|
|
854
940
|
|
|
855
|
-
const manifestSizeBefore = Object.keys(manifest).length;
|
|
941
|
+
const manifestSizeBefore = debug ? Object.keys(manifest).length : 0;
|
|
856
942
|
let hadChanges = false;
|
|
857
943
|
let parsedParents = null; // Lazy init - only create Set when needed
|
|
858
944
|
let addedElements = 0;
|
|
@@ -886,8 +972,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
886
972
|
// conditional's tracked reference to point to the replacement node.
|
|
887
973
|
const branchRef = branchNodeRegistry.get(node);
|
|
888
974
|
if (branchRef) {
|
|
889
|
-
// Find the replacement: an added node in the same mutation at the same
|
|
890
|
-
|
|
975
|
+
// Find the replacement: an added node in the same mutation at the same
|
|
976
|
+
// parent. A STAGED remount splits add and remove into different
|
|
977
|
+
// batches (the incoming wrapper is inserted early, the outgoing one
|
|
978
|
+
// removed at commit) — the wrapper's replacement back-pointer covers
|
|
979
|
+
// that case.
|
|
980
|
+
const replacement =
|
|
981
|
+
Array.from(addedNodesList).find(n => n.parentNode === target) ||
|
|
982
|
+
(node._vibeReplacedBy?.isConnected ? node._vibeReplacedBy : null);
|
|
891
983
|
if (replacement) {
|
|
892
984
|
branchRef.nodes[branchRef.index] = replacement;
|
|
893
985
|
branchNodeRegistry.set(replacement, branchRef);
|
|
@@ -895,13 +987,16 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
895
987
|
branchNodeRegistry.delete(node);
|
|
896
988
|
}
|
|
897
989
|
|
|
898
|
-
const
|
|
990
|
+
const dotAnnotation = manifestPathOf(manifest, node);
|
|
899
991
|
|
|
900
992
|
// Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
|
|
901
|
-
if (
|
|
993
|
+
if (dotAnnotation === null) return;
|
|
902
994
|
|
|
903
|
-
|
|
904
|
-
|
|
995
|
+
// Prune the node's ENTIRE manifest scope — root entry plus every
|
|
996
|
+
// descendant and branch-alias path under it. Deleting just the root
|
|
997
|
+
// entry leaked the rest, pinning each swapped-out page's detached
|
|
998
|
+
// DOM forever (the SPA navigation leak).
|
|
999
|
+
removeManifestSubtree(manifest, dotAnnotation);
|
|
905
1000
|
|
|
906
1001
|
const dotPath = dotAnnotation.split('.');
|
|
907
1002
|
const name = dotPath.pop();
|
|
@@ -955,7 +1050,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
955
1050
|
});
|
|
956
1051
|
}
|
|
957
1052
|
|
|
958
|
-
const
|
|
1053
|
+
const dotAnnotation = manifestPathOf(manifest, target);
|
|
959
1054
|
|
|
960
1055
|
// Parse the newly added node
|
|
961
1056
|
const parsedNode = parse(node);
|
|
@@ -969,8 +1064,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
969
1064
|
// (If not — e.g. mutations inside an iteration instance whose rows aren't in the
|
|
970
1065
|
// global manifest — we still hydrate the node below; we just skip tree/manifest
|
|
971
1066
|
// registration since there's no tree branch to attach to.)
|
|
972
|
-
if (
|
|
973
|
-
const [dotAnnotation] = entry;
|
|
1067
|
+
if (dotAnnotation !== null) {
|
|
974
1068
|
// navigateTree walks `children` only; when the parent lives in a
|
|
975
1069
|
// conditional branch's slot-projected content its manifest path isn't
|
|
976
1070
|
// navigable that way (see findNodeByElement). Fall back to an identity
|
|
@@ -1040,9 +1134,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1040
1134
|
|
|
1041
1135
|
// CLEANUP OF CURRENT STATE
|
|
1042
1136
|
releaseOrphanedComponentState(removedComponentIds);
|
|
1137
|
+
let hadRemovals = false;
|
|
1043
1138
|
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
1139
|
+
if (removedNodesList.length > 0) hadRemovals = true;
|
|
1044
1140
|
releaseOrphanedIterationProps(removedNodesList);
|
|
1045
1141
|
});
|
|
1142
|
+
// Subscribers anchored in the removed subtrees are dead — drop them from
|
|
1143
|
+
// the reverse index (observed removals: page swaps, reconcile, app code).
|
|
1144
|
+
if (hadRemovals) pruneDisconnected();
|
|
1046
1145
|
|
|
1047
1146
|
// Fire hooks once after all mutations are processed (not per-node)
|
|
1048
1147
|
if (hadChanges) {
|
|
@@ -1095,8 +1194,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1095
1194
|
debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
|
|
1096
1195
|
}
|
|
1097
1196
|
|
|
1098
|
-
// Log parsed elements after mutation
|
|
1099
|
-
|
|
1197
|
+
// Log parsed elements after mutation — debug-only: the entries
|
|
1198
|
+
// snapshot allocates the full manifest per batch, so skip it entirely
|
|
1199
|
+
// when nothing will be printed.
|
|
1200
|
+
const manifestSizeAfter = debug ? Object.keys(manifest).length : manifestSizeBefore;
|
|
1100
1201
|
const totalNodes = manifestSizeAfter - manifestSizeBefore;
|
|
1101
1202
|
|
|
1102
1203
|
if (totalNodes > 0) {
|
|
@@ -1213,6 +1314,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1213
1314
|
// Track cleanup state
|
|
1214
1315
|
let componentProcessingStarted = false;
|
|
1215
1316
|
let cleanupExecuted = false;
|
|
1317
|
+
let bootSettleDone = false;
|
|
1216
1318
|
|
|
1217
1319
|
// Check if cleanup should run
|
|
1218
1320
|
const checkCleanup = () => {
|
|
@@ -1229,10 +1331,24 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1229
1331
|
|
|
1230
1332
|
// Check if all processing is complete
|
|
1231
1333
|
if (shouldCleanup(rootElement)) {
|
|
1334
|
+
// One-shot settle before ready: directives whose expressions read
|
|
1335
|
+
// globals provided by component scripts (window helpers) carry no
|
|
1336
|
+
// reactive dependency for those globals, so a directive that rendered
|
|
1337
|
+
// before the defining script settled — boot renders race async imports,
|
|
1338
|
+
// runtime fetch mounts land after initial hydration — would stay empty
|
|
1339
|
+
// forever. All scripts and mounts have settled here; re-render once
|
|
1340
|
+
// against the fully-scripted world. Idempotent: rendered iterations
|
|
1341
|
+
// early-return, value-unchanged conditionals are a no-op.
|
|
1342
|
+
if (!bootSettleDone) {
|
|
1343
|
+
bootSettleDone = true;
|
|
1344
|
+
renderAllIterations(parsedTree, extractPlainValue($), manifest);
|
|
1345
|
+
settleConditionals(parsedTree, $, manifest);
|
|
1346
|
+
}
|
|
1232
1347
|
cleanup(rootElement, debug);
|
|
1233
1348
|
cleanupExecuted = true;
|
|
1234
1349
|
|
|
1235
1350
|
// Fire ready hook after cleanup completes
|
|
1351
|
+
readyFired = true;
|
|
1236
1352
|
hooks.ready.forEach((callback) => {
|
|
1237
1353
|
try {
|
|
1238
1354
|
callback();
|
|
@@ -1253,6 +1369,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1253
1369
|
if (compiledScriptsPending) {
|
|
1254
1370
|
compiledScriptsPending.then(() => {
|
|
1255
1371
|
compiledScriptsDone = true;
|
|
1372
|
+
// The initial conditional pass ran before these module scripts settled
|
|
1373
|
+
// — native MPA ordering had page modules evaluate BEFORE vibe booted,
|
|
1374
|
+
// so a gate on a module-provided global (`<!-- if window.isDev -->`)
|
|
1375
|
+
// saw the booted world. A gate like that carries no reactive
|
|
1376
|
+
// dependency to re-check it later, so settle conditionals once against
|
|
1377
|
+
// the post-module world before ready fires. Value-unchanged branches
|
|
1378
|
+
// are a no-op.
|
|
1379
|
+
settleConditionals(parsedTree, $, manifest);
|
|
1256
1380
|
checkCleanup();
|
|
1257
1381
|
});
|
|
1258
1382
|
}
|
|
@@ -1281,7 +1405,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1281
1405
|
// Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
|
|
1282
1406
|
if (typeof window !== 'undefined') {
|
|
1283
1407
|
const manifestToExport = hyperspeedTree || hyperspeedManifestData;
|
|
1284
|
-
window.
|
|
1408
|
+
(window.__vibe ??= {}).manifest = manifestToExport;
|
|
1285
1409
|
}
|
|
1286
1410
|
|
|
1287
1411
|
return $;
|