@ape-egg/vibe 2.1.17 → 2.1.19
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 +12 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +1 -1
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/manifest_builder.rs +15 -5
- package/package.json +1 -1
- package/runtime/constants.js +9 -4
- package/runtime/iterate.js +45 -0
- package/runtime/iteration-utils.js +20 -1
- package/runtime/parse.js +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.19] - 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`<!-- each list as item, i (item.id) -->` (index alias before the key) silently failed to iterate** (`runtime/constants.js`, `runtime/iteration-utils.js`, `runtime/parse.js`, `compiler/src/compiler/manifest_builder.rs`) — the iteration header grammar only accepted the key before the index (`as item (item.id), i`). When the index came first, the trailing `(item.id)` made `ITERATION_REGEX` fail to match, so `parse.js` skipped the comment entirely and the body rendered once with an undefined alias instead of iterating. `ITERATION_REGEX` now accepts the `(key)` expression in either position (a second optional key group), and a new `parseIterationHeader` helper coalesces the two and is the single source of truth used by `parse.js`. Compiled pages re-parse the preserved each comment through the same helper, so they were fixed by the runtime change; additionally the Rust `manifest_builder` (compiler 2.0.0 → 2.0.1) now strips the key before the item/index split so the emitted manifest carries a clean `indexAlias` (was `"i (item.id)"`) for both orderings. Tests: `tests/unit/iteration-utils.test.js` (parseIterationHeader), `tests/compiler/iterations-index-key/`, `e2e-runtime/iteration-index-key.html` + `tests/e2e/iteration-index-key.spec.js` (runtime + compiled).
|
|
8
|
+
|
|
9
|
+
## [2.1.18] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Loop-scoped bindings in slot content passed to a component inside an iteration didn't resolve** (`runtime/iterate.js`) — content projected into a `<component src>` is captured raw (`_vibeSlotContent`) and inlined only when `processComponent` runs, by which point the row's iteration scope is gone. So a `@[...]` in that slot content rooted in a loop alias (`item`/`index`/outer) or `this` couldn't resolve later: value bindings rendered `undefined` and name-bindings (`<icon @[row.icon]>`) never set their attribute. The new `resolveSlotContentBindings` pre-resolves those bindings into registry-backed global refs — the same snapshot mechanism the component's own prop attributes use — before the scope is lost, so the inlined slot hydrates against the right values and the row's update path refreshes them in place (the wrapper inherits `_vibeIterPropExprs` / `data-vibe-iter-prop`, so `refreshIterationComponentProps` re-evaluates them on each item change). Globals-only bindings are left raw and resolve through the normal reactive path. Tests: `e2e-runtime/slot-name-binding-loop.html`, `tests/e2e/slot-name-binding.spec.js`.
|
|
14
|
+
|
|
3
15
|
## [2.1.17] - 2026-06-22
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
Binary file
|
|
Binary file
|
package/compiler/src/Cargo.lock
CHANGED
package/compiler/src/Cargo.toml
CHANGED
|
@@ -202,16 +202,26 @@ impl ManifestBuilder {
|
|
|
202
202
|
// Extract expression: "each items as item" -> "items as item"
|
|
203
203
|
let expression = trimmed.strip_prefix("each ").unwrap_or("").to_string();
|
|
204
204
|
|
|
205
|
-
// Parse expression to extract parts: "items as item, index" or "items as item"
|
|
205
|
+
// Parse expression to extract parts: "items as item, index" or "items as item".
|
|
206
|
+
// The optional (key) expression may sit before or after the index — strip it
|
|
207
|
+
// first so the item/index split isn't polluted by it. The key itself isn't
|
|
208
|
+
// stored here; the runtime re-derives it from the preserved comment.
|
|
206
209
|
let parts: Vec<&str> = expression.split(" as ").collect();
|
|
207
210
|
let array_path = parts.get(0).unwrap_or(&"").trim().to_string();
|
|
208
211
|
let alias_part = parts.get(1).unwrap_or(&"").trim();
|
|
209
|
-
let
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
let alias_without_key = match (alias_part.find('('), alias_part.rfind(')')) {
|
|
213
|
+
(Some(open), Some(close)) if close > open => {
|
|
214
|
+
format!("{}{}", &alias_part[..open], &alias_part[close + 1..])
|
|
215
|
+
}
|
|
216
|
+
_ => alias_part.to_string(),
|
|
217
|
+
};
|
|
218
|
+
let alias_without_key = alias_without_key.trim();
|
|
219
|
+
let (item_alias, index_alias) = if let Some(comma_pos) = alias_without_key.find(',') {
|
|
220
|
+
let item = alias_without_key[..comma_pos].trim().to_string();
|
|
221
|
+
let index = alias_without_key[comma_pos + 1..].trim().to_string();
|
|
212
222
|
(item, index)
|
|
213
223
|
} else {
|
|
214
|
-
(
|
|
224
|
+
(alias_without_key.to_string(), "index".to_string())
|
|
215
225
|
};
|
|
216
226
|
|
|
217
227
|
// Find node index in siblings
|
package/package.json
CHANGED
package/runtime/constants.js
CHANGED
|
@@ -211,18 +211,23 @@ export const BINDING_REGEX = new RegExp(String.raw`\@\[(${BINDING_INNER})\]`, 'g
|
|
|
211
211
|
// Regex for detecting a pure binding (entire value is just @[expression])
|
|
212
212
|
export const PURE_BINDING_REGEX = new RegExp(String.raw`^\@\[(${BINDING_INNER})\]$`);
|
|
213
213
|
|
|
214
|
-
// Regex for parsing iteration comment syntax. Supported forms
|
|
214
|
+
// Regex for parsing iteration comment syntax. Supported forms — the index and
|
|
215
|
+
// the key are each optional and may appear in EITHER order:
|
|
215
216
|
// <!-- each items as item -->
|
|
216
217
|
// <!-- each items as item, index -->
|
|
217
218
|
// <!-- each items as item (item.id) --> // explicit key
|
|
218
|
-
// <!-- each items as item (item.id), index --> // key
|
|
219
|
-
//
|
|
219
|
+
// <!-- each items as item (item.id), index --> // key, then index
|
|
220
|
+
// <!-- each items as item, index (item.id) --> // index, then key
|
|
221
|
+
// Capture groups: arrayPath, itemAlias, keyBeforeIndex (optional), indexAlias
|
|
222
|
+
// (optional), keyAfterIndex (optional). The key lands in group 3 when written
|
|
223
|
+
// before the index and in group 5 when written after; parseIterationHeader
|
|
224
|
+
// coalesces the two — prefer that helper over destructuring the raw match.
|
|
220
225
|
// The array expression can be any JS: a state path, a window global, a method
|
|
221
226
|
// call, or an inline literal. The optional key expression is evaluated per
|
|
222
227
|
// item against scoped state to produce a stable identity for diffing — this
|
|
223
228
|
// keeps survivors stable when earlier items are removed (otherwise the
|
|
224
229
|
// fallback hash key embeds the index and triggers bulk re-render).
|
|
225
|
-
export const ITERATION_REGEX = /^each\s+(.+)\s+as\s+(\w+)(?:\s*\(\s*([^)]+?)\s*\))?(?:\s*,\s*(\w+))?\s*$/;
|
|
230
|
+
export const ITERATION_REGEX = /^each\s+(.+)\s+as\s+(\w+)(?:\s*\(\s*([^)]+?)\s*\))?(?:\s*,\s*(\w+))?(?:\s*\(\s*([^)]+?)\s*\))?\s*$/;
|
|
226
231
|
|
|
227
232
|
// Regex for detecting start of iteration comment
|
|
228
233
|
export const ITERATION_START_REGEX = /^each\s+/;
|
package/runtime/iterate.js
CHANGED
|
@@ -368,6 +368,50 @@ export const releaseOrphanedIterationProps = (nodes) => {
|
|
|
368
368
|
}
|
|
369
369
|
};
|
|
370
370
|
|
|
371
|
+
// Slot content projected into a <component src> is captured raw (`_vibeSlotContent`)
|
|
372
|
+
// and inlined only when processComponent runs — by which point the row's iteration
|
|
373
|
+
// scope is gone. Any `@[...]` in that content rooted in a loop alias (item/index/
|
|
374
|
+
// outer) or `this` therefore can't resolve later: value bindings render undefined
|
|
375
|
+
// and name-bindings (`<icon @[row.icon]>`) never set their attribute. Pre-resolve
|
|
376
|
+
// those into registry-backed global refs here — the same snapshot mechanism the
|
|
377
|
+
// component's own prop attributes use — so the inlined slot hydrates against the
|
|
378
|
+
// right values and the row's update path refreshes them in place (the wrapper
|
|
379
|
+
// inherits `_vibeIterPropExprs`/`data-vibe-iter-prop`, so refreshIterationComponentProps
|
|
380
|
+
// re-evaluates them on each item change). Globals-only bindings are left raw; they
|
|
381
|
+
// resolve through the normal reactive path against the inlined component's scope.
|
|
382
|
+
const resolveSlotContentBindings = (el, scopedState, aliases) => {
|
|
383
|
+
const html = el._vibeSlotContent;
|
|
384
|
+
if (!html || !html.includes('@[')) return;
|
|
385
|
+
const registry = ensureIterPropsRegistry();
|
|
386
|
+
const idByExpr = new Map();
|
|
387
|
+
const rewritten = html.replace(BINDING_REGEX, (whole, expr) => {
|
|
388
|
+
const usesLocalScope =
|
|
389
|
+
/\bthis\b/.test(expr) ||
|
|
390
|
+
(aliases && extractDependencies(expr).some((d) => aliases.has(d)));
|
|
391
|
+
if (!usesLocalScope) return whole;
|
|
392
|
+
let id = idByExpr.get(expr);
|
|
393
|
+
if (id === undefined) {
|
|
394
|
+
let value;
|
|
395
|
+
try {
|
|
396
|
+
value = evalInScope(expr, scopedState, el);
|
|
397
|
+
} catch {
|
|
398
|
+
return whole;
|
|
399
|
+
}
|
|
400
|
+
if (value === undefined) return whole;
|
|
401
|
+
id = `_p${__vibeIterPropCounter++}`;
|
|
402
|
+
registry[id] = value;
|
|
403
|
+
idByExpr.set(expr, id);
|
|
404
|
+
(el._vibeIterPropExprs = el._vibeIterPropExprs || []).push({ id, attrName: null, expr });
|
|
405
|
+
(el._vibeIterPropIds = el._vibeIterPropIds || []).push(id);
|
|
406
|
+
}
|
|
407
|
+
return `@[window.__vibeiterprops.${id}]`;
|
|
408
|
+
});
|
|
409
|
+
if (idByExpr.size) {
|
|
410
|
+
el._vibeSlotContent = rewritten;
|
|
411
|
+
el.setAttribute('data-vibe-iter-prop', '');
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
371
415
|
// For <component src> elements inside an iteration instance, evaluate any
|
|
372
416
|
// `@[expr]` attribute bindings against the iteration's scoped state and route
|
|
373
417
|
// every resolved value through the global iteration-prop registry. The prop
|
|
@@ -433,6 +477,7 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
433
477
|
// Leave binding raw — processComponent will handle it as a binding
|
|
434
478
|
}
|
|
435
479
|
}
|
|
480
|
+
resolveSlotContentBindings(el, scopedState, aliases);
|
|
436
481
|
}
|
|
437
482
|
}
|
|
438
483
|
};
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
// Utility functions for array iteration
|
|
2
|
-
import { ITERATION_START_REGEX, CONDITIONAL_START_REGEX } from './constants.js';
|
|
2
|
+
import { ITERATION_REGEX, ITERATION_START_REGEX, CONDITIONAL_START_REGEX } from './constants.js';
|
|
3
|
+
|
|
4
|
+
// Parse an `each` directive body (the text inside `<!-- ... -->`, markers
|
|
5
|
+
// stripped) into its parts, or null when it isn't a valid each. The index alias
|
|
6
|
+
// and the (key) expression are both optional and may be written in either order
|
|
7
|
+
// — `as item, i (item.id)` and `as item (item.id), i` are equivalent. Single
|
|
8
|
+
// source of truth for the grammar, used by the runtime parser and (via DOM
|
|
9
|
+
// re-parse of the restored markers) by compiled pages.
|
|
10
|
+
export const parseIterationHeader = (text) => {
|
|
11
|
+
const match = text.match(ITERATION_REGEX);
|
|
12
|
+
if (!match) return null;
|
|
13
|
+
const [, arrayPath, itemAlias, keyBeforeIndex, indexAlias, keyAfterIndex] = match;
|
|
14
|
+
const keyExpr = keyBeforeIndex ?? keyAfterIndex;
|
|
15
|
+
return {
|
|
16
|
+
arrayPath,
|
|
17
|
+
itemAlias,
|
|
18
|
+
keyExpr: keyExpr != null ? keyExpr.trim() : null,
|
|
19
|
+
indexAlias: indexAlias ?? null,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
3
22
|
|
|
4
23
|
// Resolve nested paths in state (e.g., "user.items" -> state.user.items)
|
|
5
24
|
// Supports bracket notation: "teams[0].combatants" -> state.teams[0].combatants
|
package/runtime/parse.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { findEndComment, findConditionalEnd } from './iteration-utils.js';
|
|
1
|
+
import { findEndComment, findConditionalEnd, parseIterationHeader } from './iteration-utils.js';
|
|
2
2
|
import {
|
|
3
3
|
NON_REACTIVE_ELEMENTS,
|
|
4
4
|
BINDING_REGEX,
|
|
5
|
-
ITERATION_REGEX,
|
|
6
5
|
CONDITIONAL_REGEX,
|
|
7
6
|
DOM_ELEMENT_PROPERTIES,
|
|
8
7
|
DEHYDRATE_CLASS_OR_ATTR,
|
|
@@ -150,10 +149,10 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
150
149
|
|
|
151
150
|
// Handle iteration comments
|
|
152
151
|
if (nodeName === '#comment') {
|
|
153
|
-
const iterationMatch = textContent.trim()
|
|
152
|
+
const iterationMatch = parseIterationHeader(textContent.trim());
|
|
154
153
|
|
|
155
154
|
if (iterationMatch) {
|
|
156
|
-
const
|
|
155
|
+
const { arrayPath, itemAlias, keyExpr, indexAlias } = iterationMatch;
|
|
157
156
|
|
|
158
157
|
try {
|
|
159
158
|
// Find matching end comment
|