@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +97 -0
  3. package/ROADMAP.md +289 -0
  4. package/boot.js +45 -0
  5. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  6. package/compiler/src/Cargo.lock +719 -40
  7. package/compiler/src/Cargo.toml +11 -2
  8. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
  9. package/compiler/src/compiler/compile.rs +552 -175
  10. package/compiler/src/compiler/component_tagger.rs +234 -0
  11. package/compiler/src/compiler/iteration_optimizer.rs +351 -0
  12. package/compiler/src/compiler/js_analyzer.rs +572 -0
  13. package/compiler/src/compiler/manifest_builder.rs +251 -26
  14. package/compiler/src/compiler/mod.rs +5 -1
  15. package/compiler/src/compiler/state_extractor.rs +140 -25
  16. package/compiler/src/compiler/value_stamper.rs +579 -88
  17. package/compiler/src/compiler/watcher.rs +579 -0
  18. package/compiler/src/config.rs +51 -8
  19. package/compiler/src/main.rs +41 -28
  20. package/compiler/src/parser/html.rs +229 -118
  21. package/component.js +23 -11
  22. package/llms.txt +304 -0
  23. package/package.json +1 -17
  24. package/runtime/cleanup.js +4 -4
  25. package/runtime/component.js +98 -21
  26. package/runtime/conditionals.js +2 -2
  27. package/runtime/constants.js +2 -1
  28. package/runtime/index.js +152 -30
  29. package/runtime/iterate.js +27 -5
  30. package/runtime/parse.js +2 -1
  31. package/runtime/pre-compiled-iterations.js +153 -0
  32. package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +204 -132
  33. package/runtime/utils.js +2 -1
  34. package/test-results/.last-run.json +4 -0
  35. package/vibe.css +19 -0
  36. 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.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",
@@ -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('dehydrate')) {
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, `removing .${cleanName} class from ${elements.length} elements`, debug);
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
- `removing [${cleanName}] attribute from ${elements.length} elements`,
67
+ `Removing [${cleanName}] attribute from ${elements.length} ${elements.length === 1 ? 'element' : 'elements'}`,
68
68
  debug,
69
69
  );
70
70
  }
@@ -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
- import { generateComponentId, executeComponentScript } from './component-state.js';
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="component"> elements
58
- const componentScripts = temp.querySelectorAll('script[type="component"]');
59
- componentScripts.forEach((script) => {
60
- const scriptContent = script.textContent?.trim() || '';
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
- // Generate component ID
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
- // Tag script element
67
- script.setAttribute('data-vibe-component-id', componentId);
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
- // Stop if we hit another component script
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.setAttribute('data-vibe-component-id', componentId);
158
+ rewriteThisBindings(sibling);
77
159
  sibling = sibling.nextElementSibling;
78
160
  }
79
161
 
80
- // Execute component script to get state
81
- const componentState = executeComponentScript(scriptContent);
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
@@ -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 activeBranch is undefined)
62
- if (node.runtime.activeBranch === undefined) {
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;
@@ -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 (hyperspeed.js)
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