@ape-egg/vibe 1.3.2 → 1.6.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 +177 -0
- package/README.md +97 -0
- package/boot.js +0 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +719 -40
- package/compiler/src/Cargo.toml +11 -2
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +552 -175
- 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 +41 -28
- 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} +204 -132
- 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pre-compiled-iterations.js
|
|
3
|
+
*
|
|
4
|
+
* Pre-compiled iteration rendering - production version of iteration optimization.
|
|
5
|
+
* Uses pre-compiled batch functions from manifest generated by the compiler.
|
|
6
|
+
*
|
|
7
|
+
* This is the production implementation that runs compiled code generated at build time.
|
|
8
|
+
* Based on the prototype in _vibe-compiled-iteration-batch.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Reusable template element for parsing compiled HTML
|
|
12
|
+
const parseTemplate = typeof document !== 'undefined' ? document.createElement('template') : null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if template has nested iterations or conditionals
|
|
16
|
+
*/
|
|
17
|
+
const hasNestedStructures = (template) => {
|
|
18
|
+
if (!template || !template.children) return false;
|
|
19
|
+
for (const key in template.children) {
|
|
20
|
+
const child = template.children[key];
|
|
21
|
+
if (!child) continue;
|
|
22
|
+
if (child.type === 'iteration' || child.type === 'conditional') return true;
|
|
23
|
+
if (hasNestedStructures(child)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if iteration node has compiled batch function
|
|
30
|
+
*/
|
|
31
|
+
export const canUseCompiled = (iterationNode) => {
|
|
32
|
+
if (!iterationNode || !iterationNode.meta) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check compiled data from manifest merge
|
|
37
|
+
const compiled = iterationNode.compiled;
|
|
38
|
+
if (!compiled || !compiled.iterations || !compiled.iterations.batchFn) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If we have a compiled batch function, we can use it even if the template
|
|
43
|
+
// has nested structures, because the compiler has already inlined them
|
|
44
|
+
// into the batch function
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get compiled function metadata from iteration node
|
|
50
|
+
*/
|
|
51
|
+
export const getCompiledMeta = (iterationNode) => {
|
|
52
|
+
const compiled = iterationNode.compiled;
|
|
53
|
+
if (!compiled || !compiled.iterations) return null;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
batchFn: compiled.iterations.batchFn,
|
|
57
|
+
itemAlias: compiled.iterations.itemAlias,
|
|
58
|
+
indexAlias: compiled.iterations.indexAlias,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render iteration using pre-compiled batch function from manifest
|
|
64
|
+
*/
|
|
65
|
+
export const renderCompiled = (iterationNode, array, state, compiledMeta, parent, endComment) => {
|
|
66
|
+
// Create function from string if not cached
|
|
67
|
+
if (!iterationNode.runtime.compiledFn) {
|
|
68
|
+
try {
|
|
69
|
+
// compiledMeta.batchFn is a complete arrow function: (arr, $) => { ... }
|
|
70
|
+
// Wrap in a function that returns it, then call to get the actual function
|
|
71
|
+
iterationNode.runtime.compiledFn = new Function('return ' + compiledMeta.batchFn)();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('[compiled-iteration] Failed to create compiled function:', e);
|
|
74
|
+
return false; // Signal failure
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build HTML using compiled function
|
|
79
|
+
const html = iterationNode.runtime.compiledFn(array, state);
|
|
80
|
+
|
|
81
|
+
// Parse and insert
|
|
82
|
+
if (parseTemplate) {
|
|
83
|
+
parseTemplate.innerHTML = html;
|
|
84
|
+
const frag = parseTemplate.content;
|
|
85
|
+
const kids = frag.children;
|
|
86
|
+
|
|
87
|
+
// Track instances - pre-allocate array for performance
|
|
88
|
+
const arrayLen = array.length;
|
|
89
|
+
const instances = new Array(arrayLen);
|
|
90
|
+
for (let i = 0; i < arrayLen; i++) {
|
|
91
|
+
instances[i] = { element: kids[i], item: array[i], index: i };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
parent.insertBefore(frag, endComment);
|
|
95
|
+
iterationNode.runtime.instances = instances;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update iteration using pre-compiled batch function (bulk rebuild)
|
|
104
|
+
*/
|
|
105
|
+
export const updateCompiled = (iterationNode, newArray, state, compiledMeta, startComment, endComment) => {
|
|
106
|
+
const parent = startComment.parentNode;
|
|
107
|
+
|
|
108
|
+
// Clear existing instances
|
|
109
|
+
if (iterationNode.runtime.instances.length > 0) {
|
|
110
|
+
const range = document.createRange();
|
|
111
|
+
range.setStartAfter(startComment);
|
|
112
|
+
range.setEndBefore(endComment);
|
|
113
|
+
range.deleteContents();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (newArray.length === 0) {
|
|
117
|
+
iterationNode.runtime.instances = [];
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create function if not cached
|
|
122
|
+
if (!iterationNode.runtime.compiledFn) {
|
|
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);
|
|
133
|
+
|
|
134
|
+
// Parse and insert
|
|
135
|
+
if (parseTemplate) {
|
|
136
|
+
parseTemplate.innerHTML = html;
|
|
137
|
+
const frag = parseTemplate.content;
|
|
138
|
+
const kids = frag.children;
|
|
139
|
+
|
|
140
|
+
// Track instances - pre-allocate array for performance
|
|
141
|
+
const arrayLen = newArray.length;
|
|
142
|
+
const instances = new Array(arrayLen);
|
|
143
|
+
for (let i = 0; i < arrayLen; i++) {
|
|
144
|
+
instances[i] = { element: kids[i], item: newArray[i], index: i };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
parent.insertBefore(frag, endComment);
|
|
148
|
+
iterationNode.runtime.instances = instances;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return false;
|
|
153
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pre-compiled Manifest - Manifest support for pre-compiled Vibe pages
|
|
3
3
|
*
|
|
4
|
-
* Handles detection and restoration of pre-compiled
|
|
4
|
+
* Handles detection, loading, and restoration of pre-compiled page manifests.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Build
|
|
7
|
+
// Build manifest with restoration data (captures markers before hydration)
|
|
8
8
|
export const buildHyperspeedManifest = (parsedTree) => {
|
|
9
9
|
// Helper to split text content by @[...] markers into array
|
|
10
10
|
const splitByMarkers = (text) => {
|
|
@@ -121,16 +121,17 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
//
|
|
124
|
+
// Manifest detection - per-page manifest
|
|
125
125
|
// Each compiled page has its own manifest: /vibe-hyperspeed/{page-path}.manifest.js
|
|
126
|
-
let
|
|
126
|
+
let hyperspeedData = null;
|
|
127
127
|
let hyperspeedDetectionAttempted = false;
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Detect page-specific manifest (async, cached after first call)
|
|
131
|
+
* Returns { manifest, path } or null
|
|
131
132
|
*/
|
|
132
133
|
const detectHyperspeed = async () => {
|
|
133
|
-
if (hyperspeedDetectionAttempted) return
|
|
134
|
+
if (hyperspeedDetectionAttempted) return hyperspeedData;
|
|
134
135
|
hyperspeedDetectionAttempted = true;
|
|
135
136
|
|
|
136
137
|
try {
|
|
@@ -170,9 +171,12 @@ const detectHyperspeed = async () => {
|
|
|
170
171
|
for (const manifestPath of possiblePaths) {
|
|
171
172
|
try {
|
|
172
173
|
const module = await import(manifestPath);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
hyperspeedData = {
|
|
175
|
+
manifest: module.default,
|
|
176
|
+
path: manifestPath
|
|
177
|
+
};
|
|
178
|
+
return hyperspeedData;
|
|
179
|
+
} catch (e) {
|
|
176
180
|
// Try next path
|
|
177
181
|
continue;
|
|
178
182
|
}
|
|
@@ -187,7 +191,7 @@ const detectHyperspeed = async () => {
|
|
|
187
191
|
};
|
|
188
192
|
|
|
189
193
|
/**
|
|
190
|
-
* Restore DOM from pre-rendered values to @[...] markers using
|
|
194
|
+
* Restore DOM from pre-rendered values to @[...] markers using pre-compiled manifest
|
|
191
195
|
* This enables FOUC-free loading while maintaining runtime reactivity
|
|
192
196
|
*
|
|
193
197
|
* Flow:
|
|
@@ -203,8 +207,8 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
203
207
|
const walkTree = (tree, element) => {
|
|
204
208
|
if (!tree || !element) return;
|
|
205
209
|
|
|
206
|
-
// Check for
|
|
207
|
-
const restoration = tree.
|
|
210
|
+
// Check for compiled restoration data
|
|
211
|
+
const restoration = tree.compiled?.restoration;
|
|
208
212
|
|
|
209
213
|
if (restoration) {
|
|
210
214
|
// Restore text content with markers if parsed contains bindings
|
|
@@ -230,95 +234,94 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
let endComment = null;
|
|
244
|
-
|
|
245
|
-
while (walker.nextNode()) {
|
|
246
|
-
const comment = walker.currentNode;
|
|
247
|
-
const text = comment.textContent.trim();
|
|
248
|
-
if (text.startsWith('if')) {
|
|
249
|
-
startComment = comment;
|
|
250
|
-
} else if (text === 'else') {
|
|
251
|
-
elseComment = comment;
|
|
252
|
-
} else if (text === '/if') {
|
|
253
|
-
endComment = comment;
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
237
|
+
// Restore attribute bindings for reactivity
|
|
238
|
+
// Two cases:
|
|
239
|
+
// 1. Boolean-like attributes with falsy values were removed - restore them
|
|
240
|
+
// 2. Value attributes were stamped - replace stamped values with markers
|
|
241
|
+
if (restoration.attributes) {
|
|
242
|
+
for (const [attrName, attrValue] of Object.entries(restoration.attributes)) {
|
|
243
|
+
// Always set the attribute to restore the marker
|
|
244
|
+
// - If missing (boolean-like, falsy): adds it back
|
|
245
|
+
// - If present (value attr, stamped): replaces stamped value with marker
|
|
246
|
+
element.setAttribute(attrName, attrValue);
|
|
256
247
|
}
|
|
248
|
+
}
|
|
257
249
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
while (current && current !== (elseComment || endComment)) {
|
|
262
|
-
const next = current.nextSibling;
|
|
263
|
-
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
264
|
-
current.remove();
|
|
265
|
-
}
|
|
266
|
-
current = next;
|
|
267
|
-
}
|
|
250
|
+
// Restore name bindings (sparse array indexed by attribute position)
|
|
251
|
+
if (restoration.nameBindings && Array.isArray(restoration.nameBindings)) {
|
|
252
|
+
const attrs = Array.from(element.attributes);
|
|
268
253
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
254
|
+
for (let i = 0; i < restoration.nameBindings.length; i++) {
|
|
255
|
+
const marker = restoration.nameBindings[i];
|
|
256
|
+
if (marker && attrs[i]) {
|
|
257
|
+
// Replace the stamped attribute name with the marker
|
|
258
|
+
const stampedName = attrs[i].name;
|
|
259
|
+
const attrValue = attrs[i].value;
|
|
260
|
+
|
|
261
|
+
element.removeAttribute(stampedName);
|
|
262
|
+
element.setAttribute(marker, attrValue);
|
|
279
263
|
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// After restoration, delete compiled.restoration (runtime will parse DOM fresh)
|
|
268
|
+
if (tree.compiled) {
|
|
269
|
+
delete tree.compiled.restoration;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
280
272
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
273
|
+
// IMPORTANT: Restore text nodes BEFORE processing conditionals/iterations
|
|
274
|
+
// Text nodes use childNodes indices which become invalid after DOM modifications
|
|
275
|
+
if (tree.children) {
|
|
276
|
+
for (const key in tree.children) {
|
|
277
|
+
if (key.startsWith('text_')) {
|
|
278
|
+
const childTree = tree.children[key];
|
|
279
|
+
const restoration = childTree.compiled?.restoration;
|
|
280
|
+
|
|
281
|
+
if (restoration?.parsed && Array.isArray(restoration.parsed)) {
|
|
282
|
+
const hasBindings = restoration.parsed.some(item =>
|
|
283
|
+
typeof item === 'string' && item.includes('@[')
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (hasBindings) {
|
|
287
|
+
const originalContent = restoration.parsed.join('');
|
|
288
|
+
|
|
289
|
+
// Extract index from key (e.g., text_0 -> 0)
|
|
290
|
+
const match = key.match(/_(\d+)$/);
|
|
291
|
+
if (match) {
|
|
292
|
+
const index = parseInt(match[1], 10);
|
|
293
|
+
|
|
294
|
+
if (index < element.childNodes.length) {
|
|
295
|
+
const textNode = element.childNodes[index];
|
|
296
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
297
|
+
textNode.textContent = originalContent;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
291
301
|
}
|
|
292
302
|
}
|
|
293
303
|
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
template.innerHTML = restoration.falseTemplate;
|
|
297
|
-
const templateNode = template.firstElementChild; // Use firstElementChild, not firstChild
|
|
298
|
-
if (templateNode) {
|
|
299
|
-
endComment.parentNode.insertBefore(templateNode, endComment);
|
|
300
|
-
}
|
|
304
|
+
if (childTree.compiled) {
|
|
305
|
+
delete childTree.compiled.restoration;
|
|
301
306
|
}
|
|
302
307
|
}
|
|
303
308
|
}
|
|
304
|
-
|
|
305
|
-
// After restoration, delete hyperspeedRestoration (runtime will parse DOM fresh)
|
|
306
|
-
delete tree.hyperspeedRestoration;
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
//
|
|
311
|
+
// Handle conditional and iteration children - process them at THIS level before recursing
|
|
312
|
+
// These are comment nodes in the DOM, not elements, so we need to find them here
|
|
310
313
|
if (tree.children) {
|
|
311
314
|
for (const key in tree.children) {
|
|
312
315
|
const childTree = tree.children[key];
|
|
313
316
|
|
|
314
|
-
// Handle iteration
|
|
315
|
-
if (childTree.type === 'iteration' && childTree.
|
|
316
|
-
const restoration = childTree.
|
|
317
|
+
// Handle iteration restoration
|
|
318
|
+
if (childTree.type === 'iteration' && childTree.compiled?.restoration) {
|
|
319
|
+
const restoration = childTree.compiled.restoration;
|
|
317
320
|
|
|
318
|
-
// Find iteration comment by
|
|
319
|
-
// This avoids index shifting issues after restoration changes DOM
|
|
321
|
+
// Find iteration comment by matching the expression
|
|
320
322
|
let startComment = null;
|
|
321
323
|
let endComment = null;
|
|
324
|
+
let depth = 0;
|
|
322
325
|
|
|
323
326
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
324
327
|
while (walker.nextNode()) {
|
|
@@ -328,11 +331,21 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
328
331
|
// Skip already processed iterations
|
|
329
332
|
if (comment._vibeProcessed) continue;
|
|
330
333
|
|
|
331
|
-
|
|
334
|
+
// Match by expression: "each items as item, i"
|
|
335
|
+
const expectedComment = `each ${restoration.expression}`;
|
|
336
|
+
if (trimmed === expectedComment && !startComment) {
|
|
332
337
|
startComment = comment;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
depth = 1;
|
|
339
|
+
} else if (startComment) {
|
|
340
|
+
if (trimmed.startsWith('each ')) {
|
|
341
|
+
depth++;
|
|
342
|
+
} else if (trimmed === '/each') {
|
|
343
|
+
depth--;
|
|
344
|
+
if (depth === 0) {
|
|
345
|
+
endComment = comment;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
336
349
|
}
|
|
337
350
|
}
|
|
338
351
|
|
|
@@ -341,76 +354,134 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
341
354
|
startComment._vibeProcessed = true;
|
|
342
355
|
endComment._vibeProcessed = true;
|
|
343
356
|
|
|
344
|
-
|
|
357
|
+
const parent = startComment.parentNode;
|
|
358
|
+
const insertionPoint = endComment.nextSibling;
|
|
359
|
+
|
|
360
|
+
// Remove all pre-rendered content between comments
|
|
345
361
|
let current = startComment.nextSibling;
|
|
346
362
|
while (current && current !== endComment) {
|
|
347
363
|
const next = current.nextSibling;
|
|
348
|
-
if (current.nodeType
|
|
364
|
+
if (current.nodeType !== Node.COMMENT_NODE) {
|
|
349
365
|
current.remove();
|
|
350
366
|
}
|
|
351
367
|
current = next;
|
|
352
368
|
}
|
|
353
369
|
|
|
354
|
-
// Insert template
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
370
|
+
// Insert the template (single item)
|
|
371
|
+
const tempContainer = document.createElement('div');
|
|
372
|
+
tempContainer.innerHTML = restoration.template;
|
|
373
|
+
|
|
374
|
+
const fragment = document.createDocumentFragment();
|
|
375
|
+
while (tempContainer.firstChild) {
|
|
376
|
+
fragment.appendChild(tempContainer.firstChild);
|
|
360
377
|
}
|
|
378
|
+
parent.insertBefore(fragment, endComment);
|
|
361
379
|
}
|
|
362
380
|
|
|
363
|
-
|
|
381
|
+
// Delete the iteration node from tree - runtime will re-create it
|
|
382
|
+
delete tree.children[key];
|
|
364
383
|
continue;
|
|
365
384
|
}
|
|
366
385
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// TODO: Implement conditional restoration when needed
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
386
|
+
if (childTree.type === 'conditional' && childTree.compiled?.restoration) {
|
|
387
|
+
const restoration = childTree.compiled.restoration;
|
|
372
388
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
389
|
+
// Find conditional comment markers in the current element
|
|
390
|
+
// But SKIP conditionals that are inside iteration blocks
|
|
391
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
392
|
+
let startComment = null;
|
|
393
|
+
let endComment = null;
|
|
394
|
+
let insideIteration = false;
|
|
380
395
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
396
|
+
while (walker.nextNode()) {
|
|
397
|
+
const comment = walker.currentNode;
|
|
398
|
+
const text = comment.textContent.trim();
|
|
399
|
+
|
|
400
|
+
// Track if we're inside an iteration block
|
|
401
|
+
if (text.startsWith('each ')) {
|
|
402
|
+
insideIteration = true;
|
|
403
|
+
continue;
|
|
404
|
+
} else if (text === '/each') {
|
|
405
|
+
insideIteration = false;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Skip conditionals inside iterations - runtime will handle them
|
|
410
|
+
if (insideIteration) continue;
|
|
411
|
+
|
|
412
|
+
// Skip already processed conditionals
|
|
413
|
+
if (comment._vibeProcessed) continue;
|
|
414
|
+
|
|
415
|
+
if (text.startsWith('if') && !startComment) {
|
|
416
|
+
startComment = comment;
|
|
417
|
+
} else if (text === '/if' && startComment && !endComment) {
|
|
418
|
+
endComment = comment;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (startComment && endComment) {
|
|
424
|
+
// Mark as processed
|
|
425
|
+
startComment._vibeProcessed = true;
|
|
426
|
+
endComment._vibeProcessed = true;
|
|
427
|
+
|
|
428
|
+
// Remove ALL pre-rendered content between start and end comments
|
|
429
|
+
// This includes the <!-- else --> marker from compiled HTML
|
|
430
|
+
let current = startComment.nextSibling;
|
|
431
|
+
while (current && current !== endComment) {
|
|
432
|
+
const next = current.nextSibling;
|
|
433
|
+
current.remove(); // Remove ALL nodes, including comment nodes
|
|
434
|
+
current = next;
|
|
388
435
|
}
|
|
436
|
+
|
|
437
|
+
// Insert the template content BEFORE endComment
|
|
438
|
+
const tempContainer = document.createElement('div');
|
|
439
|
+
tempContainer.innerHTML = restoration.template;
|
|
440
|
+
|
|
441
|
+
const fragment = document.createDocumentFragment();
|
|
442
|
+
while (tempContainer.firstChild) {
|
|
443
|
+
fragment.appendChild(tempContainer.firstChild);
|
|
444
|
+
}
|
|
445
|
+
endComment.parentNode.insertBefore(fragment, endComment);
|
|
389
446
|
}
|
|
390
|
-
|
|
447
|
+
|
|
448
|
+
// Delete restoration data AND remove the conditional node from tree
|
|
449
|
+
// Runtime will re-create it from the restored DOM
|
|
450
|
+
delete tree.children[key];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Recursively restore children (skip conditional/iteration children as they were already handled)
|
|
456
|
+
if (tree.children) {
|
|
457
|
+
for (const key in tree.children) {
|
|
458
|
+
const childTree = tree.children[key];
|
|
459
|
+
|
|
460
|
+
// Skip conditionals and iterations (already processed/deleted above)
|
|
461
|
+
if (childTree.type === 'conditional' || childTree.type === 'iteration') {
|
|
391
462
|
continue;
|
|
392
463
|
}
|
|
393
464
|
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
465
|
+
// Skip text nodes - already handled before conditional/iteration processing
|
|
466
|
+
if (key.startsWith('text_')) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Find corresponding child element by tag name and index
|
|
471
|
+
// Keys like "layout_1" mean the node at childNodes index 1 (includes text nodes)
|
|
472
|
+
// NOT the second layout element
|
|
473
|
+
// Tag names can contain digits (h1, h2, etc.) so use [a-z0-9-]+
|
|
474
|
+
const match = key.match(/^([a-z0-9-]+)_(\d+)$/);
|
|
397
475
|
if (match) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
476
|
+
const tagName = match[1].toUpperCase();
|
|
477
|
+
const nodeIndex = parseInt(match[2], 10);
|
|
400
478
|
|
|
401
|
-
//
|
|
402
|
-
|
|
479
|
+
// Get the node at this childNodes index
|
|
480
|
+
const childNode = element.childNodes[nodeIndex];
|
|
481
|
+
|
|
482
|
+
if (childNode && childNode.nodeType === Node.ELEMENT_NODE && childNode.nodeName === tagName) {
|
|
403
483
|
walkTree(childTree, childNode);
|
|
404
484
|
}
|
|
405
|
-
} else {
|
|
406
|
-
// Try to find by tag name for non-indexed keys
|
|
407
|
-
const tagName = key.split('_')[0].toUpperCase();
|
|
408
|
-
const childElement = Array.from(element.children).find(
|
|
409
|
-
child => child.nodeName === tagName
|
|
410
|
-
);
|
|
411
|
-
if (childElement) {
|
|
412
|
-
walkTree(childTree, childElement);
|
|
413
|
-
}
|
|
414
485
|
}
|
|
415
486
|
}
|
|
416
487
|
}
|
|
@@ -420,6 +491,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
420
491
|
};
|
|
421
492
|
|
|
422
493
|
// Top-level await to detect before module exports
|
|
423
|
-
|
|
494
|
+
hyperspeedData = await detectHyperspeed();
|
|
424
495
|
|
|
425
|
-
export
|
|
496
|
+
export const hyperspeedManifest = hyperspeedData?.manifest || null;
|
|
497
|
+
export const hyperspeedPath = hyperspeedData?.path || null;
|
package/runtime/utils.js
CHANGED
|
@@ -67,7 +67,8 @@ export const resolveThisPath = (path, element) => {
|
|
|
67
67
|
|
|
68
68
|
const componentId = findComponentIdForElement(element);
|
|
69
69
|
if (componentId) {
|
|
70
|
-
|
|
70
|
+
const resolved = path.replace(/^this\./, `${componentId}.`);
|
|
71
|
+
return resolved;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
return path;
|
package/vibe.css
CHANGED
|
@@ -11,3 +11,22 @@
|
|
|
11
11
|
.vibe-fouc * {
|
|
12
12
|
transition: none !important;
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
/* Vibe Dehydrate - Skip Reactive Processing
|
|
16
|
+
* Elements with [vibe-dehydrate] attribute or .vibe-dehydrate class are skipped during
|
|
17
|
+
* Vibe's parsing and hydration. Useful for displaying literal @[variable] syntax in
|
|
18
|
+
* documentation, examples, or code snippets without triggering reactivity.
|
|
19
|
+
*/
|
|
20
|
+
[vibe-dehydrate],
|
|
21
|
+
.vibe-dehydrate {
|
|
22
|
+
/* No styles applied - marker only */
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Component Wrappers - Layout Transparent
|
|
26
|
+
* Component wrappers (<component> or <div class="component">) use display: contents
|
|
27
|
+
* to make the wrapper invisible in the layout. Children render as if the wrapper doesn't exist.
|
|
28
|
+
*/
|
|
29
|
+
component,
|
|
30
|
+
div.component {
|
|
31
|
+
display: contents;
|
|
32
|
+
}
|