@ape-egg/vibe 1.9.0 → 1.9.5
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/CHANGELOG.md +71 -0
- package/README.md +90 -97
- package/ROADMAP.md +6 -12
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +1 -1
- package/compiler/src/compiler/compile.rs +66 -6
- package/compiler/src/parser/html.rs +7 -1
- package/llms.txt +175 -83
- package/package.json +2 -1
- package/runtime/affected.js +141 -52
- package/runtime/component.js +302 -148
- package/runtime/conditionals.js +45 -3
- package/runtime/constants.js +24 -5
- package/runtime/hydrate.js +35 -21
- package/runtime/index.js +50 -3
- package/runtime/iterate.js +598 -56
- package/runtime/iteration-utils.js +9 -2
- package/runtime/loop-scope.js +157 -0
- package/runtime/parse.js +95 -20
- package/runtime/pre-compiled-iterations.js +12 -0
- package/runtime/state.js +18 -1
- package/runtime/utils.js +39 -2
package/runtime/hydrate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { updateIteration } from './iterate.js';
|
|
2
2
|
import { updateConditional } from './conditionals.js';
|
|
3
3
|
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
|
|
4
|
-
import { evalInScope } from './utils.js';
|
|
4
|
+
import { evalInScope, resolveCaseInsensitivePath } from './utils.js';
|
|
5
5
|
|
|
6
6
|
export default (affected, state, manifest = {}, oldState = {}) => {
|
|
7
7
|
affected.forEach((aff) => {
|
|
@@ -24,18 +24,15 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
24
24
|
if (aff.type === 'nameBinding') {
|
|
25
25
|
const { nameBinding, matchInner, element } = aff;
|
|
26
26
|
try {
|
|
27
|
-
// HTML lowercases attribute names, so we need case-insensitive lookup
|
|
28
|
-
// Try exact match first, then
|
|
27
|
+
// HTML lowercases attribute names, so we need case-insensitive lookup.
|
|
28
|
+
// Try exact match first, then walk the path case-insensitively if the
|
|
29
|
+
// exact lookup returned nothing — this recovers camelCase property
|
|
30
|
+
// names in dotted paths like `<icon @[fx.convertsIcon]>` (arrives at
|
|
31
|
+
// runtime as `@[fx.convertsicon]`).
|
|
29
32
|
let attrName = evalInScope(matchInner, effectiveState, element);
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Find the property with case-insensitive match
|
|
34
|
-
const keys = Object.keys(effectiveState);
|
|
35
|
-
const matchingKey = keys.find(k => k.toLowerCase() === matchInner.toLowerCase());
|
|
36
|
-
if (matchingKey) {
|
|
37
|
-
attrName = effectiveState[matchingKey];
|
|
38
|
-
}
|
|
34
|
+
if (!attrName) {
|
|
35
|
+
attrName = resolveCaseInsensitivePath(effectiveState, matchInner);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
// Track multiple name bindings per element (need a map of binding -> evaluated attr)
|
|
@@ -87,17 +84,24 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
87
84
|
// Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
|
|
88
85
|
const expr = isPureBinding[1];
|
|
89
86
|
const value = evalInScope(expr, effectiveState, element);
|
|
90
|
-
element[attrName] = value;
|
|
87
|
+
if (element[attrName] !== value) element[attrName] = value;
|
|
91
88
|
if (value !== undefined && value !== null) {
|
|
92
|
-
|
|
89
|
+
const str = String(value);
|
|
90
|
+
if (element.getAttribute(attrName) !== str) element.setAttribute(attrName, str);
|
|
93
91
|
}
|
|
94
92
|
} else if (!isValueAttr && isPureBinding) {
|
|
95
|
-
// Boolean-like attributes: add or remove based on truthiness
|
|
93
|
+
// Boolean-like attributes: add or remove based on truthiness.
|
|
94
|
+
// Compare both presence AND value — initial hydration starts with
|
|
95
|
+
// the raw `@[...]` binding text as the attribute value, so
|
|
96
|
+
// `hasAttribute` alone isn't enough to know the canonical state is
|
|
97
|
+
// already set.
|
|
96
98
|
const expr = isPureBinding[1];
|
|
97
99
|
const value = evalInScope(expr, effectiveState, element);
|
|
98
100
|
if (value) {
|
|
99
|
-
element.
|
|
100
|
-
|
|
101
|
+
if (element.getAttribute(attrName) !== '') {
|
|
102
|
+
element.setAttribute(attrName, '');
|
|
103
|
+
}
|
|
104
|
+
} else if (element.hasAttribute(attrName)) {
|
|
101
105
|
element.removeAttribute(attrName);
|
|
102
106
|
}
|
|
103
107
|
} else {
|
|
@@ -105,7 +109,9 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
105
109
|
const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
|
|
106
110
|
return evalInScope(expr, effectiveState, element);
|
|
107
111
|
});
|
|
108
|
-
element.
|
|
112
|
+
if (element.getAttribute(attrName) !== newValue) {
|
|
113
|
+
element.setAttribute(attrName, newValue);
|
|
114
|
+
}
|
|
109
115
|
}
|
|
110
116
|
} catch (e) {}
|
|
111
117
|
return;
|
|
@@ -128,11 +134,19 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
128
134
|
});
|
|
129
135
|
|
|
130
136
|
// If we have a direct reference to the text node, update it specifically
|
|
131
|
-
// This prevents wiping child elements when parent has both text and element children
|
|
137
|
+
// This prevents wiping child elements when parent has both text and element children.
|
|
138
|
+
// Skip the write when the value is already correct — the browser would repaint
|
|
139
|
+
// (and any in-flight CSS transition on the row would jitter) even when no value
|
|
140
|
+
// actually changed. Reactivity coverage is unchanged: the only state changes that
|
|
141
|
+
// hit this path either produce a new value (still applied) or don't (now no-op).
|
|
142
|
+
// Skip the write when the value is already correct — the browser would
|
|
143
|
+
// repaint (and any in-flight CSS transition would jitter) even when no
|
|
144
|
+
// value actually changed. Reactivity coverage is unchanged: state changes
|
|
145
|
+
// that produce a new value still apply; state changes that don't are now
|
|
146
|
+
// proper no-ops at the DOM layer.
|
|
132
147
|
if (textNode && textNode.nodeType === 3) {
|
|
133
|
-
textNode.textContent = toReplace;
|
|
134
|
-
} else {
|
|
135
|
-
// Fallback: element has no children or is just a text container
|
|
148
|
+
if (textNode.textContent !== toReplace) textNode.textContent = toReplace;
|
|
149
|
+
} else if (element.textContent !== toReplace) {
|
|
136
150
|
element.textContent = toReplace;
|
|
137
151
|
}
|
|
138
152
|
} catch (e) {}
|
package/runtime/index.js
CHANGED
|
@@ -4,8 +4,9 @@ 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
|
+
import { installScopeResolver } from './loop-scope.js';
|
|
9
10
|
import {
|
|
10
11
|
NON_REACTIVE_ELEMENTS,
|
|
11
12
|
PHASE_ATTACH,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
PHASE_READY,
|
|
22
23
|
DEHYDRATE_CLASS_OR_ATTR,
|
|
23
24
|
} from './constants.js';
|
|
24
|
-
import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState } from './component.js';
|
|
25
|
+
import { processComponent, abortComponentFetch, collectComponentIds, releaseOrphanedComponentState, renderComponentTemplate } from './component.js';
|
|
25
26
|
import { debugLog } from './debug.js';
|
|
26
27
|
import { shouldCleanup, cleanup } from './cleanup.js';
|
|
27
28
|
import { reconcile } from './reconcile.js';
|
|
@@ -327,6 +328,10 @@ let previousState = {};
|
|
|
327
328
|
const main = (s, config = {}, stringSelector = '') => {
|
|
328
329
|
const debug = !!config?.debug;
|
|
329
330
|
const verbose = !!config?.verbose;
|
|
331
|
+
globalThis.__vibeDebug = debug;
|
|
332
|
+
|
|
333
|
+
// Expose the global `$scope` resolver used by loop-scoped `on*` handlers.
|
|
334
|
+
installScopeResolver();
|
|
330
335
|
|
|
331
336
|
// Detect if running in compiler's headless browser
|
|
332
337
|
// When true: skip cleanup to preserve [vibe] attribute in compiled HTML
|
|
@@ -550,7 +555,17 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
550
555
|
// while still detecting binding changes inside iteration instances.
|
|
551
556
|
const currentState = { ...previousState };
|
|
552
557
|
for (const prop of changedProps) {
|
|
553
|
-
|
|
558
|
+
// Distinguish "set to undefined" (key still present in $) from "deleted"
|
|
559
|
+
// (key absent from $). For deletions, removing from currentState matches
|
|
560
|
+
// the live proxy's shape — otherwise downstream consumers that pass
|
|
561
|
+
// currentState as `$` to evalInScope (e.g. iterations that re-render via
|
|
562
|
+
// bindings reading the root state) would see the deleted key as a phantom
|
|
563
|
+
// own-property with value `undefined`.
|
|
564
|
+
if (prop in $) {
|
|
565
|
+
currentState[prop] = extractPlainValue($[prop]);
|
|
566
|
+
} else {
|
|
567
|
+
delete currentState[prop];
|
|
568
|
+
}
|
|
554
569
|
}
|
|
555
570
|
|
|
556
571
|
// Find what changed (compare previousState vs currentState)
|
|
@@ -608,6 +623,11 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
608
623
|
});
|
|
609
624
|
|
|
610
625
|
// Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
|
|
626
|
+
// `configurable: true` lets a component script's scoped `$` Proxy (built in
|
|
627
|
+
// component.js) legally return a wrapped `.on` that auto-registers cleanup
|
|
628
|
+
// — the Proxy invariant rejects overriding non-configurable + non-writable
|
|
629
|
+
// data properties. Enumerable stays false so `on` doesn't leak into state
|
|
630
|
+
// snapshots or Object.keys($).
|
|
611
631
|
Object.defineProperty($, 'on', {
|
|
612
632
|
value: (event, callback) => {
|
|
613
633
|
if (hooks[event]) {
|
|
@@ -616,6 +636,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
616
636
|
return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
|
|
617
637
|
},
|
|
618
638
|
enumerable: false,
|
|
639
|
+
configurable: true,
|
|
619
640
|
});
|
|
620
641
|
|
|
621
642
|
// Promise that resolves after the ready hook fires and all ready callbacks
|
|
@@ -634,6 +655,16 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
634
655
|
enumerable: false,
|
|
635
656
|
});
|
|
636
657
|
|
|
658
|
+
// Pure-render path for surgical component HMR. Given raw component template
|
|
659
|
+
// HTML, callsite props, slot HTML, and existing componentIds, returns the
|
|
660
|
+
// processed HTML string the plugin's HMR handler can hand to $.reconcile.
|
|
661
|
+
// Scripts are NOT executed — callers use this only when they've verified
|
|
662
|
+
// script contents haven't changed (so registered state is still valid).
|
|
663
|
+
Object.defineProperty($, 'renderComponent', {
|
|
664
|
+
value: renderComponentTemplate,
|
|
665
|
+
enumerable: false,
|
|
666
|
+
});
|
|
667
|
+
|
|
637
668
|
// Initial hydration - pass plain values so iteration can do reference comparison
|
|
638
669
|
const initialState = extractPlainValue($);
|
|
639
670
|
const affectedElements = affected(parsedTree, initialState, initialState);
|
|
@@ -850,6 +881,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
850
881
|
iteratedCount += counts.iteratedCount;
|
|
851
882
|
evaluatedCount += counts.evaluatedCount;
|
|
852
883
|
|
|
884
|
+
// For inlined `<component>` wrappers belonging to an iteration row
|
|
885
|
+
// (marked by component.js's resolveIterationComponentProps transfer),
|
|
886
|
+
// stash the parsed tree on the wrapper. processCoreLoop has just run
|
|
887
|
+
// hydrate + renderAllConditionals + renderAllIterations on it, so
|
|
888
|
+
// `parsedNode` carries live `runtime.activeInstance` /
|
|
889
|
+
// `runtime.instances` data — exactly what `affected.js` needs to walk
|
|
890
|
+
// into branch / row content. Iterate.js's update path consumes this
|
|
891
|
+
// tree to re-hydrate bindings inside the inlined component on each
|
|
892
|
+
// row-scope change without rebuilding the wrapper's DOM.
|
|
893
|
+
if (node.nodeType === 1 && node._vibeIterPropExprs) {
|
|
894
|
+
node._vibeIterTree = parsedNode;
|
|
895
|
+
}
|
|
896
|
+
|
|
853
897
|
// Count elements vs nodes separately
|
|
854
898
|
if (node.nodeName.startsWith('#')) {
|
|
855
899
|
addedNodes++;
|
|
@@ -864,6 +908,9 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
864
908
|
|
|
865
909
|
// CLEANUP OF CURRENT STATE
|
|
866
910
|
releaseOrphanedComponentState(removedComponentIds);
|
|
911
|
+
mutations.forEach(({ removedNodes: removedNodesList }) => {
|
|
912
|
+
releaseOrphanedIterationProps(removedNodesList);
|
|
913
|
+
});
|
|
867
914
|
|
|
868
915
|
// Fire hooks once after all mutations are processed (not per-node)
|
|
869
916
|
if (hadChanges) {
|