@ape-egg/vibe 1.1.2 → 1.2.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 CHANGED
@@ -1,5 +1,83 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-02-03
4
+
5
+ ### Added
6
+
7
+ - **Component state isolation**: Components now have their own isolated state using `<script type="component">` blocks
8
+ - Each component gets a unique ID (`_cTIMESTAMP_RANDOM`) automatically generated and stored in `data-vibe-component-id` attribute
9
+ - Component state lives at `$[componentId].property` in the global state object
10
+ - Variable declarations (`let count = 0`) are automatically transformed to property assignments (`this.count = 0`)
11
+ - Clean separation between global state and component-specific state
12
+ - Example component (subject to change!):
13
+ ```html
14
+ <script type="component">
15
+ let count = 0;
16
+ let increment = () => { this.count++; };
17
+ </script>
18
+ <button onclick="this.increment()">Clicked @[this.count] times</button>
19
+ ```
20
+ - **`this.property` syntax**: Reference component-scoped state from anywhere inside a component
21
+ - Works in bindings: `@[this.count]`
22
+ - Works in attributes: `value="@[this.inputValue]"`
23
+ - Works in event handlers: `onclick="this.increment()"`
24
+ - Works in name bindings: `@[this.iconName]`
25
+ - Works in conditionals: `<!-- if this.isVisible -->`
26
+ - Works in iterations: `<!-- each this.items as item -->`
27
+ - Runtime automatically resolves `this.property` → `$['componentId'].property`
28
+ - **Component script execution**: `<script type="component">` blocks execute in their own scope
29
+ - Scripts run when component HTML is fetched (runtime `<component>` resolution)
30
+ - State is registered in global `$` object under component ID
31
+ - All siblings after the script tag inherit the component ID via `data-vibe-component-id` attribute
32
+ - Multiple component scripts in same HTML create separate component instances with unique IDs
33
+ - **Props and slots integration**: Component state works seamlessly with existing `<component>` features
34
+ - Props can set component state: `<component src="/card.html" theme="@[userTheme]">`
35
+ - Props work with `this.` references: `@[this.theme]` inside card.html
36
+ - Slots work inside component-scoped HTML
37
+ - Components can be nested with isolated state at each level
38
+
39
+ ### Changed
40
+
41
+ - **Component ID tagging**: DOM elements are now tagged with `data-vibe-component-id` during component processing (previously only scripts had this attribute)
42
+ - All siblings after a `<script type="component">` get tagged with the same component ID
43
+ - Tagging stops when hitting another component script or end of HTML
44
+ - Enables `this.property` resolution in any context (bindings, events, conditionals, iterations)
45
+ - **Event handler rewriting**: Event handlers with `this.property` are now rewritten at parse time
46
+ - `onclick="this.increment()"` → `onclick="$['_c123_abc'].increment()"`
47
+ - DOM properties (like `this.value`, `this.checked`) are preserved and not rewritten
48
+ - Prevents conflicts between component state access and native DOM properties
49
+
50
+ ### Technical Details
51
+
52
+ - **Component state lifecycle**:
53
+ 1. `<component src="/path.html">` fetches HTML
54
+ 2. HTML is parsed in temporary container
55
+ 3. `<script type="component">` blocks are found and executed
56
+ 4. Each script generates unique component ID
57
+ 5. Script and following siblings are tagged with `data-vibe-component-id`
58
+ 6. Component state is registered at `$[componentId]`
59
+ 7. Props are applied (with `this.` reference rewriting)
60
+ 8. Slots are replaced
61
+ 9. Transformed HTML replaces `<component>` element
62
+ 10. MutationObserver triggers reactive hydration with component context
63
+ - **`this.property` resolution**: Helper function `resolveThisPath()` in utils.js walks up DOM tree to find nearest `data-vibe-component-id`, then rewrites path from `this.property` → `componentId.property`
64
+ - **Expression evaluation**: `evalInScope()` in utils.js handles both global (`$.property`) and component-scoped (`$['componentId'].property`) state access, with case-insensitive fallback for HTML-lowercased attribute names
65
+
66
+ ---
67
+
68
+ ## [1.1.3] - 2026-02-02
69
+
70
+ ### Added
71
+
72
+ - **MutationObserver performance optimization**: Fast filter with short-circuit evaluation
73
+ - New `shouldProcessNode()` function checks for Vibe syntax before expensive processing
74
+ - Short-circuits on first match: most Vibe nodes contain `@[`, so check exits immediately
75
+ - Filters out third-party framework mutations (React, Vue, etc.) with cheap string operations
76
+ - Only walks DOM tree for nodes that actually contain Vibe syntax (`@[`, `<!-- each`, `<!-- if`, `<component>`)
77
+ - Enables efficient coexistence with other frameworks on the same page
78
+
79
+ ---
80
+
3
81
  ## [1.1.2] - 2026-02-01
