@ape-egg/vibe 2.1.14 → 2.1.16

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.16] - 2026-06-22
4
+
5
+ ### Fixed
6
+
7
+ - **Compiler 1.9.9 → 2.0.0 — bare prop substitution corrupted matching text inside string literals** (`compiler/src/parser/html.rs`, `runtime/component.js`) — the bare-prop-identifier rewrite added in 2.1.14 (so `onclick="pick(item)"` resolves the live prop) replaced *every* whole-word occurrence of the prop name, including ones inside a quoted string. A prop named `slot` rewrote the selector in `closest('brawler-slot')`, corrupting it. Both the compiler's `substitute_identifier` and the runtime's `substituteInExpr` now track string-literal boundaries (`'`, `"`, and backtick, honouring escapes; template `${…}` counts as part of the literal) and substitute only the code spans between them, leaving identifiers inside string literals intact. The two sides stay byte-for-byte identical, preserving compiled/runtime parity.
8
+
9
+ ## [2.1.15] - 2026-06-22
10
+
11
+ ### Fixed
12
+
13
+ - **Component props bound to global state inside an iteration froze instead of staying reactive** (`runtime/iterate.js`, `runtime/conditionals.js`, `runtime/component.js`) — when a `<component src>` inside an `<!-- each -->` received a prop whose expression referenced only globals (e.g. `<scalar-bar value="@[elapsedMilliseconds]">`), `resolveIterationComponentProps` snapshotted the value into the iteration-prop registry. That slot is refreshed only on array diffs, so in a row whose array never changes the prop froze at its first value and never tracked the global (the Brawling-loader / scalar-bar freeze). Such global-only prop bindings are now left raw so the normal reactive path tracks the global, while the wrapper is still tagged (`data-vibe-iter-prop` + an empty `_vibeIterPropExprs`) so `index.js` stamps its `_vibeIterTree` and `affected.js`'s `walkInlinedComponentTrees` descends in to re-hydrate the binding on a global-state change. The registry path is kept only for props that genuinely need iteration scope — `this.X` or an item/index/outer alias, detected via `extractDependencies`. Two supporting fixes: a conditional now recognizes it lives inside an iteration from its parse-time `scopeAliases` (not just a populated `parentScope`), so a conditional that mounts later through the update path — a loader whose `<!-- if -->` flips true only once combat starts — still routes its component props correctly; and `component.js` transfers the `data-vibe-iter-prop` discovery marker to the rebuilt wrapper on both the registry and the new global-only raw-binding paths. Tests: `e2e-runtime/component-prop-global-scalar.html`, `tests/e2e/component-prop-global-scalar.spec.js`.
14
+
3
15
  ## [2.1.14] - 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 = "1.9.9"
