@ape-egg/vibe 2.1.18 → 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,11 @@
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
+
3
9
  ## [2.1.18] - 2026-06-22
4
10
 
5
11
  ### 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.18",
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+/;
@@ -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