@ape-egg/vibe 1.2.0 → 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 +62 -0
- package/README.md +1 -1
- 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 +30 -86
- package/package.json +1 -1
- package/runtime/affected.js +12 -6
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +1 -1
- package/runtime/component.js +20 -13
- package/runtime/constants.js +41 -10
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +6 -1
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +236 -49
- package/runtime/iterate.js +3 -0
- package/runtime/parse.js +21 -29
- package/runtime/scope.js +5 -25
- package/runtime/utils.js +4 -13
- package/vibe.css +4 -2
package/runtime/cleanup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { debugLog } from './debug.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PHASE_READY, FOUC_CLASS_OR_ATTR } from './constants.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Check if all Vibe processing is complete and cleanup can run
|
|
@@ -7,30 +7,27 @@ import { PHASE_COMPLETE } from './constants.js';
|
|
|
7
7
|
* @returns {Boolean} - true if cleanup should run
|
|
8
8
|
*/
|
|
9
9
|
export const shouldCleanup = (rootElement) => {
|
|
10
|
-
// 1. Check for pending <component> elements
|
|
11
|
-
|
|
10
|
+
// 1. Check for pending <component> elements (only those with src - fetched components)
|
|
11
|
+
// Inline component wrappers (<component> without src) are fine to remain
|
|
12
|
+
const componentElements = rootElement.querySelectorAll('component[src], div.component[src]');
|
|
12
13
|
if (componentElements.length > 0) {
|
|
13
14
|
return false;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
// 2. Check for unhydrated bindings (literal @[...] in DOM, excluding dehydrated elements)
|
|
17
|
-
const walker = document.createTreeWalker(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
while (parent && parent !== rootElement) {
|
|
25
|
-
if (parent.hasAttribute('dehydrate')) {
|
|
26
|
-
return NodeFilter.FILTER_REJECT; // Skip dehydrated content
|
|
27
|
-
}
|
|
28
|
-
parent = parent.parentElement;
|
|
18
|
+
const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_TEXT, {
|
|
19
|
+
acceptNode(node) {
|
|
20
|
+
// Check if this text node is inside a dehydrated element
|
|
21
|
+
let parent = node.parentElement;
|
|
22
|
+
while (parent && parent !== rootElement) {
|
|
23
|
+
if (parent.hasAttribute('dehydrate')) {
|
|
24
|
+
return NodeFilter.FILTER_REJECT; // Skip dehydrated content
|
|
29
25
|
}
|
|
30
|
-
|
|
26
|
+
parent = parent.parentElement;
|
|
31
27
|
}
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
34
31
|
|
|
35
32
|
let node;
|
|
36
33
|
while ((node = walker.nextNode())) {
|
|
@@ -44,16 +41,40 @@ export const shouldCleanup = (rootElement) => {
|
|
|
44
41
|
};
|
|
45
42
|
|
|
46
43
|
/**
|
|
47
|
-
* Perform cleanup - remove
|
|
44
|
+
* Perform cleanup - remove attribute or class from all matching elements
|
|
48
45
|
* @param {Element} rootElement - Root element
|
|
49
|
-
* @param {String} attrName - Attribute name to remove (default: 'vibe')
|
|
50
46
|
* @param {Boolean} debug - Debug mode
|
|
51
47
|
*/
|
|
52
|
-
export const cleanup = (rootElement,
|
|
48
|
+
export const cleanup = (rootElement, debug = false) => {
|
|
53
49
|
// Force reflow
|
|
54
50
|
rootElement.offsetHeight;
|
|
55
51
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
// Determine if selector is a class (starts with .) or attribute (default)
|
|
53
|
+
const isClass = FOUC_CLASS_OR_ATTR.startsWith('.');
|
|
54
|
+
const cleanName = FOUC_CLASS_OR_ATTR.replace(/^\./, '').replace(/^\[/, '').replace(/\]$/, '');
|
|
55
|
+
|
|
56
|
+
if (isClass) {
|
|
57
|
+
// Remove class from all elements in document that have it
|
|
58
|
+
const elements = document.querySelectorAll(`.${cleanName}`);
|
|
59
|
+
elements.forEach((el) => el.classList.remove(cleanName));
|
|
60
|
+
debugLog(PHASE_READY, `removing .${cleanName} class from ${elements.length} elements`, debug);
|
|
61
|
+
} else {
|
|
62
|
+
// Remove attribute from all elements in document that have it
|
|
63
|
+
const elements = document.querySelectorAll(`[${cleanName}]`);
|
|
64
|
+
elements.forEach((el) => el.removeAttribute(cleanName));
|
|
65
|
+
debugLog(
|
|
66
|
+
PHASE_READY,
|
|
67
|
+
`removing [${cleanName}] attribute from ${elements.length} elements`,
|
|
68
|
+
debug,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Dispatch ready event to signal that Vibe has completed all initial processing
|
|
73
|
+
if (typeof document !== 'undefined') {
|
|
74
|
+
document.dispatchEvent(
|
|
75
|
+
new CustomEvent('vibe:ready', {
|
|
76
|
+
detail: { rootElement, cleanName, isClass },
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
59
80
|
};
|
package/runtime/component.js
CHANGED
|
@@ -20,7 +20,10 @@ export const abortComponentFetch = (element) => {
|
|
|
20
20
|
|
|
21
21
|
export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
22
22
|
const debug = !!config?.debug;
|
|
23
|
-
|
|
23
|
+
// Only process component elements with src attribute (fetched components)
|
|
24
|
+
// Supports: <component src="..."> and <div class="component" src="...">
|
|
25
|
+
// Ignores: <component> and <div class="component"> (inline component wrappers)
|
|
26
|
+
const componentElements = rootElement.querySelectorAll('component[src], div.component[src]');
|
|
24
27
|
|
|
25
28
|
if (componentElements.length === 0) {
|
|
26
29
|
if (onComplete) onComplete();
|
|
@@ -31,12 +34,6 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
31
34
|
const el = componentElements[0];
|
|
32
35
|
const src = el.getAttribute('src');
|
|
33
36
|
|
|
34
|
-
if (!src) {
|
|
35
|
-
el.remove();
|
|
36
|
-
// Don't recursively call - let MutationObserver handle it
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
37
|
// Capture children and props before fetching
|
|
41
38
|
const children = el.innerHTML.trim();
|
|
42
39
|
const props = {};
|
|
@@ -97,10 +94,10 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
97
94
|
const bindingMatch = propValue.match(/^@\[(.+)\]$/);
|
|
98
95
|
|
|
99
96
|
if (bindingMatch) {
|
|
100
|
-
// Reactive prop: replace propName
|
|
97
|
+
// Reactive prop: replace @[propName] with @[path]
|
|
101
98
|
const path = bindingMatch[1];
|
|
102
|
-
const propPattern = new RegExp(
|
|
103
|
-
transformedHtml = transformedHtml.replace(propPattern, path);
|
|
99
|
+
const propPattern = new RegExp(`@\\[${escapeRegex(propName)}\\]`, 'g');
|
|
100
|
+
transformedHtml = transformedHtml.replace(propPattern, `@[${path}]`);
|
|
104
101
|
} else {
|
|
105
102
|
// Static prop: replace @[propName] with literal value
|
|
106
103
|
const propPattern = new RegExp(`@\\[${escapeRegex(propName)}\\]`, 'g');
|
|
@@ -117,14 +114,24 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
117
114
|
// Clean up pending fetch tracker
|
|
118
115
|
pendingFetches.delete(el);
|
|
119
116
|
|
|
120
|
-
//
|
|
117
|
+
// Replace with clean component wrapper (no src, no props)
|
|
121
118
|
// Check if element still has a parent (might have been removed during fetch)
|
|
122
119
|
if (el.parentNode) {
|
|
123
|
-
|
|
120
|
+
// Create clean wrapper element (preserve tag type: component or div.component)
|
|
121
|
+
const newWrapper = el.tagName === 'DIV'
|
|
122
|
+
? document.createElement('div')
|
|
123
|
+
: document.createElement('component');
|
|
124
|
+
|
|
125
|
+
if (el.tagName === 'DIV') {
|
|
126
|
+
newWrapper.className = 'component';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
newWrapper.innerHTML = transformedHtml;
|
|
130
|
+
el.replaceWith(newWrapper);
|
|
124
131
|
debugLog(PHASE_FETCH, src, debug);
|
|
125
132
|
|
|
126
133
|
// Force immediate processing of the mutation (MutationObserver is async, but we need sync)
|
|
127
|
-
// Use microtask to process right after
|
|
134
|
+
// Use microtask to process right after replaceWith completes
|
|
128
135
|
if (config._forceSync && config._processMutations && config._observer) {
|
|
129
136
|
Promise.resolve().then(() => {
|
|
130
137
|
const pending = config._observer.takeRecords();
|
package/runtime/constants.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Debug logger name
|
|
2
2
|
export const DEBUGGER_NAME = '[vibe-debug]:';
|
|
3
|
+
export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
|
|
3
4
|
|
|
4
5
|
// Lifecycle phase names for debug logging
|
|
5
6
|
// ONE-OFF operations (run once during initialization)
|
|
6
7
|
export const PHASE_ATTACH = 'Attached'; // Latches onto DOM element (index.js)
|
|
7
8
|
export const PHASE_MANIFEST = 'Manifested'; // Creates DOM manifest (manifest.js)
|
|
8
9
|
export const PHASE_OBSERVE = 'Observer'; // Starts MutationObserver (index.js)
|
|
9
|
-
export const
|
|
10
|
+
export const PHASE_READY = 'Ready'; // Removes [vibe-fouc] attribute (or .vibe-fouc class) and dispatches vibe:ready event (cleanup.js)
|
|
10
11
|
|
|
11
12
|
// REPEATED operations (run during init + can repeat during runtime)
|
|
12
13
|
export const PHASE_PARSE = 'Parsed'; // Reads DOM structure (parse.js)
|
|
@@ -16,9 +17,12 @@ export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (condi
|
|
|
16
17
|
export const PHASE_FETCH = 'Fetched'; // Loads <component> content (component.js)
|
|
17
18
|
export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
|
|
18
19
|
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)
|
|
19
21
|
|
|
20
22
|
// Elements that should not have reactive bindings
|
|
21
|
-
|
|
23
|
+
// Note: COMPONENT is NOT in this list - inline component wrappers need to be parsed
|
|
24
|
+
// Fetched components (<component src="">) are handled separately by processComponent()
|
|
25
|
+
export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
|
|
22
26
|
|
|
23
27
|
// Attributes where the string value is meaningful (should NOT be removed when falsy)
|
|
24
28
|
// All other attributes are treated as boolean-like (removed when falsy, present when truthy)
|
|
@@ -159,14 +163,41 @@ export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
|
|
|
159
163
|
// DOM element properties that should NOT be rewritten in event handlers
|
|
160
164
|
// When parsing onclick="this.value = this.inputValue", preserve this.value (DOM) but rewrite this.inputValue (component state)
|
|
161
165
|
export const DOM_ELEMENT_PROPERTIES = new Set([
|
|
162
|
-
'value',
|
|
163
|
-
'
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
'
|
|
166
|
+
'value',
|
|
167
|
+
'checked',
|
|
168
|
+
'selected',
|
|
169
|
+
'disabled',
|
|
170
|
+
'readOnly',
|
|
171
|
+
'files',
|
|
172
|
+
'tagName',
|
|
173
|
+
'nodeName',
|
|
174
|
+
'nodeType',
|
|
175
|
+
'classList',
|
|
176
|
+
'className',
|
|
177
|
+
'id',
|
|
178
|
+
'innerHTML',
|
|
179
|
+
'outerHTML',
|
|
180
|
+
'textContent',
|
|
181
|
+
'innerText',
|
|
182
|
+
'parentElement',
|
|
183
|
+
'parentNode',
|
|
184
|
+
'nextSibling',
|
|
185
|
+
'previousSibling',
|
|
186
|
+
'firstChild',
|
|
187
|
+
'lastChild',
|
|
188
|
+
'children',
|
|
189
|
+
'childNodes',
|
|
190
|
+
'offsetWidth',
|
|
191
|
+
'offsetHeight',
|
|
192
|
+
'clientWidth',
|
|
193
|
+
'clientHeight',
|
|
194
|
+
'scrollTop',
|
|
195
|
+
'scrollLeft',
|
|
196
|
+
'scrollWidth',
|
|
197
|
+
'scrollHeight',
|
|
198
|
+
'style',
|
|
199
|
+
'dataset',
|
|
200
|
+
'attributes',
|
|
170
201
|
]);
|
|
171
202
|
|
|
172
203
|
// Regex for matching reactive bindings (@[expression])
|
package/runtime/debug.js
CHANGED
|
@@ -14,6 +14,7 @@ const PHASE_COLORS = {
|
|
|
14
14
|
Fetched: 'oklch(0.59 0.12 307)', // muted purple
|
|
15
15
|
Mutation: 'oklch(0.60 0.11 240)', // muted blue
|
|
16
16
|
Proxy: 'oklch(0.60 0.11 240)', // muted blue
|
|
17
|
+
Hyperspeed: 'oklch(0.60 0.11 180)', // muted cyan
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
// Special colors for styled segments (Tailwind 500)
|
package/runtime/hydrate.js
CHANGED
|
@@ -82,10 +82,15 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
82
82
|
attrName.startsWith('on');
|
|
83
83
|
|
|
84
84
|
if (isDomProperty && isPureBinding) {
|
|
85
|
-
// For DOM properties
|
|
85
|
+
// For DOM properties (value, checked, selected), set BOTH property AND attribute
|
|
86
|
+
// Property: Fast runtime updates, what the user sees
|
|
87
|
+
// Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
|
|
86
88
|
const expr = isPureBinding[1];
|
|
87
89
|
const value = evalInScope(expr, effectiveState, element);
|
|
88
90
|
element[attrName] = value;
|
|
91
|
+
if (value !== undefined && value !== null) {
|
|
92
|
+
element.setAttribute(attrName, String(value));
|
|
93
|
+
}
|
|
89
94
|
} else if (!isValueAttr && isPureBinding) {
|
|
90
95
|
// Boolean-like attributes: add or remove based on truthiness
|
|
91
96
|
const expr = isPureBinding[1];
|