@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 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
@@ -1599,7 +1599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1599
1599
 
1600
1600
  [[package]]
1601
1601
  name = "vibe-compiler"
1602
- version = "2.0.0"
1602
+ version = "2.0.1"
1603
1603
  dependencies = [
1604
1604
  "clap",
1605
1605
  "colored",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vibe-compiler"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  edition = "2021"
5
5
  description = "Vibe framework compiler - compiles Vibe source files into optimized output"
6
6
  authors = ["Kim Korte"]
@@ -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 (item_alias, index_alias) = if let Some(comma_pos) = alias_part.find(',') {
210
- let item = alias_part[..comma_pos].trim().to_string();
211
- let index = alias_part[comma_pos + 1..].trim().to_string();
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
- (alias_part.to_string(), "index".to_string())
224
+ (alias_without_key.to_string(), "index".to_string())
215
225
  };
216
226
 
217
227
  // Find node index in siblings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "2.1.17",
3
+ "version": "2.1.19",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
@@ -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 + index
219
- // Capture groups: arrayPath, itemAlias, keyExpr (optional), indexAlias (optional).
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+/;
@@ -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().match(ITERATION_REGEX);
152
+ const iterationMatch = parseIterationHeader(textContent.trim());
154
153
 
155
154
  if (iterationMatch) {
156
- const [_, arrayPath, itemAlias, keyExpr, indexAlias] = iterationMatch;
155
+ const { arrayPath, itemAlias, keyExpr, indexAlias } = iterationMatch;
157
156
 
158
157
  try {
159
158
  // Find matching end comment