@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.
@@ -1,46 +1,75 @@
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';
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) => !!evalInScope(expression, state, element);
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) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_' || 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 (matchStr === key || matchStr.startsWith(key + '.') || matchStr.startsWith(key + '[')) return true;
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 ? '' : matchStr[i - 1];
22
- const after = i + key.length >= matchStr.length ? '' : matchStr[i + key.length];
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 = (tree, state, newState, affected, scopedStateForHydration = null, depth = 0) => {
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 === 'iteration') {
32
- const resolvedExpr = resolveThisPath(tree.meta.arrayPath, tree.meta.startComment?.parentElement);
33
-
34
- const oldArray = evalInScope(resolvedExpr, state, tree.meta.startComment?.parentElement) ?? resolvePath(state, resolvedExpr);
35
- const newArray = evalInScope(resolvedExpr, newState, tree.meta.startComment?.parentElement) ?? resolvePath(newState, resolvedExpr);
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: 'iteration',
70
+ type: "iteration",
42
71
  node: tree,
43
- changeType: 'array',
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
- const hasCompiledInstances = tree.compiled && tree.runtime.instances.length > 0
55
- && !tree.runtime.instances[0].tree;
56
-
57
- if (hasCompiledInstances) {
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({ type: 'iteration', node: tree, changeType: 'array' });
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. scopedState is a live Proxy
72
- // that reflects current global state spreading it gives us the local vars
73
- // (cat, index) plus current global values. We then override globals with the
74
- // actual old (state) / new (newState) values so binding comparisons can detect
75
- // which state keys changed.
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
- // Pass scopedState as scopedStateForHydration so evalInScope can access iteration variables
79
- recursive(instance.tree, mergedOldState, mergedNewState, affected, instance.scopedState, depth + 1);
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 === 'conditional') {
89
- const oldValue = evaluateCondition(tree.meta.expression, state, tree.meta.startComment?.parentElement);
90
- const newValue = evaluateCondition(tree.meta.expression, newState, tree.meta.startComment?.parentElement);
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: 'conditional',
155
+ type: "conditional",
96
156
  node: tree,
97
- changeType: 'expression',
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(tree.runtime.activeInstance.parsedTree, state, newState, affected, scopedStateForHydration, depth + 1);
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) => matchesKey(resolvedInner, 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) => matchesKey(resolvedInner, 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((key) =>
143
- matchesKey(resolvedInner, key) && state[key] !== newState[key]
213
+ const changedKeys = shallowNewState.filter(
214
+ (key) =>
215
+ matchesKey(resolvedInner, key) && state[key] !== newState[key],
144
216
  );
145
- relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(resolvedInner, key));
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) => matchesKey(resolvedInner, 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) => matchesKey(resolvedInner, 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((key) =>
203
- matchesKey(resolvedInner, key) && state[key] !== newState[key]
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: 'attribute',
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) => matchesKey(resolvedInner, key) || matchesKey(resolvedInner, key.toLowerCase());
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((key) =>
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: 'nameBinding',
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 === 'object') {
278
- recursive(child, state, newState, affected, scopedStateForHydration, depth + 1);
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
  }