@ape-egg/vibe 1.3.1 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +97 -0
  3. package/ROADMAP.md +289 -0
  4. package/boot.js +45 -0
  5. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  6. package/compiler/src/Cargo.lock +719 -40
  7. package/compiler/src/Cargo.toml +11 -2
  8. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
  9. package/compiler/src/compiler/compile.rs +552 -175
  10. package/compiler/src/compiler/component_tagger.rs +234 -0
  11. package/compiler/src/compiler/iteration_optimizer.rs +351 -0
  12. package/compiler/src/compiler/js_analyzer.rs +572 -0
  13. package/compiler/src/compiler/manifest_builder.rs +251 -26
  14. package/compiler/src/compiler/mod.rs +5 -1
  15. package/compiler/src/compiler/state_extractor.rs +140 -25
  16. package/compiler/src/compiler/value_stamper.rs +579 -88
  17. package/compiler/src/compiler/watcher.rs +579 -0
  18. package/compiler/src/config.rs +51 -8
  19. package/compiler/src/main.rs +41 -28
  20. package/compiler/src/parser/html.rs +229 -118
  21. package/component.js +23 -11
  22. package/llms.txt +304 -0
  23. package/package.json +1 -17
  24. package/runtime/cleanup.js +4 -4
  25. package/runtime/component.js +98 -21
  26. package/runtime/conditionals.js +2 -2
  27. package/runtime/constants.js +2 -1
  28. package/runtime/index.js +152 -30
  29. package/runtime/iterate.js +27 -5
  30. package/runtime/parse.js +2 -1
  31. package/runtime/pre-compiled-iterations.js +153 -0
  32. package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +204 -132
  33. package/runtime/utils.js +2 -1
  34. package/test-results/.last-run.json +4 -0
  35. package/vibe.css +19 -0
  36. package/runtime/component-state.js +0 -63
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
  }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * pre-compiled-iterations.js
3
+ *
4
+ * Pre-compiled iteration rendering - production version of iteration optimization.
5
+ * Uses pre-compiled batch functions from manifest generated by the compiler.
6
+ *
7
+ * This is the production implementation that runs compiled code generated at build time.
8
+ * Based on the prototype in _vibe-compiled-iteration-batch.js
9
+ */
10
+
11
+ // Reusable template element for parsing compiled HTML
12
+ const parseTemplate = typeof document !== 'undefined' ? document.createElement('template') : null;
13
+
14
+ /**
15
+ * Check if template has nested iterations or conditionals
16
+ */
17
+ const hasNestedStructures = (template) => {
18
+ if (!template || !template.children) return false;
19
+ for (const key in template.children) {
20
+ const child = template.children[key];
21
+ if (!child) continue;
22
+ if (child.type === 'iteration' || child.type === 'conditional') return true;
23
+ if (hasNestedStructures(child)) return true;
24
+ }
25
+ return false;
26
+ };
27
+
28
+ /**
29
+ * Check if iteration node has compiled batch function
30
+ */
31
+ export const canUseCompiled = (iterationNode) => {
32
+ if (!iterationNode || !iterationNode.meta) {
33
+ return false;
34
+ }
35
+
36
+ // Check compiled data from manifest merge
37
+ const compiled = iterationNode.compiled;
38
+ if (!compiled || !compiled.iterations || !compiled.iterations.batchFn) {
39
+ return false;
40
+ }
41
+
42
+ // If we have a compiled batch function, we can use it even if the template
43
+ // has nested structures, because the compiler has already inlined them
44
+ // into the batch function
45
+ return true;
46
+ };
47
+
48
+ /**
49
+ * Get compiled function metadata from iteration node
50
+ */
51
+ export const getCompiledMeta = (iterationNode) => {
52
+ const compiled = iterationNode.compiled;
53
+ if (!compiled || !compiled.iterations) return null;
54
+
55
+ return {
56
+ batchFn: compiled.iterations.batchFn,
57
+ itemAlias: compiled.iterations.itemAlias,
58
+ indexAlias: compiled.iterations.indexAlias,
59
+ };
60
+ };
61
+
62
+ /**
63
+ * Render iteration using pre-compiled batch function from manifest
64
+ */
65
+ export const renderCompiled = (iterationNode, array, state, compiledMeta, parent, endComment) => {
66
+ // Create function from string if not cached
67
+ if (!iterationNode.runtime.compiledFn) {
68
+ try {
69
+ // compiledMeta.batchFn is a complete arrow function: (arr, $) => { ... }
70
+ // Wrap in a function that returns it, then call to get the actual function
71
+ iterationNode.runtime.compiledFn = new Function('return ' + compiledMeta.batchFn)();
72
+ } catch (e) {
73
+ console.error('[compiled-iteration] Failed to create compiled function:', e);
74
+ return false; // Signal failure
75
+ }
76
+ }
77
+
78
+ // Build HTML using compiled function
79
+ const html = iterationNode.runtime.compiledFn(array, state);
80
+
81
+ // Parse and insert
82
+ if (parseTemplate) {
83
+ parseTemplate.innerHTML = html;
84
+ const frag = parseTemplate.content;
85
+ const kids = frag.children;
86
+
87
+ // Track instances - pre-allocate array for performance
88
+ const arrayLen = array.length;
89
+ const instances = new Array(arrayLen);
90
+ for (let i = 0; i < arrayLen; i++) {
91
+ instances[i] = { element: kids[i], item: array[i], index: i };
92
+ }
93
+
94
+ parent.insertBefore(frag, endComment);
95
+ iterationNode.runtime.instances = instances;
96
+ return true;
97
+ }
98
+
99
+ return false;
100
+ };
101
+
102
+ /**
103
+ * Update iteration using pre-compiled batch function (bulk rebuild)
104
+ */
105
+ export const updateCompiled = (iterationNode, newArray, state, compiledMeta, startComment, endComment) => {
106
+ const parent = startComment.parentNode;
107
+
108
+ // Clear existing instances
109
+ if (iterationNode.runtime.instances.length > 0) {
110
+ const range = document.createRange();
111
+ range.setStartAfter(startComment);
112
+ range.setEndBefore(endComment);
113
+ range.deleteContents();
114
+ }
115
+
116
+ if (newArray.length === 0) {
117
+ iterationNode.runtime.instances = [];
118
+ return true;
119
+ }
120
+
121
+ // Create function if not cached
122
+ if (!iterationNode.runtime.compiledFn) {
123
+ try {
124
+ iterationNode.runtime.compiledFn = new Function('return ' + compiledMeta.batchFn)();
125
+ } catch (e) {
126
+ console.error('Failed to create compiled function:', e);
127
+ return false;
128
+ }
129
+ }
130
+
131
+ // Build HTML using compiled function
132
+ const html = iterationNode.runtime.compiledFn(newArray, state);
133
+
134
+ // Parse and insert
135
+ if (parseTemplate) {
136
+ parseTemplate.innerHTML = html;
137
+ const frag = parseTemplate.content;
138
+ const kids = frag.children;
139
+
140
+ // Track instances - pre-allocate array for performance
141
+ const arrayLen = newArray.length;
142
+ const instances = new Array(arrayLen);
143
+ for (let i = 0; i < arrayLen; i++) {
144
+ instances[i] = { element: kids[i], item: newArray[i], index: i };
145
+ }
146
+
147
+ parent.insertBefore(frag, endComment);
148
+ iterationNode.runtime.instances = instances;
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ };