@ape-egg/vibe 2.1.22 → 3.0.0

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.
Files changed (56) hide show
  1. package/README.md +112 -5
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +26 -17
  6. package/llms.txt +36 -5
  7. package/package.json +20 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +360 -98
  11. package/runtime/conditionals.js +111 -14
  12. package/runtime/debug.js +24 -0
  13. package/runtime/dispatch.js +172 -0
  14. package/runtime/hydrate.js +277 -110
  15. package/runtime/index.js +189 -65
  16. package/runtime/iterate.js +125 -50
  17. package/runtime/iteration-utils.js +59 -8
  18. package/runtime/manifest.js +77 -2
  19. package/runtime/parse.js +81 -11
  20. package/runtime/pre-compiled-iterations.js +19 -6
  21. package/runtime/pre-compiled-manifest.js +13 -4
  22. package/runtime/staging.js +153 -0
  23. package/runtime/state.js +31 -0
  24. package/runtime/tracking.js +173 -0
  25. package/runtime/utils.js +155 -78
  26. package/spa.js +206 -0
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1159
  29. package/ROADMAP.md +0 -397
  30. package/compiler/bin/vibe-compile.js +0 -121
  31. package/compiler/native/.gitkeep +0 -0
  32. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  33. package/compiler/native/vibe-compiler-linux-x64 +0 -0
  34. package/compiler/src/Cargo.lock +0 -2023
  35. package/compiler/src/Cargo.toml +0 -38
  36. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
  37. package/compiler/src/compiler/binding_case.rs +0 -88
  38. package/compiler/src/compiler/compile.rs +0 -2522
  39. package/compiler/src/compiler/component_tagger.rs +0 -469
  40. package/compiler/src/compiler/iteration_optimizer.rs +0 -455
  41. package/compiler/src/compiler/js_analyzer.rs +0 -715
  42. package/compiler/src/compiler/manifest_builder.rs +0 -693
  43. package/compiler/src/compiler/mod.rs +0 -15
  44. package/compiler/src/compiler/name_binding_protect.rs +0 -207
  45. package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
  46. package/compiler/src/compiler/state_extractor.rs +0 -263
  47. package/compiler/src/compiler/value_stamper.rs +0 -921
  48. package/compiler/src/compiler/watcher.rs +0 -1147
  49. package/compiler/src/config.rs +0 -239
  50. package/compiler/src/main.rs +0 -347
  51. package/compiler/src/parser/element.rs +0 -96
  52. package/compiler/src/parser/html.rs +0 -1004
  53. package/compiler/src/parser/mod.rs +0 -8
  54. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  55. package/runtime/scope.js +0 -50
  56. package/test-results/.last-run.json +0 -4