4
82
 
5
83
  ### Fixed
package/README.md CHANGED
@@ -18,20 +18,18 @@ The core reactive runtime. Works directly in the browser without any build tools
18
18
 
19
19
  ```html
20
20
  <html>
21
- <head>
22
- <link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
23
- <script type="module">
24
- import state from "@ape-egg/vibe";
25
- // Attaches to element with attribute "vibe" by default
26
- window.$ = state({ name: "World", count: 0 });
27
- </script>
28
- </head>
29
- <body vibe>
30
-
31
- <h1>Hello, @[name]!</h1>
32
- <button onclick="$.count++">Clicked @[count] times</button>
33
-
34
- </body>
21
+ <head>
22
+ <link rel="stylesheet" href="./node_modules/@ape-egg/vibe/vibe.css" />
23
+ <script type="module">
24
+ import state from './node_modules/@ape-egg/vibe/runtime/index.js';
25
+ // Attaches to element with attribute "vibe" by default
26
+ window.$ = state({ name: 'World', count: 0 });
27
+ </script>
28
+ </head>
29
+ <body vibe>
30
+ <h1>Hello, @[name]!</h1>
31
+ <button onclick="$.count++">Clicked @[count] times</button>
32
+ </body>
35
33
  </html>
36
34
  ```
37
35
 
@@ -49,12 +47,14 @@ The CSS targets `[vibe]` and hides it until hydration completes. Once Vibe finis
49
47
  Vibe supports bindings in three positions:
50
48
 
51
49
  **Text content** — Inside element tags:
50
+
52
51
  ```html
53
52
  <div>@[firstName] @[lastName]</div>
54
53
  <h1>Hello, @[name]!</h1>
55
54
  ```
56
55
 
57
56
  **Attribute values** — In attribute value position:
57
+
58
58
  ```html
59
59
  <input value="@[username]" />
60
60
  <div class="@[theme]" style="color: @[color]"></div>
@@ -62,15 +62,19 @@ Vibe supports bindings in three positions:
62
62
  ```
63
63
 
64
64
  **Attribute names** — In attribute name position (useful for dynamic attributes):
65
+
65
66
  ```html
66
67
  <icon @[iconName]></icon>
67
68
  <button @[state]>Click me</button>
68
69
  ```
69
70
 
70
71
  **CSS** — Bindings also work in style tags:
72
+
71
73
  ```html
72
74
  <style>
73
- .box { background: @[themeColor]; }
75
+ .box {
76
+ background: @[themeColor];
77
+ }
74
78
  </style>
75
79
  ```
76
80
 
@@ -78,7 +82,7 @@ Vibe supports bindings in three positions:
78
82
 
79
83
  ```html
80
84
  <!-- each items as item, index -->
81
- <li>@[index]: @[item]</li>
85
+ <li>@[index]: @[item]</li>
82
86
  <!-- /each -->
83
87
  ```
84
88
 
@@ -86,9 +90,9 @@ Nested iteration with dot paths:
86
90
 
87
91
  ```html
88
92
  <!-- each categories as category -->
89
- <!-- each category.items as item -->
90
- <span>@[item.name]</span>
91
- <!-- /each -->
93
+ <!-- each category.items as item -->
94
+ <span>@[item.name]</span>
95
+ <!-- /each -->
92
96
  <!-- /each -->
93
97
  ```
94
98
 
@@ -96,9 +100,9 @@ Nested iteration with dot paths:
96
100
 
