@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.
Files changed (57) hide show
  1. package/README.md +14 -4
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +10 -15
  6. package/llms.txt +8 -6
  7. package/package.json +19 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +312 -99
  11. package/runtime/conditionals.js +111 -14
  12. package/runtime/debug.js +24 -0
  13. package/runtime/dispatch.js +172 -0
  14. package/runtime/hydrate.js +251 -111
  15. package/runtime/index.js +180 -71
  16. package/runtime/iterate.js +125 -50
  17. package/runtime/iteration-utils.js +59 -8
  18. package/runtime/manifest.js +77 -2
  19. package/runtime/parse.js +69 -5
  20. package/runtime/pre-compiled-iterations.js +19 -6
  21. package/runtime/pre-compiled-manifest.js +13 -4
  22. package/runtime/staging.js +153 -0
  23. package/runtime/state.js +31 -0
  24. package/runtime/tracking.js +173 -0
  25. package/runtime/utils.js +155 -78
  26. package/spa.js +77 -14
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1196
  29. package/ROADMAP.md +0 -397
  30. package/compiler/bin/vibe-compile.js +0 -121
  31. package/compiler/native/.gitkeep +0 -0
  32. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  33. package/compiler/native/vibe-compiler-linux-x64 +0 -0
  34. package/compiler/src/Cargo.lock +0 -2023
  35. package/compiler/src/Cargo.toml +0 -38
  36. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
  37. package/compiler/src/compiler/binding_case.rs +0 -88
  38. package/compiler/src/compiler/compile.rs +0 -2880
  39. package/compiler/src/compiler/component_tagger.rs +0 -469
  40. package/compiler/src/compiler/iteration_optimizer.rs +0 -455
  41. package/compiler/src/compiler/js_analyzer.rs +0 -715
  42. package/compiler/src/compiler/manifest_builder.rs +0 -693
  43. package/compiler/src/compiler/mod.rs +0 -16
  44. package/compiler/src/compiler/name_binding_protect.rs +0 -207
  45. package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
  46. package/compiler/src/compiler/spa.rs +0 -477
  47. package/compiler/src/compiler/state_extractor.rs +0 -263
  48. package/compiler/src/compiler/value_stamper.rs +0 -921
  49. package/compiler/src/compiler/watcher.rs +0 -1278
  50. package/compiler/src/config.rs +0 -279
  51. package/compiler/src/main.rs +0 -358
  52. package/compiler/src/parser/element.rs +0 -96
  53. package/compiler/src/parser/html.rs +0 -1004
  54. package/compiler/src/parser/mod.rs +0 -8
  55. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  56. package/runtime/scope.js +0 -50
  57. package/test-results/.last-run.json +0 -4
package/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();
File without changes