@ape-egg/vibe 2.1.13 → 2.1.15
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/parser/html.rs +7 -1
- package/package.json +1 -1
- package/runtime/component.js +17 -3
- package/runtime/conditionals.js +9 -2
- package/runtime/iterate.js +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.15] - 2026-06-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **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`.
|
|
8
|
+
|
|
9
|
+
## [2.1.14] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Compiler 1.9.8 → 1.9.9 — a component prop referenced *bare* inside an event handler threw at fire time** (`compiler/src/parser/html.rs`, `runtime/component.js`) — prop substitution into `on*` handlers only rewrote the `$.prop` form, so a bare prop reference (`onclick="pick(item)"` where `item` is a prop) was left untouched. Because a native inline handler runs in *global* scope when it fires, that bare identifier resolved to an undefined global and threw a `ReferenceError`. Both the compiler's `substitute_props` and the runtime's `renderPropsAndSlot` now also substitute the bare prop identifier — mirroring the existing `@[...]` binding pass — so the handler receives the live prop with object identity preserved: a loop-alias path (`row.sig`) becomes `(row.sig)`, which `parse.js` rewrites to the scoped accessor, and a literal prop (`limit="5"`) inlines as the numeric `5`. The bare pass skips the `$.prop` form (lookbehind guard) so the two rewrites don't collide. Tests: `e2e-runtime/prop-in-handler.html` + `components/test-prop-handler.html`, exercised by `tests/e2e/component-props.spec.js`.
|
|
14
|
+
|
|
3
15
|
## [2.1.13] - 2026-06-22
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
Binary file
|
|
Binary file
|
package/compiler/src/Cargo.lock
CHANGED
package/compiler/src/Cargo.toml
CHANGED
|
@@ -690,7 +690,13 @@ fn substitute_props(template: &str, props: &[(String, String)]) -> String {
|
|
|
690
690
|
.replace_all(&html, |c: ®ex::Captures| {
|
|
691
691
|
let ev = c.get(1).unwrap().as_str();
|
|
692
692
|
let body = c.get(2).unwrap().as_str();
|
|
693
|
-
|
|
693
|
+
// First the `$.prop` form, then the bare prop identifier — mirroring
|
|
694
|
+
// the runtime's two-step rewrite so `onclick="pick(item)"` resolves
|
|
695
|
+
// the live prop instead of throwing ReferenceError in global scope.
|
|
696
|
+
// The bare pass skips `$.prop` (its `.`-lookbehind guard) so the two
|
|
697
|
+
// don't collide.
|
|
698
|
+
let after_state = substitute_state_ref(body, prop_name, &state_repl);
|
|
699
|
+
let rewritten = substitute_identifier(&after_state, prop_name, &ident_repl);
|
|
694
700
|
if rewritten == body {
|
|
695
701
|
c.get(0).unwrap().as_str().to_string()
|
|
696
702
|
} else {
|
package/package.json
CHANGED
package/runtime/component.js
CHANGED
|
@@ -369,7 +369,12 @@ const renderPropsAndSlot = (temp, props, slotHtml) => {
|
|
|
369
369
|
);
|
|
370
370
|
const stateRegex = new RegExp(`\\$\\.${escapeRegex(propName)}(?![a-zA-Z0-9_$])`, 'gi');
|
|
371
371
|
transformedHtml = transformedHtml.replace(EVENT_ATTR_REGEX, (match, evName, body) => {
|
|
372
|
-
|
|
372
|
+
// Substitute the bare prop identifier too (not just the `$.prop` form),
|
|
373
|
+
// mirroring the @[...] binding pass above — so `onclick="pick(item)"`
|
|
374
|
+
// resolves the live prop, not a global that throws ReferenceError. A
|
|
375
|
+
// loop-alias path (`row.sig`) becomes `(row.sig)` here; parse.js then
|
|
376
|
+
// rewrites the alias to `$scope(this,'row')`.
|
|
377
|
+
const rewritten = substituteInExpr(body.replace(stateRegex, `$.${path}`), `(${path})`);
|
|
373
378
|
return rewritten === body ? match : `on${evName}="${rewritten}"`;
|
|
374
379
|
});
|
|
375
380
|
} else {
|
|
@@ -395,7 +400,9 @@ const renderPropsAndSlot = (temp, props, slotHtml) => {
|
|
|
395
400
|
);
|
|
396
401
|
const stateRegex = new RegExp(`\\$\\.${escapeRegex(propName)}(?![a-zA-Z0-9_$])`, 'gi');
|
|
397
402
|
transformedHtml = transformedHtml.replace(EVENT_ATTR_REGEX, (match, evName, body) => {
|
|
398
|
-
|
|
403
|
+
// Same bare-identifier substitution for a literal prop: `onclick="set(limit)"`
|
|
404
|
+
// with `limit="5"` becomes `set(5)`.
|
|
405
|
+
const rewritten = substituteInExpr(body.replace(stateRegex, `$.${literal}`), literal);
|
|
399
406
|
return rewritten === body ? match : `on${evName}="${rewritten}"`;
|
|
400
407
|
});
|
|
401
408
|
}
|
|
@@ -600,8 +607,15 @@ const processSingle = (el, debug) => {
|
|
|
600
607
|
// on the next hydrate.
|
|
601
608
|
if (el._vibeIterPropIds) {
|
|
602
609
|
newWrapper._vibeIterPropIds = el._vibeIterPropIds;
|
|
603
|
-
newWrapper.setAttribute('data-vibe-iter-prop', '');
|
|
604
610
|
el._vibeIterPropIds = null;
|
|
611
|
+
}
|
|
612
|
+
// The discovery marker rides on the registry path (props with iteration
|
|
613
|
+
// aliases) AND the global-only raw-binding path (no registry slot, just
|
|
614
|
+
// `_vibeIterPropExprs = []`). Transfer it whenever present so affected.js's
|
|
615
|
+
// walkInlinedComponentTrees can find the wrapper and re-hydrate its
|
|
616
|
+
// bindings on a global-state change.
|
|
617
|
+
if (el.hasAttribute('data-vibe-iter-prop')) {
|
|
618
|
+
newWrapper.setAttribute('data-vibe-iter-prop', '');
|
|
605
619
|
el.removeAttribute('data-vibe-iter-prop');
|
|
606
620
|
}
|
|
607
621
|
// Transfer the original prop expressions too, so the iteration's
|
package/runtime/conditionals.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/runtime/iterate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|