97
101
  ```html
98
102
  <!-- if user.isAdmin -->
99
- <admin-badge>Admin</admin-badge>
103
+ <admin-badge>Admin</admin-badge>
100
104
  <!-- else -->
101
- <span>User</span>
105
+ <span>User</span>
102
106
  <!-- /if -->
103
107
  ```
104
108
 
@@ -126,9 +130,9 @@ Vibe uses recursive proxies to detect changes at any nesting level:
126
130
 
127
131
  ```javascript
128
132
  // All of these trigger reactive updates:
129
- $.user.name = "Alice";
133
+ $.user.name = 'Alice';
130
134
  $.todos[2].completed = true;
131
- $.config.theme.colors.primary = "#007bff";
135
+ $.config.theme.colors.primary = '#007bff';
132
136
  ```
133
137
 
134
138
  No need for immutable update patterns or spread operators. Just mutate and Vibe handles the rest.
@@ -211,6 +215,7 @@ Add to your `package.json`:
211
215
  ```
212
216
 
213
217
  **Defaults** (when no config):
218
+
214
219
  - `source`: `./`
215
220
  - `output`: `./compiled`
216
221
  - `components`: `<source>/components`
@@ -238,6 +243,7 @@ By default, the compiler creates deployable output by installing production depe
238
243
  3. Removes `package.json` and lockfile from output (cleanup)
239
244
 
240
245
  This ensures:
246
+
241
247
  - Compiled output only includes runtime dependencies (from `dependencies`, not `devDependencies`)
242
248
  - Local `node_modules` is never modified
243
249
  - Faster than copying (no intermediate copy step)
@@ -294,6 +300,7 @@ cp target/release/vibe-compiler ../native/vibe-compiler-darwin-arm64
294
300
  ```
295
301
 
296
302
  Supported platforms:
303
+
297
304
  - `vibe-compiler-darwin-arm64` (macOS Apple Silicon) ✅ Included
298
305
  - `vibe-compiler-darwin-x64` (macOS Intel)
299
306
  - `vibe-compiler-linux-x64`
