@ape-egg/vibe 1.7.2 → 1.9.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 +139 -0
- package/README.md +28 -0
- package/boot.js +5 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +85 -496
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +3 -11
- package/compiler/src/parser/html.rs +15 -15
- package/index.js +15 -0
- package/package.json +2 -2
- package/runtime/_vibe-compiled-iteration-batch.js +18 -9
- package/runtime/affected.js +54 -16
- package/runtime/component.js +287 -110
- package/runtime/conditionals.js +99 -7
- package/runtime/constants.js +10 -6
- package/runtime/index.js +142 -47
- package/runtime/iterate.js +364 -142
- package/runtime/iteration-utils.js +5 -1
- package/runtime/manifest.js +2 -1
- package/runtime/parse.js +67 -80
- package/runtime/pre-compiled-iterations.js +34 -21
- package/runtime/pre-compiled-manifest.js +38 -13
- package/runtime/reconcile.js +621 -0
- package/runtime/state.js +26 -7
- package/runtime/utils.js +97 -5
- package/vibe.css +3 -1
package/runtime/manifest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const recursive = (tree, results, tagChain) => {
|
|
2
|
-
|
|
2
|
+
const path = tagChain.join('.');
|
|
3
|
+
results[path] = tree.element;
|
|
3
4
|
|
|
4
5
|
if (tree.children && Object.keys(tree.children).length > 0) {
|
|
5
6
|
Object.keys(tree.children).forEach((tag) => {
|
package/runtime/parse.js
CHANGED
|
@@ -8,6 +8,66 @@ import {
|
|
|
8
8
|
DEHYDRATE_CLASS_OR_ATTR,
|
|
9
9
|
} from './constants.js';
|
|
10
10
|
|
|
11
|
+
// Walks up the DOM for the nearest component wrapper tagged by component.js.
|
|
12
|
+
// Used to rewrite `this.property` in event handlers to the component's state path.
|
|
13
|
+
const findComponentIdForElement = (element) => {
|
|
14
|
+
if (!element?.closest) return null;
|
|
15
|
+
const wrapper = element.closest('[data-vibe-component-id]');
|
|
16
|
+
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Single source of truth for reading attribute/name bindings off an element.
|
|
20
|
+
// Called from both the root handler and recursive() so they can't drift. Any
|
|
21
|
+
// element classified as a fetched component (`<component src>` or
|
|
22
|
+
// `<div class="component" src>`) returns nulls — its attributes are props
|
|
23
|
+
// owned by processComponent and must stay raw; hydrating them would coerce
|
|
24
|
+
// objects to "[object Object]" or strip boolean-like attrs to empty.
|
|
25
|
+
const captureAttributeBindings = (element) => {
|
|
26
|
+
const nodeName = element.nodeName;
|
|
27
|
+
const isFetchedComponent =
|
|
28
|
+
(nodeName === 'COMPONENT' || (nodeName === 'DIV' && element.classList?.contains('component'))) &&
|
|
29
|
+
element.hasAttribute?.('src');
|
|
30
|
+
|
|
31
|
+
if (isFetchedComponent || !element.attributes || element.attributes.length === 0) {
|
|
32
|
+
return { attributes: null, nameBindings: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const attributes = {};
|
|
36
|
+
const nameBindings = [];
|
|
37
|
+
|
|
38
|
+
for (let j = 0; j < element.attributes.length; j++) {
|
|
39
|
+
const attr = element.attributes[j];
|
|
40
|
+
BINDING_REGEX.lastIndex = 0;
|
|
41
|
+
|
|
42
|
+
// Attribute name itself contains a binding (e.g. <icon @[section.icon]>).
|
|
43
|
+
if (BINDING_REGEX.test(attr.name)) {
|
|
44
|
+
nameBindings.push(attr.name);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Rewrite `this.property` inside event handlers to the component's state path.
|
|
49
|
+
if (attr.name.startsWith('on') && attr.value.includes('this.')) {
|
|
50
|
+
const componentId = findComponentIdForElement(element);
|
|
51
|
+
if (componentId) {
|
|
52
|
+
const rewritten = attr.value.replace(/\bthis\.(\w+)/g, (match, prop) => {
|
|
53
|
+
return DOM_ELEMENT_PROPERTIES.has(prop) ? match : `$['${componentId}'].${prop}`;
|
|
54
|
+
});
|
|
55
|
+
element.setAttribute(attr.name, rewritten);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
BINDING_REGEX.lastIndex = 0;
|
|
60
|
+
if (BINDING_REGEX.test(attr.value)) {
|
|
61
|
+
attributes[attr.name] = attr.value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
attributes: Object.keys(attributes).length > 0 ? attributes : null,
|
|
67
|
+
nameBindings: nameBindings.length > 0 ? nameBindings : null,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
11
71
|
const parseHTML = (children, rootKey = undefined) =>
|
|
12
72
|
children.reduce((s, element, i) => {
|
|
13
73
|
const { nodeName, textContent } = element;
|
|
@@ -140,6 +200,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
140
200
|
// Store conditional metadata (use index for deterministic keys)
|
|
141
201
|
const conditionalKey = `conditional_${i}`;
|
|
142
202
|
result[conditionalKey] = {
|
|
203
|
+
_key: conditionalKey,
|
|
143
204
|
type: 'conditional',
|
|
144
205
|
meta: {
|
|
145
206
|
expression,
|
|
@@ -195,59 +256,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
195
256
|
const elementForBindings = isTextNode ? element.parentElement : element;
|
|
196
257
|
const textNodeRef = isTextNode ? element : null; // Store reference to actual text node
|
|
197
258
|
|
|
198
|
-
|
|
199
|
-
const attributes = {};
|
|
200
|
-
const nameBindings = [];
|
|
201
|
-
|
|
202
|
-
// Skip hydrating attributes on fetched components - they need to be passed raw
|
|
203
|
-
const isFetchedComponent =
|
|
204
|
-
(nodeName === 'COMPONENT' || (nodeName === 'DIV' && element.classList?.contains('component'))) &&
|
|
205
|
-
element.hasAttribute('src');
|
|
206
|
-
|
|
207
|
-
if (element.attributes && !isFetchedComponent) {
|
|
208
|
-
for (let j = 0; j < element.attributes.length; j++) {
|
|
209
|
-
const attr = element.attributes[j];
|
|
210
|
-
// Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
|
|
211
|
-
BINDING_REGEX.lastIndex = 0;
|
|
212
|
-
|
|
213
|
-
// Check if attribute name contains binding (e.g., @[section.icon])
|
|
214
|
-
if (BINDING_REGEX.test(attr.name)) {
|
|
215
|
-
nameBindings.push(attr.name);
|
|
216
|
-
continue; // Don't process as regular attribute
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Rewrite event handlers with this. to use component state
|
|
220
|
-
if (attr.name.startsWith('on') && attr.value.includes('this.')) {
|
|
221
|
-
const componentId = findComponentIdForElement(element);
|
|
222
|
-
if (componentId) {
|
|
223
|
-
// Rewrite this.property to $['componentId'].property, but skip DOM properties
|
|
224
|
-
const rewritten = attr.value.replace(/\bthis\.(\w+)/g, (match, prop) => {
|
|
225
|
-
return DOM_ELEMENT_PROPERTIES.has(prop) ? match : `$['${componentId}'].${prop}`;
|
|
226
|
-
});
|
|
227
|
-
element.setAttribute(attr.name, rewritten);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Check if attribute value contains binding
|
|
232
|
-
BINDING_REGEX.lastIndex = 0;
|
|
233
|
-
if (BINDING_REGEX.test(attr.value)) {
|
|
234
|
-
attributes[attr.name] = attr.value;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Helper to find component ID for an element
|
|
240
|
-
// Looks for nearest ancestor with data-vibe-component-id
|
|
241
|
-
function findComponentIdForElement(element) {
|
|
242
|
-
if (!element) return null;
|
|
243
|
-
|
|
244
|
-
// Find nearest component wrapper (tagged by component.js)
|
|
245
|
-
const wrapper = element.closest('[data-vibe-component-id]');
|
|
246
|
-
return wrapper ? wrapper.getAttribute('data-vibe-component-id') : null;
|
|
247
|
-
}
|
|
248
|
-
const hasAttributeBindings = Object.keys(attributes).length > 0;
|
|
249
|
-
const hasNameBindings = nameBindings.length > 0;
|
|
250
|
-
|
|
259
|
+
const { attributes, nameBindings } = captureAttributeBindings(element);
|
|
251
260
|
const hasChildren = childNodes.length;
|
|
252
261
|
|
|
253
262
|
if (hasChildren) {
|
|
@@ -258,8 +267,8 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
258
267
|
parsed,
|
|
259
268
|
element: elementForBindings,
|
|
260
269
|
children: recursive(iteratableChildren, undefined, new Set(), stats),
|
|
261
|
-
...(
|
|
262
|
-
...(
|
|
270
|
+
...(attributes && { attributes }),
|
|
271
|
+
...(nameBindings && { nameBindings }),
|
|
263
272
|
...(textNodeRef && { textNode: textNodeRef }),
|
|
264
273
|
};
|
|
265
274
|
} else {
|
|
@@ -267,8 +276,8 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
267
276
|
parsed: innerHTML || textContent,
|
|
268
277
|
element: elementForBindings,
|
|
269
278
|
children: {},
|
|
270
|
-
...(
|
|
271
|
-
...(
|
|
279
|
+
...(attributes && { attributes }),
|
|
280
|
+
...(nameBindings && { nameBindings }),
|
|
272
281
|
...(textNodeRef && { textNode: textNodeRef }),
|
|
273
282
|
};
|
|
274
283
|
}
|
|
@@ -281,29 +290,7 @@ export default (root, rootKey = undefined) => {
|
|
|
281
290
|
const { childNodes } = root;
|
|
282
291
|
const stats = { skipped: 0 };
|
|
283
292
|
|
|
284
|
-
|
|
285
|
-
let attributes = null;
|
|
286
|
-
let nameBindings = null;
|
|
287
|
-
if (root.attributes && root.attributes.length > 0) {
|
|
288
|
-
for (let j = 0; j < root.attributes.length; j++) {
|
|
289
|
-
const attr = root.attributes[j];
|
|
290
|
-
BINDING_REGEX.lastIndex = 0;
|
|
291
|
-
|
|
292
|
-
// Check if attribute name contains binding
|
|
293
|
-
if (BINDING_REGEX.test(attr.name)) {
|
|
294
|
-
if (!nameBindings) nameBindings = [];
|
|
295
|
-
nameBindings.push(attr.name);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Check if attribute value contains binding
|
|
300
|
-
BINDING_REGEX.lastIndex = 0;
|
|
301
|
-
if (BINDING_REGEX.test(attr.value)) {
|
|
302
|
-
if (!attributes) attributes = {};
|
|
303
|
-
attributes[attr.name] = attr.value;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
293
|
+
const { attributes, nameBindings } = captureAttributeBindings(root);
|
|
307
294
|
|
|
308
295
|
return {
|
|
309
296
|
// html: root.outerHTML,
|
|
@@ -60,23 +60,46 @@ export const getCompiledMeta = (iterationNode) => {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
63
|
+
* Build a scoped wrapper that puts state keys in scope for the batch function.
|
|
64
|
+
* The compiler generates (arr, $) => { ... } with bare variable names like `selectedCategory`,
|
|
65
|
+
* but those aren't parameters of the arrow function. We create a wrapper that defines
|
|
66
|
+
* state keys as parameters, then evaluates the batch function in that scope.
|
|
64
67
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const buildScopedFn = (batchFnStr, stateKeys) => {
|
|
69
|
+
return new Function(
|
|
70
|
+
...stateKeys, 'arr',
|
|
71
|
+
`const $ = {${stateKeys.map(k => k + ':' + k).join(',')}};
|
|
72
|
+
const __batchFn = ${batchFnStr};
|
|
73
|
+
return __batchFn(arr, $);`
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Call a compiled batch function with state keys spread into scope
|
|
79
|
+
*/
|
|
80
|
+
const callCompiled = (iterationNode, array, state, compiledMeta) => {
|
|
67
81
|
if (!iterationNode.runtime.compiledFn) {
|
|
68
82
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
iterationNode.runtime.
|
|
83
|
+
const stateKeys = Object.keys(state);
|
|
84
|
+
iterationNode.runtime.compiledFn = buildScopedFn(compiledMeta.batchFn, stateKeys);
|
|
85
|
+
iterationNode.runtime.compiledStateKeys = stateKeys;
|
|
72
86
|
} catch (e) {
|
|
73
87
|
console.error('[compiled-iteration] Failed to create compiled function:', e);
|
|
74
|
-
return
|
|
88
|
+
return null;
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
const
|
|
92
|
+
const keys = iterationNode.runtime.compiledStateKeys;
|
|
93
|
+
const values = keys.map(k => state[k]);
|
|
94
|
+
return iterationNode.runtime.compiledFn(...values, array);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render iteration using pre-compiled batch function from manifest
|
|
99
|
+
*/
|
|
100
|
+
export const renderCompiled = (iterationNode, array, state, compiledMeta, parent, endComment) => {
|
|
101
|
+
const html = callCompiled(iterationNode, array, state, compiledMeta);
|
|
102
|
+
if (html === null) return false;
|
|
80
103
|
|
|
81
104
|
// Parse and insert
|
|
82
105
|
if (parseTemplate) {
|
|
@@ -118,18 +141,8 @@ export const updateCompiled = (iterationNode, newArray, state, compiledMeta, sta
|
|
|
118
141
|
return true;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
try {
|
|
124
|
-
iterationNode.runtime.compiledFn = new Function('return ' + compiledMeta.batchFn)();
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.error('Failed to create compiled function:', e);
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Build HTML using compiled function
|
|
132
|
-
const html = iterationNode.runtime.compiledFn(newArray, state);
|
|
144
|
+
const html = callCompiled(iterationNode, newArray, state, compiledMeta);
|
|
145
|
+
if (html === null) return false;
|
|
133
146
|
|
|
134
147
|
// Parse and insert
|
|
135
148
|
if (parseTemplate) {
|
|
@@ -134,6 +134,14 @@ const detectHyperspeed = async () => {
|
|
|
134
134
|
if (hyperspeedDetectionAttempted) return hyperspeedData;
|
|
135
135
|
hyperspeedDetectionAttempted = true;
|
|
136
136
|
|
|
137
|
+
// Runtime-mode pages still have [vibe-fouc] / .vibe-fouc on their vibe root
|
|
138
|
+
// at this point — the compiler strips it at build time, and the runtime only
|
|
139
|
+
// clears it after hydration (PHASE_READY). Vibe can latch to any element, so
|
|
140
|
+
// search the whole document. If any fouc marker is still here, we're in
|
|
141
|
+
// runtime mode and no hyperspeed manifest will exist — skip the network
|
|
142
|
+
// fetches and avoid the 404 devtools noise.
|
|
143
|
+
const skipNetwork = !!document.querySelector("[vibe-fouc], .vibe-fouc");
|
|
144
|
+
|
|
137
145
|
try {
|
|
138
146
|
let pagePath = window.location.pathname;
|
|
139
147
|
|
|
@@ -185,21 +193,38 @@ const detectHyperspeed = async () => {
|
|
|
185
193
|
);
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
if (!skipNetwork) {
|
|
197
|
+
// Fully-runtime dynamic import. Hidden behind `new Function` so any
|
|
198
|
+
// bundler's static-analysis can't read into it — there's nothing we
|
|
199
|
+
// could or should tell it about these manifest paths, which are decided
|
|
200
|
+
// at runtime by searching a list.
|
|
201
|
+
const dynamicImport = new Function('p', 'return import(p)');
|
|
202
|
+
// Try each possible path
|
|
203
|
+
for (const manifestPath of possiblePaths) {
|
|
204
|
+
try {
|
|
205
|
+
const module = await dynamicImport(manifestPath);
|
|
206
|
+
hyperspeedData = {
|
|
207
|
+
manifest: module.default,
|
|
208
|
+
path: manifestPath,
|
|
209
|
+
};
|
|
210
|
+
return hyperspeedData;
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// Try next path
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
200
215
|
}
|
|
201
216
|
}
|
|
202
217
|
|
|
218
|
+
// When skipping network, yield a macrotask so module-graph timing matches
|
|
219
|
+
// the old behavior where `await import()` on a missing manifest resolved
|
|
220
|
+
// via a network 404 (macrotask), not a microtask. Without this yield, the
|
|
221
|
+
// vibe module-graph resolves too fast and the post-boot microtask fires
|
|
222
|
+
// before sibling `<script type="module">` tags (e.g. component scripts)
|
|
223
|
+
// have had a chance to register their state.
|
|
224
|
+
if (skipNetwork) {
|
|
225
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
// No manifest found
|
|
204
229
|
return null;
|
|
205
230
|
} catch {
|
|
@@ -468,7 +493,7 @@ export const restoreMarkersFromManifest = (
|
|
|
468
493
|
if (text.startsWith("if")) {
|
|
469
494
|
if (!startComment) {
|
|
470
495
|
const expression = childTree.meta?.expression;
|
|
471
|
-
const expectedText = expression ?
|
|
496
|
+
const expectedText = expression ? "if " + expression : null;
|
|
472
497
|
if (expectedText && text === expectedText) {
|
|
473
498
|
startComment = comment;
|
|
474
499
|
conditionalDepth = 1;
|