@ape-egg/vibe 1.9.1 → 1.9.5
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 +52 -0
- package/README.md +62 -111
- package/ROADMAP.md +6 -12
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +1 -1
- package/compiler/src/compiler/compile.rs +66 -6
- package/compiler/src/parser/html.rs +7 -1
- package/llms.txt +25 -0
- package/package.json +1 -1
- package/runtime/affected.js +141 -52
- package/runtime/component.js +120 -8
- package/runtime/conditionals.js +45 -3
- package/runtime/constants.js +12 -5
- package/runtime/hydrate.js +8 -11
- package/runtime/index.js +35 -1
- package/runtime/iterate.js +550 -61
- package/runtime/iteration-utils.js +9 -2
- package/runtime/loop-scope.js +157 -0
- package/runtime/parse.js +94 -20
- package/runtime/pre-compiled-iterations.js +12 -0
- package/runtime/state.js +18 -1
- package/runtime/utils.js +36 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.5] - 2026-05-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- 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).
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [1.9.4] - 2026-05-22
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **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.
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [1.9.3] - 2026-05-18
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **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.
|
|
32
|
+
- `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.
|
|
33
|
+
- 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.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [1.9.2] - 2026-05-08
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **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:
|
|
42
|
+
- **`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`.
|
|
43
|
+
- **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.
|
|
44
|
+
- **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.
|
|
45
|
+
- **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]`.
|
|
46
|
+
- 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.
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
49
|
+
- **`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.
|
|
50
|
+
|
|
51
|
+
- **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.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
3
55
|
## [1.9.1] - 2026-04-29
|
|
4
56
|
|
|
5
57
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vibe
|
|
2
2
|
|
|
3
|
-
**Version 1.9.
|
|
3
|
+
**Version 1.9.2 (Beta)** — A runtime-first reactive framework with optional compilation.
|
|
4
4
|
|
|
5
5
|
No virtual DOM. No build step required. Just modern JavaScript. When you need production optimizations, add the optional Rust-based compiler.
|
|
6
6
|
|
|
@@ -35,14 +35,31 @@ The core reactive runtime. Works directly in the browser without any build tools
|
|
|
35
35
|
</html>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### The `vibe()` Signature
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
window.$ = vibe(state, config?, targetSelector?);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **`state`** *(object, required)* — initial reactive state. Becomes `window.$`. Mutate freely (`$.count++`, `$.user.name = 'Alice'`); deep mutations trigger updates automatically. Methods on the object are preserved.
|
|
45
|
+
|
|
46
|
+
- **`config`** *(object, optional)* — runtime configuration. Currently supported keys:
|
|
47
|
+
- `debug` *(boolean, default `false`)* — colored console logs for every lifecycle phase (parse, hydrate, iterate, mutate, …). Useful for debugging reactivity issues.
|
|
48
|
+
|
|
49
|
+
- **`targetSelector`** *(string, optional)* — CSS selector for the root element vibe attaches to. **Defaults to `document.body`.** Vibe parses, hydrates, and observes mutations only inside this root — anything outside (e.g. `<head>`, sibling `<aside>` elements) is ignored. If the selector matches nothing, vibe silently falls back to `document.body`. Pass `'html'` to include `<head>` (e.g. for binding `<title>@[pageTitle]</title>`).
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
window.$ = vibe({ count: 0 }, { debug: true }, '#app');
|
|
53
|
+
```
|
|
54
|
+
|
|
38
55
|
### Prevent FOUC
|
|
39
56
|
|
|
40
57
|
To prevent a flash of unstyled content while Vibe hydrates:
|
|
41
58
|
|
|
42
59
|
1. Include `vibe.css` in your HTML
|
|
43
|
-
2. Add the `vibe` attribute to an element
|
|
60
|
+
2. Add the `vibe-fouc` attribute (or `class="vibe-fouc"`) to an element — typically `<body>`
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
`vibe.css` hides `[vibe-fouc]` and `.vibe-fouc` until hydration completes; Vibe removes the attribute/class once it's done.
|
|
46
63
|
|
|
47
64
|
### Reactive Bindings
|
|
48
65
|
|
|
@@ -110,14 +127,23 @@ Nested iteration with dot paths:
|
|
|
110
127
|
|
|
111
128
|
### Components
|
|
112
129
|
|
|
113
|
-
Runtime component loading with props and slots:
|
|
130
|
+
Runtime component loading with props and slots. Slot content (between the tags) replaces `<slot></slot>` inside the component template:
|
|
114
131
|
|
|
115
132
|
```html
|
|
133
|
+
<!-- /components/card.html -->
|
|
134
|
+
<div class="card">
|
|
135
|
+
<h3>@[title]</h3>
|
|
136
|
+
<slot></slot>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- usage -->
|
|
116
140
|
<component src="/components/card.html" title="@[pageTitle]" theme="dark">
|
|
117
141
|
<p>Content passed as slot</p>
|
|
118
142
|
</component>
|
|
119
143
|
```
|
|
120
144
|
|
|
145
|
+
`<div class="component" src="...">` is an equivalent alternative to `<component src="...">` for cases where standard HTML elements are required (validation, accessibility tooling).
|
|
146
|
+
|
|
121
147
|
Props can be reactive bindings (`title="@[pageTitle]"`), static literals (`theme="dark"`), or live objects/arrays passed through iteration scope (`<component src="/card.html" card="@[card]">` inside `<!-- each cards as card -->`). Non-primitive props are stashed in an internal registry so the child template can dot/iterate into them (`@[card.name]`, `<!-- each card.abilities as a -->`).
|
|
122
148
|
|
|
123
149
|
### Component-Local State
|
|
@@ -146,6 +172,30 @@ How it works:
|
|
|
146
172
|
|
|
147
173
|
Multi-segment paths (`@[this.user.profile.name]`), conditionals (`<!-- if this.editing -->`), and iterations (`<!-- each this.items as item -->`) all resolve against the component's bucket. Global `$` and component `this.X` coexist freely.
|
|
148
174
|
|
|
175
|
+
### Drop-In Components (no `vibe()` needed)
|
|
176
|
+
|
|
177
|
+
`component()` auto-boots the runtime. You can drop a self-contained reactive block into any HTML page — no top-level `vibe(...)` call, no global state setup, no build step. Import directly from a CDN and the runtime wires itself up:
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<component>
|
|
181
|
+
<script type="module">
|
|
182
|
+
import component from 'https://esm.sh/@ape-egg/vibe/component.js';
|
|
183
|
+
component({ count: 0 });
|
|
184
|
+
</script>
|
|
185
|
+
<button onclick="this.count--">-</button>
|
|
186
|
+
<span>Count: <strong>@[this.count]</strong></span>
|
|
187
|
+
<button onclick="this.count++">+</button>
|
|
188
|
+
</component>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
How it works:
|
|
192
|
+
|
|
193
|
+
1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper, registers state at `$[id]`, and tags the wrapper with `data-vibe-component-id`
|
|
194
|
+
2. Internally it calls `ensureBoot()` (from `@ape-egg/vibe/boot`), which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once, even if multiple `<component>` blocks call `component()`
|
|
195
|
+
3. From there, `@[this.X]`, `onclick="this.fn()"`, and `<!-- if this.X -->` work as documented
|
|
196
|
+
|
|
197
|
+
Multiple drop-in blocks on the same page each get their own state bucket. They can read each other's state via global `$['_c0'].count` if they need to coordinate, but in most drop-in cases they're independent.
|
|
198
|
+
|
|
149
199
|
### Lifecycle Hooks
|
|
150
200
|
|
|
151
201
|
```javascript
|
|
@@ -168,112 +218,12 @@ Skip reactive processing for an element:
|
|
|
168
218
|
<code vibe-dehydrate>@[this] displays literally</code>
|
|
169
219
|
```
|
|
170
220
|
|
|
171
|
-
###
|
|
172
|
-
|
|
173
|
-
Vibe uses specific patterns and keywords that have special meaning. Avoid using these for other purposes to prevent unexpected behavior:
|
|
174
|
-
|
|
175
|
-
#### Classes & Attributes
|
|
176
|
-
|
|
177
|
-
- **`vibe-fouc`** — Class or attribute for FOUC (Flash of Unstyled Content) prevention. Automatically removed after hydration completes.
|
|
178
|
-
|
|
179
|
-
```html
|
|
180
|
-
<body vibe-fouc>
|
|
181
|
-
<!-- or class="vibe-fouc" -->
|
|
182
|
-
</body>
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
- **`vibe-dehydrate`** — Class or attribute to skip reactive processing. Useful for displaying literal `@[...]` syntax in documentation.
|
|
186
|
-
```html
|
|
187
|
-
<code vibe-dehydrate>@[variable]</code>
|
|
188
|
-
<!-- or class="vibe-dehydrate" -->
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
#### Element Names & Classes
|
|
192
|
-
|
|
193
|
-
- **`<component>`** — Element name for component system. Used with `src` attribute for runtime component loading, or as a wrapper for inlined components.
|
|
194
|
-
|
|
195
|
-
```html
|
|
196
|
-
<component src="/path/to/component.html"></component>
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
- **`class="component"`** — Alternative syntax for components using standard HTML elements. Useful for HTML validation or accessibility.
|
|
200
|
-
|
|
201
|
-
```html
|
|
202
|
-
<div class="component" src="/path/to/component.html"></div>
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
- **`<slot>`** — Element name for component content injection. Gets replaced with content passed between component tags.
|
|
206
|
-
|
|
207
|
-
```html
|
|
208
|
-
<!-- In component file -->
|
|
209
|
-
<slot></slot>
|
|
210
|
-
|
|
211
|
-
<!-- Usage -->
|
|
212
|
-
<component src="...">
|
|
213
|
-
<p>This replaces the slot</p>
|
|
214
|
-
</component>
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
#### Comment Syntax
|
|
218
|
-
|
|
219
|
-
- **`<!-- each -->`** / **`<!-- /each -->`** — Iteration block markers.
|
|
220
|
-
|
|
221
|
-
```html
|
|
222
|
-
<!-- each items as item -->
|
|
223
|
-
<!-- /each -->
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
- **`<!-- if -->`** / **`<!-- else -->`** / **`<!-- /if -->`** — Conditional block markers.
|
|
227
|
-
```html
|
|
228
|
-
<!-- if condition -->
|
|
229
|
-
<!-- else -->
|
|
230
|
-
<!-- /if -->
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
#### Binding Syntax
|
|
234
|
-
|
|
235
|
-
- **`@[...]`** — Reactive binding syntax. Reserved for state references.
|
|
236
|
-
```html
|
|
237
|
-
<div>@[variable]</div>
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
#### Global Properties
|
|
241
|
-
|
|
242
|
-
- **`window.$`** — Global reactive state object. All reactive data should be accessed through this.
|
|
243
|
-
|
|
244
|
-
```javascript
|
|
245
|
-
window.$ = state({ count: 0 });
|
|
246
|
-
$.count++; // Triggers reactive updates
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
- **`window.__vibeManifest`** — Internal manifest data. Used by the compiler for optimization. Don't modify.
|
|
250
|
-
|
|
251
|
-
- **`window.__vibeCompiling`** — Internal flag. Set to `true` when running in compiler context.
|
|
252
|
-
|
|
253
|
-
#### Data Attributes
|
|
254
|
-
|
|
255
|
-
- **`data-vibe-component-id`** — Internal attribute for component scoping. Automatically added to component elements. Don't use manually.
|
|
256
|
-
|
|
257
|
-
#### Event Names
|
|
258
|
-
|
|
259
|
-
- **`vibe:ready`** — Custom event fired when Vibe completes initial hydration.
|
|
260
|
-
```javascript
|
|
261
|
-
document.addEventListener('vibe:ready', () => {
|
|
262
|
-
console.info('Vibe is ready');
|
|
263
|
-
});
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
#### Special Attribute Meanings
|
|
267
|
-
|
|
268
|
-
- **`src`** on **`<component>`** or **`<div class="component">`** — Triggers runtime component fetching. Components without `src` are treated as inline wrappers.
|
|
221
|
+
### Internal Names (don't collide)
|
|
269
222
|
|
|
270
|
-
|
|
271
|
-
<!-- Both work the same way -->
|
|
272
|
-
<component src="/components/card.html"></component>
|
|
273
|
-
<div class="component" src="/components/card.html"></div>
|
|
274
|
-
```
|
|
223
|
+
These are used by the runtime — don't repurpose them in your code:
|
|
275
224
|
|
|
276
|
-
-
|
|
225
|
+
- `window.__vibeManifest`, `window.__vibeCompiling`, `window.__vibeComponents`, `window.__vibeIterProps` — internal registries
|
|
226
|
+
- `data-vibe-component-id`, `data-vibe-iter-prop` — element-level bookkeeping attributes (set automatically)
|
|
277
227
|
|
|
278
228
|
### Deep Reactivity
|
|
279
229
|
|
|
@@ -586,11 +536,12 @@ Every time `matchesKey` can't prove an expression is unrelated to a state change
|
|
|
586
536
|
|
|
587
537
|
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.
|
|
588
538
|
|
|
589
|
-
Handled today by case-insensitive fallbacks in
|
|
590
|
-
- `runtime/hydrate.js:~32` — case-insensitive state key lookup when evaluating name bindings
|
|
539
|
+
Handled today by case-insensitive fallbacks in three places:
|
|
540
|
+
- `runtime/hydrate.js:~32` — case-insensitive state key lookup when evaluating name bindings (clone path)
|
|
591
541
|
- `runtime/affected.js:~235` — case-insensitive `matchesKey` wrapper for name bindings
|
|
542
|
+
- `runtime/iterate.js` — case-insensitive lookup against `stateKeys` inside `compileBatchFn`'s name-binding emission (batch path)
|
|
592
543
|
|
|
593
|
-
|
|
544
|
+
All three 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.
|
|
594
545
|
|
|
595
546
|
### Related: value hydration into attribute values is case-preserving
|
|
596
547
|
|
package/ROADMAP.md
CHANGED
|
@@ -6,7 +6,8 @@ Feature proposals and improvements for Vibe's runtime-first reactive framework.
|
|
|
6
6
|
|
|
7
7
|
## Proposed: Manual Hydration API
|
|
8
8
|
|
|
9
|
-
**Status**:
|
|
9
|
+
**Status**: Superseded by `$.reconcile()` (shipped in 1.9.0). See `runtime/reconcile.js` and the changelog entry. The reconciliation API solves the wholesale-replacement use cases listed below by walking the new source HTML against the live tree with a two-pointer aligner, preserving DOM identity, focus, and selection. Vibe-managed regions (iterations, conditionals, components, `<slot>` pairs) stay opaque. The shapes proposed below (`vibe.hydrate(el)` / `vibe.evaluate(html, state)` / `vibe.setHTML(el, html)`) were not implemented; `$.reconcile(target, source)` covers the same scenarios with a different ergonomic.
|
|
10
|
+
**Original status**: Proposal
|
|
10
11
|
**Priority**: Medium
|
|
11
12
|
**Category**: Core Runtime
|
|
12
13
|
|
|
@@ -265,22 +266,15 @@ Likely related to:
|
|
|
265
266
|
|
|
266
267
|
#### Resolution Path
|
|
267
268
|
|
|
268
|
-
**
|
|
269
|
-
- Remove `<tutorial>` wrapper from demos
|
|
270
|
-
- Add note in CLAUDE.md about limitation
|
|
271
|
-
- Tests validate core reactivity works correctly
|
|
269
|
+
**Recommended workaround today**: use `$.reconcile(target, source)` (shipped 1.9.0) instead of multiple raw `innerHTML` replacements. Reconcile is the supported path for wholesale HTML swaps and preserves vibe-managed regions cleanly. The original `innerHTML`-based reproduction is an unsupported pattern.
|
|
272
270
|
|
|
273
|
-
**
|
|
274
|
-
- Deep investigation into branch reference lifecycle
|
|
275
|
-
- Possibly: deep clone branches instead of sharing reference
|
|
276
|
-
- Possibly: rebuild conditional metadata on each innerHTML replacement
|
|
277
|
-
- Possibly: manual hydration API (see "Manual Hydration API" proposal above)
|
|
271
|
+
**If the bug needs a proper fix** (not yet scheduled): deep investigation into branch reference lifecycle, deep-clone the branches metadata instead of sharing references, or rebuild conditional metadata on each replacement.
|
|
278
272
|
|
|
279
273
|
This is acceptable technical debt since:
|
|
280
274
|
1. Edge case not representative of normal usage
|
|
281
275
|
2. Core reactivity (the 99% case) works correctly
|
|
282
|
-
3.
|
|
283
|
-
4.
|
|
276
|
+
3. `$.reconcile` covers the wholesale-replacement scenarios that motivated this report
|
|
277
|
+
4. Can be addressed if/when a real-world need to use raw `innerHTML` replacement surfaces
|
|
284
278
|
|
|
285
279
|
---
|
|
286
280
|
|
|
Binary file
|
|
Binary file
|
|
@@ -173,7 +173,7 @@ while (walker.nextNode()) {
|
|
|
173
173
|
- `depth = 1` when we exit nested `section.items` (line 98)
|
|
174
174
|
- `depth = 0` when we exit `menuSections` (line 483) - **this is our match**
|
|
175
175
|
|
|
176
|
-
**Files Modified**: `pre-compiled-manifest.
|
|
176
|
+
**Files Modified**: `pre-compiled-manifest.js`
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
@@ -86,6 +86,22 @@ pub fn should_skip_path(path: &Path, name: &str) -> bool {
|
|
|
86
86
|
false
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/// Rename `src="{component_src}"` to `data-vibe-recursive-src="{component_src}"`
|
|
90
|
+
/// on `<component>` / `<div class="component">` tags. Used during cache build
|
|
91
|
+
/// to neutralize cyclic refs before they reach `inline_component_elements` —
|
|
92
|
+
/// the inliner's `src=` regex won't match the renamed attribute, so it stops
|
|
93
|
+
/// re-expanding. `process_html_with_cache` restores the attribute to plain
|
|
94
|
+
/// `src=` at the end of compilation so the runtime fetches it normally.
|
|
95
|
+
fn escape_recursive_src(content: &str, component_src: &str) -> String {
|
|
96
|
+
let pattern = format!(
|
|
97
|
+
r#"(<(?:component|div)\s+[^>]*\b)src="{}""#,
|
|
98
|
+
regex::escape(component_src),
|
|
99
|
+
);
|
|
100
|
+
let re = Regex::new(&pattern).unwrap();
|
|
101
|
+
re.replace_all(content, format!(r#"$1data-vibe-recursive-src="{}""#, component_src))
|
|
102
|
+
.into_owned()
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
/// Remove FOUC prevention class and/or attribute from compiled HTML
|
|
90
106
|
/// Since HTML is pre-rendered, there's no need for FOUC prevention
|
|
91
107
|
fn remove_fouc_prevention(html: String) -> String {
|
|
@@ -1251,7 +1267,8 @@ impl Compiler {
|
|
|
1251
1267
|
// Extract and fetch components recursively
|
|
1252
1268
|
let components = self.extract_component_srcs(&transformed);
|
|
1253
1269
|
for component_src in components {
|
|
1254
|
-
|
|
1270
|
+
let mut visiting = std::collections::HashSet::new();
|
|
1271
|
+
self.fetch_component_recursive(&component_src, parser, &mut visiting);
|
|
1255
1272
|
}
|
|
1256
1273
|
}
|
|
1257
1274
|
|
|
@@ -1288,16 +1305,31 @@ impl Compiler {
|
|
|
1288
1305
|
// Extract and fetch components recursively
|
|
1289
1306
|
let components = self.extract_component_srcs(&transformed);
|
|
1290
1307
|
for component_src in components {
|
|
1291
|
-
|
|
1308
|
+
let mut visiting = std::collections::HashSet::new();
|
|
1309
|
+
self.fetch_component_recursive(&component_src, parser, &mut visiting);
|
|
1292
1310
|
}
|
|
1293
1311
|
}
|
|
1294
1312
|
|
|
1295
1313
|
Ok(())
|
|
1296
1314
|
}
|
|
1297
1315
|
|
|
1298
|
-
/// Recursively fetch a component and its nested components (for inlining)
|
|
1299
|
-
/// Returns the fully inlined content (all nested components resolved)
|
|
1300
|
-
|
|
1316
|
+
/// Recursively fetch a component and its nested components (for inlining).
|
|
1317
|
+
/// Returns the fully inlined content (all nested components resolved).
|
|
1318
|
+
///
|
|
1319
|
+
/// `visiting` tracks the chain of components currently being fetched. A
|
|
1320
|
+
/// `<component src>` that points back into the chain is a cycle (direct
|
|
1321
|
+
/// self-reference, or A→B→A) — those are valid runtime patterns (recursive
|
|
1322
|
+
/// tree components bounded by data depth) but would expand infinitely at
|
|
1323
|
+
/// compile time. Cycle references are escaped to `data-vibe-recursive-src`
|
|
1324
|
+
/// in the cached content so the inliner's `src=` regex won't keep matching
|
|
1325
|
+
/// them; `process_html_with_cache` restores the attribute name at the end
|
|
1326
|
+
/// of compilation so the runtime sees a normal `<component src>` tag.
|
|
1327
|
+
fn fetch_component_recursive(
|
|
1328
|
+
&mut self,
|
|
1329
|
+
component_src: &str,
|
|
1330
|
+
parser: &HtmlParser,
|
|
1331
|
+
visiting: &mut std::collections::HashSet<String>,
|
|
1332
|
+
) -> Option<String> {
|
|
1301
1333
|
// Normalize path: ensure it starts with / (unless it's a URL)
|
|
1302
1334
|
let normalized_src = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
1303
1335
|
component_src.to_string()
|
|
@@ -1315,6 +1347,13 @@ impl Compiler {
|
|
|
1315
1347
|
return Some(cached.clone());
|
|
1316
1348
|
}
|
|
1317
1349
|
|
|
1350
|
+
// Cycle: this component is already higher in the fetch chain. Don't
|
|
1351
|
+
// recurse — the caller will leave the `<component src>` tag as-is and
|
|
1352
|
+
// the runtime handles the recursion with real data bounds.
|
|
1353
|
+
if visiting.contains(&normalized_src) {
|
|
1354
|
+
return None;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1318
1357
|
// Fetch external or read internal component (raw content)
|
|
1319
1358
|
let content = if normalized_src.starts_with("http://") || normalized_src.starts_with("https://") {
|
|
1320
1359
|
match self.fetch_external_component_raw(&normalized_src) {
|
|
@@ -1355,15 +1394,36 @@ impl Compiler {
|
|
|
1355
1394
|
&self.config.components,
|
|
1356
1395
|
);
|
|
1357
1396
|
|
|
1397
|
+
// Mark this component as in-flight before recursing into its nested
|
|
1398
|
+
// refs. Cycles (direct self-reference or A→B→A chains) are detected
|
|
1399
|
+
// by the visiting check at the top of this function.
|
|
1400
|
+
visiting.insert(normalized_src.clone());
|
|
1401
|
+
|
|
1358
1402
|
// Recursively fetch and inline all nested components
|
|
1359
1403
|
let nested = self.extract_component_srcs(&transformed);
|
|
1360
1404
|
for nested_src in nested {
|
|
1361
|
-
if let Some(nested_content) = self.fetch_component_recursive(&nested_src, parser) {
|
|
1405
|
+
if let Some(nested_content) = self.fetch_component_recursive(&nested_src, parser, visiting) {
|
|
1362
1406
|
// Inline this nested component into the current component
|
|
1363
1407
|
transformed = parser.inline_single_component(&transformed, &nested_src, &nested_content);
|
|
1364
1408
|
}
|
|
1365
1409
|
}
|
|
1366
1410
|
|
|
1411
|
+
visiting.remove(&normalized_src);
|
|
1412
|
+
|
|
1413
|
+
// Any `<component src>` still in `transformed` whose target is in the
|
|
1414
|
+
// visiting chain (a cycle) didn't get inlined. Escape its `src=` so
|
|
1415
|
+
// `inline_component_elements` won't keep re-expanding it forever when
|
|
1416
|
+
// a page inlines this cached content. `process_html_with_cache`
|
|
1417
|
+
// restores the attribute to plain `src=` at the end of compilation so
|
|
1418
|
+
// the runtime fetches it normally.
|
|
1419
|
+
for ancestor in visiting.iter() {
|
|
1420
|
+
transformed = escape_recursive_src(&transformed, ancestor);
|
|
1421
|
+
}
|
|
1422
|
+
// Also escape direct self-references — a component pointing at itself
|
|
1423
|
+
// never made it into `visiting` (the cycle check short-circuited
|
|
1424
|
+
// before insertion), so handle it explicitly.
|
|
1425
|
+
transformed = escape_recursive_src(&transformed, &normalized_src);
|
|
1426
|
+
|
|
1367
1427
|
// Cache the fully resolved content
|
|
1368
1428
|
self.component_cache.insert(normalized_src.clone(), transformed.clone());
|
|
1369
1429
|
|
|
@@ -154,7 +154,13 @@ impl HtmlParser {
|
|
|
154
154
|
result = transform_custom_tags_to_divs(&result, reserved_elements);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
// Step 4: Restore
|
|
157
|
+
// Step 4: Restore cyclic `<component src>` references that were
|
|
158
|
+
// escaped during cache build (see compile.rs::escape_recursive_src).
|
|
159
|
+
// These are runtime-handled component references that the inliner
|
|
160
|
+
// had to skip; the runtime needs them as real `src=` attributes.
|
|
161
|
+
result = result.replace("data-vibe-recursive-src=", "src=");
|
|
162
|
+
|
|
163
|
+
// Step 5: Restore DOCTYPE if it was present
|
|
158
164
|
if let Some(dt) = doctype {
|
|
159
165
|
// Remove any existing DOCTYPE that might have been left behind
|
|
160
166
|
result = doctype_re.replace(&result, "").to_string();
|
package/llms.txt
CHANGED
|
@@ -183,6 +183,31 @@ How it works:
|
|
|
183
183
|
|
|
184
184
|
Multi-segment paths, conditionals (`<!-- if this.editing -->`), and iterations (`<!-- each this.items as item -->`) all work. Global `$` and component-local `this.X` coexist within the same template.
|
|
185
185
|
|
|
186
|
+
## Drop-In Components (Standalone Usage)
|
|
187
|
+
|
|
188
|
+
`component()` auto-boots the runtime, so a `<component>` block is a fully self-contained reactive unit. Drop it into any HTML page — no top-level `vibe(...)` call, no build step, no global state setup. Import directly from a CDN:
|
|
189
|
+
|
|
190
|
+
```html
|
|
191
|
+
<component>
|
|
192
|
+
<script type="module">
|
|
193
|
+
import component from 'https://esm.sh/@ape-egg/vibe/component.js';
|
|
194
|
+
component({ count: 0 });
|
|
195
|
+
</script>
|
|
196
|
+
<button onclick="this.count--">-</button>
|
|
197
|
+
<span>Count: @[this.count]</span>
|
|
198
|
+
<button onclick="this.count++">+</button>
|
|
199
|
+
</component>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Mechanism:
|
|
203
|
+
1. `component({...})` claims the nearest unprocessed `<component>` (or `<div class="component">`) wrapper
|
|
204
|
+
2. It calls `ensureBoot()` from `@ape-egg/vibe/boot`, which initializes `window.$`, parses the DOM, hydrates bindings, and starts the MutationObserver — exactly once per page, even if multiple drop-in blocks call `component()`
|
|
205
|
+
3. `@[this.X]`, `onclick="this.fn()"`, `<!-- if this.X -->`, etc. resolve against that block's bucket
|
|
206
|
+
|
|
207
|
+
Multiple drop-in blocks on the same page each get their own state and run independently. They can still read each other's state via global `$['<id>']` if coordination is needed.
|
|
208
|
+
|
|
209
|
+
This makes vibe usable as a "sprinkle of reactivity" library: paste a snippet into a static site, blog post, CMS-rendered page, or third-party HTML, and it just works.
|
|
210
|
+
|
|
186
211
|
## Lifecycle Hooks
|
|
187
212
|
|
|
188
213
|
```javascript
|