1602
+ version = "2.0.0"
1603
1603
  dependencies = [
1604
1604
  "clap",
1605
1605
  "colored",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vibe-compiler"
3
- version = "1.9.9"
3
+ version = "2.0.0"
4
4
  edition = "2021"
5
5
  description = "Vibe framework compiler - compiles Vibe source files into optimized output"
6
6
  authors = ["Kim Korte"]
@@ -542,7 +542,32 @@ fn substitute_identifier(expr: &str, name: &str, replacement: &str) -> String {
542
542
  }
543
543
  let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
544
544
  let mut i = 0;
545
+ // Delimiter of the string literal we're currently inside, or 0 when in code.
546
+ // A prop identifier that appears inside a quoted string (e.g. the selector in
547
+ // `closest('brawler-slot')`) must be left intact, mirroring the runtime's
548
+ // substituteInExpr. Template `${…}` counts as part of the literal here.
549
+ let mut quote: u8 = 0;
545
550
  while i < bytes.len() {
551
+ let b = bytes[i];
552
+ if quote != 0 {
553
+ out.push(b);
554
+ if b == b'\\' && i + 1 < bytes.len() {
555
+ out.push(bytes[i + 1]);
556
+ i += 2;
557
+ continue;
558
+ }
559
+ if b == quote {
560
+ quote = 0;
561
+ }
562
+ i += 1;
563
+ continue;
564
+ }
565
+ if b == b'\'' || b == b'"' || b == b'`' {
566
+ quote = b;
567
+ out.push(b);
568
+ i += 1;
569
+ continue;
570
+ }
546
571
  if i + nlen <= bytes.len()
547
572
  && bytes[i..i + nlen].eq_ignore_ascii_case(nb)
548
573
  && (i == 0 || (!is_ident_byte(bytes[i - 1]) && bytes[i - 1] != b'.'))
@@ -551,7 +576,7 @@ fn substitute_identifier(expr: &str, name: &str, replacement: &str) -> String {
551
576
  out.extend_from_slice(replacement.as_bytes());
552
577
  i += nlen;
553
578
  } else {
554
- out.push(bytes[i]);
579
+ out.push(b);
555
580
  i += 1;
556
581
  }
557
582
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
@@ -350,7 +350,23 @@ const renderPropsAndSlot = (temp, props, slotHtml) => {
350
350
  'gi'
351
351
  );
352
352
 
353
- const substituteInExpr = (expr, replacement) => expr.replace(idRegex, replacement);
353
+ // Rewrite the identifier only outside string literals. A prop/alias that also
354
+ // appears inside a quoted string — e.g. the selector in `closest('brawler-slot')`
355
+ // when the prop is `slot` — must be left intact. Split on string literals
356
+ // (single/double/backtick, honouring escapes) and substitute only the code spans
357
+ // between them. Template `${…}` interpolation counts as part of the literal here.
358
+ const stringLiteral = /(['"`])(?:\\.|(?!\1)[^\\])*\1/g;
359
+ const substituteInExpr = (expr, replacement) => {
360
+ let out = '';
361
+ let last = 0;
362
+ let m;
363
+ stringLiteral.lastIndex = 0;
364
+ while ((m = stringLiteral.exec(expr))) {
365
+ out += expr.slice(last, m.index).replace(idRegex, replacement) + m[0];
366
+ last = m.index + m[0].length;
367
+ }
368
+ return out + expr.slice(last).replace(idRegex, replacement);
369
+ };
354
370
 
355
371
  if (bindingMatch) {
356
372
  const path = bindingMatch[1];
@@ -607,8 +623,15 @@ const processSingle = (el, debug) => {
607
623
  // on the next hydrate.
608
624
  if (el._vibeIterPropIds) {
609
625
  newWrapper._vibeIterPropIds = el._vibeIterPropIds;
610
- newWrapper.setAttribute('data-vibe-iter-prop', '');
611
626
  el._vibeIterPropIds = null;
627
+ }
628
+ // The discovery marker rides on the registry path (props with iteration
629
+ // aliases) AND the global-only raw-binding path (no registry slot, just
630
+ // `_vibeIterPropExprs = []`). Transfer it whenever present so affected.js's
631
+ // walkInlinedComponentTrees can find the wrapper and re-hydrate its
632
+ // bindings on a global-state change.
633
+ if (el.hasAttribute('data-vibe-iter-prop')) {
634
+ newWrapper.setAttribute('data-vibe-iter-prop', '');
612
635
  el.removeAttribute('data-vibe-iter-prop');
613
636
  }
614
637
  // Transfer the original prop expressions too, so the iteration's
@@ -104,7 +104,14 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
104
104
  // live `@[stateKey]` bindings — which is what global state-change reactivity
105
105
  // depends on (the registry path snapshots a value and doesn't react to
106
106
  // global state changes on its own).
107
- if (Object.keys(parentScope).length > 0) {
107
+ // `parentScope` is populated on the initial-render path, but a conditional
108
+ // mounted via the update path (a parent conditional flipping true after load)
109
+ // — or any deeper-nested conditional — arrives with an empty parentScope, the
110
+ // iteration's scoped state flowing in through `state` instead. `scopeAliases`
111
+ // is set at parse time and persists, so it reliably marks a conditional that
112
+ // lexically lives inside an iteration regardless of which path mounts it — the
113
+ // Brawling loader, whose conditional flips true only once combat starts.
114
+ if (Object.keys(parentScope).length > 0 || node.meta.scopeAliases?.length) {
108
115
  node.runtime.inIteration = true;
109
116
  }
110
117
 
@@ -189,7 +196,7 @@ const mountBranch = (node, branchData, state, manifest, parentScope) => {
189
196
  // global-state bindings — going through the registry would snapshot the
190
197
  // value and break reactivity.
191
198
  if (node.runtime.inIteration) {
192
- resolveIterationComponentProps(clonedNodes, scopedState);
199
+ resolveIterationComponentProps(clonedNodes, scopedState, aliasSet);
193
200
  }
194
201
 
195
202
  // Insert cloned nodes into DOM and register in branch registry
@@ -3,7 +3,7 @@ import affected from './affected.js';
3
3
  import hydrate from './hydrate.js';
4
4
  import { resolvePath, getItemKey, computeDiff } from './iteration-utils.js';
5
5
  import { resolveThisPath, evalInScope, findComponentIdForElement, rememberScopedKeys } from './utils.js';
6
- import { managedNodes } from './conditionals.js';
6
+ import { managedNodes, extractDependencies } from './conditionals.js';
7
7
  import { stampInstanceScopes } from './loop-scope.js';
8
8
  import {
9
9
  BINDING_REGEX,
@@ -372,7 +372,7 @@ export const releaseOrphanedIterationProps = (nodes) => {
372
372
  // inlines the component, since by then iteration scope is gone. Only called
373
373
  // from iteration code paths; conditionals don't need this because their branch
374
374
  // content is registered in the global manifest and reacts to state updates.
375
- export const resolveIterationComponentProps = (nodes, scopedState) => {
375
+ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
376
376
  for (let n = 0; n < nodes.length; n++) {
377
377
  const node = nodes[n];
378
378
  if (node.nodeType !== 1) continue;
@@ -388,6 +388,26 @@ export const resolveIterationComponentProps = (nodes, scopedState) => {
388
388
  const match = attr.value.match(/^@\[(.+)\]$/);
389
389
  if (!match) continue;
390
390
  const expr = match[1];
391
+ // The registry snapshot only exists to carry iteration-local values
392
+ // (item/index/outer aliases, or component-local `this.X`) past the point
393
+ // where processComponent inlines the component and that scope is gone.
394
+ // A prop whose expression references ONLY globals doesn't need it — and
395
+ // routing it through the snapshot would freeze it, since the slot is
396
+ // refreshed solely on array diffs (the Brawling-loader freeze: a prop
397
+ // bound to `elapsedMilliseconds` in a row whose array never changes).
398
+ // Leave such a binding raw so the normal reactive path tracks the
399
+ // global, but still tag the wrapper: `_vibeIterPropExprs` makes index.js
400
+ // stamp `_vibeIterTree`, which is what lets affected.js's
401
+ // walkInlinedComponentTrees descend in and re-hydrate the raw binding on
402
+ // a global-state change.
403
+ const usesLocalScope =
404
+ /\bthis\b/.test(expr) ||
405
+ (aliases && extractDependencies(expr).some((d) => aliases.has(d)));
406
+ if (!usesLocalScope) {
407
+ el.setAttribute('data-vibe-iter-prop', '');
408
+ el._vibeIterPropExprs = el._vibeIterPropExprs || [];
409
+ continue;
410
+ }
391
411
  try {
392
412
  const value = evalInScope(expr, scopedState, el);
393
413
  if (value === undefined) continue;
@@ -1405,7 +1425,8 @@ const buildInstance = (iterationNode, item, index, state, parentScope, liveItem)
1405
1425
  const scopedState = createScopedState(state, localVars, parentScope);
1406
1426
  const componentId = findComponentIdForElement(startComment.parentElement);
1407
1427
  const built = initializeBlock([...template.element.childNodes], scopedState, template, componentId, new Set(iterationNode.meta.scopeAliases));
1408
- resolveIterationComponentProps(built.clonedNodes, scopedState);
1428
+ const scopeAliases = new Set([itemAlias, indexAlias, ...(iterationNode.meta.scopeAliases || [])]);
1429
+ resolveIterationComponentProps(built.clonedNodes, scopedState, scopeAliases);
1409
1430
  return { ...built, scopedState, localVars, liveItem };
1410
1431
  };
1411
1432