@ape-egg/vibe 1.3.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/README.md +97 -0
- package/ROADMAP.md +289 -0
- package/boot.js +45 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +719 -40
- package/compiler/src/Cargo.toml +11 -2
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +552 -175
- package/compiler/src/compiler/component_tagger.rs +234 -0
- package/compiler/src/compiler/iteration_optimizer.rs +351 -0
- package/compiler/src/compiler/js_analyzer.rs +572 -0
- package/compiler/src/compiler/manifest_builder.rs +251 -26
- package/compiler/src/compiler/mod.rs +5 -1
- package/compiler/src/compiler/state_extractor.rs +140 -25
- package/compiler/src/compiler/value_stamper.rs +579 -88
- package/compiler/src/compiler/watcher.rs +579 -0
- package/compiler/src/config.rs +51 -8
- package/compiler/src/main.rs +41 -28
- package/compiler/src/parser/html.rs +229 -118
- package/component.js +23 -11
- package/llms.txt +304 -0
- package/package.json +1 -17
- package/runtime/cleanup.js +4 -4
- package/runtime/component.js +98 -21
- package/runtime/conditionals.js +2 -2
- package/runtime/constants.js +2 -1
- package/runtime/index.js +152 -30
- package/runtime/iterate.js +27 -5
- package/runtime/parse.js +2 -1
- package/runtime/pre-compiled-iterations.js +153 -0
- package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +204 -132
- package/runtime/utils.js +2 -1
- package/test-results/.last-run.json +4 -0
- package/vibe.css +19 -0
- package/runtime/component-state.js +0 -63
package/llms.txt
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Vibe - Complete Documentation
|
|
2
|
+
|
|
3
|
+
> Runtime-first reactivity. No virtual DOM. No build step.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Vibe is a lightweight reactive library that uses Proxy-based state and MutationObserver for fine-grained DOM updates. It works directly in the browser with zero compilation required.
|
|
8
|
+
|
|
9
|
+
**Key characteristics:**
|
|
10
|
+
- Proxy-based reactive state (`window.$`)
|
|
11
|
+
- Deep reactivity (nested mutations automatically trigger updates)
|
|
12
|
+
- Surgical DOM updates (only affected elements re-render)
|
|
13
|
+
- MutationObserver for dynamic element tracking
|
|
14
|
+
- Works with vanilla HTML - no special file format
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ape-egg/vibe
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import state from "@ape-egg/vibe";
|
|
27
|
+
window.$ = state({ name: "World", count: 0 });
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<h1>Hello, @[name]!</h1>
|
|
31
|
+
<button onclick="$.count++">Clicked @[count] times</button>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core Syntax
|
|
35
|
+
|
|
36
|
+
### Reactive Bindings
|
|
37
|
+
|
|
38
|
+
Use `@[property]` syntax anywhere in HTML or CSS:
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<!-- Text content -->
|
|
42
|
+
<div>@[firstName]</div>
|
|
43
|
+
|
|
44
|
+
<!-- Expressions -->
|
|
45
|
+
<div>@[firstName + ' ' + lastName]</div>
|
|
46
|
+
<div>@[count * 2]</div>
|
|
47
|
+
|
|
48
|
+
<!-- Attributes -->
|
|
49
|
+
<input value="@[inputValue]">
|
|
50
|
+
<button disabled="@[isLoading]">Submit</button>
|
|
51
|
+
|
|
52
|
+
<!-- CSS -->
|
|
53
|
+
<style>
|
|
54
|
+
.box { background: @[themeColor]; }
|
|
55
|
+
</style>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
⚠️ **Important**: Bindings are evaluated using `new Function()`. Do not bind untrusted user input.
|
|
59
|
+
|
|
60
|
+
### State Access
|
|
61
|
+
|
|
62
|
+
State is accessed globally via `window.$`:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Read
|
|
66
|
+
console.log($.firstName);
|
|
67
|
+
|
|
68
|
+
// Write (triggers re-render)
|
|
69
|
+
$.firstName = "John";
|
|
70
|
+
|
|
71
|
+
// Increment
|
|
72
|
+
$.count++;
|
|
73
|
+
|
|
74
|
+
// Deep mutations (also trigger re-render)
|
|
75
|
+
$.user.profile.name = "Alice";
|
|
76
|
+
$.todos[2].completed = true;
|
|
77
|
+
$.config.theme.colors.primary = "#007bff";
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Vibe uses recursive proxies to detect changes at any nesting level automatically.
|
|
81
|
+
|
|
82
|
+
## Control Flow
|
|
83
|
+
|
|
84
|
+
### Iteration
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!-- each items as item -->
|
|
88
|
+
<li>@[item]</li>
|
|
89
|
+
<!-- /each -->
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
With index:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<!-- each items as item, index -->
|
|
96
|
+
<li>@[index]: @[item]</li>
|
|
97
|
+
<!-- /each -->
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Nested Iteration
|
|
101
|
+
|
|
102
|
+
Use dot paths for nested arrays:
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<!-- each categories as category -->
|
|
106
|
+
<h2>@[category.name]</h2>
|
|
107
|
+
<!-- each category.items as item -->
|
|
108
|
+
<span>@[item.name]</span>
|
|
109
|
+
<!-- /each -->
|
|
110
|
+
<!-- /each -->
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Conditionals
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<!-- if isLoggedIn -->
|
|
117
|
+
<span>Welcome, @[username]!</span>
|
|
118
|
+
<!-- else -->
|
|
119
|
+
<span>Please log in</span>
|
|
120
|
+
<!-- /if -->
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Conditionals can be nested inside iterations and vice versa.
|
|
124
|
+
|
|
125
|
+
## Special Attributes
|
|
126
|
+
|
|
127
|
+
### Dehydrate
|
|
128
|
+
|
|
129
|
+
Skip reactive processing for an element and its children:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<code dehydrate>@[this] displays literally, not parsed</code>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use cases:
|
|
136
|
+
- Displaying `@[...]` syntax in documentation
|
|
137
|
+
- Static content that shouldn't be reactive
|
|
138
|
+
- Performance optimization for large static sections
|
|
139
|
+
|
|
140
|
+
### Boolean Attributes
|
|
141
|
+
|
|
142
|
+
Attributes not in the value whitelist are removed when falsy:
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<button disabled="@[isLoading]">Submit</button>
|
|
146
|
+
<!-- When isLoading is false, disabled attribute is removed entirely -->
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Events
|
|
150
|
+
|
|
151
|
+
Use standard inline event handlers:
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<button onclick="$.count++">Increment</button>
|
|
155
|
+
<input oninput="$.text = this.value">
|
|
156
|
+
<form onsubmit="event.preventDefault(); handleSubmit()">
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Styling
|
|
160
|
+
|
|
161
|
+
### CSS Bindings
|
|
162
|
+
|
|
163
|
+
Reactive values work inside `<style>` tags:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<style>
|
|
167
|
+
.box {
|
|
168
|
+
background: @[backgroundColor];
|
|
169
|
+
color: @[textColor];
|
|
170
|
+
width: @[width]px;
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Preventing FOUC
|
|
176
|
+
|
|
177
|
+
Hide content until hydration completes:
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<body style="visibility: hidden;">
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or use the included CSS:
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
|
|
187
|
+
<body vibe-fouc>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Dynamic Elements
|
|
191
|
+
|
|
192
|
+
Elements added via JavaScript are automatically hydrated through MutationObserver:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
const div = document.createElement('div');
|
|
196
|
+
div.innerHTML = '<span>Hello, @[name]!</span>';
|
|
197
|
+
document.body.appendChild(div);
|
|
198
|
+
// Automatically becomes reactive
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## API Reference
|
|
202
|
+
|
|
203
|
+
### `state(initialState, afterUpdate?)`
|
|
204
|
+
|
|
205
|
+
Creates reactive state and initializes the framework.
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
import state from "@ape-egg/vibe";
|
|
209
|
+
|
|
210
|
+
window.$ = state(
|
|
211
|
+
{ count: 0, user: { name: "Alice" } },
|
|
212
|
+
(newState, oldState) => {
|
|
213
|
+
console.log("State updated:", newState);
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Parameters:**
|
|
219
|
+
- `initialState` - Object containing initial state values
|
|
220
|
+
- `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
|
|
221
|
+
|
|
222
|
+
**Returns:** Proxy object for reactive state access
|
|
223
|
+
|
|
224
|
+
## Scoped Variables
|
|
225
|
+
|
|
226
|
+
Inside `<!-- each -->` blocks, these variables are available:
|
|
227
|
+
- `item` (or custom name) - current array element
|
|
228
|
+
- `index` (or custom name) - current index
|
|
229
|
+
- Parent state remains accessible via `$`
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<!-- each users as user, i -->
|
|
233
|
+
<div>@[i]: @[user.name] (total: @[users.length])</div>
|
|
234
|
+
<!-- /each -->
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Scoped State (Internal)
|
|
238
|
+
|
|
239
|
+
Each iteration instance stores a `scopedState` - a Proxy wrapper that provides access to both local variables (`item`, `index`) and global state. This enables reactive updates inside iterations:
|
|
240
|
+
|
|
241
|
+
```html
|
|
242
|
+
<!-- each menuItems as item -->
|
|
243
|
+
<a completed="@[tutorialProgress[item.id]]">
|
|
244
|
+
@[item.label]
|
|
245
|
+
</a>
|
|
246
|
+
<!-- /each -->
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
When `tutorialProgress` changes, Vibe:
|
|
250
|
+
1. Finds affected elements inside iteration instances
|
|
251
|
+
2. Uses that instance's `scopedState` for evaluation
|
|
252
|
+
3. Can access both `item` (local) and `tutorialProgress` (global)
|
|
253
|
+
4. Updates only the affected elements without re-rendering the entire iteration
|
|
254
|
+
|
|
255
|
+
## Architecture
|
|
256
|
+
|
|
257
|
+
Vibe consists of these core modules:
|
|
258
|
+
|
|
259
|
+
- **state.js** - Proxy-based reactive state container with recursive proxies for deep reactivity
|
|
260
|
+
- **parse.js** - DOM parser that finds `@[...]` bindings
|
|
261
|
+
- **link.js** - Maps elements to parsed tree nodes
|
|
262
|
+
- **hydrate.js** - Updates DOM with current state values (supports scoped state for iterations)
|
|
263
|
+
- **affected.js** - Determines which elements need updating (walks iteration instances with scoped state)
|
|
264
|
+
- **iterate.js** - Array rendering with efficient diffing (stores scoped state per instance)
|
|
265
|
+
- **conditionals.js** - Conditional block rendering
|
|
266
|
+
|
|
267
|
+
## How It Works
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
1. state() initializes the Proxy and framework
|
|
271
|
+
2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
|
|
272
|
+
3. link.js maps elements to the parsed tree
|
|
273
|
+
4. hydrate.js replaces bindings with values
|
|
274
|
+
5. iterate.js renders <!-- each --> loops
|
|
275
|
+
6. conditionals.js renders <!-- if --> blocks
|
|
276
|
+
7. MutationObserver watches for new elements
|
|
277
|
+
8. On state change: affected.js finds changed elements → hydrate.js updates them
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Current Limitations
|
|
281
|
+
|
|
282
|
+
- **No computed values**: Derived state must be calculated manually
|
|
283
|
+
- **No two-way binding sugar**: Must wire input events manually
|
|
284
|
+
- **Expression security**: `new Function()` evaluation - don't bind untrusted input
|
|
285
|
+
|
|
286
|
+
## Best Practices
|
|
287
|
+
|
|
288
|
+
1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
|
|
289
|
+
2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
|
|
290
|
+
3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
|
|
291
|
+
4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
|
|
292
|
+
5. **Mutate freely**: Deep reactivity means `$.user.name = "New"` just works - no spread operators needed
|
|
293
|
+
|
|
294
|
+
## Browser Support
|
|
295
|
+
|
|
296
|
+
Modern browsers with:
|
|
297
|
+
- Proxy (ES6)
|
|
298
|
+
- MutationObserver
|
|
299
|
+
- ES Modules
|
|
300
|
+
|
|
301
|
+
## Resources
|
|
302
|
+
|
|
303
|
+
- **Homepage**: https://vibe.korte.kim
|
|
304
|
+
- **npm**: https://www.npmjs.com/package/@ape-egg/vibe
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ape-egg/vibe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime-first reactivity with optional compiler",
|
|
6
6
|
"main": "index.js",
|
|
@@ -14,22 +14,6 @@
|
|
|
14
14
|
"bin": {
|
|
15
15
|
"vibe": "./compiler/bin/vibe-compile.js"
|
|
16
16
|
},
|
|
17
|
-
"files": [
|
|
18
|
-
"index.js",
|
|
19
|
-
"component.js",
|
|
20
|
-
"vibe.css",
|
|
21
|
-
"runtime/",
|
|
22
|
-
"compiler/bin/",
|
|
23
|
-
"compiler/native/",
|
|
24
|
-
"compiler/src/Cargo.lock",
|
|
25
|
-
"compiler/src/Cargo.toml",
|
|
26
|
-
"compiler/src/compiler/",
|
|
27
|
-
"compiler/src/config.rs",
|
|
28
|
-
"compiler/src/main.rs",
|
|
29
|
-
"compiler/src/parser/",
|
|
30
|
-
"README.md",
|
|
31
|
-
"CHANGELOG.md"
|
|
32
|
-
],
|
|
33
17
|
"keywords": [
|
|
34
18
|
"reactive",
|
|
35
19
|
"framework",
|
package/runtime/cleanup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { debugLog } from './debug.js';
|
|
2
|
-
import { PHASE_READY, FOUC_CLASS_OR_ATTR } from './constants.js';
|
|
2
|
+
import { PHASE_READY, FOUC_CLASS_OR_ATTR, DEHYDRATE_CLASS_OR_ATTR } from './constants.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Check if all Vibe processing is complete and cleanup can run
|
|
@@ -20,7 +20,7 @@ export const shouldCleanup = (rootElement) => {
|
|
|
20
20
|
// Check if this text node is inside a dehydrated element
|
|
21
21
|
let parent = node.parentElement;
|
|
22
22
|
while (parent && parent !== rootElement) {
|
|
23
|
-
if (parent.hasAttribute(
|
|
23
|
+
if (parent.hasAttribute(DEHYDRATE_CLASS_OR_ATTR) || parent.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
24
24
|
return NodeFilter.FILTER_REJECT; // Skip dehydrated content
|
|
25
25
|
}
|
|
26
26
|
parent = parent.parentElement;
|
|
@@ -57,14 +57,14 @@ export const cleanup = (rootElement, debug = false) => {
|
|
|
57
57
|
// Remove class from all elements in document that have it
|
|
58
58
|
const elements = document.querySelectorAll(`.${cleanName}`);
|
|
59
59
|
elements.forEach((el) => el.classList.remove(cleanName));
|
|
60
|
-
debugLog(PHASE_READY, `
|
|
60
|
+
debugLog(PHASE_READY, `Removing .${cleanName} class from ${elements.length} ${elements.length === 1 ? 'element' : 'elements'}`, debug);
|
|
61
61
|
} else {
|
|
62
62
|
// Remove attribute from all elements in document that have it
|
|
63
63
|
const elements = document.querySelectorAll(`[${cleanName}]`);
|
|
64
64
|
elements.forEach((el) => el.removeAttribute(cleanName));
|
|
65
65
|
debugLog(
|
|
66
66
|
PHASE_READY,
|
|
67
|
-
`
|
|
67
|
+
`Removing [${cleanName}] attribute from ${elements.length} ${elements.length === 1 ? 'element' : 'elements'}`,
|
|
68
68
|
debug,
|
|
69
69
|
);
|
|
70
70
|
}
|
package/runtime/component.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { debugLog } from './debug.js';
|
|
2
|
-
import { PHASE_FETCH } from './constants.js';
|
|
2
|
+
import { PHASE_FETCH, DEHYDRATE_CLASS_OR_ATTR } from './constants.js';
|
|
3
3
|
import { evalInScope } from './utils.js';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// Deterministic component counter
|
|
6
|
+
let componentCounter = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate unique component ID
|
|
10
|
+
* Uses deterministic counter: _c0, _c1, _c2, etc.
|
|
11
|
+
*/
|
|
12
|
+
export const generateComponentId = () => {
|
|
13
|
+
return `_c${componentCounter++}`;
|
|
14
|
+
};
|
|
5
15
|
|
|
6
16
|
// Helper to escape regex special characters
|
|
7
17
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -32,6 +42,19 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
32
42
|
|
|
33
43
|
// Process just the first element - MutationObserver will trigger next call
|
|
34
44
|
const el = componentElements[0];
|
|
45
|
+
|
|
46
|
+
// Skip if component is dehydrated (vibe-dehydrate attribute or class)
|
|
47
|
+
if (el.hasAttribute(DEHYDRATE_CLASS_OR_ATTR) || el.classList.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
48
|
+
// Skip this component and continue to next
|
|
49
|
+
if (componentElements.length > 1) {
|
|
50
|
+
// Process next component
|
|
51
|
+
processComponent(rootElement, onComplete, config);
|
|
52
|
+
} else {
|
|
53
|
+
if (onComplete) onComplete();
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
const src = el.getAttribute('src');
|
|
36
59
|
|
|
37
60
|
// Capture children and props before fetching
|
|
@@ -54,39 +77,93 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
54
77
|
const temp = document.createElement('div');
|
|
55
78
|
temp.innerHTML = html;
|
|
56
79
|
|
|
57
|
-
// Process any <script type="
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
// Process any <script type="module"> elements
|
|
81
|
+
const moduleScripts = temp.querySelectorAll('script[type="module"]');
|
|
82
|
+
moduleScripts.forEach((script) => {
|
|
83
|
+
let scriptContent = script.textContent?.trim() || '';
|
|
61
84
|
if (!scriptContent) return;
|
|
62
85
|
|
|
63
|
-
//
|
|
86
|
+
// Strip import statements (we provide component() manually)
|
|
87
|
+
// Remove lines like: import component from '...';
|
|
88
|
+
scriptContent = scriptContent.replace(/import\s+\w+\s+from\s+['"][^'"]+['"];?\s*/g, '');
|
|
89
|
+
|
|
90
|
+
// Generate component ID for this instance
|
|
64
91
|
const componentId = generateComponentId();
|
|
65
92
|
|
|
66
|
-
//
|
|
67
|
-
|
|
93
|
+
// Provide a component() function that registers state for this component
|
|
94
|
+
const componentFn = (state) => {
|
|
95
|
+
// Register component state in both places:
|
|
96
|
+
// 1. __vibeComponents registry (for pre-boot components)
|
|
97
|
+
if (!window.__vibeComponents) {
|
|
98
|
+
window.__vibeComponents = {};
|
|
99
|
+
}
|
|
100
|
+
window.__vibeComponents[componentId] = state;
|
|
101
|
+
|
|
102
|
+
// 2. Directly in window.$ (the reactive proxy) for post-boot components
|
|
103
|
+
if (window.$) {
|
|
104
|
+
window.$[componentId] = state;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Tag all siblings (everything after the script in this component)
|
|
108
|
+
let sibling = script.nextElementSibling;
|
|
109
|
+
while (sibling) {
|
|
110
|
+
// Stop if we hit another module script
|
|
111
|
+
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
sibling.setAttribute('data-vibe-component-id', componentId);
|
|
115
|
+
sibling = sibling.nextElementSibling;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Execute script with component() function in scope
|
|
120
|
+
// Use Function constructor to provide 'component' as a parameter
|
|
121
|
+
try {
|
|
122
|
+
const executeFn = new Function('component', scriptContent);
|
|
123
|
+
executeFn(componentFn);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn('[vibe] Failed to execute component script:', e);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Rewrite this.property to componentId.property in siblings
|
|
129
|
+
// This allows the runtime to resolve component-scoped bindings
|
|
130
|
+
const rewriteThisBindings = (element) => {
|
|
131
|
+
const thisRegex = /@\[this\.(\w+)\]/g;
|
|
132
|
+
|
|
133
|
+
// Rewrite in text nodes
|
|
134
|
+
Array.from(element.childNodes).forEach(node => {
|
|
135
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('@[this.')) {
|
|
136
|
+
node.textContent = node.textContent.replace(thisRegex, `@[${componentId}.$1]`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Rewrite in attributes
|
|
141
|
+
Array.from(element.attributes || []).forEach(attr => {
|
|
142
|
+
if (attr.value.includes('@[this.')) {
|
|
143
|
+
attr.value = attr.value.replace(thisRegex, `@[${componentId}.$1]`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Recurse into children
|
|
148
|
+
Array.from(element.children).forEach(child => {
|
|
149
|
+
rewriteThisBindings(child);
|
|
150
|
+
});
|
|
151
|
+
};
|
|
68
152
|
|
|
69
|
-
// Tag following siblings with this component ID
|
|
70
153
|
let sibling = script.nextElementSibling;
|
|
71
154
|
while (sibling) {
|
|
72
|
-
|
|
73
|
-
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'component') {
|
|
155
|
+
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
|
|
74
156
|
break;
|
|
75
157
|
}
|
|
76
|
-
sibling
|
|
158
|
+
rewriteThisBindings(sibling);
|
|
77
159
|
sibling = sibling.nextElementSibling;
|
|
78
160
|
}
|
|
79
161
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Register in global state
|
|
84
|
-
if (window.$) {
|
|
85
|
-
window.$[componentId] = componentState;
|
|
86
|
-
}
|
|
162
|
+
// Remove script from temp (we executed it manually)
|
|
163
|
+
script.remove();
|
|
87
164
|
});
|
|
88
165
|
|
|
89
|
-
// Get transformed HTML from temp container
|
|
166
|
+
// Get transformed HTML from temp container (scripts removed)
|
|
90
167
|
let transformedHtml = temp.innerHTML;
|
|
91
168
|
|
|
92
169
|
// Replace props
|
package/runtime/conditionals.js
CHANGED
|
@@ -58,8 +58,8 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Remove original template nodes from DOM (between start and end comments)
|
|
61
|
-
// Only do this on first render (when
|
|
62
|
-
if (node.runtime.
|
|
61
|
+
// Only do this on first render (when template hasn't been removed yet)
|
|
62
|
+
if (!node.runtime.templateRemoved) {
|
|
63
63
|
let currentNode = startComment.nextSibling;
|
|
64
64
|
while (currentNode && currentNode !== endComment) {
|
|
65
65
|
const nextNode = currentNode.nextSibling;
|
package/runtime/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Debug logger name
|
|
2
2
|
export const DEBUGGER_NAME = '[vibe-debug]:';
|
|
3
3
|
export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
|
|
4
|
+
export const DEHYDRATE_CLASS_OR_ATTR = 'vibe-dehydrate'; // Class or attribute used to skip reactive processing
|
|
4
5
|
|
|
5
6
|
// Lifecycle phase names for debug logging
|
|
6
7
|
// ONE-OFF operations (run once during initialization)
|
|
@@ -17,7 +18,7 @@ export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (condi
|
|
|
17
18
|
export const PHASE_FETCH = 'Fetched'; // Loads <component> content (component.js)
|
|
18
19
|
export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
|
|
19
20
|
export const PHASE_MUTATE = 'Mutation'; // DOM mutations detected by observer (index.js)
|
|
20
|
-
export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (
|
|
21
|
+
export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (pre-compiled-manifest.js)
|
|
21
22
|
|
|
22
23
|
// Elements that should not have reactive bindings
|
|
23
24
|
// Note: COMPONENT is NOT in this list - inline component wrappers need to be parsed
|