package/index.js CHANGED
@@ -1,2 +1,96 @@
1
- // Re-export from runtime for backwards compatibility
2
- export { default } from './runtime/index.js';
1
+ // Universal entry point for Vibe
2
+ // Usage: <script src="vibe/index.js">$.count = 0;</script> (global)
3
+ // <script src="vibe/index.js">let count = 0;</script> (component)
4
+
5
+ (async () => {
6
+ const script = document.currentScript;
7
+ const scriptContent = script?.textContent?.trim() || '';
8
+
9
+ // Determine if this is component state (has let/const/var) or global state (uses $)
10
+ const hasDeclarations = /\b(let|const|var)\s+\w+/.test(scriptContent);
11
+ const isComponent = hasDeclarations;
12
+
13
+ // Boot Vibe if not already initialized or booting
14
+ if (!window.__vibeInitialized && !window.__vibeBooting) {
15
+ window.__vibeBooting = true;
16
+
17
+ // Process ALL scripts on the page BEFORE booting
18
+ const { generateComponentId, executeComponentScript } = await import('./runtime/component-state.js');
19
+ const allComponentScripts = document.querySelectorAll('script[src*="index.js"]');
20
+ const initialState = {};
21
+ const globalStateScripts = [];
22
+
23
+ allComponentScripts.forEach((s) => {
24
+ const content = s.textContent?.trim() || '';
25
+ if (!content) return;
26
+
27
+ const hasDecl = /\b(let|const|var)\s+\w+/.test(content);
28
+ if (hasDecl) {
29
+ // This is a component script
30
+ const componentId = generateComponentId();
31
+
32
+ // Tag script and siblings
33
+ s.setAttribute('data-vibe-component-id', componentId);
34
+ s.setAttribute('type', 'component');
35
+
36
+ let sibling = s.nextElementSibling;
37
+ while (sibling) {
38
+ if (sibling.tagName === 'SCRIPT' && /\b(let|const|var)\s+\w+/.test(sibling.textContent || '')) {
39
+ break;
40
+ }
41
+ sibling.setAttribute('data-vibe-component-id', componentId);
42
+ sibling = sibling.nextElementSibling;
43
+ }
44
+
45
+ // Execute component script to get state
46
+ const componentState = executeComponentScript(content);
47
+ initialState[componentId] = componentState;
48
+ } else {
49
+ // This is a global state script - save for later
50
+ globalStateScripts.push(content);
51
+ }
52
+ });
53
+
54
+ // Execute global state scripts into initialState BEFORE booting
55
+ // This ensures global state is available during initial parse/hydrate
56
+ if (globalStateScripts.length > 0) {
57
+ const captured = {};
58
+ const fakeState = new Proxy(captured, {
59
+ set(target, key, value) {
60
+ target[key] = value;
61
+ return true;
62
+ }
63
+ });
64
+
65
+ globalStateScripts.forEach(code => {
66
+ const $ = fakeState;
67
+ eval(code);
68
+ });
69
+
70
+ // Merge global state into initialState
71
+ Object.assign(initialState, captured);
72
+ }
73
+
74
+ // Boot Vibe with both component and global states
75
+ const { default: main } = await import('./runtime/index.js');
76
+ window.$ = main(initialState, 'vibe', {});
77
+ window.__vibeInitialized = true;
78
+ window.__vibeBooting = false;
79
+ } else {
80
+ // Vibe already booted - just execute global code if any
81
+ if (window.__vibeBooting) {
82
+ await new Promise(resolve => {
83
+ const check = setInterval(() => {
84
+ if (window.__vibeInitialized) {
85
+ clearInterval(check);
86
+ resolve();
87
+ }
88
+ }, 10);
89
+ });
90
+ }
91
+
92
+ if (!isComponent && scriptContent) {
93
+ eval(scriptContent);
94
+ }
95
+ }
96
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "index.js",
18
+ "vibe.css",
18
19
  "runtime/",
19
20
  "compiler/bin/",
20
21
  "compiler/native/",
@@ -1,10 +1,10 @@
1
1
  import { resolvePath, deepEqual } from './iteration-utils.js';
2
2
  import { extractDependencies } from './conditionals.js';
3
3
  import { BINDING_REGEX } from './constants.js';
4
- import { evalInScope } from './utils.js';
4
+ import { evalInScope, resolveThisPath } from './utils.js';
5
5
 
6
6
  // Evaluate conditional expression
7
- const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
7
+ const evaluateCondition = (expression, state, element = null) => !!evalInScope(expression, state, element);
8
8
 
9
9
  // Helper function to check if a match references a specific key
10
10
  const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
@@ -12,8 +12,11 @@ const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(ke
12
12
  const recursive = (tree, state, newState, affected, scopedStateForHydration = null) => {
13
13
  // Handle iteration nodes specially
14
14
  if (tree.type === 'iteration') {
15
- const oldArray = resolvePath(state, tree.meta.arrayPath);
16
- const newArray = resolvePath(newState, tree.meta.arrayPath);
15
+ // Handle this.property for component-scoped arrays
16
+ const arrayPath = resolveThisPath(tree.meta.arrayPath, tree.meta.startComment?.parentElement);
17
+
18
+ const oldArray = resolvePath(state, arrayPath);
19
+ const newArray = resolvePath(newState, arrayPath);
17
20
 
18
21
  // Fast path: reference comparison (arrays are typically replaced, not mutated)
19
22
  // This avoids expensive O(n) deepEqual for large arrays
@@ -34,7 +37,8 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
34
37
  // Use the instance's scoped state (includes item, index, etc.)
35
38
  // Merge newState into scopedState to get updated global values
36
39
  const mergedNewState = { ...instance.scopedState, ...newState };
37
- recursive(instance.tree, instance.scopedState, mergedNewState, affected, mergedNewState);
40
+ // Pass the Proxy as scopedState so evalInScope can access iteration variables
41
+ recursive(instance.tree, instance.scopedState, mergedNewState, affected, instance.scopedState);
38
42
  }
39
43
  }
40
44
  }
@@ -44,8 +48,8 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
44
48
 
45
49
  // Handle conditional nodes specially
46
50
  if (tree.type === 'conditional') {
47
- const oldValue = evaluateCondition(tree.meta.expression, state);
48
- const newValue = evaluateCondition(tree.meta.expression, newState);
51
+ const oldValue = evaluateCondition(tree.meta.expression, state, tree.meta.startComment?.parentElement);
52
+ const newValue = evaluateCondition(tree.meta.expression, newState, tree.meta.startComment?.parentElement);
49
53
 
50
54
  // Check if condition result changed
51
55
  if (oldValue !== newValue) {
@@ -83,21 +87,24 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
83
87
  const checkedMatches = [];
84
88
 
85
89
  for (const m of matches) {
86
- const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
90
+ // Resolve this.property to componentId.property
91
+ const resolvedInner = resolveThisPath(m.inner, tree.element);
92
+
93
+ const noMatch = !shallowState.some((key) => matchesKey(resolvedInner, key));
87
94
 
88
95
  let shouldAffect = false;
89
96
  let relevantKeys = [];
90
97
 
91
98
  if (isInitialHydration) {
92
99
  // Initial hydration: affect all matched keys
93
- relevantKeys = shallowNewState.filter((key) => matchesKey(m.inner, key));
100
+ relevantKeys = shallowNewState.filter((key) => matchesKey(resolvedInner, key));
94
101
  shouldAffect = noMatch || relevantKeys.length > 0;
95
102
  } else {
96
103
  // Update: only affect if value changed
97
104
  const changedKeys = shallowNewState.filter((key) =>
98
- matchesKey(m.inner, key) && state[key] !== newState[key]
105
+ matchesKey(resolvedInner, key) && state[key] !== newState[key]
99
106
  );
100
- relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(m.inner, key));
107
+ relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(resolvedInner, key));
101
108
  shouldAffect = noMatch || changedKeys.length > 0;
102
109
  }
103
110
 
@@ -119,6 +126,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
119
126
  input: m.input,
120
127
  matches: m.matches,
121
128
  element: tree.element,
129
+ textNode: tree.textNode, // Reference to specific text node (prevents wiping children)
122
130
  scopedState: scopedStateForHydration, // Pass scoped state from iteration context
123
131
  });
124
132
  }
