@ape-egg/vibe 2.1.16 → 2.1.17

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.17] - 2026-06-22
4
+
5
+ ### Fixed
6
+
7
+ - **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>`.
8
+
3
9
  ## [2.1.16] - 2026-06-22
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
@@ -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
@@ -795,7 +806,57 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
795
806
  * that runs while clones are still detached.
796
807
  * @returns {Object} { element, tree, clonedNodes }
797
808
  */
798
- export const initializeBlock = (templateNodes, scopedState, cachedTree = null, componentId = null, aliasSet = undefined) => {
809
+ // Compiled mode bakes a fixed `data-vibe-component-id` into each inlined
810
+ // component and stamps its `@[this.x]` bindings / `$.this.x` handlers to that
811
+ // id. An iteration clones its template once per row, so every row would
812
+ // otherwise share that one id — and thus one local-state bucket. (Two brawler
813
+ // slots both resolving to `_c2.open` is why opening one ability drawer opened
814
+ // them all.) Per row, remap every baked id in the clone to a fresh one and
815
+ // rewrite the references to it; the caller then runs the row's component
816
+ // scripts so each registers isolated state under its fresh id. Runtime mode has
817
+ // no baked ids here (components are still `<component src>`), so this no-ops.
818
+ const COMPONENT_ID = /^_c\d+$/;
819
+
820
+ const isolateInlinedComponentIds = (container) => {
821
+ const remap = new Map();
822
+ for (const el of container.querySelectorAll('[data-vibe-component-id]')) {
823
+ const oldId = el.getAttribute('data-vibe-component-id');
824
+ if (!COMPONENT_ID.test(oldId)) continue;
825
+ if (!remap.has(oldId)) remap.set(oldId, generateComponentId());
826
+ el.setAttribute('data-vibe-component-id', remap.get(oldId));
827
+ }
828
+ if (!remap.size) return false;
829
+
830
+ // `_c2` must not match inside `_c20` or a longer identifier, so anchor on a
831
+ // non-word/`$` boundary before and a non-digit/word after. Bindings always
832
+ // read the id as `_cN.prop`, so the trailing `.` satisfies the lookahead.
833
+ const refs = [...remap].map(([oldId, newId]) => [
834
+ new RegExp(`(?<![\\w$])${oldId}(?![\\w\\d])`, 'g'),
835
+ newId,
836
+ ]);
837
+ const rewrite = (str) => {
838
+ let out = str;
839
+ for (const [re, newId] of refs) out = out.replace(re, newId);
840
+ return out;
841
+ };
842
+ const walk = (node) => {
843
+ if (node.nodeType === 1) {
844
+ for (const attr of node.attributes) {
845
+ if (attr.value.includes('_c')) {
846
+ const next = rewrite(attr.value);
847
+ if (next !== attr.value) attr.value = next;
848
+ }
849
+ }
850
+ for (const child of node.childNodes) walk(child);
851
+ } else if (node.nodeType === 3 && node.textContent.includes('_c')) {
852
+ node.textContent = rewrite(node.textContent);
853
+ }
854
+ };
855
+ for (const node of container.childNodes) walk(node);
856
+ return true;
857
+ };
858
+
859
+ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, componentId = null, aliasSet = undefined, isolateComponents = false) => {
799
860
  let tree;
800
861
  let clonedNodes = [];
801
862
  let firstElement = null;
@@ -833,6 +894,13 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null, c
833
894
  if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
834
895
  }
835
896
 
897
+ // Give this row its own component ids (compiled mode) before parse() reads the
898
+ // bindings, then run the row's inlined setup scripts so each registers its own
899
+ // local state under the fresh id — mirroring the conditional-branch path.
900
+ if (isolateComponents && isolateInlinedComponentIds(parseContainer)) {
901
+ executeCompiledComponentScriptsIn([...parseContainer.childNodes]);
902
+ }
903
+
836
904
  if (useCachedTree) {
837
905
  // Fast path: map cached tree structure onto cloned DOM (no regex, no DOM walking)
838
906
  tree = cloneTreeWithElements(cachedTree, parseContainer);
@@ -1424,7 +1492,7 @@ const buildInstance = (iterationNode, item, index, state, parentScope, liveItem)
1424
1492
  const localVars = { [itemAlias]: liveItem, [indexAlias]: index };
1425
1493
  const scopedState = createScopedState(state, localVars, parentScope);
1426
1494
  const componentId = findComponentIdForElement(startComment.parentElement);
1427
- const built = initializeBlock([...template.element.childNodes], scopedState, template, componentId, new Set(iterationNode.meta.scopeAliases));
1495
+ const built = initializeBlock([...template.element.childNodes], scopedState, template, componentId, new Set(iterationNode.meta.scopeAliases), true);
1428
1496
  const scopeAliases = new Set([itemAlias, indexAlias, ...(iterationNode.meta.scopeAliases || [])]);
1429
1497
  resolveIterationComponentProps(built.clonedNodes, scopedState, scopeAliases);
1430
1498
  return { ...built, scopedState, localVars, liveItem };