@ape-egg/vibe 1.6.0 → 1.7.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 -1
- package/README.md +136 -24
- package/boot.js +11 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +1 -1
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +53 -22
- package/compiler/src/compiler/component_tagger.rs +22 -3
- package/compiler/src/compiler/value_stamper.rs +75 -5
- package/compiler/src/compiler/watcher.rs +69 -2
- package/compiler/src/config.rs +10 -0
- package/compiler/src/main.rs +45 -18
- package/compiler/src/parser/html.rs +61 -9
- package/index.js +34 -5
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/runtime/cleanup.js +0 -9
- package/runtime/component.js +14 -18
- package/runtime/index.js +13 -2
- package/runtime/iterate.js +63 -15
- package/runtime/parse.js +3 -2
- package/runtime/pre-compiled-manifest.js +120 -53
package/runtime/component.js
CHANGED
|
@@ -36,7 +36,9 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
36
36
|
const componentElements = rootElement.querySelectorAll('component[src], div.component[src]');
|
|
37
37
|
|
|
38
38
|
if (componentElements.length === 0) {
|
|
39
|
-
|
|
39
|
+
// Defer onComplete to give user code a chance to register listeners
|
|
40
|
+
// This is important when all components are pre-compiled (no src attributes)
|
|
41
|
+
if (onComplete) queueMicrotask(() => onComplete());
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -50,7 +52,8 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
50
52
|
// Process next component
|
|
51
53
|
processComponent(rootElement, onComplete, config);
|
|
52
54
|
} else {
|
|
53
|
-
|
|
55
|
+
// Defer onComplete to give user code a chance to register listeners
|
|
56
|
+
if (onComplete) queueMicrotask(() => onComplete());
|
|
54
57
|
}
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
@@ -131,21 +134,21 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
131
134
|
const thisRegex = /@\[this\.(\w+)\]/g;
|
|
132
135
|
|
|
133
136
|
// Rewrite in text nodes
|
|
134
|
-
Array.from(element.childNodes).forEach(node => {
|
|
137
|
+
Array.from(element.childNodes).forEach((node) => {
|
|
135
138
|
if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('@[this.')) {
|
|
136
139
|
node.textContent = node.textContent.replace(thisRegex, `@[${componentId}.$1]`);
|
|
137
140
|
}
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
// Rewrite in attributes
|
|
141
|
-
Array.from(element.attributes || []).forEach(attr => {
|
|
144
|
+
Array.from(element.attributes || []).forEach((attr) => {
|
|
142
145
|
if (attr.value.includes('@[this.')) {
|
|
143
146
|
attr.value = attr.value.replace(thisRegex, `@[${componentId}.$1]`);
|
|
144
147
|
}
|
|
145
148
|
});
|
|
146
149
|
|
|
147
150
|
// Recurse into children
|
|
148
|
-
Array.from(element.children).forEach(child => {
|
|
151
|
+
Array.from(element.children).forEach((child) => {
|
|
149
152
|
rewriteThisBindings(child);
|
|
150
153
|
});
|
|
151
154
|
};
|
|
@@ -195,9 +198,10 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
195
198
|
// Check if element still has a parent (might have been removed during fetch)
|
|
196
199
|
if (el.parentNode) {
|
|
197
200
|
// Create clean wrapper element (preserve tag type: component or div.component)
|
|
198
|
-
const newWrapper =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
const newWrapper =
|
|
202
|
+
el.tagName === 'DIV'
|
|
203
|
+
? document.createElement('div')
|
|
204
|
+
: document.createElement('component');
|
|
201
205
|
|
|
202
206
|
if (el.tagName === 'DIV') {
|
|
203
207
|
newWrapper.className = 'component';
|
|
@@ -207,16 +211,8 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
|
|
|
207
211
|
el.replaceWith(newWrapper);
|
|
208
212
|
debugLog(PHASE_FETCH, src, debug);
|
|
209
213
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
if (config._forceSync && config._processMutations && config._observer) {
|
|
213
|
-
Promise.resolve().then(() => {
|
|
214
|
-
const pending = config._observer.takeRecords();
|
|
215
|
-
if (pending.length > 0) {
|
|
216
|
-
config._processMutations(pending);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
214
|
+
// Let MutationObserver handle the mutation naturally
|
|
215
|
+
// It will call processMutations, which will call processComponent for the next component
|
|
220
216
|
}
|
|
221
217
|
})
|
|
222
218
|
.catch((error) => {
|
package/runtime/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
PHASE_UPDATE,
|
|
19
19
|
PHASE_MUTATE,
|
|
20
20
|
PHASE_HYPERSPEED,
|
|
21
|
+
PHASE_READY,
|
|
21
22
|
DEHYDRATE_CLASS_OR_ATTR,
|
|
22
23
|
} from './constants.js';
|
|
23
24
|
import { processComponent, abortComponentFetch } from './component.js';
|
|
@@ -70,12 +71,12 @@ const shouldProcessNode = (node) => {
|
|
|
70
71
|
// Navigate tree using dot notation (handles .children at each level)
|
|
71
72
|
const navigateTree = (tree, path) => {
|
|
72
73
|
if (!path) return tree;
|
|
73
|
-
return path.split('.').reduce((node, key) => node?.children?.[key], tree);
|
|
74
|
+
return path.split('.').filter(k => k).reduce((node, key) => node?.children?.[key], tree);
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
// Get or create a node in the tree at the given path
|
|
77
78
|
const ensureNode = (tree, path) => {
|
|
78
|
-
const keys = path.split('.');
|
|
79
|
+
const keys = path.split('.').filter(k => k);
|
|
79
80
|
return keys.reduce((node, key) => {
|
|
80
81
|
if (!node.children[key]) {
|
|
81
82
|
node.children[key] = { children: {} };
|
|
@@ -497,6 +498,7 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
497
498
|
const hooks = {
|
|
498
499
|
afterUpdate: [],
|
|
499
500
|
afterDomMutation: [],
|
|
501
|
+
ready: [],
|
|
500
502
|
};
|
|
501
503
|
|
|
502
504
|
// Extract plain values from proxy (removes proxy wrappers)
|
|
@@ -950,6 +952,15 @@ const main = (s, config = {}, stringSelector = '') => {
|
|
|
950
952
|
if (shouldCleanup(rootElement)) {
|
|
951
953
|
cleanup(rootElement, debug);
|
|
952
954
|
cleanupExecuted = true;
|
|
955
|
+
|
|
956
|
+
// Fire ready hook after cleanup completes
|
|
957
|
+
hooks.ready.forEach((callback) => {
|
|
958
|
+
try {
|
|
959
|
+
callback();
|
|
960
|
+
} catch (error) {
|
|
961
|
+
console.error('[vibe] Error in ready hook:', error);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
953
964
|
}
|
|
954
965
|
};
|
|
955
966
|
|
package/runtime/iterate.js
CHANGED
|
@@ -16,9 +16,10 @@ import * as compiled from './pre-compiled-iterations.js';
|
|
|
16
16
|
* Find a comment node with matching text content in the given nodes.
|
|
17
17
|
*/
|
|
18
18
|
const findComment = (nodes, text) => {
|
|
19
|
+
const trimmedText = text.trim();
|
|
19
20
|
for (let i = 0; i < nodes.length; i++) {
|
|
20
21
|
const node = nodes[i];
|
|
21
|
-
if (node.nodeType === 8 && node.textContent.trim() ===
|
|
22
|
+
if (node.nodeType === 8 && node.textContent.trim() === trimmedText) {
|
|
22
23
|
return node;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -36,9 +37,16 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
36
37
|
parsed: originalTree.parsed,
|
|
37
38
|
element: clonedRoot,
|
|
38
39
|
children: {},
|
|
39
|
-
...(originalTree.attributes && { attributes: originalTree.attributes }),
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
// Avoid spread operator for performance
|
|
43
|
+
if (originalTree.attributes) {
|
|
44
|
+
cloned.attributes = originalTree.attributes;
|
|
45
|
+
}
|
|
46
|
+
if (originalTree.nameBindings) {
|
|
47
|
+
cloned.nameBindings = originalTree.nameBindings;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
if (!originalTree.children) return cloned;
|
|
43
51
|
|
|
44
52
|
const clonedChildNodes = clonedRoot?.childNodes;
|
|
@@ -130,26 +138,42 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
130
138
|
let clonedNodes = [];
|
|
131
139
|
let firstElement = null;
|
|
132
140
|
|
|
133
|
-
//
|
|
134
|
-
|
|
141
|
+
// TEMPORARY: Disable fast path to test if it's causing duplication
|
|
142
|
+
let canUseFastPath = false;
|
|
135
143
|
|
|
136
144
|
// Create a container for parsing (to capture all nodes including comment nodes like <!-- if -->)
|
|
137
145
|
const parseContainer = document.createElement('div');
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
firstElement
|
|
147
|
+
if (canUseFastPath) {
|
|
148
|
+
// Fast path: clone template nodes and map to cached tree structure
|
|
149
|
+
for (let i = 0; i < templateNodes.length; i++) {
|
|
150
|
+
const cloned = templateNodes[i].cloneNode(true);
|
|
151
|
+
parseContainer.appendChild(cloned);
|
|
152
|
+
if (!firstElement && cloned.nodeType === 1) {
|
|
153
|
+
firstElement = cloned;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
tree = cloneTreeWithElements(cachedTree, parseContainer);
|
|
157
|
+
} else {
|
|
158
|
+
// Slow path: clone and parse from scratch
|
|
159
|
+
for (let i = 0; i < templateNodes.length; i++) {
|
|
160
|
+
const cloned = templateNodes[i].cloneNode(true);
|
|
161
|
+
parseContainer.appendChild(cloned);
|
|
162
|
+
if (!firstElement && cloned.nodeType === 1) {
|
|
163
|
+
firstElement = cloned;
|
|
164
|
+
}
|
|
145
165
|
}
|
|
166
|
+
// Parse the entire container (includes all nodes + conditionals)
|
|
167
|
+
tree = parse(parseContainer);
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
// Parse the entire container (includes all nodes + conditionals)
|
|
149
|
-
tree = parse(parseContainer);
|
|
150
|
-
|
|
151
170
|
// Extract the cloned nodes from the container (these are the same nodes the tree references)
|
|
152
|
-
|
|
171
|
+
// Avoid Array.from for performance
|
|
172
|
+
const childNodes = parseContainer.childNodes;
|
|
173
|
+
clonedNodes = [];
|
|
174
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
175
|
+
clonedNodes.push(childNodes[i]);
|
|
176
|
+
}
|
|
153
177
|
|
|
154
178
|
// If no firstElement found, use parseContainer as fallback
|
|
155
179
|
if (!firstElement) {
|
|
@@ -271,6 +295,21 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
271
295
|
return;
|
|
272
296
|
}
|
|
273
297
|
|
|
298
|
+
// Fallback check: If markers are lost (e.g., comment nodes replaced by component loading),
|
|
299
|
+
// check actual DOM state between comments for hydrated nodes
|
|
300
|
+
let currentNode = startComment.nextSibling;
|
|
301
|
+
while (currentNode && currentNode !== endComment) {
|
|
302
|
+
if (currentNode.nodeType === 1) {
|
|
303
|
+
// Element node
|
|
304
|
+
const html = currentNode.outerHTML || '';
|
|
305
|
+
// If node doesn't have any @[...] syntax, it's been hydrated
|
|
306
|
+
if (!html.includes('@[')) {
|
|
307
|
+
return; // Already rendered
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
currentNode = currentNode.nextSibling;
|
|
311
|
+
}
|
|
312
|
+
|
|
274
313
|
// Remove template nodes from DOM on first render
|
|
275
314
|
if (!iterationNode.runtime.templateRemoved) {
|
|
276
315
|
let node = startComment.nextSibling;
|
|
@@ -361,7 +400,16 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
361
400
|
// Compiled path: Use pre-compiled batch function when available
|
|
362
401
|
if (compiled.canUseCompiled(iterationNode)) {
|
|
363
402
|
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
364
|
-
if (
|
|
403
|
+
if (
|
|
404
|
+
compiled.updateCompiled(
|
|
405
|
+
iterationNode,
|
|
406
|
+
newArray,
|
|
407
|
+
newState,
|
|
408
|
+
compiledMeta,
|
|
409
|
+
startComment,
|
|
410
|
+
endComment,
|
|
411
|
+
)
|
|
412
|
+
) {
|
|
365
413
|
return;
|
|
366
414
|
}
|
|
367
415
|
// Fall through to runtime path if compiled failed
|
package/runtime/parse.js
CHANGED
|
@@ -12,7 +12,7 @@ const parseHTML = (children, rootKey = undefined) =>
|
|
|
12
12
|
children.reduce((s, element, i) => {
|
|
13
13
|
const { nodeName, textContent } = element;
|
|
14
14
|
if (['#comment'].includes(nodeName)) {
|
|
15
|
-
return
|
|
15
|
+
return s;
|
|
16
16
|
}
|
|
17
17
|
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
|
|
18
18
|
const innerNodeIdentifier = `${name}_${i}`.toLowerCase();
|
|
@@ -77,8 +77,9 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
79
|
// Preserve runtime data from previous parse if it exists (stored on startComment by iterate.js)
|
|
80
|
+
// Only restore if the comment node is still connected to the DOM (not replaced by component loading)
|
|
80
81
|
// @ts-ignore - custom property added by iterate.js
|
|
81
|
-
runtime: element.__vibeIterationRuntime || {
|
|
82
|
+
runtime: (element.isConnected && element.__vibeIterationRuntime) || {
|
|
82
83
|
instances: [],
|
|
83
84
|
templateRemoved: false,
|
|
84
85
|
},
|
|
@@ -43,7 +43,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Check if parsed string contains bindings - split into array
|
|
46
|
-
if (typeof node.parsed ===
|
|
46
|
+
if (typeof node.parsed === "string" && node.parsed.includes("@[")) {
|
|
47
47
|
const parsedArray = splitByMarkers(node.parsed);
|
|
48
48
|
if (parsedArray) {
|
|
49
49
|
result.hyperspeedRestoration = {
|
|
@@ -53,10 +53,10 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Check for attribute bindings
|
|
56
|
-
if (node.attributes && typeof node.attributes ===
|
|
56
|
+
if (node.attributes && typeof node.attributes === "object") {
|
|
57
57
|
const attrBindings = {};
|
|
58
58
|
for (const [key, value] of Object.entries(node.attributes)) {
|
|
59
|
-
if (typeof value ===
|
|
59
|
+
if (typeof value === "string" && value.includes("@[")) {
|
|
60
60
|
attrBindings[key] = value;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -69,7 +69,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// For iterations, extract template as HTML string
|
|
72
|
-
if (node.type ===
|
|
72
|
+
if (node.type === "iteration" && node.meta?.template?.element) {
|
|
73
73
|
const templateElement = node.meta.template.element;
|
|
74
74
|
if (templateElement && templateElement.innerHTML) {
|
|
75
75
|
if (!result.hyperspeedRestoration) {
|
|
@@ -80,7 +80,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// For conditionals, extract branch templates as HTML strings
|
|
83
|
-
if (node.type ===
|
|
83
|
+
if (node.type === "conditional" && node.meta?.branches) {
|
|
84
84
|
const trueBranch = node.meta.branches.true?.element;
|
|
85
85
|
const falseBranch = node.meta.branches.false?.element;
|
|
86
86
|
|
|
@@ -98,7 +98,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Recursively process children (skip runtime-only nodes)
|
|
101
|
-
if (node.children && typeof node.children ===
|
|
101
|
+
if (node.children && typeof node.children === "object") {
|
|
102
102
|
for (const [key, childNode] of Object.entries(node.children)) {
|
|
103
103
|
result.children[key] = walkNode(childNode);
|
|
104
104
|
}
|
|
@@ -116,8 +116,8 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
116
116
|
element: null,
|
|
117
117
|
parsed: [],
|
|
118
118
|
children: walkNode(parsedTree).children, // Use children directly
|
|
119
|
-
}
|
|
120
|
-
}
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
123
|
|
|
@@ -135,8 +135,22 @@ const detectHyperspeed = async () => {
|
|
|
135
135
|
hyperspeedDetectionAttempted = true;
|
|
136
136
|
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
|
|
153
|
+
const pathSegments = pagePath.split("/").filter((s) => s);
|
|
140
154
|
|
|
141
155
|
if (pathSegments.length === 0) return null;
|
|
142
156
|
|
|
@@ -151,9 +165,11 @@ const detectHyperspeed = async () => {
|
|
|
151
165
|
// Strategy 1: vibe-hyperspeed at the same level as parent directory
|
|
152
166
|
// /compiled/playground/test.html -> /compiled/vibe-hyperspeed/playground/test.html.manifest.js
|
|
153
167
|
if (dirSegments.length >= 1) {
|
|
154
|
-
const subPath = dirSegments.slice(1).join(
|
|
155
|
-
const baseDir =
|
|
156
|
-
possiblePaths.push(
|
|
168
|
+
const subPath = dirSegments.slice(1).join("/"); // Everything after first dir
|
|
169
|
+
const baseDir = "/" + dirSegments[0]; // First directory segment
|
|
170
|
+
possiblePaths.push(
|
|
171
|
+
`${baseDir}/vibe-hyperspeed/${subPath ? subPath + "/" : ""}${fileName}.manifest.js`,
|
|
172
|
+
);
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
// Strategy 2: vibe-hyperspeed at web root (original behavior)
|
|
@@ -163,8 +179,10 @@ const detectHyperspeed = async () => {
|
|
|
163
179
|
// Strategy 3: vibe-hyperspeed relative to immediate parent
|
|
164
180
|
// /playground/test.html -> /vibe-hyperspeed/playground/test.html.manifest.js
|
|
165
181
|
if (dirSegments.length > 0) {
|
|
166
|
-
const relativePath = dirSegments.join(
|
|
167
|
-
possiblePaths.push(
|
|
182
|
+
const relativePath = dirSegments.join("/");
|
|
183
|
+
possiblePaths.push(
|
|
184
|
+
`/vibe-hyperspeed/${relativePath}/${fileName}.manifest.js`,
|
|
185
|
+
);
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
// Try each possible path
|
|
@@ -173,7 +191,7 @@ const detectHyperspeed = async () => {
|
|
|
173
191
|
const module = await import(manifestPath);
|
|
174
192
|
hyperspeedData = {
|
|
175
193
|
manifest: module.default,
|
|
176
|
-
path: manifestPath
|
|
194
|
+
path: manifestPath,
|
|
177
195
|
};
|
|
178
196
|
return hyperspeedData;
|
|
179
197
|
} catch (e) {
|
|
@@ -203,7 +221,11 @@ const detectHyperspeed = async () => {
|
|
|
203
221
|
* @param {Object} subtree - The matching subtree from manifest (e.g., manifest.children.body)
|
|
204
222
|
* @param {Object} fullManifest - The full manifest (unused now, kept for compatibility)
|
|
205
223
|
*/
|
|
206
|
-
export const restoreMarkersFromManifest = (
|
|
224
|
+
export const restoreMarkersFromManifest = (
|
|
225
|
+
rootElement,
|
|
226
|
+
subtree,
|
|
227
|
+
fullManifest = null,
|
|
228
|
+
) => {
|
|
207
229
|
const walkTree = (tree, element) => {
|
|
208
230
|
if (!tree || !element) return;
|
|
209
231
|
|
|
@@ -213,17 +235,25 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
213
235
|
if (restoration) {
|
|
214
236
|
// Restore text content with markers if parsed contains bindings
|
|
215
237
|
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
216
|
-
const hasBindings = restoration.parsed.some(
|
|
217
|
-
typeof item ===
|
|
238
|
+
const hasBindings = restoration.parsed.some(
|
|
239
|
+
(item) => typeof item === "string" && item.includes("@["),
|
|
218
240
|
);
|
|
219
241
|
|
|
220
242
|
if (hasBindings) {
|
|
221
243
|
// Reconstruct original content with markers
|
|
222
|
-
|
|
244
|
+
let originalContent = restoration.parsed.join("");
|
|
245
|
+
|
|
246
|
+
// Transform component-scoped bindings back to this. format
|
|
247
|
+
// Compiler transforms @[this.count] → @[_c0.count] for stamping
|
|
248
|
+
// Runtime expects @[this.count], so transform back
|
|
249
|
+
originalContent = originalContent.replace(/@\[_c\d+\./g, "@[this.");
|
|
223
250
|
|
|
224
251
|
// For text nodes, update parent's innerHTML
|
|
225
252
|
// For elements with children, update only text nodes
|
|
226
|
-
if (
|
|
253
|
+
if (
|
|
254
|
+
element.childNodes.length === 1 &&
|
|
255
|
+
element.childNodes[0].nodeType === 3
|
|
256
|
+
) {
|
|
227
257
|
// Single text node - replace it
|
|
228
258
|
element.childNodes[0].textContent = originalContent;
|
|
229
259
|
} else if (element.childNodes.length === 0) {
|
|
@@ -239,7 +269,13 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
239
269
|
// 1. Boolean-like attributes with falsy values were removed - restore them
|
|
240
270
|
// 2. Value attributes were stamped - replace stamped values with markers
|
|
241
271
|
if (restoration.attributes) {
|
|
242
|
-
for (
|
|
272
|
+
for (let [attrName, attrValue] of Object.entries(
|
|
273
|
+
restoration.attributes,
|
|
274
|
+
)) {
|
|
275
|
+
// Transform component-scoped bindings back to this. format
|
|
276
|
+
// Compiler transforms @[this.count] → @[_c0.count], runtime expects @[this.count]
|
|
277
|
+
attrValue = attrValue.replace(/@\[_c\d+\./g, "@[this.");
|
|
278
|
+
|
|
243
279
|
// Always set the attribute to restore the marker
|
|
244
280
|
// - If missing (boolean-like, falsy): adds it back
|
|
245
281
|
// - If present (value attr, stamped): replaces stamped value with marker
|
|
@@ -274,17 +310,22 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
274
310
|
// Text nodes use childNodes indices which become invalid after DOM modifications
|
|
275
311
|
if (tree.children) {
|
|
276
312
|
for (const key in tree.children) {
|
|
277
|
-
if (key.startsWith(
|
|
313
|
+
if (key.startsWith("text_")) {
|
|
278
314
|
const childTree = tree.children[key];
|
|
279
315
|
const restoration = childTree.compiled?.restoration;
|
|
280
316
|
|
|
281
317
|
if (restoration?.parsed && Array.isArray(restoration.parsed)) {
|
|
282
|
-
const hasBindings = restoration.parsed.some(
|
|
283
|
-
typeof item ===
|
|
318
|
+
const hasBindings = restoration.parsed.some(
|
|
319
|
+
(item) => typeof item === "string" && item.includes("@["),
|
|
284
320
|
);
|
|
285
321
|
|
|
286
322
|
if (hasBindings) {
|
|
287
|
-
|
|
323
|
+
// Transform component-scoped bindings back to this. format
|
|
324
|
+
let originalContent = restoration.parsed.join("");
|
|
325
|
+
originalContent = originalContent.replace(
|
|
326
|
+
/@\[_c\d+\./g,
|
|
327
|
+
"@[this.",
|
|
328
|
+
);
|
|
288
329
|
|
|
289
330
|
// Extract index from key (e.g., text_0 -> 0)
|
|
290
331
|
const match = key.match(/_(\d+)$/);
|
|
@@ -315,7 +356,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
315
356
|
const childTree = tree.children[key];
|
|
316
357
|
|
|
317
358
|
// Handle iteration restoration
|
|
318
|
-
if (childTree.type ===
|
|
359
|
+
if (childTree.type === "iteration" && childTree.compiled?.restoration) {
|
|
319
360
|
const restoration = childTree.compiled.restoration;
|
|
320
361
|
|
|
321
362
|
// Find iteration comment by matching the expression
|
|
@@ -323,7 +364,10 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
323
364
|
let endComment = null;
|
|
324
365
|
let depth = 0;
|
|
325
366
|
|
|
326
|
-
const walker = document.createTreeWalker(
|
|
367
|
+
const walker = document.createTreeWalker(
|
|
368
|
+
element,
|
|
369
|
+
NodeFilter.SHOW_COMMENT,
|
|
370
|
+
);
|
|
327
371
|
while (walker.nextNode()) {
|
|
328
372
|
const comment = walker.currentNode;
|
|
329
373
|
const trimmed = comment.textContent.trim();
|
|
@@ -337,9 +381,9 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
337
381
|
startComment = comment;
|
|
338
382
|
depth = 1;
|
|
339
383
|
} else if (startComment) {
|
|
340
|
-
if (trimmed.startsWith(
|
|
384
|
+
if (trimmed.startsWith("each ")) {
|
|
341
385
|
depth++;
|
|
342
|
-
} else if (trimmed ===
|
|
386
|
+
} else if (trimmed === "/each") {
|
|
343
387
|
depth--;
|
|
344
388
|
if (depth === 0) {
|
|
345
389
|
endComment = comment;
|
|
@@ -349,6 +393,10 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
349
393
|
}
|
|
350
394
|
}
|
|
351
395
|
|
|
396
|
+
// Always delete the iteration node from tree - runtime will re-create it
|
|
397
|
+
// Do this even if comments weren't found (they might have been removed by parent restoration)
|
|
398
|
+
delete tree.children[key];
|
|
399
|
+
|
|
352
400
|
if (startComment && endComment) {
|
|
353
401
|
// Mark as processed
|
|
354
402
|
startComment._vibeProcessed = true;
|
|
@@ -368,7 +416,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
368
416
|
}
|
|
369
417
|
|
|
370
418
|
// Insert the template (single item)
|
|
371
|
-
const tempContainer = document.createElement(
|
|
419
|
+
const tempContainer = document.createElement("div");
|
|
372
420
|
tempContainer.innerHTML = restoration.template;
|
|
373
421
|
|
|
374
422
|
const fragment = document.createDocumentFragment();
|
|
@@ -378,30 +426,35 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
378
426
|
parent.insertBefore(fragment, endComment);
|
|
379
427
|
}
|
|
380
428
|
|
|
381
|
-
// Delete the iteration node from tree - runtime will re-create it
|
|
382
|
-
delete tree.children[key];
|
|
383
429
|
continue;
|
|
384
430
|
}
|
|
385
431
|
|
|
386
|
-
if (
|
|
432
|
+
if (
|
|
433
|
+
childTree.type === "conditional" &&
|
|
434
|
+
childTree.compiled?.restoration
|
|
435
|
+
) {
|
|
387
436
|
const restoration = childTree.compiled.restoration;
|
|
388
437
|
|
|
389
438
|
// Find conditional comment markers in the current element
|
|
390
439
|
// But SKIP conditionals that are inside iteration blocks
|
|
391
|
-
const walker = document.createTreeWalker(
|
|
440
|
+
const walker = document.createTreeWalker(
|
|
441
|
+
element,
|
|
442
|
+
NodeFilter.SHOW_COMMENT,
|
|
443
|
+
);
|
|
392
444
|
let startComment = null;
|
|
393
445
|
let endComment = null;
|
|
394
446
|
let insideIteration = false;
|
|
447
|
+
let conditionalDepth = 0;
|
|
395
448
|
|
|
396
449
|
while (walker.nextNode()) {
|
|
397
450
|
const comment = walker.currentNode;
|
|
398
451
|
const text = comment.textContent.trim();
|
|
399
452
|
|
|
400
453
|
// Track if we're inside an iteration block
|
|
401
|
-
if (text.startsWith(
|
|
454
|
+
if (text.startsWith("each ")) {
|
|
402
455
|
insideIteration = true;
|
|
403
456
|
continue;
|
|
404
|
-
} else if (text ===
|
|
457
|
+
} else if (text === "/each") {
|
|
405
458
|
insideIteration = false;
|
|
406
459
|
continue;
|
|
407
460
|
}
|
|
@@ -412,14 +465,27 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
412
465
|
// Skip already processed conditionals
|
|
413
466
|
if (comment._vibeProcessed) continue;
|
|
414
467
|
|
|
415
|
-
if (text.startsWith(
|
|
416
|
-
startComment
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
468
|
+
if (text.startsWith("if")) {
|
|
469
|
+
if (!startComment) {
|
|
470
|
+
startComment = comment;
|
|
471
|
+
conditionalDepth = 1;
|
|
472
|
+
} else {
|
|
473
|
+
// Track nested conditionals
|
|
474
|
+
conditionalDepth++;
|
|
475
|
+
}
|
|
476
|
+
} else if (text === "/if" && startComment) {
|
|
477
|
+
conditionalDepth--;
|
|
478
|
+
if (conditionalDepth === 0) {
|
|
479
|
+
endComment = comment;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
420
482
|
}
|
|
421
483
|
}
|
|
422
484
|
|
|
485
|
+
// Always delete the conditional node from tree - runtime will re-create it
|
|
486
|
+
// Do this even if comments weren't found (they might have been removed by parent restoration)
|
|
487
|
+
delete tree.children[key];
|
|
488
|
+
|
|
423
489
|
if (startComment && endComment) {
|
|
424
490
|
// Mark as processed
|
|
425
491
|
startComment._vibeProcessed = true;
|
|
@@ -430,12 +496,12 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
430
496
|
let current = startComment.nextSibling;
|
|
431
497
|
while (current && current !== endComment) {
|
|
432
498
|
const next = current.nextSibling;
|
|
433
|
-
current.remove();
|
|
499
|
+
current.remove(); // Remove ALL nodes, including comment nodes
|
|
434
500
|
current = next;
|
|
435
501
|
}
|
|
436
502
|
|
|
437
503
|
// Insert the template content BEFORE endComment
|
|
438
|
-
const tempContainer = document.createElement(
|
|
504
|
+
const tempContainer = document.createElement("div");
|
|
439
505
|
tempContainer.innerHTML = restoration.template;
|
|
440
506
|
|
|
441
507
|
const fragment = document.createDocumentFragment();
|
|
@@ -444,10 +510,6 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
444
510
|
}
|
|
445
511
|
endComment.parentNode.insertBefore(fragment, endComment);
|
|
446
512
|
}
|
|
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
513
|
}
|
|
452
514
|
}
|
|
453
515
|
}
|
|
@@ -458,28 +520,33 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
458
520
|
const childTree = tree.children[key];
|
|
459
521
|
|
|
460
522
|
// Skip conditionals and iterations (already processed/deleted above)
|
|
461
|
-
if (
|
|
523
|
+
if (
|
|
524
|
+
childTree.type === "conditional" ||
|
|
525
|
+
childTree.type === "iteration"
|
|
526
|
+
) {
|
|
462
527
|
continue;
|
|
463
528
|
}
|
|
464
529
|
|
|
465
530
|
// Skip text nodes - already handled before conditional/iteration processing
|
|
466
|
-
if (key.startsWith(
|
|
531
|
+
if (key.startsWith("text_")) {
|
|
467
532
|
continue;
|
|
468
533
|
}
|
|
469
534
|
|
|
470
535
|
// Find corresponding child element by tag name and index
|
|
471
536
|
// Keys like "layout_1" mean the node at childNodes index 1 (includes text nodes)
|
|
472
|
-
// NOT the second layout element
|
|
473
537
|
// Tag names can contain digits (h1, h2, etc.) so use [a-z0-9-]+
|
|
474
538
|
const match = key.match(/^([a-z0-9-]+)_(\d+)$/);
|
|
475
539
|
if (match) {
|
|
476
540
|
const tagName = match[1].toUpperCase();
|
|
477
541
|
const nodeIndex = parseInt(match[2], 10);
|
|
478
542
|
|
|
479
|
-
// Get the node at this childNodes index
|
|
480
543
|
const childNode = element.childNodes[nodeIndex];
|
|
481
544
|
|
|
482
|
-
if (
|
|
545
|
+
if (
|
|
546
|
+
childNode &&
|
|
547
|
+
childNode.nodeType === Node.ELEMENT_NODE &&
|
|
548
|
+
childNode.nodeName === tagName
|
|
549
|
+
) {
|
|
483
550
|
walkTree(childTree, childNode);
|
|
484
551
|
}
|
|
485
552
|
}
|