@ape-egg/vibe 1.0.3 → 1.1.1
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 +121 -0
- package/README.md +228 -23
- package/compiler/bin/vibe-compile.js +109 -0
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +1885 -0
- package/compiler/src/Cargo.toml +29 -0
- package/compiler/src/compiler/compile.rs +1209 -0
- package/compiler/src/compiler/mod.rs +5 -0
- package/compiler/src/config.rs +184 -0
- package/compiler/src/main.rs +284 -0
- package/compiler/src/parser/element.rs +96 -0
- package/compiler/src/parser/html.rs +339 -0
- package/compiler/src/parser/mod.rs +8 -0
- package/index.js +2 -233
- package/package.json +26 -3
- package/{affected.js → runtime/affected.js} +66 -14
- package/runtime/cleanup.js +59 -0
- package/runtime/component.js +116 -0
- package/{conditionals.js → runtime/conditionals.js} +27 -23
- package/{constants.js → runtime/constants.js} +23 -3
- package/runtime/debug.js +91 -0
- package/{hydrate.js → runtime/hydrate.js} +58 -20
- package/runtime/index.js +614 -0
- package/{iterate.js → runtime/iterate.js} +53 -45
- package/{iteration-utils.js → runtime/iteration-utils.js} +11 -1
- package/{parse.js → runtime/parse.js} +37 -7
- package/runtime/state.js +52 -0
- package/{utils.js → runtime/utils.js} +13 -0
- package/llms.txt +0 -279
- package/state.js +0 -26
- /package/{_vibe-compiled-iteration-batch.js → runtime/_vibe-compiled-iteration-batch.js} +0 -0
- /package/{link.js → runtime/manifest.js} +0 -0
- /package/{vibe.css → runtime/vibe.css} +0 -0
|
@@ -126,37 +126,30 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
126
126
|
let clonedNodes = [];
|
|
127
127
|
let firstElement = null;
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
tree = cloneTreeWithElements(cachedTree, { childNodes: clonedNodes });
|
|
142
|
-
} else {
|
|
143
|
-
// Slow path: clone and parse from scratch
|
|
144
|
-
for (let i = 0; i < templateNodes.length; i++) {
|
|
145
|
-
const cloned = templateNodes[i].cloneNode(true);
|
|
146
|
-
clonedNodes.push(cloned);
|
|
147
|
-
if (!firstElement && cloned.nodeType === 1) {
|
|
148
|
-
firstElement = cloned;
|
|
149
|
-
}
|
|
129
|
+
// Always use slow path: clone and parse from scratch
|
|
130
|
+
// TODO: Re-enable fast path once cloneTreeWithElements properly handles nested conditionals
|
|
131
|
+
|
|
132
|
+
// Create a container for parsing (to capture all nodes including comment nodes like <!-- if -->)
|
|
133
|
+
const parseContainer = document.createElement('div');
|
|
134
|
+
|
|
135
|
+
// Clone all template nodes into the container
|
|
136
|
+
for (let i = 0; i < templateNodes.length; i++) {
|
|
137
|
+
const cloned = templateNodes[i].cloneNode(true);
|
|
138
|
+
parseContainer.appendChild(cloned);
|
|
139
|
+
if (!firstElement && cloned.nodeType === 1) {
|
|
140
|
+
firstElement = cloned;
|
|
150
141
|
}
|
|
142
|
+
}
|
|
151
143
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
144
|
+
// Parse the entire container (includes all nodes + conditionals)
|
|
145
|
+
tree = parse(parseContainer);
|
|
146
|
+
|
|
147
|
+
// Extract the cloned nodes from the container (these are the same nodes the tree references)
|
|
148
|
+
clonedNodes = Array.from(parseContainer.childNodes);
|
|
158
149
|
|
|
159
|
-
|
|
150
|
+
// If no firstElement found, use parseContainer as fallback
|
|
151
|
+
if (!firstElement) {
|
|
152
|
+
firstElement = parseContainer;
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
if (tree) {
|
|
@@ -229,11 +222,13 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
|
229
222
|
};
|
|
230
223
|
|
|
231
224
|
// Render all iterations in the parsed tree
|
|
232
|
-
export const renderAllIterations = (tree, state,
|
|
225
|
+
export const renderAllIterations = (tree, state, manifest, parentScope = {}) => {
|
|
226
|
+
let count = 0;
|
|
227
|
+
|
|
233
228
|
// If this is an iteration node, render it
|
|
234
229
|
if (tree.type === 'iteration') {
|
|
235
|
-
renderIteration(tree, state,
|
|
236
|
-
return;
|
|
230
|
+
renderIteration(tree, state, manifest, parentScope);
|
|
231
|
+
return 1;
|
|
237
232
|
}
|
|
238
233
|
|
|
239
234
|
// Recursively render iterations in child nodes
|
|
@@ -241,10 +236,12 @@ export const renderAllIterations = (tree, state, linkList, parentScope = {}) =>
|
|
|
241
236
|
for (const key in tree.children) {
|
|
242
237
|
const child = tree.children[key];
|
|
243
238
|
if (child && typeof child === 'object') {
|
|
244
|
-
renderAllIterations(child, state,
|
|
239
|
+
count += renderAllIterations(child, state, manifest, parentScope);
|
|
245
240
|
}
|
|
246
241
|
}
|
|
247
242
|
}
|
|
243
|
+
|
|
244
|
+
return count;
|
|
248
245
|
};
|
|
249
246
|
|
|
250
247
|
// Callback for rendering conditionals - set by conditionals.js to avoid circular import
|
|
@@ -254,13 +251,20 @@ export const setRenderAllConditionals = (fn) => {
|
|
|
254
251
|
};
|
|
255
252
|
|
|
256
253
|
// Initial render of an iteration block
|
|
257
|
-
export const renderIteration = (iterationNode, state,
|
|
254
|
+
export const renderIteration = (iterationNode, state, manifest, parentScope = {}) => {
|
|
258
255
|
const { arrayPath, itemAlias, indexAlias, template, startComment, endComment } =
|
|
259
256
|
iterationNode.meta;
|
|
260
257
|
|
|
261
258
|
// Already rendered - updates go through updateIteration
|
|
262
259
|
if (iterationNode.runtime.instances?.length > 0) return;
|
|
263
260
|
|
|
261
|
+
// Check if this iteration has already been rendered
|
|
262
|
+
// We use a marker on the startComment node itself (survives re-parsing)
|
|
263
|
+
// @ts-ignore - adding custom property to comment node
|
|
264
|
+
if (startComment.__vibeRendered) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
264
268
|
// Remove template nodes from DOM on first render
|
|
265
269
|
if (!iterationNode.runtime.templateRemoved) {
|
|
266
270
|
let node = startComment.nextSibling;
|
|
@@ -304,18 +308,22 @@ export const renderIteration = (iterationNode, state, linkList, parentScope = {}
|
|
|
304
308
|
// Recursively render nested iterations and conditionals
|
|
305
309
|
if (tree) {
|
|
306
310
|
const nestedScope = { ...parentScope, ...localVars };
|
|
307
|
-
renderAllIterations(tree, scopedState,
|
|
308
|
-
_renderAllConditionals(tree, scopedState,
|
|
311
|
+
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
312
|
+
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
309
313
|
}
|
|
310
314
|
|
|
311
|
-
instances.push({ element, tree, item, index: i, clonedNodes });
|
|
315
|
+
instances.push({ element, tree, item, index: i, clonedNodes, scopedState });
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
iterationNode.runtime.instances = instances;
|
|
319
|
+
|
|
320
|
+
// Mark comment as rendered (survives re-parsing since comment nodes persist in DOM)
|
|
321
|
+
// @ts-ignore - adding custom property to comment node
|
|
322
|
+
startComment.__vibeRendered = true;
|
|
315
323
|
};
|
|
316
324
|
|
|
317
325
|
// Update an iteration block when array changes
|
|
318
|
-
export const updateIteration = (iterationNode, newState, oldState,
|
|
326
|
+
export const updateIteration = (iterationNode, newState, oldState, manifest, parentScope = {}) => {
|
|
319
327
|
if (!iterationNode.runtime.instances || !iterationNode.runtime.templateRemoved) return;
|
|
320
328
|
|
|
321
329
|
const { arrayPath, template, startComment, endComment } = iterationNode.meta;
|
|
@@ -348,13 +356,13 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
|
|
|
348
356
|
removeInstance(iterationNode, op.index);
|
|
349
357
|
break;
|
|
350
358
|
case 'ADD':
|
|
351
|
-
addInstance(iterationNode, op.item, op.index, newState,
|
|
359
|
+
addInstance(iterationNode, op.item, op.index, newState, manifest, parentScope);
|
|
352
360
|
break;
|
|
353
361
|
case 'MOVE':
|
|
354
362
|
moveInstance(iterationNode, op.from, op.to);
|
|
355
363
|
break;
|
|
356
364
|
case 'UPDATE':
|
|
357
|
-
updateInstance(iterationNode, op.index, op.item, newState,
|
|
365
|
+
updateInstance(iterationNode, op.index, op.item, newState, manifest, parentScope);
|
|
358
366
|
break;
|
|
359
367
|
}
|
|
360
368
|
});
|
|
@@ -365,7 +373,7 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
|
|
|
365
373
|
};
|
|
366
374
|
|
|
367
375
|
// Add a new instance at the specified index
|
|
368
|
-
const addInstance = (iterationNode, item, index, state,
|
|
376
|
+
const addInstance = (iterationNode, item, index, state, manifest, parentScope) => {
|
|
369
377
|
const { itemAlias, indexAlias, template, startComment, endComment } = iterationNode.meta;
|
|
370
378
|
|
|
371
379
|
const localVars = { [itemAlias]: item, [indexAlias]: index };
|
|
@@ -387,8 +395,8 @@ const addInstance = (iterationNode, item, index, state, linkList, parentScope) =
|
|
|
387
395
|
// Recursively render nested iterations and conditionals
|
|
388
396
|
if (tree) {
|
|
389
397
|
const nestedScope = { ...parentScope, ...localVars };
|
|
390
|
-
renderAllIterations(tree, scopedState,
|
|
391
|
-
_renderAllConditionals(tree, scopedState,
|
|
398
|
+
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
399
|
+
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
392
400
|
}
|
|
393
401
|
|
|
394
402
|
iterationNode.runtime.instances.splice(index, 0, { element, tree, item, index, clonedNodes });
|
|
@@ -432,7 +440,7 @@ const moveInstance = (iterationNode, fromIndex, toIndex) => {
|
|
|
432
440
|
};
|
|
433
441
|
|
|
434
442
|
// Update an instance with new item data
|
|
435
|
-
const updateInstance = (iterationNode, index, newItem, state,
|
|
443
|
+
const updateInstance = (iterationNode, index, newItem, state, manifest, parentScope = {}) => {
|
|
436
444
|
if (index < 0 || index >= iterationNode.runtime.instances.length) return;
|
|
437
445
|
|
|
438
446
|
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
@@ -455,8 +463,8 @@ const updateInstance = (iterationNode, index, newItem, state, linkList, parentSc
|
|
|
455
463
|
// Recursively render nested iterations and conditionals
|
|
456
464
|
if (tree) {
|
|
457
465
|
const nestedScope = { ...parentScope, ...localVars };
|
|
458
|
-
renderAllIterations(tree, scopedState,
|
|
459
|
-
_renderAllConditionals(tree, scopedState,
|
|
466
|
+
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
467
|
+
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
460
468
|
}
|
|
461
469
|
|
|
462
470
|
// Update instance
|
|
@@ -59,7 +59,17 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
// Build error message with context
|
|
63
|
+
const nodeTypes = nodes.slice(startIndex - 1, Math.min(startIndex + 10, nodes.length)).map((n, idx) => {
|
|
64
|
+
const actualIdx = startIndex - 1 + idx;
|
|
65
|
+
const prefix = actualIdx === startIndex - 1 ? '→ ' : ' ';
|
|
66
|
+
if (n.nodeName === '#comment') {
|
|
67
|
+
return `${prefix}[${actualIdx}] #comment: "${n.textContent.trim()}"`;
|
|
68
|
+
}
|
|
69
|
+
return `${prefix}[${actualIdx}] ${n.nodeName}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
throw new Error(`Unmatched <!-- if --> comment: missing <!-- /if -->\nSearching from index ${startIndex} in ${nodes.length} nodes\nContext:\n${nodeTypes.join('\n')}`);
|
|
63
73
|
};
|
|
64
74
|
|
|
65
75
|
// Generate stable hash for objects
|
|
@@ -19,7 +19,7 @@ const parseHTML = (children, rootKey = undefined) =>
|
|
|
19
19
|
return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`;
|
|
20
20
|
}, '');
|
|
21
21
|
|
|
22
|
-
const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
22
|
+
const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats = { skipped: 0 }) => {
|
|
23
23
|
let result = {};
|
|
24
24
|
|
|
25
25
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -31,7 +31,10 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
31
31
|
|
|
32
32
|
// Skip non-reactive elements and dehydrated elements
|
|
33
33
|
if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
|
|
34
|
-
if (element.hasAttribute?.('dehydrate'))
|
|
34
|
+
if (element.hasAttribute?.('dehydrate')) {
|
|
35
|
+
stats.skipped++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
35
38
|
|
|
36
39
|
// Handle iteration comments
|
|
37
40
|
if (nodeName === '#comment') {
|
|
@@ -54,7 +57,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
// Parse the template recursively
|
|
57
|
-
const templateParsed = recursive([...templateContainer.childNodes]);
|
|
60
|
+
const templateParsed = recursive([...templateContainer.childNodes], undefined, new Set(), stats);
|
|
58
61
|
|
|
59
62
|
// Store iteration metadata
|
|
60
63
|
const iterationKey = `iteration_${hash()}`;
|
|
@@ -115,7 +118,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
// Parse the true branch recursively
|
|
118
|
-
const trueBranchParsed = recursive([...trueBranchContainer.childNodes]);
|
|
121
|
+
const trueBranchParsed = recursive([...trueBranchContainer.childNodes], undefined, new Set(), stats);
|
|
119
122
|
|
|
120
123
|
// Extract false branch nodes if else exists
|
|
121
124
|
let falseBranchParsed = null;
|
|
@@ -126,7 +129,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
126
129
|
falseBranchNodes.forEach((node) => {
|
|
127
130
|
falseBranchContainer.appendChild(node.cloneNode(true));
|
|
128
131
|
});
|
|
129
|
-
falseBranchParsed = recursive([...falseBranchContainer.childNodes]);
|
|
132
|
+
falseBranchParsed = recursive([...falseBranchContainer.childNodes], undefined, new Set(), stats);
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
// Store conditional metadata
|
|
@@ -184,17 +187,28 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
184
187
|
|
|
185
188
|
// Check for attribute bindings
|
|
186
189
|
const attributes = {};
|
|
190
|
+
const nameBindings = [];
|
|
187
191
|
if (element.attributes) {
|
|
188
192
|
for (let j = 0; j < element.attributes.length; j++) {
|
|
189
193
|
const attr = element.attributes[j];
|
|
190
194
|
// Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
|
|
191
195
|
BINDING_REGEX.lastIndex = 0;
|
|
196
|
+
|
|
197
|
+
// Check if attribute name contains binding (e.g., @[section.icon])
|
|
198
|
+
if (BINDING_REGEX.test(attr.name)) {
|
|
199
|
+
nameBindings.push(attr.name);
|
|
200
|
+
continue; // Don't process as regular attribute
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if attribute value contains binding
|
|
204
|
+
BINDING_REGEX.lastIndex = 0;
|
|
192
205
|
if (BINDING_REGEX.test(attr.value)) {
|
|
193
206
|
attributes[attr.name] = attr.value;
|
|
194
207
|
}
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
210
|
const hasAttributeBindings = Object.keys(attributes).length > 0;
|
|
211
|
+
const hasNameBindings = nameBindings.length > 0;
|
|
198
212
|
|
|
199
213
|
const hasChildren = childNodes.length;
|
|
200
214
|
|
|
@@ -205,8 +219,9 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
205
219
|
result[rootKey || nodeIdentifier] = {
|
|
206
220
|
parsed,
|
|
207
221
|
element,
|
|
208
|
-
children: recursive(iteratableChildren),
|
|
222
|
+
children: recursive(iteratableChildren, undefined, new Set(), stats),
|
|
209
223
|
...(hasAttributeBindings && { attributes }),
|
|
224
|
+
...(hasNameBindings && { nameBindings }),
|
|
210
225
|
};
|
|
211
226
|
} else {
|
|
212
227
|
result[nodeIdentifier] = {
|
|
@@ -214,6 +229,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
214
229
|
element,
|
|
215
230
|
children: {},
|
|
216
231
|
...(hasAttributeBindings && { attributes }),
|
|
232
|
+
...(hasNameBindings && { nameBindings }),
|
|
217
233
|
};
|
|
218
234
|
}
|
|
219
235
|
}
|
|
@@ -223,13 +239,25 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
223
239
|
|
|
224
240
|
export default (root, rootKey = undefined) => {
|
|
225
241
|
const { childNodes } = root;
|
|
242
|
+
const stats = { skipped: 0 };
|
|
226
243
|
|
|
227
244
|
// Check for attribute bindings on the root element itself (only if element has attributes)
|
|
228
245
|
let attributes = null;
|
|
246
|
+
let nameBindings = null;
|
|
229
247
|
if (root.attributes && root.attributes.length > 0) {
|
|
230
248
|
for (let j = 0; j < root.attributes.length; j++) {
|
|
231
249
|
const attr = root.attributes[j];
|
|
232
250
|
BINDING_REGEX.lastIndex = 0;
|
|
251
|
+
|
|
252
|
+
// Check if attribute name contains binding
|
|
253
|
+
if (BINDING_REGEX.test(attr.name)) {
|
|
254
|
+
if (!nameBindings) nameBindings = [];
|
|
255
|
+
nameBindings.push(attr.name);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if attribute value contains binding
|
|
260
|
+
BINDING_REGEX.lastIndex = 0;
|
|
233
261
|
if (BINDING_REGEX.test(attr.value)) {
|
|
234
262
|
if (!attributes) attributes = {};
|
|
235
263
|
attributes[attr.name] = attr.value;
|
|
@@ -241,7 +269,9 @@ export default (root, rootKey = undefined) => {
|
|
|
241
269
|
// html: root.outerHTML,
|
|
242
270
|
parsed: parseHTML([...childNodes], rootKey),
|
|
243
271
|
element: root,
|
|
244
|
-
children: recursive(Array.from(childNodes), rootKey),
|
|
272
|
+
children: recursive(Array.from(childNodes), rootKey, new Set(), stats),
|
|
245
273
|
...(attributes && { attributes }),
|
|
274
|
+
...(nameBindings && { nameBindings }),
|
|
275
|
+
stats,
|
|
246
276
|
};
|
|
247
277
|
};
|
package/runtime/state.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Track which objects are already proxied to avoid double-wrapping
|
|
2
|
+
const proxyCache = new WeakMap();
|
|
3
|
+
|
|
4
|
+
// Deep proxy: recursively wrap nested objects and arrays
|
|
5
|
+
const createDeepProxy = (target, rerender, rootState = null, rootProp = null) => {
|
|
6
|
+
// For root level, rootState is the target itself
|
|
7
|
+
if (rootState === null) {
|
|
8
|
+
rootState = target;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Check cache first
|
|
12
|
+
if (proxyCache.has(target)) {
|
|
13
|
+
return proxyCache.get(target);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const proxy = new Proxy(target, {
|
|
17
|
+
set(obj, prop, value) {
|
|
18
|
+
const oldValue = obj[prop];
|
|
19
|
+
const ref = Reflect.set(obj, prop, value);
|
|
20
|
+
|
|
21
|
+
// Only trigger rerender if value actually changed
|
|
22
|
+
if (oldValue !== value) {
|
|
23
|
+
// If we're at root level, use the prop being set
|
|
24
|
+
// If we're nested, use the root prop that contains this nested object
|
|
25
|
+
const changedProp = rootProp || prop;
|
|
26
|
+
rerender({ [changedProp]: rootState[changedProp] });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return ref;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
get(target, prop) {
|
|
33
|
+
const value = Reflect.get(target, prop);
|
|
34
|
+
|
|
35
|
+
// Don't proxy non-objects, functions, or null
|
|
36
|
+
if (value === null || typeof value !== 'object' || typeof value === 'function') {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Recursively wrap nested objects/arrays
|
|
41
|
+
// If rootProp is null (we're at root level), set it to current prop
|
|
42
|
+
// Otherwise keep passing down the same rootProp
|
|
43
|
+
const topProp = rootProp === null ? prop : rootProp;
|
|
44
|
+
return createDeepProxy(value, rerender, rootState, topProp);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
proxyCache.set(target, proxy);
|
|
49
|
+
return proxy;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default (state, rerender) => createDeepProxy(state, rerender);
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
let hashCounter = 0;
|
|
3
3
|
export const hash = () => `_${hashCounter++}`;
|
|
4
4
|
|
|
5
|
+
// Evaluate expression in the context of state
|
|
6
|
+
export const evalInScope = (expr, state) => {
|
|
7
|
+
try {
|
|
8
|
+
// Normalize whitespace - collapse newlines/spaces to single space (resilient to IDE formatting)
|
|
9
|
+
const normalized = expr.replace(/\s+/g, ' ').trim();
|
|
10
|
+
const keys = Object.keys(state);
|
|
11
|
+
const values = Object.values(state);
|
|
12
|
+
return new Function(...keys, `'use strict'; return (${normalized})`)(...values);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
5
18
|
// Instead of using lodash-es as a dependency, we run our own deepMerge (mergeWith in lodash)
|
|
6
19
|
export const deepMerge = (target, source) => {
|
|
7
20
|
// Handle null/undefined
|
package/llms.txt
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
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
|
-
- Surgical DOM updates (only affected elements re-render)
|
|
12
|
-
- MutationObserver for dynamic element tracking
|
|
13
|
-
- Works with vanilla HTML - no special file format
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @ape-egg/vibe
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
```html
|
|
24
|
-
<script type="module">
|
|
25
|
-
import state from "@ape-egg/vibe";
|
|
26
|
-
window.$ = state({ name: "World", count: 0 });
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<h1>Hello, @[name]!</h1>
|
|
30
|
-
<button onclick="$.count++">Clicked @[count] times</button>
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Core Syntax
|
|
34
|
-
|
|
35
|
-
### Reactive Bindings
|
|
36
|
-
|
|
37
|
-
Use `@[property]` syntax anywhere in HTML or CSS:
|
|
38
|
-
|
|
39
|
-
```html
|
|
40
|
-
<!-- Text content -->
|
|
41
|
-
<div>@[firstName]</div>
|
|
42
|
-
|
|
43
|
-
<!-- Expressions -->
|
|
44
|
-
<div>@[firstName + ' ' + lastName]</div>
|
|
45
|
-
<div>@[count * 2]</div>
|
|
46
|
-
|
|
47
|
-
<!-- Attributes -->
|
|
48
|
-
<input value="@[inputValue]">
|
|
49
|
-
<button disabled="@[isLoading]">Submit</button>
|
|
50
|
-
|
|
51
|
-
<!-- CSS -->
|
|
52
|
-
<style>
|
|
53
|
-
.box { background: @[themeColor]; }
|
|
54
|
-
</style>
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
⚠️ **Important**: Bindings are evaluated using `new Function()`. Do not bind untrusted user input.
|
|
58
|
-
|
|
59
|
-
### State Access
|
|
60
|
-
|
|
61
|
-
State is accessed globally via `window.$`:
|
|
62
|
-
|
|
63
|
-
```javascript
|
|
64
|
-
// Read
|
|
65
|
-
console.log($.firstName);
|
|
66
|
-
|
|
67
|
-
// Write (triggers re-render)
|
|
68
|
-
$.firstName = "John";
|
|
69
|
-
|
|
70
|
-
// Increment
|
|
71
|
-
$.count++;
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Control Flow
|
|
75
|
-
|
|
76
|
-
### Iteration
|
|
77
|
-
|
|
78
|
-
```html
|
|
79
|
-
<!-- each items as item -->
|
|
80
|
-
<li>@[item]</li>
|
|
81
|
-
<!-- /each -->
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
With index:
|
|
85
|
-
|
|
86
|
-
```html
|
|
87
|
-
<!-- each items as item, index -->
|
|
88
|
-
<li>@[index]: @[item]</li>
|
|
89
|
-
<!-- /each -->
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Nested Iteration
|
|
93
|
-
|
|
94
|
-
Use dot paths for nested arrays:
|
|
95
|
-
|
|
96
|
-
```html
|
|
97
|
-
<!-- each categories as category -->
|
|
98
|
-
<h2>@[category.name]</h2>
|
|
99
|
-
<!-- each category.items as item -->
|
|
100
|
-
<span>@[item.name]</span>
|
|
101
|
-
<!-- /each -->
|
|
102
|
-
<!-- /each -->
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Conditionals
|
|
106
|
-
|
|
107
|
-
```html
|
|
108
|
-
<!-- if isLoggedIn -->
|
|
109
|
-
<span>Welcome, @[username]!</span>
|
|
110
|
-
<!-- else -->
|
|
111
|
-
<span>Please log in</span>
|
|
112
|
-
<!-- /if -->
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Conditionals can be nested inside iterations and vice versa.
|
|
116
|
-
|
|
117
|
-
## Special Attributes
|
|
118
|
-
|
|
119
|
-
### Dehydrate
|
|
120
|
-
|
|
121
|
-
Skip reactive processing for an element and its children:
|
|
122
|
-
|
|
123
|
-
```html
|
|
124
|
-
<code dehydrate>@[this] displays literally, not parsed</code>
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Use cases:
|
|
128
|
-
- Displaying `@[...]` syntax in documentation
|
|
129
|
-
- Static content that shouldn't be reactive
|
|
130
|
-
- Performance optimization for large static sections
|
|
131
|
-
|
|
132
|
-
### Boolean Attributes
|
|
133
|
-
|
|
134
|
-
Attributes not in the value whitelist are removed when falsy:
|
|
135
|
-
|
|
136
|
-
```html
|
|
137
|
-
<button disabled="@[isLoading]">Submit</button>
|
|
138
|
-
<!-- When isLoading is false, disabled attribute is removed entirely -->
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## Events
|
|
142
|
-
|
|
143
|
-
Use standard inline event handlers:
|
|
144
|
-
|
|
145
|
-
```html
|
|
146
|
-
<button onclick="$.count++">Increment</button>
|
|
147
|
-
<input oninput="$.text = this.value">
|
|
148
|
-
<form onsubmit="event.preventDefault(); handleSubmit()">
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Styling
|
|
152
|
-
|
|
153
|
-
### CSS Bindings
|
|
154
|
-
|
|
155
|
-
Reactive values work inside `<style>` tags:
|
|
156
|
-
|
|
157
|
-
```html
|
|
158
|
-
<style>
|
|
159
|
-
.box {
|
|
160
|
-
background: @[backgroundColor];
|
|
161
|
-
color: @[textColor];
|
|
162
|
-
width: @[width]px;
|
|
163
|
-
}
|
|
164
|
-
</style>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Preventing FOUC
|
|
168
|
-
|
|
169
|
-
Hide content until hydration completes:
|
|
170
|
-
|
|
171
|
-
```html
|
|
172
|
-
<body style="visibility: hidden;">
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Or use the included CSS:
|
|
176
|
-
|
|
177
|
-
```html
|
|
178
|
-
<link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
|
|
179
|
-
<body vibe>
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Dynamic Elements
|
|
183
|
-
|
|
184
|
-
Elements added via JavaScript are automatically hydrated through MutationObserver:
|
|
185
|
-
|
|
186
|
-
```javascript
|
|
187
|
-
const div = document.createElement('div');
|
|
188
|
-
div.innerHTML = '<span>Hello, @[name]!</span>';
|
|
189
|
-
document.body.appendChild(div);
|
|
190
|
-
// Automatically becomes reactive
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## API Reference
|
|
194
|
-
|
|
195
|
-
### `state(initialState, afterUpdate?)`
|
|
196
|
-
|
|
197
|
-
Creates reactive state and initializes the framework.
|
|
198
|
-
|
|
199
|
-
```javascript
|
|
200
|
-
import state from "@ape-egg/vibe";
|
|
201
|
-
|
|
202
|
-
window.$ = state(
|
|
203
|
-
{ count: 0, user: { name: "Alice" } },
|
|
204
|
-
(newState, oldState) => {
|
|
205
|
-
console.log("State updated:", newState);
|
|
206
|
-
}
|
|
207
|
-
);
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Parameters:**
|
|
211
|
-
- `initialState` - Object containing initial state values
|
|
212
|
-
- `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
|
|
213
|
-
|
|
214
|
-
**Returns:** Proxy object for reactive state access
|
|
215
|
-
|
|
216
|
-
## Scoped Variables
|
|
217
|
-
|
|
218
|
-
Inside `<!-- each -->` blocks, these variables are available:
|
|
219
|
-
- `item` (or custom name) - current array element
|
|
220
|
-
- `index` (or custom name) - current index
|
|
221
|
-
- Parent state remains accessible via `$`
|
|
222
|
-
|
|
223
|
-
```html
|
|
224
|
-
<!-- each users as user, i -->
|
|
225
|
-
<div>@[i]: @[user.name] (total: @[users.length])</div>
|
|
226
|
-
<!-- /each -->
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## Architecture
|
|
230
|
-
|
|
231
|
-
Vibe consists of these core modules:
|
|
232
|
-
|
|
233
|
-
- **state.js** - Proxy-based reactive state container
|
|
234
|
-
- **parse.js** - DOM parser that finds `@[...]` bindings
|
|
235
|
-
- **link.js** - Maps elements to parsed tree nodes
|
|
236
|
-
- **hydrate.js** - Updates DOM with current state values
|
|
237
|
-
- **affected.js** - Determines which elements need updating
|
|
238
|
-
- **iterate.js** - Array rendering with efficient diffing
|
|
239
|
-
- **conditionals.js** - Conditional block rendering
|
|
240
|
-
|
|
241
|
-
## How It Works
|
|
242
|
-
|
|
243
|
-
```
|
|
244
|
-
1. state() initializes the Proxy and framework
|
|
245
|
-
2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
|
|
246
|
-
3. link.js maps elements to the parsed tree
|
|
247
|
-
4. hydrate.js replaces bindings with values
|
|
248
|
-
5. iterate.js renders <!-- each --> loops
|
|
249
|
-
6. conditionals.js renders <!-- if --> blocks
|
|
250
|
-
7. MutationObserver watches for new elements
|
|
251
|
-
8. On state change: affected.js finds changed elements → hydrate.js updates them
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Current Limitations
|
|
255
|
-
|
|
256
|
-
- **Top-level reactivity only**: `$.nested.prop = value` doesn't trigger updates (must replace parent object)
|
|
257
|
-
- **No computed values**: Derived state must be calculated manually
|
|
258
|
-
- **No two-way binding sugar**: Must wire input events manually
|
|
259
|
-
- **Expression security**: `new Function()` evaluation - don't bind untrusted input
|
|
260
|
-
|
|
261
|
-
## Best Practices
|
|
262
|
-
|
|
263
|
-
1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
|
|
264
|
-
2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
|
|
265
|
-
3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
|
|
266
|
-
4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
|
|
267
|
-
5. **Replace objects for deep updates**: `$.user = { ...$.user, name: "New" }`
|
|
268
|
-
|
|
269
|
-
## Browser Support
|
|
270
|
-
|
|
271
|
-
Modern browsers with:
|
|
272
|
-
- Proxy (ES6)
|
|
273
|
-
- MutationObserver
|
|
274
|
-
- ES Modules
|
|
275
|
-
|
|
276
|
-
## Resources
|
|
277
|
-
|
|
278
|
-
- **Homepage**: https://vibe.korte.kim
|
|
279
|
-
- **npm**: https://www.npmjs.com/package/@ape-egg/vibe
|