package/CHANGELOG.md DELETED
@@ -1,1159 +0,0 @@
1
- # Changelog
2
-
3
- ## [2.1.22] - 2026-07-02
4
-
5
- ### Added
6
-
7
- - **`$.on('unmount', callback)` — scope-resolved teardown event** (`runtime/component.js`, `runtime/index.js`, `index.js`) — one event name, one concept ("this context is going away"), resolved by where you subscribe. **In a component `<script>`**: the callback runs when THAT component unmounts (conditional toggle, iteration removal, reactive-src swap) and before an HMR re-run of the same component id — the scoped-`$` proxy intercepts the event name and rides the exact registry `$.on(...)` unsubscribes already use (`__vibeComponentCleanups`, drained by `releaseOrphanedComponentState`/`runComponentCleanups`). No new lifecycle machinery. **At page level**: the same subscription fires on `pagehide` — the visitor navigating away or closing the tab. Deliberately NOT `visibilitychange`: a tab switch is not an unmount, the visitor comes back (separate `hide`/`show` visibility events can be added later without touching this). Closes the SPA-mode gap where a component-owned side effect (`setInterval`, `addEventListener`, a socket) outlived its component because nothing could reach the handle: `const t = setInterval(…); $.on('unmount', () => clearInterval(t))` is the whole story. Both scopes return an unsubscribe like every other `$.on`; the pre-boot placeholder queues `unmount` subscriptions like the other events (`index.js` `_pendingListeners`), so page-level registrations made before boot aren't dropped. Works identically in runtime and compiled modes (compiled `vibe-module` scripts run through the same scoped-`$` path). Tests: `e2e-runtime/component-unmount.html` + `tests/e2e/component-unmount.spec.js` (both modes: callback fires exactly once per component unmount, interval verifiably stops, remount registers fresh, page-level fires on pagehide via localStorage trace, unsubscribe cancels).
8
- - **Reactive component src — `<component src="@[page.src]">`** (`runtime/parse.js`, `runtime/hydrate.js`, `runtime/component.js`) — the SPA routing primitive: one wrapper that fetches whatever the bound state resolves to and **re-mounts when it changes**, replacing the `<!-- if -->`-ladder-per-route pattern. `parse.js` captures ONLY the `src` binding on fetched components (every other attribute is still a raw prop owned by processComponent); initial hydration resolves the binding *before* the fetch scan, so the first mount rides the normal pipeline unchanged. On a state change, hydrate routes the update to `remountComponent`, which aborts any in-flight fetch (`pendingFetches` deletes are now ownership-guarded so an aborted fetch's cleanup can't wipe a newer fetch's controller), re-fetches, and re-mounts; the authored props and slot content ride a remount context finalize stashes on each wrapper (`_vibeMountedSrc`/`_vibeRemountProps`/`_vibeSlotContent`), and the outgoing component's state is evicted by the existing removal pass. The subtle part: every mount **replaces** the wrapper, and the observer's removal handling prunes the replaced element's tree node — taking the binding knowledge with it. So the authored binding travels ON the wrapper as `data-vibe-src` (same transport idea as `data-vibe-namebind`): the replacement's reparse recaptures it from the DOM alone, keeping the knowledge alive across arbitrarily many navigations — DOM-first, no tree surgery. A state change landing inside the swap window still resolves through the `_vibeReplacedBy` chain (now path-compressed so detached intermediates stay collectable). Compiled mode works through the same runtime path with zero compiler changes: inlining structurally skips a bound src (the target is unknowable at build time), the stamper resolves the initial value, and the manifest carries the binding — locked in by `tests/compiler/component-src-binding/`. Tests: `tests/unit/parse.test.js` (bound-src + data-vibe-src capture), `e2e-runtime/component-src-binding.html` + `tests/e2e/component-src-binding.spec.js` (both modes: initial mount, swap, prop/slot survival across re-mounts, state eviction, same-src no-op, post-swap interactivity).
9
-
10
- ### Fixed
11
-
12
- - **Deep-link reloads on catch-all routes hydrated blank — manifest resolution didn't understand `:name*`** (`runtime/pre-compiled-manifest.js`) — the route-aware manifest fast path tokenizes `:param` positions of `window.__ROUTE__` *by segment index*, which is meaningless for a trailing catch-all that matches zero-or-more segments: for `/x/fighter/7` under route `/x/:route*` it produced `/vibe-hyperspeed/x/$/7.html.manifest.js` (and the literal fallbacks 404'd too), so a compiled catch-all page (`pages/x/$$route.html`, the SPA-shell convention) went blank on every reload/deep link while client-side navigation worked. The compiler collapses `$$name.html` to the same `$` manifest token as a single `$param`, so there is exactly ONE manifest for every depth — `buildManifestCandidatePaths` now detects a trailing `:name*` and emits `<static-prefix>/$.html.manifest.js` (prefix `:param`s still tokenized), built from the raw pathname so the zero-extra-segments case (`/x` itself) resolves identically. One direct-hit request, no 404 probing. Tests: `tests/unit/pre-compiled-manifest.test.js` (locks single-param and mid-path-param behavior, catch-all at three depths, catch-all after a mid-path param); verified live: reload on `/…/fighter/7`, `/…/timer`, and the bare base all hydrate with a single 200 manifest fetch.
13
- - **Watch mode dropped edits to runtime-fetched components** (`compiler/src/compiler/compile.rs`, `compiler/src/compiler/watcher.rs`, compiler 2.0.2 → 2.0.3) — the full build always mirrors `components/` verbatim into the output, because runtime-fetched components (iter-prop each-roots, `components_as_is`, and now `<component src="@[page.src]">` targets) are served from that mirror at request time — but the watch loop never honored that contract. A changed component only triggered recompiles of its *dependent pages* (refreshing their inlined copies); the mirror itself was never rewritten, and a component that **no page inlines** — exactly what every reactive-src routing target is — mapped to zero dependents and was silently dropped: no log line, no output write, stale bytes served until the next full build (observed in Battle Brawlers: SPA-demo components edited under `dev:compiled` never reached the browser, while page edits landed fine). The watch loop now re-mirrors every changed `components/` file into the output via the new `mirror_component_files` (atomic tmp+rename, same torn-read discipline as compiled pages, parent dirs created for components born mid-session), removes deleted components from the mirror, and logs the change even with zero dependents. Tests: `compile.rs` (`changed_component_is_remirrored_to_output`); verified end-to-end against a live `--watch` process (edit → mirror updates, delete → mirror entry removed).
14
-
15
- ## [2.1.21] - 2026-07-01
16
-
17
- ### Fixed
18
-
19
- - **An orphaned `--watch` compiler held its lock forever, blocking every future watch on the same output** (`compiler/bin/vibe-compile.js`, `compiler/src/compiler/watcher.rs`, compiler 2.0.2) — the watch lock added below is only released on clean shutdown (Drop), but the compiler is a native child spawned by the `vibe-compile.js` wrapper, and killing the wrapper (Ctrl+C, a dev server's `child.kill()`, a process manager) left that child alive as an orphan still holding the lock — so the next `--watch` exited loudly naming a holder pid that was long gone. Two coordinated fixes close both escape routes: **(1)** the wrapper now forwards `SIGINT`/`SIGTERM`/`SIGHUP` and its own `exit` to the spawned child (`wireLifecycle`), so a graceful kill of the wrapper unwinds the watcher's Drop and frees the lock normally; **(2)** for the ungraceful case (`SIGKILL`, a crashed parent) where no signal is forwarded, the watcher polls its own parent pid on a background thread (`exit_when_orphaned`, unix-only) and, the moment it's reparented to init/a reaper — the unambiguous orphan signal — removes its lockfile and `process::exit`s, since `exit` skips Drop. Together they guarantee a dead owner never leaves a lock behind, complementing the stale-lock stealing the next watcher already does.
20
- - **Concurrent compilers corrupted hyperspeed manifests → pages hydrated blank with no errors** (`compiler/src/compiler/compile.rs`, `compiler/src/compiler/watcher.rs`, compiler 2.0.1 → 2.0.2) — manifest generation re-read each compiled page **from disk** after writing it, so when two `vibe compile --watch` processes shared one output directory (a leaked dev-server session next to a live one), one watcher's manifest pass could read a page the other was mid-rewrite. html5ever parses the torn prefix into a well-formed shell, producing a *valid but content-less* manifest that doesn't match its HTML — hydration mounts nothing, `vibe-fouc` never releases, and the page renders blank with zero console errors (observed in Battle Brawlers: a random subset of pages broke on every save of a widely-used component). Three-layer fix: **(1)** the compiler keeps each page's compiled HTML in memory (`Compiler.compiled_html`) and both manifest passes (`generate_manifests`, `generate_manifests_for_files`) build from those exact bytes — disk is only a fallback for pages the running compiler never produced (no-clean leftovers); **(2)** compiled HTML, stamped HTML, and manifests are written atomically (same-directory pid-tagged temp file + `rename`) so no reader — dev server, browser, or another process — can ever observe a partial file; **(3)** `watch` takes a per-output-directory lockfile (OS temp dir, keyed by canonicalized output path, holding the owner pid): a second watcher on the same output now exits loudly naming the holder instead of silently double-compiling and racing, and a lock whose process is dead (Ctrl+C/SIGTERM never unwind) is stolen. Tests: `compile.rs` (`manifest_survives_output_corruption_between_compile_and_manifests`, `incremental_manifest_survives_output_corruption`, `atomic_write_replaces_content_without_leaving_tmp_files`), `watcher.rs` (`second_watch_lock_on_same_output_fails_while_held`, `stale_lock_from_dead_process_is_stolen`, `locks_on_different_outputs_do_not_conflict`).
21
-
22
- ## [2.1.20] - 2026-06-25
23
-
24
- ### Added
25
-
26
- - **`@ape-egg/vibe/hot-module-refresh` — transport-agnostic browser HMR client** (`hot-module-refresh.js` (new), exported from `package.json`) — the surgical-HMR "brain" that reconciles a code edit into the live DOM instead of reloading the page is extracted out of `vite-plugin-vibe` into a standalone flat module the vibe package now exports. It's a *soft dependency*: nothing in the runtime imports it and it imports nothing from the runtime, depending only on the public `window.$` surface (`reconcile` / `renderComponent` / `clearComponentCache`), so a plain static server can serve it as-is — no bundler, no Vite. A transport adapter wires its channel to the brain through a single seam, `setupHotModuleRefresh({ debug, subscribe })`, where `subscribe` delivers two callbacks: `componentUpdate(path)` — re-fetch a changed component template and, for each live instance, reconcile surgically when its scripts are unchanged or re-mount otherwise (runtime mode only, since compiled output inlines components into pages) — and `pageUpdate(payload)` — re-fetch the current page and reconcile the `[vibe]` root (mode-agnostic; raw and compiled pages reconcile the same way). This lets the Vite plugin and any other dev transport share one HMR implementation. Tests: `e2e-runtime/hot-module-refresh.html`, `tests/e2e/hot-module-refresh.spec.js`.
27
-
28
- ## [2.1.19] - 2026-06-25
29
-
30
- ### Fixed
31
-
32
- - **`<!-- each list as item, i (item.id) -->` (index alias before the key) silently failed to iterate** (`runtime/constants.js`, `runtime/iteration-utils.js`, `runtime/parse.js`, `compiler/src/compiler/manifest_builder.rs`) — the iteration header grammar only accepted the key before the index (`as item (item.id), i`). When the index came first, the trailing `(item.id)` made `ITERATION_REGEX` fail to match, so `parse.js` skipped the comment entirely and the body rendered once with an undefined alias instead of iterating. `ITERATION_REGEX` now accepts the `(key)` expression in either position (a second optional key group), and a new `parseIterationHeader` helper coalesces the two and is the single source of truth used by `parse.js`. Compiled pages re-parse the preserved each comment through the same helper, so they were fixed by the runtime change; additionally the Rust `manifest_builder` (compiler 2.0.0 → 2.0.1) now strips the key before the item/index split so the emitted manifest carries a clean `indexAlias` (was `"i (item.id)"`) for both orderings. Tests: `tests/unit/iteration-utils.test.js` (parseIterationHeader), `tests/compiler/iterations-index-key/`, `e2e-runtime/iteration-index-key.html` + `tests/e2e/iteration-index-key.spec.js` (runtime + compiled).
33
-
34
- ## [2.1.18] - 2026-06-22
35
-
36
- ### Fixed
37
-
38
- - **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`.
39
-
40
- ## [2.1.17] - 2026-06-22
41
-
42
- ### Fixed
43
-
44
- - **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>`.
45
-
46
- ## [2.1.16] - 2026-06-22
47
-
48
- ### Fixed
49
-
50
- - **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.
51
-
52
- ## [2.1.15] - 2026-06-22
53
-
54
- ### Fixed
55
-
56
- - **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`.
57
-
58
- ## [2.1.14] - 2026-06-22
59
-
60
- ### Fixed
61
-
62
- - **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`.
63
-
64
- ## [2.1.13] - 2026-06-22
65
-
66
- ### Fixed
67
-
68
- - **Compiler 1.9.7 → 1.9.8 — name-bindings whose expression contains whitespace were torn apart by the HTML parser** (`compiler/src/compiler/name_binding_protect.rs` (new), `compile.rs`, `manifest_builder.rs`, `iteration_optimizer.rs`; `runtime/parse.js`, `iterate.js`, `hydrate.js`) — a name-binding sits in attribute-*name* position (`<icon @[element]>`), and HTML parsers split an attribute token on whitespace. So once a prop inlined an expression that has spaces (`@[EQUIPMENT(item, true).element]`), html5ever (and the browser) tore it into broken attributes and dropped the binding from the manifest — the element silently kept its fallback in compiled mode while non-compiled mode worked. The compiler now relocates every whitespace-bearing name-binding into a single verbatim value attribute `data-vibe-namebind="@[expr]…"` *before* any parse round-trip (value attributes survive byte-for-byte); the manifest builder and iteration optimizer read the expression back out of it, and the runtime restores the `@[expr]` name-binding form (`parse.js`, `iterate.js`) and strips the transport attribute on hydrate (`hydrate.js`). Whitespace-free name-bindings are untouched. Tests: `tests/compiler/name-binding-whitespace`, `tests/e2e/name-binding-prop`, `name-bindings`.
69
- - **Compiler 1.9.7 → 1.9.8 — `--minify` deleted significant whitespace between inline elements** (`compiler/src/compiler/compile.rs`) — minification collapsed `>\s+<` to `><`, removing whitespace between tags. For inline content that space is significant: the browser (and the non-compiled runtime) render `<em>a</em> <em>b</em>` with a space, so a status chip glued onto its following word (`💠Concussion`) in compiled+minified mode. Minify now collapses inter-element whitespace to a *single space* rather than deleting it — matching both the browser's own collapsing and the non-minified output; where the space is insignificant (between block/table/list/head elements) the parser discards it anyway. Repro: `tests/compiler/minify-significant-whitespace`.
70
- - **Name-bindings inside an iteration couldn't resolve props stashed in the iter-props global** (`runtime/utils.js`) — `resolveCaseInsensitivePath` only walked the diff state, but a name-binding inside an `<!-- each -->` reads its props from a global stash (`window.__vibeiterprops._p0.statusKey`), so a state-only walk never reached it and the camelCase leaf the parser lowercased stayed unresolved (the status-chip gray-icon parity bug — resolved in compiled mode, not runtime). It now resolves the path's root against the same scope and order `evalInScope` uses — the diff state including scoped loop aliases (via `ownKeysOf`), then globals — so iter-prop stashes and scoped aliases resolve case-insensitively. Covered by `tests/unit/utils.test.js`.
71
-
72
- ## [2.1.12] - 2026-06-22
73
-
74
- ### Fixed
75
-
76
- - **Nodes resolving inside slot-projected branch/iteration content were orphaned from the reactive tree** (`runtime/index.js`) — `navigateTree` only walks a node's `children`, but content projected across a `<slot>` boundary (a conditional branch's or an iteration instance's slotted content) is registered in the flat manifest under a path that isn't reachable through `children` — the slot node's children don't include the projected subtree. So when a `<component src>` (or any node) mounted inside such content, `navigateTree` returned null: on add, the resolved subtree never linked into the reactive tree (so it wasn't reactive); on remove, the stale tree entry lingered beside its replacement. A new `findNodeByElement` does an identity search across the *full* tree — plain `children`, conditional branch trees (`runtime.activeInstance.parsedTree`), and iteration instance trees (`runtime.instances`) — and is used as a fallback at both the add and remove sites when path navigation can't reach the parent, so the subtree links in and cleans up regardless of slot/branch/iteration projection.
77
-
78
- ## [2.1.11] - 2026-06-22
79
-
80
- ### Fixed
81
-
82
- - **Compiler 1.9.6 → 1.9.7 — name-binding expressions were lowercased, breaking camelCase identifiers** (`compiler/src/compiler/binding_case.rs` (new), `component_tagger.rs`, `manifest_builder.rs`) — html5ever lowercases attribute *names* per the HTML spec, and a name-binding lives in attribute-name position (`<icon @[selectedEquipProps(uuid).element]>`), so every parse/serialize round-trip lowered its expression (`selectedEquipProps` → `selectedequipprops`, undefined at runtime). Attribute *values* keep their case, so value-bindings were never affected. The new `binding_case` module snapshots the original-cased `@[...]` bindings before the round-trip (keyed by their lowercased form, first-wins on collision) and restores them afterward — applied both to the tagged HTML in `component_tagger` and to every string the manifest builder serializes, including its captured `name_bindings` array. Repro: `tests/compiler/name-binding-case`.
83
- - **Name-binding paths wrapped in grouping parens by component-prop substitution failed to resolve** (`runtime/utils.js`) — `resolveCaseInsensitivePath` bailed on any `(`, but reusing a component rewrites a prop reference like `props.element` into `(equipmentDetailProps).element`, and the HTML parser lowercases the surrounding name-binding attribute. It now strips grouping parens to recover the plain dotted path and resolves it case-insensitively, still bailing on a genuine function call (`selectedEquipProps(uuid)`) that needs the full evaluator. Covered by `tests/unit/utils.test.js`.
84
-
85
- ## [2.1.10] - 2026-06-21
86
-
87
- ### Fixed
88
-
89
- - **Compiler 1.9.5 → 1.9.6 — watch mode now picks up files created mid-session** (`compiler/src/compiler/watcher.rs`) — the dependency graph was built once at startup and never relearned a changed component's own dependencies. On a component edit the watcher only looked up *existing* dependents and bailed when there were none; unlike the page branch, it never re-extracted the component's `<component src>` references. So a component file created after the watcher started — and any new `<component src>` reference added by editing an existing component (e.g. a `Layout` that adds a freshly-created child) — was never recorded in the graph: editing that new file resolved to zero dependent pages and was a silent no-op (its content only reached the output incidentally, the next time some dependent page recompiled for another reason). The dep-refresh is now a single `DependencyGraph::refresh_file` shared by **both** the page and component branches (previously only pages refreshed, inline), so a component edit relearns its outgoing edges. A forward map (`component_to_used_components`) mirrors `component_to_components` so a component's own edges can be cleared symmetrically when they change. Because referencing a new component always means saving a referrer, that save now teaches the graph the new edge, and subsequent edits to the new file recompile every page that inlines it — no dev-server restart needed. Tests: `refreshing_a_component_learns_newly_added_child_references`, `refreshing_a_file_drops_stale_dependencies`, `refreshing_a_component_drops_stale_child_references` (`watcher.rs`).
90
- - **Compiler 1.9.5 → 1.9.6 — inlining a component whose end tag was split across lines left a stray `>`** (`compiler/src/parser/html.rs`) — `find_matching_close` already tolerated whitespace before the `>` of an end tag (`</component\n>`, as produced by Prettier's whitespace-controlled wrapping), but the inliner computed the replacement's end as `close_start + len("</component>")` — too short by exactly that whitespace. The trailing `>` was left behind as a stray text node beside the inlined component (the literal `>` that leaked next to components in the app). `find_matching_close` now returns the close tag's real `(start, end)` byte offsets and both inlining paths slice to `end`. Tests: `inline_component_with_split_end_tag_leaves_no_stray_gt`, and the extended `find_matching_close_tolerates_whitespace_in_end_tag` (`html.rs`).
91
-
92
- ## [2.1.9] - 2026-06-20
93
-
94
- ### Fixed
95
-
96
- - **Compiler 1.9.4 → 1.9.5 — array-literal each-root components broke when inlined** (`compiler/src/parser/html.rs`, `compile.rs`) — a component whose root is `<!-- each [prop] as a -->` receives its iterable as a prop and relies on the runtime's `__vibeiterprops` indirection: the runtime evaluates the prop binding in the *enclosing* loop scope, stashes the value in a global registry slot, and iterates that slot. Inlining instead baked the call-site's parent-loop alias straight into the each (`[card.signatureAbility]`); the runtime evaluates an array-literal iterable in global scope, where that alias is undefined, so the loop yielded zero items (the empty `AbilityCell` / status-chip bug in compiled mode). Such components (`is_iter_prop_root`) are now left as runtime `<component src>` tags instead of being inlined — in both the `src=` and custom-element inlining paths — and the `components/` directory is always mirrored to the output so the runtime can fetch their source, exactly as in non-compiled mode. Tests: `each_root_component_is_left_for_runtime`, `ordinary_component_still_inlines` (`html.rs`), and `tests/compiler/components`.
97
- - **Compiler 1.9.4 → 1.9.5 — a stateful component nested inside another stole the inner's `this.` bindings** (`compiler/src/compiler/component_tagger.rs`) — the outer component's build-time `this.` → component-id rewrite descended through a nested component that registers its OWN `component({...})` state, claiming the inner's bindings and `if`/`each` directives with the outer id before the inner was reached. The inner's live state (registered under its own runtime id) then never reached its markup → empty each-loops and dead bindings (the `DebugContent` → `ScalingModal` empty-legend bug). The rewrite now stops at any nested state-registering component (`is_state_registering_component`); each component owns the `this.` inside it and gets its own id + rewrite when `walk_tag_and_extract` reaches it. Test: `nested_component_this_resolves_to_own_id`.
98
-
99
- ## [2.1.8] - 2026-06-19
100
-
101
- ### Changed
102
-
103
- - **Compiler 1.9.3 → 1.9.4 — watch-mode recompiles are incremental at the component-cache level** (`compiler/src/compiler/watcher.rs`, `compile.rs`) — on a component edit the watcher previously cleared the *entire* component cache, so every affected page re-expanded its whole component tree even though one leaf changed. It now invalidates only the edited component plus the components whose cached inlined content embeds it — its transitive inlining ancestors, via `DependencyGraph::get_all_dependent_components` — while every unrelated component stays cached and is reused. Supporting changes: the watcher adopts the initial full-compile's warm cache (`adopt_component_cache`) so even the first edit is incremental; cache keys are resolved source-relative (`component_cache_key`, matching the `<component src>` form, with paths outside the source root excluded so external-URL components are never wrongly invalidated); and incremental manifest generation (`generate_manifests_for_files`) now runs in parallel with rayon like the full build, since per-page static analysis dominates watch latency. A page-only edit invalidates no component caches at all. Unit tests cover the stale-ancestor set and cache-key normalization (`watcher.rs` tests).
104
-
105
- ## [2.1.7] - 2026-06-19
106
-
107
- ### Fixed
108
-
109
- - **Compiled component-local state was lost when a conditional re-mounted** (`runtime/component.js`, `runtime/conditionals.js`) — on compiled pages each component's setup script is inlined as `type="vibe-module"`, but the boot-time pass only runs the scripts present at boot. A `<component>` carrying its own `component({...})` state inside a `<!-- if -->` rendered correctly the first time, then went blank after the conditional was toggled off and back on: the branch markup was restored but its `<!-- each _cN.x -->` read component-local state that had been released on unmount. `mountBranch` now runs the freshly mounted subtree's `vibe-module` scripts via a new `executeCompiledComponentScriptsIn(nodes)` — before nested iterations/conditionals render, so the re-registered state is in place when `<!-- each _cN.x -->` evaluates. The `_cN` id-counter advance and the script runner are factored out so the scoped and boot-time passes share one path. Repro: `e2e-runtime/conditional-component-remount.html`, `tests/e2e/components-in-conditionals.spec.js`.
110
-
111
- ## [2.1.6] - 2026-06-19
112
-
113
- ### Added
114
-
115
- - **Compiler 1.9.2 → 1.9.3 — constant-vs-dynamic state analysis before value-stamping** (`compiler/src/compiler/reassignment_analyzer.rs` (new), `value_stamper.rs`, `compile.rs`, `compiler/mod.rs`) — the compiler now proves which global `$` state keys are compile-time constants before baking them into pre-rendered HTML. A key is stamped only when its initial value is a primitive (string/number/bool/null) **and** nothing anywhere writes it (assignment, compound, `++`/`--`, `delete`, or a nested `$.key.x = …`); every other key stays a live `@[...]` binding for the runtime to fill. The rule is deliberately one-sided: a wrong "constant" bakes stale content into production HTML (a correctness bug), a wrong "dynamic" only forgoes the optimization (a brief FOUC), so every uncertain case resolves to dynamic. Whole-program escape signals discard the optimization entirely — aliasing `$` itself (`const x = $`), reflective writes (`Object.assign($, …)`, `Object.defineProperty`), or any source that fails to parse. The `$.key` / `$['key']` (global) vs `$[expr]` (component-state-by-id) split mirrors Vibe's own convention, so a computed component-state write is ignored rather than treated as an escape. Objects and arrays are never constant (a value alias like `const a = $.items; a.push(x)` can mutate them invisibly). Built on an SWC AST visitor with broad unit coverage (`reassignment_analyzer.rs` tests).
116
-
117
- ### Fixed
118
-
119
- - **Compiler 1.9.2 → 1.9.3 — state initialized from imports could not be resolved** (`compiler/src/compiler/js_analyzer.rs`) — the state extractor now follows default-import chains across files (`resolve_import_value` / `resolve_export_from_file` / `visit_export_default_expr`), so a `$` key whose initial value comes from an imported module resolves to its real value for stamping instead of being treated as unknown. Also tags components whose state object is built at runtime.
120
- - **Compiler 1.9.2 → 1.9.3 — `>` inside a prop binding broke component / custom-element inlining** (`compiler/src/parser/html.rs`, `component_tagger.rs`) — the inliner's attribute-run pattern was `[^>]*`, so a comparison inside a quoted binding value (`flipped="@[selectedBrawlers.length >= maxBrawlers]"`) truncated the open tag at that inner `>`, mis-parsing every following attribute and mangling the element. The attribute run now mirrors a real HTML tokenizer (`ATTR_RUN`) — a tag ends only on an *unquoted* `>`, so `>`/`>=` inside any quoted value is part of the value. Repro: `inline_component_tolerates_gt_in_prop_binding`, `inline_custom_element_tolerates_gt_in_prop_binding`.
121
-
122
- ## [2.1.5] - 2026-06-19
123
-
124
- ### Fixed
125
-
126
- - **Route-aware manifest resolution for dynamic pages** (`runtime/pre-compiled-manifest.js`) — the candidate-path builder is extracted into an exported, unit-tested `buildManifestCandidatePaths(pathname, route)` and now reads `window.__ROUTE__` (the route template the compiler/dev server injects for dynamic pages, e.g. `/brawlers/:index`). When the route marks a segment dynamic with `:param`, the compiler has already collapsed that segment to `$` in the manifest path, so the runtime points straight at the tokenized manifest (`/vibe-hyperspeed/brawlers/$.html.manifest.js`) first instead of probing literal URLs (`/brawlers/0.html.manifest.js`) that are guaranteed to 404. Params can sit mid-path (`/a/:id/b`), and the candidate list is de-duplicated (subdirectory pages otherwise produced the same URL twice). Static pages with no route hint behave exactly as before. Tests: `runtime/pre-compiled-manifest.test.mjs`.
127
- - **Compiler 1.9.1 → 1.9.2 — component `<script>` source was HTML-escaped, dropping `component(stateVar)` components** (`compiler/src/compiler/component_tagger.rs`) — re-serializing a `<script>` element from its children loses the rawtext context, so JS operators were entity-escaped (`>` → `&gt;`, `&&` → `&amp;&amp;`). The mangled source then failed to parse, demoting the component to the regex fallback (which only matches `component({` object literals) and leaving a component called with a bare variable untagged. The script's text is now read directly. Adds AST-analyzer coverage for `component(stateVar)` resolution against a realistic script shape (imports, dynamic `import().then()`, `catch {}`, optional chaining).
128
- - **Compiler 1.9.1 → 1.9.2 — component-local `this.` was only resolved in simple text/attribute bindings** (`compiler/src/compiler/component_tagger.rs`) — build-time `this.` → component-id rewriting now also covers `if`/`each`/`else if` directive comments, nested binding paths (`@[this.x.y]`), multi-reference expressions, and `$.this.X` writes in event-handler bodies (`onclick="$.this.mode = 'edit'"`) — mirroring the runtime's `STATE_THIS_PROP_REGEX` pass. Without this, compiled (manifest-restored) pages — which can't fall back to a `data-vibe-component-id` ancestor lookup — shipped literal `this.`/`$.this.` that resolved to `undefined`. Loop aliases (`each _c0.items as it`) are left untouched.
129
-
130
- ## [2.1.4] - 2026-06-18
131
-
132
- ### Fixed
133
-
134
- - **Compiler 1.9.0 → 1.9.1 — `--minify` broke `<script>` and `<style>` blocks** (`compiler/src/compiler/compile.rs`) — `minify_html` only treated `<pre>` as whitespace-significant. Collapsing newlines inside a `<script>` turned a `//` line comment (or any ASI-dependent break) into a single line, so the comment swallowed the rest of the script and `new Function` threw a `SyntaxError` at runtime; `<style>` blocks were likewise flattened. Both tags now join `<pre>` as raw blocks emitted line-for-line, while the surrounding HTML still minifies normally. Repro: `minify_preserves_script_newlines_so_line_comments_dont_swallow_code`, `minify_preserves_style_newlines` (`compile.rs` unit tests).
135
- - **Compiler 1.9.0 → 1.9.1 — component inliner overshot on end tags split across lines** (`compiler/src/parser/html.rs`) — `find_matching_close` matched `</tag>` exactly, but whitespace-controlled markup can split an end tag (`</component\n>`), which HTML permits. The depth counter then counted the nested open without ever seeing its close, so the outer-close search ran past the real boundary and swallowed every following sibling into the component. The close-tag regex now tolerates whitespace before `>` (`</tag\s*>`); `\s*` can't bridge into `</tag-foo>`, so matching stays exact on the tag name. Repro: `find_matching_close_tolerates_whitespace_in_end_tag` (`html.rs` unit test).
136
-
137
- ## [2.1.3] - 2026-06-18
138
-
139
- ### Added
140
-
141
- - **Compiler 1.8.2 → 1.9.0 — `skipFiles` config option** (`compiler/src/config.rs`, `compiler/src/compiler/compile.rs`, `compiler/src/compiler/watcher.rs`, `compiler/src/main.rs`) — projects can now add their own exclude patterns on top of the compiler's built-in skip list (test files, `*.config.js`, `node_modules`, dotfiles, build dirs). Like `reservedElements`, user values **append** to the built-ins rather than replacing them, so the sensible defaults always hold. `Config::load` merges the built-in `SKIP_FILES` const with the user array into one effective `config.skip_files`, which is now threaded into `should_skip_path` at every call site (compile pass + watch mode) instead of the function reading a hard-coded const. Matching: a bare name (`server`) matches any file or directory with that name; a pattern containing `*` is a glob (`**/*.bak`) matched against both filename and full path; dotfiles are always skipped. This removes the need for app-side post-build pruning of directories the deploy never serves (a backend `server/`, build `scripts/`, a stale `dist/`). Config-only (no CLI flag, like `reservedElements`); surfaced in `--verbose` output. Repro: `tests/compiler/skip-files`.
142
- - Docs: the compiler **Configuration** page documents `skipFiles` (example config + full section) and adds two collapsible panels revealing the built-in `reservedElements` and `skipFiles` default lists.
143
-
144
- ## [2.1.2] - 2026-06-18
145
-
146
- ### Fixed
147
-
148
- - **Compiler 1.8.0 → 1.8.1 — `--minify` mangled tags whose attributes span multiple lines** (`compiler/src/compiler/compile.rs`) — `minify_html` joined trimmed source lines with no separator, so a newline *inside* a tag vanished instead of collapsing to a space. A multi-line `<meta name="viewport" content="...">` became `<metaname="viewport"content="...">`, which the downstream stamping stage then re-parsed into deeper garbage (`initial-scale="1.0,"`, `&quot;`, a bogus `</metaname...>` close). The inter-line break is now collapsed to a single space like any other whitespace run; the existing `>\s+<` → `><` pass re-tightens genuine tag boundaries. Generic fix — applies to any multi-line tag or text node, not just `<meta>`. Repro: `tests/compiler/minify-meta`.
149
-
150
- ## [2.1.1] - 2026-06-18
151
-
152
- ### Added
153
-
154
- - **`[Fet(ca)ched]` debug event for cache hits** (`runtime/debug.js`, `runtime/constants.js`, `runtime/component.js`, `runtime/component-cache.js`) — in debug mode, a `<component src>` served from the runtime template cache now logs `[Fet(ca)ched]` instead of `[Fetched]`, so a real network fetch and a cache hit are visually distinct at a glance (both share the same purple). `component-cache.js` exposes `isComponentCached(src)`, captured before the fetch so the debug layer can tell the two apart. The phase-bracket padding widened 13 → 14 to keep `[Fet(ca)ched]` column-aligned with the other events.
155
-
156
- ## [2.1.0] - 2026-06-17
157
-
158
- ### Added
159
-
160
- - **Component template cache** (`runtime/component-cache.js`, new) — Vibe now caches each fetched `<component src>` template by `src` instead of refetching it per instance. A page that mounts the same component many times, or an SPA that re-mounts components on navigation, previously issued one network request per instance; it now issues **one per unique template**. Two mechanisms in one small module:
161
- - **In-flight coalescing** — the fetch *promise* is cached synchronously before its first `await`, so a burst of same-tick mounts of the same `src` shares a single request instead of stampeding the network. This is something a browser HTTP cache structurally cannot do (a cold cache can't dedupe concurrent requests for the same URL).
162
- - **Session-lived reuse** — later mounts, including after SPA navigation, resolve from memory with no network request at all.
163
-
164
- The cache is content-busted, never time-based: in production component templates are immutable for the life of the page, so there is nothing to invalidate and no staleness window. Per-instance props, slots, and component-local state are unaffected — only the template text is shared; each instance hydrates independently. Verified on a real page: a view that fetched 253 component files (41 unique) now fetches 41, with zero duplicate requests.
165
- - New config flag `noCache: true` (`vibe(state, { noCache })`) disables it entirely.
166
- - New public method `$.clearComponentCache(path?)` invalidates one entry (query string ignored) or all. `@ape-egg/vite-plugin-vibe` calls it on hot update so edits are reflected for freshly-mounted instances; the runtime stays free of dev-server coupling.
167
- - Unit coverage: `tests/unit/component-cache.test.js` (coalescing, caching, eviction, `noCache`, non-ok/rejected responses not persisted).
168
-
169
- ## [2.0.5] - 2026-06-16
170
-
171
- ### Fixed
172
-
173
- - **Name bindings routed through a component prop inside an iteration were silently dropped** (`runtime/iterate.js`, `runtime/component.js`) — `resolveIterationComponentProps` injects `@[window.__vibeIterProps._pN]` into a component's bindings, and prop substitution carries that accessor into the template's own bindings, including name-bindings (`<icon @[props.element]>`). HTML lowercases attribute names, so the camelCase global arrived at hydrate as `window.__vibeiterprops` and resolved to `undefined`, dropping the attribute. The iteration-prop registry global is now all-lowercase (`__vibeiterprops`) so it survives attribute-name normalization. Adds the `name-binding-prop` e2e fixture and spec.
174
-
175
- ## [2.0.4] - 2026-06-12
176
-
177
- Companion-package and website release; no runtime changes.
178
-
179
- ### Fixed
180
-
181
- - **Codie treated vibe name bindings as broken attributes** (`@ape-egg/codie`) — `cleanupBooleanAttrs` now recognizes `@[expr]` as an attribute name (so `<icon @[icon]=""></icon>` cleans up to `<icon @[icon]></icon>`), and the HTML highlighter tokenizes opening tags containing name-binding attributes instead of leaving them unhighlighted.
182
- - **Codie's editing textarea inherited host design-system styling** (`@ape-egg/codie`) — the textarea reset now also clears `box-shadow`, `border-radius` and `background`, so CSS libraries that style `textarea` globally (e.g. stylecheat's inset ring) can't bleed into the editor.
183
-
184
- ### Changed
185
-
186
- - Website: the fantasy tutorial is replaced by the interactive **Try & learn** lessons (live Codie editor on every page), a **Roadmap** page is added, toast notifications land site-wide, dark mode is the default, and the compiler status is promoted from alpha to **beta**.
187
-
188
- ## [2.0.3] - 2026-06-11
189
-
190
- Compiler parity with the 2.0.x runtime: all six findings from
191
- `get-compiler-up-to-date-with-vibe-runtime-2.0.0.md` are fixed and the doc is
192
- retired. Every former parity-gap opt-out in the e2e suite now runs dual-mode
193
- (644 e2e tests, both runtime and compiled). Compiler bumped 1.7.2 → 1.8.0.
194
-
195
- ### Fixed
196
-
197
- - **Compiled component props only substituted exact `@[prop]` bindings** (`compiler/src/parser/html.rs`) — the inliner now mirrors the runtime's `renderPropsAndSlot` exactly: case-insensitive `@[propName]` replacement, prop identifiers inside larger `@[expr]` expressions and directive comments (`if` / `else if` / `each`), `$.propName` references in event-handler bodies, bare boolean attributes (→ `true`), JSON-quoted string literals, raw numerics. Binding props (`prop="@[path]"`) rewrite identifiers to the bound path, so prop reactivity flows in both directions on compiled pages (parent→child re-render, child→parent handler writes). One shared `substitute_props` serves all three inline sites; `src`/`class` are excluded from props.
198
- - Tests: `component-props.spec.js` (all four suites), `components-in-iterations.spec.js` (all five suites) — now `testBothModes`
199
-
200
- - **Compiled iteration batch functions stamped boolean attribute bindings as strings** (`compiler/src/compiler/iteration_optimizer.rs`) — `open="@[flag]"` rendered `open="false"`, which is "open" to HTML and `[open]`-style CSS. The generator now classifies attribute bindings like the runtime batch path: DOM properties and value-style attributes keep `attr="${expr}"`, boolean-coerced attributes emit conditional presence (`${(expr) ? ' attr=""' : ''}`). Rust-side `VALUE_ATTRS`/`DOM_PROPERTIES` mirror `runtime/constants.js`; the template emitter was unified into one recursive pass that also escapes backticks and `${` in static text.
201
- - Test: `directive-nesting.spec.js` → "Outer-scope reactivity inside `<!-- each -->`" — now `testBothModes`
202
-
203
- - **`@[$.unsafe(...)]` inside a compiled iteration crashed boot** (`compiler/src/compiler/iteration_optimizer.rs`) — the generated batch function interpolated `$.unsafe(...)` against a plain `$` (uncaught TypeError; `$.ready` never resolved), and a template-literal interpolation can never honor raw-HTML semantics anyway (`RawHtml.toString()` escapes by design). Templates containing `$.unsafe(` no longer get a batch function — those iterations render through clone+hydrate, the one place that implements `$.unsafe` (innerHTML + inert subtree).
204
- - Test: `hydration-optouts.spec.js` → "Raw HTML rendering via $.unsafe()" — now `testBothModes`
205
-
206
- - **Manifest restoration templates dropped whitespace text nodes** (`compiler/src/compiler/manifest_builder.rs`) — the manifest's child keys are childNodes indices computed on the pre-stamp DOM, but restoration re-inserted templates with whitespace-only text nodes filtered out, shifting every later sibling's index. Iterations/conditionals after a restored region never got wired to their DOM markers, crashing boot with "Cannot read properties of undefined (reading '__vibeRendered')" on prop-driven compiled pages. Templates are now captured verbatim — an exact node-count round-trip.
207
-
208
- - **Restoration located directives by expression text with a subtree-wide scan** (`runtime/pre-compiled-manifest.js`) — two conditionals sharing an expression cross-matched: the outer's template landed inside the inner's markers (multi-root component scope broke this way). Directive regions are now located by their manifest key's childNodes index and processed in ascending order, so each restoration revalidates the next index. Same-expression siblings, conditionals inside iterations, and nested regions all disambiguate structurally; the `insideIteration`/`_vibeProcessed` scanning heuristics are gone.
209
-
210
- - **Compiled pages executed inlined component scripts as native page modules** (`compiler/src/parser/html.rs`, `runtime/component.js`, `runtime/index.js`) — native timing is wrong on every axis: scripts ran pre-boot so `const id = component(state); $[id]` captured the placeholder `$`, scripts with imports registered state after boot with nothing merging it into the live proxy, and `$.ready` raced async registration. The compiler now neuters src-inlined component scripts to `<script type="vibe-module">`; the runtime executes them at boot through the same injected-`component()` path fetched scripts use (shared `transformScriptContent`): live `$`, componentId claimed from the build-tagged wrapper, listener cleanup on re-execution, and async scripts gate `$.ready`. Scripts stay in the DOM (inert) so manifest indices keep matching.
211
- - Tests: `component-state.spec.js` → "Async component() with child prop binding", "component() returns componentId", "Multi-root component scope" — now `testBothModes`
212
-
213
- - **`$.this.X` in event handlers double-rewrote on compiled pages** (`runtime/parse.js`) — runtime-fetched components arrive with `$.this.` already rewritten by component.js, but compiled pages reach parse with the authored form, and the bare-`this.X` pass alone produced `$.$['id'].X` (TypeError on click). parse now consumes `$.this.X` as one reference before the bare-`this.X` pass.
214
-
215
- - **`$` snapshot fallback leaked state reads to the live root** (`runtime/utils.js`) — `dollarFor`'s helper fallback served any key missing from a snapshot, so old/new diff evaluations both read the live value for a component state bucket registered mid-cycle and change detection saw "no change" (a `<!-- if this.x -->` in an async compiled component never mounted). The fallback now serves only the root's non-enumerable helper methods (`unsafe`, `on`, `reconcile`, …); a state key missing from a snapshot reads `undefined`, as the diff requires.
216
-
217
- - **Batch path rewrote a text binding on its own line as a name binding** (`runtime/iterate.js`) — `BATCH_NAME_BINDING_REGEX` matched any whitespace-bounded `@[expr]`, so a prettier-formatted `<option>` with `@[item]` on its own line rendered ` item=""` as its text. The name-binding pass now runs only over tag spans (quote-aware `mapTagSpans` — name bindings can only exist inside a tag), and binding-bearing text nodes are trimmed in the batch template to match the clone path's trimmed writes.
218
- - Test: `batch-vs-clone-equivalence.spec.js` → dom-prop-selected region (and all other regions still byte-identical)
219
-
220
- ### Changed
221
-
222
- - **Compiler 1.7.2 → 1.8.0** — prop-substitution parity, component-script neutering (`vibe-module`), verbatim restoration templates, boolean-attribute classification in batch functions, `$.unsafe` batch opt-out.
223
-
224
- ---
225
-
226
- ## [2.0.2] - 2026-06-11
227
-
228
- ### Fixed
229
-
230
- - **Unmounting a conditional left nested-directive content behind** (`runtime/conditionals.js`) — `unmountBranch` removed only `activeInstance.nodes`, the branch's originally cloned top-level nodes. Content rendered by nested directives at the branch's top level — `<!-- each -->` rows, deeper `<!-- if -->` branches — is inserted between the *nested* directive's own comment markers after the branch mounts, so it never appears in that list and survived the unmount. Every off/on toggle of the outer conditional then accumulated another copy of the nested content. `unmountBranch` now sweeps the live range between the conditional's start/end comments (same pattern as iteration teardown), still collecting componentIds before detaching so orphaned component state is released.
231
- - Test: `tests/e2e/each-inside-if-toggle.spec.js` (nested each rows + nested if content removed on toggle-off; repeated off/on toggles don't duplicate)
232
-
233
- - **`checked`/`selected` bindings stringified into the attribute** (`runtime/hydrate.js`, `runtime/iterate.js`) — DOM-property bindings mirrored every value into the attribute as a string, so `checked="@[on]"` with falsy state produced `checked="false"` — which is *checked* to HTML and `[checked]`-style CSS, and lied in `outerHTML` snapshots. `checked`/`selected` now keep the canonical boolean attribute form: present (empty) when truthy, removed when falsy; the property still follows the state. Only `value` keeps the stringified attribute mirror. Applied in both the clone path (`hydrate.js`) and the batch path (`applyDomPropertyWrites` in `iterate.js`) so runtime and batch-rendered iterations agree.
234
- - Test: `tests/e2e/checked-attribute-sync.spec.js` (absent when falsy, empty-present when truthy, removed on truthy→falsy, property always synced)
235
-
236
- ---
237
-
238
- ## [2.0.1] - 2026-06-10
239
-
240
- ### Fixed
241
-
242
- - **Loop-alias rewriter mangled object-literal keys in inline handlers** (`runtime/loop-scope.js`) — `rewriteHandlerAliases` rewrote every standalone alias identifier, including ones in object-key position, so `onclick="f({ pack: pack.name })"` inside `<!-- each soundPacks as pack -->` hydrated to `f({ $scope(this,'pack'): $scope(this,'pack').name })` — a syntax error, and the click silently did nothing. The rewriter now tracks bracket frames with a pending-ternary count per nesting level, which is what tells an object key's `:` apart from a ternary's: an alias followed by `:` inside `{ }` with no open ternary is a key and stays put, and a shorthand property (`{ pack }`) expands to `{ pack: $scope(this,'pack') }`. Ternary branches (`go ? pack : null`), computed keys (`{ [pack]: 1 }`), and array elements keep rewriting as values.
243
- - Test: `tests/unit/loop-scope.test.js` (key position, shorthand expansion, computed keys, ternary colons inside object values, arrays, optional chaining/nullish coalescing)
244
-
245
- ---
246
-
247
- ## [2.0.0] - 2026-06-06
248
-
249
- Version rolled to 2.0.0. **Still Beta — no behavior changes since 1.9.9.** The major bump consolidates the 1.9.x line (runtime reactive core, components, optional compiler, `$.unsafe` raw-HTML, plus the recent reactivity-correctness, iteration teardown-leak, and scoped-state performance fixes); it does not signal a stability promotion or any breaking change.
250
-
251
- ---
252
-
253
- ## [1.9.9] - 2026-06-06
254
-
255
- ### Fixed
256
-
257
- - **Iteration teardown leaked removed rows** (`runtime/iterate.js`, `runtime/affected.js`, `runtime/index.js`) — when an iteration's array emptied or rows were removed, Vibe deleted the DOM but kept internal references to the detached subtrees: the per-row parsed trees, inlined per-row component trees, and the flat manifest entries (`dotPath -> element`). Those structures pinned the removed nodes in memory and made every later reconcile/affected walk traverse dead nodes — in Battle Brawlers, combat fps decayed on each Reset→Start cycle. The page MutationObserver is disconnected while Vibe renders, so removal happens unobserved and the normal observer-driven cleanup never fires; teardown must prune itself. `index.js` now pairs the flat manifest with its parsed tree via a non-enumerable `manifest.__tree` (mirroring `manifest.__live`), and `iterate.js`'s new `releaseRemovedSubtrees` prunes both views together on row removal — deleting each removed root's manifest paths (path-scoped) and tree node (whose subtree cascade covers nested iterations/conditionals/components). Proven with a WeakRef + forced-GC e2e test: after emptying the iteration, zero removed elements remain reachable.
258
- - Test: `tests/e2e/iteration-teardown-leak.spec.js` (no retained DOM after GC; repeated fill/empty cycles don't accumulate)
259
-
260
- ### Performance
261
-
262
- - **Scoped-state key/overlay lookups avoided proxy traps on the combat hot path** (`runtime/utils.js`, `runtime/iterate.js`, `runtime/affected.js`) — `createScopedState` now records each scoped-state proxy's precomputed key list and its small local overlay (loop aliases `item`/`index` + parent aliases) in WeakMaps via `rememberScopedKeys`. `evalInScope` reads the keys through `ownKeysOf` instead of `Object.keys(proxy)` (which fired the proxy's `getOwnPropertyDescriptor` trap for every key, every eval), and `affected`'s iteration descent rebuilds its per-instance merged snapshot via a cheap `{...currentGlobals, ...overlay}` plain merge (`scopedOverlayOf`) instead of spreading the proxy over every global key.
263
-
264
- ---
265
-
266
- ## [1.9.8] - 2026-06-01
267
-
268
- ### Added
269
-
270
- - **Raw-HTML rendering via `$.unsafe(expr)`** (`runtime/raw-html.js` (new), `runtime/hydrate.js`, `runtime/index.js`, `runtime/utils.js`, `compiler/src/compiler/value_stamper.rs`) — a binding that renders a trusted string as real markup instead of escaping it; Vibe's equivalent of Svelte `{@html}` / Vue `v-html` / React `dangerouslySetInnerHTML`. `$.unsafe(str)` wraps a string in a `RawHtml` marker. When the binding is the **sole content of its element** (`<p>@[$.unsafe(desc)]</p>`), the runtime sets `innerHTML` from the string and marks the injected subtree opaque (`managedNodes`) so the MutationObserver never re-walks it — injected markup is **inert**, matching Svelte. Mixed into surrounding text it falls back to escaped literal text (via the marker's `toString()`). Reactive in every scope (top-level, iterations, conditionals, components). Compiled mode paints the markup raw at stamp time (`value_stamper.rs` defines `$.unsafe` in the QuickJS context) while the manifest preserves the `@[$.unsafe(...)]` marker for runtime re-hydration. **Trusted input only — no sanitizing.**
271
- - Test: `tests/unit/raw-html.test.js`, `tests/e2e/unsafe-html.spec.js` (pure render, escaped-text fallback, reactivity, in-iteration, in-conditional, inert injected markup), and `$.unsafe` stamping in `tests/compiler/runtime`.
272
-
273
- ### Fixed
274
-
275
- - **`$` inside expressions must read from the per-cycle state, not the live root** (`runtime/utils.js`) — surfaced while wiring `$.unsafe`. The reactivity engine detects change by evaluating each expression against an old snapshot and a new snapshot and comparing; binding `$` to the live root proxy made both reads identical, silently defeating change detection for any expression reading through `$` or `this.` (which compiles to `$['id']…`) — e.g. a computed iteration array or conditional gated on component state would stop re-rendering. `evalInScope` now resolves `$` to the same state object the diff cycle is evaluating, and reaches the root's non-enumerable helper methods (`unsafe`, `on`, `reconcile`) via a thin per-state fallback proxy so `$.unsafe` stays callable from plain snapshots on the update path. State reads stay on the snapshot (diffable); only missing method names fall through to root. Scoped states already delegate to root, so the hot path is unaffected.
276
- - Test: `tests/unit/dollar-binding-scope.test.js` (a `$`-expression yields different values for two snapshots → diffable; `$.unsafe` resolves from a plain snapshot lacking it)
277
-
278
- ---
279
-
280
- ## [1.9.7] - 2026-06-01
281
-
282
- ### Fixed
283
-
284
- - **Conditional inside an iteration row didn't react to global-state changes** (`runtime/affected.js`, `runtime/hydrate.js`) — a `<!-- if … -->` (or `<!-- if … --><!-- else --><!-- /if -->`) living inside an `<!-- each -->` row, whose expression depended on a global rather than the loop item, stayed frozen on the branch active at mount time when the global later changed and the row's `item` was unchanged. Two compounding gaps, fixed at the right layer in two steps:
285
- - **Branch swap re-evaluated against the wrong scope** (`affected.js`, `hydrate.js`) — `affected` correctly detected the change against the row's merged state, but the affected entry it pushed carried only the node. `hydrate` then called `updateConditional` with the cycle's top-level state, which had no `item`, so the branch eval evaluated `selected.includes(item.id)` etc. against `item === undefined`. Rows mounted on the *if* branch happened to flip to *else* (often correct by coincidence); rows mounted on the *else* branch stayed stuck. `affected.js` now attaches the same `state`/`newState` pair the change was detected against as `scopedState`/`oldScopedState` on the conditional entry — merged row state inside an iteration, the cycle's top-level state outside — and `hydrate.js`'s conditional handler uses them when present.
286
- - **Inlined per-row components were invisible to the iteration recursion** (`affected.js`) — a `<component src>` mounted per iteration row has its parsed tree stashed on the post-process wrapper as `_vibeIterTree`, not as a child of `instance.tree`. `hydrateInlinedIterationComponents` already re-hydrates those trees, but it only runs from `updateInstance`, which only fires when the iteration's array changes. On a pure global-state change the array is unchanged, so the inlined component trees were never visited — a row-internal `<!-- if -->` *without* an else, gated on a global and living inside a per-row component (the Battle Brawlers `<brawler-menu>`/`<brawler-activity>` shape), wouldn't tear down. `affected.js`'s iteration recursion now also descends into every `_vibeIterTree` reachable from each instance's cloned nodes with the same merged scoped state, so the conditional flows through the normal affected → hydrate → `updateConditional` path and the existing `branches.else === null` handling triggers `unmountBranch`. Same path covers any other binding/conditional living inside a per-row component, so they all now react to global state changes too.
287
- - Test: `tests/e2e/conditional-in-iteration-global.spec.js` (7 tests: swap of `selected.includes(item.id)` in both directions; dynamic property lookup; compound gate with both halves; `if`-without-else direct shape; compound `if`-without-else; nested-property gate `combat.duration !== 0 && busyMap[item.id]`; and the per-row-component repro toggling `$.showMenu` through a mount→teardown→re-mount cycle)
288
-
289
- - **Loop-scoped `on*` handler inside a row-internal `<!-- if -->` resolved the previous item after the row updated** (`runtime/loop-scope.js`) — a `<!-- if -->` inside an iteration mounts its branch content separately from the row's `clonedNodes` and stamps `__vibeScope` once at mount time. When the row was later updated in place (its DOM reused for a new item), the iteration refreshed its own root stamp but the branch root kept the stale mount-time stamp, and `resolveScope` hit that first on the walk up — so a handler inside the conditional resolved the previous item. `stampInstanceScopes` now walks the instance's parsed tree and re-stamps every active conditional-branch root with the same fresh scope. Nested loops are skipped — each iteration node already manages its own instances' scopes.
290
- - Test: `tests/e2e/loop-scoped-handlers.spec.js` (+ unit coverage in `tests/unit/loop-scope.test.js`)
291
-
292
- - **Treeless (batch-rendered) iteration was rebuilt on every unrelated state change** (`runtime/iterate.js`) — `affected.js` conservatively flags a treeless iteration on any state change (it can't walk per-row trees to know what they depend on), and `updateIteration` responded with an unconditional `bulkReplace`. A background tick (e.g. a 250 ms client clock) therefore recreated every row on every flush, dropping imperatively-attached listeners (tooltip mouseleave) and any in-progress drag. `renderBatch` now remembers the rendered HTML on the iteration's runtime; before tearing down, `updateIteration` re-runs the batch — if the output is byte-identical to the previous one, the rows don't depend on what changed, and the existing DOM nodes are kept. Skipped when the template has DOM-property writes (`value`/`checked`/`selected`), which aren't reflected in the HTML string.
293
- - Test: `tests/e2e/iteration-treeless-unrelated-update.spec.js`
294
-
295
- - **Inlined component's literal-wrap `<!-- each [prop] as alias -->` froze on the original snapshot** (`runtime/iterate.js`) — when an inlined component received an object prop and wrapped it in a literal-array iteration to render its fields, the inner iteration's `forceRegistryBackedIterationUpdates` calls `updateIteration` with `oldState === newState`, but the old/new array eval produces fresh arrays containing the just-rewritten registry value, so the diff saw identical contents and emitted no UPDATE. `updateIteration` now treats `oldState === newState` as a "trust the rendered snapshot" signal — the previously rendered items on iteration instances are the only honest "old" when the registry side-effect is the only mutation — and correctly emits per-row updates.
296
-
297
- ### Added
298
-
299
- - **`<component src>` callsite receives its componentId** (`runtime/component.js`) — `processSingle` now returns the resolved `componentId` so `const id = component(state)` inside a `<script type="module">` block of a src-fetched component resolves to a non-undefined id. Matches the public component.js contract; without it, downstream `$[id]` was silently undefined for src-fetched components.
300
-
301
- ---
302
-
303
- ## [1.9.6] - 2026-05-25
304
-
305
- ### Fixed
306
-
307
- - **Loop item lost reference identity across mutations** (`runtime/state.js`, `runtime/iterate.js`, `runtime/index.js`, `runtime/loop-scope.js`) — the object a loop handed its `on*` handlers was no longer `=== $.arr[i]` after a `splice`/`push`, so identity-based code (`$.arr.indexOf(item)`, `item === $.arr[i]`) targeted the wrong row — e.g. removing two items by identity in a row would delete the wrong second row. Two compounding causes:
308
- - **Proxy double-wrapping** (`state.js`) — the deep proxy's `get` handed back a fresh proxy for nested elements, and array methods/assignments wrote that proxy back into the tree; the next read wrapped it *again*, minting a new proxy identity for the same underlying object (and making change detection compare a stored proxy against a raw, never equal). Added a `RAW` symbol so any of our proxies can expose its raw target, plus an `unwrap` helper used in `set` (never store a proxy in the raw tree) and at `createDeepProxy` entry (collapse a proxy that slipped in nested inside an assigned object literal). The cache now always returns the single canonical proxy per object.
309
- - **Loop var was a diff-snapshot clone** (`iterate.js`, `index.js`, `loop-scope.js`) — iterations render against `extractPlainValue($)` plain clones, which are never reference-identical to the proxy elements the app sees through `$`. The runtime now exposes the live proxy as `manifest.__live`; `resolveLiveArray` re-resolves the loop's array against it and each instance carries a `liveItem` that `stampInstanceScopes` prefers, so `$scope(this,'alias')` handlers (and nested conditionals stamping `__vibeScope`) receive live identity. Diffing still keys off the plain `inst.item`; only the handler-facing value is live. Falls back to the plain item when the live array can't be resolved (no worse than before).
310
- - Test: `tests/e2e/iteration-item-identity.spec.js` (handler receives the exact state-array element; identity-based removal targets the right item twice in a row across splices)
311
-
312
- ---
313
-
314
- ## [1.9.5] - 2026-05-23
315
-
316
- ### Added
317
-
318
- - **Loop-scoped `on*` event handlers** (`runtime/loop-scope.js` (new), `runtime/parse.js`, `runtime/iterate.js`, `runtime/conditionals.js`, `runtime/pre-compiled-iterations.js`, `runtime/index.js`) — inside a `<!-- each X as alias -->` loop, an event handler can reference the bare loop variable and receive the **live object** at fire time: `onclick="pick(ability)"` instead of flattening fields into `@[ability.id]`/`@[ability.ticks]`/…. The handler stays a visible native `on*` attribute (no hidden `addEventListener`), preserving the truthful-DOM / `outerHTML`-snapshot model.
319
- - At parse time a token-aware scanner rewrites a bare alias `ability` → `$scope(this,'ability')`, skipping `@[…]` binding spans, string literals, and member access (`foo.ability`). `$`, `this`, `event`, and bare function names are left untouched, so `$.state` and plain native handlers are never affected; `@[…]` handlers keep their existing stringifying behavior (backward compatible).
320
- - A global `$scope(el, name)` resolver (installed by the runtime) walks up the DOM to the nearest `__vibeScope` stamp and returns the live item — resolution is by **identity**, not array position, so it works for derived-source loops (`.filter` / `.map` / `(x || [])`) and survives keyed reorders.
321
- - Resolves recursively through **arbitrary-depth, arbitrary-combination `each` / `if` nesting** (e.g. `each > if > each > if`), including wrapper-less loops and handlers that first mount via a later state change (the update path). Achieved by persisting the full *accumulated* enclosing alias set on each iteration/conditional node (`meta.scopeAliases`) and threading it back into the render-time re-parse, and by stamping each instance/branch with the accumulated in-scope loop vars.
322
- - The Rust AOT compiler needs no changes: iterations carrying loop-scoped handlers bypass the manifest `batchFn` (which predates the rewrite) and use the runtime path that reads the rewritten template, so behavior is identical in runtime and compiled modes.
323
- - Test: `tests/unit/loop-scope.test.js` (rewriter, resolver, stamper) and `tests/e2e/loop-scoped-handlers.spec.js` (live object passed, derived source, nested aliases, keyed reorder, conditional-in-loop, deep `each/if/each/if`, wrapper-less loops, update-path mount, `$`/native handlers unaffected — runtime + compiled)
324
-
325
- ---
326
-
327
- ## [1.9.4] - 2026-05-22
328
-
329
- ### Fixed
330
-
331
- - **Binding-less `<!-- each -->` rendered only one item** (`runtime/iterate.js`) — an iteration whose repeated template contained no reactive binding referencing the loop scope (e.g. `<coin></coin>`) was never expanded: the runtime left the authored template untouched and rendered exactly one copy regardless of array length, and one copy even for an empty array. Adding any loop-scope binding (`@[i]`, `@[item.foo]`, …) made it work, which is why it stayed hidden — every working `each` happened to interpolate the loop var.
332
- - Root cause was `renderIteration`'s fallback "already rendered" check, which concluded a region was rendered when no element between the comments contained `@[...]` syntax. That heuristic conflates *already-rendered output* (no bindings left to hydrate) with *a binding-less authored template* (never had bindings to begin with), so binding-less templates were always skipped.
333
- - The fallback now keys off `managedNodes` — Vibe's authoritative DOM-ownership signal (a node lands there only when a render path produced it) — instead of the absence of `@[...]`. Detection is binding-agnostic, so a binding-less template expands to N clones (and 0 for an empty array) in both runtime and compiled modes, while the original "comment markers replaced by component re-processing" case it guarded still bails correctly.
334
- - Test: `tests/e2e/binding-less-iteration.spec.js` (binding-less body renders N, empty renders 0, bound body still renders N, reactivity grows both lists)
335
-
336
- ---
337
-
338
- ## [1.9.3] - 2026-05-18
339
-
340
- ### Fixed
341
-
342
- - **Compiler stack overflow on self-referencing components** (`compiler/src/compiler/compile.rs`, `compiler/src/parser/html.rs`) — `bun vibe:compile` aborted with `fatal runtime error: stack overflow` when any component referenced itself (e.g. `recursive-tree-node.html` containing a `<component src="/components/recursive-tree-node.html">` for runtime-bounded tree rendering). Two compounding bugs: `fetch_component_recursive` had no cycle guard so direct or transitive cycles (A → A, A → B → A) recursed until the stack overflowed; and even if recursion had been bounded, the cached content still carried the self-reference so `inline_component_elements`'s `src=` regex would keep re-expanding it forever, growing the document on every loop iteration.
343
- - `fetch_component_recursive` now takes a `visiting: &mut HashSet<String>` representing the live fetch chain. The src is inserted before recursing into nested components and removed after, so the set tracks "what's currently being resolved," not "everything ever fetched." When a recursive call sees its own src already in `visiting`, it returns `None` and the caller leaves the `<component src>` tag intact for runtime to handle.
344
- - Before caching, any `<component src="X">` in the resolved content whose `X` points back into the chain (or to the component itself) has its `src=` renamed to `data-vibe-recursive-src=` by a new module-level helper `escape_recursive_src`. The inliner's regex doesn't match the renamed attribute, so it stops re-expanding. `process_html_with_cache` restores the attribute to plain `src=` at the very end of compilation so the runtime fetches the recursion normally — escape is a compile-time-only mechanism.
345
-
346
- ---
347
-
348
- ## [1.9.2] - 2026-05-08
349
-
350
- ### Fixed
351
-
352
- - **Batch iteration path divergences from clone+hydrate** (`runtime/iterate.js`) — `renderBatch`'s template-literal compiler now produces DOM identical to the clone path for every supported binding form. Previously, simple-template iterations (single root, no nested directives, no `<component src>`) routed through batch but four binding shapes diverged silently from clone:
353
- - **`this.X` rewriting** — pre-resolved at compile time using the iteration's anchor element. Previously `this.X` was inlined literally into the `new Function()` body where `this` is `globalThis`, so component-scoped expressions evaluated to `undefined`.
354
- - **Boolean-coerced attributes** — emit a conditional template-literal segment so the attribute is *absent* when the binding is falsy, matching `hydrate.js`'s remove-when-falsy semantics. Previously batch always emitted `attr="${expr}"`, leaving `attr="false"` in the DOM when clone would have removed it.
355
- - **DOM properties** (`value` / `checked` / `selected`) — collected at compile time into a sidecar list with a transient `data-vibe-batch="…"` marker on the element, then applied in a small post-stamp loop that mirrors `hydrate.js`'s DOM-property branch (sets the property AND the attribute, removes attribute when value is `undefined`/`null`). The attribute alone isn't enough — `<option selected="false">` is still selected by HTML semantics, only the property write makes it correct.
356
- - **Name bindings** (`<el @[expr]>`) — emit a conditional template-literal segment that produces ` resolvedName=""` when truthy / nothing when falsy, with HTML-lowercase fallback so `<icon @[attrName]>` still resolves the camelCase state key after the browser parses it as `@[attrname]`.
357
- - The compiled batch function now also receives `$` as its last parameter so component-scoped expressions like `$['_c0'].chosen` (produced by the `this.X` rewrite) resolve against live state.
358
- - New equivalence harness: `tests/e2e/batch-vs-clone-equivalence.spec.js` flips a `__vibeForceClonePath` debug flag and compares region-by-region across both paths. 13 regions cover all four gaps plus regression cases for the binding forms that already worked.
359
-
360
- - **`this.X` inside iteration rows didn't resolve in the clone path either** (`runtime/utils.js`, `runtime/iterate.js`) — `findComponentIdForElement` walked from the row's cloned element up to the detached `parseContainer` and stopped, returning `null`. Hydrate then evaluated `this.X` against `globalThis`, producing `undefined`. Added a fallback: when `closest('[data-vibe-component-id]')` finds nothing, walk to the root and check a `_vibeComponentId` expando. `initializeBlock` now stashes the iteration's owning component id on the parseContainer (set from `findComponentIdForElement(startComment.parentElement)` in `renderIteration` and `buildInstance`), so detached hydrate sees the right component context. Latent bug that didn't surface until the equivalence harness above exercised `@[item === this.chosen]` and `@[item.toUpperCase() === this.chosen.toUpperCase()]` inside iteration rows.
361
-
362
- - **Combined default + named imports in component scripts** (`runtime/component.js`) — `<script type="module">` blocks that mix default and named imports (e.g. `import component, { foo } from '...'`) are now rewritten to two separate `await import()` calls (one for the default binding, one for the named bindings). Previously only one form was supported per import statement.
363
-
364
- ---
365
-
366
- ## [1.9.1] - 2026-04-29
367
-
368
- ### Added
369
-
370
- - **Non-primitive iteration props** (`runtime/iterate.js`) — `<component src>` props inside `<!-- each -->` can now receive objects and arrays. Primitives stringify as before; non-primitives snapshot into `window.__vibeIterProps` and the attribute becomes `@[window.__vibeIterProps._pN]`, so child templates can dot/iterate into the value (`@[card.name]`, `<!-- each card.abilities as a -->`). Slots are released on wrapper detach via `releaseOrphanedIterationProps`.
371
- - Test: `tests/e2e/component-in-iteration-rich-props.spec.js`
372
- - **Idempotent hydrate writes** (`runtime/hydrate.js`) — attribute, boolean-attribute, and text writes now compare current vs. new value and skip the write when unchanged. Eliminates needless paints and CSS-transition jitter on every re-hydrate (e.g. 4 Hz client-clock tick re-running unrelated bindings).
373
- - Test: `tests/e2e/idempotent-hydrate.spec.js`
374
- - **Multi-segment `this.X.Y` rewriting** (`runtime/component.js`, `runtime/constants.js`) — `@[this.user.name]`, `@[this.x + 1]`, etc. inside a component now rewrite the whole `this.X` head consistently. Centralized via `THIS_PROP_REGEX` / `STATE_THIS_PROP_REGEX` so `parse.js`, `utils.js`, and `component.js` share one definition.
375
- - **`$.renderComponent` — pure-render path for surgical HMR** (`runtime/component.js`, `runtime/index.js`) — extracted prop substitution + slot inlining into a non-executing helper. The vite plugin uses this with `$.reconcile` to update components in place when only the template changes; scripts are not re-run, so registered component state survives.
376
- - **`./boot` package export** (`package.json`) — `import { boot } from '@ape-egg/vibe/boot'` is now usable from consumers.
377
-
378
- ### Fixed
379
-
380
- - **Iteration-prop registry slots freed prematurely on wrapper swap** (`runtime/component.js`, `runtime/iterate.js`) — `processSingle.finalize` and the plugin's `remount` now transfer `_vibeIterPropIds` from the soon-to-be-detached element to its replacement. Without the transfer, `releaseOrphanedIterationProps` would clear registry entries that the new wrapper's bindings still reference, rendering them as `undefined`.
381
- - **Batch-render path stringified `<component src>` props** (`runtime/iterate.js`) — `canUseBatchRender` now skips templates containing `<component src>` so iteration items with rich props go through the clone+hydrate path that resolves them via the registry instead of through template-literal interpolation that coerces objects to `[object Object]`.
382
-
383
- ---
384
-
385
- ## [1.9.0] - 2026-04-18
386
-
387
- ### Added
388
-
389
- - **`$.reconcile()` — subtree reconciliation API** (`runtime/reconcile.js`, new 621-line module) — reconcile a Vibe-managed subtree against new source HTML with a tag-aligned, two-pointer walker. Vibe-owned regions (iterations, conditionals, components, `<slot>` pairs) are treated as opaque; their internals stay state-driven. DOM identity, focus, and selection survive wherever a match is found.
390
- - Exposed as `$.reconcile` (non-enumerable so it stays out of state snapshots)
391
- - Test: `tests/e2e/reconcile.spec.js`
392
-
393
- - **Component state cleanup on unmount** (`runtime/component.js`, `runtime/conditionals.js`, `runtime/index.js`, `runtime/manifest.js`) — when a `<component>` leaves the DOM (conditional branch swap, iteration key removal, etc.), its entry in `window.__vibeComponents[id]` and `window.$[id]` is evicted. `collectComponentIds` walks removed subtrees; `releaseOrphanedComponentState` verifies the DOM is fully gone before deleting state. Prevents state leaks from components that mount and unmount repeatedly.
394
- - Test: `tests/e2e/component-state-cleanup.spec.js`
395
-
396
- - **Arbitrary JS expressions in `<!-- each -->`** (`runtime/constants.js`) — `ITERATION_REGEX` relaxed from `[\w.\[\]]+` to `.+`. The array expression is now evaluated via `evalInScope`, so you can iterate over method calls, window globals, inline array literals, filters, and `Array.from(...)`:
397
- ```html
398
- <!-- each items.filter(x => x.active) as item -->
399
- <!-- each Array.from({length: 10}, (_, i) => i) as n -->
400
- <!-- each window.fights as fight, i -->
401
- ```
402
- - Test: `tests/e2e/each-expression.spec.js`
403
-
404
- - **Missing-identifier safety in expressions** (`runtime/utils.js`) — `evalInScope` now wraps free identifiers that aren't state keys, reserved words, or known globals in `typeof` guards so they resolve to `undefined` instead of throwing `ReferenceError`. `@[!missingProp]` → `true`, `@[optional?.x]` → `undefined`. Uses `typeof` rather than a `var` hoist so real globals (app functions on `window` like `getLevelByExperience`) stay reachable.
405
-
406
- - **Arrow function parameters and object-literal keys recognized** (`runtime/utils.js`) — identifier rewriting now skips `(a, b) =>` / `x =>` parameter lists and `{ key: value }` property names. Expressions like `items.map(x => x.name)` and `@[{ a: 1 }[k]]` no longer get turned into invalid JS by the missing-identifier pass.
407
-
408
- - **Prop substitution covers directive comments and event handlers** (`runtime/component.js`) — prop identifiers now resolve inside `<!-- if propName -->`, `<!-- else if propName -->`, `<!-- each propName as item -->` and `onclick="$.propName = x"` in addition to `@[...]`. Writing `$.value = x` inside an event handler on a child component whose parent passed `value="@[email]"` updates `$.email` on the parent — natural two-way binding with no extra API.
409
- - Tests: `tests/e2e/prop-in-directive.spec.js`, `tests/e2e/prop-in-expression.spec.js`, `tests/e2e/two-way-binding.spec.js`
410
-
411
- - **Pre-boot `$.ready` promise** (`index.js`, `boot.js`) — the placeholder returned by `vibe()` before boot now exposes a `.ready` promise that chains to the real post-boot `$.ready`. Consumers that captured `window.$` early (tests, auto-initializers) can `await $.ready` without polling. New `chainInstanceReady` export links the two.
412
-
413
- - **Async component scripts** (`runtime/component.js`) — `<script type="module">` blocks in components with `import` statements are rewritten to `await import()` and executed via `AsyncFunction`. Sync execution preserved when there are no imports (keeps boot timing unchanged). Component finalization waits for all async scripts before inlining.
414
- - Tests: `tests/e2e/async-component-state.spec.js`, `tests/e2e/parallel-components.spec.js`
415
-
416
- - **Promises never proxied as reactive state** (`runtime/state.js`) — proxy `get` trap now bails on `value instanceof Promise`. Wrapping a Promise violates the Proxy invariant when exposed as a non-writable property (e.g. `$.ready`) and makes no sense as reactive state.
417
-
418
- ### Fixed
419
-
420
- - **HTML-entity decoding in compiled iteration templates** (`runtime/_vibe-compiled-iteration-batch.js`) — when a template's attributes get serialized via `innerHTML`, the browser encodes `<`, `>`, `"`, `&`, `'`. The batch-function compiler was wrapping those encoded expressions directly in a template literal, producing invalid JS for bindings like `@[x > 0 ? 'a' : 'b']` when used inside an attribute. Entities are now decoded before interpolation.
421
-
422
- - **`<!-- each -->` over `Array.from(...)` and other inline expressions** — previously failed silently because `ITERATION_REGEX` only accepted simple paths. See `ITERATION_REGEX` change above.
423
-
424
- - **Attribute bindings on fetched `<component src>` wrappers were coerced to strings** (`runtime/parse.js`) — `captureAttributeBindings` now returns nulls for elements that are still `<component src>` / `<div class="component" src>` (pre-fetch), so raw prop values survive untouched for `processComponent` to substitute. Post-fetch wrappers (no `src`) capture attributes normally.
425
-
426
- ### Changed
427
-
428
- - **Iteration engine rework** (`runtime/iterate.js`, +378 lines over 1.8.1) — unified update path around tagged instance keys, bulk-replacement fast path when old/new arrays share no common keys (uses `Range.deleteContents` + single `innerHTML`), and DocumentFragment batching inside `renderIteration` so N inserts become one.
429
-
430
- - **Debug scaffolding removed** (`runtime/index.js`) — an ad-hoc `[vibe-debug] rerender` `console.info` and the associated `window.__lastHits` / `window.__parsedTree` probes (leftover from an investigation) have been deleted.
431
-
432
- ### Documentation
433
-
434
- - **README — "Future improvements to refactor"** — documents the string-based dependency-tracking approach used by `affected()` and its always-affected fallback semantics, and the HTML-lowercased attribute-name handling.
435
-
436
- ### Packaging
437
-
438
- - **`@ape-egg/vite-plugin-vibe` published** — new sibling package providing a Vite plugin with HMR for Vibe pages and components. See its own `README.md` and `CHANGELOG.md`.
439
-
440
- ---
441
-
442
- ## [1.8.1] - 2026-04-12
443
-
444
- ### Added
445
-
446
- - **`<slot>` boundary preserved in processed DOM** — component slot content is now wrapped in a `<slot>` element instead of being spliced in directly. Dev tools, HMR, and anything that needs to locate slot boundaries can query for `<slot>` instead of tracking comment pairs.
447
- - `runtime/component.js` — `<slot></slot>` replaced with `<slot>${children}</slot>`
448
- - `compiler/src/parser/html.rs` — compiler emits the same wrapping
449
- - `vibe.css` — `slot, div.slot { display: contents }` so the wrapper is layout-transparent
450
-
451
- ### Fixed
452
-
453
- - **Conditionals inside doubly-nested component slots lost their templates** — when a `<component src>` appeared inside another component's slot content, Vibe's parser ran `renderConditional` (which strips template nodes) before `processComponent` captured the slot content, so branch templates were gone by the time the nested component tried to use them.
454
- - `runtime/index.js` — `processMutations` now captures `_vibeSlotContent` for all nested `<component src>` elements before parse/hydrate runs
455
- - Test: `tests/e2e/conditional-in-nested-slot.spec.js`
456
-
457
- - **Bindings with complex expressions re-hydrated on every state change** — `matchesKey` did prefix-only matching, so expressions like `@[Math.floor(coins / 100)]` couldn't be matched to any state key and hit the "always affected" fallback. A 250ms client clock would re-hydrate every such binding 4× per second.
458
- - `runtime/affected.js` — `matchesKey` now also does word-boundary search so it finds `coins` as an identifier inside the expression
459
- - Test: `tests/e2e/expression-dependency.spec.js`
460
-
461
- - **Iteration instance comparisons used live Proxy as "old state"** — `scopedState` is a Proxy that reflects current global state, so comparing `state[k]` vs `newState[k]` showed both as the new value. Global state changes inside iterations weren't detected as changes.
462
- - `runtime/affected.js` — iteration recursion now builds plain-object snapshots from previousState/currentState merged with scopedState's local vars
463
-
464
- - **Name bindings re-hydrated on every state change** — HTML lowercases attribute names, so `<page @[pageName]>` becomes `@[pagename]` in the DOM. `matchesKey('pagename', 'pageName')` failed (case-sensitive), triggering the always-affected fallback and causing constant attribute flashing.
465
- - `runtime/affected.js` — name binding dependency check now matches state keys case-insensitively, mirroring `hydrate.js`'s existing case-insensitive evaluation fallback
466
-
467
- ### Documentation
468
-
469
- - `README.md` — "Future improvements to refactor" section documenting string-based dependency tracking limitations and HTML lowercase attribute handling
470
-
471
- ---
472
-
473
- ## [1.8.0] - 2026-04-10
474
-
475
- ### Added
476
-
477
- - **State batching** — Multiple `$.prop = value` assignments in the same microtask now produce a single rerender instead of cascading updates
478
- - Proxy `set` trap calls `scheduleFlush()` (via `queueMicrotask`) instead of `rerender()` directly
479
- - Eliminates cascading rerenders from `afterUpdate` hooks setting state
480
- - Rerender callback now does full diff of `previousState` vs `currentState` (no longer receives individual changed prop)
481
-
482
- - **DOM ownership tracking** — Two new data structures prevent re-processing of already-managed nodes
483
- - `managedNodes` (`WeakSet`): Tracks nodes inserted by `mountBranch` / `renderIteration`. `shouldProcessNode` skips them.
484
- - `branchNodeRegistry` (`WeakMap`): Maps DOM nodes to their conditional branch, so `processComponent`'s `el.replaceWith()` correctly updates conditional tracking
485
-
486
- - **Parallel component loading** — Same-level `<component src>` elements now fetch in parallel
487
- - `isNestedInUnprocessedComponent()` filters slot content from premature processing
488
- - `processSingle()` handles fetch + script execution + DOM replacement per component
489
- - `processComponent()` fires all top-level fetches simultaneously; nested components discovered after parent finalizes
490
-
491
- - **Component script imports** — `<script type="module">` in components now supports `import` statements
492
- - Static imports rewritten to dynamic `await import()` (default, named, namespace, side-effect)
493
- - `import component from '...'` is stripped (Vibe injects it as a parameter)
494
- - `AsyncFunction` constructor used only when imports exist; sync path preserved for boot timing
495
-
496
- - **Bracket notation in paths** — `resolvePath` now handles `teams[0].combatants` via `path.match(/[^.\[\]]+/g)` splitting
497
-
498
- - **Quoted strings in binding expressions** — `BINDING_REGEX` now supports `@[x.replace('.png', '-mugshot.png')]`
499
-
500
- ### Changed
501
-
502
- - **Conditional branch lifecycle** — `mountBranch` now integrates branch tree into both the conditional node's children and the manifest (via `addBranchToManifest`). `unmountBranch` cleans up both. Makes branch content visible to the main update loop and MutationObserver.
503
-
504
- - **New-node processing** — `processCoreLoop` filters iteration and conditional types from `affectedElements` when `isNewNode = true`. Initial render goes through `renderAllIterations` / `renderAllConditionals`, not the update/diff path.
505
-
506
- - **Component discovery after state changes** — After hydrate + renderAll in the state callback, scans for unresolved `<component src="">` elements that conditionals/iterations may have mounted while the observer was disconnected.
507
-
508
- ### Performance
509
-
510
- - **`evalInScope` function cache** — Compiled `new Function()` objects cached by expression + state keys signature. For 1000 rows × 4 bindings, reduces 4000 Function compilations to ~4.
511
- - **Bulk iteration replacement** — When old and new arrays share no common keys, skips O(n²) LCS entirely. Uses `Range.deleteContents()` for instant clear + `fastPath.renderFast` (batch string concatenation + single `innerHTML`) for simple templates.
512
- - **DocumentFragment batching** — `renderIteration` collects all cloned nodes in a DocumentFragment, single `insertBefore` at the end instead of N × M individual DOM mutations.
513
- - **Scoped state `ownKeys` caching** — `createScopedState` pre-computes the combined key list once at Proxy creation. `ownKeys()` trap returns cached array directly instead of rebuilding 3 arrays + 1 Set per call.
514
- - **`extractPlainValue` optimization** — Indexed `for` loops, `Object.keys()` instead of `for..in`, inline primitive check, pre-allocated arrays.
515
- - **Old-array ground truth** — `updateIteration` uses rendered instances as truth when `oldState` disagrees with actual instance count, preventing mismatched diff operations.
516
-
517
- ### Removed
518
-
519
- - **`window.__VIBE_FAST_ITERATION__` experimental flag** — Replaced by automatic bulk replacement detection (no opt-in needed)
520
- - **Per-prop rerender** — State proxy no longer passes `{ [changedProp]: ... }` to rerender; full diff via batching replaces it
521
-
522
- ---
523
-
524
- ## [1.7.2] - 2026-02-27
525
-
526
- ### Fixed
527
-
528
- - **Component inlining — hyphenated custom elements inside slot content** (`find_matching_close`)
529
- - `<component-card>`, `<component-grid>`, etc. inside slot content incorrectly matched `<component\b`, incrementing the depth counter without a corresponding close
530
- - The real `</component>` could never bring the depth back to 0, so `find_matching_close` returned `None` and the outer component was left un-inlined (causing `Cannot GET /components/Layout.html` at runtime)
531
- - Fixed by replacing `\b` with `(?:\s|>|/>)` — only matches actual `<component` tags followed by whitespace or `>`, not hyphenated variants
532
-
533
- ---
534
-
535
- ## [1.7.1] - 2026-02-27
536
-
537
- ### Added
538
-
539
- - **Linux x64 binary** (`vibe-compiler-linux-x64`) — cross-compiled from macOS ARM using `musl-cross` and the `x86_64-unknown-linux-musl` Rust target
540
- - Enables the compiler to run on Linux environments (e.g. Vercel, Docker, CI) without a local Rust toolchain
541
- - New `compiler:build:linux` npm script builds the musl binary
542
- - `publish:vibe:prepare` now builds both macOS ARM and Linux x64 binaries before publishing
543
-
544
- ### Changed
545
-
546
- - **`reqwest` TLS backend** — switched from `native-tls` to `rustls-tls` with `default-features = false`
547
- - Eliminates the OpenSSL dependency that prevented cross-compilation to `x86_64-unknown-linux-musl`
548
- - Pure-Rust TLS implementation; no system libraries required
549
-
550
- ---
551
-
552
- ## [1.7.0] - 2026-02-19
553
-
554
- ### Fixed
555
-
556
- - **Watch mode stale component cache** - Incremental recompilation no longer uses stale component content
557
- - Component cache is cleared before every recompile trigger, ensuring components are always re-read from disk
558
- - Component cache and parser element cache are also invalidated immediately when a component file changes
559
- - Fixes corrupted output (old event handlers, wrong attributes) that appeared on every watch-mode save after the first
560
-
561
- - **Custom element transform — nested hyphenated tags** (`elementsAsIs: false`)
562
- - `<accordion-content>` inside `<accordion>` previously produced `<div class="accordion" -content>` due to the `<accordion>` regex partially matching the longer tag name
563
- - Fixed by requiring whitespace or end-of-tag immediately after the tag name in the opening tag regex
564
- - Tags are now also sorted by descending length so more specific names (e.g. `accordion-content`) are always processed before their prefixes (`accordion`)
565
-
566
- - **Custom element transform — existing class preserved** (`elementsAsIs: false`)
567
- - `<crow class="i-should-preserve">` previously compiled to `<div class="crow">`, discarding the original class
568
- - Existing `class="..."` is now merged: result is `<div class="crow i-should-preserve">`
569
-
570
- - **`elementsAsIs` config respected in dependency scanning**
571
- - `fetch_components_for_files` and `fetch_all_components` were hardcoded to `elements_as_is: false`, ignoring the project config
572
- - Both functions now correctly read `self.config.elements_as_is` and `self.config.reserved_elements`
573
-
574
- - **`vibe-dehydrate` protected during manifest stamping**
575
- - Content inside `<template vibe-dehydrate>` was incorrectly processed during value stamping
576
- - Dehydrated regions are now extracted before processing and restored afterwards
577
-
578
- - **Conditional evaluation errors handled gracefully**
579
- - A failed `<!-- if ... -->` expression (undefined variable, syntax error) now defaults to `false` instead of crashing
580
-
581
- ### Changed
582
-
583
- - **Lifecycle event API** — `vibe()` now returns an object that supports `.on(event, callback)` before boot completes
584
- - `$.on('ready', cb)` — fires once after all components load and initial processing completes
585
- - `$.on('afterUpdate', cb)` — fires on every state change
586
- - `$.on('afterDomMutation', cb)` — fires after every DOM mutation batch
587
- - Listeners registered before boot are queued and replayed once the runtime is ready
588
-
589
- - **Component `onComplete` timing** — deferred via `queueMicrotask` to give user code a chance to register listeners before the ready callback fires
590
-
591
- - **Iteration performance** — `cloneTreeNode` avoids spread operator; `nameBindings` now correctly carried through cloned iteration trees
592
-
593
- ---
594
-
595
- ## [1.6.1] - 2026-02-12
596
-
597
- ### Fixed
598
-
599
- - **Compiler output directory handling** - Fixed duplicate file generation in output
600
- - Compiler now properly handles source and output paths without preserving source directory structure
601
- - Files from `./src` are now correctly written to root of output directory (e.g., `compiled/index.html`) instead of `compiled/src/index.html`
602
- - Canonicalized source path for reliable comparison and path stripping
603
- - Updated all processing functions to use canonical source path for relative path calculation
604
-
605
- - **Manifest detection improvements** - Better handling of directory URLs and extensionless paths
606
- - Directory URLs now normalized: `/compiled/` → `/compiled/index.html`
607
- - Extensionless URLs now normalized: `/compiled/mypage` → `/compiled/mypage.html`
608
- - Fixes manifest detection when visiting pages without explicit `.html` extension
609
- - Enables proper hyperspeed loading for directory index pages
610
-
611
- ---
612
-
613
- ## [1.6.0] - 2026-02-12
614
-
615
- ### Added
616
-
617
- - **Watch mode** - Compiler now supports file watching with incremental compilation
618
- - New `--watch` flag monitors source files for changes and automatically recompiles
619
- - Intelligent change detection tracks affected files and their dependencies
620
- - Transitive dependency tracking: changes to components trigger recompilation of pages using them
621
- - Blacklist support: watch mode respects SKIP_FILES patterns (tests/, node_modules/, etc.)
622
- - Debounced file events (300ms) prevent excessive compilation during rapid changes
623
- - Outputs only changed files for fast incremental builds
624
- - First compile shows full output, subsequent compiles show only deltas
625
- - **Reserved element validation** - Compiler now prevents component naming conflicts
626
- - New `reservedElements` config option (replaces `excludeTags`)
627
- - Defaults to all HTML5 elements plus "component" keyword
628
- - User-provided values append to defaults (not replace)
629
- - Case-sensitive validation: `nav.html` → Error, `Nav.html` → OK
630
- - Compile-time error with clear message showing conflicting filename
631
- - Prevents runtime confusion between HTML elements and custom components
632
- - **Component path case-sensitivity** - Component paths are now treated as case-sensitive
633
- - `<component src="path/to/MyComponent.html">` and `<component src="path/to/mycomponent.html">` are different
634
- - Both runtime and compiler preserve exact case in paths
635
- - Cache keys include full case-sensitive path
636
- - Enables PascalCase naming convention for components while supporting any casing
637
-
638
- ### Changed
639
-
640
- - **Component inlining architecture** - Complete rewrite from iterative to recursive fetching
641
- - Removed MAX_ITERATIONS constant and loop-based approach
642
- - `fetch_component_recursive()` now returns fully resolved content with all nested components inlined
643
- - Components are cached only after being fully resolved (prevents incomplete content in cache)
644
- - `inline_component_elements()` now does single pass (no loops)
645
- - Significant performance improvement: sub-100ms compilation for complex nested components
646
- - **Framework element handling** - `<component>` is now recognized as a framework element
647
- - Never transformed to `<div>` regardless of `--elements-as-is` flag
648
- - Custom elements (e.g., `<card>`, `<text>`) are transformed when `elements_as_is: false`
649
- - Framework elements vs custom elements properly distinguished in compiler
650
- - Wrapper `<component>` tags remain after inlining (expected by runtime)
651
- - **Component inlining behavior** - Custom elements are always inlined
652
- - `<card>` → transformed to `<component>` → inlined even with `--components-as-is`
653
- - `--components-as-is` only affects explicit `<component src="...">` tags
654
- - Consistent behavior: custom elements compile away, framework elements remain
655
- - **Path normalization** - Component paths now consistently normalized
656
- - All paths start with `/` (unless external URL)
657
- - Cache uses normalized paths to prevent duplicates (`/components/nav.html` vs `./components/nav.html`)
658
- - Fixes cache pollution from different path formats for same component
659
- - **Config naming** - `excludeTags` renamed to `reservedElements` throughout codebase
660
- - Better describes purpose (reserved from use as component names)
661
- - Updated in Rust compiler, config files, and documentation
662
- - Backward compatible: old config key still works but deprecated
663
-
664
- ### Fixed
665
-
666
- - Watch mode was compiling blacklisted files (tests/, node_modules/)
667
- - Component inlining created infinite nested wrappers (100+ levels)
668
- - Empty headlines in tests due to broken external component fetching
669
- - Path case normalization could cause duplicates in component cache
670
- - Accessibility transformation incorrectly converting `<component>` to divs
671
- - Test expectations mismatched actual framework element behavior
672
-
673
- ### Performance
674
-
675
- - Watch mode incremental compilation: ~100ms for typical changes
676
- - First full compilation: ~300-500ms
677
- - Component fetching: recursive approach 10x faster than iterative (no MAX_ITERATIONS overhead)
678
- - Path normalization prevents redundant fetches of same component
679
-
680
- ---
681
-
682
- ## [1.5.0] - 2026-02-09
683
-
684
- ### Added
685
-
686
- - **Nested iteration compilation** - Compiler now handles infinitely nested `<!-- each -->` blocks
687
- - Recursive processing in `iteration_optimizer.rs` with depth counting for comment pair matching
688
- - Nested loops compile to IIFEs (Immediately Invoked Function Expressions) with template literals
689
- - Inner iterations inlined directly into outer batch functions for optimal performance
690
- - Example: `<!-- each categories as cat --><!-- each cat.items as item -->` compiles to single optimized function
691
- - **QuickJS JavaScript runtime** - Full expression evaluation at compile time
692
- - Embedded QuickJS engine (`rquickjs = "0.6"`) for JavaScript evaluation in Rust
693
- - No external dependencies - increases binary size by ~1-2MB
694
- - Evaluates any JavaScript expression: `@[categories.length]`, `@[items[0]]`, `@[user.name.toUpperCase()]`
695
- - State set in global scope: `Object.assign(globalThis, $)` matches Vibe runtime behavior
696
- - Fast evaluation: ~160ms overhead for 35 files (~5ms per file)
697
- - **Complete pre-rendering** - Zero FOUC with all bindings pre-rendered for SEO
698
- - All `@[expression]` bindings evaluated and stamped into HTML at compile time
699
- - Handles property access (`@[user.name]`), array methods (`@[categories.length]`), and complex expressions
700
- - Nested iterations fully pre-rendered with merged state for each iteration context
701
- - Falls back gracefully: undefined expressions left as `@[...]` for runtime hydration
702
- - New test: `stamp_array_length` validates `.length` property evaluation
703
-
704
- ### Changed
705
-
706
- - **Value stamper rewrite** (`value_stamper.rs`) - Complete overhaul to use QuickJS
707
- - Replaced JSON path resolution with JavaScript expression evaluation
708
- - `eval_expression()` handles any valid JavaScript with state in scope
709
- - `eval_array_path()` evaluates array paths for iteration rendering
710
- - Iteration rendering creates merged state (parent + item + index) for nested context
711
- - Removed manual property traversal code - JavaScript engine handles it all
712
- - **Compiled iteration updates** - Always use compiled path when available (`iterate.js:364`)
713
- - Previously only used compiled updates for edge cases (empty↔full, large arrays >100)
714
- - Now uses compiled batch functions for ALL updates when manifest has `compiled.iterations.batchFn`
715
- - Fixes issue where small array updates (3→4 items) fell through to incompatible runtime path
716
- - Ensures consistent performance regardless of array size or transition type
717
- - **State extraction improvements** (`state_extractor.rs`) - Better error handling
718
- - Silently skips unparseable state objects instead of failing compilation
719
- - Enables graceful degradation when state contains functions or complex expressions
720
-
721
- ### Fixed
722
-
723
- - Compiled iterations not updating when array size changes (e.g., add/remove items)
724
- - Pre-rendering skipped for JavaScript expressions like `@[categories.length]`
725
- - Nested iteration values showing as `@[item]` instead of actual data
726
- - Runtime path attempting to handle compiled iterations incorrectly
727
-
728
- ### Performance
729
-
730
- - Pre-rendering adds ~160ms to compilation for 35 files (~435ms total, up from ~310ms)
731
- - QuickJS evaluation: ~1-5ms per binding
732
- - Compiled nested iterations: Same performance as shallow iterations (no recursion overhead at runtime)
733
- - Zero runtime cost for pre-rendered bindings - HTML arrives with values already stamped
734
-
735
- ---
736
-
737
- ## [1.4.0] - 2026-02-09
738
-
739
- ### Added
740
-
741
- - **Iteration optimization** - Compiler now generates optimized batch functions for `<!-- each -->` loops
742
- - New `iteration_optimizer.rs` module generates string-based batch render functions
743
- - Provides 2-3x performance improvement for iteration rendering (15-17ms vs 40ms for 1000 rows)
744
- - Only applies to shallow iterations (nested iterations still use runtime path)
745
- - Compiled batch functions are stored in manifest and executed at runtime
746
- - **Pre-compiled iterations runtime** (`runtime/pre-compiled-iterations.js`)
747
- - Production implementation of compiled iteration rendering
748
- - Uses pre-compiled batch functions from manifest generated at build time
749
- - Based on the prototype in `_vibe-compiled-iteration-batch.js`
750
- - Automatically falls back to runtime rendering for nested iterations or missing batch functions
751
- - **Compiler configuration** - New `iterationsAsIs` flag
752
- - Set to `true` in `vibe-compiler` config to skip iteration optimization
753
- - Iterations pass through unchanged and are handled entirely by runtime
754
- - Default: `false` (iterations are optimized)
755
- - **Hyperspeed benchmark** - New benchmark page for measuring pre-compiled performance
756
- - Tests iteration optimization with 1000 rows
757
- - Compares runtime vs compiled iteration rendering
758
- - Available at `e2e-runtime/hyperspeed-benchmark.html`
759
- - **Compiler test coverage** for iteration compilation
760
- - `tests/compiler/iterations/` - Tests iteration optimization is applied
761
- - `tests/compiler/iterations-as-is/` - Tests `iterationsAsIs` flag skips optimization
762
- - E2E tests verify compiled iterations render correctly
763
-
764
- ### Changed
765
-
766
- - **Renamed `runtime/hyperspeed.js` → `runtime/pre-compiled-manifest.js`**
767
- - Better naming to reflect that it handles all pre-compiled features, not just "hyperspeed"
768
- - Updated all imports and references throughout codebase
769
- - **Enhanced manifest merging** - Compiler manifest now preserves compiled iteration data
770
- - Iteration nodes retain `compiled.iterations.batchFn` from manifest during runtime merge
771
- - Prevents compiled data from being discarded when merging with runtime tree
772
- - **Debug logging improvements**
773
- - Added manifest filename to debug output ("Loaded index.manifest.js, page is pre-compiled")
774
- - Shows compiled feature count (e.g., "3 iterations optimized")
775
- - Better visibility into which optimizations are active
776
- - **Runtime iteration handling** - Iterations check for compiled batch functions before falling back
777
- - `canUseCompiled()` determines if iteration can use batch function
778
- - Falls back to runtime rendering for nested structures or missing compiled data
779
- - Seamless integration between compiled and runtime paths
780
-
781
- ### Fixed
782
-
783
- - Runtime cleanup now properly handles compiled iteration nodes
784
- - Manifest merge no longer discards compiled data from iteration nodes
785
- - Debug logging visibility flag now correctly propagates through manifest loading
786
- - Iteration restoration respects compiled batch functions
787
-
788
- ---
789
-
790
- ## [1.3.2] - 2025-02-06
791
-
792
- ### Fixed
793
-
794
- - **Package completeness**: Added missing `boot.js` to npm package
795
- - File was missing from the "files" allowlist in package.json
796
-
797
- ### Changed
798
-
799
- - **Package strategy**: Switched from allowlist to denylist approach
800
- - Removed "files" field from package.json
801
- - Added `.npmignore` to exclude build artifacts (`target/`) and dev files
802
- - Ensures all source files are included without manual maintenance
803
-
804
- ---
805
-
806
- ## [1.3.1] - 2025-02-06
807
-
808
- ### Fixed
809
-
810
- - **Package exports**: Added missing `./component` export to package.json
811
- - Enables proper import: `import component from '@ape-egg/vibe/component'`
812
- - Previously `component.js` was included in package files but not exposed via exports field
813
-
814
- ---
815
-
816
- ## [1.3.0] - 2025-02-06
817
-
818
- ### Added
819
-
820
- - **Static Analysis Compiler**: Replaced browser-based manifest generation with pure Rust static analysis
821
- - New compiler modules: `state_extractor.rs`, `manifest_builder.rs`, `value_stamper.rs`
822
- - ~3000x performance improvement (10-100 seconds → ~6ms for manifest generation)
823
- - Pre-rendering support for iterations with initial state values
824
- - Graceful handling of unparseable state (skips files instead of failing entire compilation)
825
-
826
- - **Enhanced vibe() and component() API**:
827
- - Added `config` parameter (second argument) for runtime configuration
828
- - Added `targetSelector` parameter (third argument) for custom root element selection
829
- - Multiple calls accumulate state, config/targetSelector use "first wins" strategy
830
- - Example: `vibe({ count: 0 }, { debug: true }, 'body')`
831
-
832
- - **Expanded Compilation Scope**: Compiler now processes all HTML files recursively
833
- - Compiles all `<source-root>/**/*.html` (excluding `components/` directory)
834
- - Generates manifests for all `<output-dir>/**/*.html` (excluding `components/`)
835
- - Previously limited to `<source-root>/pages/` only
836
-
837
- ### Changed
838
-
839
- - **Iteration Restoration**: Improved DOM restoration algorithm
840
- - Uses TreeWalker to find iteration comment pairs (more robust)
841
- - Avoids index-based lookup that breaks after DOM structure changes
842
- - Runtime now fully controls iteration nodes (skipped during manifest merge)
843
-
844
- - **Compiler Output**: Enhanced user experience with better formatting
845
- - Manifest generation occurs before "Compilation successful!" message
846
- - Verbose mode (`--verbose`) shows only warnings/errors for manifest generation
847
- - Clean title: "Generating manifests (static analysis)"
848
- - Summary format: `Generated manifests (X files, Y skipped) in Zms`
849
- - Positioned between "Compiled HTML" and "Copied files" in output
850
-
851
- - **Debug Logging**: Refined hyperspeed detection messages
852
- - Removed redundant path-specific log
853
- - Changed to: "Detected vibe-hyperspeed. Applying pre-compiled manifest."
854
-
855
- ### Removed
856
-
857
- - **Browser Automation Dependencies**: Eliminated heavy runtime dependencies
858
- - Removed: chromiumoxide, tiny_http, tokio, futures
859
- - Deleted 414 lines of browser automation code (`manifest.rs`)
860
- - Pure Rust implementation with no external processes or async complexity
861
-
862
- ### Fixed
863
-
864
- - Manifest merge conflicts between hyperspeed (pre-compiled) and runtime trees
865
- - Index shifting bugs during restoration phase that caused key mismatches
866
- - Iteration rendering producing duplicate items (3x3 instead of 3)
867
- - Pre-rendered iteration content not being properly replaced with reactive templates
868
-
869
- ### Performance
870
-
871
- - Manifest generation: 10-100 seconds → ~6ms (~3000x faster)
872
- - Total compilation time: Typically completes in 200-300ms for medium projects
873
- - Zero browser startup overhead
874
- - Reduced memory footprint (no Chromium instance)
875
-
876
- ---
877
-
878
- ## [1.2.0] - 2026-02-03
879
-
880
- ### Added
881
-
882
- - **Component state isolation**: Components now have their own isolated state using `<script type="component">` blocks
883
- - Each component gets a unique ID (`_cTIMESTAMP_RANDOM`) automatically generated and stored in `data-vibe-component-id` attribute
884
- - Component state lives at `$[componentId].property` in the global state object
885
- - Variable declarations (`let count = 0`) are automatically transformed to property assignments (`this.count = 0`)
886
- - Clean separation between global state and component-specific state
887
- - Example component (subject to change!):
888
- ```html
889
- <script type="component">
890
- let count = 0;
891
- let increment = () => { this.count++; };
892
- </script>
893
- <button onclick="this.increment()">Clicked @[this.count] times</button>
894
- ```
895
- - **`this.property` syntax**: Reference component-scoped state from anywhere inside a component
896
- - Works in bindings: `@[this.count]`
897
- - Works in attributes: `value="@[this.inputValue]"`
898
- - Works in event handlers: `onclick="this.increment()"`
899
- - Works in name bindings: `@[this.iconName]`
900
- - Works in conditionals: `<!-- if this.isVisible -->`
901
- - Works in iterations: `<!-- each this.items as item -->`
902
- - Runtime automatically resolves `this.property` → `$['componentId'].property`
903
- - **Component script execution**: `<script type="component">` blocks execute in their own scope
904
- - Scripts run when component HTML is fetched (runtime `<component>` resolution)
905
- - State is registered in global `$` object under component ID
906
- - All siblings after the script tag inherit the component ID via `data-vibe-component-id` attribute
907
- - Multiple component scripts in same HTML create separate component instances with unique IDs
908
- - **Props and slots integration**: Component state works seamlessly with existing `<component>` features
909
- - Props can set component state: `<component src="/card.html" theme="@[userTheme]">`
910
- - Props work with `this.` references: `@[this.theme]` inside card.html
911
- - Slots work inside component-scoped HTML
912
- - Components can be nested with isolated state at each level
913
-
914
- ### Changed
915
-
916
- - **Component ID tagging**: DOM elements are now tagged with `data-vibe-component-id` during component processing (previously only scripts had this attribute)
917
- - All siblings after a `<script type="component">` get tagged with the same component ID
918
- - Tagging stops when hitting another component script or end of HTML
919
- - Enables `this.property` resolution in any context (bindings, events, conditionals, iterations)
920
- - **Event handler rewriting**: Event handlers with `this.property` are now rewritten at parse time
921
- - `onclick="this.increment()"` → `onclick="$['_c123_abc'].increment()"`
922
- - DOM properties (like `this.value`, `this.checked`) are preserved and not rewritten
923
- - Prevents conflicts between component state access and native DOM properties
924
-
925
- ### Technical Details
926
-
927
- - **Component state lifecycle**:
928
- 1. `<component src="/path.html">` fetches HTML
929
- 2. HTML is parsed in temporary container
930
- 3. `<script type="component">` blocks are found and executed
931
- 4. Each script generates unique component ID
932
- 5. Script and following siblings are tagged with `data-vibe-component-id`
933
- 6. Component state is registered at `$[componentId]`
934
- 7. Props are applied (with `this.` reference rewriting)
935
- 8. Slots are replaced
936
- 9. Transformed HTML replaces `<component>` element
937
- 10. MutationObserver triggers reactive hydration with component context
938
- - **`this.property` resolution**: Helper function `resolveThisPath()` in utils.js walks up DOM tree to find nearest `data-vibe-component-id`, then rewrites path from `this.property` → `componentId.property`
939
- - **Expression evaluation**: `evalInScope()` in utils.js handles both global (`$.property`) and component-scoped (`$['componentId'].property`) state access, with case-insensitive fallback for HTML-lowercased attribute names
940
-
941
- ---
942
-
943
- ## [1.1.3] - 2026-02-02
944
-
945
- ### Added
946
-
947
- - **MutationObserver performance optimization**: Fast filter with short-circuit evaluation
948
- - New `shouldProcessNode()` function checks for Vibe syntax before expensive processing
949
- - Short-circuits on first match: most Vibe nodes contain `@[`, so check exits immediately
950
- - Filters out third-party framework mutations (React, Vue, etc.) with cheap string operations
951
- - Only walks DOM tree for nodes that actually contain Vibe syntax (`@[`, `<!-- each`, `<!-- if`, `<component>`)
952
- - Enables efficient coexistence with other frameworks on the same page
953
-
954
- ---
955
-
956
- ## [1.1.2] - 2026-02-01
957
-
958
- ### Fixed
959
-
960
- - **Compiler hotfix**: Empty custom element tags no longer cause HTML content to be cut out during compilation
961
- - Root cause: `<Nav></Nav>` was compiled to `<component src="/components/Nav.html">` (no closing tag), causing the inlining regex to match from Nav's opening tag to the next component's closing tag (e.g., `<Headline>`'s `</component>`), treating all HTML in between as children of Nav
962
- - Solution: All custom element tags now compile to properly closed `<component>...</component>` tags, even when empty
963
- - Prevents regex from matching across multiple components
964
-
965
- ---
966
-
967
- ## [1.1.1] - 2026-02-01
968
-
969
- ### Added
970
-
971
- - **Compiler CLI**: Modern command structure with subcommand support
972
- - New bin command: `vibe`
973
- - Subcommand syntax: `bunx vibe compile [options]` or shorthand `bunx vibe c [options]`
974
- - Accessible directly via `bunx` without manual npm scripts
975
- - Automatic detection of unknown subcommands with helpful error messages
976
- - Backward compatibility: works with or without `compile` subcommand
977
- - **Validation time tracking**: Compiler now reports component validation phase timing separately
978
- - Shows time spent fetching external components
979
- - Helps explain total compilation time vs individual phase times
980
- - Displayed as: `* Validated components in XXms`
981
- - **Component tree output**: Improved verbose logging for component compilation
982
- - Tree structure with nested dependencies (e.g., nav.html → menu.html)
983
- - Usage counts for each unique component
984
- - Color-coded: internal (green) vs external (magenta)
985
- - Summary format: `Compiled components (X internal, Y external)`
986
- - Clean hierarchical display with `└─` for children
987
- - **Compiler documentation**: Updated READMEs with new CLI commands
988
- - `/vibe/README.md` and `/vibe/compiler/README.md` now show `bunx vibe compile` syntax
989
- - Added npm scripts examples for optional shortcuts
990
- - Removed outdated `bun vibe:compile` references
991
-
992
- ### Changed
993
-
994
- - **Compiler output formatting**: Consistent spacing between major sections (Vibe Compiler, Compiler config, Compiled components, etc.)
995
- - **Verbose logging**: Removed redundant component loading list, streamlined validation output
996
-
997
- ---
998
-
999
- ## [1.0.7] - 2026-01-30
1000
-
1001
- ### Added
1002
-
1003
- - **`<component>` element**: Runtime implementation of reusable HTML components
1004
- - Fetch HTML from local or remote sources: `<component src="/components/card.html">`
1005
- - Props system: Pass reactive bindings `headline="@[title]"` or static values `theme="dark"`
1006
- - Children via `<slot>`: Content between tags replaces `<slot></slot>` in fetched HTML
1007
- - Recursive resolution: Fetched HTML can contain more `<component>` elements
1008
- - Runtime-first with future compiler optimization planned (Phase 5)
1009
- - **Deep reactivity**: Nested property mutations now automatically trigger updates
1010
- - Recursive proxy implementation wraps all nested objects and arrays
1011
- - `$.obj.nested.prop = x` now triggers reactivity (previously required `$.obj = { ...$.obj, nested: { ...$.obj.nested, prop: x } }`)
1012
- - WeakMap cache prevents double-wrapping of proxies
1013
- - Tracks root property for each mutation to trigger correct affected element checks
1014
- - Same approach as Vue 3 - tiny performance cost for massive DX improvement
1015
- - **Scoped state for iterations**: Iteration instances now preserve local context for reactive updates
1016
- - Each `<!-- each items as item -->` instance stores its own `scopedState` (Proxy wrapper with local variables)
1017
- - When global state changes (e.g., `tutorialProgress`), affected elements inside iterations can access both local (`item`) and global variables
1018
- - Enables reactive attributes like `completed="@[tutorialProgress[item.id]]"` to update correctly without full re-render
1019
- - Scoped state flows through `affected.js` → `hydrate.js` pipeline for proper evaluation context
1020
- - **Compiler (first pass)**: Rust-based optional build tool
1021
- - Asset handling with whitelist approach (fonts, images, media, docs automatically copied)
1022
- - HTML/CSS/JS processing
1023
- - Configuration via `package.json` (`vibe-compiler` section)
1024
- - Node.js wrapper for cross-platform execution
1025
- - Foundation for future optimizations (component inlining, iteration optimization, two-way binding sugar)
1026
- - **Unit tests**: Comprehensive unit test suite (212 tests) for runtime modules using Bun's test framework - tests pure function layer (utils, state, iteration-utils, conditionals, affected, manifest) with aggressive edge case coverage
1027
- - **NPM scripts**: Added `bun unit` and `bun unit:watch` commands for running unit tests
1028
- - **Name bindings**: Bindings in attribute name position now supported
1029
- - Syntax: `<icon @[section.icon]></icon>` evaluates expression and sets it as attribute name
1030
- - HTML lowercases attribute names, so case-insensitive property lookup is used for simple properties
1031
- - Old attributes are automatically cleaned up when the bound value changes
1032
- - Useful for dynamic attribute names like icon libraries, custom elements, or conditional attributes
1033
- - Works in all contexts: root level, iterations, conditionals, and inside `<component>` elements
1034
-
1035
- ### Fixed
1036
-
1037
- - **Replace abort**: `<component>` elements now properly abort pending fetches (via `AbortController`) when removed from DOM before fetch completes, preventing "no parent node" errors and saving bandwidth
1038
- - **Nested iterations and conditionals**: Fixed issue where nested `<!-- each -->` and `<!-- if -->` blocks inside `<component>` elements would render then immediately disappear
1039
- - Root cause: MutationObserver re-parsing created fresh runtime state, causing iterations to think they needed re-rendering
1040
- - Solution: Added `__vibeRendered` marker on comment nodes that persists across re-parses
1041
- - Now properly handles complex nested structures like menu systems with dynamic sections and items
1042
- - **Conditionals wrapping iteration content**: Fixed conditionals in attribute name position (e.g., `<!-- if condition -->` wrapping `<nav-section>`) not being detected inside iteration templates
1043
- - Root cause: `initializeBlock()` only parsed the first element node, missing comment nodes like `<!-- if -->`
1044
- - Solution: Parse all template nodes in a container to capture comment nodes, ensuring tree element references match actual DOM nodes
1045
- - Enables patterns like conditional menu items: `<!-- each items as item --><!-- if condition --><item>...</item><!-- /if --><!-- /each -->`
1046
- - **StructuredClone with proxies**: Replaced `structuredClone()` with custom `deepClone()` that properly unwraps proxy objects for state snapshots in hooks
1047
-
1048
- ---
1049
-
1050
- ## [1.0.6] - 2026-01-29
1051
-
1052
- ### Added
1053
-
1054
- - **Debug mode**: New optional third parameter to enable colored console logging of Vibe's lifecycle phases (Attach, Parse, Manifest, Hydrate, Iterate, Condition, Observe, Fetch, Complete, Update, Mutate)
1055
- - **Debug helper**: Centralized `debugLog()` function with phase-specific color coding and tree-style indentation for nested operations
1056
-
1057
- ### Fixed
1058
-
1059
- - **Nested brackets**: `BINDING_REGEX` and `PURE_BINDING_REGEX` now correctly parse bindings with one level of nested brackets (e.g., `@[items[0]]`, `@[obj[key]]`)
1060
-
1061
- ### Changed
1062
-
1063
- - **Internal naming**: Renamed `link.js` → `manifest.js` and `linkList` → `manifest` throughout codebase to better reflect its purpose as a DOM element registry/manifest
1064
- - **Phase constants**: Added `PHASE_*` constants for all lifecycle phases to ensure consistent naming across debug logs
1065
-
1066
- ---
1067
-
1068
- ## [1.0.5] - 2026-01-25
1069
-
1070
- ### Fixed
1071
-
1072
- - **Expression evaluation**: Normalize whitespace in `@[...]` expressions before evaluation, making bindings resilient to IDE auto-formatting that may break expressions across multiple lines
1073
-
1074
- ### Changed
1075
-
1076
- - **DRY refactor**: Consolidated expression evaluation into single `evalInScope()` function in utils.js, used by hydrate.js, conditionals.js, and affected.js
1077
-
1078
- ---
1079
-
1080
- ## [1.0.4] - 2026-01-24
1081
-
1082
- ### Fixed
1083
-
1084
- - **MutationObserver**: Use `takeRecords()` to preserve pending mutations before disconnecting during state changes, preventing queued DOM mutations (e.g., innerHTML replacements) from being lost when state updates occur simultaneously
1085
-
1086
- ---
1087
-
1088
- ## [1.0.3] - 2026-01-23
1089
-
1090
- ### Added
1091
-
1092
- - **MutationObserver**: Now processes `<!-- if -->` and `<!-- each -->` comments when new nodes are added to DOM, enabling dynamic content to use vibe's control flow without manual processing
1093
-
1094
- ### Fixed
1095
-
1096
- - **affected.js**: Only marks elements as affected when their specific bound values change, preventing unnecessary DOM updates when unrelated state changes (fixes `@[color]` in `<style>` updating on every keystroke when `@[firstName]` changes)
1097
-
1098
- ---
1099
-
1100
- ## [1.0.2] - 2026-01-20
1101
-
1102
- ### Fixed
1103
-
1104
- - **parse.js**: Root element attribute bindings now captured when parsing individual nodes (fixes dynamically added elements with `value="@[...]"` not being hydrated)
1105
- - **MutationObserver**: Added nodes now only hydrate themselves and descendants, not the entire tree (fixes focus loss and mutation cascade on dynamic DOM updates)
1106
- - **MutationObserver**: Removed unnecessary hydration calls when nodes are removed (cleanup only)
1107
- - **MutationObserver**: Batched hook firing - `afterDomMutation` now fires once after all mutations are processed, not per-node (major performance fix for bulk DOM updates)
1108
- - **MutationObserver**: Parent elements are now re-parsed only once per mutation batch, not per-child (performance optimization)
1109
-
1110
- ---
1111
-
1112
- ## [1.0.1] - 2026-01-20
1113
-
1114
- ### Fixed
1115
-
1116
- - Fixed iteration diff bug where MOVE operations corrupted indices, causing wrong items to be deleted
1117
-
1118
- ---
1119
-
1120
- ## [1.0.0] - 2026-01-19
1121
-
1122
- ### Added
1123
-
1124
- - Initial public release
1125
- - Proxy-based reactive state (`window.$`)
1126
- - MutationObserver for automatic DOM tracking
1127
- - Fine-grained updates (no virtual DOM)
1128
-
1129
- ### Core Features
1130
-
1131
- - **Reactive bindings**: `@[property]` syntax in HTML and CSS
1132
- - **Iteration**: `<!-- each items as item, i -->` with efficient diffing
1133
- - **Nested iteration**: `<!-- each category.items as item -->`
1134
- - **Conditionals**: `<!-- if condition -->...<!-- else -->...<!-- /if -->`
1135
- - **Dehydrate**: `<div vibe-dehydrate>` to skip reactive processing
1136
- - **Expression evaluation**: `@[count * 2]`, `@[firstName + ' ' + lastName]`
1137
-
1138
- ### Performance
1139
-
1140
- - Surgical DOM updates - only affected elements re-render
1141
-
1142
- ---
1143
-
1144
- ## [0.0.5] - 2026-01-15
1145
-
1146
- - Heavy optimizations for iteration rendering
1147
- - Added benchmark tooling
1148
- - Improved diffing algorithm for array changes
1149
-
1150
- ## [0.0.4] - 2026-01-10
1151
-
1152
- - Renamed from "Soulbound" to "Vibe"
1153
- - Published to npm as `@ape-egg/vibe`
1154
-
1155
- ## [0.0.1-0.0.3] - 2026-01-06 to 2026-01-09
1156
-
1157
- - Initial development and prototyping
1158
- - Core architecture: parse, link, hydrate, affected, state
1159
- - Basic iteration and conditional support