@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
|
@@ -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,20 +121,35 @@ 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 {
|
|
137
|
-
|
|
138
|
+
let pagePath = window.location.pathname;
|
|
139
|
+
|
|
140
|
+
// Normalize path: handle directory URLs and missing extensions
|
|
141
|
+
if (pagePath.endsWith('/')) {
|
|
142
|
+
// /compiled/ -> /compiled/index.html
|
|
143
|
+
pagePath = pagePath + 'index.html';
|
|
144
|
+
} else if (!pagePath.includes('.')) {
|
|
145
|
+
// /compiled/mypage -> /compiled/mypage.html
|
|
146
|
+
const lastSlash = pagePath.lastIndexOf('/');
|
|
147
|
+
const lastSegment = pagePath.substring(lastSlash + 1);
|
|
148
|
+
if (lastSegment && !lastSegment.includes('.')) {
|
|
149
|
+
pagePath = pagePath + '.html';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
const pathSegments = pagePath.split('/').filter(s => s);
|
|
139
154
|
|
|
140
155
|
if (pathSegments.length === 0) return null;
|
|
@@ -170,9 +185,12 @@ const detectHyperspeed = async () => {
|
|
|
170
185
|
for (const manifestPath of possiblePaths) {
|
|
171
186
|
try {
|
|
172
187
|
const module = await import(manifestPath);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
hyperspeedData = {
|
|
189
|
+
manifest: module.default,
|
|
190
|
+
path: manifestPath
|
|
191
|
+
};
|
|
192
|
+
return hyperspeedData;
|
|
193
|
+
} catch (e) {
|
|
176
194
|
// Try next path
|
|
177
195
|
continue;
|
|
178
196
|
}
|
|
@@ -187,7 +205,7 @@ const detectHyperspeed = async () => {
|
|
|
187
205
|
};
|
|
188
206
|
|
|
189
207
|
/**
|
|
190
|
-
* Restore DOM from pre-rendered values to @[...] markers using
|
|
208
|
+
* Restore DOM from pre-rendered values to @[...] markers using pre-compiled manifest
|
|
191
209
|
* This enables FOUC-free loading while maintaining runtime reactivity
|
|
192
210
|
*
|
|
193
211
|
* Flow:
|
|
@@ -203,8 +221,8 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
203
221
|
const walkTree = (tree, element) => {
|
|
204
222
|
if (!tree || !element) return;
|
|
205
223
|
|
|
206
|
-
// Check for
|
|
207
|
-
const restoration = tree.
|
|
224
|
+
// Check for compiled restoration data
|
|
225
|
+
const restoration = tree.compiled?.restoration;
|
|
208
226
|
|
|
209
227
|
if (restoration) {
|
|
210
228
|
// Restore text content with markers if parsed contains bindings
|
|
@@ -230,95 +248,94 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
230
248
|
}
|
|
231
249
|
}
|
|
232
250
|
|
|
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
|
-
}
|
|
251
|
+
// Restore attribute bindings for reactivity
|
|
252
|
+
// Two cases:
|
|
253
|
+
// 1. Boolean-like attributes with falsy values were removed - restore them
|
|
254
|
+
// 2. Value attributes were stamped - replace stamped values with markers
|
|
255
|
+
if (restoration.attributes) {
|
|
256
|
+
for (const [attrName, attrValue] of Object.entries(restoration.attributes)) {
|
|
257
|
+
// Always set the attribute to restore the marker
|
|
258
|
+
// - If missing (boolean-like, falsy): adds it back
|
|
259
|
+
// - If present (value attr, stamped): replaces stamped value with marker
|
|
260
|
+
element.setAttribute(attrName, attrValue);
|
|
256
261
|
}
|
|
262
|
+
}
|
|
257
263
|
|
|
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
|
-
}
|
|
264
|
+
// Restore name bindings (sparse array indexed by attribute position)
|
|
265
|
+
if (restoration.nameBindings && Array.isArray(restoration.nameBindings)) {
|
|
266
|
+
const attrs = Array.from(element.attributes);
|
|
268
267
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
268
|
+
for (let i = 0; i < restoration.nameBindings.length; i++) {
|
|
269
|
+
const marker = restoration.nameBindings[i];
|
|
270
|
+
if (marker && attrs[i]) {
|
|
271
|
+
// Replace the stamped attribute name with the marker
|
|
272
|
+
const stampedName = attrs[i].name;
|
|
273
|
+
const attrValue = attrs[i].value;
|
|
274
|
+
|
|
275
|
+
element.removeAttribute(stampedName);
|
|
276
|
+
element.setAttribute(marker, attrValue);
|
|
279
277
|
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// After restoration, delete compiled.restoration (runtime will parse DOM fresh)
|
|
282
|
+
if (tree.compiled) {
|
|
283
|
+
delete tree.compiled.restoration;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// IMPORTANT: Restore text nodes BEFORE processing conditionals/iterations
|
|
288
|
+
// Text nodes use childNodes indices which become invalid after DOM modifications
|
|
289
|
+
if (tree.children) {
|
|
290
|
+
for (const key in tree.children) {
|
|
291
|
+
if (key.startsWith('text_')) {
|
|
292
|
+
const childTree = tree.children[key];
|
|
293
|
+
const restoration = childTree.compiled?.restoration;
|
|
294
|
+
|
|
295
|
+
if (restoration?.parsed && Array.isArray(restoration.parsed)) {
|
|
296
|
+
const hasBindings = restoration.parsed.some(item =>
|
|
297
|
+
typeof item === 'string' && item.includes('@[')
|
|
298
|
+
);
|
|
280
299
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
300
|
+
if (hasBindings) {
|
|
301
|
+
const originalContent = restoration.parsed.join('');
|
|
302
|
+
|
|
303
|
+
// Extract index from key (e.g., text_0 -> 0)
|
|
304
|
+
const match = key.match(/_(\d+)$/);
|
|
305
|
+
if (match) {
|
|
306
|
+
const index = parseInt(match[1], 10);
|
|
307
|
+
|
|
308
|
+
if (index < element.childNodes.length) {
|
|
309
|
+
const textNode = element.childNodes[index];
|
|
310
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
311
|
+
textNode.textContent = originalContent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
291
315
|
}
|
|
292
316
|
}
|
|
293
317
|
|
|
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
|
-
}
|
|
318
|
+
if (childTree.compiled) {
|
|
319
|
+
delete childTree.compiled.restoration;
|
|
301
320
|
}
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
|
-
|
|
305
|
-
// After restoration, delete hyperspeedRestoration (runtime will parse DOM fresh)
|
|
306
|
-
delete tree.hyperspeedRestoration;
|
|
307
323
|
}
|
|
308
324
|
|
|
309
|
-
//
|
|
325
|
+
// Handle conditional and iteration children - process them at THIS level before recursing
|
|
326
|
+
// These are comment nodes in the DOM, not elements, so we need to find them here
|
|
310
327
|
if (tree.children) {
|
|
311
328
|
for (const key in tree.children) {
|
|
312
329
|
const childTree = tree.children[key];
|
|
313
330
|
|
|
314
|
-
// Handle iteration
|
|
315
|
-
if (childTree.type === 'iteration' && childTree.
|
|
316
|
-
const restoration = childTree.
|
|
331
|
+
// Handle iteration restoration
|
|
332
|
+
if (childTree.type === 'iteration' && childTree.compiled?.restoration) {
|
|
333
|
+
const restoration = childTree.compiled.restoration;
|
|
317
334
|
|
|
318
|
-
// Find iteration comment by
|
|
319
|
-
// This avoids index shifting issues after restoration changes DOM
|
|
335
|
+
// Find iteration comment by matching the expression
|
|
320
336
|
let startComment = null;
|
|
321
337
|
let endComment = null;
|
|
338
|
+
let depth = 0;
|
|
322
339
|
|
|
323
340
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
324
341
|
while (walker.nextNode()) {
|
|
@@ -328,11 +345,21 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
328
345
|
// Skip already processed iterations
|
|
329
346
|
if (comment._vibeProcessed) continue;
|
|
330
347
|
|
|
331
|
-
|
|
348
|
+
// Match by expression: "each items as item, i"
|
|
349
|
+
const expectedComment = `each ${restoration.expression}`;
|
|
350
|
+
if (trimmed === expectedComment && !startComment) {
|
|
332
351
|
startComment = comment;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
352
|
+
depth = 1;
|
|
353
|
+
} else if (startComment) {
|
|
354
|
+
if (trimmed.startsWith('each ')) {
|
|
355
|
+
depth++;
|
|
356
|
+
} else if (trimmed === '/each') {
|
|
357
|
+
depth--;
|
|
358
|
+
if (depth === 0) {
|
|
359
|
+
endComment = comment;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
336
363
|
}
|
|
337
364
|
}
|
|
338
365
|
|
|
@@ -341,76 +368,134 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
341
368
|
startComment._vibeProcessed = true;
|
|
342
369
|
endComment._vibeProcessed = true;
|
|
343
370
|
|
|
344
|
-
|
|
371
|
+
const parent = startComment.parentNode;
|
|
372
|
+
const insertionPoint = endComment.nextSibling;
|
|
373
|
+
|
|
374
|
+
// Remove all pre-rendered content between comments
|
|
345
375
|
let current = startComment.nextSibling;
|
|
346
376
|
while (current && current !== endComment) {
|
|
347
377
|
const next = current.nextSibling;
|
|
348
|
-
if (current.nodeType
|
|
378
|
+
if (current.nodeType !== Node.COMMENT_NODE) {
|
|
349
379
|
current.remove();
|
|
350
380
|
}
|
|
351
381
|
current = next;
|
|
352
382
|
}
|
|
353
383
|
|
|
354
|
-
// Insert template
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
384
|
+
// Insert the template (single item)
|
|
385
|
+
const tempContainer = document.createElement('div');
|
|
386
|
+
tempContainer.innerHTML = restoration.template;
|
|
387
|
+
|
|
388
|
+
const fragment = document.createDocumentFragment();
|
|
389
|
+
while (tempContainer.firstChild) {
|
|
390
|
+
fragment.appendChild(tempContainer.firstChild);
|
|
360
391
|
}
|
|
392
|
+
parent.insertBefore(fragment, endComment);
|
|
361
393
|
}
|
|
362
394
|
|
|
363
|
-
|
|
395
|
+
// Delete the iteration node from tree - runtime will re-create it
|
|
396
|
+
delete tree.children[key];
|
|
364
397
|
continue;
|
|
365
398
|
}
|
|
366
399
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// TODO: Implement conditional restoration when needed
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
400
|
+
if (childTree.type === 'conditional' && childTree.compiled?.restoration) {
|
|
401
|
+
const restoration = childTree.compiled.restoration;
|
|
372
402
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
403
|
+
// Find conditional comment markers in the current element
|
|
404
|
+
// But SKIP conditionals that are inside iteration blocks
|
|
405
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
406
|
+
let startComment = null;
|
|
407
|
+
let endComment = null;
|
|
408
|
+
let insideIteration = false;
|
|
380
409
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
410
|
+
while (walker.nextNode()) {
|
|
411
|
+
const comment = walker.currentNode;
|
|
412
|
+
const text = comment.textContent.trim();
|
|
413
|
+
|
|
414
|
+
// Track if we're inside an iteration block
|
|
415
|
+
if (text.startsWith('each ')) {
|
|
416
|
+
insideIteration = true;
|
|
417
|
+
continue;
|
|
418
|
+
} else if (text === '/each') {
|
|
419
|
+
insideIteration = false;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Skip conditionals inside iterations - runtime will handle them
|
|
424
|
+
if (insideIteration) continue;
|
|
425
|
+
|
|
426
|
+
// Skip already processed conditionals
|
|
427
|
+
if (comment._vibeProcessed) continue;
|
|
428
|
+
|
|
429
|
+
if (text.startsWith('if') && !startComment) {
|
|
430
|
+
startComment = comment;
|
|
431
|
+
} else if (text === '/if' && startComment && !endComment) {
|
|
432
|
+
endComment = comment;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (startComment && endComment) {
|
|
438
|
+
// Mark as processed
|
|
439
|
+
startComment._vibeProcessed = true;
|
|
440
|
+
endComment._vibeProcessed = true;
|
|
441
|
+
|
|
442
|
+
// Remove ALL pre-rendered content between start and end comments
|
|
443
|
+
// This includes the <!-- else --> marker from compiled HTML
|
|
444
|
+
let current = startComment.nextSibling;
|
|
445
|
+
while (current && current !== endComment) {
|
|
446
|
+
const next = current.nextSibling;
|
|
447
|
+
current.remove(); // Remove ALL nodes, including comment nodes
|
|
448
|
+
current = next;
|
|
388
449
|
}
|
|
450
|
+
|
|
451
|
+
// Insert the template content BEFORE endComment
|
|
452
|
+
const tempContainer = document.createElement('div');
|
|
453
|
+
tempContainer.innerHTML = restoration.template;
|
|
454
|
+
|
|
455
|
+
const fragment = document.createDocumentFragment();
|
|
456
|
+
while (tempContainer.firstChild) {
|
|
457
|
+
fragment.appendChild(tempContainer.firstChild);
|
|
458
|
+
}
|
|
459
|
+
endComment.parentNode.insertBefore(fragment, endComment);
|
|
389
460
|
}
|
|
390
|
-
|
|
461
|
+
|
|
462
|
+
// Delete restoration data AND remove the conditional node from tree
|
|
463
|
+
// Runtime will re-create it from the restored DOM
|
|
464
|
+
delete tree.children[key];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Recursively restore children (skip conditional/iteration children as they were already handled)
|
|
470
|
+
if (tree.children) {
|
|
471
|
+
for (const key in tree.children) {
|
|
472
|
+
const childTree = tree.children[key];
|
|
473
|
+
|
|
474
|
+
// Skip conditionals and iterations (already processed/deleted above)
|
|
475
|
+
if (childTree.type === 'conditional' || childTree.type === 'iteration') {
|
|
391
476
|
continue;
|
|
392
477
|
}
|
|
393
478
|
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
479
|
+
// Skip text nodes - already handled before conditional/iteration processing
|
|
480
|
+
if (key.startsWith('text_')) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Find corresponding child element by tag name and index
|
|
485
|
+
// Keys like "layout_1" mean the node at childNodes index 1 (includes text nodes)
|
|
486
|
+
// NOT the second layout element
|
|
487
|
+
// Tag names can contain digits (h1, h2, etc.) so use [a-z0-9-]+
|
|
488
|
+
const match = key.match(/^([a-z0-9-]+)_(\d+)$/);
|
|
397
489
|
if (match) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
490
|
+
const tagName = match[1].toUpperCase();
|
|
491
|
+
const nodeIndex = parseInt(match[2], 10);
|
|
400
492
|
|
|
401
|
-
//
|
|
402
|
-
|
|
493
|
+
// Get the node at this childNodes index
|
|
494
|
+
const childNode = element.childNodes[nodeIndex];
|
|
495
|
+
|
|
496
|
+
if (childNode && childNode.nodeType === Node.ELEMENT_NODE && childNode.nodeName === tagName) {
|
|
403
497
|
walkTree(childTree, childNode);
|
|
404
498
|
}
|
|
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
499
|
}
|
|
415
500
|
}
|
|
416
501
|
}
|
|
@@ -420,6 +505,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
420
505
|
};
|
|
421
506
|
|
|
422
507
|
// Top-level await to detect before module exports
|
|
423
|
-
|
|
508
|
+
hyperspeedData = await detectHyperspeed();
|
|
424
509
|
|
|
425
|
-
export
|
|
510
|
+
export const hyperspeedManifest = hyperspeedData?.manifest || null;
|
|
511
|
+
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
|
+
}
|