@ape-egg/vibe 1.3.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,17 @@
1
1
  import { debugLog } from './debug.js';
2
- import { PHASE_FETCH } from './constants.js';
2
+ import { PHASE_FETCH, DEHYDRATE_CLASS_OR_ATTR } from './constants.js';
3
3
  import { evalInScope } from './utils.js';
4
- import { generateComponentId, executeComponentScript } from './component-state.js';
4
+
5
+ // Deterministic component counter
6
+ let componentCounter = 0;
7
+
8
+ /**
9
+ * Generate unique component ID
10
+ * Uses deterministic counter: _c0, _c1, _c2, etc.
11
+ */
12
+ export const generateComponentId = () => {
13
+ return `_c${componentCounter++}`;
14
+ };
5
15
 
6
16
  // Helper to escape regex special characters
7
17
  const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -32,6 +42,19 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
32
42
 
33
43
  // Process just the first element - MutationObserver will trigger next call
34
44
  const el = componentElements[0];
45
+
46
+ // Skip if component is dehydrated (vibe-dehydrate attribute or class)
47
+ if (el.hasAttribute(DEHYDRATE_CLASS_OR_ATTR) || el.classList.contains(DEHYDRATE_CLASS_OR_ATTR)) {
48
+ // Skip this component and continue to next
49
+ if (componentElements.length > 1) {
50
+ // Process next component
51
+ processComponent(rootElement, onComplete, config);
52
+ } else {
53
+ if (onComplete) onComplete();
54
+ }
55
+ return;
56
+ }
57
+
35
58
  const src = el.getAttribute('src');
36
59
 
37
60
  // Capture children and props before fetching
@@ -54,39 +77,93 @@ export const processComponent = (rootElement, onComplete, config = {}) => {
54
77
  const temp = document.createElement('div');
55
78
  temp.innerHTML = html;
56
79
 
57
- // Process any <script type="component"> elements
58
- const componentScripts = temp.querySelectorAll('script[type="component"]');
59
- componentScripts.forEach((script) => {
60
- const scriptContent = script.textContent?.trim() || '';
80
+ // Process any <script type="module"> elements
81
+ const moduleScripts = temp.querySelectorAll('script[type="module"]');
82
+ moduleScripts.forEach((script) => {
83
+ let scriptContent = script.textContent?.trim() || '';
61
84
  if (!scriptContent) return;
62
85
 
63
- // Generate component ID
86
+ // Strip import statements (we provide component() manually)
87
+ // Remove lines like: import component from '...';
88
+ scriptContent = scriptContent.replace(/import\s+\w+\s+from\s+['"][^'"]+['"];?\s*/g, '');
89
+
90
+ // Generate component ID for this instance
64
91
  const componentId = generateComponentId();
65
92
 
66
- // Tag script element
67
- script.setAttribute('data-vibe-component-id', componentId);
93
+ // Provide a component() function that registers state for this component
94
+ const componentFn = (state) => {
95
+ // Register component state in both places:
96
+ // 1. __vibeComponents registry (for pre-boot components)
97
+ if (!window.__vibeComponents) {
98
+ window.__vibeComponents = {};
99
+ }
100
+ window.__vibeComponents[componentId] = state;
101
+
102
+ // 2. Directly in window.$ (the reactive proxy) for post-boot components
103
+ if (window.$) {
104
+ window.$[componentId] = state;
105
+ }
106
+
107
+ // Tag all siblings (everything after the script in this component)
108
+ let sibling = script.nextElementSibling;
109
+ while (sibling) {
110
+ // Stop if we hit another module script
111
+ if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
112
+ break;
113
+ }
114
+ sibling.setAttribute('data-vibe-component-id', componentId);
115
+ sibling = sibling.nextElementSibling;
116
+ }
117
+ };
118
+
119
+ // Execute script with component() function in scope
120
+ // Use Function constructor to provide 'component' as a parameter
121
+ try {
122
+ const executeFn = new Function('component', scriptContent);
123
+ executeFn(componentFn);
124
+ } catch (e) {
125
+ console.warn('[vibe] Failed to execute component script:', e);
126
+ }
127
+
128
+ // Rewrite this.property to componentId.property in siblings
129
+ // This allows the runtime to resolve component-scoped bindings
130
+ const rewriteThisBindings = (element) => {
131
+ const thisRegex = /@\[this\.(\w+)\]/g;
132
+
133
+ // Rewrite in text nodes
134
+ Array.from(element.childNodes).forEach(node => {
135
+ if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('@[this.')) {
136
+ node.textContent = node.textContent.replace(thisRegex, `@[${componentId}.$1]`);
137
+ }
138
+ });
139
+
140
+ // Rewrite in attributes
141
+ Array.from(element.attributes || []).forEach(attr => {
142
+ if (attr.value.includes('@[this.')) {
143
+ attr.value = attr.value.replace(thisRegex, `@[${componentId}.$1]`);
144
+ }
145
+ });
146
+
147
+ // Recurse into children
148
+ Array.from(element.children).forEach(child => {
149
+ rewriteThisBindings(child);
150
+ });
151
+ };
68
152
 
69
- // Tag following siblings with this component ID
70
153
  let sibling = script.nextElementSibling;
71
154
  while (sibling) {
72
- // Stop if we hit another component script
73
- if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'component') {
155
+ if (sibling.tagName === 'SCRIPT' && sibling.getAttribute('type') === 'module') {
74
156
  break;
75
157
  }
76
- sibling.setAttribute('data-vibe-component-id', componentId);
158
+ rewriteThisBindings(sibling);
77
159
  sibling = sibling.nextElementSibling;
78
160
  }
79
161
 
80
- // Execute component script to get state
81
- const componentState = executeComponentScript(scriptContent);
82
-
83
- // Register in global state
84
- if (window.$) {
85
- window.$[componentId] = componentState;
86
- }
162
+ // Remove script from temp (we executed it manually)
163
+ script.remove();
87
164
  });
88
165
 
89
- // Get transformed HTML from temp container
166
+ // Get transformed HTML from temp container (scripts removed)
90
167
  let transformedHtml = temp.innerHTML;
91
168
 
92
169
  // Replace props
@@ -58,8 +58,8 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
58
58
  }
