@adaas/are-html 0.0.22 → 0.0.23

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 (129) hide show
  1. package/dist/browser/index.d.mts +176 -8
  2. package/dist/browser/index.mjs +661 -235
  3. package/dist/browser/index.mjs.map +1 -1
  4. package/dist/node/{AreBinding.attribute-doUvtOjc.d.mts → AreBinding.attribute-BWzEIw6H.d.mts} +45 -0
  5. package/dist/node/{AreBinding.attribute-Bm5LlOyE.d.ts → AreBinding.attribute-GpT-5Qmf.d.ts} +45 -0
  6. package/dist/node/attributes/AreBinding.attribute.d.mts +1 -1
  7. package/dist/node/attributes/AreBinding.attribute.d.ts +1 -1
  8. package/dist/node/attributes/AreDirective.attribute.d.mts +1 -1
  9. package/dist/node/attributes/AreDirective.attribute.d.ts +1 -1
  10. package/dist/node/attributes/AreEvent.attribute.d.mts +1 -1
  11. package/dist/node/attributes/AreEvent.attribute.d.ts +1 -1
  12. package/dist/node/attributes/AreStatic.attribute.d.mts +1 -1
  13. package/dist/node/attributes/AreStatic.attribute.d.ts +1 -1
  14. package/dist/node/directives/AreDirectiveFor.directive.d.mts +18 -1
  15. package/dist/node/directives/AreDirectiveFor.directive.d.ts +18 -1
  16. package/dist/node/directives/AreDirectiveFor.directive.js +57 -9
  17. package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
  18. package/dist/node/directives/AreDirectiveFor.directive.mjs +57 -9
  19. package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
  20. package/dist/node/directives/AreDirectiveIf.directive.d.mts +1 -1
  21. package/dist/node/directives/AreDirectiveIf.directive.d.ts +1 -1
  22. package/dist/node/directives/AreDirectiveShow.directive.d.mts +1 -1
  23. package/dist/node/directives/AreDirectiveShow.directive.d.ts +1 -1
  24. package/dist/node/engine/AreHTML.compiler.d.mts +1 -1
  25. package/dist/node/engine/AreHTML.compiler.d.ts +1 -1
  26. package/dist/node/engine/AreHTML.compiler.js +4 -0
  27. package/dist/node/engine/AreHTML.compiler.js.map +1 -1
  28. package/dist/node/engine/AreHTML.compiler.mjs +4 -0
  29. package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
  30. package/dist/node/engine/AreHTML.constants.d.mts +33 -1
  31. package/dist/node/engine/AreHTML.constants.d.ts +33 -1
  32. package/dist/node/engine/AreHTML.constants.js +166 -0
  33. package/dist/node/engine/AreHTML.constants.js.map +1 -1
  34. package/dist/node/engine/AreHTML.constants.mjs +165 -1
  35. package/dist/node/engine/AreHTML.constants.mjs.map +1 -1
  36. package/dist/node/engine/AreHTML.context.d.mts +66 -0
  37. package/dist/node/engine/AreHTML.context.d.ts +66 -0
  38. package/dist/node/engine/AreHTML.context.js +98 -0
  39. package/dist/node/engine/AreHTML.context.js.map +1 -1
  40. package/dist/node/engine/AreHTML.context.mjs +98 -0
  41. package/dist/node/engine/AreHTML.context.mjs.map +1 -1
  42. package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
  43. package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
  44. package/dist/node/engine/AreHTML.interpreter.js +66 -10
  45. package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
  46. package/dist/node/engine/AreHTML.interpreter.mjs +66 -10
  47. package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
  48. package/dist/node/engine/AreHTML.lifecycle.d.mts +1 -8
  49. package/dist/node/engine/AreHTML.lifecycle.d.ts +1 -8
  50. package/dist/node/engine/AreHTML.lifecycle.js +29 -44
  51. package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
  52. package/dist/node/engine/AreHTML.lifecycle.mjs +29 -44
  53. package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
  54. package/dist/node/engine/AreHTML.tokenizer.d.mts +1 -1
  55. package/dist/node/engine/AreHTML.tokenizer.d.ts +1 -1
  56. package/dist/node/engine/AreHTML.tokenizer.js +7 -1
  57. package/dist/node/engine/AreHTML.tokenizer.js.map +1 -1
  58. package/dist/node/engine/AreHTML.tokenizer.mjs +7 -1
  59. package/dist/node/engine/AreHTML.tokenizer.mjs.map +1 -1
  60. package/dist/node/engine/AreHTML.transformer.d.mts +1 -1
  61. package/dist/node/engine/AreHTML.transformer.d.ts +1 -1
  62. package/dist/node/index.d.mts +4 -3
  63. package/dist/node/index.d.ts +4 -3
  64. package/dist/node/index.js +7 -0
  65. package/dist/node/index.mjs +1 -0
  66. package/dist/node/instructions/AddStaticHTML.instruction.d.mts +8 -0
  67. package/dist/node/instructions/AddStaticHTML.instruction.d.ts +8 -0
  68. package/dist/node/instructions/AddStaticHTML.instruction.js +31 -0
  69. package/dist/node/instructions/AddStaticHTML.instruction.js.map +1 -0
  70. package/dist/node/instructions/AddStaticHTML.instruction.mjs +24 -0
  71. package/dist/node/instructions/AddStaticHTML.instruction.mjs.map +1 -0
  72. package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
  73. package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
  74. package/dist/node/instructions/AreHTML.instructions.constants.js +1 -0
  75. package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
  76. package/dist/node/instructions/AreHTML.instructions.constants.mjs +1 -0
  77. package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
  78. package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
  79. package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
  80. package/dist/node/lib/AreDirective/AreDirective.component.d.mts +1 -1
  81. package/dist/node/lib/AreDirective/AreDirective.component.d.ts +1 -1
  82. package/dist/node/lib/AreDirective/AreDirective.types.d.mts +1 -1
  83. package/dist/node/lib/AreDirective/AreDirective.types.d.ts +1 -1
  84. package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.mts +1 -1
  85. package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.ts +1 -1
  86. package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.mts +1 -1
  87. package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.ts +1 -1
  88. package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.mts +1 -1
  89. package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.ts +1 -1
  90. package/dist/node/lib/AreHTMLNode/AreHTMLNode.js +51 -0
  91. package/dist/node/lib/AreHTMLNode/AreHTMLNode.js.map +1 -1
  92. package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs +51 -0
  93. package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs.map +1 -1
  94. package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
  95. package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
  96. package/dist/node/nodes/AreComment.d.mts +1 -1
  97. package/dist/node/nodes/AreComment.d.ts +1 -1
  98. package/dist/node/nodes/AreComponent.d.mts +1 -1
  99. package/dist/node/nodes/AreComponent.d.ts +1 -1
  100. package/dist/node/nodes/AreInterpolation.d.mts +1 -1
  101. package/dist/node/nodes/AreInterpolation.d.ts +1 -1
  102. package/dist/node/nodes/AreRoot.d.mts +1 -1
  103. package/dist/node/nodes/AreRoot.d.ts +1 -1
  104. package/dist/node/nodes/AreText.d.mts +1 -1
  105. package/dist/node/nodes/AreText.d.ts +1 -1
  106. package/examples/dashboard/concept.ts +1 -1
  107. package/examples/dashboard/dist/index.html +1 -1
  108. package/examples/dashboard/dist/{mqh9ryml-xat335.js → mqiw5sqa-ypckmj.js} +403 -57
  109. package/examples/for-perf/dist/index.html +1 -1
  110. package/examples/for-perf/dist/{mqh9ryfo-6a8d0o.js → mqj1mpf2-z4aokv.js} +558 -117
  111. package/examples/for-perf/dist/{mqh9ryfq-4pf5cv.js → mqj1mpff-4fr7mw.js} +558 -117
  112. package/examples/signal-routing/dist/index.html +1 -1
  113. package/examples/signal-routing/dist/{mqh9ryc9-dkcbkx.js → mqiwo23h-bhcolu.js} +413 -60
  114. package/package.json +5 -5
  115. package/src/directives/AreDirectiveFor.directive.ts +99 -16
  116. package/src/engine/AreHTML.compiler.ts +13 -0
  117. package/src/engine/AreHTML.constants.ts +142 -0
  118. package/src/engine/AreHTML.context.ts +112 -0
  119. package/src/engine/AreHTML.interpreter.ts +114 -13
  120. package/src/engine/AreHTML.lifecycle.ts +81 -74
  121. package/src/engine/AreHTML.tokenizer.ts +30 -1
  122. package/src/index.ts +1 -0
  123. package/src/instructions/AddStaticHTML.instruction.ts +23 -0
  124. package/src/instructions/AreHTML.instructions.constants.ts +1 -0
  125. package/src/instructions/AreHTML.instructions.types.ts +9 -0
  126. package/src/lib/AreHTMLNode/AreHTMLNode.ts +74 -0
  127. package/src/lib/AreRoot/AreRoot.component.ts +3 -3
  128. package/tests/StaticIsland.test.ts +115 -0
  129. package/examples/for-perf/dist/mqh9ryde-m243t8.js +0 -15223
