@ape-egg/vibe 2.3.0 → 3.0.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/README.md +14 -4
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +10 -15
- package/llms.txt +8 -6
- package/package.json +19 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +312 -99
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +251 -111
- package/runtime/index.js +180 -71
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +69 -5
- 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 +77 -14
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1196
- 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 -2880
- 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 -16
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/spa.rs +0 -477
- 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 -1278
- package/compiler/src/config.rs +0 -279
- package/compiler/src/main.rs +0 -358
- 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 = {};
|
|
@@ -614,6 +616,47 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
614
616
|
|
|
615
617
|
// Observer callback will be defined below (already declared above before processComponent)
|
|
616
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
|
+
|
|
617
660
|
const $ = state(s, (changedProps) => {
|
|
618
661
|
// Selective extraction: only extract changed props, preserve references for unchanged.
|
|
619
662
|
// This ensures affected() correctly skips iterations whose arrays didn't change,
|
|
@@ -633,52 +676,17 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
633
676
|
}
|
|
634
677
|
}
|
|
635
678
|
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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) {
|
|
640
686
|
debugLog(PHASE_UPDATE, 'state changed', debug);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (observer) {
|
|
645
|
-
pendingMutations = observer.takeRecords();
|
|
646
|
-
observer.disconnect();
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Run core loop on the full tree (hydrate will use affected list)
|
|
650
|
-
// isNewNode = false because this is a state update, not a DOM mutation
|
|
651
|
-
hydrate(affectedElements, currentState, manifest, previousState);
|
|
652
|
-
|
|
653
|
-
// After hydrate, check for conditional/iteration changes
|
|
654
|
-
const conditionalCount = renderAllConditionals(parsedTree, currentState, manifest);
|
|
655
|
-
const iterationCount = renderAllIterations(parsedTree, currentState, manifest);
|
|
656
|
-
|
|
657
|
-
if (observer) {
|
|
658
|
-
observer.observe(rootElement, {
|
|
659
|
-
attributes: false,
|
|
660
|
-
characterData: false,
|
|
661
|
-
childList: true,
|
|
662
|
-
subtree: true,
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// Process mutations that were pending before we disconnected
|
|
666
|
-
if (pendingMutations.length > 0 && processMutations) {
|
|
667
|
-
processMutations(pendingMutations);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Conditionals/iterations may have mounted new DOM while observer was disconnected.
|
|
672
|
-
// Scan for unresolved <component src=""> elements that need fetching.
|
|
673
|
-
if (componentProcessingStarted) {
|
|
674
|
-
const componentConfig = {
|
|
675
|
-
...config,
|
|
676
|
-
_forceSync: true,
|
|
677
|
-
_observer: observer,
|
|
678
|
-
_processMutations: processMutations,
|
|
679
|
-
};
|
|
680
|
-
processComponent(rootElement, null, componentConfig);
|
|
681
|
-
}
|
|
687
|
+
patchWithObserverPaused(() => {
|
|
688
|
+
dispatchFlush(dirty, currentState, previousState, manifest);
|
|
689
|
+
});
|
|
682
690
|
}
|
|
683
691
|
|
|
684
692
|
// Store previous state for hooks (currentState is already plain, no need to clone)
|
|
@@ -693,15 +701,48 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
693
701
|
// — the Proxy invariant rejects overriding non-configurable + non-writable
|
|
694
702
|
// data properties. Enumerable stays false so `on` doesn't leak into state
|
|
695
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
|
+
|
|
696
742
|
Object.defineProperty($, 'on', {
|
|
697
743
|
value: (event, callback) => {
|
|
698
744
|
if (event === 'ready' && readyFired) {
|
|
699
|
-
|
|
700
|
-
callback();
|
|
701
|
-
} catch (error) {
|
|
702
|
-
console.error('[vibe] Error in ready hook:', error);
|
|
703
|
-
}
|
|
704
|
-
return () => {};
|
|
745
|
+
return queueLateReady(callback);
|
|
705
746
|
}
|
|
706
747
|
if (hooks[event]) {
|
|
707
748
|
hooks[event].push(callback);
|
|
@@ -728,6 +769,37 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
728
769
|
enumerable: false,
|
|
729
770
|
});
|
|
730
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
|
+
|
|
731
803
|
// Mark a trusted string as raw HTML. A binding that is the sole content of
|
|
732
804
|
// its element — `<p>@[$.unsafe(desc)]</p>` — sets innerHTML from the string
|
|
733
805
|
// instead of escaping it via textContent. Trusted input only (no sanitizing,
|
|
@@ -743,7 +815,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
743
815
|
// processed HTML string the plugin's HMR handler can hand to $.reconcile.
|
|
744
816
|
// Scripts are NOT executed — callers use this only when they've verified
|
|
745
817
|
// script contents haven't changed (so registered state is still valid).
|
|
746
|
-
Object.defineProperty($, '
|
|
818
|
+
Object.defineProperty($, '_renderComponent', {
|
|
747
819
|
value: renderComponentTemplate,
|
|
748
820
|
enumerable: false,
|
|
749
821
|
});
|
|
@@ -866,7 +938,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
866
938
|
// Early exit if no mutations to process (common case)
|
|
867
939
|
if (mutations.length === 0) return;
|
|
868
940
|
|
|
869
|
-
const manifestSizeBefore = Object.keys(manifest).length;
|
|
941
|
+
const manifestSizeBefore = debug ? Object.keys(manifest).length : 0;
|
|
870
942
|
let hadChanges = false;
|
|
871
943
|
let parsedParents = null; // Lazy init - only create Set when needed
|
|
872
944
|
let addedElements = 0;
|
|
@@ -900,8 +972,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
900
972
|
// conditional's tracked reference to point to the replacement node.
|
|
901
973
|
const branchRef = branchNodeRegistry.get(node);
|
|
902
974
|
if (branchRef) {
|
|
903
|
-
// Find the replacement: an added node in the same mutation at the same
|
|
904
|
-
|
|
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);
|
|
905
983
|
if (replacement) {
|
|
906
984
|
branchRef.nodes[branchRef.index] = replacement;
|
|
907
985
|
branchNodeRegistry.set(replacement, branchRef);
|
|
@@ -909,13 +987,16 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
909
987
|
branchNodeRegistry.delete(node);
|
|
910
988
|
}
|
|
911
989
|
|
|
912
|
-
const
|
|
990
|
+
const dotAnnotation = manifestPathOf(manifest, node);
|
|
913
991
|
|
|
914
992
|
// Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
|
|
915
|
-
if (
|
|
993
|
+
if (dotAnnotation === null) return;
|
|
916
994
|
|
|
917
|
-
|
|
918
|
-
|
|
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);
|
|
919
1000
|
|
|
920
1001
|
const dotPath = dotAnnotation.split('.');
|
|
921
1002
|
const name = dotPath.pop();
|
|
@@ -969,7 +1050,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
969
1050
|
});
|
|
970
1051
|
}
|
|
971
1052
|
|
|
972
|
-
const
|
|
1053
|
+
const dotAnnotation = manifestPathOf(manifest, target);
|
|
973
1054
|
|
|
974
1055
|
// Parse the newly added node
|
|
975
1056
|
const parsedNode = parse(node);
|
|
@@ -983,8 +1064,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
983
1064
|
// (If not — e.g. mutations inside an iteration instance whose rows aren't in the
|
|
984
1065
|
// global manifest — we still hydrate the node below; we just skip tree/manifest
|
|
985
1066
|
// registration since there's no tree branch to attach to.)
|
|
986
|
-
if (
|
|
987
|
-
const [dotAnnotation] = entry;
|
|
1067
|
+
if (dotAnnotation !== null) {
|
|
988
1068
|
// navigateTree walks `children` only; when the parent lives in a
|
|
989
1069
|
// conditional branch's slot-projected content its manifest path isn't
|
|
990
1070
|
// navigable that way (see findNodeByElement). Fall back to an identity
|
|
@@ -1054,9 +1134,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1054
1134
|
|
|
1055
1135
|
// CLEANUP OF CURRENT STATE
|
|
1056
1136
|
releaseOrphanedComponentState(removedComponentIds);
|
|
1137
|
+
let hadRemovals = false;
|
|
1057
1138
|
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
1139
|
+
if (removedNodesList.length > 0) hadRemovals = true;
|
|
1058
1140
|
releaseOrphanedIterationProps(removedNodesList);
|
|
1059
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();
|
|
1060
1145
|
|
|
1061
1146
|
// Fire hooks once after all mutations are processed (not per-node)
|
|
1062
1147
|
if (hadChanges) {
|
|
@@ -1109,8 +1194,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1109
1194
|
debugLog(PHASE_MUTATE, '', debug, 0, topmostParent || addedElementsList[0]);
|
|
1110
1195
|
}
|
|
1111
1196
|
|
|
1112
|
-
// Log parsed elements after mutation
|
|
1113
|
-
|
|
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;
|
|
1114
1201
|
const totalNodes = manifestSizeAfter - manifestSizeBefore;
|
|
1115
1202
|
|
|
1116
1203
|
if (totalNodes > 0) {
|
|
@@ -1227,6 +1314,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1227
1314
|
// Track cleanup state
|
|
1228
1315
|
let componentProcessingStarted = false;
|
|
1229
1316
|
let cleanupExecuted = false;
|
|
1317
|
+
let bootSettleDone = false;
|
|
1230
1318
|
|
|
1231
1319
|
// Check if cleanup should run
|
|
1232
1320
|
const checkCleanup = () => {
|
|
@@ -1243,6 +1331,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1243
1331
|
|
|
1244
1332
|
// Check if all processing is complete
|
|
1245
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
|
+
}
|
|
1246
1347
|
cleanup(rootElement, debug);
|
|
1247
1348
|
cleanupExecuted = true;
|
|
1248
1349
|
|
|
@@ -1268,6 +1369,14 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1268
1369
|
if (compiledScriptsPending) {
|
|
1269
1370
|
compiledScriptsPending.then(() => {
|
|
1270
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);
|
|
1271
1380
|
checkCleanup();
|
|
1272
1381
|
});
|
|
1273
1382
|
}
|
|
@@ -1296,7 +1405,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
1296
1405
|
// Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
|
|
1297
1406
|
if (typeof window !== 'undefined') {
|
|
1298
1407
|
const manifestToExport = hyperspeedTree || hyperspeedManifestData;
|
|
1299
|
-
window.
|
|
1408
|
+
(window.__vibe ??= {}).manifest = manifestToExport;
|
|
1300
1409
|
}
|
|
1301
1410
|
|
|
1302
1411
|
return $;
|