@ape-egg/vibe 2.1.15 → 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 +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 +26 -1
- package/package.json +1 -1
- package/runtime/component.js +17 -1
- package/runtime/iterate.js +71 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [2.1.16] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
3
15
|
## [2.1.15] - 2026-06-22
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
Binary file
|
|
Binary file
|
package/compiler/src/Cargo.lock
CHANGED
package/compiler/src/Cargo.toml
CHANGED
|
@@ -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(
|
|
579
|
+
out.push(b);
|
|
555
580
|
i += 1;
|
|
556
581
|
}
|
|
557
582
|
}
|
package/package.json
CHANGED
package/runtime/component.js
CHANGED
|
@@ -350,7 +350,23 @@ const renderPropsAndSlot = (temp, props, slotHtml) => {
|
|
|
350
350
|
'gi'
|
|
351
351
|
);
|
|
352
352
|
|
|
353
|
-
|
|
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];
|
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
|
|
@@ -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
|
-
|
|
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 };
|