@@ -140,18 +148,21 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
140
148
  }
141
149
 
142
150
  for (const m of attrMatches) {
143
- const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
151
+ // Resolve this.property to componentId.property
152
+ const resolvedInner = resolveThisPath(m.inner, tree.element);
153
+
154
+ const noMatch = !shallowState.some((key) => matchesKey(resolvedInner, key));
144
155
 
145
156
  let shouldAffect = false;
146
157
 
147
158
  if (isInitialHydration) {
148
159
  // Initial hydration: affect all matched keys
149
- const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
160
+ const newMatches = shallowNewState.filter((key) => matchesKey(resolvedInner, key));
150
161
  shouldAffect = noMatch || newMatches.length > 0;
151
162
  } else {
152
163
  // Update: only affect if value changed
153
164
  const changedKeys = shallowNewState.filter((key) =>
154
- matchesKey(m.inner, key) && state[key] !== newState[key]
165
+ matchesKey(resolvedInner, key) && state[key] !== newState[key]
155
166
  );
156
167
  shouldAffect = noMatch || changedKeys.length > 0;
157
168
  }
@@ -162,7 +173,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
162
173
  attrName,
163
174
  attrValue,
164
175
  matchOuter: m.outer,
165
- matchInner: m.inner,
176
+ matchInner: m.inner, // Keep original, evalInScope will resolve this.
166
177
  element: tree.element,
167
178
  scopedState: scopedStateForHydration, // Pass scoped state from iteration context
168
179
  });
@@ -186,16 +197,19 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
186
197
  }
187
198
 
