@ape-egg/vibe 2.3.0 → 3.0.1
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 +14 -4
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +10 -15
- package/llms.txt +8 -6
- package/package.json +19 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +312 -99
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +251 -111
- package/runtime/index.js +180 -71
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +69 -5
- 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 +77 -14
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1196
- 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 -2880
- 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 -16
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/spa.rs +0 -477
- 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 -1278
- package/compiler/src/config.rs +0 -279
- package/compiler/src/main.rs +0 -358
- 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/ROADMAP.md
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
# Vibe Roadmap
|
|
2
|
-
|
|
3
|
-
Feature proposals and improvements for Vibe's runtime-first reactive framework.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Proposed: Manual Hydration API
|
|
8
|
-
|
|
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
|
|
11
|
-
**Priority**: Medium
|
|
12
|
-
**Category**: Core Runtime
|
|
13
|
-
|
|
14
|
-
### Problem
|
|
15
|
-
|
|
16
|
-
Vibe's MutationObserver automatically hydrates `@[bindings]` on initial page load and incremental DOM changes, but fails when developers perform wholesale DOM replacement via `innerHTML`:
|
|
17
|
-
|
|
18
|
-
```js
|
|
19
|
-
// This doesn't trigger Vibe's hydration:
|
|
20
|
-
element.innerHTML = '<h1>Welcome, @[race]!</h1>';
|
|
21
|
-
// Result: Literal text "@[race]" instead of evaluated "human"
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
This happens because:
|
|
25
|
-
1. `innerHTML` replacement destroys all old DOM nodes and creates new ones
|
|
26
|
-
2. Vibe's MutationObserver is designed for incremental mutations, not complete replacement
|
|
27
|
-
3. No mechanism exists to manually trigger re-hydration
|
|
28
|
-
|
|
29
|
-
### Current Workaround
|
|
30
|
-
|
|
31
|
-
Users must manually evaluate bindings before setting innerHTML:
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
const evaluateBindings = (html) => {
|
|
35
|
-
return html.replace(/@\[([^\]]+)\]/g, (match, expression) => {
|
|
36
|
-
const keys = Object.keys(window.$);
|
|
37
|
-
const values = Object.values(window.$);
|
|
38
|
-
const result = new Function(...keys, `return ${expression}`)(...values);
|
|
39
|
-
return result ?? '';
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
element.innerHTML = evaluateBindings(html); // Manually evaluated
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
**Issues with this approach:**
|
|
47
|
-
- Not DRY - duplicates Vibe's internal evaluation logic
|
|
48
|
-
- Fragile - user's regex might not match Vibe's parser exactly
|
|
49
|
-
- Knowledge burden - users need to know when manual evaluation is needed
|
|
50
|
-
- Inconsistent - some bindings auto-hydrate, others need manual work
|
|
51
|
-
|
|
52
|
-
### Use Cases
|
|
53
|
-
|
|
54
|
-
This affects multiple real-world scenarios:
|
|
55
|
-
|
|
56
|
-
1. **Dynamic content replacement** (tutorials, articles, modals)
|
|
57
|
-
2. **Client-side routing** (replacing page sections with new HTML)
|
|
58
|
-
3. **Lazy-loaded sections** (loading HTML from server with bindings)
|
|
59
|
-
4. **Template cloning** (using `<template>` elements with `@[bindings]`)
|
|
60
|
-
5. **Server-sent HTML** (SSR-like patterns where server sends HTML with bindings)
|
|
61
|
-
|
|
62
|
-
### Proposed Solution
|
|
63
|
-
|
|
64
|
-
Add a manual hydration API that allows users to trigger Vibe's binding evaluation:
|
|
65
|
-
|
|
66
|
-
#### Option 1: Element Hydration
|
|
67
|
-
```js
|
|
68
|
-
vibe.hydrate(element);
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**Usage:**
|
|
72
|
-
```js
|
|
73
|
-
tutorial.innerHTML = '<h1>Welcome, @[race]!</h1>';
|
|
74
|
-
vibe.hydrate(tutorial); // Scan tutorial and children for @[bindings]
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
**Pros:**
|
|
78
|
-
- Most flexible - works with any element
|
|
79
|
-
- Matches web component patterns (`connectedCallback()`)
|
|
80
|
-
- Clear intent - "scan this element"
|
|
81
|
-
|
|
82
|
-
**Cons:**
|
|
83
|
-
- Requires import/reference to vibe library
|
|
84
|
-
- Two-step process (set innerHTML, then hydrate)
|
|
85
|
-
|
|
86
|
-
#### Option 2: HTML String Evaluation
|
|
87
|
-
```js
|
|
88
|
-
const evaluated = vibe.evaluate(html, state);
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**Usage:**
|
|
92
|
-
```js
|
|
93
|
-
const html = '<h1>Welcome, @[race]!</h1>';
|
|
94
|
-
const evaluated = vibe.evaluate(html, window.$);
|
|
95
|
-
tutorial.innerHTML = evaluated;
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
**Pros:**
|
|
99
|
-
- Pure function - easier to test
|
|
100
|
-
- Works without DOM access
|
|
101
|
-
- Can be used server-side or in workers
|
|
102
|
-
|
|
103
|
-
**Cons:**
|
|
104
|
-
- Users must manage state passing
|
|
105
|
-
- Doesn't handle nested/dynamic state updates
|
|
106
|
-
|
|
107
|
-
#### Option 3: Safe innerHTML Setter
|
|
108
|
-
```js
|
|
109
|
-
vibe.setHTML(element, html);
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**Usage:**
|
|
113
|
-
```js
|
|
114
|
-
vibe.setHTML(tutorial, '<h1>Welcome, @[race]!</h1>');
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Pros:**
|
|
118
|
-
- Single operation - set and hydrate in one call
|
|
119
|
-
- Matches platform APIs (`element.setHTML()`)
|
|
120
|
-
- Simplest API surface
|
|
121
|
-
|
|
122
|
-
**Cons:**
|
|
123
|
-
- Yet another setter abstraction
|
|
124
|
-
- Might conflict with future platform APIs
|
|
125
|
-
|
|
126
|
-
### Recommendation
|
|
127
|
-
|
|
128
|
-
**Implement Option 1** (`vibe.hydrate(element)`):
|
|
129
|
-
- Aligns with Vibe's runtime-first philosophy
|
|
130
|
-
- Gives users explicit control over hydration timing
|
|
131
|
-
- Most flexible for different scenarios
|
|
132
|
-
- Clear and predictable behavior
|
|
133
|
-
|
|
134
|
-
### Implementation Notes
|
|
135
|
-
|
|
136
|
-
```js
|
|
137
|
-
// Expose on the state proxy:
|
|
138
|
-
window.$ = state({ race: 'human' });
|
|
139
|
-
window.$.vibe.hydrate(element); // Scan element for @[bindings]
|
|
140
|
-
|
|
141
|
-
// Or as a module export:
|
|
142
|
-
import state, { hydrate } from '@ape-egg/vibe';
|
|
143
|
-
hydrate(element);
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Should support:
|
|
147
|
-
- Single element: `hydrate(tutorial)`
|
|
148
|
-
- Multiple elements: `hydrate([el1, el2])`
|
|
149
|
-
- Selector: `hydrate('tutorial')` (convenience)
|
|
150
|
-
|
|
151
|
-
### Related
|
|
152
|
-
|
|
153
|
-
Compare to other frameworks:
|
|
154
|
-
- **Alpine.js**: `Alpine.initTree(el)` - manual initialization
|
|
155
|
-
- **Vue**: `app.mount(el)` - mount to element
|
|
156
|
-
- **Svelte**: Compiler handles this at build time
|
|
157
|
-
- **HTMX**: `htmx.process(el)` - process element for attributes
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Known Issues
|
|
162
|
-
|
|
163
|
-
### innerHTML Replacement Corrupts Conditional Branches in Iterations
|
|
164
|
-
|
|
165
|
-
**Status**: Bug
|
|
166
|
-
**Priority**: Low (edge case)
|
|
167
|
-
**Category**: Core Runtime
|
|
168
|
-
**Discovered**: 2026-01-25
|
|
169
|
-
|
|
170
|
-
#### Problem
|
|
171
|
-
|
|
172
|
-
When a parent element containing iterations with nested conditionals has its `innerHTML` replaced multiple times with identical HTML, the conditional's `else` branches become `null` after 2-3 replacements. This causes conditionals to fail rendering.
|
|
173
|
-
|
|
174
|
-
**Root cause:**
|
|
175
|
-
1. Iteration template's `branches` object is shared across all iteration instances (iterate.js:65: `branches, // Branch templates are reused`)
|
|
176
|
-
2. When `innerHTML` replacement happens, something mutates the shared `branches.else` to `null`
|
|
177
|
-
3. All instances reference the same corrupted branches object
|
|
178
|
-
4. Subsequent renders have no `else` branch template to mount
|
|
179
|
-
|
|
180
|
-
#### Reproduction
|
|
181
|
-
|
|
182
|
-
Programmatic test case:
|
|
183
|
-
|
|
184
|
-
```js
|
|
185
|
-
// HTML structure
|
|
186
|
-
const html = `
|
|
187
|
-
<item-list>
|
|
188
|
-
<!-- each items as item, i -->
|
|
189
|
-
<item-card>
|
|
190
|
-
<span>@[item]</span>
|
|
191
|
-
<!-- if i % 2 === 0 -->
|
|
192
|
-
<badge>Even</badge>
|
|
193
|
-
<!-- else -->
|
|
194
|
-
<badge secondary>Odd</badge>
|
|
195
|
-
<!-- /if -->
|
|
196
|
-
</item-card>
|
|
197
|
-
<!-- /each -->
|
|
198
|
-
</item-list>
|
|
199
|
-
`;
|
|
200
|
-
|
|
201
|
-
// State
|
|
202
|
-
window.$ = state({ items: ['Apple', 'Banana', 'Cherry'] });
|
|
203
|
-
|
|
204
|
-
// Trigger the bug
|
|
205
|
-
const container = document.querySelector('[vibe]');
|
|
206
|
-
|
|
207
|
-
// First replacement: works
|
|
208
|
-
container.innerHTML = html;
|
|
209
|
-
await new Promise(r => setTimeout(r, 100));
|
|
210
|
-
|
|
211
|
-
// Second replacement: works
|
|
212
|
-
container.innerHTML = html;
|
|
213
|
-
await new Promise(r => setTimeout(r, 100));
|
|
214
|
-
|
|
215
|
-
// Third replacement: branches.else becomes NULL
|
|
216
|
-
container.innerHTML = html;
|
|
217
|
-
await new Promise(r => setTimeout(r, 100));
|
|
218
|
-
|
|
219
|
-
// Result: Even/Odd badges fail to render in the third iteration
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
#### Observations
|
|
223
|
-
|
|
224
|
-
1. Cloning `branches` object during iteration (shallow copy) doesn't prevent the bug
|
|
225
|
-
2. The mutation happens BEFORE cloning, meaning the original template is corrupted
|
|
226
|
-
3. Setting a property trap on `branches.else` doesn't catch the mutation (already null when accessed)
|
|
227
|
-
4. Only affects conditionals inside iterations - standalone conditionals work fine
|
|
228
|
-
5. Only triggers with multiple innerHTML replacements - single replacement works
|
|
229
|
-
|
|
230
|
-
#### Affected Patterns
|
|
231
|
-
|
|
232
|
-
This bug only affects:
|
|
233
|
-
- Replacing innerHTML multiple times with identical HTML containing iterations + conditionals
|
|
234
|
-
- Demo infrastructure like tutorial.js that re-renders on state changes
|
|
235
|
-
- Not representative of typical usage patterns
|
|
236
|
-
|
|
237
|
-
Does NOT affect:
|
|
238
|
-
- Normal reactivity (state changes)
|
|
239
|
-
- Single innerHTML replacement
|
|
240
|
-
- Iterations without conditionals
|
|
241
|
-
- Conditionals outside iterations
|
|
242
|
-
- Incremental DOM mutations (appendChild, insertBefore, etc.)
|
|
243
|
-
|
|
244
|
-
#### Workaround
|
|
245
|
-
|
|
246
|
-
Avoid multiple innerHTML replacements on parents containing iteration+conditional templates. Instead:
|
|
247
|
-
1. Use incremental DOM APIs (appendChild, createElement)
|
|
248
|
-
2. Replace innerHTML once at initialization only
|
|
249
|
-
3. Use Vibe's normal reactivity for updates
|
|
250
|
-
4. Don't wrap demo content in `<tutorial>` that re-renders via innerHTML
|
|
251
|
-
|
|
252
|
-
#### Investigation Log
|
|
253
|
-
|
|
254
|
-
Debugging attempts (2026-01-25):
|
|
255
|
-
- ✓ Confirmed `branches.else` becomes `null` after 3rd innerHTML replacement
|
|
256
|
-
- ✓ Added deep cloning of branches object - didn't help (already null before clone)
|
|
257
|
-
- ✓ Added Object.defineProperty trap - didn't fire (already null)
|
|
258
|
-
- ✓ Checked parsing logic - correctly finds else comments
|
|
259
|
-
- ✗ Unable to identify where mutation occurs
|
|
260
|
-
- ✗ Unable to reproduce with simpler test case (needs tutorial.js pattern)
|
|
261
|
-
|
|
262
|
-
Likely related to:
|
|
263
|
-
- MutationObserver's removedNodes callback cleaning up references
|
|
264
|
-
- Template caching/reuse strategy in iterate.js
|
|
265
|
-
- Interaction between parse → clone → hydrate → render cycle
|
|
266
|
-
|
|
267
|
-
#### Resolution Path
|
|
268
|
-
|
|
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.
|
|
270
|
-
|
|
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.
|
|
272
|
-
|
|
273
|
-
This is acceptable technical debt since:
|
|
274
|
-
1. Edge case not representative of normal usage
|
|
275
|
-
2. Core reactivity (the 99% case) works correctly
|
|
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
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
## Proposed: `settled` Lifecycle Event (app quiescence)
|
|
282
|
-
|
|
283
|
-
**Status**: Proposal
|
|
284
|
-
**Priority**: Medium
|
|
285
|
-
**Category**: Core Runtime / Lifecycle
|
|
286
|
-
**Discovered**: 2026-06-16 (battle-brawlers: boss-unlock toast on the arena page)
|
|
287
|
-
|
|
288
|
-
### Problem
|
|
289
|
-
|
|
290
|
-
There is no event for *"the app has finished its initial boot/render churn and is now idle — run post-boot side-effects here."* The existing hooks don't cover it:
|
|
291
|
-
|
|
292
|
-
- **`ready`** (`PHASE_READY`, removes `[vibe-fouc]` + dispatches `vibe:ready`) fires after the **first** render — but that first render is the *dataless/initial* state. Async data (e.g. a socket-loaded game state) arrives *after* `ready` and triggers a second, often **multi-second** data-driven re-render. So `ready` is "first paint," not "settled."
|
|
293
|
-
- **`afterUpdate`** fires on **every** flush — it's a per-change signal, not a one-shot "we've quiesced" signal, and it fires *during* the churn.
|
|
294
|
-
|
|
295
|
-
Consequently, state set imperatively right after boot is unreliable. Concrete case: pushing a toast (`$.notifications.push(...)`) when game state loads — the toast is added before the heavy first data render paints, the main thread is blocked through its 4s auto-dismiss window, and it's removed unseen. "The heavy boot churn eats it."
|
|
296
|
-
|
|
297
|
-
### What it would accomplish
|
|
298
|
-
|
|
299
|
-
A one-time, post-quiescence hook so consumers can reliably run boot-time side-effects: toasts/announcements, focus management, scroll restoration, analytics "app interactive", third-party widget init — anything that must happen *after* the initial render storm rather than during it.
|
|
300
|
-
|
|
301
|
-
```js
|
|
302
|
-
$.on('settled', () => notify.success('…')); // lands; paints; lives its full duration
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### Proposed shape
|
|
306
|
-
|
|
307
|
-
`settled` fires **once per page load**, after: (a) Vibe's reactive flush queue has drained, and (b) the browser reports a free frame. Implementation sketch: when a flush completes with no further flush queued, schedule `requestIdleCallback(cb, { timeout })` (with a `setTimeout` fallback for Safari < 17); if still no flush was queued when it fires, dispatch `settled`.
|
|
308
|
-
|
|
309
|
-
**Honest caveat — it's a heuristic, not a guarantee.** Perpetual tickers make true quiescence undecidable (battle-brawlers runs a 1s `setInterval` clock that flushes forever). This is the same class of fuzziness as Playwright's `networkidle` ("no requests for 500ms" — which Playwright now discourages as flaky). `settled` should mean "drained + one idle frame," documented as best-effort. Vibe *can* reliably know its own flush queue is empty; only external async (sockets/timers) is fuzzy, and the idle check bounds that.
|
|
310
|
-
|
|
311
|
-
### Current workaround
|
|
312
|
-
|
|
313
|
-
Hand-roll the idle wait at the call site with `requestIdleCallback`. See `webdev/webdev-game-stack/pages/the-arena.html` (boss-unlock toast): defer the toast to `requestIdleCallback({ timeout })` so it lands after the arena's heavy first render. Works, but every consumer re-implements it and must know it's needed.
|
|
314
|
-
|
|
315
|
-
### Related (accurate precedents)
|
|
316
|
-
|
|
317
|
-
- **Angular** — `ApplicationRef.isStable` (Observable) / `NgZone.onStable`: emits when the app has no pending micro/macro tasks. This is the genuine *app-level quiescence* analog to `settled`.
|
|
318
|
-
- **Vue** — `nextTick()`: resolves after the current reactive flush → DOM updated. *Flush-level* (one flush), not full idle.
|
|
319
|
-
- **Svelte** — `tick()`: resolves after pending state is applied to the DOM. Also flush-level.
|
|
320
|
-
- **React** — no first-class app-settled signal; composed from `useEffect` + `requestIdleCallback`, or inferred via concurrent features (`useTransition` `isPending`, Suspense).
|
|
321
|
-
|
|
322
|
-
**Not this:** `onMount` (Svelte) / `onMounted` (Vue) / `useEffect(fn, [])` (React) are **component-level "this component finished mounting"** hooks. They are *not* an app-wide quiescence signal and are not what `settled` proposes — don't model `settled` on them.
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
## Proposed: Surgical HMR in Compiled (Hyperspeed) Mode
|
|
327
|
-
|
|
328
|
-
**Status**: Proposal
|
|
329
|
-
**Priority**: High (developer experience)
|
|
330
|
-
**Category**: Compiler + Core Runtime (orchestrated by vite-plugin-vibe)
|
|
331
|
-
**Discovered**: 2026-06-19 (battle-brawlers `dev:compiled` loop — a one-line edit to a leaf component recompiles every page that inlines it)
|
|
332
|
-
|
|
333
|
-
### Problem
|
|
334
|
-
|
|
335
|
-
Runtime mode already has surgical component HMR: vite-plugin-vibe refetches the changed component's source, calls `$.renderComponent(...)` + `$.reconcile(...)`, and patches **only the live instances** of that component — no page-level work. State is preserved by reusing the component ids.
|
|
336
|
-
|
|
337
|
-
Compiled (hyperspeed) mode cannot do this today. The compiler **inlines components into each page's HTML and per-page manifest**, which *dissolves component identity*: the page manifest tree keeps the boundary as an anonymous positional node (`…→ component_1 → game-layout_0 → component_0 → …`) but nothing records *which source file produced it*. So the only invalidation the compiler can compute on a file save is the inverse graph — **component → dependent pages** — and it recompiles all of them.
|
|
338
|
-
|
|
339
|
-
Concrete cost: `AccountProgression.html` is included by `Sidebar` and `Overlay`, both of which live in `Layout`, which **every** page mounts. One leaf edit → all 27 pages + 27 manifests recompiled (~2.7s), versus runtime mode's ~instant single-boundary patch.
|
|
340
|
-
|
|
341
|
-
### Key realization — this is *not* "teach the compiler the runtime engine"
|
|
342
|
-
|
|
343
|
-
The instinct is that surgical HMR means giving the Rust compiler the engine's runtime awareness (component instantiation, props, slots, live DOM) just to know what to recompile. It does **not**. Two things are already in place:
|
|
344
|
-
|
|
345
|
-
1. **The runtime already ships the surgical engine** — `runtime/reconcile.js`, `runtime/component.js`, `runtime/component-cache.js`. Compiled pages boot the *same* runtime, so the patching machinery is already in the browser in compiled mode.
|
|
346
|
-
2. **The compiler already builds the static include-graph** ("Building dependency graph…") and already knows component identity *during* inlining — it simply discards it in the output.
|
|
347
|
-
|
|
348
|
-
What's missing is purely **emission + addressing**, not awareness:
|
|
349
|
-
|
|
350
|
-
- The compiler needs to **stamp each boundary with its `src`** and (Option A) **emit each component's compiled subtree as its own small unit** that pages *reference* rather than absorb.
|
|
351
|
-
- The runtime needs a **public boundary-swap API** built on the existing `reconcile` + `component-cache`, taking a recompiled unit + the live boundary and re-rendering just that subtree (live props/state stay the runtime's job — the compiler never needs them).
|
|
352
|
-
|
|
353
|
-
The compiler and the engine have *converged on the same structural model from opposite ends* (both understand `<component src>` and the component tree). Surgical HMR **connects** those two seams through the tooling layer; it does not merge them, and it does not give the compiler runtime DOM awareness. The compiler's job stays build-time-static (include graph + boundary metadata); the runtime's job stays runtime-dynamic (props, state, DOM).
|
|
354
|
-
|
|
355
|
-
### Layering constraint (non-negotiable)
|
|
356
|
-
|
|
357
|
-
`vite-plugin-vibe` is **sugar on top**. The dependency direction is one-way:
|
|
358
|
-
|
|
359
|
-
```
|
|
360
|
-
vite-plugin-vibe ──imports──▶ Vibe (compiler output + runtime public APIs)
|
|
361
|
-
Vibe ──never───▶ vite-plugin-vibe
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
- **Capabilities live in Vibe**, transport-agnostic:
|
|
365
|
-
- Compiler: emit boundary `src` identity; recompile a single component to its unit (`--watch` already incremental).
|
|
366
|
-
- Runtime: a public `swap(boundary, unit)` / `patchComponent(...)` API (reusing `reconcile`, `component-cache`, and component-id reuse for state preservation). Knows nothing about websockets or HMR.
|
|
367
|
-
- **Orchestration lives in the plugin**: watch the compiler's `--watch` output, carry the recompiled unit over the dev websocket, call the runtime's swap API. The plugin already sends a `vibe:component-update` event in runtime mode — the compiled path emits the *same* event with a compiled payload.
|
|
368
|
-
|
|
369
|
-
Neither the compiler nor the runtime gains a dependency on the plugin. The plugin remains optional sugar that delivers true HMR to **both** runtime dev and compiled dev.
|
|
370
|
-
|
|
371
|
-
### Two implementation paths
|
|
372
|
-
|
|
373
|
-
**Option A — per-component compiled units (full).**
|
|
374
|
-
Compiler tags `component_N` boundaries with `src` and modularizes manifests so each component's subtree is a separately-emittable, separately-loadable module; pages reference units instead of inlining them. Repurpose the dep graph from *component→pages* to *component→its unit*. An edit recompiles one small unit. Runtime swaps boundaries from the new unit. **Manifest modularization is the bulk of the work** (today a page manifest is one ~440KB blob).
|
|
375
|
-
|
|
376
|
-
**Option B — hybrid, recommended first (small lift).**
|
|
377
|
-
In dev only, the compiler **keeps the boundary markers + the call-site props/slot** in the compiled output instead of fully dissolving them. On a component edit, the plugin treats *just that boundary* like a **runtime** component — refetch source → `renderComponent` + `reconcile` that one subtree — and leaves the rest of the page compiled. This reuses vite-plugin-vibe's existing runtime-HMR path almost verbatim; the **only** compiler change is "don't throw away boundary + props/slot metadata in a dev compiled build." Gets ~90% of the benefit; the edited boundary is re-expanded by the runtime engine rather than from a compiled unit (fine for dev).
|
|
378
|
-
|
|
379
|
-
### Trade-offs
|
|
380
|
-
|
|
381
|
-
- **Dev/prod divergence.** Inlining *is* hyperspeed; a boundary-preserving dev build is structurally different from the inlined prod build, so compiled-specific bugs (cross-boundary manifest merging, minified inlined scripts) may not reproduce in surgical-dev. Every framework lives with this (dev HMR builds ≠ prod). Keep **both** modes — surgical dev for iteration, full-inline compile for fidelity passes — and pick per task.
|
|
382
|
-
- **Only internal edits go surgical.** Changing a *call site* (props/slot a page passes) still recompiles that page — same as runtime HMR re-mounting on a prop change.
|
|
383
|
-
|
|
384
|
-
### Related (precedents — all do exactly this shape)
|
|
385
|
-
|
|
386
|
-
- **Svelte** (`svelte-hmr` / Vite plugin): each component compiles to a module with an HMR proxy; a changed component hot-swaps its instances, preserving state where it can.
|
|
387
|
-
- **Vue SFC HMR**: compiler stamps `__hmrId`; runtime `rerender`/`reload` patches just that component's instances.
|
|
388
|
-
- **React Fast Refresh**: components compile with register/signature metadata + module boundaries; the refresh runtime re-renders only affected components, preserving hook state.
|
|
389
|
-
- **Vite HMR boundaries**: `import.meta.hot.accept` — module-scoped patching instead of full reload.
|
|
390
|
-
|
|
391
|
-
All three compile components into **independently-addressable, hot-swappable units with HMR boundaries**, then patch via a runtime accept API. This proposal is the same pattern adapted to Vibe's manifest model, with the orchestration deliberately kept in the (one-way-dependent) plugin.
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
## Future Proposals
|
|
396
|
-
|
|
397
|
-
*This section reserved for additional feature proposals*
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { platform, arch } from 'node:os';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const nativeDir = join(__dirname, '..', 'native');
|
|
11
|
-
const originalCwd = process.cwd();
|
|
12
|
-
|
|
13
|
-
// Map Node.js platform/arch to binary names
|
|
14
|
-
const getPlatformBinary = () => {
|
|
15
|
-
const os = platform();
|
|
16
|
-
const cpu = arch();
|
|
17
|
-
|
|
18
|
-
const platformMap = {
|
|
19
|
-
'darwin-arm64': 'vibe-compiler-darwin-arm64',
|
|
20
|
-
'darwin-x64': 'vibe-compiler-darwin-x64',
|
|
21
|
-
'linux-x64': 'vibe-compiler-linux-x64',
|
|
22
|
-
'linux-arm64': 'vibe-compiler-linux-arm64',
|
|
23
|
-
'win32-x64': 'vibe-compiler-win32-x64.exe',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const key = `${os}-${cpu}`;
|
|
27
|
-
const binary = platformMap[key];
|
|
28
|
-
|
|
29
|
-
if (!binary) {
|
|
30
|
-
console.error(`Unsupported platform: ${os}-${cpu}`);
|
|
31
|
-
console.error('Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return binary;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// The wrapper is what gets killed (Ctrl+C, a dev server's child.kill(), a
|
|
39
|
-
// process manager) — without forwarding, the compiler child survives as an
|
|
40
|
-
// orphan whose watch lock blocks every future `--watch` on the same output.
|
|
41
|
-
const wireLifecycle = (child) => {
|
|
42
|
-
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) =>
|
|
43
|
-
process.on(signal, () => child.kill(signal)),
|
|
44
|
-
);
|
|
45
|
-
process.on('exit', () => child.kill());
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const run = () => {
|
|
49
|
-
const binary = getPlatformBinary();
|
|
50
|
-
const binaryPath = join(nativeDir, binary);
|
|
51
|
-
let userArgs = process.argv.slice(2);
|
|
52
|
-
|
|
53
|
-
// Handle subcommand: vibe compile [args]
|
|
54
|
-
// If first arg is "compile" or "c", remove it (the binary doesn't need it)
|
|
55
|
-
if (userArgs[0] === 'compile' || userArgs[0] === 'c') {
|
|
56
|
-
userArgs = userArgs.slice(1);
|
|
57
|
-
} else if (userArgs[0] && !userArgs[0].startsWith('-')) {
|
|
58
|
-
// Unknown subcommand
|
|
59
|
-
console.error(`Unknown subcommand: ${userArgs[0]}`);
|
|
60
|
-
console.error('Available subcommands: compile (or c)');
|
|
61
|
-
console.error('Usage: vibe compile [options]');
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
// If no subcommand provided, default to compile behavior (pass args as-is)
|
|
65
|
-
|
|
66
|
-
// Check if native binary exists
|
|
67
|
-
if (!existsSync(binaryPath)) {
|
|
68
|
-
// Fall back to cargo run for development
|
|
69
|
-
console.info('[vibe-compile] Native binary not found, building with cargo...');
|
|
70
|
-
|
|
71
|
-
const srcDir = join(__dirname, '..', 'src');
|
|
72
|
-
|
|
73
|
-
if (!existsSync(join(srcDir, 'Cargo.toml'))) {
|
|
74
|
-
console.error('Neither native binary nor Rust source found.');
|
|
75
|
-
console.error(`Expected binary at: ${binaryPath}`);
|
|
76
|
-
console.error(`Or Cargo.toml at: ${srcDir}`);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Pass --cwd to tell the Rust binary the original working directory
|
|
81
|
-
const cargoArgs = ['run', '--release', '--', '--cwd', originalCwd, ...userArgs];
|
|
82
|
-
|
|
83
|
-
const cargo = spawn('cargo', cargoArgs, {
|
|
84
|
-
cwd: srcDir,
|
|
85
|
-
stdio: 'inherit',
|
|
86
|
-
});
|
|
87
|
-
wireLifecycle(cargo);
|
|
88
|
-
|
|
89
|
-
cargo.on('error', (err) => {
|
|
90
|
-
if (err.code === 'ENOENT') {
|
|
91
|
-
console.error('Cargo not found. Please install Rust: https://rustup.rs');
|
|
92
|
-
} else {
|
|
93
|
-
console.error('Failed to run cargo:', err.message);
|
|
94
|
-
}
|
|
95
|
-
process.exit(1);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
cargo.on('close', (code) => {
|
|
99
|
-
process.exit(code ?? 0);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Run the native binary (it runs in the user's cwd, so no --cwd needed)
|
|
106
|
-
const child = spawn(binaryPath, userArgs, {
|
|
107
|
-
stdio: 'inherit',
|
|
108
|
-
});
|
|
109
|
-
wireLifecycle(child);
|
|
110
|
-
|
|
111
|
-
child.on('error', (err) => {
|
|
112
|
-
console.error('Failed to run vibe-compiler:', err.message);
|
|
113
|
-
process.exit(1);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
child.on('close', (code) => {
|
|
117
|
-
process.exit(code ?? 0);
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
run();
|
package/compiler/native/.gitkeep
DELETED
|
File without changes
|
|
Binary file
|
|
Binary file
|