@ape-egg/vibe 1.2.0 → 1.3.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 +62 -0
- package/README.md +1 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/compiler/compile.rs +204 -51
- package/compiler/src/compiler/manifest_builder.rs +432 -0
- package/compiler/src/compiler/mod.rs +3 -0
- package/compiler/src/compiler/state_extractor.rs +148 -0
- package/compiler/src/compiler/value_stamper.rs +222 -0
- package/compiler/src/config.rs +0 -5
- package/compiler/src/main.rs +45 -20
- package/compiler/src/parser/html.rs +22 -8
- package/index.js +30 -86
- package/package.json +1 -1
- package/runtime/affected.js +12 -6
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +1 -1
- package/runtime/component.js +20 -13
- package/runtime/constants.js +41 -10
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +6 -1
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +236 -49
- package/runtime/iterate.js +3 -0
- package/runtime/parse.js +21 -29
- package/runtime/scope.js +5 -25
- package/runtime/utils.js +4 -13
- package/vibe.css +4 -2
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperspeed - Pre-compiled manifest support for Vibe
|
|
3
|
+
*
|
|
4
|
+
* Handles detection and restoration of pre-compiled pages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Build hyperspeed manifest with restoration data (captures markers before hydration)
|
|
8
|
+
export const buildHyperspeedManifest = (parsedTree) => {
|
|
9
|
+
// Helper to split text content by @[...] markers into array
|
|
10
|
+
const splitByMarkers = (text) => {
|
|
11
|
+
const regex = /@\[[^\]]+\]/g;
|
|
12
|
+
const parts = [];
|
|
13
|
+
let lastIndex = 0;
|
|
14
|
+
|
|
15
|
+
text.replace(regex, (match, index) => {
|
|
16
|
+
// Add text before marker
|
|
17
|
+
if (index > lastIndex) {
|
|
18
|
+
parts.push(text.slice(lastIndex, index));
|
|
19
|
+
}
|
|
20
|
+
// Add marker
|
|
21
|
+
parts.push(match);
|
|
22
|
+
lastIndex = index + match.length;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Add remaining text
|
|
26
|
+
if (lastIndex < text.length) {
|
|
27
|
+
parts.push(text.slice(lastIndex));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parts.length > 0 ? parts : null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const walkNode = (node) => {
|
|
34
|
+
const result = {
|
|
35
|
+
element: null,
|
|
36
|
+
parsed: [],
|
|
37
|
+
children: {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Copy type for iterations/conditionals
|
|
41
|
+
if (node.type) {
|
|
42
|
+
result.type = node.type;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if parsed string contains bindings - split into array
|
|
46
|
+
if (typeof node.parsed === 'string' && node.parsed.includes('@[')) {
|
|
47
|
+
const parsedArray = splitByMarkers(node.parsed);
|
|
48
|
+
if (parsedArray) {
|
|
49
|
+
result.hyperspeedRestoration = {
|
|
50
|
+
parsed: parsedArray,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for attribute bindings
|
|
56
|
+
if (node.attributes && typeof node.attributes === 'object') {
|
|
57
|
+
const attrBindings = {};
|
|
58
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
59
|
+
if (typeof value === 'string' && value.includes('@[')) {
|
|
60
|
+
attrBindings[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(attrBindings).length > 0) {
|
|
64
|
+
if (!result.hyperspeedRestoration) {
|
|
65
|
+
result.hyperspeedRestoration = {};
|
|
66
|
+
}
|
|
67
|
+
result.hyperspeedRestoration.attributes = attrBindings;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For iterations, extract template as HTML string
|
|
72
|
+
if (node.type === 'iteration' && node.meta?.template?.element) {
|
|
73
|
+
const templateElement = node.meta.template.element;
|
|
74
|
+
if (templateElement && templateElement.innerHTML) {
|
|
75
|
+
if (!result.hyperspeedRestoration) {
|
|
76
|
+
result.hyperspeedRestoration = {};
|
|
77
|
+
}
|
|
78
|
+
result.hyperspeedRestoration.template = templateElement.innerHTML;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For conditionals, extract branch templates as HTML strings
|
|
83
|
+
if (node.type === 'conditional' && node.meta?.branches) {
|
|
84
|
+
const trueBranch = node.meta.branches.true?.element;
|
|
85
|
+
const falseBranch = node.meta.branches.false?.element;
|
|
86
|
+
|
|
87
|
+
if (trueBranch?.innerHTML || falseBranch?.innerHTML) {
|
|
88
|
+
if (!result.hyperspeedRestoration) {
|
|
89
|
+
result.hyperspeedRestoration = {};
|
|
90
|
+
}
|
|
91
|
+
if (trueBranch?.innerHTML) {
|
|
92
|
+
result.hyperspeedRestoration.trueTemplate = trueBranch.innerHTML;
|
|
93
|
+
}
|
|
94
|
+
if (falseBranch?.innerHTML) {
|
|
95
|
+
result.hyperspeedRestoration.falseTemplate = falseBranch.innerHTML;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Recursively process children (skip runtime-only nodes)
|
|
101
|
+
if (node.children && typeof node.children === 'object') {
|
|
102
|
+
for (const [key, childNode] of Object.entries(node.children)) {
|
|
103
|
+
result.children[key] = walkNode(childNode);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Wrap in body structure (parsedTree is already the body's children)
|
|
111
|
+
return {
|
|
112
|
+
element: null,
|
|
113
|
+
parsed: [],
|
|
114
|
+
children: {
|
|
115
|
+
body: {
|
|
116
|
+
element: null,
|
|
117
|
+
parsed: [],
|
|
118
|
+
children: walkNode(parsedTree).children, // Use children directly
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Hyperspeed detection - per-page manifest
|
|
125
|
+
// Each compiled page has its own manifest: /vibe-hyperspeed/{page-path}.manifest.js
|
|
126
|
+
let hyperspeedManifest = null;
|
|
127
|
+
let hyperspeedDetectionAttempted = false;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Detect page-specific manifest (async, cached after first call)
|
|
131
|
+
*/
|
|
132
|
+
const detectHyperspeed = async () => {
|
|
133
|
+
if (hyperspeedDetectionAttempted) return hyperspeedManifest;
|
|
134
|
+
hyperspeedDetectionAttempted = true;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const pagePath = window.location.pathname;
|
|
138
|
+
const pathSegments = pagePath.split('/').filter(s => s);
|
|
139
|
+
|
|
140
|
+
if (pathSegments.length === 0) return null;
|
|
141
|
+
|
|
142
|
+
// Extract file name and directory parts
|
|
143
|
+
// For /compiled/playground/test.html -> ['compiled', 'playground', 'test.html']
|
|
144
|
+
const fileName = pathSegments[pathSegments.length - 1];
|
|
145
|
+
const dirSegments = pathSegments.slice(0, -1); // All parts except filename
|
|
146
|
+
|
|
147
|
+
// Build possible manifest paths
|
|
148
|
+
const possiblePaths = [];
|
|
149
|
+
|
|
150
|
+
// Strategy 1: vibe-hyperspeed at the same level as parent directory
|
|
151
|
+
// /compiled/playground/test.html -> /compiled/vibe-hyperspeed/playground/test.html.manifest.js
|
|
152
|
+
if (dirSegments.length >= 1) {
|
|
153
|
+
const subPath = dirSegments.slice(1).join('/'); // Everything after first dir
|
|
154
|
+
const baseDir = '/' + dirSegments[0]; // First directory segment
|
|
155
|
+
possiblePaths.push(`${baseDir}/vibe-hyperspeed/${subPath ? subPath + '/' : ''}${fileName}.manifest.js`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Strategy 2: vibe-hyperspeed at web root (original behavior)
|
|
159
|
+
// /compiled/playground/test.html -> /vibe-hyperspeed/compiled/playground/test.html.manifest.js
|
|
160
|
+
possiblePaths.push(`/vibe-hyperspeed${pagePath}.manifest.js`);
|
|
161
|
+
|
|
162
|
+
// Strategy 3: vibe-hyperspeed relative to immediate parent
|
|
163
|
+
// /playground/test.html -> /vibe-hyperspeed/playground/test.html.manifest.js
|
|
164
|
+
if (dirSegments.length > 0) {
|
|
165
|
+
const relativePath = dirSegments.join('/');
|
|
166
|
+
possiblePaths.push(`/vibe-hyperspeed/${relativePath}/${fileName}.manifest.js`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try each possible path
|
|
170
|
+
for (const manifestPath of possiblePaths) {
|
|
171
|
+
try {
|
|
172
|
+
const module = await import(manifestPath);
|
|
173
|
+
hyperspeedManifest = module.default;
|
|
174
|
+
return hyperspeedManifest;
|
|
175
|
+
} catch {
|
|
176
|
+
// Try next path
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// No manifest found
|
|
182
|
+
return null;
|
|
183
|
+
} catch {
|
|
184
|
+
// No manifest for this page - runtime-only mode
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Restore DOM from pre-rendered values to @[...] markers using hyperspeed manifest
|
|
191
|
+
* This enables FOUC-free loading while maintaining runtime reactivity
|
|
192
|
+
*
|
|
193
|
+
* Flow:
|
|
194
|
+
* 1. Page loads with pre-rendered values: <strong>John Doe</strong> (visible, no FOUC)
|
|
195
|
+
* 2. Restoration: Replace with markers: <strong>@[firstName] @[lastName]</strong>
|
|
196
|
+
* 3. Runtime processes normally: Finds markers, makes reactive
|
|
197
|
+
*
|
|
198
|
+
* @param {Element} rootElement - The DOM element to restore (e.g., <body>)
|
|
199
|
+
* @param {Object} subtree - The matching subtree from manifest (e.g., manifest.children.body)
|
|
200
|
+
* @param {Object} fullManifest - The full manifest (unused now, kept for compatibility)
|
|
201
|
+
*/
|
|
202
|
+
export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest = null) => {
|
|
203
|
+
const walkTree = (tree, element) => {
|
|
204
|
+
if (!tree || !element) return;
|
|
205
|
+
|
|
206
|
+
// Check for hyperspeed restoration data
|
|
207
|
+
const restoration = tree.hyperspeedRestoration;
|
|
208
|
+
|
|
209
|
+
if (restoration) {
|
|
210
|
+
// Restore text content with markers if parsed contains bindings
|
|
211
|
+
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
212
|
+
const hasBindings = restoration.parsed.some(item =>
|
|
213
|
+
typeof item === 'string' && item.includes('@[')
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (hasBindings) {
|
|
217
|
+
// Reconstruct original content with markers
|
|
218
|
+
const originalContent = restoration.parsed.join('');
|
|
219
|
+
|
|
220
|
+
// For text nodes, update parent's innerHTML
|
|
221
|
+
// For elements with children, update only text nodes
|
|
222
|
+
if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) {
|
|
223
|
+
// Single text node - replace it
|
|
224
|
+
element.childNodes[0].textContent = originalContent;
|
|
225
|
+
} else if (element.childNodes.length === 0) {
|
|
226
|
+
// No children - set textContent
|
|
227
|
+
element.textContent = originalContent;
|
|
228
|
+
}
|
|
229
|
+
// If element has multiple children, they'll be handled recursively
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Skip attribute restoration - manifest merge provides all binding metadata
|
|
234
|
+
// Attributes are already pre-rendered correctly by compiler
|
|
235
|
+
// restoration.attributes exists only for manifest merge, not for DOM restoration
|
|
236
|
+
|
|
237
|
+
// Restore conditionals
|
|
238
|
+
if ((restoration.trueTemplate || restoration.falseTemplate) && tree.type === 'conditional') {
|
|
239
|
+
// Find comment markers
|
|
240
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
241
|
+
let startComment = null;
|
|
242
|
+
let elseComment = null;
|
|
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
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (startComment && endComment) {
|
|
259
|
+
// Remove pre-rendered content in true branch
|
|
260
|
+
let current = startComment.nextSibling;
|
|
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
|
+
}
|
|
268
|
+
|
|
269
|
+
// Remove pre-rendered content in false branch if exists
|
|
270
|
+
if (elseComment) {
|
|
271
|
+
current = elseComment.nextSibling;
|
|
272
|
+
while (current && current !== endComment) {
|
|
273
|
+
const next = current.nextSibling;
|
|
274
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
275
|
+
current.remove();
|
|
276
|
+
}
|
|
277
|
+
current = next;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Insert templates (runtime will process them)
|
|
282
|
+
if (restoration.trueTemplate) {
|
|
283
|
+
const template = document.createElement('div');
|
|
284
|
+
template.innerHTML = restoration.trueTemplate;
|
|
285
|
+
const templateNode = template.firstElementChild; // Use firstElementChild, not firstChild
|
|
286
|
+
if (templateNode) {
|
|
287
|
+
(elseComment || endComment).parentNode.insertBefore(
|
|
288
|
+
templateNode,
|
|
289
|
+
elseComment || endComment
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (restoration.falseTemplate && elseComment) {
|
|
295
|
+
const template = document.createElement('div');
|
|
296
|
+
template.innerHTML = restoration.falseTemplate;
|
|
297
|
+
const templateNode = template.firstElementChild; // Use firstElementChild, not firstChild
|
|
298
|
+
if (templateNode) {
|
|
299
|
+
endComment.parentNode.insertBefore(templateNode, endComment);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// After restoration, delete hyperspeedRestoration (runtime will parse DOM fresh)
|
|
306
|
+
delete tree.hyperspeedRestoration;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Recursively restore children
|
|
310
|
+
if (tree.children) {
|
|
311
|
+
for (const key in tree.children) {
|
|
312
|
+
const childTree = tree.children[key];
|
|
313
|
+
|
|
314
|
+
// Handle iteration nodes with restoration data
|
|
315
|
+
if (childTree.type === 'iteration' && childTree.hyperspeedRestoration) {
|
|
316
|
+
const restoration = childTree.hyperspeedRestoration;
|
|
317
|
+
|
|
318
|
+
// Find iteration comment by searching (not using index)
|
|
319
|
+
// This avoids index shifting issues after restoration changes DOM
|
|
320
|
+
let startComment = null;
|
|
321
|
+
let endComment = null;
|
|
322
|
+
|
|
323
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
324
|
+
while (walker.nextNode()) {
|
|
325
|
+
const comment = walker.currentNode;
|
|
326
|
+
const trimmed = comment.textContent.trim();
|
|
327
|
+
|
|
328
|
+
// Skip already processed iterations
|
|
329
|
+
if (comment._vibeProcessed) continue;
|
|
330
|
+
|
|
331
|
+
if (trimmed.startsWith('each ') && !startComment) {
|
|
332
|
+
startComment = comment;
|
|
333
|
+
} else if (trimmed === '/each' && startComment && !endComment) {
|
|
334
|
+
endComment = comment;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (startComment && endComment) {
|
|
340
|
+
// Mark as processed
|
|
341
|
+
startComment._vibeProcessed = true;
|
|
342
|
+
endComment._vibeProcessed = true;
|
|
343
|
+
|
|
344
|
+
// Remove all pre-rendered items between comments
|
|
345
|
+
let current = startComment.nextSibling;
|
|
346
|
+
while (current && current !== endComment) {
|
|
347
|
+
const next = current.nextSibling;
|
|
348
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
349
|
+
current.remove();
|
|
350
|
+
}
|
|
351
|
+
current = next;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Insert template from restoration data
|
|
355
|
+
const temp = document.createElement('div');
|
|
356
|
+
temp.innerHTML = restoration.template;
|
|
357
|
+
const templateNode = temp.firstElementChild;
|
|
358
|
+
if (templateNode) {
|
|
359
|
+
endComment.parentNode.insertBefore(templateNode, endComment);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
delete childTree.hyperspeedRestoration;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Handle conditional nodes (similar approach)
|
|
368
|
+
if (childTree.type === 'conditional') {
|
|
369
|
+
// TODO: Implement conditional restoration when needed
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle text nodes with restoration data
|
|
374
|
+
if (key.startsWith('text_') && childTree.hyperspeedRestoration) {
|
|
375
|
+
const restoration = childTree.hyperspeedRestoration;
|
|
376
|
+
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
377
|
+
const hasBindings = restoration.parsed.some(item =>
|
|
378
|
+
typeof item === 'string' && item.includes('@[')
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if (hasBindings) {
|
|
382
|
+
const originalContent = restoration.parsed.join('');
|
|
383
|
+
if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) {
|
|
384
|
+
element.childNodes[0].textContent = originalContent;
|
|
385
|
+
} else if (element.childNodes.length === 0) {
|
|
386
|
+
element.textContent = originalContent;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
delete childTree.hyperspeedRestoration;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Find corresponding child element
|
|
395
|
+
// Keys like "strong_0" use childNodes indices (includes text nodes/comments)
|
|
396
|
+
const match = key.match(/_(\d+)$/);
|
|
397
|
+
if (match) {
|
|
398
|
+
const index = parseInt(match[1], 10);
|
|
399
|
+
const childNode = element.childNodes[index]; // Use childNodes (all nodes)
|
|
400
|
+
|
|
401
|
+
// Only walk if it's an element node
|
|
402
|
+
if (childNode && childNode.nodeType === Node.ELEMENT_NODE) {
|
|
403
|
+
walkTree(childTree, childNode);
|
|
404
|
+
}
|
|
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
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
walkTree(subtree, rootElement);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Top-level await to detect before module exports
|
|
423
|
+
hyperspeedManifest = await detectHyperspeed();
|
|
424
|
+
|
|
425
|
+
export { hyperspeedManifest };
|