@ape-egg/vibe 1.7.2 → 1.9.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/CHANGELOG.md +139 -0
- package/README.md +28 -0
- package/boot.js +5 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +85 -496
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +3 -11
- package/compiler/src/parser/html.rs +15 -15
- package/index.js +15 -0
- package/package.json +2 -2
- package/runtime/_vibe-compiled-iteration-batch.js +18 -9
- package/runtime/affected.js +54 -16
- package/runtime/component.js +287 -110
- package/runtime/conditionals.js +99 -7
- package/runtime/constants.js +10 -6
- package/runtime/index.js +142 -47
- package/runtime/iterate.js +364 -142
- package/runtime/iteration-utils.js +5 -1
- package/runtime/manifest.js +2 -1
- package/runtime/parse.js +67 -80
- package/runtime/pre-compiled-iterations.js +34 -21
- package/runtime/pre-compiled-manifest.js +38 -13
- package/runtime/reconcile.js +621 -0
- package/runtime/state.js +26 -7
- package/runtime/utils.js +97 -5
- package/vibe.css +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,144 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.0] - 2026-04-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`$.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.
|
|
8
|
+
- Exposed as `$.reconcile` (non-enumerable so it stays out of state snapshots)
|
|
9
|
+
- Test: `tests/e2e/reconcile.spec.js`
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- Test: `tests/e2e/component-state-cleanup.spec.js`
|
|
13
|
+
|
|
14
|
+
- **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(...)`:
|
|
15
|
+
```html
|
|
16
|
+
<!-- each items.filter(x => x.active) as item -->
|
|
17
|
+
<!-- each Array.from({length: 10}, (_, i) => i) as n -->
|
|
18
|
+
<!-- each window.fights as fight, i -->
|
|
19
|
+
```
|
|
20
|
+
- Test: `tests/e2e/each-expression.spec.js`
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
|
|
26
|
+
- **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.
|
|
27
|
+
- Tests: `tests/e2e/prop-in-directive.spec.js`, `tests/e2e/prop-in-expression.spec.js`, `tests/e2e/two-way-binding.spec.js`
|
|
28
|
+
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
31
|
+
- **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.
|
|
32
|
+
- Tests: `tests/e2e/async-component-state.spec.js`, `tests/e2e/parallel-components.spec.js`
|
|
33
|
+
|
|
34
|
+
- **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.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **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.
|
|
39
|
+
|
|
40
|
+
- **`<!-- each -->` over `Array.from(...)` and other inline expressions** — previously failed silently because `ITERATION_REGEX` only accepted simple paths. See `ITERATION_REGEX` change above.
|
|
41
|
+
|
|
42
|
+
- **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.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **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.
|
|
47
|
+
|
|
48
|
+
- **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.
|
|
49
|
+
|
|
50
|
+
### Documentation
|
|
51
|
+
|
|
52
|
+
- **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.
|
|
53
|
+
|
|
54
|
+
### Packaging
|
|
55
|
+
|
|
56
|
+
- **`@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`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## [1.8.1] - 2026-04-12
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **`<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.
|
|
65
|
+
- `runtime/component.js` — `<slot></slot>` replaced with `<slot>${children}</slot>`
|
|
66
|
+
- `compiler/src/parser/html.rs` — compiler emits the same wrapping
|
|
67
|
+
- `vibe.css` — `slot, div.slot { display: contents }` so the wrapper is layout-transparent
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **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.
|
|
72
|
+
- `runtime/index.js` — `processMutations` now captures `_vibeSlotContent` for all nested `<component src>` elements before parse/hydrate runs
|
|
73
|
+
- Test: `tests/e2e/conditional-in-nested-slot.spec.js`
|
|
74
|
+
|
|
75
|
+
- **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.
|
|
76
|
+
- `runtime/affected.js` — `matchesKey` now also does word-boundary search so it finds `coins` as an identifier inside the expression
|
|
77
|
+
- Test: `tests/e2e/expression-dependency.spec.js`
|
|
78
|
+
|
|
79
|
+
- **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.
|
|
80
|
+
- `runtime/affected.js` — iteration recursion now builds plain-object snapshots from previousState/currentState merged with scopedState's local vars
|
|
81
|
+
|
|
82
|
+
- **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.
|
|
83
|
+
- `runtime/affected.js` — name binding dependency check now matches state keys case-insensitively, mirroring `hydrate.js`'s existing case-insensitive evaluation fallback
|
|
84
|
+
|
|
85
|
+
### Documentation
|
|
86
|
+
|
|
87
|
+
- `README.md` — "Future improvements to refactor" section documenting string-based dependency tracking limitations and HTML lowercase attribute handling
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## [1.8.0] - 2026-04-10
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- **State batching** — Multiple `$.prop = value` assignments in the same microtask now produce a single rerender instead of cascading updates
|
|
96
|
+
- Proxy `set` trap calls `scheduleFlush()` (via `queueMicrotask`) instead of `rerender()` directly
|
|
97
|
+
- Eliminates cascading rerenders from `afterUpdate` hooks setting state
|
|
98
|
+
- Rerender callback now does full diff of `previousState` vs `currentState` (no longer receives individual changed prop)
|
|
99
|
+
|
|
100
|
+
- **DOM ownership tracking** — Two new data structures prevent re-processing of already-managed nodes
|
|
101
|
+
- `managedNodes` (`WeakSet`): Tracks nodes inserted by `mountBranch` / `renderIteration`. `shouldProcessNode` skips them.
|
|
102
|
+
- `branchNodeRegistry` (`WeakMap`): Maps DOM nodes to their conditional branch, so `processComponent`'s `el.replaceWith()` correctly updates conditional tracking
|
|
103
|
+
|
|
104
|
+
- **Parallel component loading** — Same-level `<component src>` elements now fetch in parallel
|
|
105
|
+
- `isNestedInUnprocessedComponent()` filters slot content from premature processing
|
|
106
|
+
- `processSingle()` handles fetch + script execution + DOM replacement per component
|
|
107
|
+
- `processComponent()` fires all top-level fetches simultaneously; nested components discovered after parent finalizes
|
|
108
|
+
|
|
109
|
+
- **Component script imports** — `<script type="module">` in components now supports `import` statements
|
|
110
|
+
- Static imports rewritten to dynamic `await import()` (default, named, namespace, side-effect)
|
|
111
|
+
- `import component from '...'` is stripped (Vibe injects it as a parameter)
|
|
112
|
+
- `AsyncFunction` constructor used only when imports exist; sync path preserved for boot timing
|
|
113
|
+
|
|
114
|
+
- **Bracket notation in paths** — `resolvePath` now handles `teams[0].combatants` via `path.match(/[^.\[\]]+/g)` splitting
|
|
115
|
+
|
|
116
|
+
- **Quoted strings in binding expressions** — `BINDING_REGEX` now supports `@[x.replace('.png', '-mugshot.png')]`
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
|
|
120
|
+
- **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.
|
|
121
|
+
|
|
122
|
+
- **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.
|
|
123
|
+
|
|
124
|
+
- **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.
|
|
125
|
+
|
|
126
|
+
### Performance
|
|
127
|
+
|
|
128
|
+
- **`evalInScope` function cache** — Compiled `new Function()` objects cached by expression + state keys signature. For 1000 rows × 4 bindings, reduces 4000 Function compilations to ~4.
|
|
129
|
+
- **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.
|
|
130
|
+
- **DocumentFragment batching** — `renderIteration` collects all cloned nodes in a DocumentFragment, single `insertBefore` at the end instead of N × M individual DOM mutations.
|
|
131
|
+
- **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.
|
|
132
|
+
- **`extractPlainValue` optimization** — Indexed `for` loops, `Object.keys()` instead of `for..in`, inline primitive check, pre-allocated arrays.
|
|
133
|
+
- **Old-array ground truth** — `updateIteration` uses rendered instances as truth when `oldState` disagrees with actual instance count, preventing mismatched diff operations.
|
|
134
|
+
|
|
135
|
+
### Removed
|
|
136
|
+
|
|
137
|
+
- **`window.__VIBE_FAST_ITERATION__` experimental flag** — Replaced by automatic bulk replacement detection (no opt-in needed)
|
|
138
|
+
- **Per-prop rerender** — State proxy no longer passes `{ [changedProp]: ... }` to rerender; full diff via batching replaces it
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
3
142
|
## [1.7.2] - 2026-02-27
|
|
4
143
|
|
|
5
144
|
### Fixed
|
package/README.md
CHANGED
|
@@ -525,3 +525,31 @@ Modern browsers with Proxy and MutationObserver support.
|
|
|
525
525
|
## License
|
|
526
526
|
|
|
527
527
|
ISC
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Future improvements to refactor
|
|
532
|
+
|
|
533
|
+
### Dependency tracking uses string matching, not AST
|
|
534
|
+
|
|
535
|
+
`affected()` determines which bindings to re-hydrate by string-matching expression text against state key names. When matching fails, a safety fallback marks the binding as affected on every state change.
|
|
536
|
+
|
|
537
|
+
- `runtime/affected.js:15` — `matchesKey` does exact/prefix match, then word-boundary search
|
|
538
|
+
- `runtime/affected.js:~122` — `shouldAffect = noMatch || ...` fallback for unmatched expressions
|
|
539
|
+
- `runtime/affected.js:~248` — same fallback for name bindings
|
|
540
|
+
|
|
541
|
+
Every time `matchesKey` can't prove an expression is unrelated to a state change, that binding re-hydrates. A proper AST-based dependency extraction at parse time would eliminate the fallback entirely: each binding stores its exact dependency set, and runtime just checks if any dep key changed.
|
|
542
|
+
|
|
543
|
+
### HTML lowercases attribute names — breaks name binding matching
|
|
544
|
+
|
|
545
|
+
The browser lowercases all attribute names during HTML parsing. `<page @[pageName]>` becomes `@[pagename]` in the DOM. Every value hydrated into an attribute NAME position loses its case. `@[myCoolPage]` → DOM stores `@[mycoolpage]`, causing comparison mismatches against camelCase state keys.
|
|
546
|
+
|
|
547
|
+
Handled today by case-insensitive fallbacks in two places:
|
|
548
|
+
- `runtime/hydrate.js:~32` — case-insensitive state key lookup when evaluating name bindings
|
|
549
|
+
- `runtime/affected.js:~235` — case-insensitive `matchesKey` wrapper for name bindings
|
|
550
|
+
|
|
551
|
+
Both are workarounds for HTML's behavior. A cleaner architecture would centralize the case-insensitive state key resolution into one helper, or normalize the name binding expression to canonical state-key case at first evaluation and cache it on the tree node.
|
|
552
|
+
|
|
553
|
+
### Related: value hydration into attribute values is case-preserving
|
|
554
|
+
|
|
555
|
+
Only attribute NAMES are lowercased by HTML, not values. So `title="@[pageName]"` and `@[pageName]` in text content preserve case and work without fallbacks — the issue is specific to name bindings.
|
package/boot.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Used by both index.js (global state) and component.js (component state)
|
|
3
3
|
|
|
4
4
|
import main from './runtime/index.js';
|
|
5
|
-
import { getPendingListeners } from './index.js';
|
|
5
|
+
import { getPendingListeners, chainInstanceReady } from './index.js';
|
|
6
6
|
|
|
7
7
|
let bootQueued = false;
|
|
8
8
|
let booted = false;
|
|
@@ -40,6 +40,10 @@ export const boot = () => {
|
|
|
40
40
|
// Boot with merged state
|
|
41
41
|
window.$ = main(mergedState, config, targetSelector);
|
|
42
42
|
|
|
43
|
+
// Forward real $.ready to the pre-boot placeholder's ready promise so
|
|
44
|
+
// any consumer that captured the placeholder can still await readiness.
|
|
45
|
+
chainInstanceReady(window.$.ready);
|
|
46
|
+
|
|
43
47
|
// Apply pending listeners from vibe instance
|
|
44
48
|
const pendingListeners = getPendingListeners();
|
|
45
49
|
if (pendingListeners) {
|
|
Binary file
|
|
Binary file
|