@@ -15,6 +15,7 @@ import { AddElementInstruction } from "@adaas/are-html/instructions/AddElement.i
15
15
  import { AddListenerInstruction } from "@adaas/are-html/instructions/AddListener.instruction";
16
16
  import { AddTextInstruction } from "@adaas/are-html/instructions/AddText.instruction";
17
17
  import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
18
+ import { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
18
19
  import { HideElementInstruction } from "@adaas/are-html/instructions/HideElement.instruction";
19
20
  import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
20
21
  import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
@@ -84,16 +85,25 @@ export class AreHTMLInterpreter extends AreInterpreter {
84
85
  ? context.container.createElementNS(SVG_NAMESPACE, tag)
85
86
  : context.container.createElement(tag);
86
87
 
87
- if (mountPoint.nodeType === Node.ELEMENT_NODE) {
88
+ // Index the element synchronously so descendants can resolve this
89
+ // node as their mount point during the same (still off-document) pass.
90
+ context.setInstructionElement(declaration, element);
91
+
92
+ const attach = mountPoint.nodeType === Node.ELEMENT_NODE
88
93
  // parent is a real element — just append
89
- mountPoint.appendChild(element);
90
- } else {
94
+ ? () => mountPoint.appendChild(element)
91
95
  // parent is an anchor (comment/text node) — insert before it
92
96
  // so content always appears before the anchor marker
93
- mountPoint.parentNode?.insertBefore(element, mountPoint);
94
- }
97
+ : () => { mountPoint.parentNode?.insertBefore(element, mountPoint); };
95
98
 
96
- context.setInstructionElement(declaration, element);
99
+ // When batching, only the mutation that touches the *live* document
100
+ // needs deferring; appends into an already-detached subtree are free
101
+ // and run immediately so the offline tree keeps taking shape.
102
+ if (context.isBatching && mountPoint.isConnected) {
103
+ context.deferAttach(attach);
104
+ } else {
105
+ attach();
106
+ }
97
107
 
98
108
  } else {
99
109
  const mountPoint = context.container.getElementById(node.id);
@@ -108,9 +118,17 @@ export class AreHTMLInterpreter extends AreInterpreter {
108
118
  ? context.container.createElementNS(SVG_NAMESPACE, tag)
109
119
  : context.container.createElement(tag);
110
120
 
111
- mountPoint.parentNode?.replaceChild(element, mountPoint);
112
-
113
121
  context.setInstructionElement(declaration, element);
122
+
123
+ const attach = () => { mountPoint.parentNode?.replaceChild(element, mountPoint); };
124
+
125
+ // The placeholder lives in the live DOM — defer the swap so the
126
+ // whole subtree built into `element` lands in one mutation.
127
+ if (context.isBatching && mountPoint.isConnected) {
128
+ context.deferAttach(attach);
129
+ } else {
130
+ attach();
131
+ }
114
132
  }
115
133
 
116
134
  // Register the element in the context index
@@ -135,7 +153,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
135
153
  ) {
136
154
  const element = context.getElementByInstruction(declaration);
137
155
 
138
- if (element && element.parentNode) {
156
+ // Skip the live-DOM detach when the element is already off-document: an
157
+ // ancestor removed earlier in the same unmount took this whole subtree
158
+ // with it, and a detached subtree is garbage-collected as a unit. The
159
+ // per-node removeChild would be wasted work. The index bookkeeping below
160
+ // must still run to drop the strong references that would pin it in memory.
161
+ if (element && element.parentNode && element.isConnected) {
139
162
  element.parentNode.removeChild(element);
140
163
  }
141
164
 
@@ -283,7 +306,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
283
306
 
284
307
  const { name } = mutation.payload;
285
308
 
286
- if (name && element.nodeType === Node.ELEMENT_NODE) {
309
+ // No point stripping attributes off an element that is already
310
+ // detached from the live document (it is about to be discarded with
311
+ // its whole subtree). Only touch attributes while the element is live.
312
+ if (name && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
287
313
  const colonIdx = name.indexOf(':');
288
314
  if (colonIdx > 0) {
289
315
  const ns = SVG_ATTRIBUTE_NS[name.slice(0, colonIdx)];
@@ -343,6 +369,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
343
369
 
344
370
  if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
345
371
 
372
+ // A detached element is being discarded with its subtree — restoring its
373
+ // inline display would be pointless work. Only restore while it is live.
374
+ if (!element.isConnected) return;
375
+
346
376
  const el = element as HTMLElement;
347
377
 
348
378
  // Restore the cached inline display. An explicit payload display, when
@@ -510,7 +540,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
510
540
  const listener = (mutation.payload as any)._callback as EventListenerOrEventListenerObject | undefined;
511
541
 
512
542
  if (listener) {
513
- element.removeEventListener(eventName, listener);
543
+ // Detaching a listener from an element that is already off-document is
544
+ // unnecessary — the element and its listeners are collected together.
545
+ // Still clear the context bookkeeping so no strong reference lingers.
546
+ if (element.isConnected) {
547
+ element.removeEventListener(eventName, listener);
548
+ }
514
549
  context.removeListener(element, name, listener);
515
550
  (mutation.payload as any)._callback = undefined;
516
551
  }
@@ -592,11 +627,73 @@ export class AreHTMLInterpreter extends AreInterpreter {
592
627
 
593
628
  if (!element) return;
594
629
 
595
- element.parentNode?.removeChild(element);
630
+ // Off-document text nodes vanish with their detached parent subtree, so
631
+ // skip the removeChild and just release the index reference.
632
+ if (element.isConnected) {
633
+ element.parentNode?.removeChild(element);
634
+ }
596
635
  context.removeInstructionElement(declaration);
597
636
  }
598
637
 
599
638
 
639
+ // ─────────────────────────────────────────────────────────────────────────────
640
+ // ── AddStaticHTML — Apply / Update / Revert ──────────────────────────────────
641
+ // ─────────────────────────────────────────────────────────────────────────────
642
+ // Materialises a fully static subtree (a "static island") onto its host
643
+ // element in a single pass. The tokenizer captured the inner markup verbatim
644
+ // instead of exploding it into per-element/per-text AreNodes, so here we hand
645
+ // the markup to the browser parser ONCE (via a cached <template>) and clone
646
+ // the pre-parsed fragment in. This collapses N DOM mutations into one and
647
+ // decodes HTML entities (e.g. &nbsp;) natively.
648
+ @A_Frame.Define({
649
+ description: 'Inject a static island\'s inner markup onto its host element in one pass via a cached, browser-parsed <template> clone. Decodes HTML entities natively.'
650
+ })
651
+ @AreInterpreter.Apply(AreHTMLInstructions.AddStaticHTML)
652
+ @AreInterpreter.Update(AreHTMLInstructions.AddStaticHTML)
653
+ addStaticHTML(
654
+ @A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
655
+ @A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
656
+ @A_Inject(A_Logger) logger?: A_Logger,
657
+ ) {
658
+ const element = context.getElementByInstruction(mutation.parent!);
659
+
660
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
661
+ throw new AreInterpreterError({
662
+ title: 'Element Not Found',
663
+ description: `Could not find a DOM element associated with the instruction ASEID "${mutation.parent}". Ensure the host element is rendered before materialising its static island.`
664
+ });
665
+ }
666
+
667
+ const el = element as HTMLElement;
668
+ const { html } = mutation.payload;
669
+
670
+ // Clear any previously injected content (Update path) before re-injecting.
671
+ el.textContent = '';
672
+
673
+ // Parse once (in the host tag's context) and clone the cached fragment.
674
+ const fragment = context.getStaticFragment(el.tagName.toLowerCase(), html);
675
+ el.appendChild(fragment.cloneNode(true));
676
+
677
+ logger?.debug('green', `Static island materialised onto <${(mutation.owner.parent ?? mutation.owner)?.aseid?.toString?.()}>`);
678
+ }
679
+
680
+
681
+ @A_Frame.Define({
682
+ description: 'Clear a static island\'s injected markup from its host element on revert.'
683
+ })
684
+ @AreInterpreter.Revert(AreHTMLInstructions.AddStaticHTML)
685
+ removeStaticHTML(
686
+ @A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
687
+ @A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
688
+ ) {
689
+ const element = context.getElementByInstruction(mutation.parent!);
690
+
691
+ if (element && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
692
+ (element as HTMLElement).textContent = '';
693
+ }
694
+ }
695
+
696
+
600
697
 
601
698
  @A_Frame.Define({
602
699
  description: 'Add a comment node to the DOM based on the provided declaration instruction.'
@@ -667,7 +764,11 @@ export class AreHTMLInterpreter extends AreInterpreter {
667
764
 
668
765
  if (!element) return;
669
766
 
670
- element.parentNode?.removeChild(element);
767
+ // Off-document comment anchors are discarded with their detached parent
768
+ // subtree; skip the removeChild and just release the index reference.
769
+ if (element.isConnected) {
770
+ element.parentNode?.removeChild(element);
771
+ }
671
772
  context.removeInstructionElement(declaration);
672
773
  }
673
774
 
@@ -10,7 +10,6 @@ import { AreDirectiveFeatures } from "@adaas/are-html/directive/AreDirective.con
10
10
  import { AreHTMLEngineContext } from "./AreHTML.context";
11
11
  import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
12
12
  import { A_Frame } from "@adaas/a-frame/core";
13
- import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
14
13
 
15
14
 
16
15
  @A_Frame.Define({
@@ -19,14 +18,6 @@ import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper"
19
18
  })
20
19
  export class AreHTMLLifecycle extends AreLifecycle {
21
20
 
22
- /**
23
- * Per-chunk time budget (ms) for the time-sliced initial mount walk. While
24
- * mounting a large subtree we keep applying nodes until this much wall-clock
25
- * time has elapsed, then yield to the browser so it can paint and process
26
- * input before the next chunk. ~16ms targets a single animation frame.
27
- */
28
- private static readonly MOUNT_BUDGET_MS = 16;
29
-
30
21
  @AreLifecycle.Init(AreComponentNode)
31
22
  initComponent(
32
23
  @A_Inject(A_Caller) node: AreHTMLNode,
@@ -112,83 +103,99 @@ export class AreHTMLLifecycle extends AreLifecycle {
112
103
  if (scene.isInactive) return;
113
104
 
114
105
  /**
115
- * 1. Render the root of this mount itself.
116
- */
117
- node.interpret();
118
-
119
- /**
120
- * 2. Walk the descendant subtree iteratively with an explicit enter/exit
121
- * stack so we can TIME-SLICE the work. The previous implementation
122
- * recursed via `child.mount()`, which fires onBeforeMount → onMount
123
- * (interpret + recurse) → onAfterMount per node and runs the whole tree
124
- * in one synchronous, un-yielding block. For large initial trees that
125
- * froze the main thread on first page load.
106
+ * Open a batching scope for the whole (synchronous) mount pass. Every
107
+ * element created below is built into a *detached* root and attached to
108
+ * the live DOM only when the batch flushes — turning O(nodes) reflows into
109
+ * a single one per mount root. `try/finally` guarantees the batch closes
110
+ * (and the depth counter stays balanced) even if interpretation throws.
126
111
  *
127
- * We replicate the exact per-node hook ordering:
128
- * - enter → onBeforeMount, then (if active) interpret + queue children
129
- * - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
130
- * post-order, matching the recursive `node.mount()` contract)
131
- *
132
- * Small trees complete entirely within a single time budget and the
133
- * handler returns `void` synchronously — a true fast-path with NO
134
- * behavioural change for typical UIs. Only genuinely large trees exceed
135
- * the budget, at which point we yield a macrotask (letting the browser
136
- * paint / stay responsive) and resume the remaining work, returning a
137
- * Promise that resolves when the whole subtree is mounted.
112
+ * The context is resolved from the node's scope (the scope the onMount
113
+ * feature runs in) so the override keeps the base `mount` signature.
138
114
  */
139
- interface MountFrame { node: AreHTMLNode; entered: boolean; }
140
-
141
- const stack: MountFrame[] = [];
142
- for (let i = node.children.length - 1; i >= 0; i--) {
143
- stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
144
- }
115
+ const context = node.scope.resolve<AreHTMLEngineContext>(AreHTMLEngineContext);
145
116
 
146
- const step = (): void => {
147
- const frame = stack[stack.length - 1];
148
- const current = frame.node;
117
+ context?.beginBatch();
149
118
 
150
- if (frame.entered) {
151
- // Post-order exit: the whole subtree below `current` is mounted.
152
- stack.pop();
153
- current.call(AreNodeFeatures.onAfterMount, current.scope);
154
- return;
119
+ /**
120
+ * `onAfterMount` must observe the node already connected to the live
121
+ * document (consumer components may measure layout / focus there). Since
122
+ * the subtree is built off-document and attached only when the batch
123
+ * flushes, we collect the post-order `onAfterMount` targets here and fire
124
+ * them once the flush has connected everything.
125
+ */
126
+ const afterMountQueue: AreHTMLNode[] = [];
127
+
128
+ try {
129
+ /**
130
+ * 1. Render the root of this mount itself.
131
+ */
132
+ node.interpret();
133
+
134
+ /**
135
+ * 2. Walk the descendant subtree iteratively with an explicit enter/exit
136
+ * stack. We keep the iterative (non-recursive) shape — it is cheaper
137
+ * than deep recursion and gives us a single, clear place that owns the
138
+ * per-node hook ordering:
139
+ * - enter → onBeforeMount, then (if active) interpret + queue children
140
+ * - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
141
+ * post-order, matching the recursive `node.mount()` contract)
142
+ *
143
+ * [!] The initial mount is intentionally ATOMIC (fully synchronous). It
144
+ * does NOT time-slice / yield. Yielding mid-walk exposes a partially
145
+ * mounted tree to the event loop: any update dispatched during a gap
146
+ * (signal-driven re-render, async data load, etc.) interprets a node
147
+ * whose parent has not been mounted yet — producing
148
+ * `mount-point-not-found` and out-of-order DOM. The whole page must
149
+ * therefore appear in the DOM in one uninterrupted pass. Heavy lists
150
+ * are sliced at the source instead (see AreDirectiveFor), where the
151
+ * batching is reentrancy-safe.
152
+ */
153
+ interface MountFrame { node: AreHTMLNode; entered: boolean; }
154
+
155
+ const stack: MountFrame[] = [];
156
+ for (let i = node.children.length - 1; i >= 0; i--) {
157
+ stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
155
158
  }
156
159
 
157
- frame.entered = true;
160
+ while (stack.length > 0) {
161
+ const frame = stack[stack.length - 1];
162
+ const current = frame.node;
163
+
164
+ if (frame.entered) {
165
+ // Post-order exit: the whole subtree below `current` is mounted.
166
+ // Defer the onAfterMount hook until after the batch flush so the
167
+ // node is connected to the live DOM when it runs.
168
+ stack.pop();
169
+ afterMountQueue.push(current);
170
+ continue;
171
+ }
158
172
 
159
- // onBeforeMount always fires (even for inactive nodes), matching the
160
- // recursive AreNode.mount() semantics.
161
- current.call(AreNodeFeatures.onBeforeMount, current.scope);
173
+ frame.entered = true;
162
174
 
163
- if (!current.scene.isInactive) {
164
- current.interpret();
165
- // Push children in reverse so they pop in document order.
166
- for (let i = current.children.length - 1; i >= 0; i--) {
167
- stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
168
- }
169
- }
170
- };
175
+ // onBeforeMount always fires (even for inactive nodes), matching the
176
+ // recursive AreNode.mount() semantics.
177
+ current.call(AreNodeFeatures.onBeforeMount, current.scope);
171
178
 
172
- const drive = (): void | Promise<void> => {
173
- const start = AreSchedulerHelper.now();
174
- while (stack.length > 0) {
175
- step();
176
- if (stack.length > 0 && AreSchedulerHelper.now() - start >= AreHTMLLifecycle.MOUNT_BUDGET_MS) {
177
- // Budget exhausted with work remaining — yield, then resume.
178
- return new Promise<void>((resolve, reject) => {
179
- AreSchedulerHelper.scheduleMacrotask(() => {
180
- try {
181
- resolve(drive());
182
- } catch (error) {
183
- reject(error);
184
- }
185
- });
186
- });
179
+ if (!current.scene.isInactive) {
180
+ current.interpret();
181
+ // Push children in reverse so they pop in document order.
182
+ for (let i = current.children.length - 1; i >= 0; i--) {
183
+ stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
184
+ }
187
185
  }
188
186
  }
189
- };
187
+ } finally {
188
+ // Flush the deferred attachments — the fully built subtree lands in the
189
+ // live DOM in a single pass.
190
+ context?.endBatch();
191
+ }
190
192
 
191
- return drive();
193
+ // The subtree is now connected; fire onAfterMount in the original
194
+ // post-order, each with its node live in the document.
195
+ for (let i = 0; i < afterMountQueue.length; i++) {
196
+ const mounted = afterMountQueue[i];
197
+ mounted.call(AreNodeFeatures.onAfterMount, mounted.scope);
198
+ }
192
199
  }
193
200
 
194
201
 
@@ -8,6 +8,8 @@ import { AreEventAttribute } from "@adaas/are-html/attributes/AreEvent.attribute
8
8
  import { AreBindingAttribute } from "@adaas/are-html/attributes/AreBinding.attribute";
9
9
  import { AreStaticAttribute } from "@adaas/are-html/attributes/AreStatic.attribute";
10
10
  import { AreHTMLAttribute } from "../lib/AreHTMLAttribute/AreHTML.attribute";
11
+ import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
12
+ import { isStaticMarkup } from "./AreHTML.constants";
11
13
  import { A_Frame } from "@adaas/a-frame/core";
12
14
 
13
15
 
@@ -30,7 +32,34 @@ export class AreHTMLTokenizer extends AreTokenizer {
30
32
  @A_Inject(A_Logger) logger?: A_Logger
31
33
  ): void {
32
34
 
33
- super.tokenize(node, context, logger);
35
+ /**
36
+ * Static-island fast path.
37
+ *
38
+ * When a node's entire inner subtree is fully static — no `{{ }}`
39
+ * interpolations, no dynamic (`$`/`:`/`@`) attributes and only standard
40
+ * HTML tags — we skip exploding it into one child AreNode per element /
41
+ * text run and instead capture the inner markup verbatim. The interpreter
42
+ * later materialises it in a single pass (browser-parsed innerHTML /
43
+ * cached `<template>` clone), which:
44
+ * - eliminates N node + scope + scene + instruction allocations,
45
+ * - collapses N isolated DOM mutations into one, and
46
+ * - decodes HTML entities (e.g. `&nbsp;`) for free.
47
+ *
48
+ * Routing outlets (AreRootNode) are intentionally excluded — they own
49
+ * their content dynamically. The node's OWN attributes are still
50
+ * extracted below, so a dynamic attribute on the island root itself
51
+ * (e.g. `<div :class="x"> …static… </div>`) keeps working.
52
+ */
53
+ const isStaticIsland =
54
+ node instanceof AreComponentNode &&
55
+ !!node.content &&
56
+ isStaticMarkup(node.content);
57
+
58
+ if (isStaticIsland) {
59
+ (node as AreHTMLNode).markStatic(node.content);
60
+ } else {
61
+ super.tokenize(node, context, logger);
62
+ }
34
63
 
35
64
  context.startPerformance('attributeExtraction');
36
65
 
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export * from './instructions/AddAttribute.instruction';
21
21
  export * from './instructions/AddElement.instruction';
22
22
  export * from './instructions/AddInterpolation.instruction';
23
23
  export * from './instructions/AddListener.instruction';
24
+ export * from './instructions/AddStaticHTML.instruction';
24
25
  export * from './instructions/AddStyle.instruction';
25
26
  export * from './instructions/AddText.instruction';
26
27
  export * from './instructions/HideElement.instruction';
@@ -0,0 +1,23 @@
1
+ import { A_Frame } from "@adaas/a-frame/core"
2
+ import { AreDeclaration, AreMutation, AreInstructionSerialized } from "@adaas/are";
3
+ import { AreHtmlAddStaticHTMLInstructionPayload } from "./AreHTML.instructions.types";
4
+ import { AreHTMLInstructions } from "./AreHTML.instructions.constants";
5
+
6
+
7
+ @A_Frame.Define({
8
+ namespace: 'a-are-html',
9
+ description: 'Materialises a fully static subtree (a "static island") onto its parent element in a single pass via browser-parsed innerHTML / a cached <template> clone. Apply injects the markup; revert clears it. Decodes HTML entities (e.g. &nbsp;) for free.'
10
+ })
11
+ export class AddStaticHTMLInstruction extends AreMutation<AreHtmlAddStaticHTMLInstructionPayload> {
12
+
13
+ constructor(
14
+ parent: AreDeclaration,
15
+ props: AreHtmlAddStaticHTMLInstructionPayload | AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>
16
+ ) {
17
+ if ('aseid' in props) {
18
+ super(props as AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>);
19
+ } else {
20
+ super(AreHTMLInstructions.AddStaticHTML, parent, props);
21
+ }
22
+ }
23
+ }
@@ -8,5 +8,6 @@ export const AreHTMLInstructions = {
8
8
  AddListener: '_AreHTML_AddListener',
9
9
  AddInterpolation: '_AreHTML_AddInterpolation',
10
10
  AddComment: '_AreHTML_AddComment',
11
+ AddStaticHTML: '_AreHTML_AddStaticHTML',
11
12
  HideElement: '_AreHTML_HideElement',
12
13
  } as const
@@ -34,6 +34,15 @@ export type AreHtmlAddStyleInstructionPayload = {
34
34
  styles: string;
35
35
  }
36
36
 
37
+ export type AreHtmlAddStaticHTMLInstructionPayload = {
38
+ /**
39
+ * Verbatim inner markup of a static island, materialised on the parent
40
+ * element in a single pass (browser-parsed `innerHTML` / cached `<template>`
41
+ * clone). Decodes HTML entities (e.g. `&nbsp;`) for free via the parser.
42
+ */
43
+ html: string;
44
+ }
45
+
37
46
  export type AreHtmlHideInstructionPayload = {
38
47
  /**
39
48
  * Optional explicit display value to restore when the element becomes
@@ -16,6 +16,19 @@ import { AreDirectiveMeta } from "@adaas/are-html/directive/AreDirective.meta";
16
16
  description: 'AreHTMLNode represents a node in the HTML structure. It extends the base AreNode and includes properties and methods specific to HTML nodes, such as handling attributes, directives, events, and styles.'
17
17
  })
18
18
  export class AreHTMLNode extends AreNode {
19
+ /**
20
+ * When set, this node is a *static island* root: its entire inner subtree
21
+ * was detected (at tokenize time) to contain no ARE-reactive constructs —
22
+ * no interpolations, no dynamic attributes and only standard HTML tags.
23
+ *
24
+ * Instead of being exploded into one child AreNode per element/text node,
25
+ * the inner markup is preserved verbatim here and materialised in a single
26
+ * pass by the interpreter (browser-parsed `innerHTML` / cached `<template>`
27
+ * clone). The node's OWN attributes (including any dynamic `:`/`@`/`$` on
28
+ * the island root) still compile and stay reactive as usual.
29
+ */
30
+ protected _staticInnerHTML?: string;
31
+
19
32
  /**
20
33
  * Actual node type.
21
34
  * By default it's a tag name
@@ -23,6 +36,67 @@ export class AreHTMLNode extends AreNode {
23
36
  get tag(): string {
24
37
  return this.aseid.entity;
25
38
  }
39
+
40
+ /**
41
+ * The verbatim inner markup captured when this node was identified as a
42
+ * static island, or `undefined` for ordinary (per-node) nodes.
43
+ */
44
+ get staticInnerHTML(): string | undefined {
45
+ return this._staticInnerHTML;
46
+ }
47
+
48
+ /**
49
+ * Whether this node is a static-island root (see `_staticInnerHTML`).
50
+ */
51
+ get isStaticIsland(): boolean {
52
+ return this._staticInnerHTML !== undefined;
53
+ }
54
+
55
+ /**
56
+ * Marks this node as a static-island root, capturing the verbatim inner
57
+ * markup to be materialised in one shot by the interpreter. Called by the
58
+ * tokenizer when the node's inner content is detected to be fully static.
59
+ */
60
+ markStatic(innerHTML: string): void {
61
+ this._staticInnerHTML = innerHTML;
62
+ }
63
+
64
+ /**
65
+ * Deep-clone the node. Overridden to carry over the static-island marker
66
+ * (`_staticInnerHTML`), which lives on AreHTMLNode and is therefore NOT
67
+ * copied by the base AreNode.clone(). Without this, cloning a directive
68
+ * template ($if/$for) that wraps a static island (e.g. `<span $if>★</span>`)
69
+ * would drop the captured inner markup and render an empty element. The
70
+ * base clone() recurses via each child's polymorphic clone(), so nested
71
+ * island children are preserved automatically through this override.
72
+ */
73
+ clone<T extends AreNode = AreNode>(this: T): T {
74
+ const cloned = super.clone() as unknown as AreHTMLNode;
75
+ const self = this as unknown as AreHTMLNode;
76
+
77
+ if (self._staticInnerHTML !== undefined)
78
+ cloned.markStatic(self._staticInnerHTML);
79
+
80
+ return cloned as unknown as T;
81
+ }
82
+
83
+ /**
84
+ * Clone the node while transferring its existing scope to the clone (used by
85
+ * the $if/$for directives to turn the original node into a lightweight group
86
+ * container). Overridden for the same reason as `clone()`: the static-island
87
+ * marker must survive so a directive applied to an island root keeps its
88
+ * inner markup.
89
+ */
90
+ cloneWithScope<T extends AreNode = AreNode>(this: T): T {
91
+ const cloned = super.cloneWithScope() as unknown as AreHTMLNode;
92
+ const self = this as unknown as AreHTMLNode;
93
+
94
+ if (self._staticInnerHTML !== undefined)
95
+ cloned.markStatic(self._staticInnerHTML);
96
+
97
+ return cloned as unknown as T;
98
+ }
99
+
26
100
  /**
27
101
  * The static attributes defined for the node, which are typically used to represent static properties or characteristics of the node that do not change based on the context or state. These attributes are usually defined in the template and are not reactive.
28
102
  *
@@ -160,9 +160,9 @@ export class AreRoot extends Are {
160
160
  child.transform();
161
161
 
162
162
  child.compile();
163
- // The HTML engine time-slices large initial mounts; await so a heavy
164
- // routed component renders in yielding chunks instead of freezing the
165
- // main thread on first entry. Small subtrees resolve synchronously.
163
+ // The initial mount is atomic (synchronous): the routed subtree is
164
+ // rendered in one uninterrupted pass so no update can observe a
165
+ // partially mounted tree. The await is a harmless no-op here.
166
166
  await child.mount();
167
167
  }
168
168
  }