@ape-egg/vibe 1.1.2 → 1.3.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 +140 -0
- package/README.md +30 -23
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/compiler/compile.rs +204 -51
- package/compiler/src/compiler/manifest_builder.rs +432 -0
- package/compiler/src/compiler/mod.rs +3 -0
- package/compiler/src/compiler/state_extractor.rs +148 -0
- package/compiler/src/compiler/value_stamper.rs +222 -0
- package/compiler/src/config.rs +0 -5
- package/compiler/src/main.rs +45 -20
- package/compiler/src/parser/html.rs +22 -8
- package/index.js +40 -2
- package/package.json +2 -1
- package/runtime/affected.js +44 -24
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +63 -0
- package/runtime/component.js +59 -15
- package/runtime/conditionals.js +4 -4
- package/runtime/constants.js +46 -2
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +23 -21
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +322 -69
- package/runtime/iterate.js +15 -4
- package/runtime/parse.js +48 -10
- package/runtime/scope.js +50 -0
- package/runtime/state.js +10 -5
- package/runtime/utils.js +63 -5
- package/{runtime/vibe.css → vibe.css} +4 -2
package/runtime/parse.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { hash } from './utils.js';
|
|
2
1
|
import { findEndComment, findConditionalEnd } from './iteration-utils.js';
|
|
3
2
|
import {
|
|
4
3
|
NON_REACTIVE_ELEMENTS,
|
|
5
4
|
BINDING_REGEX,
|
|
6
5
|
ITERATION_REGEX,
|
|
7
6
|
CONDITIONAL_REGEX,
|
|
7
|
+
DOM_ELEMENT_PROPERTIES,
|
|
8
8
|
} from './constants.js';
|
|
9
9
|
|
|
10
10
|
const parseHTML = (children, rootKey = undefined) =>
|
|
@@ -59,9 +59,9 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
59
59
|
// Parse the template recursively
|
|
60
60
|
const templateParsed = recursive([...templateContainer.childNodes], undefined, new Set(), stats);
|
|
61
61
|
|
|
62
|
-
// Store iteration metadata
|
|
63
|
-
const iterationKey = `iteration_${
|
|
64
|
-
|
|
62
|
+
// Store iteration metadata (use index for deterministic keys)
|
|
63
|
+
const iterationKey = `iteration_${i}`;
|
|
64
|
+
const iterationNode = {
|
|
65
65
|
type: 'iteration',
|
|
66
66
|
meta: {
|
|
67
67
|
arrayPath,
|
|
@@ -75,12 +75,15 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
75
75
|
children: templateParsed,
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
|
-
runtime
|
|
78
|
+
// Preserve runtime data from previous parse if it exists (stored on startComment by iterate.js)
|
|
79
|
+
// @ts-ignore - custom property added by iterate.js
|
|
80
|
+
runtime: element.__vibeIterationRuntime || {
|
|
79
81
|
instances: [],
|
|
80
82
|
templateRemoved: false,
|
|
81
83
|
},
|
|
82
84
|
children: {},
|
|
83
85
|
};
|
|
86
|
+
result[iterationKey] = iterationNode;
|
|
84
87
|
|
|
85
88
|
// Mark template indices as processed
|
|
86
89
|
for (let j = i + 1; j < endIndex; j++) {
|
|
@@ -132,8 +135,8 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
132
135
|
falseBranchParsed = recursive([...falseBranchContainer.childNodes], undefined, new Set(), stats);
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
// Store conditional metadata
|
|
136
|
-
const conditionalKey = `conditional_${
|
|
138
|
+
// Store conditional metadata (use index for deterministic keys)
|
|
139
|
+
const conditionalKey = `conditional_${i}`;
|
|
137
140
|
result[conditionalKey] = {
|
|
138
141
|
type: 'conditional',
|
|
139
142
|
meta: {
|
|
@@ -185,10 +188,21 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
185
188
|
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
|
|
186
189
|
const nodeIdentifier = `${name}_${i}`.toLowerCase();
|
|
187
190
|
|
|
191
|
+
// For text nodes, use parent element (text nodes can't have attributes)
|
|
192
|
+
const isTextNode = nodeName === '#text';
|
|
193
|
+
const elementForBindings = isTextNode ? element.parentElement : element;
|
|
194
|
+
const textNodeRef = isTextNode ? element : null; // Store reference to actual text node
|
|
195
|
+
|
|
188
196
|
// Check for attribute bindings
|
|
189
197
|
const attributes = {};
|
|
190
198
|
const nameBindings = [];
|
|
191
|
-
|
|
199
|
+
|
|
200
|
+
// Skip hydrating attributes on fetched components - they need to be passed raw
|
|
201
|
+
const isFetchedComponent =
|
|
202
|
+
(nodeName === 'COMPONENT' || (nodeName === 'DIV' && element.classList?.contains('component'))) &&
|
|
203
|
+
element.hasAttribute('src');
|
|
204
|
+
|
|
205
|
+
if (element.attributes && !isFetchedComponent) {
|
|
192
206
|
for (let j = 0; j < element.attributes.length; j++) {
|
|
193
207
|
const attr = element.attributes[j];
|
|
194
208
|
// Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
|
|
@@ -200,6 +214,18 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
200
214
|
continue; // Don't process as regular attribute
|
|
201
215
|
}
|
|
202
216
|
|
|
217
|
+
// Rewrite event handlers with this. to use component state
|
|
218
|
+
if (attr.name.startsWith('on') && attr.value.includes('this.')) {
|
|
219
|
+
const componentId = findComponentIdForElement(element);
|
|
220
|
+
if (componentId) {
|
|
221
|
+
// Rewrite this.property to $['componentId'].property, but skip DOM properties
|
|
222
|
+
const rewritten = attr.value.replace(/\bthis\.(\w+)/g, (match, prop) => {
|
|
223
|
+
return DOM_ELEMENT_PROPERTIES.has(prop) ? match : `$['${componentId}'].${prop}`;
|
|
224
|
+
});
|
|
225
|
+
element.setAttribute(attr.name, rewritten);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
203
229
|
// Check if attribute value contains binding
|
|
204
230
|
BINDING_REGEX.lastIndex = 0;
|
|
205
231
|
if (BINDING_REGEX.test(attr.value)) {
|
|
@@ -207,6 +233,16 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
207
233
|
}
|
|
208
234
|
}
|
|
209
235
|
}
|
|
236
|
+
|
|
237
|
+
// Helper to find component ID for an element
|
|
238
|
+
// Looks for nearest ancestor with data-vibe-component-id
|
|
239
|
+
function findComponentIdForElement(element) {
|
|
240
|
+
if (!element) return null;
|
|
241
|
+
|
|
242
|
+
// Find nearest component wrapper (tagged by component.js)
|
|
243
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
244
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
245
|
+
}
|
|
210
246
|
const hasAttributeBindings = Object.keys(attributes).length > 0;
|
|
211
247
|
const hasNameBindings = nameBindings.length > 0;
|
|
212
248
|
|
|
@@ -218,18 +254,20 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
218
254
|
|
|
219
255
|
result[rootKey || nodeIdentifier] = {
|
|
220
256
|
parsed,
|
|
221
|
-
element,
|
|
257
|
+
element: elementForBindings,
|
|
222
258
|
children: recursive(iteratableChildren, undefined, new Set(), stats),
|
|
223
259
|
...(hasAttributeBindings && { attributes }),
|
|
224
260
|
...(hasNameBindings && { nameBindings }),
|
|
261
|
+
...(textNodeRef && { textNode: textNodeRef }),
|
|
225
262
|
};
|
|
226
263
|
} else {
|
|
227
264
|
result[nodeIdentifier] = {
|
|
228
265
|
parsed: innerHTML || textContent,
|
|
229
|
-
element,
|
|
266
|
+
element: elementForBindings,
|
|
230
267
|
children: {},
|
|
231
268
|
...(hasAttributeBindings && { attributes }),
|
|
232
269
|
...(hasNameBindings && { nameBindings }),
|
|
270
|
+
...(textNodeRef && { textNode: textNodeRef }),
|
|
233
271
|
};
|
|
234
272
|
}
|
|
235
273
|
}
|
package/runtime/scope.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Scope resolution for component state
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find the component ID that owns this element
|
|
5
|
+
* Finds nearest ancestor with data-vibe-component-id (set by component.js)
|
|
6
|
+
* @param {Element} element - DOM element to find component for
|
|
7
|
+
* @returns {string|null} - Component ID or null if not in component scope
|
|
8
|
+
*/
|
|
9
|
+
export const findComponentId = (element) => {
|
|
10
|
+
if (!element || !element.closest) return null;
|
|
11
|
+
|
|
12
|
+
// Find nearest component wrapper (tagged by component.js)
|
|
13
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
14
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a property path with component scope fallback
|
|
19
|
+
* Checks component state first, then global state
|
|
20
|
+
* @param {string} path - Property path (e.g., "count" or "user.name")
|
|
21
|
+
* @param {Element} element - DOM element for context
|
|
22
|
+
* @param {object} globalState - Global $ object
|
|
23
|
+
* @returns {*} - Resolved value
|
|
24
|
+
*/
|
|
25
|
+
export const resolveWithScope = (path, element, globalState) => {
|
|
26
|
+
const componentId = findComponentId(element);
|
|
27
|
+
|
|
28
|
+
if (componentId && globalState[componentId]) {
|
|
29
|
+
// Check component state first
|
|
30
|
+
const componentState = globalState[componentId];
|
|
31
|
+
const value = resolvePath(path, componentState);
|
|
32
|
+
|
|
33
|
+
if (value !== undefined) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback to global state
|
|
39
|
+
return resolvePath(path, globalState);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a dot-notation path in an object
|
|
44
|
+
* @param {string} path - Property path
|
|
45
|
+
* @param {object} obj - Object to resolve path in
|
|
46
|
+
* @returns {*} - Resolved value or undefined
|
|
47
|
+
*/
|
|
48
|
+
const resolvePath = (path, obj) => {
|
|
49
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
50
|
+
};
|
package/runtime/state.js
CHANGED
|
@@ -16,17 +16,22 @@ const createDeepProxy = (target, rerender, rootState = null, rootProp = null) =>
|
|
|
16
16
|
const proxy = new Proxy(target, {
|
|
17
17
|
set(obj, prop, value) {
|
|
18
18
|
const oldValue = obj[prop];
|
|
19
|
-
const ref = Reflect.set(obj, prop, value);
|
|
20
19
|
|
|
21
20
|
// Only trigger rerender if value actually changed
|
|
22
21
|
if (oldValue !== value) {
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Perform the mutation
|
|
23
|
+
const ref = Reflect.set(obj, prop, value);
|
|
24
|
+
|
|
25
|
+
// Trigger rerender with changed state
|
|
26
|
+
// If we're nested, use the root prop; otherwise use the prop itself
|
|
25
27
|
const changedProp = rootProp || prop;
|
|
26
|
-
rerender({ [changedProp]: rootState[changedProp] });
|
|
28
|
+
rerender({ [changedProp]: rootState[changedProp] }, null);
|
|
29
|
+
|
|
30
|
+
return ref;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
// No change, just set
|
|
34
|
+
return Reflect.set(obj, prop, value);
|
|
30
35
|
},
|
|
31
36
|
|
|
32
37
|
get(target, prop) {
|
package/runtime/utils.js
CHANGED
|
@@ -3,18 +3,76 @@ let hashCounter = 0;
|
|
|
3
3
|
export const hash = () => `_${hashCounter++}`;
|
|
4
4
|
|
|
5
5
|
// Evaluate expression in the context of state
|
|
6
|
-
|
|
6
|
+
// Handles @[this.property] for component state and @[property] for global state
|
|
7
|
+
export const evalInScope = (expr, state, element = null) => {
|
|
7
8
|
try {
|
|
8
9
|
// Normalize whitespace - collapse newlines/spaces to single space (resilient to IDE formatting)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
let normalized = expr.replace(/\s+/g, ' ').trim();
|
|
11
|
+
|
|
12
|
+
// If expression contains 'this.', replace with component state path
|
|
13
|
+
if (normalized.includes('this.') && element) {
|
|
14
|
+
const componentId = findComponentIdForElement(element);
|
|
15
|
+
if (componentId) {
|
|
16
|
+
// Replace this.property with $['componentId'].property
|
|
17
|
+
normalized = normalized.replace(/\bthis\.(\w+)/g, `$['${componentId}'].$1`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add $ to scope for this.property references
|
|
22
|
+
const effectiveState = { ...state, $: state };
|
|
23
|
+
|
|
24
|
+
const keys = Object.keys(effectiveState);
|
|
25
|
+
const values = Object.values(effectiveState);
|
|
26
|
+
|
|
27
|
+
const result = new Function(...keys, `'use strict'; return (${normalized})`)(...values);
|
|
28
|
+
|
|
29
|
+
// If result is undefined and we're accessing a component property, try case-insensitive match
|
|
30
|
+
// This handles HTML lowercasing attribute names like @[this.iconName] -> @[this.iconname]
|
|
31
|
+
if (result === undefined) {
|
|
32
|
+
const componentMatch = normalized.match(/\$\['([^']+)'\]\.(\w+)/);
|
|
33
|
+
if (componentMatch) {
|
|
34
|
+
const [, componentId, propName] = componentMatch;
|
|
35
|
+
const componentState = state[componentId];
|
|
36
|
+
if (componentState) {
|
|
37
|
+
// Try case-insensitive property lookup
|
|
38
|
+
const actualKey = Object.keys(componentState).find(k => k.toLowerCase() === propName.toLowerCase());
|
|
39
|
+
if (actualKey) {
|
|
40
|
+
return componentState[actualKey];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
13
47
|
} catch (e) {
|
|
14
48
|
return undefined;
|
|
15
49
|
}
|
|
16
50
|
};
|
|
17
51
|
|
|
52
|
+
// Helper to find component ID for an element
|
|
53
|
+
// Walks up DOM tree to find nearest component wrapper
|
|
54
|
+
export const findComponentIdForElement = (element) => {
|
|
55
|
+
if (!element || !element.closest) return null;
|
|
56
|
+
|
|
57
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
58
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Helper to resolve this.property paths to componentId.property
|
|
62
|
+
// Used for component-scoped iterations, conditionals, etc.
|
|
63
|
+
export const resolveThisPath = (path, element) => {
|
|
64
|
+
if (!path.startsWith('this.')) {
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const componentId = findComponentIdForElement(element);
|
|
69
|
+
if (componentId) {
|
|
70
|
+
return path.replace(/^this\./, `${componentId}.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return path;
|
|
74
|
+
};
|
|
75
|
+
|
|
18
76
|
// Instead of using lodash-es as a dependency, we run our own deepMerge (mergeWith in lodash)
|
|
19
77
|
export const deepMerge = (target, source) => {
|
|
20
78
|
// Handle null/undefined
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Elements with [vibe] attribute are hidden until framework removes it after hydration.
|
|
3
3
|
* This prevents flash of unprocessed content and disables transitions during init.
|
|
4
4
|
*/
|
|
5
|
-
[vibe]
|
|
5
|
+
[vibe-fouc],
|
|
6
|
+
.vibe-fouc {
|
|
6
7
|
visibility: hidden;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
[vibe]
|
|
10
|
+
[vibe-fouc] *,
|
|
11
|
+
.vibe-fouc * {
|
|
10
12
|
transition: none !important;
|
|
11
13
|
}
|