@ape-egg/vibe 1.6.1 → 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.
@@ -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 === 'string' && node.parsed.includes('@[')) {
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 === 'object') {
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 === 'string' && value.includes('@[')) {
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 === 'iteration' && node.meta?.template?.element) {
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 === 'conditional' && node.meta?.branches) {
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 === 'object') {
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
 
@@ -138,19 +138,19 @@ const detectHyperspeed = async () => {
138
138
  let pagePath = window.location.pathname;
139
139
 
140
140
  // Normalize path: handle directory URLs and missing extensions
141
- if (pagePath.endsWith('/')) {
141
+ if (pagePath.endsWith("/")) {
142
142
  // /compiled/ -> /compiled/index.html
143
- pagePath = pagePath + 'index.html';
144
- } else if (!pagePath.includes('.')) {
143
+ pagePath = pagePath + "index.html";
144
+ } else if (!pagePath.includes(".")) {
145
145
  // /compiled/mypage -> /compiled/mypage.html
146
- const lastSlash = pagePath.lastIndexOf('/');
146
+ const lastSlash = pagePath.lastIndexOf("/");
147
147
  const lastSegment = pagePath.substring(lastSlash + 1);
148
- if (lastSegment && !lastSegment.includes('.')) {
149
- pagePath = pagePath + '.html';
148
+ if (lastSegment && !lastSegment.includes(".")) {
149
+ pagePath = pagePath + ".html";
150
150
  }
151
151
  }
152
152
 
153
- const pathSegments = pagePath.split('/').filter(s => s);
153
+ const pathSegments = pagePath.split("/").filter((s) => s);
154
154
 
155
155
  if (pathSegments.length === 0) return null;
156
156
 
@@ -165,9 +165,11 @@ const detectHyperspeed = async () => {
165
165
  // Strategy 1: vibe-hyperspeed at the same level as parent directory
166
166
  // /compiled/playground/test.html -> /compiled/vibe-hyperspeed/playground/test.html.manifest.js
167
167
  if (dirSegments.length >= 1) {
168
- const subPath = dirSegments.slice(1).join('/'); // Everything after first dir
169
- const baseDir = '/' + dirSegments[0]; // First directory segment
170
- possiblePaths.push(`${baseDir}/vibe-hyperspeed/${subPath ? subPath + '/' : ''}${fileName}.manifest.js`);
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
+ );
171
173
  }
172
174
 
173
175
  // Strategy 2: vibe-hyperspeed at web root (original behavior)
@@ -177,8 +179,10 @@ const detectHyperspeed = async () => {
177
179
  // Strategy 3: vibe-hyperspeed relative to immediate parent
178
180
  // /playground/test.html -> /vibe-hyperspeed/playground/test.html.manifest.js
179
181
  if (dirSegments.length > 0) {
180
- const relativePath = dirSegments.join('/');
181
- possiblePaths.push(`/vibe-hyperspeed/${relativePath}/${fileName}.manifest.js`);
182
+ const relativePath = dirSegments.join("/");
183
+ possiblePaths.push(
184
+ `/vibe-hyperspeed/${relativePath}/${fileName}.manifest.js`,
185
+ );
182
186
  }
183
187
 
184
188
  // Try each possible path
@@ -187,7 +191,7 @@ const detectHyperspeed = async () => {
187
191
  const module = await import(manifestPath);
188
192
  hyperspeedData = {
189
193
  manifest: module.default,
190
- path: manifestPath
194
+ path: manifestPath,
191
195
  };
192
196
  return hyperspeedData;
193
197
  } catch (e) {
@@ -217,7 +221,11 @@ const detectHyperspeed = async () => {
217
221
  * @param {Object} subtree - The matching subtree from manifest (e.g., manifest.children.body)
218
222
  * @param {Object} fullManifest - The full manifest (unused now, kept for compatibility)
219
223
  */
220
- export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest = null) => {
224
+ export const restoreMarkersFromManifest = (
225
+ rootElement,
226
+ subtree,
227
+ fullManifest = null,
228
+ ) => {
221
229
  const walkTree = (tree, element) => {
222
230
  if (!tree || !element) return;
223
231
 
@@ -227,17 +235,25 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
227
235
  if (restoration) {
228
236
  // Restore text content with markers if parsed contains bindings
229
237
  if (restoration.parsed && Array.isArray(restoration.parsed)) {
230
- const hasBindings = restoration.parsed.some(item =>
231
- typeof item === 'string' && item.includes('@[')
238
+ const hasBindings = restoration.parsed.some(
239
+ (item) => typeof item === "string" && item.includes("@["),
232
240
  );
233
241
 
234
242
  if (hasBindings) {
235
243
  // Reconstruct original content with markers
236
- const originalContent = restoration.parsed.join('');
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.");
237
250
 
238
251
  // For text nodes, update parent's innerHTML
239
252
  // For elements with children, update only text nodes
240
- if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) {
253
+ if (
254
+ element.childNodes.length === 1 &&
255
+ element.childNodes[0].nodeType === 3
256
+ ) {
241
257
  // Single text node - replace it
242
258
  element.childNodes[0].textContent = originalContent;
243
259
  } else if (element.childNodes.length === 0) {
@@ -253,7 +269,13 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
253
269
  // 1. Boolean-like attributes with falsy values were removed - restore them
254
270
  // 2. Value attributes were stamped - replace stamped values with markers
255
271
  if (restoration.attributes) {
256
- for (const [attrName, attrValue] of Object.entries(restoration.attributes)) {
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
+
257
279
  // Always set the attribute to restore the marker
258
280
  // - If missing (boolean-like, falsy): adds it back
259
281
  // - If present (value attr, stamped): replaces stamped value with marker
@@ -288,17 +310,22 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
288
310
  // Text nodes use childNodes indices which become invalid after DOM modifications
289
311
  if (tree.children) {
290
312
  for (const key in tree.children) {
291
- if (key.startsWith('text_')) {
313
+ if (key.startsWith("text_")) {
292
314
  const childTree = tree.children[key];
293
315
  const restoration = childTree.compiled?.restoration;
294
316
 
295
317
  if (restoration?.parsed && Array.isArray(restoration.parsed)) {
296
- const hasBindings = restoration.parsed.some(item =>
297
- typeof item === 'string' && item.includes('@[')
318
+ const hasBindings = restoration.parsed.some(
319
+ (item) => typeof item === "string" && item.includes("@["),
298
320
  );
299
321
 
300
322
  if (hasBindings) {
301
- const originalContent = restoration.parsed.join('');
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
+ );
302
329
 
303
330
  // Extract index from key (e.g., text_0 -> 0)
304
331
  const match = key.match(/_(\d+)$/);
@@ -329,7 +356,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
329
356
  const childTree = tree.children[key];
330
357
 
331
358
  // Handle iteration restoration
332
- if (childTree.type === 'iteration' && childTree.compiled?.restoration) {
359
+ if (childTree.type === "iteration" && childTree.compiled?.restoration) {
333
360
  const restoration = childTree.compiled.restoration;
334
361
 
335
362
  // Find iteration comment by matching the expression
@@ -337,7 +364,10 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
337
364
  let endComment = null;
338
365
  let depth = 0;
339
366
 
340
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
367
+ const walker = document.createTreeWalker(
368
+ element,
369
+ NodeFilter.SHOW_COMMENT,
370
+ );
341
371
  while (walker.nextNode()) {
342
372
  const comment = walker.currentNode;
343
373
  const trimmed = comment.textContent.trim();
@@ -351,9 +381,9 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
351
381
  startComment = comment;
352
382
  depth = 1;
353
383
  } else if (startComment) {
354
- if (trimmed.startsWith('each ')) {
384
+ if (trimmed.startsWith("each ")) {
355
385
  depth++;
356
- } else if (trimmed === '/each') {
386
+ } else if (trimmed === "/each") {
357
387
  depth--;
358
388
  if (depth === 0) {
359
389
  endComment = comment;
@@ -363,6 +393,10 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
363
393
  }
364
394
  }
365
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
+
366
400
  if (startComment && endComment) {
367
401
  // Mark as processed
368
402
  startComment._vibeProcessed = true;
@@ -382,7 +416,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
382
416
  }
383
417
 
384
418
  // Insert the template (single item)
385
- const tempContainer = document.createElement('div');
419
+ const tempContainer = document.createElement("div");
386
420
  tempContainer.innerHTML = restoration.template;
387
421
 
388
422
  const fragment = document.createDocumentFragment();
@@ -392,30 +426,35 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
392
426
  parent.insertBefore(fragment, endComment);
393
427
  }
394
428
 
395
- // Delete the iteration node from tree - runtime will re-create it
396
- delete tree.children[key];
397
429
  continue;
398
430
  }
399
431
 
400
- if (childTree.type === 'conditional' && childTree.compiled?.restoration) {
432
+ if (
433
+ childTree.type === "conditional" &&
434
+ childTree.compiled?.restoration
435
+ ) {
401
436
  const restoration = childTree.compiled.restoration;
402
437
 
403
438
  // Find conditional comment markers in the current element
404
439
  // But SKIP conditionals that are inside iteration blocks
405
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
440
+ const walker = document.createTreeWalker(
441
+ element,
442
+ NodeFilter.SHOW_COMMENT,
443
+ );
406
444
  let startComment = null;
407
445
  let endComment = null;
408
446
  let insideIteration = false;
447
+ let conditionalDepth = 0;
409
448
 
410
449
  while (walker.nextNode()) {
411
450
  const comment = walker.currentNode;
412
451
  const text = comment.textContent.trim();
413
452
 
414
453
  // Track if we're inside an iteration block
415
- if (text.startsWith('each ')) {
454
+ if (text.startsWith("each ")) {
416
455
  insideIteration = true;
417
456
  continue;
418
- } else if (text === '/each') {
457
+ } else if (text === "/each") {
419
458
  insideIteration = false;
420
459
  continue;
421
460
  }
@@ -426,14 +465,27 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
426
465
  // Skip already processed conditionals
427
466
  if (comment._vibeProcessed) continue;
428
467
 
429
- if (text.startsWith('if') && !startComment) {
430
- startComment = comment;
431
- } else if (text === '/if' && startComment && !endComment) {
432
- endComment = comment;
433
- break;
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
+ }
434
482
  }
435
483
  }
436
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
+
437
489
  if (startComment && endComment) {
438
490
  // Mark as processed
439
491
  startComment._vibeProcessed = true;
@@ -444,12 +496,12 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
444
496
  let current = startComment.nextSibling;
445
497
  while (current && current !== endComment) {
446
498
  const next = current.nextSibling;
447
- current.remove(); // Remove ALL nodes, including comment nodes
499
+ current.remove(); // Remove ALL nodes, including comment nodes
448
500
  current = next;
449
501
  }
450
502
 
451
503
  // Insert the template content BEFORE endComment
452
- const tempContainer = document.createElement('div');
504
+ const tempContainer = document.createElement("div");
453
505
  tempContainer.innerHTML = restoration.template;
454
506
 
455
507
  const fragment = document.createDocumentFragment();
@@ -458,10 +510,6 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
458
510
  }
459
511
  endComment.parentNode.insertBefore(fragment, endComment);
460
512
  }
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
513
  }
466
514
  }
467
515
  }
@@ -472,28 +520,33 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
472
520
  const childTree = tree.children[key];
473
521
 
474
522
  // Skip conditionals and iterations (already processed/deleted above)
475
- if (childTree.type === 'conditional' || childTree.type === 'iteration') {
523
+ if (
524
+ childTree.type === "conditional" ||
525
+ childTree.type === "iteration"
526
+ ) {
476
527
  continue;
477
528
  }
478
529
 
479
530
  // Skip text nodes - already handled before conditional/iteration processing
480
- if (key.startsWith('text_')) {
531
+ if (key.startsWith("text_")) {
481
532
  continue;
482
533
  }
483
534
 
484
535
  // Find corresponding child element by tag name and index
485
536
  // Keys like "layout_1" mean the node at childNodes index 1 (includes text nodes)
486
- // NOT the second layout element
487
537
  // Tag names can contain digits (h1, h2, etc.) so use [a-z0-9-]+
488
538
  const match = key.match(/^([a-z0-9-]+)_(\d+)$/);
489
539
  if (match) {
490
540
  const tagName = match[1].toUpperCase();
491
541
  const nodeIndex = parseInt(match[2], 10);
492
542
 
493
- // Get the node at this childNodes index
494
543
  const childNode = element.childNodes[nodeIndex];
495
544
 
496
- if (childNode && childNode.nodeType === Node.ELEMENT_NODE && childNode.nodeName === tagName) {
545
+ if (
546
+ childNode &&
547
+ childNode.nodeType === Node.ELEMENT_NODE &&
548
+ childNode.nodeName === tagName
549
+ ) {
497
550
  walkTree(childTree, childNode);
498
551
  }
499
552
  }