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