@ape-egg/vibe 1.0.2 → 1.0.5
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 +32 -0
- package/ROADMAP.md +289 -0
- package/_vibe-compiled-iteration-batch.js +9 -8
- package/affected.js +47 -25
- package/conditionals.js +15 -24
- package/constants.js +103 -22
- package/hydrate.js +7 -19
- package/index.js +43 -32
- package/iterate.js +54 -31
- package/iteration-utils.js +17 -17
- package/link.js +1 -1
- package/package.json +1 -1
- package/parse.js +39 -32
- package/state.js +5 -5
- package/utils.js +16 -4
package/iteration-utils.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ITERATION_START_REGEX, CONDITIONAL_START_REGEX } from './constants.js';
|
|
|
4
4
|
// Resolve nested paths in state (e.g., "user.items" -> state.user.items)
|
|
5
5
|
export const resolvePath = (obj, path) => {
|
|
6
6
|
if (!path || !obj) return undefined;
|
|
7
|
-
return path.split(
|
|
7
|
+
return path.split('.').reduce((acc, part) => acc?.[part], obj);
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
// Clone template element preserving structure
|
|
@@ -16,11 +16,11 @@ export const cloneTemplate = (templateElement) => {
|
|
|
16
16
|
export const findEndComment = (nodes, startIndex) => {
|
|
17
17
|
let depth = 1;
|
|
18
18
|
for (let i = startIndex; i < nodes.length; i++) {
|
|
19
|
-
if (nodes[i].nodeName ===
|
|
19
|
+
if (nodes[i].nodeName === '#comment') {
|
|
20
20
|
const text = nodes[i].textContent.trim();
|
|
21
21
|
if (ITERATION_START_REGEX.test(text)) {
|
|
22
22
|
depth++;
|
|
23
|
-
} else if (text ===
|
|
23
|
+
} else if (text === '/each') {
|
|
24
24
|
depth--;
|
|
25
25
|
if (depth === 0) {
|
|
26
26
|
return i;
|
|
@@ -28,7 +28,7 @@ export const findEndComment = (nodes, startIndex) => {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
throw new Error(
|
|
31
|
+
throw new Error('Unmatched <!-- each --> comment: missing <!-- /each -->');
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
// Find matching <!-- /if --> and optional <!-- else --> with depth tracking
|
|
@@ -38,7 +38,7 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
38
38
|
let elseIndex = null;
|
|
39
39
|
|
|
40
40
|
for (let i = startIndex; i < nodes.length; i++) {
|
|
41
|
-
if (nodes[i].nodeName ===
|
|
41
|
+
if (nodes[i].nodeName === '#comment') {
|
|
42
42
|
const text = nodes[i].textContent.trim();
|
|
43
43
|
|
|
44
44
|
// Check for nested if
|
|
@@ -46,11 +46,11 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
46
46
|
depth++;
|
|
47
47
|
}
|
|
48
48
|
// Check for else at current depth
|
|
49
|
-
else if (text ===
|
|
49
|
+
else if (text === 'else' && depth === 1 && elseIndex === null) {
|
|
50
50
|
elseIndex = i;
|
|
51
51
|
}
|
|
52
52
|
// Check for /if
|
|
53
|
-
else if (text ===
|
|
53
|
+
else if (text === '/if') {
|
|
54
54
|
depth--;
|
|
55
55
|
if (depth === 0) {
|
|
56
56
|
return { elseIndex, endIndex: i };
|
|
@@ -59,13 +59,13 @@ export const findConditionalEnd = (nodes, startIndex) => {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
throw new Error(
|
|
62
|
+
throw new Error('Unmatched <!-- if --> comment: missing <!-- /if -->');
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
// Generate stable hash for objects
|
|
66
66
|
export const stableHash = (obj) => {
|
|
67
|
-
if (obj === null || obj === undefined) return
|
|
68
|
-
if (typeof obj !==
|
|
67
|
+
if (obj === null || obj === undefined) return 'null';
|
|
68
|
+
if (typeof obj !== 'object') return String(obj);
|
|
69
69
|
|
|
70
70
|
try {
|
|
71
71
|
// Sort keys for stable hashing
|
|
@@ -96,7 +96,7 @@ export const deepEqual = (a, b) => {
|
|
|
96
96
|
return a === b;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
if (typeof a !==
|
|
99
|
+
if (typeof a !== 'object' || typeof b !== 'object') {
|
|
100
100
|
return a === b;
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -166,12 +166,12 @@ export const longestCommonSubsequence = (arr1, arr2) => {
|
|
|
166
166
|
// Generate unique key for array items
|
|
167
167
|
export const getItemKey = (item, index) => {
|
|
168
168
|
// 1. If item has 'id' property, use it
|
|
169
|
-
if (item && typeof item ===
|
|
169
|
+
if (item && typeof item === 'object' && 'id' in item) {
|
|
170
170
|
return `id_${item.id}`;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// 2. If item is primitive, use value + index
|
|
174
|
-
if (typeof item !==
|
|
174
|
+
if (typeof item !== 'object' || item === null) {
|
|
175
175
|
return `val_${item}_${index}`;
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -204,7 +204,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
204
204
|
// Check if item content changed
|
|
205
205
|
if (!deepEqual(oldArray[oldIdx], newArray[newIdx])) {
|
|
206
206
|
operations.push({
|
|
207
|
-
type:
|
|
207
|
+
type: 'UPDATE',
|
|
208
208
|
index: newIdx,
|
|
209
209
|
oldIndex: oldIdx,
|
|
210
210
|
item: newArray[newIdx],
|
|
@@ -220,7 +220,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
220
220
|
oldKeys.forEach((key, idx) => {
|
|
221
221
|
if (!processedOld.has(idx)) {
|
|
222
222
|
operations.push({
|
|
223
|
-
type:
|
|
223
|
+
type: 'REMOVE',
|
|
224
224
|
index: idx,
|
|
225
225
|
key,
|
|
226
226
|
});
|
|
@@ -231,7 +231,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
231
231
|
newKeys.forEach((key, idx) => {
|
|
232
232
|
if (!processedNew.has(idx)) {
|
|
233
233
|
operations.push({
|
|
234
|
-
type:
|
|
234
|
+
type: 'ADD',
|
|
235
235
|
index: idx,
|
|
236
236
|
item: newArray[idx],
|
|
237
237
|
key,
|
|
@@ -246,7 +246,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
|
|
|
246
246
|
return priority[a.type] - priority[b.type];
|
|
247
247
|
}
|
|
248
248
|
// For REMOVE, process from end to beginning
|
|
249
|
-
if (a.type ===
|
|
249
|
+
if (a.type === 'REMOVE') {
|
|
250
250
|
return b.index - a.index;
|
|
251
251
|
}
|
|
252
252
|
// For others, process in order
|
package/link.js
CHANGED
package/package.json
CHANGED
package/parse.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { hash } from './utils.js'
|
|
2
|
-
import { findEndComment, findConditionalEnd } from './iteration-utils.js'
|
|
3
|
-
import {
|
|
1
|
+
import { hash } from './utils.js';
|
|
2
|
+
import { findEndComment, findConditionalEnd } from './iteration-utils.js';
|
|
3
|
+
import {
|
|
4
|
+
NON_REACTIVE_ELEMENTS,
|
|
5
|
+
BINDING_REGEX,
|
|
6
|
+
ITERATION_REGEX,
|
|
7
|
+
CONDITIONAL_REGEX,
|
|
8
|
+
} from './constants.js';
|
|
4
9
|
|
|
5
10
|
const parseHTML = (children, rootKey = undefined) =>
|
|
6
11
|
children.reduce((s, element, i) => {
|
|
7
|
-
const { nodeName, textContent } = element
|
|
12
|
+
const { nodeName, textContent } = element;
|
|
8
13
|
if (['#comment'].includes(nodeName)) {
|
|
9
|
-
return `${s}${nodeName === '#comment' ? `asd` : textContent}
|
|
14
|
+
return `${s}${nodeName === '#comment' ? `asd` : textContent}`;
|
|
10
15
|
}
|
|
11
|
-
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName
|
|
12
|
-
const innerNodeIdentifier = `${name}_${i}`.toLowerCase()
|
|
16
|
+
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
|
|
17
|
+
const innerNodeIdentifier = `${name}_${i}`.toLowerCase();
|
|
13
18
|
|
|
14
|
-
return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}
|
|
15
|
-
}, '')
|
|
19
|
+
return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`;
|
|
20
|
+
}, '');
|
|
16
21
|
|
|
17
22
|
const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
18
23
|
let result = {};
|
|
@@ -44,7 +49,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
44
49
|
|
|
45
50
|
// Create a temporary container for the template
|
|
46
51
|
const templateContainer = document.createElement('div');
|
|
47
|
-
templateNodes.forEach(node => {
|
|
52
|
+
templateNodes.forEach((node) => {
|
|
48
53
|
templateContainer.appendChild(node.cloneNode(true));
|
|
49
54
|
});
|
|
50
55
|
|
|
@@ -64,14 +69,14 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
64
69
|
template: {
|
|
65
70
|
parsed: parseHTML([...templateContainer.childNodes]),
|
|
66
71
|
element: templateContainer,
|
|
67
|
-
children: templateParsed
|
|
68
|
-
}
|
|
72
|
+
children: templateParsed,
|
|
73
|
+
},
|
|
69
74
|
},
|
|
70
75
|
runtime: {
|
|
71
76
|
instances: [],
|
|
72
|
-
templateRemoved: false
|
|
77
|
+
templateRemoved: false,
|
|
73
78
|
},
|
|
74
|
-
children: {}
|
|
79
|
+
children: {},
|
|
75
80
|
};
|
|
76
81
|
|
|
77
82
|
// Mark template indices as processed
|
|
@@ -105,7 +110,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
105
110
|
|
|
106
111
|
// Create temporary container for true branch
|
|
107
112
|
const trueBranchContainer = document.createElement('div');
|
|
108
|
-
trueBranchNodes.forEach(node => {
|
|
113
|
+
trueBranchNodes.forEach((node) => {
|
|
109
114
|
trueBranchContainer.appendChild(node.cloneNode(true));
|
|
110
115
|
});
|
|
111
116
|
|
|
@@ -118,7 +123,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
118
123
|
if (elseIndex !== null) {
|
|
119
124
|
const falseBranchNodes = Array.from(children).slice(elseIndex + 1, endIndex);
|
|
120
125
|
falseBranchContainer = document.createElement('div');
|
|
121
|
-
falseBranchNodes.forEach(node => {
|
|
126
|
+
falseBranchNodes.forEach((node) => {
|
|
122
127
|
falseBranchContainer.appendChild(node.cloneNode(true));
|
|
123
128
|
});
|
|
124
129
|
falseBranchParsed = recursive([...falseBranchContainer.childNodes]);
|
|
@@ -137,21 +142,23 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
137
142
|
if: {
|
|
138
143
|
parsed: parseHTML([...trueBranchContainer.childNodes]),
|
|
139
144
|
element: trueBranchContainer,
|
|
140
|
-
children: trueBranchParsed
|
|
145
|
+
children: trueBranchParsed,
|
|
141
146
|
},
|
|
142
|
-
else: falseBranchContainer
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
else: falseBranchContainer
|
|
148
|
+
? {
|
|
149
|
+
parsed: parseHTML([...falseBranchContainer.childNodes]),
|
|
150
|
+
element: falseBranchContainer,
|
|
151
|
+
children: falseBranchParsed,
|
|
152
|
+
}
|
|
153
|
+
: null,
|
|
154
|
+
},
|
|
148
155
|
},
|
|
149
156
|
runtime: {
|
|
150
157
|
activeBranch: undefined,
|
|
151
158
|
activeInstance: null,
|
|
152
|
-
templateRemoved: false
|
|
159
|
+
templateRemoved: false,
|
|
153
160
|
},
|
|
154
|
-
children: {}
|
|
161
|
+
children: {},
|
|
155
162
|
};
|
|
156
163
|
|
|
157
164
|
// Mark template indices as processed
|
|
@@ -199,23 +206,23 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
|
199
206
|
parsed,
|
|
200
207
|
element,
|
|
201
208
|
children: recursive(iteratableChildren),
|
|
202
|
-
...(hasAttributeBindings && { attributes })
|
|
209
|
+
...(hasAttributeBindings && { attributes }),
|
|
203
210
|
};
|
|
204
211
|
} else {
|
|
205
212
|
result[nodeIdentifier] = {
|
|
206
213
|
parsed: innerHTML || textContent,
|
|
207
214
|
element,
|
|
208
215
|
children: {},
|
|
209
|
-
...(hasAttributeBindings && { attributes })
|
|
216
|
+
...(hasAttributeBindings && { attributes }),
|
|
210
217
|
};
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
return result;
|
|
215
|
-
}
|
|
222
|
+
};
|
|
216
223
|
|
|
217
224
|
export default (root, rootKey = undefined) => {
|
|
218
|
-
const { childNodes } = root
|
|
225
|
+
const { childNodes } = root;
|
|
219
226
|
|
|
220
227
|
// Check for attribute bindings on the root element itself (only if element has attributes)
|
|
221
228
|
let attributes = null;
|
|
@@ -235,6 +242,6 @@ export default (root, rootKey = undefined) => {
|
|
|
235
242
|
parsed: parseHTML([...childNodes], rootKey),
|
|
236
243
|
element: root,
|
|
237
244
|
children: recursive(Array.from(childNodes), rootKey),
|
|
238
|
-
...(attributes && { attributes })
|
|
239
|
-
}
|
|
240
|
-
}
|
|
245
|
+
...(attributes && { attributes }),
|
|
246
|
+
};
|
|
247
|
+
};
|
package/state.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export default (state, rerender) =>
|
|
2
2
|
new Proxy(state, {
|
|
3
3
|
set(obj, prop, value) {
|
|
4
|
-
const ref = Reflect.set(...arguments)
|
|
4
|
+
const ref = Reflect.set(...arguments);
|
|
5
5
|
rerender({
|
|
6
6
|
[prop]: value,
|
|
7
|
-
})
|
|
7
|
+
});
|
|
8
8
|
// console.info("state change", { obj, prop, value });
|
|
9
|
-
return ref
|
|
9
|
+
return ref;
|
|
10
10
|
},
|
|
11
11
|
get(target, prop) {
|
|
12
12
|
// if (typeof target[prop] === 'function') {
|
|
@@ -21,6 +21,6 @@ export default (state, rerender) =>
|
|
|
21
21
|
|
|
22
22
|
// }
|
|
23
23
|
|
|
24
|
-
return Reflect.get(...arguments)
|
|
24
|
+
return Reflect.get(...arguments);
|
|
25
25
|
},
|
|
26
|
-
})
|
|
26
|
+
});
|
package/utils.js
CHANGED
|
@@ -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
|
|
@@ -9,7 +22,7 @@ export const deepMerge = (target, source) => {
|
|
|
9
22
|
if (target == null) return source;
|
|
10
23
|
|
|
11
24
|
// If source is not an object, return it
|
|
12
|
-
if (typeof source !==
|
|
25
|
+
if (typeof source !== 'object') return source;
|
|
13
26
|
|
|
14
27
|
// If source is an array, replace target array (don't merge arrays)
|
|
15
28
|
if (Array.isArray(source)) return source;
|
|
@@ -27,8 +40,8 @@ export const deepMerge = (target, source) => {
|
|
|
27
40
|
if (
|
|
28
41
|
targetValue != null &&
|
|
29
42
|
sourceValue != null &&
|
|
30
|
-
typeof targetValue ===
|
|
31
|
-
typeof sourceValue ===
|
|
43
|
+
typeof targetValue === 'object' &&
|
|
44
|
+
typeof sourceValue === 'object' &&
|
|
32
45
|
!Array.isArray(targetValue) &&
|
|
33
46
|
!Array.isArray(sourceValue)
|
|
34
47
|
) {
|
|
@@ -42,4 +55,3 @@ export const deepMerge = (target, source) => {
|
|
|
42
55
|
|
|
43
56
|
return result;
|
|
44
57
|
};
|
|
45
|
-
|