@ape-egg/vibe 2.3.0 → 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 (57) hide show
  1. package/README.md +14 -4
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +10 -15
  6. package/llms.txt +8 -6
  7. package/package.json +19 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +312 -99
  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 +251 -111
  15. package/runtime/index.js +180 -71
  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 +69 -5
  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 +77 -14
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1196
  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 -2880
  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 -16
  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/spa.rs +0 -477
  47. package/compiler/src/compiler/state_extractor.rs +0 -263
  48. package/compiler/src/compiler/value_stamper.rs +0 -921
  49. package/compiler/src/compiler/watcher.rs +0 -1278
  50. package/compiler/src/config.rs +0 -279
  51. package/compiler/src/main.rs +0 -358
  52. package/compiler/src/parser/element.rs +0 -96
  53. package/compiler/src/parser/html.rs +0 -1004
  54. package/compiler/src/parser/mod.rs +0 -8
  55. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  56. package/runtime/scope.js +0 -50
  57. package/test-results/.last-run.json +0 -4
package/CHANGELOG.md DELETED
@@ -1,1196 +0,0 @@
1
- # Changelog
2
-
3
- ## [2.3.0] - 2026-07-02
4
-
5
- ### Changed
6
-
7
- - **SPA shell ships without `vibe-fouc`** (compiler `spa.rs`) — `vibe-fouc` absence is the compiled-mode marker (the runtime gates hyperspeed manifest loading on it), so the shell carrying it silently ran in runtime mode and never fetched its own manifest — and broke consumers keying compiled-detection off the marker (Battle Brawlers' COMPILED badge). The shell now strips the attribute from the body-attr union instead of adding it: nothing flashes — the outlet is empty and fragments hydrate off-DOM before insertion. Test: `tests/compiler/spa/` (body-attr union asserts the absence); the compiled SPA e2e now exercises the shell's manifest-driven boot.
8
- - **SPA shell vibe-import fallback is browser-resolvable** (compiler `spa.rs`) — when no page imports vibe directly (the import lives in a bundled app-boot module, the game-stack shape), the shell previously emitted the bare `@ape-egg/vibe` specifier, which no browser loads without an import map. The fallback is now the conventional `/node_modules/@ape-egg/vibe/index.js` (spa.js derived from it); harvested page import styles still win when present. Test: `tests/compiler/spa-bundled-vibe/`.
9
- - **Defaults semantics folded into `vibe()` itself; `@ape-egg/vibe/defaults` removed** (`index.js`, `defaults.js` (deleted), compiler `spa.rs`) — `vibe(state)` called once booted now seeds missing keys only (`applyDefaults`, exported from the main entry) instead of `Object.assign`-clobbering live state. "Initial state, declared again" finally means the same thing in both lifetimes, so the separate defaults entry and the compiler SPA mode's fragment import rewrite are gone — fragment scripts are carried byte-identical with the page's own import. 2.2.0 was never published (npm is at 2.1.22), so nothing external loses the entry. Pre-boot behavior is untouched: on a fresh document load MPA behavior is byte-identical. Tests: `tests/unit/defaults.test.js` retargeted to the main entry; `tests/compiler/spa/` + `tests/compiler/spa-inline-children/` now assert imports pass through untouched; the compiled-SPA state-survival e2e proves the folded semantics end-to-end through real fragments.
10
-
11
- ### Added
12
-
13
- - **Watch mode hot-reloads its config** (compiler `watcher.rs`, `config.rs`) — a running `--watch` was a spawn-time config snapshot: flipping `"spa"` (or any vibe-compiler option) in package.json did nothing until you found which process owned the output lock and restarted it by hand. The watcher now watches package.json explicitly (outside the source tree and past `skip_files`, which routinely lists it for copying) and, when the loaded config actually differs from the spawn baseline (formatting-only writes are ignored via `Config: PartialEq`), restarts itself in place: lock released, then `exec` of the same binary with the same argv — CLI flag overrides re-apply with perfect parity, and the pid is preserved so a parent process supervising the child (a dev server watching for compiler exit) never notices. Full recompile under the new config follows naturally, and the standard output-clean removes the other mode's artifacts. Test: `tests/compiler/watch-config-reload/` (spawn `--watch` under `spa: true`, flip to `false`, assert the output switches shape live).
14
- - **Fetched-component mounts are fouc-covered until settled** (`runtime/component.js`) — finalize inserts a fetched component's content and the observer hydrates it across LATER mutation batches, with paints in between: until hydration lands, selectors keyed on hydrated attributes (a name-bound `<page @[page.name]>` driving `page[pvp]`-scoped rules) don't match and the content flashes unstyled — ~300ms of raw skeleton on every Battle Brawlers SPA navigation. The replacement wrapper now carries `vibe-fouc` from insertion until its subtree settles (`shouldCleanup(wrapper)` — the page-ready predicate — checked per afterDomMutation; a wrapper unmounted mid-hydration releases the hook). Applies to every runtime-fetched component, not just SPA outlets; nested fetched children compose (the parent stays covered until they settle too). Test: `tests/e2e/spa-router.spec.js` ("stays fouc-covered until its subtree settles" — deterministic via a held child fetch, both modes).
15
- - **Late `$.on('ready')` fires immediately** (`runtime/index.js`) — the ready phase happens once; a listener registered after it now runs on the spot (same try/catch as phase-time listeners) instead of silently never firing, with parity to the already-late-safe `$.ready` promise. Late registration is the SPA norm: fragment scripts execute on mount, long after the shell booted — an app-boot module imported by a fragment registers its ready work exactly there. Test: `e2e-runtime/late-ready.html` + `tests/e2e/late-ready.spec.js` (both modes: pre-boot control listener fires at the phase, post-ready listener fires immediately).
16
- - **Named routes — `$.page.name`** (`spa.js`) — `resolve()` and the default `onNavigate` now carry `name`, the route slug: segments joined with dashes, params flattened to their bare name (`'/' → 'home'`, `'/pve/:id' → 'pve-id'`, `'/docs/:rest*' → 'docs-rest'`, the `'*'` fallback keeps its literal `'*'`). Stable across param values, so markup hangs page-scoped attributes and active checks on it — `<page @[page.name]>`, `page.name.startsWith('pve')` — the shape MPA route parsers already derive, so page code ports to SPA untouched. Contract is now `$.page = { path, route, params, src, name }`. Tests: `tests/unit/spa.test.js` ("route names" — root, static, multi-segment, param flattening + stability, catch-all, `'*'`), name assertions in both SPA e2e suites.
17
- - **Keyed outlet — `key` on fetched components** (`runtime/parse.js`, `runtime/hydrate.js`, `runtime/component.js`; compiler `spa.rs`, 2.1.1 → 2.2.0) — `key="@[expr]"` on a `<component src>` declares the mount's identity: when the resolved key changes, the component remounts even though the src is unchanged. The compiler's SPA shell now emits `<component src="@[page.src]" key="@[page.path]"></component>`, so param→param navigation on the same route (`/brawlers/0 → /brawlers/1` — same fragment src) mounts fresh, exactly like the MPA reload it replaces. Mechanics: fetched components capture the bound key alongside src (every other attribute stays a raw prop); a key change force-remounts the current src through `forceRemount` — a pending fetch is already a fresh mount (a src change in the same flush deduped for free) and an unmounted wrapper's first mount stays owned by the normal pass; finalize rides `data-vibe-key` + the last resolved key across wrapper replacements the same way `data-vibe-src` travels, so the knowledge survives arbitrarily many navigations. `src` and `key` are the wrapper's own contract and are never passed to the component as props (`data-vibe-*` transport attributes likewise excluded). Tests: `tests/e2e/spa-router.spec.js` + `tests/e2e/spa-compiled.spec.js` (param→param remount runs the fragment script again, both modes + through the real generated shell), `tests/compiler/spa/` + `tests/compiler/spa-inline-children/` (shell emits the keyed outlet).
18
-
19
- ## [2.2.1] - 2026-07-02
20
-
21
- ### Fixed
22
-
23
- - **`nodeModulesAsIs` copied gigabytes of build artifacts through a symlinked local package** (`compiler/src/compiler/compile.rs`, compiler 2.1.0 → 2.1.1) — the as-is copy walks `node_modules` blindly, and a dev-workspace symlink (`node_modules/@ape-egg/vibe → <local vibe repo>`) dragged the package's cargo `target/` along: a 15MB copy became **4.2GB and ~4.3s per clean compile**. `copy_dir_recursive` now honors the Cache Directory Tagging spec — any directory carrying a signed `CACHEDIR.TAG` (cargo stamps `target/` with exactly this so copy/backup tools can skip it) is skipped, with the signature header verified so the check can't false-positive on an ordinary file name. Same fixture: 16MB in 9ms. Nested `node_modules` inside packages, dotdirs like `.bin`, and everything else copy as before — the skip is opt-in by the directory's own declaration, not name-based guessing. Test: `compile.rs` (`as_is_copy_skips_cachedir_tagged_directories`). Affects every project that symlinks the local vibe package with `nodeModulesAsIs: true` (Battle Brawlers' compiled builds included).
24
-
25
- ## [2.2.0] - 2026-07-02
26
-
27
- ### Added
28
-
29
- - **`@ape-egg/vibe/spa` — standalone SPA router** (`spa.js` (new), exported from `package.json`) — the tier between a handrolled router and compiler SPA mode, shaped exactly like `hot-module-refresh.js`: a flat module importing nothing from the runtime, depending only on the public `window.$` surface, usable without Vibe entirely via one seam. One contract: `$.page = { path, route, params, src }`; a reactive `<component src="@[page.src]">` turns that into the route outlet. `resolve(location, routes)` is the pure core (accepts anything with a `.pathname` or a bare string; `:param` captures one segment, trailing `:name*` zero-or-more, `'*'` is a position-independent declared fallback applied only after every real route missed; tables are pre-sorted most-specific-first, first match wins; trailing slashes resolve). `setupSpa({ routes, onNavigate? })` wires one document-level click listener (claims same-origin, unmodified, untargeted clicks whose pathname matches a REAL route — `'*'` never claims, same-page hash anchors keep the native jump, everything else navigates natively, which is what makes mixed MPA/SPA output work), pushState + one fresh-object `$.page` assignment + `document.title` swap when the route carries one + scroll-to-top; `popstate` re-resolves WITH the `'*'` fallback (symmetric with deep-link entry) and doesn't scroll — the browser restores. Returns `{ navigate, dispose }`: `navigate(path)` claims like a click and falls back to a native load for unrouted paths; `dispose()` releases the listeners. The default `onNavigate` reads `window.$` at call time; passing a custom one makes the module a pure router. Tests: `tests/unit/spa.test.js` (resolve: params, catch-alls at every depth, specificity, `'*'`, no-match, location-likes), `e2e-runtime/spa-router.html` + `tests/e2e/spa-router.spec.js` (both modes: claiming without document loads, params → bindings, title swaps, global-state survival vs fresh component mounts, unmount teardown, popstate both directions, native fallthrough, hash anchors, programmatic navigate, dispose).
30
- - **`@ape-egg/vibe/defaults` — the root entry with defaults semantics for booted state** (`defaults.js` (new), exported from `package.json`) — `vibe()` state is app-lifetime under SPA: already booted, only keys that do not yet exist on `$` are set (`applyDefaults`, shallow and key-level — exported pure for reuse), so a re-mounted page fragment's `vibe({ ...globalState, notifications: [] })` seeds on first mount and never clobbers live state after; pre-boot it delegates to `vibe()` unchanged, so on a fresh document load defaults ≡ assign and MPA behavior is byte-identical. The documented rule this encodes: `vibe()` state = app-lifetime, `component()` state = mount-lifetime — per-visit-reset state belongs in a component (the Timer pattern). Tests: `tests/unit/defaults.test.js` (missing keys set; existing — including falsy and `undefined`-valued — never overwritten; shallow; returns target), exercised end-to-end by the compiler SPA e2e's state-survival spec.
31
- - **Compiler SPA mode — `"spa": true` / `--spa`** (`compiler/src/compiler/spa.rs` (new), `compile.rs`, `watcher.rs`, `config.rs`, `main.rs`, compiler 2.0.3 → 2.1.0) — compiles the SAME MPA `pages/` tree into: **page fragments** at `output/components/vibe-spa/<pages-relative-path>` (body content, head `<style>` tags prepended, page `<script type="module">` carried with ONE rewrite — the vibe import specifier redirected to the defaults entry; body module scripts get the same redirect; `componentsAsIs` composes: `true` keeps child `<component src>` tags runtime-fetched and deduped through the component cache, `false` — the default — inlines children INTO each fragment so every route is one self-contained fetch, with each inlined child carried in the compiled-document form: `ComponentTagger` runs per fragment, so wrappers get deterministic `data-vibe-component-id`s, `this.` bindings and `$.this.` handlers are rewritten to those ids, and child scripts stay `type="vibe-module"`); a **generated route table** (`$param` → `:param`, terminal `$$name` → `:name*`, terminal `index` serves its directory; titles harvested from each page's `<title>`; ordered non-catch-alls-deepest-first-statics-before-params, then catch-alls — the exact-beats-param and everything-beats-catch-all rules a full table needs on top of scanRoutes' deepest-first); and a **composed shell** at `output/index.html` — plain tier-2 runtime-Vibe code (deduped head union where `<meta>` keeps only the set common to every page — page-specific meta dropped with a verbose note — links/scripts stack first-seen; the `/` route's title; body attribute union + `vibe-fouc`, divergences noted in verbose; a generated boot script reusing the pages' own vibe import style; the `<component src="@[page.src]"></component>` outlet). SPA output has **no** `pages/` directory, **no** per-page manifests, and **no** per-page stamping — fragments are runtime-parsed on mount (per-route lazy loading falls out of the bound src for free). The shell still gets a manifest but is deliberately **unstamped**: it is served at every route path while manifests resolve by URL, and stamping would have stripped the raw outlet binding — deep links would have hydrated a dead outlet (stamp-skip is keyed in `generate_file_manifest`). The fragments mirror prunes orphans (deleted pages leave no stale fragment), `components/vibe-spa` in source is reserved (hard error), a source-root `index.html` being shadowed by the shell warns, and an unmount-hygiene warning names any page script that starts side effects (`setInterval`/`setTimeout`/`addEventListener`/`new WebSocket`) without referencing `$.on('unmount'`. Watch mode: any pages-tree change (edit/add/delete, or a component edit whose dependents are pages) re-runs the whole SPA pass — fragments, shell, route table, orphan prune, shell manifest — in ~10ms, with component-cache invalidation shared with the per-page path. Config parsing tolerates the future per-page object form (`"spa": {...}` reads as enabled instead of silently resetting the whole section). Deferred by design until the compiled branch-script double-run is fixed: the inline fragment mode (`componentsAsIs: false` semantics — fragments inlined into the shell behind route conditionals). Tests: `spa.rs` + `config.rs` + `compile.rs` Rust units (route grammar, ordering, import rewrite forms, hygiene, tolerant config, watch-cycle sync incl. orphan prune), `tests/compiler/spa/` (14 bun tests over a mini pages tree: shell composition, head dedup + meta drop, body-attr union, fragment content and rewrites, no pages dir / no page manifests, shell manifest carries the binding, warnings), `tests/e2e-spa-app/` + `tests/e2e/spa-compiled.spec.js` (a real compiled app driven in the browser: root entry, no-match entry leaves the outlet empty with no garbage fetch, claimed navigation, **defaults-merge state survival across navigation**, unmount teardown, params, popstate, scoped catch-all claiming, native fallthrough to a plain MPA document in the same output, shared child components).
32
-
33
- - **Fetched mounts execute inlined `vibe-module` scripts** (`runtime/component.js`) — a freshly fetched component's subtree is the fourth delivery mode after boot, conditional branches, and iteration rows: `processSingle`'s finalize now runs `executeCompiledComponentScriptsIn` on the mounted wrapper, so build-inlined children inside a fetched SPA fragment register their `component({...})` state under their build-tagged ids and the rewritten `_cN` bindings hydrate. A no-op everywhere else — runtime component sources and the compiler's raw components mirror never contain `vibe-module` scripts. Locked in by `tests/compiler/spa-inline-children/` (fragment carries tagged wrapper + vibe-module script + `_cN`-rewritten bindings/handlers, props and slot stamped, no child fetch) and verified in-browser: child state is live and reactive inside a fetched fragment, resets per visit (mount-lifetime), zero child network fetches.
34
-
35
- ### Fixed
36
-
37
- - **An unresolved reactive component src fetched a garbage URL instead of mounting nothing** (`runtime/hydrate.js`, `runtime/component.js`) — with `<component src="@[page.src]">` and `page.src` unset (a no-match deep link: the SPA contract says the outlet stays empty, no built-in 404), hydration stringified the binding to `"undefined"` and the component pass happily fetched it — and where hydration left the raw attribute, the initial component scan fetched the literal `@[page.src]` as a URL. Root fix at both layers: hydration now resolves nullish binding values to empty and, when nothing resolves, moves the authored binding onto `data-vibe-src` — the established transport `parse.js` already reads — and drops the fetchable `src`, so the component scan and the fouc/cleanup gate both treat the outlet as settled rather than pending; `remountComponent`'s pre-fetch early-return (which trusts a pending boot pass to pick the value up) now applies only when the element actually had a `src` attribute for that pass to see — a declaration-form wrapper was invisible to it, and the observer doesn't watch attributes, so hydration owns its first fetch too. Locked in by the compiled SPA e2e: entry at an unrouted URL mounts nothing and the request log contains no `undefined`/`@[` fetches; late resolution (navigating from a no-match entry to a real route) mounts normally.
38
- - **A relative `--cwd` let watch mode write INTO the source tree** (`compiler/src/config.rs`, compiler 2.1.0) — every derived path (source, output, pages) joins from `working_dir`, and filesystem-watch events always carry absolute paths, so a relative `--cwd` made `strip_prefix` fail throughout the watcher; in the incremental manifest pass the un-stripped absolute path then escaped `output_dir.join(...)` entirely (an absolute join replaces the base) and the pass **stamped a source page in place and wrote a manifest beside it**. `Config::load` now canonicalizes `working_dir` up front — one root fix that makes every downstream base absolute. (The repo's own `bun dev` always passed an absolute `$OLDPWD`, which is why this never bit before.)
39
-
40
- ## [2.1.22] - 2026-07-02
41
-
42
- ### Added
43
-
44
- - **`$.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).
45
- - **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).
46
-
47
- ### Fixed
48
-
49
- - **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.
50
- - **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).
51
-
52
- ## [2.1.21] - 2026-07-01
53
-
54
- ### Fixed
55
-
56
- - **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.
57
- - **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`).
58
-
59
- ## [2.1.20] - 2026-06-25
60
-
61
- ### Added
62
-
63
- - **`@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`.
64
-
65
- ## [2.1.19] - 2026-06-25
66
-
67
- ### Fixed
68
-
69
- - **`<!-- 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).
70
-
71
- ## [2.1.18] - 2026-06-22
72
-
73
- ### Fixed
74
-
75
- - **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`.
76
-
77
- ## [2.1.17] - 2026-06-22
78
-
79
- ### Fixed
80
-
81
- - **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>`.
82
-
83
- ## [2.1.16] - 2026-06-22
84
-
85
- ### Fixed
86
-
87
- - **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.
88
-
89
- ## [2.1.15] - 2026-06-22
90
-
91
- ### Fixed
92
-
93
- - **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`.
94
-
95
- ## [2.1.14] - 2026-06-22
96
-
97
- ### Fixed
98
-
99
- - **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`.
100
-
101
- ## [2.1.13] - 2026-06-22
102
-
103
- ### Fixed
104
-
105
- - **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`.
106
- - **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`.
107
- - **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`.
108
-
109
- ## [2.1.12] - 2026-06-22
110
-
111
- ### Fixed
112
-
113
- - **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.
114
-
115
- ## [2.1.11] - 2026-06-22
116
-
117
- ### Fixed
118
-
119
- - **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`.
120
- - **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`.
121
-
122
- ## [2.1.10] - 2026-06-21
123
-
124
- ### Fixed
125
-
126
- - **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`).
127
- - **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`).
128
-
129
- ## [2.1.9] - 2026-06-20
130
-
131
- ### Fixed
132
-
133
- - **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`.
134
- - **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`.
135
-
136
- ## [2.1.8] - 2026-06-19
137
-
138
- ### Changed
139
-
140
- - **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).
141
-
142
- ## [2.1.7] - 2026-06-19
143
-
144
- ### Fixed
145
-
146
- - **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`.
147
-
148
- ## [2.1.6] - 2026-06-19
149
-
150
- ### Added
151
-
152
- - **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).
153
-
154
- ### Fixed
155
-
156
- - **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.
157
- - **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`.
158
-
159
- ## [2.1.5] - 2026-06-19
160
-
161
- ### Fixed
162
-
163
- - **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`.
164
- - **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).
165
- - **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.
166
-
167
- ## [2.1.4] - 2026-06-18
168
-
169
- ### Fixed
170
-
171
- - **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).
172
- - **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).
173
-
174
- ## [2.1.3] - 2026-06-18
175
-
176
- ### Added
177
-
178
- - **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`.
179
- - 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.
180
-
181
- ## [2.1.2] - 2026-06-18
182
-
183
- ### Fixed
184
-
185
- - **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`.
186
-
187
- ## [2.1.1] - 2026-06-18
188
-
189
- ### Added
190
-
191
- - **`[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.
192
-
193
- ## [2.1.0] - 2026-06-17
194
-
195
- ### Added
196
-
197
- - **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:
198
- - **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).
199
- - **Session-lived reuse** — later mounts, including after SPA navigation, resolve from memory with no network request at all.
200
-
201
- 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.
202
- - New config flag `noCache: true` (`vibe(state, { noCache })`) disables it entirely.
203
- - 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.
204
- - Unit coverage: `tests/unit/component-cache.test.js` (coalescing, caching, eviction, `noCache`, non-ok/rejected responses not persisted).
205
-
206
- ## [2.0.5] - 2026-06-16
207
-
208
- ### Fixed
209
-
210
- - **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.
211
-
212
- ## [2.0.4] - 2026-06-12
213
-
214
- Companion-package and website release; no runtime changes.
215
-
216
- ### Fixed
217
-
218
- - **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.
219
- - **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.
220
-
221
- ### Changed
222
-
223
- - 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**.
224
-
225
- ## [2.0.3] - 2026-06-11
226
-
227
- Compiler parity with the 2.0.x runtime: all six findings from
228
- `get-compiler-up-to-date-with-vibe-runtime-2.0.0.md` are fixed and the doc is
229
- retired. Every former parity-gap opt-out in the e2e suite now runs dual-mode
230
- (644 e2e tests, both runtime and compiled). Compiler bumped 1.7.2 → 1.8.0.
231
-
232
- ### Fixed
233
-
234
- - **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.
235
- - Tests: `component-props.spec.js` (all four suites), `components-in-iterations.spec.js` (all five suites) — now `testBothModes`
236
-
237
- - **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.
238
- - Test: `directive-nesting.spec.js` → "Outer-scope reactivity inside `<!-- each -->`" — now `testBothModes`
239
-
240
- - **`@[$.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).
241
- - Test: `hydration-optouts.spec.js` → "Raw HTML rendering via $.unsafe()" — now `testBothModes`
242
-
243
- - **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.
244
-
245
- - **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.
246
-
247
- - **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.
248
- - Tests: `component-state.spec.js` → "Async component() with child prop binding", "component() returns componentId", "Multi-root component scope" — now `testBothModes`
249
-
250
- - **`$.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.
251
-
252
- - **`$` 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.
253
-
254
- - **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.
255
- - Test: `batch-vs-clone-equivalence.spec.js` → dom-prop-selected region (and all other regions still byte-identical)
256
-
257
- ### Changed
258
-
259
- - **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.
260
-
261
- ---
262
-
263
- ## [2.0.2] - 2026-06-11
264
-
265
- ### Fixed
266
-
267
- - **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.
268
- - 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)
269
-
270
- - **`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.
271
- - Test: `tests/e2e/checked-attribute-sync.spec.js` (absent when falsy, empty-present when truthy, removed on truthy→falsy, property always synced)
272
-
273
- ---
274
-
275
- ## [2.0.1] - 2026-06-10
276
-
277
- ### Fixed
278
-
279
- - **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.
280
- - Test: `tests/unit/loop-scope.test.js` (key position, shorthand expansion, computed keys, ternary colons inside object values, arrays, optional chaining/nullish coalescing)
281
-
282
- ---
283
-
284
- ## [2.0.0] - 2026-06-06
285
-
286
- 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.
287
-
288
- ---
289
-
290
- ## [1.9.9] - 2026-06-06
291
-
292
- ### Fixed
293
-
294
- - **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.
295
- - Test: `tests/e2e/iteration-teardown-leak.spec.js` (no retained DOM after GC; repeated fill/empty cycles don't accumulate)
296
-
297
- ### Performance
298
-
299
- - **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.
300
-
301
- ---
302
-
303
- ## [1.9.8] - 2026-06-01
304
-
305
- ### Added
306
-
307
- - **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.**
308
- - 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`.
309
-
310
- ### Fixed
311
-
312
- - **`$` 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.
313
- - 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)
314
-
315
- ---
316
-
317
- ## [1.9.7] - 2026-06-01
318
-
319
- ### Fixed
320
-
321
- - **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:
322
- - **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.
323
- - **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.
324
- - 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)
325
-
326
- - **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.
327
- - Test: `tests/e2e/loop-scoped-handlers.spec.js` (+ unit coverage in `tests/unit/loop-scope.test.js`)
328
-
329
- - **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.
330
- - Test: `tests/e2e/iteration-treeless-unrelated-update.spec.js`
331
-
332
- - **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.
333
-
334
- ### Added
335
-
336
- - **`<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.
337
-
338
- ---
339
-
340
- ## [1.9.6] - 2026-05-25
341
-
342
- ### Fixed
343
-
344
- - **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:
345
- - **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.
346
- - **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).
347
- - 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)
348
-
349
- ---
350
-
351
- ## [1.9.5] - 2026-05-23
352
-
353
- ### Added
354
-
355
- - **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.
356
- - 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).
357
- - 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.
358
- - 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.
359
- - 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.
360
- - 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)
361
-
362
- ---
363
-
364
- ## [1.9.4] - 2026-05-22
365
-
366
- ### Fixed
367
-
368
- - **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.
369
- - 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.
370
- - 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.
371
- - 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)
372
-
373
- ---
374
-
375
- ## [1.9.3] - 2026-05-18
376
-
377
- ### Fixed
378
-
379
- - **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.
380
- - `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.
381
- - 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.
382
-
383
- ---
384
-
385
- ## [1.9.2] - 2026-05-08
386
-
387
- ### Fixed
388
-
389
- - **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:
390
- - **`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`.
391
- - **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.
392
- - **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.
393
- - **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]`.
394
- - 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.
395
- - 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.
396
-
397
- - **`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.
398
-
399
- - **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.
400
-
401
- ---
402
-
403
- ## [1.9.1] - 2026-04-29
404
-
405
- ### Added
406
-
407
- - **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`.
408
- - Test: `tests/e2e/component-in-iteration-rich-props.spec.js`
409
- - **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).
410
- - Test: `tests/e2e/idempotent-hydrate.spec.js`
411
- - **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.
412
- - **`$.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.
413
- - **`./boot` package export** (`package.json`) — `import { boot } from '@ape-egg/vibe/boot'` is now usable from consumers.
414
-
415
- ### Fixed
416
-
417
- - **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`.
418
- - **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]`.
419
-
420
- ---
421
-
422
- ## [1.9.0] - 2026-04-18
423
-
424
- ### Added
425
-
426
- - **`$.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.
427
- - Exposed as `$.reconcile` (non-enumerable so it stays out of state snapshots)
428
- - Test: `tests/e2e/reconcile.spec.js`
429
-
430
- - **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.
431
- - Test: `tests/e2e/component-state-cleanup.spec.js`
432
-
433
- - **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(...)`:
434
- ```html
435
- <!-- each items.filter(x => x.active) as item -->
436
- <!-- each Array.from({length: 10}, (_, i) => i) as n -->
437
- <!-- each window.fights as fight, i -->
438
- ```
439
- - Test: `tests/e2e/each-expression.spec.js`
440
-
441
- - **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.
442
-
443
- - **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.
444
-
445
- - **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.
446
- - Tests: `tests/e2e/prop-in-directive.spec.js`, `tests/e2e/prop-in-expression.spec.js`, `tests/e2e/two-way-binding.spec.js`
447
-
448
- - **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.
449
-
450
- - **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.
451
- - Tests: `tests/e2e/async-component-state.spec.js`, `tests/e2e/parallel-components.spec.js`
452
-
453
- - **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.
454
-
455
- ### Fixed
456
-
457
- - **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.
458
-
459
- - **`<!-- each -->` over `Array.from(...)` and other inline expressions** — previously failed silently because `ITERATION_REGEX` only accepted simple paths. See `ITERATION_REGEX` change above.
460
-
461
- - **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.
462
-
463
- ### Changed
464
-
465
- - **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.
466
-
467
- - **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.
468
-
469
- ### Documentation
470
-
471
- - **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.
472
-
473
- ### Packaging
474
-
475
- - **`@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`.
476
-
477
- ---
478
-
479
- ## [1.8.1] - 2026-04-12
480
-
481
- ### Added
482
-
483
- - **`<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.
484
- - `runtime/component.js` — `<slot></slot>` replaced with `<slot>${children}</slot>`
485
- - `compiler/src/parser/html.rs` — compiler emits the same wrapping
486
- - `vibe.css` — `slot, div.slot { display: contents }` so the wrapper is layout-transparent
487
-
488
- ### Fixed
489
-
490
- - **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.
491
- - `runtime/index.js` — `processMutations` now captures `_vibeSlotContent` for all nested `<component src>` elements before parse/hydrate runs
492
- - Test: `tests/e2e/conditional-in-nested-slot.spec.js`
493
-
494
- - **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.
495
- - `runtime/affected.js` — `matchesKey` now also does word-boundary search so it finds `coins` as an identifier inside the expression
496
- - Test: `tests/e2e/expression-dependency.spec.js`
497
-
498
- - **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.
499
- - `runtime/affected.js` — iteration recursion now builds plain-object snapshots from previousState/currentState merged with scopedState's local vars
500
-
501
- - **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.
502
- - `runtime/affected.js` — name binding dependency check now matches state keys case-insensitively, mirroring `hydrate.js`'s existing case-insensitive evaluation fallback
503
-
504
- ### Documentation
505
-
506
- - `README.md` — "Future improvements to refactor" section documenting string-based dependency tracking limitations and HTML lowercase attribute handling
507
-
508
- ---
509
-
510
- ## [1.8.0] - 2026-04-10
511
-
512
- ### Added
513
-
514
- - **State batching** — Multiple `$.prop = value` assignments in the same microtask now produce a single rerender instead of cascading updates
515
- - Proxy `set` trap calls `scheduleFlush()` (via `queueMicrotask`) instead of `rerender()` directly
516
- - Eliminates cascading rerenders from `afterUpdate` hooks setting state
517
- - Rerender callback now does full diff of `previousState` vs `currentState` (no longer receives individual changed prop)
518
-
519
- - **DOM ownership tracking** — Two new data structures prevent re-processing of already-managed nodes
520
- - `managedNodes` (`WeakSet`): Tracks nodes inserted by `mountBranch` / `renderIteration`. `shouldProcessNode` skips them.
521
- - `branchNodeRegistry` (`WeakMap`): Maps DOM nodes to their conditional branch, so `processComponent`'s `el.replaceWith()` correctly updates conditional tracking
522
-
523
- - **Parallel component loading** — Same-level `<component src>` elements now fetch in parallel
524
- - `isNestedInUnprocessedComponent()` filters slot content from premature processing
525
- - `processSingle()` handles fetch + script execution + DOM replacement per component
526
- - `processComponent()` fires all top-level fetches simultaneously; nested components discovered after parent finalizes
527
-
528
- - **Component script imports** — `<script type="module">` in components now supports `import` statements
529
- - Static imports rewritten to dynamic `await import()` (default, named, namespace, side-effect)
530
- - `import component from '...'` is stripped (Vibe injects it as a parameter)
531
- - `AsyncFunction` constructor used only when imports exist; sync path preserved for boot timing
532
-
533
- - **Bracket notation in paths** — `resolvePath` now handles `teams[0].combatants` via `path.match(/[^.\[\]]+/g)` splitting
534
-
535
- - **Quoted strings in binding expressions** — `BINDING_REGEX` now supports `@[x.replace('.png', '-mugshot.png')]`
536
-
537
- ### Changed
538
-
539
- - **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.
540
-
541
- - **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.
542
-
543
- - **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.
544
-
545
- ### Performance
546
-
547
- - **`evalInScope` function cache** — Compiled `new Function()` objects cached by expression + state keys signature. For 1000 rows × 4 bindings, reduces 4000 Function compilations to ~4.
548
- - **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.
549
- - **DocumentFragment batching** — `renderIteration` collects all cloned nodes in a DocumentFragment, single `insertBefore` at the end instead of N × M individual DOM mutations.
550
- - **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.
551
- - **`extractPlainValue` optimization** — Indexed `for` loops, `Object.keys()` instead of `for..in`, inline primitive check, pre-allocated arrays.
552
- - **Old-array ground truth** — `updateIteration` uses rendered instances as truth when `oldState` disagrees with actual instance count, preventing mismatched diff operations.
553
-
554
- ### Removed
555
-
556
- - **`window.__VIBE_FAST_ITERATION__` experimental flag** — Replaced by automatic bulk replacement detection (no opt-in needed)
557
- - **Per-prop rerender** — State proxy no longer passes `{ [changedProp]: ... }` to rerender; full diff via batching replaces it
558
-
559
- ---
560
-
561
- ## [1.7.2] - 2026-02-27
562
-
563
- ### Fixed
564
-
565
- - **Component inlining — hyphenated custom elements inside slot content** (`find_matching_close`)
566
- - `<component-card>`, `<component-grid>`, etc. inside slot content incorrectly matched `<component\b`, incrementing the depth counter without a corresponding close
567
- - 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)
568
- - Fixed by replacing `\b` with `(?:\s|>|/>)` — only matches actual `<component` tags followed by whitespace or `>`, not hyphenated variants
569
-
570
- ---
571
-
572
- ## [1.7.1] - 2026-02-27
573
-
574
- ### Added
575
-
576
- - **Linux x64 binary** (`vibe-compiler-linux-x64`) — cross-compiled from macOS ARM using `musl-cross` and the `x86_64-unknown-linux-musl` Rust target
577
- - Enables the compiler to run on Linux environments (e.g. Vercel, Docker, CI) without a local Rust toolchain
578
- - New `compiler:build:linux` npm script builds the musl binary
579
- - `publish:vibe:prepare` now builds both macOS ARM and Linux x64 binaries before publishing
580
-
581
- ### Changed
582
-
583
- - **`reqwest` TLS backend** — switched from `native-tls` to `rustls-tls` with `default-features = false`
584
- - Eliminates the OpenSSL dependency that prevented cross-compilation to `x86_64-unknown-linux-musl`
585
- - Pure-Rust TLS implementation; no system libraries required
586
-
587
- ---
588
-
589
- ## [1.7.0] - 2026-02-19
590
-
591
- ### Fixed
592
-
593
- - **Watch mode stale component cache** - Incremental recompilation no longer uses stale component content
594
- - Component cache is cleared before every recompile trigger, ensuring components are always re-read from disk
595
- - Component cache and parser element cache are also invalidated immediately when a component file changes
596
- - Fixes corrupted output (old event handlers, wrong attributes) that appeared on every watch-mode save after the first
597
-
598
- - **Custom element transform — nested hyphenated tags** (`elementsAsIs: false`)
599
- - `<accordion-content>` inside `<accordion>` previously produced `<div class="accordion" -content>` due to the `<accordion>` regex partially matching the longer tag name
600
- - Fixed by requiring whitespace or end-of-tag immediately after the tag name in the opening tag regex
601
- - Tags are now also sorted by descending length so more specific names (e.g. `accordion-content`) are always processed before their prefixes (`accordion`)
602
-
603
- - **Custom element transform — existing class preserved** (`elementsAsIs: false`)
604
- - `<crow class="i-should-preserve">` previously compiled to `<div class="crow">`, discarding the original class
605
- - Existing `class="..."` is now merged: result is `<div class="crow i-should-preserve">`
606
-
607
- - **`elementsAsIs` config respected in dependency scanning**
608
- - `fetch_components_for_files` and `fetch_all_components` were hardcoded to `elements_as_is: false`, ignoring the project config
609
- - Both functions now correctly read `self.config.elements_as_is` and `self.config.reserved_elements`
610
-
611
- - **`vibe-dehydrate` protected during manifest stamping**
612
- - Content inside `<template vibe-dehydrate>` was incorrectly processed during value stamping
613
- - Dehydrated regions are now extracted before processing and restored afterwards
614
-
615
- - **Conditional evaluation errors handled gracefully**
616
- - A failed `<!-- if ... -->` expression (undefined variable, syntax error) now defaults to `false` instead of crashing
617
-
618
- ### Changed
619
-
620
- - **Lifecycle event API** — `vibe()` now returns an object that supports `.on(event, callback)` before boot completes
621
- - `$.on('ready', cb)` — fires once after all components load and initial processing completes
622
- - `$.on('afterUpdate', cb)` — fires on every state change
623
- - `$.on('afterDomMutation', cb)` — fires after every DOM mutation batch
624
- - Listeners registered before boot are queued and replayed once the runtime is ready
625
-
626
- - **Component `onComplete` timing** — deferred via `queueMicrotask` to give user code a chance to register listeners before the ready callback fires
627
-
628
- - **Iteration performance** — `cloneTreeNode` avoids spread operator; `nameBindings` now correctly carried through cloned iteration trees
629
-
630
- ---
631
-
632
- ## [1.6.1] - 2026-02-12
633
-
634
- ### Fixed
635
-
636
- - **Compiler output directory handling** - Fixed duplicate file generation in output
637
- - Compiler now properly handles source and output paths without preserving source directory structure
638
- - Files from `./src` are now correctly written to root of output directory (e.g., `compiled/index.html`) instead of `compiled/src/index.html`
639
- - Canonicalized source path for reliable comparison and path stripping
640
- - Updated all processing functions to use canonical source path for relative path calculation
641
-
642
- - **Manifest detection improvements** - Better handling of directory URLs and extensionless paths
643
- - Directory URLs now normalized: `/compiled/` → `/compiled/index.html`
644
- - Extensionless URLs now normalized: `/compiled/mypage` → `/compiled/mypage.html`
645
- - Fixes manifest detection when visiting pages without explicit `.html` extension
646
- - Enables proper hyperspeed loading for directory index pages
647
-
648
- ---
649
-
650
- ## [1.6.0] - 2026-02-12
651
-
652
- ### Added
653
-
654
- - **Watch mode** - Compiler now supports file watching with incremental compilation
655
- - New `--watch` flag monitors source files for changes and automatically recompiles
656
- - Intelligent change detection tracks affected files and their dependencies
657
- - Transitive dependency tracking: changes to components trigger recompilation of pages using them
658
- - Blacklist support: watch mode respects SKIP_FILES patterns (tests/, node_modules/, etc.)
659
- - Debounced file events (300ms) prevent excessive compilation during rapid changes
660
- - Outputs only changed files for fast incremental builds
661
- - First compile shows full output, subsequent compiles show only deltas
662
- - **Reserved element validation** - Compiler now prevents component naming conflicts
663
- - New `reservedElements` config option (replaces `excludeTags`)
664
- - Defaults to all HTML5 elements plus "component" keyword
665
- - User-provided values append to defaults (not replace)
666
- - Case-sensitive validation: `nav.html` → Error, `Nav.html` → OK
667
- - Compile-time error with clear message showing conflicting filename
668
- - Prevents runtime confusion between HTML elements and custom components
669
- - **Component path case-sensitivity** - Component paths are now treated as case-sensitive
670
- - `<component src="path/to/MyComponent.html">` and `<component src="path/to/mycomponent.html">` are different
671
- - Both runtime and compiler preserve exact case in paths
672
- - Cache keys include full case-sensitive path
673
- - Enables PascalCase naming convention for components while supporting any casing
674
-
675
- ### Changed
676
-
677
- - **Component inlining architecture** - Complete rewrite from iterative to recursive fetching
678
- - Removed MAX_ITERATIONS constant and loop-based approach
679
- - `fetch_component_recursive()` now returns fully resolved content with all nested components inlined
680
- - Components are cached only after being fully resolved (prevents incomplete content in cache)
681
- - `inline_component_elements()` now does single pass (no loops)
682
- - Significant performance improvement: sub-100ms compilation for complex nested components
683
- - **Framework element handling** - `<component>` is now recognized as a framework element
684
- - Never transformed to `<div>` regardless of `--elements-as-is` flag
685
- - Custom elements (e.g., `<card>`, `<text>`) are transformed when `elements_as_is: false`
686
- - Framework elements vs custom elements properly distinguished in compiler
687
- - Wrapper `<component>` tags remain after inlining (expected by runtime)
688
- - **Component inlining behavior** - Custom elements are always inlined
689
- - `<card>` → transformed to `<component>` → inlined even with `--components-as-is`
690
- - `--components-as-is` only affects explicit `<component src="...">` tags
691
- - Consistent behavior: custom elements compile away, framework elements remain
692
- - **Path normalization** - Component paths now consistently normalized
693
- - All paths start with `/` (unless external URL)
694
- - Cache uses normalized paths to prevent duplicates (`/components/nav.html` vs `./components/nav.html`)
695
- - Fixes cache pollution from different path formats for same component
696
- - **Config naming** - `excludeTags` renamed to `reservedElements` throughout codebase
697
- - Better describes purpose (reserved from use as component names)
698
- - Updated in Rust compiler, config files, and documentation
699
- - Backward compatible: old config key still works but deprecated
700
-
701
- ### Fixed
702
-
703
- - Watch mode was compiling blacklisted files (tests/, node_modules/)
704
- - Component inlining created infinite nested wrappers (100+ levels)
705
- - Empty headlines in tests due to broken external component fetching
706
- - Path case normalization could cause duplicates in component cache
707
- - Accessibility transformation incorrectly converting `<component>` to divs
708
- - Test expectations mismatched actual framework element behavior
709
-
710
- ### Performance
711
-
712
- - Watch mode incremental compilation: ~100ms for typical changes
713
- - First full compilation: ~300-500ms
714
- - Component fetching: recursive approach 10x faster than iterative (no MAX_ITERATIONS overhead)
715
- - Path normalization prevents redundant fetches of same component
716
-
717
- ---
718
-
719
- ## [1.5.0] - 2026-02-09
720
-
721
- ### Added
722
-
723
- - **Nested iteration compilation** - Compiler now handles infinitely nested `<!-- each -->` blocks
724
- - Recursive processing in `iteration_optimizer.rs` with depth counting for comment pair matching
725
- - Nested loops compile to IIFEs (Immediately Invoked Function Expressions) with template literals
726
- - Inner iterations inlined directly into outer batch functions for optimal performance
727
- - Example: `<!-- each categories as cat --><!-- each cat.items as item -->` compiles to single optimized function
728
- - **QuickJS JavaScript runtime** - Full expression evaluation at compile time
729
- - Embedded QuickJS engine (`rquickjs = "0.6"`) for JavaScript evaluation in Rust
730
- - No external dependencies - increases binary size by ~1-2MB
731
- - Evaluates any JavaScript expression: `@[categories.length]`, `@[items[0]]`, `@[user.name.toUpperCase()]`
732
- - State set in global scope: `Object.assign(globalThis, $)` matches Vibe runtime behavior
733
- - Fast evaluation: ~160ms overhead for 35 files (~5ms per file)
734
- - **Complete pre-rendering** - Zero FOUC with all bindings pre-rendered for SEO
735
- - All `@[expression]` bindings evaluated and stamped into HTML at compile time
736
- - Handles property access (`@[user.name]`), array methods (`@[categories.length]`), and complex expressions
737
- - Nested iterations fully pre-rendered with merged state for each iteration context
738
- - Falls back gracefully: undefined expressions left as `@[...]` for runtime hydration
739
- - New test: `stamp_array_length` validates `.length` property evaluation
740
-
741
- ### Changed
742
-
743
- - **Value stamper rewrite** (`value_stamper.rs`) - Complete overhaul to use QuickJS
744
- - Replaced JSON path resolution with JavaScript expression evaluation
745
- - `eval_expression()` handles any valid JavaScript with state in scope
746
- - `eval_array_path()` evaluates array paths for iteration rendering
747
- - Iteration rendering creates merged state (parent + item + index) for nested context
748
- - Removed manual property traversal code - JavaScript engine handles it all
749
- - **Compiled iteration updates** - Always use compiled path when available (`iterate.js:364`)
750
- - Previously only used compiled updates for edge cases (empty↔full, large arrays >100)
751
- - Now uses compiled batch functions for ALL updates when manifest has `compiled.iterations.batchFn`
752
- - Fixes issue where small array updates (3→4 items) fell through to incompatible runtime path
753
- - Ensures consistent performance regardless of array size or transition type
754
- - **State extraction improvements** (`state_extractor.rs`) - Better error handling
755
- - Silently skips unparseable state objects instead of failing compilation
756
- - Enables graceful degradation when state contains functions or complex expressions
757
-
758
- ### Fixed
759
-
760
- - Compiled iterations not updating when array size changes (e.g., add/remove items)
761
- - Pre-rendering skipped for JavaScript expressions like `@[categories.length]`
762
- - Nested iteration values showing as `@[item]` instead of actual data
763
- - Runtime path attempting to handle compiled iterations incorrectly
764
-
765
- ### Performance
766
-
767
- - Pre-rendering adds ~160ms to compilation for 35 files (~435ms total, up from ~310ms)
768
- - QuickJS evaluation: ~1-5ms per binding
769
- - Compiled nested iterations: Same performance as shallow iterations (no recursion overhead at runtime)
770
- - Zero runtime cost for pre-rendered bindings - HTML arrives with values already stamped
771
-
772
- ---
773
-
774
- ## [1.4.0] - 2026-02-09
775
-
776
- ### Added
777
-
778
- - **Iteration optimization** - Compiler now generates optimized batch functions for `<!-- each -->` loops
779
- - New `iteration_optimizer.rs` module generates string-based batch render functions
780
- - Provides 2-3x performance improvement for iteration rendering (15-17ms vs 40ms for 1000 rows)
781
- - Only applies to shallow iterations (nested iterations still use runtime path)
782
- - Compiled batch functions are stored in manifest and executed at runtime
783
- - **Pre-compiled iterations runtime** (`runtime/pre-compiled-iterations.js`)
784
- - Production implementation of compiled iteration rendering
785
- - Uses pre-compiled batch functions from manifest generated at build time
786
- - Based on the prototype in `_vibe-compiled-iteration-batch.js`
787
- - Automatically falls back to runtime rendering for nested iterations or missing batch functions
788
- - **Compiler configuration** - New `iterationsAsIs` flag
789
- - Set to `true` in `vibe-compiler` config to skip iteration optimization
790
- - Iterations pass through unchanged and are handled entirely by runtime
791
- - Default: `false` (iterations are optimized)
792
- - **Hyperspeed benchmark** - New benchmark page for measuring pre-compiled performance
793
- - Tests iteration optimization with 1000 rows
794
- - Compares runtime vs compiled iteration rendering
795
- - Available at `e2e-runtime/hyperspeed-benchmark.html`
796
- - **Compiler test coverage** for iteration compilation
797
- - `tests/compiler/iterations/` - Tests iteration optimization is applied
798
- - `tests/compiler/iterations-as-is/` - Tests `iterationsAsIs` flag skips optimization
799
- - E2E tests verify compiled iterations render correctly
800
-
801
- ### Changed
802
-
803
- - **Renamed `runtime/hyperspeed.js` → `runtime/pre-compiled-manifest.js`**
804
- - Better naming to reflect that it handles all pre-compiled features, not just "hyperspeed"
805
- - Updated all imports and references throughout codebase
806
- - **Enhanced manifest merging** - Compiler manifest now preserves compiled iteration data
807
- - Iteration nodes retain `compiled.iterations.batchFn` from manifest during runtime merge
808
- - Prevents compiled data from being discarded when merging with runtime tree
809
- - **Debug logging improvements**
810
- - Added manifest filename to debug output ("Loaded index.manifest.js, page is pre-compiled")
811
- - Shows compiled feature count (e.g., "3 iterations optimized")
812
- - Better visibility into which optimizations are active
813
- - **Runtime iteration handling** - Iterations check for compiled batch functions before falling back
814
- - `canUseCompiled()` determines if iteration can use batch function
815
- - Falls back to runtime rendering for nested structures or missing compiled data
816
- - Seamless integration between compiled and runtime paths
817
-
818
- ### Fixed
819
-
820
- - Runtime cleanup now properly handles compiled iteration nodes
821
- - Manifest merge no longer discards compiled data from iteration nodes
822
- - Debug logging visibility flag now correctly propagates through manifest loading
823
- - Iteration restoration respects compiled batch functions
824
-
825
- ---
826
-
827
- ## [1.3.2] - 2025-02-06
828
-
829
- ### Fixed
830
-
831
- - **Package completeness**: Added missing `boot.js` to npm package
832
- - File was missing from the "files" allowlist in package.json
833
-
834
- ### Changed
835
-
836
- - **Package strategy**: Switched from allowlist to denylist approach
837
- - Removed "files" field from package.json
838
- - Added `.npmignore` to exclude build artifacts (`target/`) and dev files
839
- - Ensures all source files are included without manual maintenance
840
-
841
- ---
842
-
843
- ## [1.3.1] - 2025-02-06
844
-
845
- ### Fixed
846
-
847
- - **Package exports**: Added missing `./component` export to package.json
848
- - Enables proper import: `import component from '@ape-egg/vibe/component'`
849
- - Previously `component.js` was included in package files but not exposed via exports field
850
-
851
- ---
852
-
853
- ## [1.3.0] - 2025-02-06
854
-
855
- ### Added
856
-
857
- - **Static Analysis Compiler**: Replaced browser-based manifest generation with pure Rust static analysis
858
- - New compiler modules: `state_extractor.rs`, `manifest_builder.rs`, `value_stamper.rs`
859
- - ~3000x performance improvement (10-100 seconds → ~6ms for manifest generation)
860
- - Pre-rendering support for iterations with initial state values
861
- - Graceful handling of unparseable state (skips files instead of failing entire compilation)
862
-
863
- - **Enhanced vibe() and component() API**:
864
- - Added `config` parameter (second argument) for runtime configuration
865
- - Added `targetSelector` parameter (third argument) for custom root element selection
866
- - Multiple calls accumulate state, config/targetSelector use "first wins" strategy
867
- - Example: `vibe({ count: 0 }, { debug: true }, 'body')`
868
-
869
- - **Expanded Compilation Scope**: Compiler now processes all HTML files recursively
870
- - Compiles all `<source-root>/**/*.html` (excluding `components/` directory)
871
- - Generates manifests for all `<output-dir>/**/*.html` (excluding `components/`)
872
- - Previously limited to `<source-root>/pages/` only
873
-
874
- ### Changed
875
-
876
- - **Iteration Restoration**: Improved DOM restoration algorithm
877
- - Uses TreeWalker to find iteration comment pairs (more robust)
878
- - Avoids index-based lookup that breaks after DOM structure changes
879
- - Runtime now fully controls iteration nodes (skipped during manifest merge)
880
-
881
- - **Compiler Output**: Enhanced user experience with better formatting
882
- - Manifest generation occurs before "Compilation successful!" message
883
- - Verbose mode (`--verbose`) shows only warnings/errors for manifest generation
884
- - Clean title: "Generating manifests (static analysis)"
885
- - Summary format: `Generated manifests (X files, Y skipped) in Zms`
886
- - Positioned between "Compiled HTML" and "Copied files" in output
887
-
888
- - **Debug Logging**: Refined hyperspeed detection messages
889
- - Removed redundant path-specific log
890
- - Changed to: "Detected vibe-hyperspeed. Applying pre-compiled manifest."
891
-
892
- ### Removed
893
-
894
- - **Browser Automation Dependencies**: Eliminated heavy runtime dependencies
895
- - Removed: chromiumoxide, tiny_http, tokio, futures
896
- - Deleted 414 lines of browser automation code (`manifest.rs`)
897
- - Pure Rust implementation with no external processes or async complexity
898
-
899
- ### Fixed
900
-
901
- - Manifest merge conflicts between hyperspeed (pre-compiled) and runtime trees
902
- - Index shifting bugs during restoration phase that caused key mismatches
903
- - Iteration rendering producing duplicate items (3x3 instead of 3)
904
- - Pre-rendered iteration content not being properly replaced with reactive templates
905
-
906
- ### Performance
907
-
908
- - Manifest generation: 10-100 seconds → ~6ms (~3000x faster)
909
- - Total compilation time: Typically completes in 200-300ms for medium projects
910
- - Zero browser startup overhead
911
- - Reduced memory footprint (no Chromium instance)
912
-
913
- ---
914
-
915
- ## [1.2.0] - 2026-02-03
916
-
917
- ### Added
918
-
919
- - **Component state isolation**: Components now have their own isolated state using `<script type="component">` blocks
920
- - Each component gets a unique ID (`_cTIMESTAMP_RANDOM`) automatically generated and stored in `data-vibe-component-id` attribute
921
- - Component state lives at `$[componentId].property` in the global state object
922
- - Variable declarations (`let count = 0`) are automatically transformed to property assignments (`this.count = 0`)
923
- - Clean separation between global state and component-specific state
924
- - Example component (subject to change!):
925
- ```html
926
- <script type="component">
927
- let count = 0;
928
- let increment = () => { this.count++; };
929
- </script>
930
- <button onclick="this.increment()">Clicked @[this.count] times</button>
931
- ```
932
- - **`this.property` syntax**: Reference component-scoped state from anywhere inside a component
933
- - Works in bindings: `@[this.count]`
934
- - Works in attributes: `value="@[this.inputValue]"`
935
- - Works in event handlers: `onclick="this.increment()"`
936
- - Works in name bindings: `@[this.iconName]`
937
- - Works in conditionals: `<!-- if this.isVisible -->`
938
- - Works in iterations: `<!-- each this.items as item -->`
939
- - Runtime automatically resolves `this.property` → `$['componentId'].property`
940
- - **Component script execution**: `<script type="component">` blocks execute in their own scope
941
- - Scripts run when component HTML is fetched (runtime `<component>` resolution)
942
- - State is registered in global `$` object under component ID
943
- - All siblings after the script tag inherit the component ID via `data-vibe-component-id` attribute
944
- - Multiple component scripts in same HTML create separate component instances with unique IDs
945
- - **Props and slots integration**: Component state works seamlessly with existing `<component>` features
946
- - Props can set component state: `<component src="/card.html" theme="@[userTheme]">`
947
- - Props work with `this.` references: `@[this.theme]` inside card.html
948
- - Slots work inside component-scoped HTML
949
- - Components can be nested with isolated state at each level
950
-
951
- ### Changed
952
-
953
- - **Component ID tagging**: DOM elements are now tagged with `data-vibe-component-id` during component processing (previously only scripts had this attribute)
954
- - All siblings after a `<script type="component">` get tagged with the same component ID
955
- - Tagging stops when hitting another component script or end of HTML
956
- - Enables `this.property` resolution in any context (bindings, events, conditionals, iterations)
957
- - **Event handler rewriting**: Event handlers with `this.property` are now rewritten at parse time
958
- - `onclick="this.increment()"` → `onclick="$['_c123_abc'].increment()"`
959
- - DOM properties (like `this.value`, `this.checked`) are preserved and not rewritten
960
- - Prevents conflicts between component state access and native DOM properties
961
-
962
- ### Technical Details
963
-
964
- - **Component state lifecycle**:
965
- 1. `<component src="/path.html">` fetches HTML
966
- 2. HTML is parsed in temporary container
967
- 3. `<script type="component">` blocks are found and executed
968
- 4. Each script generates unique component ID
969
- 5. Script and following siblings are tagged with `data-vibe-component-id`
970
- 6. Component state is registered at `$[componentId]`
971
- 7. Props are applied (with `this.` reference rewriting)
972
- 8. Slots are replaced
973
- 9. Transformed HTML replaces `<component>` element
974
- 10. MutationObserver triggers reactive hydration with component context
975
- - **`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`
976
- - **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
977
-
978
- ---
979
-
980
- ## [1.1.3] - 2026-02-02
981
-
982
- ### Added
983
-
984
- - **MutationObserver performance optimization**: Fast filter with short-circuit evaluation
985
- - New `shouldProcessNode()` function checks for Vibe syntax before expensive processing
986
- - Short-circuits on first match: most Vibe nodes contain `@[`, so check exits immediately
987
- - Filters out third-party framework mutations (React, Vue, etc.) with cheap string operations
988
- - Only walks DOM tree for nodes that actually contain Vibe syntax (`@[`, `<!-- each`, `<!-- if`, `<component>`)
989
- - Enables efficient coexistence with other frameworks on the same page
990
-
991
- ---
992
-
993
- ## [1.1.2] - 2026-02-01
994
-
995
- ### Fixed
996
-
997
- - **Compiler hotfix**: Empty custom element tags no longer cause HTML content to be cut out during compilation
998
- - 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
999
- - Solution: All custom element tags now compile to properly closed `<component>...</component>` tags, even when empty
1000
- - Prevents regex from matching across multiple components
1001
-
1002
- ---
1003
-
1004
- ## [1.1.1] - 2026-02-01
1005
-
1006
- ### Added
1007
-
1008
- - **Compiler CLI**: Modern command structure with subcommand support
1009
- - New bin command: `vibe`
1010
- - Subcommand syntax: `bunx vibe compile [options]` or shorthand `bunx vibe c [options]`
1011
- - Accessible directly via `bunx` without manual npm scripts
1012
- - Automatic detection of unknown subcommands with helpful error messages
1013
- - Backward compatibility: works with or without `compile` subcommand
1014
- - **Validation time tracking**: Compiler now reports component validation phase timing separately
1015
- - Shows time spent fetching external components
1016
- - Helps explain total compilation time vs individual phase times
1017
- - Displayed as: `* Validated components in XXms`
1018
- - **Component tree output**: Improved verbose logging for component compilation
1019
- - Tree structure with nested dependencies (e.g., nav.html → menu.html)
1020
- - Usage counts for each unique component
1021
- - Color-coded: internal (green) vs external (magenta)
1022
- - Summary format: `Compiled components (X internal, Y external)`
1023
- - Clean hierarchical display with `└─` for children
1024
- - **Compiler documentation**: Updated READMEs with new CLI commands
1025
- - `/vibe/README.md` and `/vibe/compiler/README.md` now show `bunx vibe compile` syntax
1026
- - Added npm scripts examples for optional shortcuts
1027
- - Removed outdated `bun vibe:compile` references
1028
-
1029
- ### Changed
1030
-
1031
- - **Compiler output formatting**: Consistent spacing between major sections (Vibe Compiler, Compiler config, Compiled components, etc.)
1032
- - **Verbose logging**: Removed redundant component loading list, streamlined validation output
1033
-
1034
- ---
1035
-
1036
- ## [1.0.7] - 2026-01-30
1037
-
1038
- ### Added
1039
-
1040
- - **`<component>` element**: Runtime implementation of reusable HTML components
1041
- - Fetch HTML from local or remote sources: `<component src="/components/card.html">`
1042
- - Props system: Pass reactive bindings `headline="@[title]"` or static values `theme="dark"`
1043
- - Children via `<slot>`: Content between tags replaces `<slot></slot>` in fetched HTML
1044
- - Recursive resolution: Fetched HTML can contain more `<component>` elements
1045
- - Runtime-first with future compiler optimization planned (Phase 5)
1046
- - **Deep reactivity**: Nested property mutations now automatically trigger updates
1047
- - Recursive proxy implementation wraps all nested objects and arrays
1048
- - `$.obj.nested.prop = x` now triggers reactivity (previously required `$.obj = { ...$.obj, nested: { ...$.obj.nested, prop: x } }`)
1049
- - WeakMap cache prevents double-wrapping of proxies
1050
- - Tracks root property for each mutation to trigger correct affected element checks
1051
- - Same approach as Vue 3 - tiny performance cost for massive DX improvement
1052
- - **Scoped state for iterations**: Iteration instances now preserve local context for reactive updates
1053
- - Each `<!-- each items as item -->` instance stores its own `scopedState` (Proxy wrapper with local variables)
1054
- - When global state changes (e.g., `tutorialProgress`), affected elements inside iterations can access both local (`item`) and global variables
1055
- - Enables reactive attributes like `completed="@[tutorialProgress[item.id]]"` to update correctly without full re-render
1056
- - Scoped state flows through `affected.js` → `hydrate.js` pipeline for proper evaluation context
1057
- - **Compiler (first pass)**: Rust-based optional build tool
1058
- - Asset handling with whitelist approach (fonts, images, media, docs automatically copied)
1059
- - HTML/CSS/JS processing
1060
- - Configuration via `package.json` (`vibe-compiler` section)
1061
- - Node.js wrapper for cross-platform execution
1062
- - Foundation for future optimizations (component inlining, iteration optimization, two-way binding sugar)
1063
- - **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
1064
- - **NPM scripts**: Added `bun unit` and `bun unit:watch` commands for running unit tests
1065
- - **Name bindings**: Bindings in attribute name position now supported
1066
- - Syntax: `<icon @[section.icon]></icon>` evaluates expression and sets it as attribute name
1067
- - HTML lowercases attribute names, so case-insensitive property lookup is used for simple properties
1068
- - Old attributes are automatically cleaned up when the bound value changes
1069
- - Useful for dynamic attribute names like icon libraries, custom elements, or conditional attributes
1070
- - Works in all contexts: root level, iterations, conditionals, and inside `<component>` elements
1071
-
1072
- ### Fixed
1073
-
1074
- - **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
1075
- - **Nested iterations and conditionals**: Fixed issue where nested `<!-- each -->` and `<!-- if -->` blocks inside `<component>` elements would render then immediately disappear
1076
- - Root cause: MutationObserver re-parsing created fresh runtime state, causing iterations to think they needed re-rendering
1077
- - Solution: Added `__vibeRendered` marker on comment nodes that persists across re-parses
1078
- - Now properly handles complex nested structures like menu systems with dynamic sections and items
1079
- - **Conditionals wrapping iteration content**: Fixed conditionals in attribute name position (e.g., `<!-- if condition -->` wrapping `<nav-section>`) not being detected inside iteration templates
1080
- - Root cause: `initializeBlock()` only parsed the first element node, missing comment nodes like `<!-- if -->`
1081
- - Solution: Parse all template nodes in a container to capture comment nodes, ensuring tree element references match actual DOM nodes
1082
- - Enables patterns like conditional menu items: `<!-- each items as item --><!-- if condition --><item>...</item><!-- /if --><!-- /each -->`
1083
- - **StructuredClone with proxies**: Replaced `structuredClone()` with custom `deepClone()` that properly unwraps proxy objects for state snapshots in hooks
1084
-
1085
- ---
1086
-
1087
- ## [1.0.6] - 2026-01-29
1088
-
1089
- ### Added
1090
-
1091
- - **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)
1092
- - **Debug helper**: Centralized `debugLog()` function with phase-specific color coding and tree-style indentation for nested operations
1093
-
1094
- ### Fixed
1095
-
1096
- - **Nested brackets**: `BINDING_REGEX` and `PURE_BINDING_REGEX` now correctly parse bindings with one level of nested brackets (e.g., `@[items[0]]`, `@[obj[key]]`)
1097
-
1098
- ### Changed
1099
-
1100
- - **Internal naming**: Renamed `link.js` → `manifest.js` and `linkList` → `manifest` throughout codebase to better reflect its purpose as a DOM element registry/manifest
1101
- - **Phase constants**: Added `PHASE_*` constants for all lifecycle phases to ensure consistent naming across debug logs
1102
-
1103
- ---
1104
-
1105
- ## [1.0.5] - 2026-01-25
1106
-
1107
- ### Fixed
1108
-
1109
- - **Expression evaluation**: Normalize whitespace in `@[...]` expressions before evaluation, making bindings resilient to IDE auto-formatting that may break expressions across multiple lines
1110
-
1111
- ### Changed
1112
-
1113
- - **DRY refactor**: Consolidated expression evaluation into single `evalInScope()` function in utils.js, used by hydrate.js, conditionals.js, and affected.js
1114
-
1115
- ---
1116
-
1117
- ## [1.0.4] - 2026-01-24
1118
-
1119
- ### Fixed
1120
-
1121
- - **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
1122
-
1123
- ---
1124
-
1125
- ## [1.0.3] - 2026-01-23
1126
-
1127
- ### Added
1128
-
1129
- - **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
1130
-
1131
- ### Fixed
1132
-
1133
- - **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)
1134
-
1135
- ---
1136
-
1137
- ## [1.0.2] - 2026-01-20
1138
-
1139
- ### Fixed
1140
-
1141
- - **parse.js**: Root element attribute bindings now captured when parsing individual nodes (fixes dynamically added elements with `value="@[...]"` not being hydrated)
1142
- - **MutationObserver**: Added nodes now only hydrate themselves and descendants, not the entire tree (fixes focus loss and mutation cascade on dynamic DOM updates)
1143
- - **MutationObserver**: Removed unnecessary hydration calls when nodes are removed (cleanup only)
1144
- - **MutationObserver**: Batched hook firing - `afterDomMutation` now fires once after all mutations are processed, not per-node (major performance fix for bulk DOM updates)
1145
- - **MutationObserver**: Parent elements are now re-parsed only once per mutation batch, not per-child (performance optimization)
1146
-
1147
- ---
1148
-
1149
- ## [1.0.1] - 2026-01-20
1150
-
1151
- ### Fixed
1152
-
1153
- - Fixed iteration diff bug where MOVE operations corrupted indices, causing wrong items to be deleted
1154
-
1155
- ---
1156
-
1157
- ## [1.0.0] - 2026-01-19
1158
-
1159
- ### Added
1160
-
1161
- - Initial public release
1162
- - Proxy-based reactive state (`window.$`)
1163
- - MutationObserver for automatic DOM tracking
1164
- - Fine-grained updates (no virtual DOM)
1165
-
1166
- ### Core Features
1167
-
1168
- - **Reactive bindings**: `@[property]` syntax in HTML and CSS
1169
- - **Iteration**: `<!-- each items as item, i -->` with efficient diffing
1170
- - **Nested iteration**: `<!-- each category.items as item -->`
1171
- - **Conditionals**: `<!-- if condition -->...<!-- else -->...<!-- /if -->`
1172
- - **Dehydrate**: `<div vibe-dehydrate>` to skip reactive processing
1173
- - **Expression evaluation**: `@[count * 2]`, `@[firstName + ' ' + lastName]`
1174
-
1175
- ### Performance
1176
-
1177
- - Surgical DOM updates - only affected elements re-render
1178
-
1179
- ---
1180
-
1181
- ## [0.0.5] - 2026-01-15
1182
-
1183
- - Heavy optimizations for iteration rendering
1184
- - Added benchmark tooling
1185
- - Improved diffing algorithm for array changes
1186
-
1187
- ## [0.0.4] - 2026-01-10
1188
-
1189
- - Renamed from "Soulbound" to "Vibe"
1190
- - Published to npm as `@ape-egg/vibe`
1191
-
1192
- ## [0.0.1-0.0.3] - 2026-01-06 to 2026-01-09
1193
-
1194
- - Initial development and prototyping
1195
- - Core architecture: parse, link, hydrate, affected, state
1196
- - Basic iteration and conditional support