188
199
  for (const m of nameMatches) {
189
- const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
200
+ // Resolve this.property to componentId.property
201
+ const resolvedInner = resolveThisPath(m.inner, tree.element);
202
+
203
+ const noMatch = !shallowState.some((key) => matchesKey(resolvedInner, key));
190
204
 
191
205
  let shouldAffect = false;
192
206
 
193
207
  if (isInitialHydration) {
194
- const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
208
+ const newMatches = shallowNewState.filter((key) => matchesKey(resolvedInner, key));
195
209
  shouldAffect = noMatch || newMatches.length > 0;
196
210
  } else {
197
211
  const changedKeys = shallowNewState.filter((key) =>
198
- matchesKey(m.inner, key) && state[key] !== newState[key]
212
+ matchesKey(resolvedInner, key) && state[key] !== newState[key]
199
213
  );
200
214
  shouldAffect = noMatch || changedKeys.length > 0;
201
215
  }
@@ -205,7 +219,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
205
219
  type: 'nameBinding',
206
220
  nameBinding,
207
221
  matchOuter: m.outer,
208
- matchInner: m.inner,
222
+ matchInner: m.inner, // Keep original, evalInScope will resolve this.
209
223
  element: tree.element,
210
224
  scopedState: scopedStateForHydration,
211
225
  });
