@ape-egg/vibe 2.1.16 → 2.1.18
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/package.json +1 -1
- package/runtime/iterate.js +116 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.18] - 2026-06-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **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`.
|
|
8
|
+
|
|
9
|
+
## [2.1.17] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Compiled inlined components inside an iteration shared one local-state bucket across all rows** (`runtime/iterate.js`) — in compiled mode each inlined component carries a fixed `data-vibe-component-id` (`_cN`) with its `@[this.x]` bindings and `$.this.x` handlers stamped to that id. An iteration clones its template once per row, so every row reused the same baked id — and therefore the same `component({...})` state bucket — and opening one row's ability drawer opened them all (two brawler slots both resolving to `_c2.open`). `initializeBlock` now isolates components per row: `isolateInlinedComponentIds` remaps every baked `_cN` in the clone to a fresh `generateComponentId()` and rewrites the `_cN.prop` references in attributes and text (boundary-anchored so `_c2` never matches inside `_c20`), then runs the row's inlined `vibe-module` setup scripts so each registers isolated state under its fresh id — mirroring the conditional-branch path. Batch render is also disabled for templates carrying an inlined component script (it emits one shared HTML string per row, which would duplicate the baked id), routing them through the clone-and-isolate path instead. Runtime mode is unaffected: there are no baked ids there — components are still `<component src>`.
|
|
14
|
+
|
|
3
15
|
## [2.1.16] - 2026-06-22
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
package/runtime/iterate.js
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
// Pre-compiled iteration optimization (production)
|
|
17
17
|
import * as compiled from './pre-compiled-iterations.js';
|
|
18
18
|
|
|
19
|
+
import { generateComponentId, executeCompiledComponentScriptsIn } from './component.js';
|
|
20
|
+
|
|
19
21
|
// Runtime batch-render helpers for full-replacement of simple templates.
|
|
20
22
|
// Build an HTML string via template-literal compilation, then parse once —
|
|
21
23
|
// avoids per-item clone/parse/hydrate in the hot path.
|
|
@@ -56,6 +58,14 @@ const hasNestedStructures = (tree) => {
|
|
|
56
58
|
const hasComponentSrc = (templateEl) =>
|
|
57
59
|
!!templateEl.querySelector?.('component[src], div.component[src]');
|
|
58
60
|
|
|
61
|
+
// A compiled inlined component with its own setup script carries component-local
|
|
62
|
+
// state (`component({...})` → `$._cN`). Batch render emits one shared HTML string
|
|
63
|
+
// per row, which would duplicate the baked `_cN` id across rows and collapse
|
|
64
|
+
// their state into one bucket. Route these through the clone path, where
|
|
65
|
+
// initializeBlock isolates each row's component ids.
|
|
66
|
+
const hasInlinedComponentScript = (templateEl) =>
|
|
67
|
+
!!templateEl.querySelector?.('script[type="vibe-module"]');
|
|
68
|
+
|
|
59
69
|
// `__vibeForceClonePath` is a debug/test escape hatch — set it on globalThis to
|
|
60
70
|
// route every iteration through the clone+hydrate path, even templates that
|
|
61
71
|
// would otherwise qualify for batch. Used by the batch-vs-clone-equivalence
|
|
@@ -65,7 +75,8 @@ const canUseBatchRender = (template) =>
|
|
|
65
75
|
!globalThis.__vibeForceClonePath &&
|
|
66
76
|
!hasNestedStructures(template) &&
|
|
67
77
|
template.element.children.length <= 1 &&
|
|
68
|
-
!hasComponentSrc(template.element)
|
|
78
|
+
!hasComponentSrc(template.element) &&
|
|
79
|
+
!hasInlinedComponentScript(template.element);
|
|
69
80
|
|
|
70
81
|
// Patterns used by compileBatchFn to recognize bindings in attribute-name and
|
|
71
82
|
// attribute-value positions. The inner alternation mirrors BINDING_INNER from
|
|
@@ -357,6 +368,50 @@ export const releaseOrphanedIterationProps = (nodes) => {
|
|
|
357
368
|
}
|
|
358
369
|
};
|
|
359
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
|
+
|
|
360
415
|
// For <component src> elements inside an iteration instance, evaluate any
|
|
361
416
|
// `@[expr]` attribute bindings against the iteration's scoped state and route
|
|
362
417
|
// every resolved value through the global iteration-prop registry. The prop
|
|
@@ -422,6 +477,7 @@ export const resolveIterationComponentProps = (nodes, scopedState, aliases) => {
|
|
|
422
477
|
// Leave binding raw — processComponent will handle it as a binding
|
|
423
478
|
}
|
|
424
479
|
}
|
|
480
|
+
resolveSlotContentBindings(el, scopedState, aliases);
|
|
425
481
|
}
|
|
426
482
|
}
|
|
427
483
|
};
|
|
@@ -795,7 +851,57 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
795
851
|
* that runs while clones are still detached.
|
|
796
852
|
* @returns {Object} { element, tree, clonedNodes }
|
|
797
853
|
*/
|
|
798
|
-
|
|
854
|
+
// Compiled mode bakes a fixed `data-vibe-component-id` into each inlined
|
|
855
|
+
// component and stamps its `@[this.x]` bindings / `$.this.x` handlers to that
|
|
856
|
+
// id. An iteration clones its template once per row, so every row would
|
|
857
|
+
// otherwise share that one id — and thus one local-state bucket. (Two brawler
|
|
858
|
+
// slots both resolving to `_c2.open` is why opening one ability drawer opened
|
|
859
|
+
// them all.) Per row, remap every baked id in the clone to a fresh one and
|
|
860
|
+
// rewrite the references to it; the caller then runs the row's component
|
|
861
|
+
// scripts so each registers isolated state under its fresh id. Runtime mode has
|
|
862
|
+
// no baked ids here (components are still `<component src>`), so this no-ops.
|
|
863
|
+
const COMPONENT_ID = /^_c\d+$/;
|
|
864
|
+
|
|
865
|
+
const isolateInlinedComponentIds = (container) => {
|
|
866
|
+
const remap = new Map();
|
|
867
|
+
for (const el of container.querySelectorAll('[data-vibe-component-id]')) {
|
|
868
|
+
const oldId = el.getAttribute('data-vibe-component-id');
|
|
869
|
+
if (!COMPONENT_ID.test(oldId)) continue;
|
|
870
|
+
if (!remap.has(oldId)) remap.set(oldId, generateComponentId());
|
|
871
|
+
el.setAttribute('data-vibe-component-id', remap.get(oldId));
|
|
872
|
+
}
|
|
873
|
+
if (!remap.size) return false;
|
|
874
|
+
|
|
875
|
+
// `_c2` must not match inside `_c20` or a longer identifier, so anchor on a
|
|
876
|
+
// non-word/`$` boundary before and a non-digit/word after. Bindings always
|
|
877
|
+
// read the id as `_cN.prop`, so the trailing `.` satisfies the lookahead.
|
|
878
|
+
const refs = [...remap].map(([oldId, newId]) => [
|
|
879
|
+
new RegExp(`(?<![\\w$])${oldId}(?![\\w\\d])`, 'g'),
|
|
880
|
+
newId,
|
|
881
|
+
]);
|
|
882
|
+
const rewrite = (str) => {
|
|
883
|
+
let out = str;
|
|
884
|
+
for (const [re, newId] of refs) out = out.replace(re, newId);
|
|
885
|
+
return out;
|
|
886
|
+
};
|
|
887
|
+
const walk = (node) => {
|
|
888
|
+
if (node.nodeType === 1) {
|
|
889
|
+
for (const attr of node.attributes) {
|
|
890
|
+
if (attr.value.includes('_c')) {
|
|
891
|
+
const next = rewrite(attr.value);
|
|
892
|
+
if (next !== attr.value) attr.value = next;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (const child of node.childNodes) walk(child);
|
|
896
|
+
} else if (node.nodeType === 3 && node.textContent.includes('_c')) {
|
|
897
|
+
node.textContent = rewrite(node.textContent);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
for (const node of container.childNodes) walk(node);
|
|
901
|
+
return true;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
export const initializeBlock = (templateNodes, scopedState, cachedTree = null, componentId = null, aliasSet = undefined, isolateComponents = false) => {
|
|
799
905
|
let tree;
|
|
800
906
|
let clonedNodes = [];
|
|
801
907
|
let firstElement = null;
|
|
@@ -833,6 +939,13 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
|
|
|
833
939
|
if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
|
|
834
940
|
}
|
|
835
941
|
|
|
942
|
+
// Give this row its own component ids (compiled mode) before parse() reads the
|
|
943
|
+
// bindings, then run the row's inlined setup scripts so each registers its own
|
|
944
|
+
// local state under the fresh id — mirroring the conditional-branch path.
|
|
945
|
+
if (isolateComponents && isolateInlinedComponentIds(parseContainer)) {
|
|
946
|
+
executeCompiledComponentScriptsIn([...parseContainer.childNodes]);
|
|
947
|
+
}
|
|
948
|
+
|
|
836
949
|
if (useCachedTree) {
|
|
837
950
|
// Fast path: map cached tree structure onto cloned DOM (no regex, no DOM walking)
|
|
838
951
|
tree = cloneTreeWithElements(cachedTree, parseContainer);
|
|
@@ -1424,7 +1537,7 @@ const buildInstance = (iterationNode, item, index, state, parentScope, liveItem)
|
|
|
1424
1537
|
const localVars = { [itemAlias]: liveItem, [indexAlias]: index };
|
|
1425
1538
|
const scopedState = createScopedState(state, localVars, parentScope);
|
|
1426
1539
|
const componentId = findComponentIdForElement(startComment.parentElement);
|
|
1427
|
-
const built = initializeBlock([...template.element.childNodes], scopedState, template, componentId, new Set(iterationNode.meta.scopeAliases));
|
|
1540
|
+
const built = initializeBlock([...template.element.childNodes], scopedState, template, componentId, new Set(iterationNode.meta.scopeAliases), true);
|
|
1428
1541
|
const scopeAliases = new Set([itemAlias, indexAlias, ...(iterationNode.meta.scopeAliases || [])]);
|
|
1429
1542
|
resolveIterationComponentProps(built.clonedNodes, scopedState, scopeAliases);
|
|
1430
1543
|
return { ...built, scopedState, localVars, liveItem };
|