@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/affected.js
CHANGED
|
@@ -1,46 +1,75 @@
|
|
|
1
|
-
import { resolvePath, deepEqual } from
|
|
2
|
-
import { extractDependencies } from
|
|
3
|
-
import { BINDING_REGEX } from
|
|
4
|
-
import { evalInScope, resolveThisPath } from
|
|
1
|
+
import { resolvePath, deepEqual } from "./iteration-utils.js";
|
|
2
|
+
import { extractDependencies } from "./conditionals.js";
|
|
3
|
+
import { BINDING_REGEX } from "./constants.js";
|
|
4
|
+
import { evalInScope, resolveThisPath } from "./utils.js";
|
|
5
5
|
|
|
6
6
|
// Evaluate conditional expression
|
|
7
|
-
const evaluateCondition = (expression, state, element = null) =>
|
|
7
|
+
const evaluateCondition = (expression, state, element = null) =>
|
|
8
|
+
!!evalInScope(expression, state, element);
|
|
8
9
|
|
|
9
10
|
// Helper function to check if a match references a specific key.
|
|
10
11
|
// Fast path: exact match or property access (`user.name` matches `user`).
|
|
11
12
|
// Slow path: word-boundary search for complex expressions like `Math.floor(coins / 100)`
|
|
12
13
|
// where the key appears as an identifier anywhere in the expression.
|
|
13
|
-
const isIdentChar = (c) =>
|
|
14
|
+
const isIdentChar = (c) =>
|
|
15
|
+
(c >= "a" && c <= "z") ||
|
|
16
|
+
(c >= "A" && c <= "Z") ||
|
|
17
|
+
(c >= "0" && c <= "9") ||
|
|
18
|
+
c === "_" ||
|
|
19
|
+
c === "$";
|
|
14
20
|
|
|
15
21
|
const matchesKey = (matchStr, key) => {
|
|
16
|
-
if (
|
|
22
|
+
if (
|
|
23
|
+
matchStr === key ||
|
|
24
|
+
matchStr.startsWith(key + ".") ||
|
|
25
|
+
matchStr.startsWith(key + "[")
|
|
26
|
+
)
|
|
27
|
+
return true;
|
|
17
28
|
|
|
18
29
|
// Search for key as a standalone identifier (word boundaries on both sides)
|
|
19
30
|
let i = 0;
|
|
20
31
|
while ((i = matchStr.indexOf(key, i)) !== -1) {
|
|
21
|
-
const before = i === 0 ?
|
|
22
|
-
const after =
|
|
32
|
+
const before = i === 0 ? "" : matchStr[i - 1];
|
|
33
|
+
const after =
|
|
34
|
+
i + key.length >= matchStr.length ? "" : matchStr[i + key.length];
|
|
23
35
|
if (!isIdentChar(before) && !isIdentChar(after)) return true;
|
|
24
36
|
i += key.length;
|
|
25
37
|
}
|
|
26
38
|
return false;
|
|
27
39
|
};
|
|
28
40
|
|
|
29
|
-
const recursive = (
|
|
41
|
+
const recursive = (
|
|
42
|
+
tree,
|
|
43
|
+
state,
|
|
44
|
+
newState,
|
|
45
|
+
affected,
|
|
46
|
+
scopedStateForHydration = null,
|
|
47
|
+
depth = 0,
|
|
48
|
+
) => {
|
|
30
49
|
// Handle iteration nodes specially
|
|
31
|
-
if (tree.type ===
|
|
32
|
-
const resolvedExpr = resolveThisPath(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
if (tree.type === "iteration") {
|
|
51
|
+
const resolvedExpr = resolveThisPath(
|
|
52
|
+
tree.meta.arrayPath,
|
|
53
|
+
tree.meta.startComment?.parentElement,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const oldArray =
|
|
57
|
+
evalInScope(resolvedExpr, state, tree.meta.startComment?.parentElement) ??
|
|
58
|
+
resolvePath(state, resolvedExpr);
|
|
59
|
+
const newArray =
|
|
60
|
+
evalInScope(
|
|
61
|
+
resolvedExpr,
|
|
62
|
+
newState,
|
|
63
|
+
tree.meta.startComment?.parentElement,
|
|
64
|
+
) ?? resolvePath(newState, resolvedExpr);
|
|
36
65
|
|
|
37
66
|
// Fast path: reference comparison (arrays are typically replaced, not mutated)
|
|
38
67
|
// This avoids expensive O(n) deepEqual for large arrays
|
|
39
68
|
if (oldArray !== newArray) {
|
|
40
69
|
affected.push({
|
|
41
|
-
type:
|
|
70
|
+
type: "iteration",
|
|
42
71
|
node: tree,
|
|
43
|
-
changeType:
|
|
72
|
+
changeType: "array",
|
|
44
73
|
});
|
|
45
74
|
return affected;
|
|
46
75
|
}
|
|
@@ -51,16 +80,26 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
51
80
|
// Compiled iterations have instances without tree/scopedState — the batch function
|
|
52
81
|
// may reference global state keys (e.g., `selectedCategory` in a button binding).
|
|
53
82
|
// If any state key changed, trigger a rebuild so the batch function re-evaluates.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
// Instances rendered without a per-row parsed tree — compiled (batch
|
|
84
|
+
// fn from the manifest) or runtime batch (fast path for simple
|
|
85
|
+
// templates) — can't be walked by the recursive descent below to
|
|
86
|
+
// detect nested outer-state bindings. Mark the whole iteration as
|
|
87
|
+
// affected so updateIteration runs and rebuilds the rows.
|
|
88
|
+
const hasTreelessInstances =
|
|
89
|
+
tree.runtime.instances.length > 0 &&
|
|
90
|
+
!tree.runtime.instances[0].tree;
|
|
91
|
+
|
|
92
|
+
if (hasTreelessInstances) {
|
|
58
93
|
const isInitialHydration = state === newState;
|
|
59
94
|
if (!isInitialHydration) {
|
|
60
95
|
const stateKeys = Object.keys(state);
|
|
61
|
-
const hasChangedKey = stateKeys.some(k => state[k] !== newState[k]);
|
|
96
|
+
const hasChangedKey = stateKeys.some((k) => state[k] !== newState[k]);
|
|
62
97
|
if (hasChangedKey) {
|
|
63
|
-
affected.push({
|
|
98
|
+
affected.push({
|
|
99
|
+
type: "iteration",
|
|
100
|
+
node: tree,
|
|
101
|
+
changeType: "array",
|
|
102
|
+
});
|
|
64
103
|
return affected;
|
|
65
104
|
}
|
|
66
105
|
}
|
|
@@ -68,15 +107,28 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
68
107
|
|
|
69
108
|
for (const instance of tree.runtime.instances) {
|
|
70
109
|
if (instance.tree && instance.scopedState) {
|
|
71
|
-
// Build plain-object snapshots for comparison.
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
110
|
+
// Build plain-object snapshots for comparison. Spreading
|
|
111
|
+
// instance.scopedState yields its local vars (item, index) plus
|
|
112
|
+
// whatever globals were captured at scope-creation time; the
|
|
113
|
+
// overlaying spread of state/newState then writes the *current*
|
|
114
|
+
// values from this update cycle. Both merged states are plain
|
|
115
|
+
// objects with up-to-date values.
|
|
76
116
|
const mergedOldState = { ...instance.scopedState, ...state };
|
|
77
117
|
const mergedNewState = { ...instance.scopedState, ...newState };
|
|
78
|
-
//
|
|
79
|
-
|
|
118
|
+
// scopedStateForHydration must reflect the new state so hydrate's
|
|
119
|
+
// bindings inside the row see post-update values. instance.scopedState
|
|
120
|
+
// is frozen against whatever target renderIteration was called with
|
|
121
|
+
// (often the plain initialState snapshot from index.js), so reading
|
|
122
|
+
// outer-state keys through it returns stale values after later
|
|
123
|
+
// mutations. mergedNewState carries the live globals + localVars.
|
|
124
|
+
recursive(
|
|
125
|
+
instance.tree,
|
|
126
|
+
mergedOldState,
|
|
127
|
+
mergedNewState,
|
|
128
|
+
affected,
|
|
129
|
+
mergedNewState,
|
|
130
|
+
depth + 1,
|
|
131
|
+
);
|
|
80
132
|
}
|
|
81
133
|
}
|
|
82
134
|
}
|
|
@@ -85,23 +137,38 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
85
137
|
}
|
|
86
138
|
|
|
87
139
|
// Handle conditional nodes specially
|
|
88
|
-
if (tree.type ===
|
|
89
|
-
const oldValue = evaluateCondition(
|
|
90
|
-
|
|
140
|
+
if (tree.type === "conditional") {
|
|
141
|
+
const oldValue = evaluateCondition(
|
|
142
|
+
tree.meta.expression,
|
|
143
|
+
state,
|
|
144
|
+
tree.meta.startComment?.parentElement,
|
|
145
|
+
);
|
|
146
|
+
const newValue = evaluateCondition(
|
|
147
|
+
tree.meta.expression,
|
|
148
|
+
newState,
|
|
149
|
+
tree.meta.startComment?.parentElement,
|
|
150
|
+
);
|
|
91
151
|
|
|
92
152
|
// Check if condition result changed
|
|
93
153
|
if (oldValue !== newValue) {
|
|
94
154
|
affected.push({
|
|
95
|
-
type:
|
|
155
|
+
type: "conditional",
|
|
96
156
|
node: tree,
|
|
97
|
-
changeType:
|
|
157
|
+
changeType: "expression",
|
|
98
158
|
});
|
|
99
159
|
return affected;
|
|
100
160
|
}
|
|
101
161
|
|
|
102
162
|
// Condition didn't change, check for affected elements inside active branch
|
|
103
163
|
if (tree.runtime.activeInstance && tree.runtime.activeInstance.parsedTree) {
|
|
104
|
-
return recursive(
|
|
164
|
+
return recursive(
|
|
165
|
+
tree.runtime.activeInstance.parsedTree,
|
|
166
|
+
state,
|
|
167
|
+
newState,
|
|
168
|
+
affected,
|
|
169
|
+
scopedStateForHydration,
|
|
170
|
+
depth + 1,
|
|
171
|
+
);
|
|
105
172
|
}
|
|
106
173
|
|
|
107
174
|
return affected;
|
|
@@ -128,21 +195,29 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
128
195
|
// Resolve this.property to componentId.property
|
|
129
196
|
const resolvedInner = resolveThisPath(m.inner, tree.element);
|
|
130
197
|
|
|
131
|
-
const noMatch = !shallowState.some((key) =>
|
|
198
|
+
const noMatch = !shallowState.some((key) =>
|
|
199
|
+
matchesKey(resolvedInner, key),
|
|
200
|
+
);
|
|
132
201
|
|
|
133
202
|
let shouldAffect = false;
|
|
134
203
|
let relevantKeys = [];
|
|
135
204
|
|
|
136
205
|
if (isInitialHydration) {
|
|
137
206
|
// Initial hydration: affect all matched keys
|
|
138
|
-
relevantKeys = shallowNewState.filter((key) =>
|
|
207
|
+
relevantKeys = shallowNewState.filter((key) =>
|
|
208
|
+
matchesKey(resolvedInner, key),
|
|
209
|
+
);
|
|
139
210
|
shouldAffect = noMatch || relevantKeys.length > 0;
|
|
140
211
|
} else {
|
|
141
212
|
// Update: only affect if value changed
|
|
142
|
-
const changedKeys = shallowNewState.filter(
|
|
143
|
-
|
|
213
|
+
const changedKeys = shallowNewState.filter(
|
|
214
|
+
(key) =>
|
|
215
|
+
matchesKey(resolvedInner, key) && state[key] !== newState[key],
|
|
144
216
|
);
|
|
145
|
-
relevantKeys =
|
|
217
|
+
relevantKeys =
|
|
218
|
+
changedKeys.length > 0
|
|
219
|
+
? changedKeys
|
|
220
|
+
: shallowState.filter((key) => matchesKey(resolvedInner, key));
|
|
146
221
|
shouldAffect = noMatch || changedKeys.length > 0;
|
|
147
222
|
}
|
|
148
223
|
|
|
@@ -189,25 +264,30 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
189
264
|
// Resolve this.property to componentId.property
|
|
190
265
|
const resolvedInner = resolveThisPath(m.inner, tree.element);
|
|
191
266
|
|
|
192
|
-
const noMatch = !shallowState.some((key) =>
|
|
267
|
+
const noMatch = !shallowState.some((key) =>
|
|
268
|
+
matchesKey(resolvedInner, key),
|
|
269
|
+
);
|
|
193
270
|
|
|
194
271
|
let shouldAffect = false;
|
|
195
272
|
|
|
196
273
|
if (isInitialHydration) {
|
|
197
274
|
// Initial hydration: affect all matched keys
|
|
198
|
-
const newMatches = shallowNewState.filter((key) =>
|
|
275
|
+
const newMatches = shallowNewState.filter((key) =>
|
|
276
|
+
matchesKey(resolvedInner, key),
|
|
277
|
+
);
|
|
199
278
|
shouldAffect = noMatch || newMatches.length > 0;
|
|
200
279
|
} else {
|
|
201
280
|
// Update: only affect if value changed
|
|
202
|
-
const changedKeys = shallowNewState.filter(
|
|
203
|
-
|
|
281
|
+
const changedKeys = shallowNewState.filter(
|
|
282
|
+
(key) =>
|
|
283
|
+
matchesKey(resolvedInner, key) && state[key] !== newState[key],
|
|
204
284
|
);
|
|
205
285
|
shouldAffect = noMatch || changedKeys.length > 0;
|
|
206
286
|
}
|
|
207
287
|
|
|
208
288
|
if (shouldAffect) {
|
|
209
289
|
affected.push({
|
|
210
|
-
type:
|
|
290
|
+
type: "attribute",
|
|
211
291
|
attrName,
|
|
212
292
|
attrValue,
|
|
213
293
|
matchOuter: m.outer,
|
|
@@ -239,7 +319,9 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
239
319
|
const resolvedInner = resolveThisPath(m.inner, tree.element);
|
|
240
320
|
// HTML lowercases attribute names, so expression may be lowercase while state
|
|
241
321
|
// keys are camelCase. Match case-insensitively by trying both direct and lowercased.
|
|
242
|
-
const matchKey = (key) =>
|
|
322
|
+
const matchKey = (key) =>
|
|
323
|
+
matchesKey(resolvedInner, key) ||
|
|
324
|
+
matchesKey(resolvedInner, key.toLowerCase());
|
|
243
325
|
|
|
244
326
|
const noMatch = !shallowState.some(matchKey);
|
|
245
327
|
|
|
@@ -249,15 +331,15 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
249
331
|
const newMatches = shallowNewState.filter(matchKey);
|
|
250
332
|
shouldAffect = noMatch || newMatches.length > 0;
|
|
251
333
|
} else {
|
|
252
|
-
const changedKeys = shallowNewState.filter(
|
|
253
|
-
matchKey(key) && state[key] !== newState[key]
|
|
334
|
+
const changedKeys = shallowNewState.filter(
|
|
335
|
+
(key) => matchKey(key) && state[key] !== newState[key],
|
|
254
336
|
);
|
|
255
337
|
shouldAffect = noMatch || changedKeys.length > 0;
|
|
256
338
|
}
|
|
257
339
|
|
|
258
340
|
if (shouldAffect) {
|
|
259
341
|
affected.push({
|
|
260
|
-
type:
|
|
342
|
+
type: "nameBinding",
|
|
261
343
|
nameBinding,
|
|
262
344
|
matchOuter: m.outer,
|
|
263
345
|
matchInner: m.inner, // Keep original, evalInScope will resolve this.
|
|
@@ -274,8 +356,15 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
|
|
|
274
356
|
if (children) {
|
|
275
357
|
for (const key in children) {
|
|
276
358
|
const child = children[key];
|
|
277
|
-
if (child && typeof child ===
|
|
278
|
-
recursive(
|
|
359
|
+
if (child && typeof child === "object") {
|
|
360
|
+
recursive(
|
|
361
|
+
child,
|
|
362
|
+
state,
|
|
363
|
+
newState,
|
|
364
|
+
affected,
|
|
365
|
+
scopedStateForHydration,
|
|
366
|
+
depth + 1,
|
|
367
|
+
);
|
|
279
368
|
}
|
|
280
369
|
}
|
|
281
370
|
}
|