59
59
 
60
60
  // Remove original template nodes from DOM (between start and end comments)
61
- // Only do this on first render (when activeBranch is undefined)
62
- if (node.runtime.activeBranch === undefined) {
61
+ // Only do this on first render (when template hasn't been removed yet)
62
+ if (!node.runtime.templateRemoved) {
63
63
  let currentNode = startComment.nextSibling;
64
64
  while (currentNode && currentNode !== endComment) {
65
65
  const nextNode = currentNode.nextSibling;
@@ -1,6 +1,7 @@
1
1
  // Debug logger name
2
2
  export const DEBUGGER_NAME = '[vibe-debug]:';
3
3
  export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
4
+ export const DEHYDRATE_CLASS_OR_ATTR = 'vibe-dehydrate'; // Class or attribute used to skip reactive processing
4
5
 
5
6
  // Lifecycle phase names for debug logging
6
7
  // ONE-OFF operations (run once during initialization)
@@ -17,7 +18,7 @@ export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (condi
17
18
  export const PHASE_FETCH = 'Fetched'; // Loads <component> content (component.js)
18
19
  export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
19
20
  export const PHASE_MUTATE = 'Mutation'; // DOM mutations detected by observer (index.js)
20
- export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (hyperspeed.js)
21
+ export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (pre-compiled-manifest.js)
21
22
 
22
23
  // Elements that should not have reactive bindings
23
24
  // Note: COMPONENT is NOT in this list - inline component wrappers need to be parsed
package/runtime/index.js CHANGED
@@ -18,16 +18,17 @@ import {
18
18
  PHASE_UPDATE,
19
19
  PHASE_MUTATE,
20
20
  PHASE_HYPERSPEED,
21
+ DEHYDRATE_CLASS_OR_ATTR,
21
22
  } from './constants.js';
22
23
  import { processComponent, abortComponentFetch } from './component.js';
23
24
  import { debugLog } from './debug.js';
24
25
  import { shouldCleanup, cleanup } from './cleanup.js';
25
- import { generateComponentId, executeComponentScript } from './component-state.js';
26
26
  import {
27
27
  buildHyperspeedManifest,
28
28
  hyperspeedManifest,
29
+ hyperspeedPath,
29
30
  restoreMarkersFromManifest,
30
- } from './hyperspeed.js';
31
+ } from './pre-compiled-manifest.js';
31
32
 
32
33
  // Wire up cross-module dependency after all modules are loaded
33
34
  setRenderAllConditionals(renderAllConditionals);
@@ -54,7 +55,10 @@ const shouldProcessNode = (node) => {
54
55
  if (NON_REACTIVE_ELEMENTS.includes(current.nodeName)) {
55
56
  return false;
56
57
  }
57
- if (current.hasAttribute?.('dehydrate')) {
58
+ if (
59
+ current.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) ||
60
+ current.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)
61
+ ) {
58
62
  return false;
59
63
  }
60
64
  current = current.parentElement;
@@ -183,13 +187,14 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
183
187
  if (!hyperspeedTree) return runtimeTree;
184
188
  if (!runtimeTree) return hyperspeedTree;
185
189
 
186
- // Start with deep clone of hyperspeed (foundation)
187
- const merged = deepCloneNode(hyperspeedTree);
190
+ // hyperspeedTree is already a clone (done before restoration to avoid mutation)
191
+ const merged = hyperspeedTree;
188
192
 
189
193
  // Helper to recursively augment hyperspeed with runtime data
190
194
  const augmentWithRuntime = (mergedNode, runtimeNode) => {
191
195
  if (!runtimeNode) return;
192
196
 
197
+ // Debug: log node types
193
198
  // Populate DOM references from runtime
194
199
  if (runtimeNode.element) {
195
200
  mergedNode.element = runtimeNode.element;
@@ -200,18 +205,43 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
200
205
  mergedNode.parsed = runtimeNode.parsed;
201
206
  }
202
207
 
208
+ // Populate name bindings from runtime (detected after restoration)
209
+ if (runtimeNode.nameBindings) {
210
+ mergedNode.nameBindings = runtimeNode.nameBindings;
211
+ }
212
+
213
+ // Populate attributes from runtime (detected after restoration)
214
+ if (runtimeNode.attributes) {
215
+ mergedNode.attributes = runtimeNode.attributes;
216
+ }
217
+
218
+ // Populate textNode reference from runtime (for text node children)
219
+ if (runtimeNode.textNode) {
220
+ mergedNode.textNode = runtimeNode.textNode;
221
+ }
222
+
203
223
  // For iterations: populate all meta and runtime from runtime
204
224
  if (mergedNode.type === 'iteration' && runtimeNode.type === 'iteration') {
225
+ // Preserve compiled data from manifest (has compiled batch function)
226
+ const compiledData = mergedNode.compiled;
227
+
205
228
  // Copy entire meta object from runtime (all properties needed)
206
229
  mergedNode.meta = runtimeNode.meta;
207
230
  // Copy runtime object (instances, etc.)
208
231
  mergedNode.runtime = runtimeNode.runtime;
232
+
233
+ // Restore compiled data if it was present
234
+ if (compiledData) {
235
+ mergedNode.compiled = compiledData;
236
+ }
209
237
  }
210
238
 
211
- // For conditionals: populate all meta from runtime
239
+ // For conditionals: populate all meta and runtime from runtime
212
240
  if (mergedNode.type === 'conditional' && runtimeNode.type === 'conditional') {
213
241
  // Copy entire meta object from runtime (all properties needed)
214
242
  mergedNode.meta = runtimeNode.meta;
243
+ // Copy runtime object (activeBranch, activeInstance, templateRemoved)
244
+ mergedNode.runtime = runtimeNode.runtime;
215
245
  }
216
246
 
217
247
  // Augment children recursively
@@ -221,10 +251,40 @@ const mergeManifests = (hyperspeedTree, runtimeTree) => {
221
251
  for (const key in runtimeNode.children) {
222
252
  const runtimeChild = runtimeNode.children[key];
223
253
 
224
- // Skip iterations - let runtime control them entirely
225
- // Iterations are dynamic and restoration changes DOM structure
254
+ // For iterations: augment existing node, don't replace
226
255
  if (runtimeChild.type === 'iteration') {
256
+ const hyperspeedChild = mergedNode.children[key];
257
+
258
+ if (hyperspeedChild) {
259
+ // Preserve compiled data from hyperspeed
260
+ const compiledData = hyperspeedChild.compiled;
261
+
262
+ // Update meta and runtime from runtime node
263
+ mergedNode.children[key].meta = runtimeChild.meta;
264
+ mergedNode.children[key].runtime = runtimeChild.runtime;
265
+
266
+ // Keep compiled data from hyperspeed (it has batchFn)
267
+ if (compiledData) {
268
+ mergedNode.children[key].compiled = compiledData;
269
+ }
270
+ } else {
271
+ // No hyperspeed node, just use runtime
272
+ mergedNode.children[key] = runtimeChild;
273
+ }
274
+ continue;
275
+ }
276
+
277
+ // For conditionals: use runtime node but preserve compiled data
278
+ if (runtimeChild.type === 'conditional') {
279
+ const hyperspeedChild = mergedNode.children[key];
280
+ const compiledData = hyperspeedChild?.compiled;
281
+
227
282
  mergedNode.children[key] = runtimeChild;
283
+
284
+ // Restore compiled data if it existed
285
+ if (compiledData) {
286
+ mergedNode.children[key].compiled = compiledData;
287
+ }
228
288
  continue;
229
289
  }
230
290
 
@@ -277,7 +337,7 @@ const main = (s, config = {}, stringSelector = '') => {
277
337
  }
278
338
  }
279
339
 
280
- debugLog(PHASE_ATTACH, `Vibe attached to`, true, 0, rootElement);
340
+ debugLog(PHASE_ATTACH, `Vibe attached to`, debug, 0, rootElement);
281
341
 
282
342
  // Component tagging is now handled by component.js before boot
283
343
  // Component state is merged into s by boot.js
@@ -286,7 +346,38 @@ const main = (s, config = {}, stringSelector = '') => {
286
346
  // This allows pre-rendered values to be visible (no FOUC) but makes DOM reactive
287
347
  let hyperspeedSubtree = null;
288
348
  if (hyperspeedTree) {
289
- debugLog(PHASE_HYPERSPEED, 'Applied pre-compiled vibe-hyperspeed/**/*.manifest.js', debug);
349
+ const manifestName = hyperspeedPath ? hyperspeedPath.split('/').pop() : 'manifest.js';
350
+
351
+ // Count compiled features
352
+ const countCompiledIterations = (tree) => {
353
+ let count = 0;
354
+ if (tree.type === 'iteration' && tree.compiled?.iterations?.batchFn) count++;
355
+ if (tree.children) {
356
+ for (const key in tree.children) {
357
+ count += countCompiledIterations(tree.children[key]);
358
+ }
359
+ }
360
+ return count;
361
+ };
362
+
363
+ const compiledIterationCount = countCompiledIterations(hyperspeedTree);
364
+
365
+ // Log features that are enabled
366
+ debugLog(PHASE_HYPERSPEED, `Loaded ${manifestName}, page is pre-compiled`, debug);
367
+
368
+ if (compiledIterationCount > 0) {
369
+ debugLog(
370
+ PHASE_HYPERSPEED,
371
+ [
372
+ { text: String(compiledIterationCount), colored: true },
373
+ {
374
+ text: ` ${compiledIterationCount === 1 ? 'iteration' : 'iterations'} optimized`,
375
+ colored: false,
376
+ },
377
+ ],
378
+ debug,
379
+ );
380
+ }
290
381
 
291
382
  // Find matching subtree by DOM path (not just tag name)
292
383
  // Build path from document to rootElement
@@ -347,14 +438,19 @@ const main = (s, config = {}, stringSelector = '') => {
347
438
  };
348
439
 
349
440
  const domPath = buildDomPath(rootElement);
350
- hyperspeedSubtree = findManifestNodeByPath(hyperspeedTree, domPath);
441
+ let subtreeFromManifest = findManifestNodeByPath(hyperspeedTree, domPath);
351
442
 
352
- if (!hyperspeedSubtree) {
443
+ if (!subtreeFromManifest) {
353
444
  // Fallback to root if path matching fails
354
- hyperspeedSubtree = hyperspeedTree;
445
+ subtreeFromManifest = hyperspeedTree;
355
446
  }
356
447
 
357
- restoreMarkersFromManifest(rootElement, hyperspeedSubtree, hyperspeedTree);
448
+ // IMPORTANT: Clone BEFORE restoration because restoration mutates the tree
449
+ // We need TWO clones: one for restoration (gets mutated), one for merging (stays intact)
450
+ hyperspeedSubtree = deepCloneNode(subtreeFromManifest); // For merging
451
+ const cloneForRestoration = deepCloneNode(subtreeFromManifest); // For restoration
452
+
453
+ restoreMarkersFromManifest(rootElement, cloneForRestoration, hyperspeedTree);
358
454
  }
359
455
 
360
456
  // Runtime parses DOM (which now has restored markers if hyperspeed was used)
@@ -379,9 +475,9 @@ const main = (s, config = {}, stringSelector = '') => {
379
475
 
380
476
  const segments = [
381
477
  { text: `${elementsOnly}`, colored: true },
382
- { text: ' elements (', colored: false },
478
+ { text: ` ${elementsOnly === 1 ? 'element' : 'elements'} (`, colored: false },
383
479
  { text: `${totalCount}`, color: 'slate' },
384
- { text: ' total nodes)', colored: false },
480
+ { text: ` total ${totalCount === 1 ? 'node' : 'nodes'})`, colored: false },
385
481
  ];
386
482
  if (parsedTree.stats?.skipped > 0) {
387
483
  segments.push(
@@ -483,7 +579,10 @@ const main = (s, config = {}, stringSelector = '') => {
483
579
  debugLog(
484
580
  PHASE_HYDRATE,
485
581
  [
486
- { text: `${affectedElements.length} bindings (`, colored: false },
582
+ {
583
+ text: `${affectedElements.length} ${affectedElements.length === 1 ? 'binding' : 'bindings'} (`,
584
+ colored: false,
585
+ },
487
586
  { text: '@[...]', color: 'pink' },
488
587
  { text: ')', colored: false },
489
588
  ],
@@ -501,7 +600,10 @@ const main = (s, config = {}, stringSelector = '') => {
501
600
  debugLog(
502
601
  PHASE_ITERATE,
503
602
  [
504
- { text: `${iterationCount} iterations (`, colored: false },
603
+ {
604
+ text: `${iterationCount} ${iterationCount === 1 ? 'iteration' : 'iterations'} (`,
605
+ colored: false,
606
+ },
505
607
  { text: '<!-- each -->', color: 'commentGreen' },
506
608
  { text: ')', colored: false },
507
609
  ],
@@ -513,7 +615,10 @@ const main = (s, config = {}, stringSelector = '') => {
513
615
  debugLog(
514
616
  PHASE_CONDITION,
515
617
  [
516
- { text: `${conditionalCount} conditionals (`, colored: false },
618
+ {
619
+ text: `${conditionalCount} ${conditionalCount === 1 ? 'conditional' : 'conditionals'} (`,
620
+ colored: false,
621
+ },
517
622
  { text: '<!-- if -->', color: 'commentGreen' },
518
623
  { text: ')', colored: false },
519
624
  ],
@@ -667,25 +772,31 @@ const main = (s, config = {}, stringSelector = '') => {
667
772
  // Added
668
773
  if (addedElements > 0) {
669
774
  segments.push({ text: `+${addedElements}`, color: 'green' });
670
- segments.push({ text: ' elements', colored: false });
775
+ segments.push({
776
+ text: ` ${addedElements === 1 ? 'element' : 'elements'}`,
777
+ colored: false,
778
+ });
671
779
  }
672
780
  if (addedNodes > 0) {
673
781
  if (addedElements > 0) segments.push({ text: ', ', colored: false });
674
782
  segments.push({ text: `+${addedNodes}`, color: 'slate' });
675
- segments.push({ text: ' nodes', colored: false });
783
+ segments.push({ text: ` ${addedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
676
784
  }
677
785
 
678
786
  // Removed
679
787
  if (removedElements > 0) {
680
788
  if (addedElements > 0 || addedNodes > 0) segments.push({ text: ', ', colored: false });
681
789
  segments.push({ text: `-${removedElements}`, color: 'red' });
682
- segments.push({ text: ' elements', colored: false });
790
+ segments.push({
791
+ text: ` ${removedElements === 1 ? 'element' : 'elements'}`,
792
+ colored: false,
793
+ });
683
794
  }
684
795
  if (removedNodes > 0) {
685
796
  if (addedElements > 0 || addedNodes > 0 || removedElements > 0)
686
797
  segments.push({ text: ', ', colored: false });
687
798
  segments.push({ text: `-${removedNodes}`, color: 'slate' });
688
- segments.push({ text: ' nodes', colored: false });
799
+ segments.push({ text: ` ${removedNodes === 1 ? 'node' : 'nodes'}`, colored: false });
689
800
  }
690
801
 
691
802
  // Pass element reference if exactly one element was mutated (not counting nodes)
@@ -716,9 +827,9 @@ const main = (s, config = {}, stringSelector = '') => {
716
827
 
717
828
  const parseSegments = [
718
829
  { text: `${elementsOnly}`, colored: true },
719
- { text: ' elements (', colored: false },
830
+ { text: ` ${elementsOnly === 1 ? 'element' : 'elements'} (`, colored: false },
720
831
  { text: `${totalNodes}`, color: 'slate' },
721
- { text: ' total nodes)', colored: false },
832
+ { text: ` total ${totalNodes === 1 ? 'node' : 'nodes'})`, colored: false },
722
833
  ];
723
834
 
724
835
  if (totalSkipped > 0) {
@@ -736,7 +847,10 @@ const main = (s, config = {}, stringSelector = '') => {
736
847
  debugLog(
737
848
  PHASE_HYDRATE,
738
849
  [
739
- { text: `${hydratedCount} bindings (`, colored: false },
850
+ {
851
+ text: `${hydratedCount} ${hydratedCount === 1 ? 'binding' : 'bindings'} (`,
852
+ colored: false,
853
+ },
740
854
  { text: '@[...]', color: 'pink' },
741
855
  { text: ')', colored: false },
742
856
  ],
@@ -749,7 +863,10 @@ const main = (s, config = {}, stringSelector = '') => {
749
863
  debugLog(
750
864
  PHASE_ITERATE,
751
865
  [
752
- { text: `${iteratedCount} iterations (`, colored: false },
866
+ {
867
+ text: `${iteratedCount} ${iteratedCount === 1 ? 'iteration' : 'iterations'} (`,
868
+ colored: false,
869
+ },
753
870
  { text: '<!-- each -->', color: 'commentGreen' },
754
871
  { text: ')', colored: false },
755
872
  ],
@@ -762,7 +879,10 @@ const main = (s, config = {}, stringSelector = '') => {
762
879
  debugLog(
763
880
  PHASE_CONDITION,
764
881
  [
765
- { text: `${evaluatedCount} conditionals (`, colored: false },
882
+ {
883
+ text: `${evaluatedCount} ${evaluatedCount === 1 ? 'conditional' : 'conditionals'} (`,
884
+ colored: false,
885
+ },
766
886
  { text: '<!-- if -->', color: 'commentGreen' },
767
887
  { text: ')', colored: false },
768
888
  ],
@@ -856,9 +976,11 @@ const main = (s, config = {}, stringSelector = '') => {
856
976
  componentConfig,
857
977
  );
858
978
 
859
- // Export hyperspeed manifest globally for compiler extraction
979
+ // Export hyperspeed manifest globally for compiler extraction and optimizations
980
+ // Use pre-compiled manifest if available (has compiledBatchFn), otherwise runtime-generated
860
981
  if (typeof window !== 'undefined') {
861
- window.__vibeManifest = hyperspeedManifestData;
982
+ const manifestToExport = hyperspeedTree || hyperspeedManifestData;
983
+ window.__vibeManifest = manifestToExport;
862
984
  }
863
985
 
864
986
  return $;
@@ -9,6 +9,9 @@ import { resolveThisPath } from './utils.js';
9
9
  // This is a preview of what Vibe Compiled (Phase 2) will do automatically
10
10
  import * as fastPath from './_vibe-compiled-iteration-batch.js';
11
11
 
12
+ // Pre-compiled iteration optimization (production)
13
+ import * as compiled from './pre-compiled-iterations.js';
14
+
12
15
  /**
13
16
  * Find a comment node with matching text content in the given nodes.
14
17
  */
@@ -257,7 +260,9 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
257
260
  iterationNode.meta;
258
261
 
259
262
  // Already rendered - updates go through updateIteration
260
- if (iterationNode.runtime.instances?.length > 0) return;
263
+ if (iterationNode.runtime.instances?.length > 0) {
264
+ return;
265
+ }
261
266
 
262
267
  // Check if this iteration has already been rendered
263
268
  // We use a marker on the startComment node itself (survives re-parsing)
@@ -288,6 +293,18 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
288
293
  return;
289
294
  }
290
295
 
296
+ // Compiled path: Check for pre-compiled batch function in manifest
297
+ if (compiled.canUseCompiled(iterationNode)) {
298
+ const compiledMeta = compiled.getCompiledMeta(iterationNode);
299
+ if (compiled.renderCompiled(iterationNode, array, state, compiledMeta, parent, endComment)) {
300
+ // Mark as rendered
301
+ startComment.__vibeRendered = true;
302
+ startComment.__vibeIterationRuntime = iterationNode.runtime;
303
+ return;
304
+ }
305
+ // Fall through to runtime path if compiled failed
306
+ }
307
+
291
308
  // Fast path: opt-in via window.__VIBE_FAST_ITERATION__ (preview of Vibe Compiled)
292
309
  if (window.__VIBE_FAST_ITERATION__ && fastPath.canUseFastPath(template)) {
293
310
  fastPath.renderFast(iterationNode, array, state, parent, endComment);
@@ -341,12 +358,17 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
341
358
  const oldArray = resolvePath(oldState, resolvedArrayPath) || [];
342
359
  const newArray = resolvePath(newState, resolvedArrayPath) || [];
343
360
 
361
+ // Compiled path: Use pre-compiled batch function when available
362
+ if (compiled.canUseCompiled(iterationNode)) {
363
+ const compiledMeta = compiled.getCompiledMeta(iterationNode);
364
+ if (compiled.updateCompiled(iterationNode, newArray, newState, compiledMeta, startComment, endComment)) {
365
+ return;
366
+ }
367
+ // Fall through to runtime path if compiled failed
368
+ }
369
+
344
370
  // Fast path: opt-in via window.__VIBE_FAST_ITERATION__ (preview of Vibe Compiled)
345
371
  // Use for bulk operations (large arrays or empty→full transitions)
346
- const isEmptyToFull = oldArray.length === 0 && newArray.length > 0;
347
- const isFullToEmpty = oldArray.length > 0 && newArray.length === 0;
348
- const isLargeArray = newArray.length > 100 || oldArray.length > 100;
349
-
350
372
  if (
351
373
  window.__VIBE_FAST_ITERATION__ &&
352
374
  fastPath.canUseFastPath(template) &&
package/runtime/parse.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  ITERATION_REGEX,
6
6
  CONDITIONAL_REGEX,
7
7
  DOM_ELEMENT_PROPERTIES,
8
+ DEHYDRATE_CLASS_OR_ATTR,
8
9
  } from './constants.js';
9
10
 
10
11
  const parseHTML = (children, rootKey = undefined) =>
@@ -31,7 +32,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
31
32
 
32
33
  // Skip non-reactive elements and dehydrated elements
33
34
  if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
34
- if (element.hasAttribute?.('dehydrate')) {
35
+ if (element.hasAttribute?.(DEHYDRATE_CLASS_OR_ATTR) || element.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)) {
35
36
  stats.skipped++;
36
37
  continue;
37
38
  }