@ape-egg/vibe 1.3.2 → 1.6.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 +195 -0
- package/README.md +214 -16
- package/boot.js +0 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +720 -41
- package/compiler/src/Cargo.toml +12 -3
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +562 -180
- package/compiler/src/compiler/component_tagger.rs +234 -0
- package/compiler/src/compiler/iteration_optimizer.rs +351 -0
- package/compiler/src/compiler/js_analyzer.rs +572 -0
- package/compiler/src/compiler/manifest_builder.rs +251 -26
- package/compiler/src/compiler/mod.rs +5 -1
- package/compiler/src/compiler/state_extractor.rs +140 -25
- package/compiler/src/compiler/value_stamper.rs +579 -88
- package/compiler/src/compiler/watcher.rs +579 -0
- package/compiler/src/config.rs +51 -8
- package/compiler/src/main.rs +47 -31
- package/compiler/src/parser/html.rs +229 -118
- package/component.js +23 -11
- package/package.json +1 -1
- package/runtime/cleanup.js +4 -4
- package/runtime/component.js +98 -21
- package/runtime/conditionals.js +2 -2
- package/runtime/constants.js +2 -1
- package/runtime/index.js +152 -30
- package/runtime/iterate.js +27 -5
- package/runtime/parse.js +2 -1
- package/runtime/pre-compiled-iterations.js +153 -0
- package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +219 -133
- package/runtime/utils.js +2 -1
- package/test-results/.last-run.json +4 -0
- package/vibe.css +19 -0
- package/runtime/component-state.js +0 -63
package/runtime/component.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { debugLog } from './debug.js';
|
|
2
|
-
import { PHASE_FETCH } from './constants.js';
|
|
2
|
+
import { PHASE_FETCH, DEHYDRATE_CLASS_OR_ATTR } from './constants.js';
|
|
3
3
|
import { evalInScope } from './utils.js';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// Deterministic component counter
|
|
6
|
+
let componentCounter = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate unique component ID
|
|
10
|
+
* Uses deterministic counter: _c0, _c1, _c2, etc.
|
|
11
|
+
*/
|
|
12
|
+
export const generateComponentId = () => {
|
|
13
|
+
return `_c${componentCounter++}`;
|
|
14
|
+
};
|
|
5
15
|
|
|
6
16
|
// Helper to escape regex special characters
|
|
7
17
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -32,6 +42,19 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
32
42
|
|
|
33
43
|
// Process just the first element - MutationObserver will trigger next call
|
|
34
44
|
const el = componentElements[0];
|
|
45
|
+
|
|
46
|
+
// Skip if component is dehydrated (vibe-dehydrate attribute or class)
|
|
47
|
+
if (el.hasAttribute(DEHYDRATE_CLASS_OR_ATTR) || el.classList.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
48
|
+
// Skip this component and continue to next
|
|
49
|
+
if (componentElements.length > 1) {
|
|
50
|
+
// Process next component
|
|
51
|
+
processComponent(rootElement, onComplete, config);
|
|
52
|
+
} else {
|
|
53
|
+
if (onComplete) onComplete();
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
const src = el.getAttribute('src');
|
|
36
59
|
|
|
37
60
|
// Capture children and props before fetching
|
|
@@ -54,39 +77,93 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
54
77
|
const temp = document.createElement('div');
|
|
55
78
|
temp.innerHTML = html;
|
|
56
79
|
|
|
57
|
-
// Process any <script type="
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
// Process any <script type="module"> elements
|
|
81
|
+
const moduleScripts = temp.querySelectorAll('script[type="module"]');
|
|
82
|
+
moduleScripts.forEach((script) => {
|
|
83
|
+
let scriptContent = script.textContent?.trim() || '';
|
|
61
84
|
if (!scriptContent) return;
|
|
62
85
|
|
|
63
|
-
//
|
|
86
|
+
// Strip import statements (we provide component() manually)
|
|
87
|
+
// Remove lines like: import component from '...';
|
|
88
|
+
scriptContent = scriptContent.replace(/import\s+\w+\s+from\s+['"][^'"]+['"];?\s*/g, '');
|
|
89
|
+
|
|
90
|
+
// Generate component ID for this instance
|
|
64
91
|
const componentId = generateComponentId();
|
|
65
92
|
|
|
66
|
-
//
|
|
67
|
-
|
|
93
|
+
// Provide a component() function that registers state for this component
|
|
94
|
+
const componentFn = (state) => {
|
|
95
|
+
// Register component state in both places:
|
|
96
|
+
// 1. __vibeComponents registry (for pre-boot components)
|
|
97
|
+
if (!window.__vibeComponents) {
|
|
98
|
+
window.__vibeComponents = {};
|
|
99
|
+
}
|
|
100
|
+
window.__vibeComponents[componentId] = state;
|
|
101
|
+
|
|
102
|
+
// 2. Directly in window.$ (the reactive proxy) for post-boot components
|
|
103
|
+
if (window.$) {
|
|
104
|
+
window.$[componentId] = state;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Tag all siblings (everything after the script in this component)
|
|
108
|
+
let sibling = script.nextElementSibling;
|
|
109
|
+
while (sibling) {
|
|
110
|
+
// Stop if we hit another module script
|
|
111
|
+
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
sibling.setAttribute('data-vibe-component-id', componentId);
|
|
115
|
+
sibling = sibling.nextElementSibling;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Execute script with component() function in scope
|
|
120
|
+
// Use Function constructor to provide 'component' as a parameter
|
|
121
|
+
try {
|
|
122
|
+
const executeFn = new Function('component', scriptContent);
|
|
123
|
+
executeFn(componentFn);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn('[vibe] Failed to execute component script:', e);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Rewrite this.property to componentId.property in siblings
|
|
129
|
+
// This allows the runtime to resolve component-scoped bindings
|
|
130
|
+
const rewriteThisBindings = (element) => {
|
|
131
|
+
const thisRegex = /@\[this\.(\w+)\]/g;
|
|
132
|
+
|
|
133
|
+
// Rewrite in text nodes
|
|
134
|
+
Array.from(element.childNodes).forEach(node => {
|
|
135
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('@[this.')) {
|
|
136
|
+
node.textContent = node.textContent.replace(thisRegex, `@[${componentId}.$1]`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Rewrite in attributes
|
|
141
|
+
Array.from(element.attributes || []).forEach(attr => {
|
|
142
|
+
if (attr.value.includes('@[this.')) {
|
|
143
|
+
attr.value = attr.value.replace(thisRegex, `@[${componentId}.$1]`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Recurse into children
|
|
148
|
+
Array.from(element.children).forEach(child => {
|
|
149
|
+
rewriteThisBindings(child);
|
|
150
|
+
});
|
|
151
|
+
};
|
|
68
152
|
|
|
69
|
-
// Tag following siblings with this component ID
|
|
70
153
|
let sibling = script.nextElementSibling;
|
|
71
154
|
while (sibling) {
|
|
72
|
-
|
|
73
|
-
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'component') {
|
|
155
|
+
if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
|
|
74
156
|
break;
|
|
75
157
|
}
|
|
76
|
-
sibling
|
|
158
|
+
rewriteThisBindings(sibling);
|
|
77
159
|
sibling = sibling.nextElementSibling;
|
|
78
160
|
}
|
|
79
161
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Register in global state
|
|
84
|
-
if (window.$) {
|
|
85
|
-
window.$[componentId] = componentState;
|
|
86
|
-
}
|
|
162
|
+
// Remove script from temp (we executed it manually)
|
|
163
|
+
script.remove();
|
|
87
164
|
});
|
|
88
165
|
|
|
89
|
-
// Get transformed HTML from temp container
|
|
166
|
+
// Get transformed HTML from temp container (scripts removed)
|
|
90
167
|
let transformedHtml = temp.innerHTML;
|
|
91
168
|
|
|
92
169
|
// Replace props
|
package/runtime/conditionals.js
CHANGED
|
@@ -58,8 +58,8 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Remove original template nodes from DOM (between start and end comments)
|
|
61
|
-
// Only do this on first render (when
|
|
62
|
-
if (node.runtime.
|
|
61
|
+
// Only do this on first render (when template hasn't been removed yet)
|
|
62
|
+
if (!node.runtime.templateRemoved) {
|
|
63
63
|
let currentNode = startComment.nextSibling;
|
|
64
64
|
while (currentNode && currentNode !== endComment) {
|
|
65
65
|
const nextNode = currentNode.nextSibling;
|
package/runtime/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Debug logger name
|
|
2
2
|
export const DEBUGGER_NAME = '[vibe-debug]:';
|
|
3
3
|
export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
|
|
4
|
+
export const DEHYDRATE_CLASS_OR_ATTR = 'vibe-dehydrate'; // Class or attribute used to skip reactive processing
|
|
4
5
|
|
|
5
6
|
// Lifecycle phase names for debug logging
|
|
6
7
|
// ONE-OFF operations (run once during initialization)
|
|
@@ -17,7 +18,7 @@ export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (condi
|
|
|
17
18
|
export const PHASE_FETCH = 'Fetched'; // Loads <component> content (component.js)
|
|
18
19
|
export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
|
|
19
20
|
export const PHASE_MUTATE = 'Mutation'; // DOM mutations detected by observer (index.js)
|
|
20
|
-
export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (
|
|
21
|
+
export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (pre-compiled-manifest.js)
|
|
21
22
|
|
|
22
23
|
// Elements that should not have reactive bindings
|
|
23
24
|
// Note: COMPONENT is NOT in this list - inline component wrappers need to be parsed
|
package/runtime/index.js
CHANGED
|
@@ -18,16 +18,17 @@ import {
|
|
|
18
18
|
PHASE_UPDATE,
|
|
19
19
|
PHASE_MUTATE,
|
|
20
20
|
PHASE_HYPERSPEED,
|
|
21
|
+
DEHYDRATE_CLASS_OR_ATTR,
|
|
21
22
|
} from './constants.js';
|
|
22
23
|
import { processComponent, abortComponentFetch } from './component.js';
|
|
23
24
|
import { debugLog } from './debug.js';
|
|
24
25
|
import { shouldCleanup, cleanup } from './cleanup.js';
|
|
25
|
-
import { generateComponentId, executeComponentScript } from './component-state.js';
|
|
26
26
|
import {
|
|
27
27
|
buildHyperspeedManifest,
|
|
28
28
|
hyperspeedManifest,
|
|
29
|
+
hyperspeedPath,
|
|
29
30
|
restoreMarkersFromManifest,
|
|
30
|
-
} from './
|
|
31
|
+
} from './pre-compiled-manifest.js';
|
|
31
32
|
|
|
32
33
|
// Wire up cross-module dependency after all modules are loaded
|
|
33
34
|
setRenderAllConditionals(renderAllConditionals);
|
|
@@ -54,7 +55,10 @@ const shouldProcessNode = (node) => {
|
|
|
54
55
|
if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
|
|
55
56
|
return false;
|
|
56
57
|
}
|
|
57
|
-
if (
|
|
58
|
+
if (
|
|
59
|
+
current.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) ||
|
|
60
|
+
current.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)
|
|
61
|
+
) {
|
|
58
62
|
return false;
|
|
59
63
|
}
|
|
60
64
|
current = current.parentElement;
|
|
@@ -183,13 +187,14 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
183
187
|
if (!hyperspeedTree) return runtimeTree;
|
|
184
188
|
if (!runtimeTree) return hyperspeedTree;
|
|
185
189
|
|
|
186
|
-
//
|
|
187
|
-
const merged =
|
|
190
|
+
// hyperspeedTree is already a clone (done before restoration to avoid mutation)
|
|
191
|
+
const merged = hyperspeedTree;
|
|
188
192
|
|
|
189
193
|
// Helper to recursively augment hyperspeed with runtime data
|
|
190
194
|
const augmentWithRuntime = (mergedNode, runtimeNode) => {
|
|
191
195
|
if (!runtimeNode) return;
|
|
192
196
|
|
|
197
|
+
// Debug: log node types
|
|
193
198
|
// Populate DOM references from runtime
|
|
194
199
|
if (runtimeNode.element) {
|
|
195
200
|
mergedNode.element = runtimeNode.element;
|
|
@@ -200,18 +205,43 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
200
205
|
mergedNode.parsed = runtimeNode.parsed;
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
// Populate name bindings from runtime (detected after restoration)
|
|
209
|
+
if (runtimeNode.nameBindings) {
|
|
210
|
+
mergedNode.nameBindings = runtimeNode.nameBindings;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Populate attributes from runtime (detected after restoration)
|
|
214
|
+
if (runtimeNode.attributes) {
|
|
215
|
+
mergedNode.attributes = runtimeNode.attributes;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Populate textNode reference from runtime (for text node children)
|
|
219
|
+
if (runtimeNode.textNode) {
|
|
220
|
+
mergedNode.textNode = runtimeNode.textNode;
|
|
221
|
+
}
|
|
222
|
+
|
|
203
223
|
// For iterations: populate all meta and runtime from runtime
|
|
204
224
|
if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
|
|
225
|
+
// Preserve compiled data from manifest (has compiled batch function)
|
|
226
|
+
const compiledData = mergedNode.compiled;
|
|
227
|
+
|
|
205
228
|
// Copy entire meta object from runtime (all properties needed)
|
|
206
229
|
mergedNode.meta = runtimeNode.meta;
|
|
207
230
|
// Copy runtime object (instances, etc.)
|
|
208
231
|
mergedNode.runtime = runtimeNode.runtime;
|
|
232
|
+
|
|
233
|
+
// Restore compiled data if it was present
|
|
234
|
+
if (compiledData) {
|
|
235
|
+
mergedNode.compiled = compiledData;
|
|
236
|
+
}
|
|
209
237
|
}
|
|
210
238
|
|
|
211
|
-
// For conditionals: populate all meta from runtime
|
|
239
|
+
// For conditionals: populate all meta and runtime from runtime
|
|
212
240
|
if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
|
|
213
241
|
// Copy entire meta object from runtime (all properties needed)
|
|
214
242
|
mergedNode.meta = runtimeNode.meta;
|
|
243
|
+
// Copy runtime object (activeBranch, activeInstance, templateRemoved)
|
|
244
|
+
mergedNode.runtime = runtimeNode.runtime;
|
|
215
245
|
}
|
|
216
246
|
|
|
217
247
|
// Augment children recursively
|
|
@@ -221,10 +251,40 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
|
|
|
221
251
|
for (const key in runtimeNode.children) {
|
|
222
252
|
const runtimeChild = runtimeNode.children[key];
|
|
223
253
|
|
|
224
|
-
//
|
|
225
|
-
// Iterations are dynamic and restoration changes DOM structure
|
|
254
|
+
// For iterations: augment existing node, don't replace
|
|
226
255
|
if (runtimeChild.type === 'iteration') {
|
|
256
|
+
const hyperspeedChild = mergedNode.children[key];
|
|
257
|
+
|
|
258
|
+
if (hyperspeedChild) {
|
|
259
|
+
// Preserve compiled data from hyperspeed
|
|
260
|
+
const compiledData = hyperspeedChild.compiled;
|
|
261
|
+
|
|
262
|
+
// Update meta and runtime from runtime node
|
|
263
|
+
mergedNode.children[key].meta = runtimeChild.meta;
|
|
264
|
+
mergedNode.children[key].runtime = runtimeChild.runtime;
|
|
265
|
+
|
|
266
|
+
// Keep compiled data from hyperspeed (it has batchFn)
|
|
267
|
+
if (compiledData) {
|
|
268
|
+
mergedNode.children[key].compiled = compiledData;
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
// No hyperspeed node, just use runtime
|
|
272
|
+
mergedNode.children[key] = runtimeChild;
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// For conditionals: use runtime node but preserve compiled data
|
|
278
|
+
if (runtimeChild.type === 'conditional') {
|
|
279
|
+
const hyperspeedChild = mergedNode.children[key];
|
|
280
|
+
const compiledData = hyperspeedChild?.compiled;
|
|
281
|
+
|
|
227
282
|
mergedNode.children[key] = runtimeChild;
|
|
283
|
+
|
|
284
|
+
// Restore compiled data if it existed
|
|
285
|
+
if (compiledData) {
|
|
286
|
+
mergedNode.children[key].compiled = compiledData;
|
|
287
|
+
}
|
|
228
288
|
continue;
|
|
229
289
|
}
|
|
230
290
|
|
|
@@ -277,7 +337,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
277
337
|
}
|
|
278
338
|
}
|
|
279
339
|
|
|
280
|
-
debugLog(PHASE_ATTACH, `Vibe attached to`,
|
|
340
|
+
debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
|
|
281
341
|
|
|
282
342
|
// Component tagging is now handled by component.js before boot
|
|
283
343
|
// Component state is merged into s by boot.js
|
|
@@ -286,7 +346,38 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
286
346
|
// This allows pre-rendered values to be visible (no FOUC) but makes DOM reactive
|
|
287
347
|
let hyperspeedSubtree = null;
|
|
288
348
|
if (hyperspeedTree) {
|
|
289
|
-
|
|
349
|
+
const manifestName = hyperspeedPath ? hyperspeedPath.split('/').pop() : 'manifest.js';
|
|
350
|
+
|
|
351
|
+
// Count compiled features
|
|
352
|
+
const countCompiledIterations = (tree) => {
|
|
353
|
+
let count = 0;
|
|
354
|
+
if (tree.type === 'iteration' && tree.compiled?.iterations?.batchFn) count++;
|
|
355
|
+
if (tree.children) {
|
|
356
|
+
for (const key in tree.children) {
|
|
357
|
+
count += countCompiledIterations(tree.children[key]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return count;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const compiledIterationCount = countCompiledIterations(hyperspeedTree);
|
|
364
|
+
|
|
365
|
+
// Log features that are enabled
|
|
366
|
+
debugLog(PHASE_HYPERSPEED, `Loaded ${manifestName}, page is pre-compiled`, debug);
|
|
367
|
+
|
|
368
|
+
if (compiledIterationCount > 0) {
|
|
369
|
+
debugLog(
|
|
370
|
+
PHASE_HYPERSPEED,
|
|
371
|
+
[
|
|
372
|
+
{ text: String(compiledIterationCount), colored: true },
|
|
373
|
+
{
|
|
374
|
+
text: ` ${compiledIterationCount === 1 ? 'iteration' : 'iterations'} optimized`,
|
|
375
|
+
colored: false,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
debug,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
290
381
|
|
|
291
382
|
// Find matching subtree by DOM path (not just tag name)
|
|
292
383
|
// Build path from document to rootElement
|
|
@@ -347,14 +438,19 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
347
438
|
};
|
|
348
439
|
|
|
349
440
|
const domPath = buildDomPath(rootElement);
|
|
350
|
-
|
|
441
|
+
let subtreeFromManifest = findManifestNodeByPath(hyperspeedTree, domPath);
|
|
351
442
|
|
|
352
|
-
if (!
|
|
443
|
+
if (!subtreeFromManifest) {
|
|
353
444
|
// Fallback to root if path matching fails
|
|
354
|
-
|
|
445
|
+
subtreeFromManifest = hyperspeedTree;
|
|
355
446
|
}
|
|
356
447
|
|
|
357
|
-
|
|
448
|
+
// IMPORTANT: Clone BEFORE restoration because restoration mutates the tree
|
|
449
|
+
// We need TWO clones: one for restoration (gets mutated), one for merging (stays intact)
|
|
450
|
+
hyperspeedSubtree = deepCloneNode(subtreeFromManifest); // For merging
|
|
451
|
+
const cloneForRestoration = deepCloneNode(subtreeFromManifest); // For restoration
|
|
452
|
+
|
|
453
|
+
restoreMarkersFromManifest(rootElement, cloneForRestoration, hyperspeedTree);
|
|
358
454
|
}
|
|
359
455
|
|
|
360
456
|
// Runtime parses DOM (which now has restored markers if hyperspeed was used)
|
|
@@ -379,9 +475,9 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
379
475
|
|
|
380
476
|
const segments = [
|
|
381
477
|
{ text: `${elementsOnly}`, colored: true },
|
|
382
|
-
{ text: ' elements (
|
|
478
|
+
{ text: ` ${elementsOnly === 1 ? 'element' : 'elements'} (`, colored: false },
|
|
383
479
|
{ text: `${totalCount}`, color: 'slate' },
|
|
384
|
-
{ text:
|
|
480
|
+
{ text: ` total ${totalCount === 1 ? 'node' : 'nodes'})`, colored: false },
|
|
385
481
|
];
|
|
386
482
|
if (parsedTree.stats?.skipped > 0) {
|
|
387
483
|
segments.push(
|
|
@@ -483,7 +579,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
483
579
|
debugLog(
|
|
484
580
|
PHASE_HYDRATE,
|
|
485
581
|
[
|
|
486
|
-
{
|
|
582
|
+
{
|
|
583
|
+
text: `${affectedElements.length} ${affectedElements.length === 1 ? 'binding' : 'bindings'} (`,
|
|
584
|
+
colored: false,
|
|
585
|
+
},
|
|
487
586
|
{ text: '@[...]', color: 'pink' },
|
|
488
587
|
{ text: ')', colored: false },
|
|
489
588
|
],
|
|
@@ -501,7 +600,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
501
600
|
debugLog(
|
|
502
601
|
PHASE_ITERATE,
|
|
503
602
|
[
|
|
504
|
-
{
|
|
603
|
+
{
|
|
604
|
+
text: `${iterationCount} ${iterationCount === 1 ? 'iteration' : 'iterations'} (`,
|
|
605
|
+
colored: false,
|
|
606
|
+
},
|
|
505
607
|
{ text: '<!-- each -->', color: 'commentGreen' },
|
|
506
608
|
{ text: ')', colored: false },
|
|
507
609
|
],
|
|
@@ -513,7 +615,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
513
615
|
debugLog(
|
|
514
616
|
PHASE_CONDITION,
|
|
515
617
|
[
|
|
516
|
-
{
|
|
618
|
+
{
|
|
619
|
+
text: `${conditionalCount} ${conditionalCount === 1 ? 'conditional' : 'conditionals'} (`,
|
|
620
|
+
colored: false,
|
|
621
|
+
},
|
|
517
622
|
{ text: '<!-- if -->', color: 'commentGreen' },
|
|
518
623
|
{ text: ')', colored: false },
|
|
519
624
|
],
|
|
@@ -667,25 +772,31 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
667
772
|
// Added
|
|
668
773
|
if (addedElements > 0) {
|
|
669
774
|
segments.push({ text: `+${addedElements}`, color: 'green' });
|
|
670
|
-
segments.push({
|
|
775
|
+
segments.push({
|
|
776
|
+
text: ` ${addedElements === 1 ? 'element' : 'elements'}`,
|
|
777
|
+
colored: false,
|
|
778
|
+
});
|
|
671
779
|
}
|
|
672
780
|
if (addedNodes > 0) {
|
|
673
781
|
if (addedElements > 0) segments.push({ text: ', ', colored: false });
|
|
674
782
|
segments.push({ text: `+${addedNodes}`, color: 'slate' });
|
|
675
|
-
segments.push({ text: ' nodes'
|
|
783
|
+
segments.push({ text: ` ${addedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
|
|
676
784
|
}
|
|
677
785
|
|
|
678
786
|
// Removed
|
|
679
787
|
if (removedElements > 0) {
|
|
680
788
|
if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
|
|
681
789
|
segments.push({ text: `-${removedElements}`, color: 'red' });
|
|
682
|
-
segments.push({
|
|
790
|
+
segments.push({
|
|
791
|
+
text: ` ${removedElements === 1 ? 'element' : 'elements'}`,
|
|
792
|
+
colored: false,
|
|
793
|
+
});
|
|
683
794
|
}
|
|
684
795
|
if (removedNodes > 0) {
|
|
685
796
|
if (addedElements > 0 || addedNodes > 0 || removedElements > 0)
|
|
686
797
|
segments.push({ text: ', ', colored: false });
|
|
687
798
|
segments.push({ text: `-${removedNodes}`, color: 'slate' });
|
|
688
|
-
segments.push({ text: ' nodes'
|
|
799
|
+
segments.push({ text: ` ${removedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
|
|
689
800
|
}
|
|
690
801
|
|
|
691
802
|
// Pass element reference if exactly one element was mutated (not counting nodes)
|
|
@@ -716,9 +827,9 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
716
827
|
|
|
717
828
|
const parseSegments = [
|
|
718
829
|
{ text: `${elementsOnly}`, colored: true },
|
|
719
|
-
{ text: ' elements (
|
|
830
|
+
{ text: ` ${elementsOnly === 1 ? 'element' : 'elements'} (`, colored: false },
|
|
720
831
|
{ text: `${totalNodes}`, color: 'slate' },
|
|
721
|
-
{ text:
|
|
832
|
+
{ text: ` total ${totalNodes === 1 ? 'node' : 'nodes'})`, colored: false },
|
|
722
833
|
];
|
|
723
834
|
|
|
724
835
|
if (totalSkipped > 0) {
|
|
@@ -736,7 +847,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
736
847
|
debugLog(
|
|
737
848
|
PHASE_HYDRATE,
|
|
738
849
|
[
|
|
739
|
-
{
|
|
850
|
+
{
|
|
851
|
+
text: `${hydratedCount} ${hydratedCount === 1 ? 'binding' : 'bindings'} (`,
|
|
852
|
+
colored: false,
|
|
853
|
+
},
|
|
740
854
|
{ text: '@[...]', color: 'pink' },
|
|
741
855
|
{ text: ')', colored: false },
|
|
742
856
|
],
|
|
@@ -749,7 +863,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
749
863
|
debugLog(
|
|
750
864
|
PHASE_ITERATE,
|
|
751
865
|
[
|
|
752
|
-
{
|
|
866
|
+
{
|
|
867
|
+
text: `${iteratedCount} ${iteratedCount === 1 ? 'iteration' : 'iterations'} (`,
|
|
868
|
+
colored: false,
|
|
869
|
+
},
|
|
753
870
|
{ text: '<!-- each -->', color: 'commentGreen' },
|
|
754
871
|
{ text: ')', colored: false },
|
|
755
872
|
],
|
|
@@ -762,7 +879,10 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
762
879
|
debugLog(
|
|
763
880
|
PHASE_CONDITION,
|
|
764
881
|
[
|
|
765
|
-
{
|
|
882
|
+
{
|
|
883
|
+
text: `${evaluatedCount} ${evaluatedCount === 1 ? 'conditional' : 'conditionals'} (`,
|
|
884
|
+
colored: false,
|
|
885
|
+
},
|
|
766
886
|
{ text: '<!-- if -->', color: 'commentGreen' },
|
|
767
887
|
{ text: ')', colored: false },
|
|
768
888
|
],
|
|
@@ -856,9 +976,11 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
856
976
|
componentConfig,
|
|
857
977
|
);
|
|
858
978
|
|
|
859
|
-
// Export hyperspeed manifest globally for compiler extraction
|
|
979
|
+
// Export hyperspeed manifest globally for compiler extraction and optimizations
|
|
980
|
+
// Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
|
|
860
981
|
if (typeof window !== 'undefined') {
|
|
861
|
-
|
|
982
|
+
const manifestToExport = hyperspeedTree || hyperspeedManifestData;
|
|
983
|
+
window.__vibeManifest = manifestToExport;
|
|
862
984
|
}
|
|
863
985
|
|
|
864
986
|
return $;
|
package/runtime/iterate.js
CHANGED
|
@@ -9,6 +9,9 @@ import { resolveThisPath } from './utils.js';
|
|
|
9
9
|
// This is a preview of what Vibe Compiled (Phase 2) will do automatically
|
|
10
10
|
import * as fastPath from './_vibe-compiled-iteration-batch.js';
|
|
11
11
|
|
|
12
|
+
// Pre-compiled iteration optimization (production)
|
|
13
|
+
import * as compiled from './pre-compiled-iterations.js';
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Find a comment node with matching text content in the given nodes.
|
|
14
17
|
*/
|
|
@@ -257,7 +260,9 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
257
260
|
iterationNode.meta;
|
|
258
261
|
|
|
259
262
|
// Already rendered - updates go through updateIteration
|
|
260
|
-
if (iterationNode.runtime.instances?.length > 0)
|
|
263
|
+
if (iterationNode.runtime.instances?.length > 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
261
266
|
|
|
262
267
|
// Check if this iteration has already been rendered
|
|
263
268
|
// We use a marker on the startComment node itself (survives re-parsing)
|
|
@@ -288,6 +293,18 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
288
293
|
return;
|
|
289
294
|
}
|
|
290
295
|
|
|
296
|
+
// Compiled path: Check for pre-compiled batch function in manifest
|
|
297
|
+
if (compiled.canUseCompiled(iterationNode)) {
|
|
298
|
+
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
299
|
+
if (compiled.renderCompiled(iterationNode, array, state, compiledMeta, parent, endComment)) {
|
|
300
|
+
// Mark as rendered
|
|
301
|
+
startComment.__vibeRendered = true;
|
|
302
|
+
startComment.__vibeIterationRuntime = iterationNode.runtime;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Fall through to runtime path if compiled failed
|
|
306
|
+
}
|
|
307
|
+
|
|
291
308
|
// Fast path: opt-in via window.__VIBE_FAST_ITERATION__ (preview of Vibe Compiled)
|
|
292
309
|
if (window.__VIBE_FAST_ITERATION__ && fastPath.canUseFastPath(template)) {
|
|
293
310
|
fastPath.renderFast(iterationNode, array, state, parent, endComment);
|
|
@@ -341,12 +358,17 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
341
358
|
const oldArray = resolvePath(oldState, resolvedArrayPath) || [];
|
|
342
359
|
const newArray = resolvePath(newState, resolvedArrayPath) || [];
|
|
343
360
|
|
|
361
|
+
// Compiled path: Use pre-compiled batch function when available
|
|
362
|
+
if (compiled.canUseCompiled(iterationNode)) {
|
|
363
|
+
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
364
|
+
if (compiled.updateCompiled(iterationNode, newArray, newState, compiledMeta, startComment, endComment)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Fall through to runtime path if compiled failed
|
|
368
|
+
}
|
|
369
|
+
|
|
344
370
|
// Fast path: opt-in via window.__VIBE_FAST_ITERATION__ (preview of Vibe Compiled)
|
|
345
371
|
// Use for bulk operations (large arrays or empty→full transitions)
|
|
346
|
-
const isEmptyToFull = oldArray.length === 0 && newArray.length > 0;
|
|
347
|
-
const isFullToEmpty = oldArray.length > 0 && newArray.length === 0;
|
|
348
|
-
const isLargeArray = newArray.length > 100 || oldArray.length > 100;
|
|
349
|
-
|
|
350
372
|
if (
|
|
351
373
|
window.__VIBE_FAST_ITERATION__ &&
|
|
352
374
|
fastPath.canUseFastPath(template) &&
|
package/runtime/parse.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ITERATION_REGEX,
|
|
6
6
|
CONDITIONAL_REGEX,
|
|
7
7
|
DOM_ELEMENT_PROPERTIES,
|
|
8
|
+
DEHYDRATE_CLASS_OR_ATTR,
|
|
8
9
|
} from './constants.js';
|
|
9
10
|
|
|
10
11
|
const parseHTML = (children, rootKey = undefined) =>
|
|
@@ -31,7 +32,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
31
32
|
|
|
32
33
|
// Skip non-reactive elements and dehydrated elements
|
|
33
34
|
if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
|
|
34
|
-
if (element.hasAttribute?.(
|
|
35
|
+
if (element.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) || element.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
35
36
|
stats.skipped++;
|
|
36
37
|
continue;
|
|
37
38
|
}
|