@@ -0,0 +1,63 @@
1
+ // Shared utility for processing component state from <script type="component">
2
+
3
+ /**
4
+ * Generate unique component ID
5
+ */
6
+ export const generateComponentId = () => {
7
+ return `_c${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
8
+ };
9
+
10
+ /**
11
+ * Transform variable declarations to property assignments
12
+ * let count = 0; → this.count = 0;
13
+ */
14
+ const transformDeclarations = (code) => {
15
+ return code
16
+ // Transform: let varName = value;
17
+ .replace(/\b(let|const|var)\s+(\w+)\s*=\s*([^;]+);/g, 'this.$2 = $3;')
18
+ // Transform: let varName;
19
+ .replace(/\b(let|const|var)\s+(\w+)\s*;/g, 'this.$2 = undefined;');
20
+ };
21
+
22
+ /**
23
+ * Execute component script and return state object
24
+ * @param {string} scriptContent - JavaScript code to execute
25
+ * @returns {object} - Component state object
26
+ */
27
+ export const executeComponentScript = (scriptContent) => {
28
+ const componentState = {};
29
+
30
+ if (!scriptContent || !scriptContent.trim()) {
31
+ return componentState;
32
+ }
33
+
34
+ try {
35
+ // Transform declarations to property assignments
36
+ const transformed = transformDeclarations(scriptContent);
37
+
38
+ // Execute script with 'this' = componentState
39
+ const fn = new Function(transformed);
40
+ fn.call(componentState);
41
+
42
+ } catch (error) {
43
+ console.error('[vibe] Error executing component script:', error);
44
+ console.error('[vibe] Script content:', scriptContent);
45
+ }
46
+
47
+ return componentState;
48
+ };
49
+
50
+ /**
51
+ * Register component state in global store
52
+ * @param {string} componentId - Unique component identifier
53
+ * @param {object} state - Component state object
54
+ * @param {object} globalState - Global $ object
55
+ */
56
+ export const registerComponentState = (componentId, state, globalState) => {
57
+ if (!globalState[componentId]) {
58
+ globalState[componentId] = state;
59
+ } else {
60
+ // Component already registered, merge new state
61
+ Object.assign(globalState[componentId], state);
62
+ }
63
+ };
@@ -1,6 +1,7 @@
1
1
  import { debugLog } from './debug.js';
2
2
  import { PHASE_FETCH } from './constants.js';
3
3
  import { evalInScope } from './utils.js';
4
+ import { generateComponentId, executeComponentScript } from './component-state.js';
4
5
 
5
6
  // Helper to escape regex special characters
6
7
  const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -52,8 +53,44 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
52
53
  fetch(src, { signal: controller.signal })
53
54
  .then((r) => r.text())
54
55
  .then((html) => {
55
- // Transform the fetched HTML
56
- let transformedHtml = html;
56
+ // Parse HTML in temporary container to process component scripts
57
+ const temp = document.createElement('div');
58
+ temp.innerHTML = html;
59
+
60
+ // Process any <script type="component"> elements
61
+ const componentScripts = temp.querySelectorAll('script[type="component"]');
62
+ componentScripts.forEach((script) => {
63
+ const scriptContent = script.textContent?.trim() || '';
64
+ if (!scriptContent) return;
65
+
66
+ // Generate component ID
67
+ const componentId = generateComponentId();
68
+
69
+ // Tag script element
70
+ script.setAttribute('data-vibe-component-id', componentId);
71
+
72
+ // Tag following siblings with this component ID
73
+ let sibling = script.nextElementSibling;
74
+ while (sibling) {
75
+ // Stop if we hit another component script
76
+ if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'component') {
77
+ break;
78
+ }
79
+ sibling.setAttribute('data-vibe-component-id', componentId);
80
+ sibling = sibling.nextElementSibling;
81
+ }
82
+
83
+ // Execute component script to get state
84
+ const componentState = executeComponentScript(scriptContent);
85
+
86
+ // Register in global state
87
+ if (window.$) {
88
+ window.$[componentId] = componentState;
89
+ }
90
+ });
91
+
92
+ // Get transformed HTML from temp container
93
+ let transformedHtml = temp.innerHTML;
57
94
 
58
95
  // Replace props
59
96
  Object.entries(props).forEach(([propName, propValue]) => {
@@ -5,7 +5,7 @@ import { createScopedState, renderAllIterations, initializeBlock } from './itera
5
5
  import { evalInScope } from './utils.js';
6
6
 
7
7
  // Evaluate conditional expression in state context
8
- const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
8
+ const evaluateCondition = (expression, state, element = null) => !!evalInScope(expression, state, element);
9
9
 
10
10
  // Extract state dependencies from an expression
11
11
  // e.g., "count > 5" → ["count"]
@@ -78,7 +78,7 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
78
78
  startComment.__vibeRendered = true;
79
79
 
80
80
  // Evaluate condition with current state
81
- const conditionResult = evaluateCondition(expression, state);
81
+ const conditionResult = evaluateCondition(expression, state, startComment.parentElement);
82
82
 
83
83
  // Determine which branch to mount
84
84
  const branchToMount = conditionResult ? branches.if : branches.else;
@@ -154,7 +154,7 @@ const unmountBranch = (node) => {
154
154
 
155
155
  // Update conditional when dependencies change
156
156
  export const updateConditional = (node, newState, oldState, manifest, parentScope = {}) => {
157
- const { expression, branches } = node.meta;
157
+ const { expression, branches, startComment } = node.meta;
158
158
 
159
159
  // If not yet rendered, skip (renderConditional handles initial render)
160
160
  if (!node.runtime.templateRemoved) {
@@ -162,7 +162,7 @@ export const updateConditional = (node, newState, oldState, manifest, parentScop
162
162
  }
163
163
 
164
164
  // Evaluate expression with new state
165
- const newConditionResult = evaluateCondition(expression, newState);
165
+ const newConditionResult = evaluateCondition(expression, newState, startComment?.parentElement);
166
166
  const newBranchData = newConditionResult ? branches.if : branches.else;
167
167
 
168
168
  // Check if branch changed (compare references)
@@ -156,6 +156,19 @@ export const VALUE_ATTRS = [
156
156
  // Properties that should be set directly on the DOM element (not as attributes)
157
157
  export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
158
158
 
159
+ // DOM element properties that should NOT be rewritten in event handlers
160
+ // When parsing onclick="this.value = this.inputValue", preserve this.value (DOM) but rewrite this.inputValue (component state)
161
+ export const DOM_ELEMENT_PROPERTIES = new Set([
162
+ 'value', 'checked', 'selected', 'disabled', 'readOnly', 'files',
163
+ 'tagName', 'nodeName', 'nodeType', 'classList', 'className', 'id',
164
+ 'innerHTML', 'outerHTML', 'textContent', 'innerText',
165
+ 'parentElement', 'parentNode', 'nextSibling', 'previousSibling',
166
+ 'firstChild', 'lastChild', 'children', 'childNodes',
167
+ 'offsetWidth', 'offsetHeight', 'clientWidth', 'clientHeight',
168
+ 'scrollTop', 'scrollLeft', 'scrollWidth', 'scrollHeight',
169
+ 'style', 'dataset', 'attributes'
170
+ ]);
171
+
159
172
  // Regex for matching reactive bindings (@[expression])
160
173
  // Supports one level of nested brackets: @[items[0]] or @[obj[key]]
161
174
  export const BINDING_REGEX = /\@\[((?:[^\[\]]|\[[^\]]*\])+)\]/g;