octane 0.2.12 → 0.2.13

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 (243) hide show
  1. package/README.md +5 -0
  2. package/dist/behavior-root.d.ts +13 -3
  3. package/dist/behavior-root.js +28 -5
  4. package/dist/cjs/behavior-root.cjs +32 -6
  5. package/dist/cjs/class-names.cjs +49 -0
  6. package/dist/cjs/constants.cjs +9 -0
  7. package/dist/cjs/css.cjs +6 -27
  8. package/dist/cjs/data-encoding.cjs +160 -0
  9. package/dist/cjs/dom-binding-handoff.cjs +93 -0
  10. package/dist/cjs/dom-binding-protocol.cjs +115 -0
  11. package/dist/cjs/dom-binding-styles.cjs +292 -0
  12. package/dist/cjs/dom-bindings.cjs +700 -0
  13. package/dist/cjs/dom-focused-move.cjs +66 -0
  14. package/dist/cjs/dom-tables.cjs +4 -94
  15. package/dist/cjs/error-codes.client.generated.cjs +33 -0
  16. package/dist/cjs/error-codes.server.generated.cjs +19 -0
  17. package/dist/cjs/hydration/control-capture.cjs +244 -0
  18. package/dist/cjs/hydration/event-capture.cjs +125 -43
  19. package/dist/cjs/hydration/interaction-config.cjs +50 -2
  20. package/dist/cjs/hydration-markers.cjs +37 -0
  21. package/dist/cjs/independent-hydration-protocol.cjs +106 -0
  22. package/dist/cjs/index.cjs +6 -0
  23. package/dist/cjs/runtime.cjs +2791 -215
  24. package/dist/cjs/runtime.server.cjs +716 -76
  25. package/dist/cjs/server/early-signals.cjs +45 -0
  26. package/dist/cjs/server/index.cjs +44 -0
  27. package/dist/cjs/server/rpc-batch.cjs +149 -0
  28. package/dist/cjs/server/rpc-stream.cjs +146 -0
  29. package/dist/cjs/server/rpc.cjs +34 -15
  30. package/dist/cjs/server/signal-query-observation.cjs +158 -0
  31. package/dist/cjs/server/streamed-region.cjs +52 -0
  32. package/dist/cjs/server/streamed-signals.cjs +512 -0
  33. package/dist/cjs/server-call.cjs +103 -0
  34. package/dist/cjs/server-rpc-batch-client.cjs +141 -0
  35. package/dist/cjs/server-rpc-client.cjs +94 -27
  36. package/dist/cjs/server-rpc-protocol.cjs +141 -0
  37. package/dist/cjs/server-rpc-stream-client.cjs +118 -0
  38. package/dist/cjs/signals/actions.cjs +474 -0
  39. package/dist/cjs/signals/client.cjs +4 -1
  40. package/dist/cjs/signals/computations.cjs +460 -0
  41. package/dist/cjs/signals/control-binding.cjs +368 -0
  42. package/dist/cjs/signals/derived-facade.cjs +50 -0
  43. package/dist/cjs/signals/document-owner.cjs +71 -0
  44. package/dist/cjs/signals/early-values.cjs +155 -0
  45. package/dist/cjs/signals/encoding.cjs +6 -95
  46. package/dist/cjs/signals/engine.cjs +276 -12
  47. package/dist/cjs/signals/errors.cjs +18 -0
  48. package/dist/cjs/signals/facade.cjs +477 -0
  49. package/dist/cjs/signals/graph.cjs +163 -20
  50. package/dist/cjs/signals/handle-protocol.cjs +36 -0
  51. package/dist/cjs/signals/index.cjs +81 -2
  52. package/dist/cjs/signals/native-read-client.cjs +10 -0
  53. package/dist/cjs/signals/owner-context.cjs +112 -0
  54. package/dist/cjs/signals/query-attempt-observer.cjs +68 -0
  55. package/dist/cjs/signals/query-facade.cjs +72 -0
  56. package/dist/cjs/signals/read-protocol.cjs +32 -0
  57. package/dist/cjs/signals/requests.cjs +287 -14
  58. package/dist/cjs/signals/scalar-computations.cjs +76 -0
  59. package/dist/cjs/signals/server.cjs +4 -1
  60. package/dist/cjs/signals/transition-candidate.cjs +567 -0
  61. package/dist/cjs/signals/transition-state.cjs +106 -0
  62. package/dist/cjs/signals/types.cjs +50 -0
  63. package/dist/cjs/stream-protocol.cjs +3 -0
  64. package/dist/cjs/streamed-signals-protocol.cjs +159 -0
  65. package/dist/cjs/style-values.cjs +122 -0
  66. package/dist/cjs/version.cjs +1 -1
  67. package/dist/class-names.d.ts +5 -0
  68. package/dist/class-names.js +25 -0
  69. package/dist/compiler/bundler.js +34 -6
  70. package/dist/compiler/compile.js +2234 -559
  71. package/dist/compiler/dom-binding-program.js +935 -0
  72. package/dist/compiler/dom-binding-request.js +57 -0
  73. package/dist/compiler/dom-bindings.js +2245 -0
  74. package/dist/compiler/hook-deps.js +26 -0
  75. package/dist/compiler/hydrate-boundaries.js +358 -15
  76. package/dist/compiler/index.d.ts +38 -1
  77. package/dist/compiler/index.js +1 -0
  78. package/dist/compiler/native-attribute-reads.js +127 -0
  79. package/dist/compiler/native-read-diagnostics.js +48 -9
  80. package/dist/compiler/native-read-types.js +55 -32
  81. package/dist/compiler/server-context.js +118 -0
  82. package/dist/compiler/signal-attempt-reads.js +479 -0
  83. package/dist/compiler/signal-declarations.js +557 -0
  84. package/dist/compiler/signal-start-reads.js +413 -0
  85. package/dist/compiler/slot-hooks.js +57 -10
  86. package/dist/compiler/template-ir.js +200 -0
  87. package/dist/compiler/typescript.d.ts +2 -0
  88. package/dist/compiler/typescript.js +2 -0
  89. package/dist/compiler/vite.d.ts +3 -1
  90. package/dist/compiler/vite.js +122 -4
  91. package/dist/compiler/volar.d.ts +6 -1
  92. package/dist/compiler/volar.js +674 -143
  93. package/dist/constants.d.ts +1 -0
  94. package/dist/constants.js +10 -0
  95. package/dist/css.d.ts +2 -5
  96. package/dist/css.js +2 -23
  97. package/dist/data-encoding.d.ts +9 -0
  98. package/dist/data-encoding.js +134 -0
  99. package/dist/dom-binding-classes.d.ts +15 -0
  100. package/dist/dom-binding-classes.js +129 -0
  101. package/dist/dom-binding-controls.d.ts +2 -0
  102. package/dist/dom-binding-controls.js +6 -0
  103. package/dist/dom-binding-handoff.d.ts +55 -0
  104. package/dist/dom-binding-handoff.js +62 -0
  105. package/dist/dom-binding-program.d.ts +300 -0
  106. package/dist/dom-binding-program.js +1625 -0
  107. package/dist/dom-binding-projections.d.ts +15 -0
  108. package/dist/dom-binding-projections.js +187 -0
  109. package/dist/dom-binding-protocol.d.ts +13 -0
  110. package/dist/dom-binding-protocol.js +88 -0
  111. package/dist/dom-binding-signals.d.ts +21 -0
  112. package/dist/dom-binding-signals.js +109 -0
  113. package/dist/dom-binding-styles.d.ts +17 -0
  114. package/dist/dom-binding-styles.js +272 -0
  115. package/dist/dom-bindings.d.ts +89 -0
  116. package/dist/dom-bindings.js +670 -0
  117. package/dist/dom-focused-move.d.ts +6 -0
  118. package/dist/dom-focused-move.js +42 -0
  119. package/dist/dom-tables.d.ts +1 -3
  120. package/dist/dom-tables.js +1 -91
  121. package/dist/error-codes.client.generated.d.ts +9 -0
  122. package/dist/error-codes.client.generated.js +33 -0
  123. package/dist/error-codes.server.generated.d.ts +5 -0
  124. package/dist/error-codes.server.generated.js +19 -0
  125. package/dist/hydration/control-capture.d.ts +62 -0
  126. package/dist/hydration/control-capture.js +223 -0
  127. package/dist/hydration/document-lifecycle.d.ts +23 -0
  128. package/dist/hydration/document-lifecycle.js +80 -0
  129. package/dist/hydration/event-capture.d.ts +20 -1
  130. package/dist/hydration/event-capture.js +128 -42
  131. package/dist/hydration/independent-island.d.ts +45 -0
  132. package/dist/hydration/independent-island.js +233 -0
  133. package/dist/hydration/index.d.ts +8 -1
  134. package/dist/hydration/index.js +55 -1
  135. package/dist/hydration/interaction-config.d.ts +5 -0
  136. package/dist/hydration/interaction-config.js +44 -1
  137. package/dist/hydration/stream-delivery.d.ts +32 -0
  138. package/dist/hydration/stream-delivery.js +333 -0
  139. package/dist/hydration/stream-receiver.d.ts +26 -0
  140. package/dist/hydration/stream-receiver.js +204 -0
  141. package/dist/hydration/stream-result-receiver.d.ts +70 -0
  142. package/dist/hydration/stream-result-receiver.js +246 -0
  143. package/dist/hydration/streamed-signals.d.ts +44 -0
  144. package/dist/hydration/streamed-signals.js +186 -0
  145. package/dist/hydration/types.d.ts +6 -0
  146. package/dist/hydration-markers.d.ts +7 -0
  147. package/dist/hydration-markers.js +10 -0
  148. package/dist/independent-hydration-protocol.d.ts +31 -0
  149. package/dist/independent-hydration-protocol.js +80 -0
  150. package/dist/index.d.ts +2 -0
  151. package/dist/index.js +4 -0
  152. package/dist/internal/signal-read.d.ts +1 -0
  153. package/dist/internal/signal-read.js +4 -0
  154. package/dist/jsx-runtime.d.ts +45 -6
  155. package/dist/public-types.d.ts +15 -1
  156. package/dist/runtime.d.ts +259 -21
  157. package/dist/runtime.js +2812 -220
  158. package/dist/runtime.server.d.ts +86 -6
  159. package/dist/runtime.server.js +712 -76
  160. package/dist/server/early-signals.d.ts +14 -0
  161. package/dist/server/early-signals.js +34 -0
  162. package/dist/server/index.d.ts +10 -2
  163. package/dist/server/index.js +48 -1
  164. package/dist/server/rpc-batch.d.ts +17 -0
  165. package/dist/server/rpc-batch.js +129 -0
  166. package/dist/server/rpc-stream.d.ts +9 -0
  167. package/dist/server/rpc-stream.js +125 -0
  168. package/dist/server/rpc.d.ts +5 -1
  169. package/dist/server/rpc.js +37 -13
  170. package/dist/server/signal-query-observation.d.ts +3 -0
  171. package/dist/server/signal-query-observation.js +134 -0
  172. package/dist/server/streamed-region.d.ts +15 -0
  173. package/dist/server/streamed-region.js +30 -0
  174. package/dist/server/streamed-signals.d.ts +45 -0
  175. package/dist/server/streamed-signals.js +488 -0
  176. package/dist/server-call.d.ts +41 -0
  177. package/dist/server-call.js +75 -0
  178. package/dist/server-rpc-batch-client.d.ts +7 -0
  179. package/dist/server-rpc-batch-client.js +119 -0
  180. package/dist/server-rpc-client.d.ts +3 -1
  181. package/dist/server-rpc-client.js +99 -27
  182. package/dist/server-rpc-protocol.d.ts +47 -0
  183. package/dist/server-rpc-protocol.js +109 -0
  184. package/dist/server-rpc-stream-client.d.ts +5 -0
  185. package/dist/server-rpc-stream-client.js +98 -0
  186. package/dist/signals/actions.d.ts +12 -0
  187. package/dist/signals/actions.js +466 -0
  188. package/dist/signals/client.d.ts +2 -0
  189. package/dist/signals/client.js +1 -0
  190. package/dist/signals/computations.d.ts +37 -0
  191. package/dist/signals/computations.js +455 -0
  192. package/dist/signals/control-binding.d.ts +31 -0
  193. package/dist/signals/control-binding.js +353 -0
  194. package/dist/signals/derived-facade.d.ts +3 -0
  195. package/dist/signals/derived-facade.js +25 -0
  196. package/dist/signals/document-owner.d.ts +10 -0
  197. package/dist/signals/document-owner.js +43 -0
  198. package/dist/signals/early-values.d.ts +24 -0
  199. package/dist/signals/early-values.js +124 -0
  200. package/dist/signals/encoding.d.ts +1 -3
  201. package/dist/signals/encoding.js +3 -93
  202. package/dist/signals/engine.d.ts +53 -4
  203. package/dist/signals/engine.js +275 -16
  204. package/dist/signals/errors.d.ts +7 -0
  205. package/dist/signals/errors.js +16 -0
  206. package/dist/signals/facade.d.ts +74 -0
  207. package/dist/signals/facade.js +452 -0
  208. package/dist/signals/graph.d.ts +50 -5
  209. package/dist/signals/graph.js +170 -19
  210. package/dist/signals/handle-protocol.d.ts +4 -0
  211. package/dist/signals/handle-protocol.js +11 -0
  212. package/dist/signals/index.d.ts +12 -3
  213. package/dist/signals/index.js +71 -2
  214. package/dist/signals/native-read-client.d.ts +3 -0
  215. package/dist/signals/native-read-client.js +10 -0
  216. package/dist/signals/owner-context.d.ts +22 -0
  217. package/dist/signals/owner-context.js +77 -0
  218. package/dist/signals/query-attempt-observer.d.ts +49 -0
  219. package/dist/signals/query-attempt-observer.js +41 -0
  220. package/dist/signals/query-facade.d.ts +3 -0
  221. package/dist/signals/query-facade.js +49 -0
  222. package/dist/signals/read-protocol.d.ts +32 -3
  223. package/dist/signals/read-protocol.js +26 -0
  224. package/dist/signals/requests.d.ts +38 -5
  225. package/dist/signals/requests.js +298 -15
  226. package/dist/signals/scalar-computations.d.ts +2 -0
  227. package/dist/signals/scalar-computations.js +60 -0
  228. package/dist/signals/server.d.ts +2 -0
  229. package/dist/signals/server.js +1 -0
  230. package/dist/signals/transition-candidate.d.ts +71 -0
  231. package/dist/signals/transition-candidate.js +557 -0
  232. package/dist/signals/transition-state.d.ts +51 -0
  233. package/dist/signals/transition-state.js +70 -0
  234. package/dist/signals/types.d.ts +95 -5
  235. package/dist/signals/types.js +20 -0
  236. package/dist/stream-protocol.d.ts +0 -5
  237. package/dist/stream-protocol.js +3 -0
  238. package/dist/streamed-signals-protocol.d.ts +60 -0
  239. package/dist/streamed-signals-protocol.js +130 -0
  240. package/dist/style-values.d.ts +3 -0
  241. package/dist/style-values.js +96 -0
  242. package/dist/version.js +1 -1
  243. package/package.json +37 -1
@@ -40,6 +40,12 @@ import {
40
40
  withDeferredImports,
41
41
  } from '@tsrx/core';
42
42
  import { parseModule } from '#octane/compiler-parser';
43
+ import {
44
+ collectServerFunctionNodes,
45
+ serverContextTypeImports,
46
+ serverFunctionContextIndex,
47
+ unwrapServerFunctionInitializer,
48
+ } from './server-context.js';
43
49
  import { createStyleScopePass } from './style-scopes.js';
44
50
  import { decodeHTMLStrict } from 'entities';
45
51
  import { print as esrapPrint } from 'esrap';
@@ -47,6 +53,7 @@ import esrapTsx from 'esrap/languages/tsx';
47
53
  import { buildFatSegments } from './fat-segments.js';
48
54
  import {
49
55
  METHOD_DEP_IMPORT,
56
+ analyzeCallbackDependencies,
50
57
  analyzeHookDependencies,
51
58
  applyHookDependencies,
52
59
  isInvariantLiteral,
@@ -93,7 +100,24 @@ import {
93
100
  wrapNativeReadScope,
94
101
  wrapNativeWarmScope,
95
102
  } from './native-read-codegen.js';
96
- import { createTextTypeFactsLookup } from './text-type-facts.js';
103
+ import { createTextTypeFactsLookup, normalizeTextTypeFilename } from './text-type-facts.js';
104
+ import { lowerSignalDeclarations } from './signal-declarations.js';
105
+ import { lowerSignalAttemptReads } from './signal-attempt-reads.js';
106
+ import { startIndependentSignalReads } from './signal-start-reads.js';
107
+ import {
108
+ isDirectSignalHandleExpression,
109
+ lowerNativeAttributeReads,
110
+ } from './native-attribute-reads.js';
111
+ import { prepareDomBindings } from './dom-bindings.js';
112
+ import { parseDomBindingRequest } from './dom-binding-request.js';
113
+ import {
114
+ createTemplateIr,
115
+ appendTemplatePart,
116
+ appendTemplateIr,
117
+ templatePart,
118
+ templateElement,
119
+ serializeTemplateIr,
120
+ } from './template-ir.js';
97
121
  import { collectProvenContextBindings, isProvenContextUse } from './context-use.js';
98
122
  import { applyCssModuleConstants } from './css-module-constants.js';
99
123
  import { assertUniversalRuntimeTarget, normalizeUniversalRuntime } from './universal-runtime.js';
@@ -507,6 +531,135 @@ function attrBindingUpdateHelper(bind, inlineBindingGuards = false) {
507
531
  }
508
532
  }
509
533
 
534
+ function canCarryDirectSignalHandle(node) {
535
+ if (!node || node.metadata?.octane_string_child || node.metadata?.octane_primitive_text_child) {
536
+ return false;
537
+ }
538
+ if (
539
+ node.type === 'TSAsExpression' ||
540
+ node.type === 'TSTypeAssertion' ||
541
+ node.type === 'TSNonNullExpression' ||
542
+ node.type === 'ParenthesizedExpression' ||
543
+ node.type === 'ChainExpression'
544
+ ) {
545
+ return canCarryDirectSignalHandle(node.expression);
546
+ }
547
+ if (node.type === 'ConditionalExpression') {
548
+ return (
549
+ canCarryDirectSignalHandle(node.consequent) || canCarryDirectSignalHandle(node.alternate)
550
+ );
551
+ }
552
+ if (node.type === 'LogicalExpression') {
553
+ return canCarryDirectSignalHandle(node.left) || canCarryDirectSignalHandle(node.right);
554
+ }
555
+ if (node.type === 'SequenceExpression') {
556
+ return canCarryDirectSignalHandle(node.expressions.at(-1));
557
+ }
558
+ if (
559
+ (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') &&
560
+ (node.callee?.type === 'MemberExpression' ||
561
+ node.callee?.type === 'OptionalMemberExpression') &&
562
+ (node.callee.computed
563
+ ? node.callee.property?.type === 'Literal' && node.callee.property.value === 'get'
564
+ : node.callee.property?.name === 'get')
565
+ ) {
566
+ return false;
567
+ }
568
+ return ![
569
+ 'Literal',
570
+ 'TemplateLiteral',
571
+ 'UnaryExpression',
572
+ 'BinaryExpression',
573
+ 'UpdateExpression',
574
+ 'ObjectExpression',
575
+ 'ArrayExpression',
576
+ 'FunctionExpression',
577
+ 'ArrowFunctionExpression',
578
+ 'ClassExpression',
579
+ 'JSXElement',
580
+ 'JSXFragment',
581
+ ].includes(node.type);
582
+ }
583
+
584
+ function spreadContainsDirectSignalHandle(node) {
585
+ if (isDirectSignalHandleExpression(node)) return true;
586
+ while (
587
+ node &&
588
+ (node.type === 'TSAsExpression' ||
589
+ node.type === 'TSTypeAssertion' ||
590
+ node.type === 'TSNonNullExpression')
591
+ ) {
592
+ node = node.expression;
593
+ }
594
+ if (node?.type === 'ObjectExpression') {
595
+ return (node.properties ?? []).some((property) => {
596
+ if (property.type === 'SpreadElement' || property.type === 'SpreadProperty') {
597
+ return spreadContainsDirectSignalHandle(property.argument);
598
+ }
599
+ return spreadContainsDirectSignalHandle(property.value);
600
+ });
601
+ }
602
+ if (node?.type === 'ArrayExpression') {
603
+ return (node.elements ?? []).some(spreadContainsDirectSignalHandle);
604
+ }
605
+ return false;
606
+ }
607
+
608
+ function directSignalSite(ctx, node, kind = 'binding') {
609
+ const position = node?.start ?? `${node?.loc?.start?.line ?? 0}:${node?.loc?.start?.column ?? 0}`;
610
+ // Branch/loop helper names differ between the client and server compilers.
611
+ // Identity belongs to the authored site, not its generated function owner.
612
+ return `${kind === 'input' ? 'i' : 'b'}:${strongHash(
613
+ `octane:signal-${kind}-site:2\0${normalizeTextTypeFilename(ctx.filename) ?? ctx.filename}\0${position}`,
614
+ )}`;
615
+ }
616
+
617
+ function componentInvocationSite(ctx, node) {
618
+ const position = node?.start ?? `${node?.loc?.start?.line ?? 0}:${node?.loc?.start?.column ?? 0}`;
619
+ // Authored positions identify call sites independently of the server/client
620
+ // helper names introduced for loops, branches, and value-position fragments.
621
+ return `c:${strongHash(
622
+ `octane:component-invocation-site:1\0${normalizeTextTypeFilename(ctx.filename) ?? ctx.filename}\0${position}`,
623
+ )}`;
624
+ }
625
+
626
+ function markDirectSignalBinding(binding, ctx, origin, kind) {
627
+ if (!canCarryDirectSignalHandle(binding.expr)) return binding;
628
+ ctx.signalBindingsUsed = true;
629
+ if (binding.kind !== 'text' && binding.kind !== 'textOnlyChild') ctx.signalBindingsEager = true;
630
+ return {
631
+ ...binding,
632
+ signalDirect: true,
633
+ signalSite: directSignalSite(ctx, origin, kind),
634
+ };
635
+ }
636
+
637
+ function ssrSignalValue(node, ctx, origin, capability = false) {
638
+ if (!(capability ? canCarryDirectSignalHandle(node) : isDirectSignalHandleExpression(node))) {
639
+ return node;
640
+ }
641
+ ctx.signalBindingsUsed = true;
642
+ ctx.runtimeNeeded.add('ssrSignalValue');
643
+ return ssrCall('ssrSignalValue', [node], origin ?? node);
644
+ }
645
+
646
+ function ssrSignalStyleObject(node, ctx, origin) {
647
+ // Non-native compilation only: preserve targeted binding reads without
648
+ // opting an otherwise ordinary module into a native renderer read frame.
649
+ if (node?.type !== 'ObjectExpression') return node;
650
+ let changed = false;
651
+ const properties = (node.properties ?? []).map((property) => {
652
+ if (property.type === 'SpreadElement' || property.type === 'SpreadProperty') {
653
+ return property;
654
+ }
655
+ const value = ssrSignalValue(property.value, ctx, origin ?? property, true);
656
+ if (value === property.value) return property;
657
+ changed = true;
658
+ return { ...property, value };
659
+ });
660
+ return changed ? { ...node, properties } : node;
661
+ }
662
+
510
663
  /**
511
664
  * An ALIAS: an authored range that emits nothing of its own but means the same
512
665
  * thing as one that does. A component's closing tag is the case — `</Card>` is
@@ -878,8 +1031,8 @@ function hasPotentialDangerouslySetInnerHTML(node) {
878
1031
  const attrs = node.attributes || node.openingElement?.attributes || [];
879
1032
  return attrs.some(
880
1033
  (attr) =>
881
- attr.type === 'SpreadAttribute' ||
882
- attr.type === 'JSXSpreadAttribute' ||
1034
+ ((attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') &&
1035
+ !attr._octaneKnownAttributeSpread) ||
883
1036
  ((attr.type === 'Attribute' || attr.type === 'JSXAttribute') &&
884
1037
  jsxAttrRawName(attr) === 'dangerouslySetInnerHTML'),
885
1038
  );
@@ -889,6 +1042,7 @@ function hasDefinitelyPresentDirectDangerouslySetInnerHTML(node) {
889
1042
  const attrs = node.attributes || node.openingElement?.attributes || [];
890
1043
  let state = false;
891
1044
  for (const attr of attrs) {
1045
+ if (attr._octaneKnownAttributeSpread) continue;
892
1046
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
893
1047
  // A later spread may overwrite or introduce the prop, so the effective
894
1048
  // writer is no longer statically known. A following direct writer can
@@ -915,6 +1069,7 @@ function hasDefinitelyPresentDirectChildrenProp(node) {
915
1069
  const attrs = node.attributes || node.openingElement?.attributes || [];
916
1070
  let state = false;
917
1071
  for (const attr of attrs) {
1072
+ if (attr._octaneKnownAttributeSpread) continue;
918
1073
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
919
1074
  state = null;
920
1075
  continue;
@@ -1192,6 +1347,7 @@ const HOOK_MEMO_RUNTIME_HELPERS = new Set([
1192
1347
  ]);
1193
1348
  const NATIVE_READ_RUNTIME_HELPERS = new Set([
1194
1349
  'nativeStyleBinding',
1350
+ 'nativeProjectionBinding',
1195
1351
  'readNativeDomStyle',
1196
1352
  'readNativeDomProps',
1197
1353
  'enableNativeReadCollection',
@@ -1213,6 +1369,27 @@ const NATIVE_READ_RUNTIME_HELPERS = new Set([
1213
1369
  'nativeCreateScopedElement',
1214
1370
  ]);
1215
1371
  const INTERNAL_CLIENT_RUNTIME_HELPERS = new Set([
1372
+ 'bindPresentationView',
1373
+ 'beginPresentationHydration',
1374
+ 'endPresentationHydration',
1375
+ 'presentationWrite',
1376
+ 'presentationHostWrite',
1377
+ 'presentationStructure',
1378
+ 'presentationFailure',
1379
+ 'markBindingChildren',
1380
+ 'bindingText',
1381
+ 'bindingChildSlot',
1382
+ 'setBindingClass',
1383
+ 'setBindingClassIfChanged',
1384
+ 'enableSignalBindings',
1385
+ 'createElementAt',
1386
+ 'createElementFromConfig',
1387
+ 'bindSignalText',
1388
+ 'bindSignalChild',
1389
+ 'bindSignalAttribute',
1390
+ 'bindSignalValue',
1391
+ 'bindSignalChecked',
1392
+ 'bindSignalHostPropSources',
1216
1393
  'canSplitStyleProperties',
1217
1394
  'styleObjectPrototype',
1218
1395
  'replaceRef',
@@ -1235,6 +1412,18 @@ const INTERNAL_CLIENT_RUNTIME_HELPERS = new Set([
1235
1412
  ...NATIVE_READ_RUNTIME_HELPERS,
1236
1413
  ]);
1237
1414
  const INTERNAL_SERVER_RUNTIME_HELPERS = new Set([
1415
+ 'bindPresentationView',
1416
+ 'ssrBindingHtml',
1417
+ 'ssrBindingBlock',
1418
+ 'ssrBindingChild',
1419
+ 'ssrBindingKey',
1420
+ 'ssrBindingClass',
1421
+ 'enableServerSignalBindings',
1422
+ 'createElementAt',
1423
+ 'createElementFromConfig',
1424
+ 'ssrSignalValue',
1425
+ 'ssrSignalControlValue',
1426
+ 'ssrSignalControlAttrs',
1238
1427
  'ssrSpreadContent',
1239
1428
  ...NATIVE_READ_RUNTIME_HELPERS,
1240
1429
  ]);
@@ -1617,6 +1806,7 @@ function lowerImportedErrorBoundaries(ast) {
1617
1806
  function inheritSoleCompRoot(bodyNodes, ctx) {
1618
1807
  if (bodyNodes.length !== 1) return false;
1619
1808
  const n = bodyNodes[0];
1809
+ if (n._octaneBindingSite !== undefined) return false;
1620
1810
  if (n.type !== 'Element' || !isComponentTag(n)) return false;
1621
1811
  // Returned-JSX extraction can merge an authored key/spread config into one
1622
1812
  // spread hole. Its descriptor still needs its own keyed reconciliation range.
@@ -6835,6 +7025,19 @@ function wrapAsBlockStmt(node) {
6835
7025
  return b.block([node]);
6836
7026
  }
6837
7027
 
7028
+ // Native host children already lower a JSX ternary through the same @if ABI.
7029
+ // Keep that positional claim at the callers; root/fragment value holes remain
7030
+ // renderable values. Binding planning shares this exact arm conversion and only
7031
+ // adds the compiler-issued site receipt to the otherwise unchanged statement.
7032
+ function conditionalJsxStatement(expr) {
7033
+ if (!isConditionalJsx(expr)) return null;
7034
+ return {
7035
+ ...b.if(expr.test, wrapAsBlockStmt(expr.consequent), wrapAsBlockStmt(expr.alternate)),
7036
+ loc: expr.loc,
7037
+ ...(expr._octaneBindingSite ? { _octaneBindingSite: expr._octaneBindingSite } : {}),
7038
+ };
7039
+ }
7040
+
6838
7041
  /** `xs.map(x => <li/>)` — detect so we can throw a useful "use for-of" error. */
6839
7042
  function isJsxReturningMapCall(node) {
6840
7043
  if (!node || node.type !== 'CallExpression') return false;
@@ -7338,6 +7541,7 @@ function anchorlessRootShape(node) {
7338
7541
  * the hydrator can use it as the keyed item boundary without an item pair.
7339
7542
  */
7340
7543
  function isSsrMarkerlessForItem(node) {
7544
+ if (node?._octaneBindingSite !== undefined) return false;
7341
7545
  const body = node?.body?.body || [];
7342
7546
  const jsxChildren = body.filter((s) => isJsxNode(s));
7343
7547
  return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
@@ -7918,21 +8122,6 @@ function identifierName(node) {
7918
8122
  return null;
7919
8123
  }
7920
8124
 
7921
- function unwrapServerFunctionInitializer(node) {
7922
- let current = node;
7923
- while (
7924
- current &&
7925
- (current.type === 'TSAsExpression' ||
7926
- current.type === 'TSSatisfiesExpression' ||
7927
- current.type === 'TSNonNullExpression' ||
7928
- current.type === 'TypeCastExpression' ||
7929
- current.type === 'ParenthesizedExpression')
7930
- ) {
7931
- current = current.expression;
7932
- }
7933
- return current;
7934
- }
7935
-
7936
8125
  function collectServerFunctionBindings(statements) {
7937
8126
  const functions = new Set();
7938
8127
  const aliases = new Map();
@@ -8095,6 +8284,8 @@ function analyzeServerModule(ast, filename) {
8095
8284
  const statements = declaration.body?.body || [];
8096
8285
  validateServerModuleIsolation(ast, declaration, filename);
8097
8286
  const functionBindings = collectServerFunctionBindings(statements);
8287
+ const functionNodes = collectServerFunctionNodes(statements);
8288
+ const contextImports = serverContextTypeImports(statements);
8098
8289
  for (const statement of statements) {
8099
8290
  if (statement.type === 'ExportDefaultDeclaration') {
8100
8291
  throw new Error('`module server` does not support default exports; use named functions.');
@@ -8151,7 +8342,15 @@ function analyzeServerModule(ast, filename) {
8151
8342
  }
8152
8343
  }
8153
8344
 
8154
- return { declaration, imports, exports, filename };
8345
+ const contexts = new Map();
8346
+ for (const [exported, local] of exports) {
8347
+ const fn = functionNodes.get(local);
8348
+ if (fn === undefined) continue;
8349
+ const contextIndex = serverFunctionContextIndex(fn, contextImports, filename);
8350
+ if (contextIndex !== -1) contexts.set(exported, contextIndex);
8351
+ }
8352
+
8353
+ return { declaration, imports, exports, contexts, filename };
8155
8354
  }
8156
8355
 
8157
8356
  /**
@@ -8169,15 +8368,19 @@ function emitServerModuleClientStubs(info, ctx) {
8169
8368
  if (!imported || !local) continue;
8170
8369
  ctx.runtimeNeeded.add('__serverRpc');
8171
8370
  const hash = strongHash(info.filename + '#' + imported);
8371
+ const contextIndex = info.contexts.get(imported);
8372
+ const rpcArgs =
8373
+ contextIndex === undefined
8374
+ ? [b.literal(hash, JSON.stringify(hash)), b.id('args')]
8375
+ : [
8376
+ b.literal(hash, JSON.stringify(hash)),
8377
+ b.call(b.member(b.id('args'), 'slice'), b.literal(0), b.literal(contextIndex)),
8378
+ b.member(b.id('args'), b.literal(contextIndex), true),
8379
+ b.literal(true, 'true'),
8380
+ ];
8172
8381
  nodes.push(
8173
8382
  inheritOriginLoc(
8174
- b.const(
8175
- local,
8176
- b.arrow(
8177
- [b.rest(b.id('args'))],
8178
- b.call('_$__serverRpc', b.literal(hash, JSON.stringify(hash)), b.id('args')),
8179
- ),
8180
- ),
8383
+ b.const(local, b.arrow([b.rest(b.id('args'))], b.call('_$__serverRpc', ...rpcArgs))),
8181
8384
  specifier,
8182
8385
  ),
8183
8386
  );
@@ -8244,13 +8447,29 @@ function emitServerModuleServerNodes(info, ctx) {
8244
8447
  ...namespaceBody,
8245
8448
  ];
8246
8449
  for (const [exported, local] of info.exports) {
8450
+ const contextIndex = info.contexts.get(exported);
8451
+ let value = b.id(local);
8452
+ if (contextIndex !== undefined) {
8453
+ ctx.runtimeNeeded.add('__registerServerFunction');
8454
+ const hash = strongHash(info.filename + '#' + exported);
8455
+ value = b.call(
8456
+ '_$__registerServerFunction',
8457
+ value,
8458
+ b.literal(contextIndex),
8459
+ b.object([
8460
+ b.prop('init', b.id('id'), b.literal(hash, JSON.stringify(hash))),
8461
+ b.prop('init', b.id('module'), b.literal(info.filename, JSON.stringify(info.filename))),
8462
+ b.prop('init', b.id('export'), b.literal(exported, JSON.stringify(exported))),
8463
+ ]),
8464
+ );
8465
+ }
8247
8466
  serverStatements.push(
8248
8467
  inheritOriginLoc(
8249
8468
  b.stmt(
8250
8469
  b.assignment(
8251
8470
  '=',
8252
8471
  b.member(b.id('server'), b.literal(exported, JSON.stringify(exported)), true),
8253
- b.id(local),
8472
+ value,
8254
8473
  ),
8255
8474
  ),
8256
8475
  origin,
@@ -8889,11 +9108,326 @@ export function compile(source, filename, options) {
8889
9108
  return compileAuthored(source, filename, options, null);
8890
9109
  }
8891
9110
 
9111
+ // A provider may describe an imported pure attribute factory's exact stable own
9112
+ // data fields. The import binding, not a spelling such as `stylex.attrs`, is authority.
9113
+ function markKnownAttributeSpreads(ast, contracts) {
9114
+ if (contracts === undefined) return ast;
9115
+ if (!Array.isArray(contracts)) throw new TypeError('knownAttributeSpreads must be an array.');
9116
+ if (contracts.length === 0) return ast;
9117
+ const paths = new Set();
9118
+ const shorthandProviders = new Map();
9119
+ for (const contract of contracts) {
9120
+ if (
9121
+ !contract ||
9122
+ typeof contract.source !== 'string' ||
9123
+ !contract.source ||
9124
+ typeof contract.imported !== 'string' ||
9125
+ !contract.imported ||
9126
+ !Array.isArray(contract.fields) ||
9127
+ contract.fields.length === 0 ||
9128
+ new Set(contract.fields).size !== contract.fields.length ||
9129
+ contract.fields.some(
9130
+ (field) =>
9131
+ typeof field !== 'string' ||
9132
+ !/^(?:class|className|style|title|role|aria-[a-z][a-z0-9_-]*|data-[a-z][a-z0-9_-]*)$/.test(
9133
+ field,
9134
+ ) ||
9135
+ field.startsWith('data-octane-'),
9136
+ ) ||
9137
+ (contract.members !== undefined &&
9138
+ (!Array.isArray(contract.members) ||
9139
+ contract.members.some(
9140
+ (member) => typeof member !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(member),
9141
+ ))) ||
9142
+ (contract.imported === '*' && !contract.members?.length) ||
9143
+ (contract.style !== undefined &&
9144
+ (contract.style !== 'object' || !contract.fields.includes('style')))
9145
+ )
9146
+ throw new TypeError(
9147
+ 'Invalid knownAttributeSpreads import or fixed native attribute contract.',
9148
+ );
9149
+ const path = JSON.stringify([contract.source, contract.imported, contract.members ?? []]);
9150
+ if (paths.has(path))
9151
+ throw new TypeError(
9152
+ 'Duplicate knownAttributeSpreads contract for the same imported factory.',
9153
+ );
9154
+ paths.add(path);
9155
+ if (contract.jsxAttribute !== undefined) {
9156
+ if (
9157
+ typeof contract.jsxAttribute !== 'string' ||
9158
+ !/^[A-Za-z_$][\w$-]*$/.test(contract.jsxAttribute) ||
9159
+ shorthandProviders.has(contract.jsxAttribute)
9160
+ )
9161
+ throw new TypeError('Invalid or ambiguous knownAttributeSpreads jsxAttribute.');
9162
+ shorthandProviders.set(contract.jsxAttribute, contract);
9163
+ }
9164
+ }
9165
+ const imports = new Map();
9166
+ for (const declaration of ast.body) {
9167
+ if (declaration.type !== 'ImportDeclaration' || declaration.importKind === 'type') continue;
9168
+ for (const specifier of declaration.specifiers) {
9169
+ if (specifier.importKind === 'type') continue;
9170
+ const imported =
9171
+ specifier.type === 'ImportNamespaceSpecifier'
9172
+ ? '*'
9173
+ : specifier.type === 'ImportDefaultSpecifier'
9174
+ ? 'default'
9175
+ : (specifier.imported.name ?? specifier.imported.value);
9176
+ const matches = contracts.filter(
9177
+ (contract) =>
9178
+ contract.source === declaration.source.value && contract.imported === imported,
9179
+ );
9180
+ if (matches.length) imports.set(specifier.local.name, matches);
9181
+ }
9182
+ }
9183
+ const shorthandAttributes = new Map();
9184
+ const usedNames = new Set();
9185
+ if (shorthandProviders.size > 0) {
9186
+ mapAst(ast, (node) => {
9187
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') usedNames.add(node.name);
9188
+ if (node.type !== 'JSXOpeningElement' && node.type !== 'Element') return null;
9189
+ const name = node.type === 'Element' ? node.id : node.name;
9190
+ if (
9191
+ (name?.type !== 'JSXIdentifier' && name?.type !== 'Identifier') ||
9192
+ typeof name.name !== 'string' ||
9193
+ name.name[0] !== name.name[0].toLowerCase()
9194
+ )
9195
+ return null;
9196
+ const seen = new Set();
9197
+ for (const attr of node.attributes ?? []) {
9198
+ if (attr.type !== 'JSXAttribute' && attr.type !== 'Attribute') continue;
9199
+ const raw = attr.name?.name ?? attr.name;
9200
+ const contract = shorthandProviders.get(raw);
9201
+ if (
9202
+ !contract ||
9203
+ seen.has(raw) ||
9204
+ attr.value?.type !== 'JSXExpressionContainer' ||
9205
+ attr.value.expression?.type === 'JSXEmptyExpression'
9206
+ )
9207
+ continue;
9208
+ seen.add(raw);
9209
+ shorthandAttributes.set(attr, contract);
9210
+ }
9211
+ return null;
9212
+ });
9213
+ }
9214
+ if (imports.size === 0 && shorthandAttributes.size === 0) return ast;
9215
+ const lexical = createLexicalAnalysis(ast);
9216
+ const addedImports = [];
9217
+ const addedBindings = new Map();
9218
+ let nativeReadName;
9219
+ let hasProjection = false;
9220
+ const read = (node) => {
9221
+ if (nativeReadName === undefined) {
9222
+ nativeReadName = '_nativeAttributeValue';
9223
+ for (let suffix = 1; usedNames.has(nativeReadName); suffix++)
9224
+ nativeReadName = `_nativeAttributeValue${suffix}`;
9225
+ usedNames.add(nativeReadName);
9226
+ addedImports.push(
9227
+ inheritOriginLoc(
9228
+ b.imports([['readNativeDomValue', nativeReadName]], 'octane/internal/signal-read'),
9229
+ node,
9230
+ ),
9231
+ );
9232
+ }
9233
+ const call = inheritOriginLoc(b.call(b.id(nativeReadName), node), node);
9234
+ call.metadata = { octaneNativeSignalRead: true };
9235
+ return call;
9236
+ };
9237
+ const result = mapAst(ast, (node) => {
9238
+ const shorthand = shorthandAttributes.get(node);
9239
+ if (shorthand) {
9240
+ let local;
9241
+ for (const [name, matches] of imports) {
9242
+ if (
9243
+ matches.includes(shorthand) &&
9244
+ lexical.resolveBinding(lexical.nodeScopes.get(node), name)?.scope === lexical.rootScope
9245
+ ) {
9246
+ local = name;
9247
+ break;
9248
+ }
9249
+ }
9250
+ if (local === undefined) {
9251
+ local = addedBindings.get(shorthand);
9252
+ if (local === undefined) {
9253
+ local = '_jsxAttribute';
9254
+ for (let suffix = 1; usedNames.has(local); suffix++) local = `_jsxAttribute${suffix}`;
9255
+ usedNames.add(local);
9256
+ addedBindings.set(shorthand, local);
9257
+ addedImports.push(
9258
+ inheritOriginLoc(
9259
+ shorthand.imported === '*'
9260
+ ? b.import_all(local, shorthand.source)
9261
+ : shorthand.imported === 'default'
9262
+ ? b.import_declaration(
9263
+ [
9264
+ {
9265
+ type: 'ImportDefaultSpecifier',
9266
+ local: b.id(local),
9267
+ metadata: { path: [] },
9268
+ },
9269
+ ],
9270
+ shorthand.source,
9271
+ )
9272
+ : b.imports([[shorthand.imported, local]], shorthand.source),
9273
+ node,
9274
+ ),
9275
+ );
9276
+ }
9277
+ }
9278
+ let callee = b.id(local);
9279
+ for (const member of shorthand.members ?? []) callee = b.member(callee, b.id(member));
9280
+ const lifted = lowerNativeAttributeReads(node.value.expression, read);
9281
+ hasProjection ||= lifted.reactive;
9282
+ return {
9283
+ ...node,
9284
+ value: {
9285
+ ...node.value,
9286
+ expression: inheritOriginLoc(b.call(callee, lifted.expression), node),
9287
+ },
9288
+ _octaneKnownAttributeSpread: {
9289
+ fields: [...shorthand.fields],
9290
+ style: shorthand.style,
9291
+ ...(lifted.reactive ? { projection: true } : {}),
9292
+ },
9293
+ };
9294
+ }
9295
+ if (node.type !== 'JSXSpreadAttribute' && node.type !== 'SpreadAttribute') return null;
9296
+ const call = node.argument;
9297
+ if (call?.type !== 'CallExpression' || call.optional) return null;
9298
+ let callee = call.callee;
9299
+ const members = [];
9300
+ while (callee?.type === 'MemberExpression' && !callee.computed && !callee.optional) {
9301
+ members.unshift(callee.property.name);
9302
+ callee = callee.object;
9303
+ }
9304
+ if (callee?.type !== 'Identifier') return null;
9305
+ const matches = imports.get(callee.name);
9306
+ if (
9307
+ !matches ||
9308
+ lexical.resolveBinding(lexical.nodeScopes.get(callee), callee.name)?.scope !==
9309
+ lexical.rootScope
9310
+ )
9311
+ return null;
9312
+ const contract = matches.find((candidate) => {
9313
+ const expected = candidate.members ?? [];
9314
+ return (
9315
+ expected.length === members.length &&
9316
+ expected.every((member, index) => member === members[index])
9317
+ );
9318
+ });
9319
+ return contract
9320
+ ? {
9321
+ ...node,
9322
+ _octaneKnownAttributeSpread: { fields: [...contract.fields], style: contract.style },
9323
+ }
9324
+ : null;
9325
+ });
9326
+ if (hasProjection)
9327
+ mapAst(result, (node) => {
9328
+ if (node.type !== 'JSXOpeningElement' && node.type !== 'Element') return null;
9329
+ const attributes = node.attributes ?? [];
9330
+ const identity = (name) =>
9331
+ (name === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(name) ?? name)).toLowerCase();
9332
+ for (const attr of attributes) {
9333
+ if (!attr._octaneKnownAttributeSpread?.projection) continue;
9334
+ const fields = new Set(attr._octaneKnownAttributeSpread.fields.map(identity));
9335
+ for (const other of attributes) {
9336
+ if (other === attr) continue;
9337
+ const known = other._octaneKnownAttributeSpread;
9338
+ if (
9339
+ (known && known.fields.some((field) => fields.has(identity(field)))) ||
9340
+ (!known &&
9341
+ (other.type === 'JSXSpreadAttribute' ||
9342
+ other.type === 'SpreadAttribute' ||
9343
+ fields.has(identity(jsxAttrRawName(other)))))
9344
+ )
9345
+ throw new Error(
9346
+ 'Native JSX signal projections cannot overlap other writers for their fields or an unknown spread.',
9347
+ );
9348
+ }
9349
+ }
9350
+ return null;
9351
+ });
9352
+ return addedImports.length > 0 || hasProjection
9353
+ ? {
9354
+ ...result,
9355
+ body: [...addedImports, ...result.body],
9356
+ ...(hasProjection
9357
+ ? { metadata: { ...result.metadata, octaneNativeAttributeProjection: true } }
9358
+ : {}),
9359
+ }
9360
+ : result;
9361
+ }
9362
+
9363
+ // Keep the factory evaluation at its authored native element/iteration. Each
9364
+ // declared field then uses the normal compiler writer; there is no dynamic key
9365
+ // dispatcher and no second factory call. These are compiler-owned IR cells.
9366
+ function expandKnownAttributeSpreads(attributes) {
9367
+ if (!attributes.some((attr) => attr._octaneKnownAttributeSpread)) return attributes;
9368
+ return attributes.flatMap((attr) => {
9369
+ if (!attr._octaneKnownAttributeSpread) return [attr];
9370
+ if (
9371
+ attr._octaneKnownAttributeSpread.projection &&
9372
+ attr._octaneKnownAttributeSpread.fields.some(
9373
+ (field) => field === 'class' || field === 'className',
9374
+ ) &&
9375
+ attributes.some(
9376
+ (other) => other !== attr && ['class', 'className'].includes(jsxAttrRawName(other)),
9377
+ )
9378
+ ) {
9379
+ throw new Error(
9380
+ 'A reactive native attribute projection cannot share its class with scoped or authored classes. Compose classes in the projection.',
9381
+ );
9382
+ }
9383
+ const source = {
9384
+ binding: null,
9385
+ expression: null,
9386
+ projection: attr._octaneKnownAttributeSpread.projection === true,
9387
+ fields: attr._octaneKnownAttributeSpread.fields,
9388
+ };
9389
+ const argument = attr.argument ?? attr.value.expression;
9390
+ return [
9391
+ { knownSource: source, argument },
9392
+ ...attr._octaneKnownAttributeSpread.fields.map((field) => {
9393
+ const object = inheritOriginLoc(b.id('_knownAttributeSpread'), argument);
9394
+ object.metadata = { octaneKnownAttributeSource: source };
9395
+ const expression = inheritOriginLoc(
9396
+ b.conditional(
9397
+ b.binary('==', object, b.literal(null)),
9398
+ undefinedNode(),
9399
+ b.member(object, b.literal(field), true),
9400
+ ),
9401
+ argument,
9402
+ );
9403
+ expression.metadata = { octaneKnownAttributeRead: true };
9404
+ const attribute = inheritOriginLoc(
9405
+ b.jsx_attribute(
9406
+ inheritOriginLoc(b.jsx_id(field), attr),
9407
+ b.jsx_expression_container(expression, argument),
9408
+ ),
9409
+ attr,
9410
+ );
9411
+ if (source.projection) attribute._octaneKnownProjectionSource = source;
9412
+ return attribute;
9413
+ }),
9414
+ ];
9415
+ });
9416
+ }
9417
+
9418
+ function resolveKnownAttributeSource(expression, valueOf) {
9419
+ if (!expression?.metadata?.octaneKnownAttributeRead) return expression;
9420
+ return mapAst(expression, (node) => {
9421
+ const source = node.type === 'Identifier' && node.metadata?.octaneKnownAttributeSource;
9422
+ return source ? inheritOriginLoc(valueOf(source), node) : null;
9423
+ });
9424
+ }
9425
+
8892
9426
  // Internal bundler entry point. Unlike the old __hydratePrepared handoff, this
8893
9427
  // always validates authored source and only exposes the hydrate slice that the
8894
9428
  // same compilation already prepared for production void-export classification.
8895
9429
  export function compileForBundler(source, filename, options) {
8896
- const metadata = { hydrateAst: null, cssModuleConstantImports: [] };
9430
+ const metadata = { hydrateAst: null, cssModuleConstantImports: [], independentWidgets: [] };
8897
9431
  const result = compileAuthored(source, filename, options, metadata);
8898
9432
  return { result, ...metadata };
8899
9433
  }
@@ -8904,7 +9438,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8904
9438
  throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
8905
9439
  }
8906
9440
  const cleanFilename = cleanCompileFilename(filename);
8907
- const analyzedAst = markParserSensitiveHosts(
9441
+ let analyzedAst = markParserSensitiveHosts(
8908
9442
  normalizeAuthoredJsxLiterals(parseModule(source, cleanFilename)),
8909
9443
  );
8910
9444
  analyzeTsrx(analyzedAst, cleanFilename);
@@ -8913,20 +9447,89 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8913
9447
  assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
8914
9448
  const strongAnalysis = assertStrongMode(analyzedAst, source, cleanFilename, options);
8915
9449
  const strongModeEnabled = strongAnalysis?.enabled === true;
8916
- if (bundlerMetadata !== null) bundlerMetadata.hydrateAst = analyzedAst;
9450
+ analyzedAst = markKnownAttributeSpreads(analyzedAst, options?.knownAttributeSpreads);
9451
+ if (analyzedAst.metadata?.octaneNativeAttributeProjection) {
9452
+ options = { ...options, nativeReads: true };
9453
+ assertNativeReadOptions(options);
9454
+ }
9455
+ const bindingRequest = parseDomBindingRequest(filename);
9456
+ const bindingExport = bindingRequest?.exportName ?? null;
9457
+ if (
9458
+ bindingExport !== null &&
9459
+ (mode !== 'client' ||
9460
+ hydrateBoundaryPathFromId(filename) !== null ||
9461
+ (options?.renderer?.target && options.renderer.target !== 'dom'))
9462
+ ) {
9463
+ throw new Error(
9464
+ 'Octane DOM binding queries require the client DOM target without a Hydrate query.',
9465
+ );
9466
+ }
9467
+ const hasDomBindings =
9468
+ bindingExport !== null ||
9469
+ source.includes('use dom bindings') ||
9470
+ source.includes('adoptBindings') ||
9471
+ source.includes('mountBindings');
9472
+ let bindingConstants;
9473
+ // Binding plans classify authored child expressions before signal lowering or
9474
+ // JSX extraction. Use the same source-bound proof pass here, exactly once;
9475
+ // query modules no longer contain the original child ranges after planning.
9476
+ const bindingAst = hasDomBindings
9477
+ ? prepareDomBindings(
9478
+ applyStringChildProofs(analyzedAst, source, cleanFilename, options?.textTypeFacts),
9479
+ source,
9480
+ cleanFilename,
9481
+ bindingExport,
9482
+ {
9483
+ normalizeChildren,
9484
+ conditionalJsxStatement,
9485
+ isText: isKnownTextChildExpression,
9486
+ canBorrowSsrHostBranchRange,
9487
+ analyzeCallbackDependencies,
9488
+ canCarryDirectSignalHandle,
9489
+ isDirectSignalHandleExpression,
9490
+ bakeStaticAttr,
9491
+ escapeHtml,
9492
+ mount: bindingRequest?.mount ?? false,
9493
+ props: bindingRequest?.props ?? null,
9494
+ ...(mode === 'client' &&
9495
+ !options?.dev &&
9496
+ !options?.hmr &&
9497
+ (!options?.renderer?.target || options.renderer.target === 'dom')
9498
+ ? {
9499
+ collectConstants(names) {
9500
+ if (names.length > 0)
9501
+ bindingConstants = {
9502
+ version: 1,
9503
+ source: strongHash(source),
9504
+ names,
9505
+ };
9506
+ },
9507
+ }
9508
+ : null),
9509
+ },
9510
+ )
9511
+ : analyzedAst;
9512
+ const attemptAst = lowerSignalAttemptReads(bindingAst);
9513
+ const signalAst = lowerSignalDeclarations(
9514
+ startIndependentSignalReads(
9515
+ attemptAst,
9516
+ options?.textTypeFacts || (options?.renderer?.target && options.renderer.target !== 'dom')
9517
+ ? null
9518
+ : isKnownTextChildExpression,
9519
+ ),
9520
+ cleanFilename,
9521
+ );
9522
+ if (bundlerMetadata !== null) bundlerMetadata.hydrateAst = signalAst;
8917
9523
  const memoizedAst = strongModeEnabled
8918
- ? applyStrongAutomaticMemo(analyzedAst, {
9524
+ ? applyStrongAutomaticMemo(signalAst, {
8919
9525
  ...options,
8920
9526
  filename: cleanFilename,
8921
9527
  strongHookAnalysis: strongAnalysis.strongHookAnalysis,
8922
9528
  })
8923
- : analyzedAst;
8924
- const textTypedAst = applyStringChildProofs(
8925
- memoizedAst,
8926
- source,
8927
- cleanFilename,
8928
- options?.textTypeFacts,
8929
- );
9529
+ : signalAst;
9530
+ const textTypedAst = hasDomBindings
9531
+ ? memoizedAst
9532
+ : applyStringChildProofs(memoizedAst, source, cleanFilename, options?.textTypeFacts);
8930
9533
  let constantAst = textTypedAst;
8931
9534
  if (typeof options?.resolveCssModuleConstant === 'function') {
8932
9535
  const constants = applyCssModuleConstants(
@@ -8960,6 +9563,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8960
9563
  if (strongAnalysis?.diagnostics.length > 0) {
8961
9564
  result.diagnostics = [...strongAnalysis.diagnostics, ...(result.diagnostics ?? [])];
8962
9565
  }
9566
+ if (bindingConstants !== undefined) result.bindingConstants = bindingConstants;
8963
9567
  return result;
8964
9568
  }
8965
9569
 
@@ -9051,6 +9655,7 @@ function compileInternal(
9051
9655
  ).diagnostics;
9052
9656
  if (bundlerMetadata !== null) {
9053
9657
  bundlerMetadata.hydrateAst = hydratePreparation.ast;
9658
+ bundlerMetadata.independentWidgets = hydratePreparation.independentWidgets ?? [];
9054
9659
  }
9055
9660
  const compiled = compileInternal(
9056
9661
  source,
@@ -9400,6 +10005,8 @@ function compileInternal(
9400
10005
  ownedCssInjections: new Set(),
9401
10006
  componentOwners: [],
9402
10007
  currentComponentOwner: null,
10008
+ signalBindingsUsed: ast._octaneSignalDeclarations === true,
10009
+ signalBindingsEager: ast._octaneSignalDeclarations === true,
9403
10010
  currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
9404
10011
  currentMapTemps: null, // expression temps (map receiver/method, folded condition) owned by this function
9405
10012
  currentAutoMemoOffset: 0, // flat compiler-cache cell offset for the body being emitted
@@ -9930,18 +10537,18 @@ function compileInternal(
9930
10537
  ).filter((n) => n.type !== 'HeadHoist');
9931
10538
  if (inheritSoleCompRoot(bodyNodes, ctx)) eligible = false;
9932
10539
  }
9933
- info.eligible = eligible;
10540
+ info.eligible = eligible && !compNode._octaneBindingView;
9934
10541
  // Single-ELEMENT-root output: the component's body renders exactly one plain
9935
10542
  // DOM element (not a component tag, fragment, or control-flow). Such a
9936
10543
  // component self-delimits via that element on CLIENT mount — its
9937
10544
  // `componentSlot` needs no `comp`/`/comp` markers (singleRoot path), exactly
9938
10545
  // like a single-root `@for` item. (Output-shape based — independent of which
9939
10546
  // hooks it calls.)
9940
- info.singleRoot = singleHostComponentRoot(compNode);
10547
+ info.singleRoot = !compNode._octaneBindingView && singleHostComponentRoot(compNode);
9941
10548
  // Transitive @if-arm form of the same proof — resolved by the fixed point
9942
10549
  // below once every same-module callee an arm depends on is itself proven.
9943
10550
  info.singleRootIfDeps =
9944
- !info.singleRoot && isVoidJsxCodeBlockFunction(compNode)
10551
+ !compNode._octaneBindingView && !info.singleRoot && isVoidJsxCodeBlockFunction(compNode)
9945
10552
  ? collectSingleRootIfDeps(compNode.body.render, locals, ctx)
9946
10553
  : null;
9947
10554
  }
@@ -10501,6 +11108,22 @@ function compileInternal(
10501
11108
  ctx.runtimeNeeded.add('__vtSeen');
10502
11109
  vtHintNodes.push(inheritOriginLoc(b.stmt(b.call(rtAlias('__vtSeen'))), moduleOrigin));
10503
11110
  }
11111
+ const signalBindingNodes = [];
11112
+ if (ctx.signalBindingsUsed) {
11113
+ ctx.runtimeNeeded.add('enableSignalBindings');
11114
+ signalBindingNodes.push(
11115
+ inheritOriginLoc(
11116
+ b.stmt(
11117
+ b.call(
11118
+ '_$enableSignalBindings',
11119
+ b.literal(1),
11120
+ ...(ctx.signalBindingsEager ? [] : [b.literal(true)]),
11121
+ ),
11122
+ ),
11123
+ moduleOrigin,
11124
+ ),
11125
+ );
11126
+ }
10504
11127
 
10505
11128
  // All remaining authored memo sites have now received their real hook slots.
10506
11129
  // Lower callable helpers, returned-JSX functions, explicit-slot calls and
@@ -10509,6 +11132,7 @@ function compileInternal(
10509
11132
  // accidentally import DOM memo helpers for a universal renderer's scope.
10510
11133
  let moduleBody = [
10511
11134
  ...nativeReadActivationNodes(ctx, moduleOrigin),
11135
+ ...signalBindingNodes,
10512
11136
  ...vtHintNodes,
10513
11137
  ...delegateNodes,
10514
11138
  ...styleNodes,
@@ -10561,9 +11185,12 @@ function compileInternal(
10561
11185
  };
10562
11186
  const printed = printNodeWithMap(program, ctx);
10563
11187
 
10564
- /** @type {{ code: string, map: any, diagnostics: any, inspect?: any, __universalSourceMapComposed?: boolean }} */
11188
+ /** @type {{ code: string, map: any, diagnostics: any, inspect?: any, streamedSignals?: true, __universalSourceMapComposed?: boolean }} */
10565
11189
  const result = {
10566
11190
  code: printed.code,
11191
+ ...(ast._octaneSignalDeclarations === true || ctx.nativeReads
11192
+ ? { streamedSignals: true }
11193
+ : null),
10567
11194
  map: {
10568
11195
  version: 3,
10569
11196
  sources: [ctx.mapSourceName],
@@ -10684,6 +11311,8 @@ function compileServer(
10684
11311
  nativeModuleStyles: options?.nativeReads === true && hasModuleStyleMaps(ast.body),
10685
11312
  hmr: false, // SSR never hot-swaps in place; client/server production slot shapes stay aligned
10686
11313
  dev: !!(options && options.dev),
11314
+ signalBindingsUsed: ast._octaneSignalDeclarations === true,
11315
+ signalBindingsEager: ast._octaneSignalDeclarations === true || options?.nativeReads === true,
10687
11316
  // SSR MIRROR of the parallel-`use()` pipeline (docs/suspense-parallel-use-
10688
11317
  // plan.md Phase 5): the same memoize (Pass A) + hoist/batch (Pass B)
10689
11318
  // transforms run on server bodies, emitting `_$puMemo`/`_$puBatch` — the
@@ -10862,12 +11491,34 @@ function compileServer(
10862
11491
  // the flush may register `hookSlots` as a needed runtime import.
10863
11492
  flushTailHookSymbols(ctx);
10864
11493
  const nativeActivation = nativeReadActivationNodes(ctx, ctx._moduleOrigin);
11494
+ const signalActivation = [];
11495
+ if (ctx.signalBindingsUsed || ctx.signalBindingsEager) {
11496
+ ctx.runtimeNeeded.add('enableServerSignalBindings');
11497
+ signalActivation.push(
11498
+ inheritOriginLoc(
11499
+ b.stmt(
11500
+ b.call(
11501
+ '_$enableServerSignalBindings',
11502
+ b.literal(1),
11503
+ ...(ctx.signalBindingsEager ? [] : [b.literal(true)]),
11504
+ ),
11505
+ ),
11506
+ ctx._moduleOrigin,
11507
+ ),
11508
+ );
11509
+ }
10865
11510
  const runtimeImportNodes = buildRuntimeImportNodes(ctx, 'octane/server', ctx._moduleOrigin);
10866
11511
  const helperNodes = hoistedHelperNodes(ctx);
10867
11512
  const program = {
10868
11513
  type: 'Program',
10869
11514
  sourceType: 'module',
10870
- body: [...runtimeImportNodes, ...nativeActivation, ...helperNodes, ...bodyNodes],
11515
+ body: [
11516
+ ...runtimeImportNodes,
11517
+ ...nativeActivation,
11518
+ ...signalActivation,
11519
+ ...helperNodes,
11520
+ ...bodyNodes,
11521
+ ],
10871
11522
  metadata: { path: [] },
10872
11523
  start: ast.start,
10873
11524
  end: ast.end,
@@ -10876,6 +11527,9 @@ function compileServer(
10876
11527
  const printed = printNodeWithMap(program, ctx);
10877
11528
  const result = {
10878
11529
  code: printed.code,
11530
+ ...(ast._octaneSignalDeclarations === true || ctx.nativeReads
11531
+ ? { streamedSignals: true }
11532
+ : null),
10879
11533
  map: {
10880
11534
  version: 3,
10881
11535
  sources: [ctx.mapSourceName],
@@ -10998,6 +11652,20 @@ function compileServerComponent(node, ctx) {
10998
11652
  if (componentReferencedAboveDeclaration(ctx, node, name)) {
10999
11653
  const declaration = isDefault ? b.export_default(fn) : isExported ? b.export(fn) : fn;
11000
11654
  const nodes = [inheritOriginLoc(declaration, node)];
11655
+ if (node._octaneBindingView) {
11656
+ nodes.push(
11657
+ inheritOriginLoc(
11658
+ b.stmt(
11659
+ b.call(
11660
+ requireRuntimeForContext(ctx, 'bindPresentationView'),
11661
+ b.id(name),
11662
+ b.literal(node._octaneBindingView.id),
11663
+ ),
11664
+ ),
11665
+ node,
11666
+ ),
11667
+ );
11668
+ }
11001
11669
  if (warmNode !== null) {
11002
11670
  nodes.push(
11003
11671
  inheritOriginLoc(
@@ -11015,7 +11683,19 @@ function compileServerComponent(node, ctx) {
11015
11683
  return nodes;
11016
11684
  }
11017
11685
 
11018
- const initializer = functionExpressionFromDeclaration(fn, node);
11686
+ let initializer = functionExpressionFromDeclaration(fn, node);
11687
+ if (node._octaneBindingView) {
11688
+ initializer = markPure(
11689
+ inheritOriginLoc(
11690
+ b.call(
11691
+ requireRuntimeForContext(ctx, 'bindPresentationView'),
11692
+ initializer,
11693
+ b.literal(node._octaneBindingView.id),
11694
+ ),
11695
+ node,
11696
+ ),
11697
+ );
11698
+ }
11019
11699
  const declaration = inheritOriginLoc(b.const(name, initializer), node);
11020
11700
  const nodes = [
11021
11701
  isExported && !isDefault ? inheritOriginLoc(b.export(declaration), node) : declaration,
@@ -11321,8 +12001,19 @@ function ssrCompileBodyWithMapTemps(
11321
12001
  // needs no carrier. Other bodies can cross a component/value boundary, where
11322
12002
  // the carrier distinguishes compiled HTML from authored text returns. This
11323
12003
  // flag belongs to this body only; nested callbacks keep their own contract.
11324
- if (!unboxedHtml) ctx.runtimeNeeded.add('ssrHtml');
11325
- body.push(b.return(unboxedHtml ? htmlExpr : ssrCall('ssrHtml', [htmlExpr], node)));
12004
+ const htmlHelper = node._octaneBindingView ? 'ssrBindingHtml' : 'ssrHtml';
12005
+ if (!unboxedHtml) ctx.runtimeNeeded.add(htmlHelper);
12006
+ body.push(
12007
+ b.return(
12008
+ unboxedHtml
12009
+ ? htmlExpr
12010
+ : ssrCall(
12011
+ htmlHelper,
12012
+ [htmlExpr, ...(node._octaneBindingView ? [b.literal(node._octaneBindingView.id)] : [])],
12013
+ node,
12014
+ ),
12015
+ ),
12016
+ );
11326
12017
  const origin =
11327
12018
  node.loc != null
11328
12019
  ? node
@@ -11353,10 +12044,20 @@ function ssrCompileBodyWithMapTemps(
11353
12044
  // flow — all serialize elements or `<!--[-->…<!--]-->` ranges
11354
12045
  // that break text-node adjacency on their own)
11355
12046
  function textAdjacencyKind(node, ctx) {
12047
+ if (
12048
+ node._octaneBindingText ||
12049
+ node._octaneBindingOpaque ||
12050
+ node._octaneBindingValue ||
12051
+ node._octaneBindingSlot
12052
+ )
12053
+ return 'other';
11356
12054
  if (node.type !== 'Text') return 'other';
11357
12055
  const lit = staticTextLiteral(node.expression);
11358
12056
  if (lit !== null) return lit === '' ? 'empty' : 'static';
11359
- return isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals) ? 'dyn' : 'other';
12057
+ return isDirectSignalHandleExpression(node.expression) ||
12058
+ isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)
12059
+ ? 'dyn'
12060
+ : 'other';
11360
12061
  }
11361
12062
 
11362
12063
  // Does the child at `i` have a text-producing sibling next to it (looking
@@ -11622,10 +12323,17 @@ function ssrEmitNode(
11622
12323
  componentNs,
11623
12324
  nlGuard = false,
11624
12325
  ) {
12326
+ if (node._octaneBindingValue || node._octaneBindingSlot)
12327
+ return ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
11625
12328
  switch (node.type) {
11626
12329
  case 'Text': {
11627
12330
  const expr = node.expression;
11628
- if (expr && expr.type === 'Literal' && typeof expr.value === 'string') {
12331
+ if (
12332
+ !node._octaneBindingText &&
12333
+ expr &&
12334
+ expr.type === 'Literal' &&
12335
+ typeof expr.value === 'string'
12336
+ ) {
11629
12337
  if (ctx.dev && ctx._ssrNestingText === true) {
11630
12338
  ctx.runtimeNeeded.add('ssrNestingText');
11631
12339
  return ssrCall(
@@ -11663,30 +12371,44 @@ function ssrEmitNode(
11663
12371
  // primitive to text) — mirrors Ripple's `{expr}` vs `{expr as string}`.
11664
12372
  // rewriteHookCalls: a `use(thenable)` in this hole bypasses the setup
11665
12373
  // rewrite, so key it here too (else it collides with sibling/nested use()).
11666
- if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
12374
+ if (
12375
+ isDirectSignalHandleExpression(expr) ||
12376
+ isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
12377
+ ) {
11667
12378
  // ssrTextPre = ssrText + the runtime leading-'\n' protection (the value
11668
12379
  // isn't known at compile time here).
11669
12380
  const fn =
11670
12381
  ctx.dev && ctx._ssrNestingText === true
11671
12382
  ? 'ssrNestingText'
11672
- : nlGuard
12383
+ : nlGuard && !node._octaneBindingText
11673
12384
  ? 'ssrTextPre'
11674
12385
  : 'ssrText';
11675
12386
  ctx.runtimeNeeded.add(fn);
11676
- return ssrCall(fn, [resolveStyleExpr(rewriteHookCalls(expr, ctx, name), cssHash)], node);
12387
+ const value = resolveStyleExpr(rewriteHookCalls(expr, ctx, name), cssHash);
12388
+ const text = ssrCall(fn, [ssrSignalValue(value, ctx, node, true)], node);
12389
+ return node._octaneBindingText
12390
+ ? ssrBindingRange(text, node._octaneBindingText, 't', ctx, node)
12391
+ : text;
11677
12392
  }
11678
12393
  // rewriteJsxValues lowers any JSX embedded in the expression (e.g.
11679
12394
  // `{cond && <div/>}`, a ternary, a `.map(x => <Row/>)`) to printable
11680
12395
  // createElement(...) descriptors — exactly like ssrEmitTsrxExpression and
11681
12396
  // the client makeChildCall. Without it the raw JSX leaks into the emitted
11682
12397
  // ssrChild(...) call as unparseable source.
11683
- const childHelper = nlGuard ? 'ssrChildPre' : 'ssrChild';
12398
+ const childHelper = node._octaneBindingOpaque
12399
+ ? 'ssrBindingChild'
12400
+ : nlGuard
12401
+ ? 'ssrChildPre'
12402
+ : 'ssrChild';
11684
12403
  ctx.runtimeNeeded.add(childHelper);
11685
12404
  const childExpr = ssrCall(
11686
12405
  childHelper,
11687
12406
  [
11688
12407
  resolveStyleExpr(rewriteJsxValues(rewriteHookCalls(expr, ctx, name), ctx), cssHash),
11689
12408
  b.id('__s'),
12409
+ ...(node._octaneBindingOpaque
12410
+ ? [b.literal(bindingMarker(node._octaneBindingOpaque, 'o'))]
12411
+ : []),
11690
12412
  ],
11691
12413
  node,
11692
12414
  );
@@ -11767,7 +12489,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11767
12489
  rejectVoidElementContent(tag, node, ctx);
11768
12490
  rejectTextareaValueChildren(tag, node, ctx);
11769
12491
  rejectDangerouslySetInnerHTMLChildren(tag, node, ctx);
11770
- const attrs = node.attributes || node.openingElement?.attributes || [];
12492
+ const attrs = expandKnownAttributeSpreads(
12493
+ node.attributes || node.openingElement?.attributes || [],
12494
+ );
11771
12495
  const devFormActionAttribute =
11772
12496
  ctx.dev && (tag === 'form' || tag === 'button' || tag === 'input')
11773
12497
  ? attrs.find(
@@ -11867,8 +12591,35 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11867
12591
  else directAttributeIdentities.add(identity);
11868
12592
  }
11869
12593
  const firstSpreadIdx = attrs.findIndex(
11870
- (a) => a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute',
12594
+ (a) => !a.knownSource && (a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute'),
11871
12595
  );
12596
+ const hostSignalSite = directSignalSite(ctx, node, 'binding');
12597
+ let directSignalControlSite = null;
12598
+ const markDirectControl = (expression, origin) => {
12599
+ if (!canCarryDirectSignalHandle(expression)) return expression;
12600
+ const site = firstSpreadIdx === -1 ? directSignalSite(ctx, node, 'input') : hostSignalSite;
12601
+ if (directSignalControlSite === null) {
12602
+ if (directAttributeIdentities.has('data-octane-input')) {
12603
+ throw new Error(
12604
+ '`data-octane-input` is reserved for compiler-owned writable signal controls.',
12605
+ );
12606
+ }
12607
+ directSignalControlSite = site;
12608
+ bakeLit(` data-octane-input="${escapeAttr(site)}"`, 'data-octane-input', null, null);
12609
+ }
12610
+ ctx.signalBindingsUsed = true;
12611
+ ctx.runtimeNeeded.add('ssrSignalControlValue');
12612
+ return ssrCall('ssrSignalControlValue', [expression, b.literal(site)], origin);
12613
+ };
12614
+ if (firstSpreadIdx !== -1 && (tag === 'input' || tag === 'textarea' || tag === 'select')) {
12615
+ directSignalControlSite = hostSignalSite;
12616
+ if (directAttributeIdentities.has('data-octane-input')) {
12617
+ throw new Error(
12618
+ '`data-octane-input` is reserved for compiler-owned writable signal controls.',
12619
+ );
12620
+ }
12621
+ bakeLit(` data-octane-input="${escapeAttr(hostSignalSite)}"`, 'data-octane-input', null, null);
12622
+ }
11872
12623
  // Customized built-ins suppress native unknown-property diagnostics based on
11873
12624
  // the final `is` prop, regardless of where that prop appears in JSX. Resolve
11874
12625
  // directly authored DEV hosts through the existing whole-element attribute
@@ -12023,13 +12774,40 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12023
12774
  };
12024
12775
 
12025
12776
  for (let attrI = 0; attrI < attrs.length; attrI++) {
12026
- const attr = attrs[attrI];
12777
+ let attr = attrs[attrI];
12778
+ if (attr.knownSource) {
12779
+ attr.knownSource.expression = bindAttributeEvaluation(
12780
+ tsrxExprNode(attr.argument, ctx, name, inlinedSubs),
12781
+ );
12782
+ continue;
12783
+ }
12784
+ if (attr.value?.type === 'JSXExpressionContainer') {
12785
+ const expression = resolveKnownAttributeSource(
12786
+ attr.value.expression,
12787
+ (source) => source.expression,
12788
+ );
12789
+ if (expression !== attr.value.expression)
12790
+ attr = { ...attr, value: { ...attr.value, expression } };
12791
+ }
12027
12792
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
12028
12793
  ensureAttrPart();
12794
+ ctx.signalBindingsUsed = true;
12029
12795
  ctx.runtimeNeeded.add('ssrAttrs');
12030
12796
  ctx.runtimeNeeded.add('ssrSnapshotSpread');
12031
12797
  const tmp = bindAttributeEvaluation(
12032
- ssrCall('ssrSnapshotSpread', [tsrxExprNode(attr.argument, ctx, name, inlinedSubs)], attr),
12798
+ ssrCall(
12799
+ 'ssrSnapshotSpread',
12800
+ [
12801
+ tsrxExprNode(attr.argument, ctx, name, inlinedSubs),
12802
+ ...(tag === 'input' || tag === 'textarea' || tag === 'select'
12803
+ ? [b.literal(hostSignalSite)]
12804
+ : ctx.nativeReads
12805
+ ? [undefinedNode()]
12806
+ : []),
12807
+ ...(ctx.nativeReads ? [b.literal(true)] : []),
12808
+ ],
12809
+ attr,
12810
+ ),
12033
12811
  );
12034
12812
  if (resolveFormControlsAcrossSpreads) {
12035
12813
  if (tag !== 'textarea' && formControlPart === -1) {
@@ -12072,6 +12850,29 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12072
12850
  }
12073
12851
  if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') continue;
12074
12852
  const rawAttrName = jsxAttrRawName(attr);
12853
+ if (attr._octaneBindingClassGroups) {
12854
+ const groups = attr._octaneBindingClassGroups;
12855
+ ctx.runtimeNeeded.add('ssrBindingClass');
12856
+ flush();
12857
+ parts.push(
12858
+ ssrCall(
12859
+ 'ssrBindingClass',
12860
+ [
12861
+ b.literal(groups.receipt),
12862
+ bindAttributeEvaluation(
12863
+ tsrxExprNode(
12864
+ b.array([b.array(groups.baseline), b.array(groups.groups)]),
12865
+ ctx,
12866
+ name,
12867
+ inlinedSubs,
12868
+ ),
12869
+ ),
12870
+ ],
12871
+ attr,
12872
+ ),
12873
+ );
12874
+ continue;
12875
+ }
12075
12876
  if (rawAttrName === 'key') {
12076
12877
  bindDiscardedAttributeValue(attr.value);
12077
12878
  continue;
@@ -12143,10 +12944,13 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12143
12944
  const ctlInner =
12144
12945
  val == null ? null : val.type === 'JSXExpressionContainer' ? val.expression : val;
12145
12946
  if (tag === 'input') {
12146
- const ctlExpr =
12947
+ let ctlExpr =
12147
12948
  ctlInner === null
12148
12949
  ? inheritOriginLoc(b.literal(true, 'true'), attr)
12149
12950
  : tsrxExprNode(ctlInner, ctx, name, inlinedSubs);
12951
+ if (attrName === 'value' || attrName === 'checked') {
12952
+ ctlExpr = markDirectControl(ctlExpr, attr);
12953
+ }
12150
12954
  if (formControlPart === -1) {
12151
12955
  flush();
12152
12956
  formControlPart = parts.length;
@@ -12161,11 +12965,12 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12161
12965
  }
12162
12966
  // textarea / select: value/defaultValue never serialize as attributes —
12163
12967
  // captured for the content position (textarea) / projection scope (select).
12164
- const ctlExpr = bindAttributeEvaluation(
12968
+ let ctlExpr =
12165
12969
  ctlInner === null
12166
12970
  ? inheritOriginLoc(b.literal(true, 'true'), attr)
12167
- : tsrxExprNode(ctlInner, ctx, name, inlinedSubs),
12168
- );
12971
+ : tsrxExprNode(ctlInner, ctx, name, inlinedSubs);
12972
+ if (attrName === 'value') ctlExpr = markDirectControl(ctlExpr, attr);
12973
+ ctlExpr = bindAttributeEvaluation(ctlExpr);
12169
12974
  if (resolveFormControlsAcrossSpreads) {
12170
12975
  if (tag === 'select' && formControlPart === -1) {
12171
12976
  flush();
@@ -12174,6 +12979,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12174
12979
  }
12175
12980
  formControlSources.push(ssrDirectSource(ctx, attrName, ctlExpr, attr));
12176
12981
  } else if (attrName === 'value') {
12982
+ if (directSignalControlSite !== null) {
12983
+ formControlSources.push(ssrDirectSource(ctx, attrName, ctlExpr, attr));
12984
+ }
12177
12985
  ctlValue = ctlExpr;
12178
12986
  ctlValueAttr = attr;
12179
12987
  } else {
@@ -12297,6 +13105,8 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12297
13105
  cssHash,
12298
13106
  );
12299
13107
  attrExpr = tsrxExprNode(attrInner, ctx, name, inlinedSubs);
13108
+ if (!(ctx.nativeReads && attrName === 'style'))
13109
+ attrExpr = ssrSignalValue(attrExpr, ctx, attr, true);
12300
13110
  }
12301
13111
  attrSources.push(
12302
13112
  ssrDirectSource(
@@ -12338,6 +13148,13 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12338
13148
  let inner = val.type === 'JSXExpressionContainer' ? val.expression : val;
12339
13149
 
12340
13150
  if (attrName === 'style') {
13151
+ if (!ctx.nativeReads && isDirectSignalHandleExpression(inner)) {
13152
+ throw new Error(
13153
+ 'Direct signal style objects cannot install property-targeted bindings. ' +
13154
+ 'Use fixed style properties such as `style={{ color: color$ }}`, or use `.get()` ' +
13155
+ 'for a one-way whole-style value.',
13156
+ );
13157
+ }
12341
13158
  inner = resolveStyleExpr(inner, cssHash);
12342
13159
  if (!isAfterSpread && inner.type === 'Literal' && typeof inner.value === 'string') {
12343
13160
  bakeLit(` style="${escapeAttr(inner.value)}"`, attrName, attr.name, inner);
@@ -12356,15 +13173,11 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12356
13173
  flush();
12357
13174
  ctx.runtimeNeeded.add('ssrStyle');
12358
13175
  registerAttrLoweringOrigin(ctx, attr.name, 'ssrStyle', null);
13176
+ inner = tsrxExprNode(inner, ctx, name, inlinedSubs);
12359
13177
  if (ctx.nativeReads)
12360
13178
  inner = b.call(requireRuntimeForContext(ctx, 'readNativeDomStyle'), inner);
12361
- parts.push(
12362
- ssrCall(
12363
- 'ssrStyle',
12364
- [bindAttributeEvaluation(tsrxExprNode(inner, ctx, name, inlinedSubs))],
12365
- attr,
12366
- ),
12367
- );
13179
+ else inner = ssrSignalStyleObject(inner, ctx, attr);
13180
+ parts.push(ssrCall('ssrStyle', [bindAttributeEvaluation(inner)], attr));
12368
13181
  continue;
12369
13182
  }
12370
13183
 
@@ -12446,7 +13259,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12446
13259
  flush();
12447
13260
  ctx.runtimeNeeded.add('ssrAttr');
12448
13261
  registerAttrLoweringOrigin(ctx, attr.name, 'ssrAttr', attrName);
12449
- const valueExpr = bindAttributeEvaluation(tsrxExprNode(inner, ctx, name, inlinedSubs));
13262
+ const valueExpr = bindAttributeEvaluation(
13263
+ ssrSignalValue(tsrxExprNode(inner, ctx, name, inlinedSubs), ctx, attr, true),
13264
+ );
12450
13265
  if (
12451
13266
  devFormActionSources !== null &&
12452
13267
  (attrName === 'type' ||
@@ -12474,10 +13289,18 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12474
13289
  );
12475
13290
  parts.push(singleDirectAttribute);
12476
13291
  }
13292
+ let signalControlAttrs = null;
13293
+ if (directSignalControlSite !== null) {
13294
+ ctx.runtimeNeeded.add('ssrSignalControlAttrs');
13295
+ signalControlAttrs = ssrCall('ssrSignalControlAttrs', [b.array(formControlSources)], node);
13296
+ }
12477
13297
  if (formControlPart !== -1) {
12478
13298
  if (tag === 'input') {
12479
13299
  ctx.runtimeNeeded.add('ssrInputAttrs');
12480
- const inputAttrs = ssrCall('ssrInputAttrs', [b.array(formControlSources)], node);
13300
+ let inputAttrs = ssrCall('ssrInputAttrs', [b.array(formControlSources)], node);
13301
+ if (signalControlAttrs !== null) {
13302
+ inputAttrs = inheritOriginLoc(b.binary('+', inputAttrs, signalControlAttrs), node);
13303
+ }
12481
13304
  // React serializes/coerces ordinary attributes before projecting the
12482
13305
  // effective checked/value state. Expressions and spread getters have
12483
13306
  // already run into temps in authored order; move only this serialization
@@ -12488,8 +13311,18 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12488
13311
  parts.push(inputAttrs);
12489
13312
  } else {
12490
13313
  ctx.runtimeNeeded.add('ssrSelectAttrs');
12491
- parts[formControlPart] = ssrCall('ssrSelectAttrs', [b.array(formControlSources)], node);
12492
- }
13314
+ const selectAttrs = ssrCall('ssrSelectAttrs', [b.array(formControlSources)], node);
13315
+ parts[formControlPart] =
13316
+ signalControlAttrs === null
13317
+ ? selectAttrs
13318
+ : inheritOriginLoc(b.binary('+', selectAttrs, signalControlAttrs), node);
13319
+ }
13320
+ } else if (signalControlAttrs !== null) {
13321
+ // Textarea values live in content; a direct select value lives in its
13322
+ // option scope. Neither needs a form-attribute slot, but both still need
13323
+ // the winning writable identity on the opening tag for early edits.
13324
+ flush();
13325
+ parts.push(signalControlAttrs);
12493
13326
  }
12494
13327
  if (attrPart !== -1) {
12495
13328
  ctx.runtimeNeeded.add('ssrAttrs');
@@ -12589,6 +13422,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12589
13422
  } else if (
12590
13423
  htmlSources.length === 0 &&
12591
13424
  onlyChild0 !== null &&
13425
+ !onlyChild0._octaneBindingOpaque &&
13426
+ !onlyChild0._octaneBindingValue &&
13427
+ !onlyChild0._octaneBindingSlot &&
12592
13428
  staticTextLiteral(onlyChild0.expression) === null &&
12593
13429
  !isKnownTextChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
12594
13430
  ) {
@@ -12808,11 +13644,12 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12808
13644
 
12809
13645
  function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
12810
13646
  registerOriginAlias(ctx, node.closingElement?.name, node.id || node.openingElement?.name);
13647
+ const invocationSite = componentInvocationSite(ctx, node);
12811
13648
  // M3 inherit-range: consume the body-root flag ONCE, before this component's
12812
13649
  // props/children compile below (they recurse into ssrEmitNodes/ssrCompileSub
12813
13650
  // and must not inherit it). Set by ssrCompileBody only for the sole
12814
13651
  // comp-call root of a `@{}` body — which is exactly this emit.
12815
- const inherit = ctx._ssrInheritRoot === true;
13652
+ const inherit = ctx._ssrInheritRoot === true && !node._octaneBindingSite;
12816
13653
  ctx._ssrInheritRoot = false;
12817
13654
  // Capture before compiling attributes/children: nested subs temporarily mutate
12818
13655
  // this context flag. Only this component's immediate children sub inherits the
@@ -12967,11 +13804,11 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
12967
13804
  // The direct mode-only Activity was lowered to ActivityStatement. Richer
12968
13805
  // Activity configs and generic explicit-key + spread sites keep one ordered
12969
13806
  // element config, matching createElement without changing simple call sites.
12970
- ctx.runtimeNeeded.add('createElement');
13807
+ ctx.runtimeNeeded.add('createElementFromConfig');
12971
13808
  ctx.runtimeNeeded.add('ssrChild');
12972
13809
  const descriptor = ssrCall(
12973
- 'createElement',
12974
- [compExpr, inheritOriginLoc(b.object(propNodes), node)],
13810
+ 'createElementFromConfig',
13811
+ [b.literal(invocationSite), compExpr, inheritOriginLoc(b.object(propNodes), node)],
12975
13812
  node,
12976
13813
  );
12977
13814
  const child = ssrCall('ssrChild', [descriptor, b.id('__s')], node);
@@ -12991,16 +13828,38 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
12991
13828
  args.push(
12992
13829
  b.literal(componentNs, JSON.stringify(componentNs)),
12993
13830
  b.literal(inherit, inherit ? 'true' : 'false'),
13831
+ keyExpr ?? undefinedNode(),
13832
+ b.literal(invocationSite),
13833
+ );
13834
+ } else {
13835
+ args.push(
13836
+ b.literal(inherit, inherit ? 'true' : 'false'),
13837
+ keyExpr ?? undefinedNode(),
13838
+ undefinedNode(),
13839
+ b.literal(invocationSite),
12994
13840
  );
12995
- if (keyExpr !== null) args.push(keyExpr);
12996
- } else if (keyExpr !== null) {
12997
- args.push(b.literal(inherit, inherit ? 'true' : 'false'), keyExpr);
12998
- } else if (inherit) {
12999
- args.push(b.literal(true, 'true'));
13841
+ }
13842
+ if (node._octaneBindingSite) {
13843
+ args.push(b.literal(bindingMarker(node._octaneBindingSite, 'v')));
13000
13844
  }
13001
13845
  return ssrCall(helper, args, node);
13002
13846
  }
13003
13847
 
13848
+ function bindingMarker(site, kind) {
13849
+ return '[b;' + site.id + ';' + site.site + ';' + kind;
13850
+ }
13851
+
13852
+ function bindingChildMarker(node) {
13853
+ return node._octaneBindingSlot
13854
+ ? bindingMarker(node._octaneBindingSlot, 's')
13855
+ : bindingMarker(node._octaneBindingValue, 't');
13856
+ }
13857
+
13858
+ function ssrBindingRange(content, site, kind, ctx, origin) {
13859
+ ctx.runtimeNeeded.add('ssrBindingBlock');
13860
+ return ssrCall('ssrBindingBlock', [content, b.literal(bindingMarker(site, kind))], origin);
13861
+ }
13862
+
13004
13863
  // ---------------------------------------------------------------------------
13005
13864
  // Server control flow — @if/@for/@switch/@try lowered to HTML-string builders.
13006
13865
  // Each branch/item/case body is compiled (via ssrCompileSub) into a server
@@ -13201,6 +14060,29 @@ function ssrEmitIf(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs)
13201
14060
  node.alternate,
13202
14061
  )
13203
14062
  : ssrHtmlTemplate([], node, ctx);
14063
+ if (node._octaneBindingSite) {
14064
+ return ssrCall(
14065
+ 'ssrControl',
14066
+ [
14067
+ b.literal(ssrControlKey('if', node)),
14068
+ ssrThunk(
14069
+ b.conditional(
14070
+ testExpr,
14071
+ ssrBindingRange(thenInner, node._octaneBindingSite, thenSub ? '0' : '-1', ctx, node),
14072
+ ssrBindingRange(
14073
+ elseInner,
14074
+ node._octaneBindingSite,
14075
+ node.alternate ? '1' : '-1',
14076
+ ctx,
14077
+ node,
14078
+ ),
14079
+ ),
14080
+ node,
14081
+ ),
14082
+ ],
14083
+ node,
14084
+ );
14085
+ }
13204
14086
  return ssrCall(
13205
14087
  'ssrBlock',
13206
14088
  [
@@ -13266,6 +14148,7 @@ function ssrEmitActivity(node, ctx, name, inlinedSubs, parentNs, cssHash, compon
13266
14148
  }
13267
14149
 
13268
14150
  function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
14151
+ const bindingSite = node._octaneBindingSite;
13269
14152
  // rewriteHookCalls: key any `use(thenable)` in the @for iterable expression.
13270
14153
  const itemsExpr = rewriteHookCalls(node.right, ctx, name);
13271
14154
  const mapCall = node.nativeArrayMap || null;
@@ -13369,22 +14252,37 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13369
14252
  node.index ? [b.id('__it'), b.id('__i')] : [b.id('__it')],
13370
14253
  node,
13371
14254
  );
13372
- const sharedItemRange = markerlessItem || canShareSsrComponentItemRange(node, ctx);
13373
- const itemHtml = sharedItemRange ? itemCall : ssrCall('ssrBlock', [itemCall], node);
13374
- const renderItem = itemNeedsIdentity
14255
+ const sharedItemRange =
14256
+ !bindingSite && (markerlessItem || canShareSsrComponentItemRange(node, ctx));
14257
+ const itemHtml =
14258
+ sharedItemRange || bindingSite ? itemCall : ssrCall('ssrBlock', [itemCall], node);
14259
+ let renderItem = itemNeedsIdentity
13375
14260
  ? ssrCall(
13376
14261
  'ssrForItem',
13377
14262
  [
13378
- itemKey,
14263
+ bindingSite ? b.id('__bindingKeyValue') : itemKey,
13379
14264
  b.id(itemSub.fnName),
13380
14265
  b.id('__it'),
13381
14266
  node.index ? b.id('__i') : ssrVoid(node),
13382
14267
  b.id('__s'),
13383
- b.literal(!sharedItemRange),
14268
+ b.literal(!sharedItemRange && !bindingSite),
14269
+ ...(mapCall === null ? [] : [b.literal(true)]),
13384
14270
  ],
13385
14271
  node,
13386
14272
  )
13387
14273
  : itemHtml;
14274
+ if (bindingSite) {
14275
+ ctx.runtimeNeeded.add('ssrBindingBlock');
14276
+ ctx.runtimeNeeded.add('ssrBindingKey');
14277
+ renderItem = ssrCall(
14278
+ 'ssrBindingBlock',
14279
+ [
14280
+ renderItem,
14281
+ b.binary('+', b.literal(bindingMarker(bindingSite, 'k;')), b.id('__bindingKey')),
14282
+ ],
14283
+ node,
14284
+ );
14285
+ }
13388
14286
  if (node.empty) ctx.runtimeNeeded.add('ssrArm');
13389
14287
  if (itemNeedsIdentity) ctx.runtimeNeeded.add('ssrForItem');
13390
14288
  // Render every item into one incrementally-built string. Avoid map().join():
@@ -13398,8 +14296,30 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13398
14296
  const index = b.id('__i');
13399
14297
  const loopBody = b.block([
13400
14298
  b.const('__it', b.member(items, index, true)),
14299
+ ...(bindingSite
14300
+ ? [
14301
+ b.const('__bindingKeyValue', itemKey),
14302
+ b.const(
14303
+ '__bindingKey',
14304
+ ssrCall('ssrBindingKey', [b.id('__bindingKeyValue'), b.id('__bindingKeys')], node),
14305
+ ),
14306
+ ]
14307
+ : []),
13401
14308
  b.stmt(b.assignment('+=', b.id('__html'), renderItem)),
13402
14309
  ]);
14310
+ const forRange = (content, hasItems) =>
14311
+ bindingSite
14312
+ ? ssrCall(
14313
+ 'ssrBindingBlock',
14314
+ [
14315
+ content,
14316
+ b.literal(
14317
+ '[f' + (hasItems ? '1' : '0') + ';b;' + bindingSite.id + ';' + bindingSite.site,
14318
+ ),
14319
+ ],
14320
+ node,
14321
+ )
14322
+ : ssrCall('ssrForBlock', [content, b.literal(hasItems)], node);
13403
14323
  const render = b.arrow(
13404
14324
  [],
13405
14325
  b.block([
@@ -13411,10 +14331,11 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13411
14331
  ),
13412
14332
  b.if(
13413
14333
  b.binary('===', b.member(items, 'length'), b.literal(0)),
13414
- b.return(ssrCall('ssrForBlock', [emptyCall, b.literal(false, 'false')], node)),
14334
+ b.return(forRange(emptyCall, false)),
13415
14335
  null,
13416
14336
  ),
13417
- ...(itemNeedsIdentity && keyDeclaration !== null ? [keyDeclaration] : []),
14337
+ ...((itemNeedsIdentity || bindingSite) && keyDeclaration !== null ? [keyDeclaration] : []),
14338
+ ...(bindingSite ? [b.const('__bindingKeys', b.new(b.id('Set'), node))] : []),
13418
14339
  b.let('__html', b.literal('')),
13419
14340
  b.for(
13420
14341
  b.let('__i', b.literal(0)),
@@ -13422,7 +14343,7 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13422
14343
  b.update('++', index),
13423
14344
  loopBody,
13424
14345
  ),
13425
- b.return(ssrCall('ssrForBlock', [b.id('__html'), b.literal(true, 'true')], node)),
14346
+ b.return(forRange(b.id('__html'), true)),
13426
14347
  ]),
13427
14348
  );
13428
14349
  const fast = ssrCall('ssrControl', [b.literal(ssrControlKey('for', node)), render], node);
@@ -13632,6 +14553,33 @@ function ssrControlKey(kind, node) {
13632
14553
  // renderer component so its template scopes and hydration range both survive.
13633
14554
  function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
13634
14555
  const expr = node.expression;
14556
+ if (node._octaneBindingOpaque || node._octaneBindingValue || node._octaneBindingSlot) {
14557
+ ctx.runtimeNeeded.add('ssrBindingChild');
14558
+ const child = ssrCall(
14559
+ 'ssrBindingChild',
14560
+ [
14561
+ resolveStyleExpr(rewriteJsxValues(rewriteHookCalls(expr, ctx, name), ctx), cssHash),
14562
+ b.id('__s'),
14563
+ b.literal(
14564
+ node._octaneBindingValue || node._octaneBindingSlot
14565
+ ? bindingChildMarker(node)
14566
+ : bindingMarker(node._octaneBindingOpaque, 'o'),
14567
+ ),
14568
+ ],
14569
+ node,
14570
+ );
14571
+ if (componentNs === null) return child;
14572
+ ctx.runtimeNeeded.add('ssrInNamespace');
14573
+ return ssrCall('ssrInNamespace', [b.literal(componentNs), ssrThunk(child, node)], node);
14574
+ }
14575
+ if (isDirectSignalHandleExpression(expr)) {
14576
+ ctx.runtimeNeeded.add('ssrText');
14577
+ return ssrCall(
14578
+ 'ssrText',
14579
+ [ssrSignalValue(resolveStyleExpr(rewriteHookCalls(expr, ctx, name), cssHash), ctx, node)],
14580
+ node,
14581
+ );
14582
+ }
13635
14583
  if (node.returnedJsxValue === true && requiresTemplateNormalization(expr, 'html', true, ctx)) {
13636
14584
  // A returned JSX value that contains template-only syntax is one compiled
13637
14585
  // renderer on the client (rather than a descriptor array whose value
@@ -13656,7 +14604,19 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
13656
14604
  );
13657
14605
  inlinedSubs.push(sub.fn);
13658
14606
  ctx.runtimeNeeded.add('ssrComponent');
13659
- return ssrCall('ssrComponent', [b.id('__s'), b.id(sub.fnName), b.object([])], node);
14607
+ return ssrCall(
14608
+ 'ssrComponent',
14609
+ [
14610
+ b.id('__s'),
14611
+ b.id(sub.fnName),
14612
+ b.object([]),
14613
+ b.literal(false),
14614
+ undefinedNode(),
14615
+ undefinedNode(),
14616
+ b.literal(componentInvocationSite(ctx, node)),
14617
+ ],
14618
+ node,
14619
+ );
13660
14620
  }
13661
14621
  if (
13662
14622
  expr &&
@@ -13686,10 +14646,7 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
13686
14646
  // position — body/arm/fragment roots and component-children roots,
13687
14647
  // where a rich hole rides the de-opt value path because a portal there
13688
14648
  // has no host to stamp (`_ssrHostChildPos`).
13689
- const asIf = {
13690
- ...b.if(expr.test, wrapAsBlockStmt(expr.consequent), wrapAsBlockStmt(expr.alternate)),
13691
- loc: expr.loc, // same devLoc/control-key position as the client's claim
13692
- };
14649
+ const asIf = conditionalJsxStatement(expr);
13693
14650
  return ssrEmitIf(asIf, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
13694
14651
  }
13695
14652
  ctx.runtimeNeeded.add('ssrChild');
@@ -14162,6 +15119,17 @@ function compileComponent(node, ctx, options) {
14162
15119
  // back to a full reload — the capture predates the wrapper).
14163
15120
  statements.push(guarded(b.assignment('=', b.id(name), b.call('_$hmr', b.id(name)))));
14164
15121
  }
15122
+ if (node._octaneBindingView) {
15123
+ statements.push(
15124
+ guarded(
15125
+ b.call(
15126
+ requireRuntimeForContext(ctx, 'bindPresentationView'),
15127
+ b.id(name),
15128
+ b.literal(node._octaneBindingView.id),
15129
+ ),
15130
+ ),
15131
+ );
15132
+ }
14165
15133
  return { nodes: statements };
14166
15134
  }
14167
15135
 
@@ -14191,6 +15159,18 @@ function compileComponent(node, ctx, options) {
14191
15159
  // its fresh body to the previous canonical wrapper and re-export that same
14192
15160
  // identity. Production components retain their tree-shakeable const binding.
14193
15161
  let valueExpr = hmrWrap && isExported ? inheritOriginLoc(b.call('_$hmr', abiFn), node) : abiFn;
15162
+ if (node._octaneBindingView) {
15163
+ valueExpr = markPure(
15164
+ inheritOriginLoc(
15165
+ b.call(
15166
+ requireRuntimeForContext(ctx, 'bindPresentationView'),
15167
+ valueExpr,
15168
+ b.literal(node._octaneBindingView.id),
15169
+ ),
15170
+ node,
15171
+ ),
15172
+ );
15173
+ }
14194
15174
  if (!hmrWrap && componentInfo?.singleRoot === true) {
14195
15175
  valueExpr = inheritOriginLoc(singleRootInitializer(ctx, valueExpr), node);
14196
15176
  componentInfo.singleRootInitialized = true;
@@ -14221,18 +15201,204 @@ function compileComponent(node, ctx, options) {
14221
15201
  return { nodes: [declNode] };
14222
15202
  }
14223
15203
 
15204
+ /** Gate explicitly eligible presentation views; ordinary codegen stays direct. */
15205
+ function preparePresentationHydration(body, node, ctx) {
15206
+ const proof = node._octanePresentationHydration;
15207
+ if (!proof || ctx.mode === 'server' || ctx._universalRuntimeUnit != null) return body;
15208
+ const writers = new Set([
15209
+ 'bindSignalAttribute',
15210
+ 'setEventHandler',
15211
+ 'setAttribute',
15212
+ 'setPlainAttribute',
15213
+ 'setURLAttribute',
15214
+ 'setStringData',
15215
+ 'setBooleanAttribute',
15216
+ 'setAriaAttribute',
15217
+ 'setClassName',
15218
+ 'setClassAttr',
15219
+ 'updateFreshClassName',
15220
+ 'updateFreshClassAttr',
15221
+ 'setBindingClass',
15222
+ 'setStyle',
15223
+ 'setStyleProperty',
15224
+ 'setStyleProperties',
15225
+ 'nativeStyleBinding',
15226
+ 'nativeProjectionBinding',
15227
+ ]);
15228
+ if (proof.structural) {
15229
+ writers.add('setText');
15230
+ writers.add('bindSignalText');
15231
+ writers.add('markDangerouslySetInnerHTMLChildren');
15232
+ }
15233
+ for (const name of [...writers]) {
15234
+ if (name.startsWith('set')) writers.add(`${name}IfChanged`);
15235
+ }
15236
+ const preparation = new Set([
15237
+ 'clone',
15238
+ 'devHtmlNesting',
15239
+ 'devEventListener',
15240
+ 'queueRefAttach',
15241
+ 'replaceRef',
15242
+ 'queueOwnRefDetach',
15243
+ 'readNativeDomStyle',
15244
+ 'readNativeDomProps',
15245
+ ]);
15246
+ if (proof.structural) {
15247
+ preparation.add('snapshotSpread');
15248
+ preparation.add('child');
15249
+ preparation.add('sibling');
15250
+ preparation.add('compilerMemoRegion');
15251
+ preparation.add('hookMemoEqual');
15252
+ preparation.add('beginNativeReadWitness');
15253
+ preparation.add('finishNativeReadWitness');
15254
+ preparation.add('replayNativeReadWitness');
15255
+ preparation.add('validateNativeReadWitness');
15256
+ preparation.add('drainFrag');
15257
+ // Registration stores the compiler-owned plan; it does not invoke it or write DOM.
15258
+ preparation.add('registerWarmPlan');
15259
+ preparation.add('presentationStructure');
15260
+ preparation.add('bindingText');
15261
+ preparation.add('bindingChildSlot');
15262
+ preparation.add('markBindingChildren');
15263
+ preparation.add('markChildrenBlock');
15264
+ }
15265
+ const helpers = new Map(
15266
+ [...ctx.runtimeNeeded].map((name) => [runtimeAliasForContext(ctx, name), name]),
15267
+ );
15268
+ let supported = proof.supported || proof.conditionalRest === true;
15269
+ const hostProofs = new Map();
15270
+ // Visit generated statements, but not authored callbacks or cleanup bodies.
15271
+ // New DOM helpers fail closed until their preparation contract is audited.
15272
+ const visit = (value, rewrite) => {
15273
+ if (!value || typeof value !== 'object') return value;
15274
+ if (Array.isArray(value)) return value.map((item) => visit(item, rewrite));
15275
+ if (!value.type || /Function/.test(value.type)) return value;
15276
+ if (value.type === 'ReturnStatement' && !value._octanePendingOutputGuard) supported = false;
15277
+ const helper =
15278
+ value.type === 'CallExpression' && value.callee.type === 'Identifier'
15279
+ ? helpers.get(value.callee.name)
15280
+ : undefined;
15281
+ const host =
15282
+ helper === 'bindSignalHostPropSources' &&
15283
+ proof.structural &&
15284
+ value._octanePresentationHostSources?.length > 0;
15285
+ if (
15286
+ helper &&
15287
+ !host &&
15288
+ !writers.has(helper) &&
15289
+ !preparation.has(helper) &&
15290
+ !/^bag(?:\d+|Of)$/.test(helper)
15291
+ )
15292
+ supported = false;
15293
+ const result = { ...value };
15294
+ for (const key of Object.keys(value)) {
15295
+ if (key !== 'loc' && key !== 'metadata') result[key] = visit(value[key], rewrite);
15296
+ }
15297
+ if (rewrite && host) {
15298
+ const records = value._octanePresentationHostSources;
15299
+ const key = JSON.stringify(records);
15300
+ let receipt = hostProofs.get(key);
15301
+ if (receipt === undefined) {
15302
+ receipt = b.id(allocCompilerName(ctx, '__presentationHostSources'));
15303
+ hostProofs.set(key, receipt);
15304
+ ctx.hoistedHelpers.push(
15305
+ inheritOriginLoc(
15306
+ b.const(
15307
+ receipt,
15308
+ b.array(
15309
+ records.map((record) =>
15310
+ b.object(
15311
+ Object.entries(record).map(([name, field]) =>
15312
+ b.prop('init', b.id(name), b.literal(field)),
15313
+ ),
15314
+ ),
15315
+ ),
15316
+ ),
15317
+ ),
15318
+ value,
15319
+ ),
15320
+ );
15321
+ }
15322
+ const args = result.arguments.slice();
15323
+ while (args.length < 7) args.push(undefinedNode());
15324
+ return inheritOriginLoc(
15325
+ b.call(
15326
+ requireRuntimeForContext(ctx, 'presentationHostWrite'),
15327
+ result.callee,
15328
+ ...args,
15329
+ receipt,
15330
+ ),
15331
+ value,
15332
+ );
15333
+ }
15334
+ return rewrite && writers.has(helper)
15335
+ ? inheritOriginLoc(
15336
+ b.call(
15337
+ requireRuntimeForContext(ctx, 'presentationWrite'),
15338
+ result.callee,
15339
+ b.literal(helper),
15340
+ ...result.arguments,
15341
+ ),
15342
+ value,
15343
+ )
15344
+ : result;
15345
+ };
15346
+ visit(body, false);
15347
+ const frame = b.id(allocCompilerName(ctx, '__presentationHydration'));
15348
+ const completed = b.id(allocCompilerName(ctx, '__presentationComplete'));
15349
+ const failure = proof.structural ? b.id(allocCompilerName(ctx, '__presentationFailure')) : null;
15350
+ const statements = supported ? visit(body, true) : body;
15351
+ let directiveEnd = 0;
15352
+ while (
15353
+ statements[directiveEnd]?.type === 'ExpressionStatement' &&
15354
+ typeof statements[directiveEnd].expression?.value === 'string'
15355
+ )
15356
+ directiveEnd++;
15357
+ const begin = b.call(
15358
+ requireRuntimeForContext(ctx, 'beginPresentationHydration'),
15359
+ b.id('__s'),
15360
+ b.literal(proof.id),
15361
+ b.literal(supported),
15362
+ ...(proof.structural ? [b.literal(true)] : []),
15363
+ ...(proof.conditionalRest ? [b.literal(true)] : []),
15364
+ );
15365
+ // A new, unsupported writer can still render normally, but must reject an
15366
+ // attempted lease before publishing anything. It never opens a frame.
15367
+ if (!supported)
15368
+ return [...statements.slice(0, directiveEnd), b.stmt(begin), ...statements.slice(directiveEnd)];
15369
+ return [
15370
+ ...statements.slice(0, directiveEnd),
15371
+ b.const(frame, begin),
15372
+ b.let(completed, b.literal(false)),
15373
+ b.try(
15374
+ b.block([
15375
+ ...statements.slice(directiveEnd),
15376
+ b.stmt(b.assignment('=', completed, b.literal(true))),
15377
+ ]),
15378
+ failure === null
15379
+ ? null
15380
+ : b.catch_clause(
15381
+ failure,
15382
+ null,
15383
+ b.block([
15384
+ b.stmt(b.call(requireRuntimeForContext(ctx, 'presentationFailure'), failure)),
15385
+ ]),
15386
+ ),
15387
+ b.block([
15388
+ b.stmt(b.call(requireRuntimeForContext(ctx, 'endPresentationHydration'), frame, completed)),
15389
+ ]),
15390
+ ),
15391
+ ];
15392
+ }
15393
+
14224
15394
  /**
14225
- * Generate just the `function (...) { ... }` text for a component-shaped node.
14226
- * Used both for top-level components and for inlined for-of item bodies.
14227
- *
14228
- * `parentNs` is the namespace this body's JSX is rendered into. Top-level
14229
- * components use 'opaque' because their call site can select HTML, SVG, or
14230
- * MathML; an if/for/try body inherits its known host namespace.
14231
- *
14232
- * `cssHash` is the enclosing component's scoped-style hash (or null) — used to
14233
- * resolve `{style ('cls')}` expressions to "<hash> cls" strings.
15395
+ * Generate the function AST for a top-level component or inlined item body.
15396
+ * `parentNs` is opaque for top-level components and inherited by directive
15397
+ * bodies. `cssHash` selects the enclosing scoped-style expression fallback.
14234
15398
  */
14235
15399
  function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null, options = null) {
15400
+ const previousPresentationHydration = ctx.presentationHydration;
15401
+ ctx.presentationHydration = node._octanePresentationHydration;
14236
15402
  const returnedOutput = options?.returnedOutput === true;
14237
15403
  const previousAutoCalculatedRenderableRefs = ctx.currentAutoCalculatedRenderableRefs;
14238
15404
  let autoCalculatedDeclarations = null;
@@ -14712,7 +15878,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14712
15878
  b.member(b.member(b.id('__s'), 'block'), 'pending'),
14713
15879
  b.unary('!', b.member(b.member(b.id('__s'), 'block'), 'crossRenderUpdate')),
14714
15880
  ),
14715
- b.return(null),
15881
+ { ...b.return(null), _octanePendingOutputGuard: true },
14716
15882
  null,
14717
15883
  ),
14718
15884
  node,
@@ -14835,13 +16001,15 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14835
16001
  ctx.currentMapTemps = prevMapTemps;
14836
16002
  // ONE FunctionDeclaration node — the caller prints it (once, with the full
14837
16003
  // esrap map for top-level components) or embeds it in an enclosing body.
16004
+ const presentationBody = preparePresentationHydration(bodyStatements, node, ctx);
16005
+ ctx.presentationHydration = previousPresentationHydration;
14838
16006
  const emittedFunction = b.function_declaration(
14839
16007
  b.id(name, node.id ?? node),
14840
16008
  fnParams,
14841
16009
  b.block(
14842
16010
  ctx.nativeReads
14843
- ? wrapNativeReadScope(bodyStatements, b.id('__s'), nativeReadNames(ctx))
14844
- : bodyStatements,
16011
+ ? wrapNativeReadScope(presentationBody, b.id('__s'), nativeReadNames(ctx))
16012
+ : presentationBody,
14845
16013
  ),
14846
16014
  );
14847
16015
  return inheritOriginLoc(
@@ -18285,10 +19453,29 @@ function compileReturnJsxFunction(node, ctx, options) {
18285
19453
  // export follow-up statements — compile() embeds them in the single module
18286
19454
  // AST, whose one esrap print yields the whole module map (chained consumers
18287
19455
  // like @octanejs/mdx's two-stage .mdx map read segments off that print).
19456
+ const bindingStamp = node._octaneBindingView
19457
+ ? [
19458
+ inheritOriginLoc(
19459
+ b.stmt(
19460
+ b.logical(
19461
+ '&&',
19462
+ b.binary('===', b.unary('typeof', b.id(name)), b.literal('function')),
19463
+ b.call(
19464
+ requireRuntimeForContext(ctx, 'bindPresentationView'),
19465
+ b.id(name),
19466
+ b.literal(node._octaneBindingView.id),
19467
+ ),
19468
+ ),
19469
+ ),
19470
+ node,
19471
+ ),
19472
+ ]
19473
+ : [];
18288
19474
  if (options && options.hmrWrap) {
18289
19475
  const nodes = [
18290
19476
  fn,
18291
19477
  inheritOriginLoc(b.stmt(b.assignment('=', b.id(name), b.call('_$hmr', b.id(name)))), node),
19478
+ ...bindingStamp,
18292
19479
  ];
18293
19480
  if (options.default) {
18294
19481
  nodes.push(
@@ -18305,10 +19492,11 @@ function compileReturnJsxFunction(node, ctx, options) {
18305
19492
  return { nodes };
18306
19493
  }
18307
19494
  if (options && options.default) {
18308
- return { nodes: [fn, inheritOriginLoc(b.export_default(b.id(name)), node)] };
19495
+ return { nodes: [fn, ...bindingStamp, inheritOriginLoc(b.export_default(b.id(name)), node)] };
18309
19496
  }
18310
- if (options && options.export) return { nodes: [inheritOriginLoc(b.export(fn), node)] };
18311
- return { nodes: [fn] };
19497
+ if (options && options.export)
19498
+ return { nodes: [inheritOriginLoc(b.export(fn), node), ...bindingStamp] };
19499
+ return { nodes: [fn, ...bindingStamp] };
18312
19500
  }
18313
19501
 
18314
19502
  function nativeReturnedJsxValue(
@@ -18785,6 +19973,23 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18785
19973
  newAttrs.push(attr);
18786
19974
  continue;
18787
19975
  }
19976
+ if (attr._octaneBindingClassGroups) {
19977
+ const lift = (expression) => {
19978
+ const name = `h${holeProps.length}`;
19979
+ holeProps.push(objectProp(name, rewriteJsxValues(expression, ctx)));
19980
+ return memberProps(name, expression);
19981
+ };
19982
+ const group = attr._octaneBindingClassGroups;
19983
+ newAttrs.push({
19984
+ ...attr,
19985
+ _octaneBindingClassGroups: {
19986
+ receipt: group.receipt,
19987
+ baseline: group.baseline.map(lift),
19988
+ groups: group.groups.map(lift),
19989
+ },
19990
+ });
19991
+ continue;
19992
+ }
18788
19993
  const v = attr.value;
18789
19994
  const inner = v && v.type === 'JSXExpressionContainer' ? v.expression : v;
18790
19995
  // Dynamic attr = an expression value that isn't a static literal/string.
@@ -18853,9 +20058,10 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18853
20058
  const annotation = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18854
20059
  ? b.ts_keyword_type('string')
18855
20060
  : expr.typeAnnotation;
18856
- newChildren.push(
18857
- b.jsx_expression_container(b.ts_as(memberProps(hn, expr.expression), annotation)),
18858
- );
20061
+ newChildren.push({
20062
+ ...child,
20063
+ expression: b.ts_as(memberProps(hn, expr.expression), annotation),
20064
+ });
18859
20065
  } else {
18860
20066
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(expr, ctx, childNs)));
18861
20067
  // The renderer only sees `props.hN`, so it cannot rediscover an inline
@@ -18866,15 +20072,16 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18866
20072
  const rendered = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18867
20073
  ? b.ts_as(member, b.ts_keyword_type('string'))
18868
20074
  : member;
18869
- newChildren.push(b.jsx_expression_container(rendered));
20075
+ newChildren.push({ ...child, expression: rendered });
18870
20076
  }
18871
20077
  } else if (t === 'Element' || t === 'JSXElement') {
18872
20078
  if (isLongFormTemplateSentinel(child, childNs, true, ctx)) {
18873
20079
  newChildren.push(extractFragment(child, ctx, holeProps, childNs));
18874
20080
  } else if (
18875
20081
  isComponentTag(child) &&
18876
- ctx._foldCtx?.templateComponentChildren === true &&
18877
- requiresTemplateNormalization(child, childNs, true, ctx)
20082
+ (child._octaneBindingSite ||
20083
+ (ctx._foldCtx?.templateComponentChildren === true &&
20084
+ requiresTemplateNormalization(child, childNs, true, ctx)))
18878
20085
  ) {
18879
20086
  // Component children normally become descriptor children at return-value
18880
20087
  // position. A directive cannot be represented by lowerJsxChild, though, so
@@ -18943,7 +20150,12 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18943
20150
  // before fragment extraction. It is still an owner-side renderable hole.
18944
20151
  const hn = `h${holeProps.length}`;
18945
20152
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(child.expression, ctx, childNs)));
18946
- newChildren.push(b.jsx_expression_container(memberProps(hn, child)));
20153
+ newChildren.push({
20154
+ ...b.jsx_expression_container(memberProps(hn, child)),
20155
+ ...(child._octaneBindingOpaque ? { _octaneBindingOpaque: child._octaneBindingOpaque } : {}),
20156
+ ...(child._octaneBindingValue ? { _octaneBindingValue: child._octaneBindingValue } : {}),
20157
+ ...(child._octaneBindingSlot ? { _octaneBindingSlot: child._octaneBindingSlot } : {}),
20158
+ });
18947
20159
  } else if ((t === 'IfStatement' || t === 'JSXIfExpression') && ctx._foldCtx) {
18948
20160
  // FOLD a directive: lower its branch bodies on the COMPONENT side (so the
18949
20161
  // `__then$N`/`__else$N` helpers keep their closure over setup locals/props),
@@ -18960,7 +20172,12 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18960
20172
  inheritOriginLoc(b.if(child.test, child.consequent, child.alternate || null), child),
18961
20173
  // The clause keyword's own span — `@else` starts before its
18962
20174
  // block, so it is the only authored range that names the arm.
18963
- { alternateKeyword: child.alternateKeyword ?? null },
20175
+ {
20176
+ alternateKeyword: child.alternateKeyword ?? null,
20177
+ ...(child._octaneBindingSite
20178
+ ? { _octaneBindingSite: child._octaneBindingSite }
20179
+ : {}),
20180
+ },
18964
20181
  )
18965
20182
  : child;
18966
20183
  const ic = makeIfCall(ifNode, ctx, fc.compInlinedSubs, fc.parentNs, fc.cssHash);
@@ -19043,6 +20260,9 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
19043
20260
  index: child.index || null,
19044
20261
  empty: child.empty || null,
19045
20262
  emptyKeyword: child.emptyKeyword ?? null,
20263
+ ...(child._octaneBindingSite
20264
+ ? { _octaneBindingSite: child._octaneBindingSite }
20265
+ : {}),
19046
20266
  },
19047
20267
  )
19048
20268
  : child;
@@ -19350,9 +20570,17 @@ function lowerHostFragment(
19350
20570
  } else {
19351
20571
  ctx.hoistedHelpers.push(renderer);
19352
20572
  }
19353
- ctx.runtimeNeeded.add('createElement');
20573
+ ctx.runtimeNeeded.add('createElementFromConfig');
19354
20574
  // The fragment-renderer call maps to the authored fragment root.
19355
- return inheritOriginLoc(b.call('_$createElement', b.id(fragName), b.object(holeProps)), node);
20575
+ return inheritOriginLoc(
20576
+ b.call(
20577
+ '_$createElementFromConfig',
20578
+ b.literal(componentInvocationSite(ctx, node)),
20579
+ b.id(fragName),
20580
+ b.object(holeProps),
20581
+ ),
20582
+ node,
20583
+ );
19356
20584
  }
19357
20585
 
19358
20586
  /**
@@ -19657,7 +20885,7 @@ function serverValueDirectiveFold(
19657
20885
  directive,
19658
20886
  ),
19659
20887
  );
19660
- ctx.runtimeNeeded.add('createElement');
20888
+ ctx.runtimeNeeded.add('createElementFromConfig');
19661
20889
  // The wrapper call stands where the directive was authored, and every printed
19662
20890
  // node needs an origin (assertNodeLocs). Inside a component body the enclosing
19663
20891
  // statement supplied one; a module-level statement does not.
@@ -19669,7 +20897,12 @@ function serverValueDirectiveFold(
19669
20897
  );
19670
20898
  }
19671
20899
  return inheritOriginLoc(
19672
- b.call('_$createElement', b.id(wrapperName), b.object(descriptorProps)),
20900
+ b.call(
20901
+ '_$createElementFromConfig',
20902
+ b.literal(componentInvocationSite(ctx, directive)),
20903
+ b.id(wrapperName),
20904
+ b.object(descriptorProps),
20905
+ ),
19673
20906
  directive,
19674
20907
  );
19675
20908
  };
@@ -20210,6 +21443,7 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20210
21443
  const nameNode = node.openingElement?.name || node.id;
20211
21444
  const activity = isActivityLongForm(node, ctx);
20212
21445
  const componentTag = activity || isComponentTag(node);
21446
+ const invocationSite = componentTag ? componentInvocationSite(ctx, node) : null;
20213
21447
  // Host (lowercase) tag → string literal (`'li'`) for the de-opt renderer;
20214
21448
  // component (capitalized / member / dynamic) → the identifier/member ref.
20215
21449
  const compNode = activity
@@ -20227,6 +21461,15 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20227
21461
  const attrs = node.attributes || node.openingElement?.attributes || [];
20228
21462
  const properties = [];
20229
21463
  for (const attr of attrs) {
21464
+ if (
21465
+ !componentTag &&
21466
+ attr._octaneKnownAttributeSpread &&
21467
+ attr.type !== 'JSXSpreadAttribute' &&
21468
+ attr.type !== 'SpreadAttribute'
21469
+ )
21470
+ throw new Error(
21471
+ 'Native JSX attribute shorthands are not supported in descriptor-backed JSX values; use an owned template element.',
21472
+ );
20230
21473
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
20231
21474
  properties.push(b.spread(rewriteJsxValues(attr.argument, ctx)));
20232
21475
  continue;
@@ -20424,13 +21667,30 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20424
21667
  ? requireRuntimeForContext(ctx, 'nativeCreateScopedElement')
20425
21668
  : rtAlias('createScopedElement');
20426
21669
  const readChildren = inheritOriginLoc(b.arrow([], memoizedChildrenBody ?? childrenValue), node);
20427
- descriptor = inheritOriginLoc(b.call(scopedElement, compNode, propsNode, readChildren), node);
21670
+ descriptor = inheritOriginLoc(
21671
+ b.call(
21672
+ scopedElement,
21673
+ compNode,
21674
+ propsNode,
21675
+ readChildren,
21676
+ ...(invocationSite === null ? [] : [b.literal(invocationSite)]),
21677
+ ),
21678
+ node,
21679
+ );
20428
21680
  } else {
20429
- ctx.runtimeNeeded.add('createElement');
21681
+ ctx.runtimeNeeded.add(componentTag ? 'createElementFromConfig' : 'createElement');
20430
21682
  // Remaining scaffolding (callee, props object, spread/diagnostic wrappers,
20431
21683
  // static-content literals) maps to the authored JSX element.
20432
21684
  descriptor = inheritOriginLoc(
20433
- b.call('_$createElement', compNode, propsNode, ...loweredChildren),
21685
+ componentTag
21686
+ ? b.call(
21687
+ '_$createElementFromConfig',
21688
+ b.literal(invocationSite),
21689
+ compNode,
21690
+ propsNode,
21691
+ ...(loweredChildren.length ? [b.array(loweredChildren)] : []),
21692
+ )
21693
+ : b.call('_$createElement', compNode, propsNode, ...loweredChildren),
20434
21694
  node,
20435
21695
  );
20436
21696
  }
@@ -21074,6 +22334,14 @@ function rewriteOpaqueTitles(node, ctx, namespace = 'html') {
21074
22334
  function headAttrsExpression(el) {
21075
22335
  const attrProps = [];
21076
22336
  for (const a of el.openingElement.attributes || []) {
22337
+ if (
22338
+ a._octaneKnownAttributeSpread &&
22339
+ a.type !== 'JSXSpreadAttribute' &&
22340
+ a.type !== 'SpreadAttribute'
22341
+ )
22342
+ throw new Error(
22343
+ 'Native JSX attribute shorthands are not supported on hoisted head elements.',
22344
+ );
21077
22345
  if (a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute') {
21078
22346
  attrProps.push(b.spread(a.argument));
21079
22347
  continue;
@@ -21457,6 +22725,14 @@ function normalizeChildren(
21457
22725
  out.push({
21458
22726
  type: needsRichDispatch(expression) ? 'TSRXExpression' : 'Text',
21459
22727
  expression,
22728
+ ...(n._octaneBindingText === undefined ? {} : { _octaneBindingText: n._octaneBindingText }),
22729
+ ...(n._octaneBindingOpaque === undefined
22730
+ ? {}
22731
+ : { _octaneBindingOpaque: n._octaneBindingOpaque }),
22732
+ ...(n._octaneBindingValue === undefined
22733
+ ? {}
22734
+ : { _octaneBindingValue: n._octaneBindingValue }),
22735
+ ...(n._octaneBindingSlot === undefined ? {} : { _octaneBindingSlot: n._octaneBindingSlot }),
21460
22736
  });
21461
22737
  } else if (n.type === 'JSXElement') {
21462
22738
  // Long-form `<Fragment>…</Fragment>` (canary `enableFragmentRefs`
@@ -21573,6 +22849,7 @@ function normalizeChildren(
21573
22849
  selfClosing: n.openingElement.selfClosing,
21574
22850
  loc: n.loc, // preserve element position for dev hydration LOC (component slots)
21575
22851
  };
22852
+ if (n._octaneBindingSite !== undefined) element._octaneBindingSite = n._octaneBindingSite;
21576
22853
  // `unstable_Activity` starts with a lowercase letter but is a component
21577
22854
  // when it resolves to the builtin. Keep that fact on this rare node only.
21578
22855
  if (isActivityLongForm(n, ctx)) element.activityDescriptor = true;
@@ -21624,6 +22901,9 @@ function normalizeChildren(
21624
22901
  out.push(
21625
22902
  Object.assign(inheritOriginLoc(b.if(n.test, n.consequent, n.alternate || null), n), {
21626
22903
  alternateKeyword: n.alternateKeyword ?? null,
22904
+ ...(n._octaneBindingSite === undefined
22905
+ ? {}
22906
+ : { _octaneBindingSite: n._octaneBindingSite }),
21627
22907
  }),
21628
22908
  );
21629
22909
  } else if (n.type === 'JSXForExpression') {
@@ -21637,6 +22917,9 @@ function normalizeChildren(
21637
22917
  index: n.index || null,
21638
22918
  empty: n.empty || null,
21639
22919
  emptyKeyword: n.emptyKeyword ?? null,
22920
+ ...(n._octaneBindingSite === undefined
22921
+ ? {}
22922
+ : { _octaneBindingSite: n._octaneBindingSite }),
21640
22923
  }),
21641
22924
  );
21642
22925
  } else if (n.type === 'JSXTryExpression') {
@@ -22358,8 +23641,24 @@ function stripTsOnlyWrappers(node) {
22358
23641
  function nativeReadActivationNodes(ctx, origin) {
22359
23642
  // Install the graph-free driver before any authored root invocation, not
22360
23643
  // after parameter evaluation inside a syntactically recognized component.
22361
- // Plain data modules do not pass through this renderer compilation path.
22362
- if (!ctx.nativeReads) return [];
23644
+ // Binding-only activation modules use their selected native adapters, not
23645
+ // this renderer driver. Retain the plain-module renderer-import policy for
23646
+ // hand-authored createElement/root calls without generated runtime helpers.
23647
+ if (
23648
+ !ctx.nativeReads ||
23649
+ (ctx.runtimeNeeded.size === 0 &&
23650
+ !ctx.activityModuleAst.body.some(
23651
+ (node) =>
23652
+ node.type === 'ImportDeclaration' &&
23653
+ node.importKind !== 'type' &&
23654
+ ['octane', 'octane/server', 'octane/signals/client', 'octane/signals/server'].includes(
23655
+ node.source?.value,
23656
+ ) &&
23657
+ (node.specifiers.length === 0 ||
23658
+ node.specifiers.some((specifier) => specifier.importKind !== 'type')),
23659
+ ))
23660
+ )
23661
+ return [];
22363
23662
  return [
22364
23663
  inheritOriginLoc(
22365
23664
  b.stmt(b.call(requireRuntimeForContext(ctx, 'enableNativeReadCollection'), b.literal(1))),
@@ -22954,7 +24253,7 @@ function planJsx(
22954
24253
  // A dynamic text hole with a text-producing neighbour: the server emits a
22955
24254
  // `<!-- -->` separator between the two texts (else the parser would merge
22956
24255
  // them), and only the hole-aware sibling() walk knows to step across it.
22957
- elementBindings.some((b) => b.kind === 'text' && b.adjacentText);
24256
+ elementBindings.some((b) => b.kind === 'text' && (b.adjacentText || b.bindingMarker));
22958
24257
  if (hasHoles) {
22959
24258
  ctx.runtimeNeeded.add('child');
22960
24259
  ctx.runtimeNeeded.add('sibling');
@@ -23144,14 +24443,21 @@ function planJsx(
23144
24443
  // htext/htextSwap/setAttribute (which only have the element) can report `file:line:col`.
23145
24444
  if (_locStamped && b.kind !== 'text') stampHostLoc(elVar, b.path, undefined);
23146
24445
  else if (_locStamped) stampHostLoc(ensureVar(b.path), b.path, undefined);
23147
- if (b.kind === 'text' || b.kind === 'textOnlyChild') ctx.runtimeNeeded.add('setText');
23148
- if (b.kind === 'text') ctx.runtimeNeeded.add('htextSwap');
24446
+ const signalHelper = directSignalBindingHelper(b);
24447
+ if (signalHelper !== null) {
24448
+ ctx.runtimeNeeded.add(signalHelper);
24449
+ registerAttrLoweringOrigin(ctx, b.nameOrigin, signalHelper, b.name);
24450
+ }
24451
+ if (!b.signalDirect && (b.kind === 'text' || b.kind === 'textOnlyChild')) {
24452
+ ctx.runtimeNeeded.add('setText');
24453
+ }
24454
+ if (b.kind === 'text') ctx.runtimeNeeded.add(b.bindingMarker ? 'bindingText' : 'htextSwap');
23149
24455
  if (b.kind === 'textOnlyChild') ctx.runtimeNeeded.add('htext');
23150
24456
  // Mounts use the unconditional writer; reactive scalar bindings use either
23151
24457
  // that writer behind an inline guard or its compact comparison helper.
23152
24458
  // Claim both authored-name tokens when both paths exist, and do not retain
23153
24459
  // a mount-only import for deferred writes.
23154
- const attrHelper = attrBindingHelper(b);
24460
+ const attrHelper = b.signalDirect ? null : attrBindingHelper(b);
23155
24461
  if (attrHelper !== null) {
23156
24462
  if (b.kind === 'class' && b.fresh && (!b.deferred || (!b.mountOnly && inlineBindingGuards)))
23157
24463
  ctx.runtimeNeeded.add('normalizeClass');
@@ -23169,9 +24475,12 @@ function planJsx(
23169
24475
  if (b.kind === 'dangerCommit') ctx.runtimeNeeded.add('setDangerouslySetInnerHTMLSources');
23170
24476
  if (b.kind === 'formCommit') ctx.runtimeNeeded.add('setFormControlSources');
23171
24477
  if (b.kind === 'hostCommit') {
23172
- ctx.runtimeNeeded.add('setHostPropSources');
23173
24478
  if (ctx.nativeReads) b.readStyle = requireRuntimeForContext(ctx, 'readNativeDomStyle');
23174
- ctx.runtimeNeeded.add('queueOwnRefDetach');
24479
+ if (b.signalHostSources) ctx.runtimeNeeded.add('bindSignalHostPropSources');
24480
+ else {
24481
+ ctx.runtimeNeeded.add('setHostPropSources');
24482
+ ctx.runtimeNeeded.add('queueOwnRefDetach');
24483
+ }
23175
24484
  }
23176
24485
  // A commit-phase collector takes the element's props as one grouped call,
23177
24486
  // so each source's name literal — not the shared helper — is what claims
@@ -23206,9 +24515,12 @@ function planJsx(
23206
24515
  ctx.runtimeNeeded.add(b.spread ? 'canSplitStyleProperties' : 'isHydratingStyle');
23207
24516
  if (b.spread) ctx.runtimeNeeded.add('styleObjectPrototype');
23208
24517
  }
23209
- if (b.kind === 'nativeStyle') {
24518
+ if (b.kind === 'nativeStyle' || b.kind === 'nativeProjection') {
23210
24519
  b.slotIndex = ++nativeStyleSlots;
23211
- b.helper = requireRuntimeForContext(ctx, 'nativeStyleBinding');
24520
+ b.helper = requireRuntimeForContext(
24521
+ ctx,
24522
+ b.kind === 'nativeStyle' ? 'nativeStyleBinding' : 'nativeProjectionBinding',
24523
+ );
23212
24524
  }
23213
24525
  if (b.kind === 'spread') {
23214
24526
  ctx.runtimeNeeded.add('setSpread');
@@ -23831,18 +25143,26 @@ function planJsx(
23831
25143
  ctx.runtimeNeeded.add('ifBlock');
23832
25144
  registerDirectiveOrigin(ctx, org, ['_$ifBlock', ic.thenHelper, ic.elseHelper]);
23833
25145
  registerClauseOrigin(ctx, ic.alternateKeyword, [ic.elseHelper]);
23834
- const call = b.stmt(
23835
- b.call(
23836
- '_$ifBlock',
23837
- b.id('__s'),
23838
- b.literal(slotIndex),
23839
- hostExpr,
23840
- condition,
23841
- helperRefNode(ic.thenHelper),
23842
- inheritOriginLoc(helperRefNode(ic.elseHelper ?? 'null'), ic.alternateKeyword),
23843
- ...trailing,
23844
- ),
25146
+ let invocation = b.call(
25147
+ '_$ifBlock',
25148
+ b.id('__s'),
25149
+ b.literal(slotIndex),
25150
+ hostExpr,
25151
+ condition,
25152
+ helperRefNode(ic.thenHelper),
25153
+ inheritOriginLoc(helperRefNode(ic.elseHelper ?? 'null'), ic.alternateKeyword),
25154
+ ...trailing,
23845
25155
  );
25156
+ if (ctx.presentationHydration?.structural && ic.bindingSite) {
25157
+ invocation = b.call(
25158
+ requireRuntimeForContext(ctx, 'presentationStructure'),
25159
+ invocation.callee,
25160
+ b.literal('if'),
25161
+ b.literal(bindingMarker(ic.bindingSite, '')),
25162
+ ...invocation.arguments,
25163
+ );
25164
+ }
25165
+ const call = b.stmt(invocation);
23846
25166
  pushAfterStmt(ic.id, org, conditionDeclaration ? b.block([conditionDeclaration, call]) : call);
23847
25167
  }
23848
25168
  for (const cc of compCalls) {
@@ -23850,7 +25170,10 @@ function planJsx(
23850
25170
  const org = cc.origin ?? planOrigin;
23851
25171
  const hostExpr = () => hostNodeFor(`_compHost$${cc.id}`);
23852
25172
  if (cc.hostChildrenBinding != null) {
23853
- const props = () => bagFieldNode(bag, `_host$${cc.hostChildrenBinding.id}`);
25173
+ const props = () => {
25174
+ const host = bagFieldNode(bag, `_host$${cc.hostChildrenBinding.id}`);
25175
+ return cc.hostChildrenBinding.signalHostSources ? b.member(host, 'resolved') : host;
25176
+ };
23854
25177
  cc.valueExpr = b.conditional(
23855
25178
  propertyIsEnumerableCall(props(), 'children'),
23856
25179
  b.member(props(), 'children'),
@@ -23865,34 +25188,87 @@ function planJsx(
23865
25188
  // component calls; only the emitted runtime call differs.
23866
25189
  if (cc.isChild) {
23867
25190
  const V = () => b.id('_v');
23868
- // MARKERLESS only-child renderable: append a primitive as a single Text
23869
- // node (no `<!>`, no slot state). Either the shared helper or the inline
23870
- // row guard compares its cached value/node before the ordinary child path.
23871
- if (cc.onlyChildText && !noTemplate) {
23872
- const chp = () => bagFieldNode(bag, `_chp$${cc.id}`);
23873
- const chv = () => bagFieldNode(bag, `_chv$${cc.id}`);
23874
- // A host that can receive dangerouslySetInnerHTML must validate its
23875
- // current child on EVERY render. The ordinary primitive fast path can
23876
- // identity-skip an unchanged child while a raw-HTML writer activates in
23877
- // the same render, bypassing childTextHole's mutual-exclusion check.
23878
- if (cc.potentialDangerouslySetInnerHTML) {
23879
- ctx.runtimeNeeded.add('childTextHole');
23880
- pushAfterStmt(
23881
- cc.id,
23882
- org,
23883
- b.block([
23884
- b.const('_v', cc.valueExpr),
23885
- b.stmt(
23886
- b.assignment(
23887
- '=',
23888
- chv(),
23889
- b.call(
23890
- '_$childTextHole',
23891
- b.id('__s'),
23892
- b.literal(slotIndex),
23893
- hostExpr(),
23894
- V(),
23895
- chv(),
25191
+ const childAnchor = anchorNodeFor(cc, 'compAnchor');
25192
+ if (cc.bindingMarker) {
25193
+ ctx.runtimeNeeded.add('bindingChildSlot');
25194
+ pushAfterStmt(
25195
+ cc.id,
25196
+ org,
25197
+ b.stmt(
25198
+ b.call(
25199
+ '_$bindingChildSlot',
25200
+ b.id('__s'),
25201
+ b.literal(slotIndex),
25202
+ hostExpr(),
25203
+ cc.valueExpr,
25204
+ b.literal(cc.bindingMarker),
25205
+ childAnchor ?? b.literal(null),
25206
+ cc.anchorVar ? b.literal(true) : undefinedNode(),
25207
+ ),
25208
+ ),
25209
+ );
25210
+ continue;
25211
+ }
25212
+ if (!noTemplate && cc.signalSite != null && (ctx.signalBindingsUsed || ctx.nativeReads)) {
25213
+ ctx.signalBindingsUsed = true;
25214
+ ctx.signalBindingsEager = true;
25215
+ ctx.runtimeNeeded.add('bindSignalChild');
25216
+ const tokenKey = `_sigch$${cc.id}`;
25217
+ bag.constField(tokenKey, b.literal(null));
25218
+ pushAfterStmt(
25219
+ cc.id,
25220
+ org,
25221
+ b.stmt(
25222
+ b.assignment(
25223
+ '=',
25224
+ bagFieldNode(bag, tokenKey),
25225
+ b.call(
25226
+ '_$bindSignalChild',
25227
+ b.id('__s'),
25228
+ bagFieldNode(bag, tokenKey),
25229
+ b.literal(slotIndex),
25230
+ hostExpr(),
25231
+ cc.valueExpr,
25232
+ b.literal(cc.signalSite),
25233
+ childAnchor ?? b.literal(null),
25234
+ cc.anchorVar ? b.literal(true) : undefinedNode(),
25235
+ undefinedNode(),
25236
+ cc.coalesceRange ? b.literal(true) : undefinedNode(),
25237
+ cc.onlyChildText ? b.literal(true) : undefinedNode(),
25238
+ ),
25239
+ ),
25240
+ ),
25241
+ );
25242
+ continue;
25243
+ }
25244
+ // MARKERLESS only-child renderable: append a primitive as a single Text
25245
+ // node (no `<!>`, no slot state). Either the shared helper or the inline
25246
+ // row guard compares its cached value/node before the ordinary child path.
25247
+ if (cc.onlyChildText && !noTemplate) {
25248
+ const chp = () => bagFieldNode(bag, `_chp$${cc.id}`);
25249
+ const chv = () => bagFieldNode(bag, `_chv$${cc.id}`);
25250
+ // A host that can receive dangerouslySetInnerHTML must validate its
25251
+ // current child on EVERY render. The ordinary primitive fast path can
25252
+ // identity-skip an unchanged child while a raw-HTML writer activates in
25253
+ // the same render, bypassing childTextHole's mutual-exclusion check.
25254
+ if (cc.potentialDangerouslySetInnerHTML) {
25255
+ ctx.runtimeNeeded.add('childTextHole');
25256
+ pushAfterStmt(
25257
+ cc.id,
25258
+ org,
25259
+ b.block([
25260
+ b.const('_v', cc.valueExpr),
25261
+ b.stmt(
25262
+ b.assignment(
25263
+ '=',
25264
+ chv(),
25265
+ b.call(
25266
+ '_$childTextHole',
25267
+ b.id('__s'),
25268
+ b.literal(slotIndex),
25269
+ hostExpr(),
25270
+ V(),
25271
+ chv(),
23896
25272
  ),
23897
25273
  ),
23898
25274
  ),
@@ -23971,7 +25347,6 @@ function planJsx(
23971
25347
  }
23972
25348
  // Anchor node (null = append into an in-template element host) — see
23973
25349
  // anchorNodeFor.
23974
- const childAnchor = anchorNodeFor(cc, 'compAnchor');
23975
25350
  if (noTemplate) {
23976
25351
  // No bag to cache on → the small `textSlot` wrapper (fast inline for a
23977
25352
  // primitive into a text slot; delegates to `childSlot` otherwise).
@@ -24056,17 +25431,36 @@ function planJsx(
24056
25431
  liteMemo ? 'componentSlotLite' : cc.voidComponent ? 'componentSlotVoid' : 'componentSlot',
24057
25432
  );
24058
25433
  const memoAnchor = anchorNodeFor(cc, 'compAnchor');
24059
- const trailing = memoAnchor ? [memoAnchor] : [];
24060
- if (!liteMemo && cc.inheritRange) {
24061
- if (trailing.length === 0) trailing.push(undefinedNode());
24062
- trailing.push(undefinedNode(), undefinedNode(), b.literal(true));
24063
- } else if (!liteMemo && cc.singleRoot) {
24064
- if (trailing.length === 0) trailing.push(undefinedNode());
24065
- trailing.push(undefinedNode(), b.literal(true));
24066
- }
25434
+ const trailing = liteMemo
25435
+ ? [memoAnchor ?? undefinedNode(), b.literal(cc.invocationSite)]
25436
+ : [
25437
+ memoAnchor ?? undefinedNode(),
25438
+ undefinedNode(),
25439
+ cc.singleRoot ? b.literal(true) : undefinedNode(),
25440
+ cc.inheritRange ? b.literal(true) : undefinedNode(),
25441
+ undefinedNode(),
25442
+ b.literal(cc.invocationSite),
25443
+ ];
24067
25444
  const witnessMiss = cc.autoMemoWitnesses.length
24068
25445
  ? witnessMissChain(cc.autoMemoWitnesses)
24069
25446
  : null;
25447
+ let invocation = b.call(
25448
+ componentHelper,
25449
+ b.id('__s'),
25450
+ b.literal(slotIndex),
25451
+ hostExpr(),
25452
+ cc.compNode,
25453
+ cc.propsExpr,
25454
+ ...trailing,
25455
+ );
25456
+ if (ctx.presentationHydration?.structural && cc.bindingSite)
25457
+ invocation = b.call(
25458
+ requireRuntimeForContext(ctx, 'presentationStructure'),
25459
+ invocation.callee,
25460
+ b.literal('view'),
25461
+ b.literal(bindingMarker(cc.bindingSite, 'v')),
25462
+ ...invocation.arguments,
25463
+ );
24070
25464
  pushAfterStmt(
24071
25465
  cc.id,
24072
25466
  org,
@@ -24074,17 +25468,7 @@ function planJsx(
24074
25468
  ctx,
24075
25469
  cc.autoMemoDeps,
24076
25470
  slotIndex,
24077
- b.stmt(
24078
- b.call(
24079
- componentHelper,
24080
- b.id('__s'),
24081
- b.literal(slotIndex),
24082
- hostExpr(),
24083
- cc.compNode,
24084
- cc.propsExpr,
24085
- ...trailing,
24086
- ),
24087
- ),
25471
+ b.stmt(invocation),
24088
25472
  witnessMiss,
24089
25473
  cc.autoMemoContextAware,
24090
25474
  depNodeFor(cc),
@@ -24124,6 +25508,8 @@ function planJsx(
24124
25508
  undefinedNode(),
24125
25509
  undefinedNode(),
24126
25510
  b.literal(true),
25511
+ undefinedNode(),
25512
+ b.literal(cc.invocationSite),
24127
25513
  ),
24128
25514
  ),
24129
25515
  );
@@ -24150,7 +25536,8 @@ function planJsx(
24150
25536
  hostExpr(),
24151
25537
  cc.compNode,
24152
25538
  cc.propsExpr,
24153
- ...(liteAnchor ? [liteAnchor] : []),
25539
+ liteAnchor ?? undefinedNode(),
25540
+ b.literal(cc.invocationSite),
24154
25541
  ),
24155
25542
  ),
24156
25543
  );
@@ -24163,52 +25550,32 @@ function planJsx(
24163
25550
  // unmount move the slot DOM along with the block; an element host with
24164
25551
  // no in-template anchor can safely append).
24165
25552
  const compAnchor = anchorNodeFor(cc, 'compAnchor');
24166
- const trailing = [];
24167
- let anchorFilled = false;
24168
- if (compAnchor) {
24169
- trailing.push(compAnchor);
24170
- anchorFilled = true;
24171
- }
24172
- // key arg is positional AFTER anchor in componentSlot's signature. When a
24173
- // key is present but anchor isn't, supply `undefined` for the anchor slot
24174
- // so the key lands in the right argument position — the runtime's
24175
- // `anchor ?? null` still routes through appendChild as before.
24176
- if (cc.keyExpr != null) {
24177
- if (!anchorFilled) {
24178
- trailing.push(undefinedNode());
24179
- anchorFilled = true;
24180
- }
24181
- trailing.push(cc.keyExpr);
24182
- }
24183
- // singleRoot is the 8th positional arg (after anchor, key). It's gated on
24184
- // no-key, so backfill anchor + key placeholders to land it in the right
24185
- // slot. `true` = proven same-module single-element root; `2` = the
24186
- // cross-module sentinel (runtime checks the callee's $$singleRoot stamp).
24187
- if (cc.singleRoot || cc.maybeSingleRoot) {
24188
- if (!anchorFilled) {
24189
- trailing.push(undefinedNode());
24190
- anchorFilled = true;
24191
- }
24192
- trailing.push(undefinedNode(), cc.singleRoot ? b.literal(true) : b.literal(2));
24193
- }
24194
- // Persist key ownership separately from the current key VALUE so
24195
- // `key={undefined}` remains an independent reconciliation boundary.
24196
- if (cc.keyExpr != null) trailing.push(undefinedNode(), undefinedNode(), b.literal(true));
24197
- pushAfterStmt(
24198
- cc.id,
24199
- org,
24200
- b.stmt(
24201
- b.call(
24202
- componentHelper,
24203
- b.id('__s'),
24204
- b.literal(slotIndex),
24205
- hostExpr(),
24206
- cc.compNode,
24207
- cc.propsExpr,
24208
- ...trailing,
24209
- ),
24210
- ),
25553
+ const trailing = [
25554
+ compAnchor ?? undefinedNode(),
25555
+ cc.keyExpr ?? undefinedNode(),
25556
+ cc.singleRoot ? b.literal(true) : cc.maybeSingleRoot ? b.literal(2) : undefinedNode(),
25557
+ undefinedNode(),
25558
+ cc.keyExpr != null ? b.literal(true) : undefinedNode(),
25559
+ b.literal(cc.invocationSite),
25560
+ ];
25561
+ let invocation = b.call(
25562
+ componentHelper,
25563
+ b.id('__s'),
25564
+ b.literal(slotIndex),
25565
+ hostExpr(),
25566
+ cc.compNode,
25567
+ cc.propsExpr,
25568
+ ...trailing,
24211
25569
  );
25570
+ if (ctx.presentationHydration?.structural && cc.bindingSite)
25571
+ invocation = b.call(
25572
+ requireRuntimeForContext(ctx, 'presentationStructure'),
25573
+ invocation.callee,
25574
+ b.literal('view'),
25575
+ b.literal(bindingMarker(cc.bindingSite, 'v')),
25576
+ ...invocation.arguments,
25577
+ );
25578
+ pushAfterStmt(cc.id, org, b.stmt(invocation));
24212
25579
  }
24213
25580
  for (const pc of ctx._portalCalls) {
24214
25581
  const slotIndex = pc.slotIndex;
@@ -24585,6 +25952,16 @@ function commitSourceRows(sources, valueOf) {
24585
25952
  return b.array(rows);
24586
25953
  }
24587
25954
 
25955
+ // Preserve the authored rest site's identity through known-spread expansion.
25956
+ // Its authored spread ordinal is not the eventual host-source row index.
25957
+ function presentationHostSourcesCall(bind, ...args) {
25958
+ const call = b.call('_$bindSignalHostPropSources', ...args);
25959
+ const receipts = bind.sources.flatMap(({ binding }, source) =>
25960
+ binding.bindingRest ? [{ ...binding.bindingRest, source }] : [],
25961
+ );
25962
+ return receipts.length === 0 ? call : { ...call, _octanePresentationHostSources: receipts };
25963
+ }
25964
+
24588
25965
  // Recombine a native spread snapshot only for mount/hydration or a key collision.
24589
25966
  // Spreading the snapshot cannot replay getters: the authored spreads already
24590
25967
  // copied their values before any of the trailing expressions were evaluated.
@@ -24649,9 +26026,11 @@ function emitDeferredMount(bind, elVar, bag) {
24649
26026
  // Whole-object and spread-prefix styles diff on `_sty`; other grouped styles
24650
26027
  // use it only as a first-render marker. Each trailing property keeps a scalar.
24651
26028
  bag.constField(
24652
- bind.kind === 'style' || bind.kind === 'styleProperties'
24653
- ? `_sty$${bind.id}`
24654
- : `_prev$${bind.id}`,
26029
+ bind.signalDirect
26030
+ ? `_sig$${bind.id}`
26031
+ : bind.kind === 'style' || bind.kind === 'styleProperties'
26032
+ ? `_sty$${bind.id}`
26033
+ : `_prev$${bind.id}`,
24655
26034
  bind.kind === 'styleProperty' || bind.kind === 'styleProperties' ? 'style-unset' : 'undefined',
24656
26035
  );
24657
26036
  if (bind.kind === 'styleProperties') {
@@ -24675,7 +26054,57 @@ function bindingOrigin(bind) {
24675
26054
  return bind.expr && bind.expr.loc ? bind.expr : bind.origin;
24676
26055
  }
24677
26056
 
26057
+ function directSignalBindingHelper(bind) {
26058
+ if (!bind.signalDirect) return null;
26059
+ if (bind.kind === 'text' || bind.kind === 'textOnlyChild') return 'bindSignalText';
26060
+ if (bind.kind === 'value' || bind.kind === 'selectValue') return 'bindSignalValue';
26061
+ if (bind.kind === 'checked' || bind.kind === 'checkedCheckable') return 'bindSignalChecked';
26062
+ return 'bindSignalAttribute';
26063
+ }
26064
+
26065
+ function directSignalBindingArgs(bind, host, previous, value = bind.expr, previousValue) {
26066
+ const common = [b.id('__s'), previous, host];
26067
+ if (bind.kind === 'text' || bind.kind === 'textOnlyChild') {
26068
+ return [
26069
+ ...common,
26070
+ value,
26071
+ b.literal(bind.signalSite, JSON.stringify(bind.signalSite)),
26072
+ b.literal(bind.kind === 'textOnlyChild'),
26073
+ ...(previousValue !== undefined
26074
+ ? [
26075
+ bind.seededText ? b.literal(1) : undefinedNode(),
26076
+ bind.bindingMarker ? b.literal(bind.bindingMarker) : undefinedNode(),
26077
+ previousValue,
26078
+ ]
26079
+ : bind.bindingMarker
26080
+ ? [bind.seededText ? b.literal(1) : undefinedNode(), b.literal(bind.bindingMarker)]
26081
+ : bind.seededText
26082
+ ? [b.literal(1)]
26083
+ : []),
26084
+ ];
26085
+ }
26086
+ if (
26087
+ bind.kind === 'value' ||
26088
+ bind.kind === 'selectValue' ||
26089
+ bind.kind === 'checked' ||
26090
+ bind.kind === 'checkedCheckable'
26091
+ ) {
26092
+ return [...common, bind.expr, b.literal(bind.signalSite, JSON.stringify(bind.signalSite))];
26093
+ }
26094
+ return [
26095
+ ...common,
26096
+ attrLoweringToken(b.literal(bind.name), bind),
26097
+ bind.expr,
26098
+ b.literal(bind.signalSite, JSON.stringify(bind.signalSite)),
26099
+ b.literal(bind.kind, JSON.stringify(bind.kind)),
26100
+ ];
26101
+ }
26102
+
24678
26103
  function emitBindingMount(bind, elVar, bag) {
26104
+ const knownExpression = resolveKnownAttributeSource(bind.expr, (source) =>
26105
+ b.id(bag.local(`_prev$${source.binding.id}`)),
26106
+ );
26107
+ if (knownExpression !== bind.expr) bind = { ...bind, expr: knownExpression };
24679
26108
  if (bind.deferred) return emitDeferredMount(bind, elVar, bag);
24680
26109
  const org = bindingOrigin(bind);
24681
26110
  const st = (node) => inheritOriginLoc(node, org);
@@ -24689,6 +26118,46 @@ function emitBindingMount(bind, elVar, bag) {
24689
26118
  // name; everything else in the call maps to the value expression.
24690
26119
  const callee = () => attrLoweringToken(b.id(`_$${attrBindingHelper(bind)}`), bind);
24691
26120
  const nameLit = () => attrLoweringToken(b.literal(bind.name), bind);
26121
+ const signalHelper = directSignalBindingHelper(bind);
26122
+ if (signalHelper !== null) {
26123
+ const text = bind.kind === 'text' || bind.kind === 'textOnlyChild';
26124
+ let mount = b.call(
26125
+ attrLoweringToken(b.id(`_$${signalHelper}`), bind),
26126
+ ...directSignalBindingArgs(bind, el(), undefinedNode(), text ? V() : bind.expr),
26127
+ );
26128
+ if (text) {
26129
+ // A possible handle is not a subscription. Primitive text mounts through
26130
+ // the canonical hydration/seeded-placeholder writer, without journaling
26131
+ // a freshly cloned Text as though it were an existing update target.
26132
+ const scalarMount =
26133
+ bind.kind === 'textOnlyChild'
26134
+ ? b.call('_$htext', el(), V(), ...(bind.seededText ? [b.literal(1)] : []))
26135
+ : bind.bindingMarker
26136
+ ? b.call('_$bindingText', el(), V(), b.literal(bind.bindingMarker))
26137
+ : b.call('_$htextSwap', el(), V());
26138
+ mount = b.conditional(
26139
+ b.logical(
26140
+ '||',
26141
+ b.binary('===', V(), b.literal(null)),
26142
+ b.logical(
26143
+ '&&',
26144
+ b.binary('!==', b.unary('typeof', V()), b.literal('object')),
26145
+ b.binary('!==', b.unary('typeof', V()), b.literal('function')),
26146
+ ),
26147
+ ),
26148
+ scalarMount,
26149
+ mount,
26150
+ );
26151
+ }
26152
+ return st(
26153
+ b.block([
26154
+ ...mountHost(),
26155
+ ...(text ? [b.const('_v', bind.expr)] : []),
26156
+ b.stmt(b.assignment('=', local(`_sig$${bind.id}`), mount)),
26157
+ ...(text ? [b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V()))] : []),
26158
+ ]),
26159
+ );
26160
+ }
24692
26161
  // `suppressHydrationWarning`: stamp a JS flag (NOT a DOM attribute) the runtime reads to
24693
26162
  // keep the server value + skip the warning on a hydration mismatch for this element.
24694
26163
  if (bind.kind === 'suppress') {
@@ -24714,11 +26183,34 @@ function emitBindingMount(bind, elVar, bag) {
24714
26183
  if (bind.kind === 'formDiagnostic') {
24715
26184
  const args = [el(), b.literal(bind.diagnosticKind)];
24716
26185
  if (bind.selectedBinding !== undefined) {
24717
- args.push(b.arrow([], bagFieldNode(bag, `_prev$${bind.selectedBinding.id}`)));
26186
+ const selected = bind.selectedBinding;
26187
+ args.push(
26188
+ b.arrow(
26189
+ [],
26190
+ bagFieldNode(bag, `${selected.signalDirect ? '_sig' : '_prev'}$${selected.id}`),
26191
+ ),
26192
+ );
24718
26193
  }
24719
26194
  return st(b.stmt(b.call('_$queueFormAuthoringDiagnostic', ...args)));
24720
26195
  }
24721
26196
  switch (bind.kind) {
26197
+ case 'nativeProjection': {
26198
+ return st(
26199
+ b.block([
26200
+ ...mountHost(),
26201
+ b.stmt(
26202
+ b.call(
26203
+ bind.helper,
26204
+ b.id('__s'),
26205
+ b.literal(bind.slotIndex),
26206
+ el(),
26207
+ b.arrow([], bind.expr),
26208
+ b.array(bind.fields.map((field) => b.literal(field))),
26209
+ ),
26210
+ ),
26211
+ ]),
26212
+ );
26213
+ }
24722
26214
  case 'nativeStyle': {
24723
26215
  return st(
24724
26216
  b.block([
@@ -24800,6 +26292,29 @@ function emitBindingMount(bind, elVar, bag) {
24800
26292
  const sources = commitSourceRows(bind.sources, (binding, spread) =>
24801
26293
  b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
24802
26294
  );
26295
+ if (bind.signalHostSources) {
26296
+ return st(
26297
+ b.block([
26298
+ ...hostMount,
26299
+ b.stmt(
26300
+ b.assignment(
26301
+ '=',
26302
+ propsLocal,
26303
+ presentationHostSourcesCall(
26304
+ bind,
26305
+ b.id('__s'),
26306
+ undefinedNode(),
26307
+ el(),
26308
+ sources,
26309
+ b.literal(bind.signalSite, JSON.stringify(bind.signalSite)),
26310
+ b.literal(bind.hasNestedChildren === true),
26311
+ ...(bind.readStyle ? [b.id(bind.readStyle)] : []),
26312
+ ),
26313
+ ),
26314
+ ),
26315
+ ]),
26316
+ );
26317
+ }
24803
26318
  // The cleanup closure reads the bag through the captured `_b` — the bag
24804
26319
  // exists by the time any cleanup runs (committed at mount end), and the
24805
26320
  // `_host$` field is re-written by updates, so the read must be live.
@@ -24844,7 +26359,18 @@ function emitBindingMount(bind, elVar, bag) {
24844
26359
  return st(
24845
26360
  b.block([
24846
26361
  b.const('_v', bind.expr),
24847
- b.stmt(b.assignment('=', local(`_txt$${bind.id}`), b.call('_$htextSwap', el(), V()))),
26362
+ b.stmt(
26363
+ b.assignment(
26364
+ '=',
26365
+ local(`_txt$${bind.id}`),
26366
+ b.call(
26367
+ bind.bindingMarker ? '_$bindingText' : '_$htextSwap',
26368
+ el(),
26369
+ V(),
26370
+ ...(bind.bindingMarker ? [b.literal(bind.bindingMarker)] : []),
26371
+ ),
26372
+ ),
26373
+ ),
24848
26374
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
24849
26375
  ]),
24850
26376
  );
@@ -25100,6 +26626,10 @@ function emitBindingMount(bind, elVar, bag) {
25100
26626
  }
25101
26627
 
25102
26628
  function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
26629
+ const knownExpression = resolveKnownAttributeSource(bind.expr, (source) =>
26630
+ bagFieldNode(bag, `_prev$${source.binding.id}`),
26631
+ );
26632
+ if (knownExpression !== bind.expr) bind = { ...bind, expr: knownExpression };
25103
26633
  if (bind.mountOnly) return null;
25104
26634
  const org = bindingOrigin(bind);
25105
26635
  const st = (node) => inheritOriginLoc(node, org);
@@ -25112,7 +26642,74 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25112
26642
  const callee = () =>
25113
26643
  attrLoweringToken(b.id(`_$${attrBindingUpdateHelper(bind, inlineBindingGuards)}`), bind);
25114
26644
  const nameLit = () => attrLoweringToken(b.literal(bind.name), bind);
26645
+ const signalHelper = directSignalBindingHelper(bind);
26646
+ if (signalHelper !== null) {
26647
+ if (bind.kind === 'text' || bind.kind === 'textOnlyChild') {
26648
+ return st(
26649
+ b.block([
26650
+ b.const('_v', bind.expr),
26651
+ // Repeated primitives need neither a binding read nor a bag write.
26652
+ // Objects/functions still re-enter: an unchanged handle can have
26653
+ // changed value, pending, or error state since the last render.
26654
+ b.if(
26655
+ b.logical(
26656
+ '||',
26657
+ b.binary('!==', F('_prev'), V()),
26658
+ b.logical(
26659
+ '&&',
26660
+ b.binary('!==', V(), b.literal(null)),
26661
+ b.logical(
26662
+ '||',
26663
+ b.binary('===', b.unary('typeof', V()), b.literal('object')),
26664
+ b.binary('===', b.unary('typeof', V()), b.literal('function')),
26665
+ ),
26666
+ ),
26667
+ ),
26668
+ b.block([
26669
+ b.stmt(
26670
+ b.assignment(
26671
+ '=',
26672
+ F('_sig'),
26673
+ b.call(
26674
+ b.id(`_$${signalHelper}`),
26675
+ ...directSignalBindingArgs(bind, F('_el'), F('_sig'), V(), F('_prev')),
26676
+ ),
26677
+ ),
26678
+ ),
26679
+ b.stmt(b.assignment('=', F('_prev'), V())),
26680
+ ]),
26681
+ ),
26682
+ ]),
26683
+ );
26684
+ }
26685
+ return st(
26686
+ b.stmt(
26687
+ b.assignment(
26688
+ '=',
26689
+ F('_sig'),
26690
+ b.call(
26691
+ attrLoweringToken(b.id(`_$${signalHelper}`), bind),
26692
+ ...directSignalBindingArgs(bind, F('_el'), F('_sig')),
26693
+ ),
26694
+ ),
26695
+ ),
26696
+ );
26697
+ }
25115
26698
  switch (bind.kind) {
26699
+ case 'nativeProjection': {
26700
+ return st(
26701
+ b.stmt(
26702
+ b.call(
26703
+ bind.helper,
26704
+ b.id('__s'),
26705
+ b.literal(bind.slotIndex),
26706
+ F('_el'),
26707
+ b.arrow([], bind.expr),
26708
+ b.array(bind.fields.map((field) => b.literal(field))),
26709
+ ),
26710
+ ),
26711
+ );
26712
+ }
25116
26713
  case 'nativeStyle': {
25117
26714
  return st(
25118
26715
  b.stmt(b.call(bind.helper, b.id('__s'), b.literal(bind.slotIndex), F('_el'), bind.expr)),
@@ -25187,6 +26784,26 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25187
26784
  const sources = commitSourceRows(bind.sources, (binding, spread) =>
25188
26785
  bagFieldNode(bag, `${spread ? '_sp' : '_prev'}$${binding.id}`),
25189
26786
  );
26787
+ if (bind.signalHostSources) {
26788
+ return st(
26789
+ b.stmt(
26790
+ b.assignment(
26791
+ '=',
26792
+ F('_host'),
26793
+ presentationHostSourcesCall(
26794
+ bind,
26795
+ b.id('__s'),
26796
+ F('_host'),
26797
+ F('_el'),
26798
+ sources,
26799
+ b.literal(bind.signalSite, JSON.stringify(bind.signalSite)),
26800
+ b.literal(bind.hasNestedChildren === true),
26801
+ ...(bind.readStyle ? [b.id(bind.readStyle)] : []),
26802
+ ),
26803
+ ),
26804
+ ),
26805
+ );
26806
+ }
25190
26807
  return st(
25191
26808
  b.stmt(
25192
26809
  b.assignment(
@@ -25555,206 +27172,8 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25555
27172
  // HTML emission
25556
27173
  // ===========================================================================
25557
27174
 
25558
- // ---------------------------------------------------------------------------
25559
- // Template IR + origin recording (`inspect: true`). Static template HTML is
25560
- // runtime DATA, not JavaScript syntax: the browser-facing `template(html)` ABI
25561
- // intentionally stays a string. The compiler represents that data as an
25562
- // structured tree of elements and typed data chunks until allocTemplate
25563
- // serializes the completed template exactly once. Child templates compose by
25564
- // transferring nodes, so no intermediate HTML is joined and no origin offsets
25565
- // need to be shifted during construction. Origin spans remain local to their
25566
- // chunk; the one serializer shifts them into final logical-HTML coordinates.
25567
- //
25568
- // Origin collection stays gated on `ctx.inspect`. The normal compile path pays
25569
- // for the template chunks (the structural representation replacing string
25570
- // concatenation) but not for source-span arrays.
25571
- // ---------------------------------------------------------------------------
25572
-
25573
- /**
25574
- * @typedef {{ start: number, end: number, srcStart: number, srcEnd: number,
25575
- * kind: 'tag-open' | 'tag-close' | 'attr-name' | 'attr-value' | 'text' }} TemplateOrigin
25576
- */
25577
-
25578
- /**
25579
- * @typedef {'syntax' | 'tag-open' | 'tag-close' | 'attribute' | 'text' |
25580
- * 'anchor' | 'fragment-open' | 'fragment-close' | 'raw'} TemplatePartKind
25581
- */
25582
-
25583
- /**
25584
- * @typedef {{
25585
- * type: 'TemplatePart',
25586
- * kind: TemplatePartKind,
25587
- * value: string,
25588
- * origins: TemplateOrigin[] | null,
25589
- * length: number,
25590
- * }} TemplatePart
25591
- */
25592
-
25593
- /**
25594
- * @typedef {{
25595
- * type: 'TemplateElement',
25596
- * tag: string,
25597
- * namespace: string,
25598
- * synthetic: boolean,
25599
- * opening: TemplatePart,
25600
- * attributes: TemplatePart[],
25601
- * openingEnd: TemplatePart,
25602
- * children: TemplateNode[],
25603
- * closing: TemplatePart | null,
25604
- * length: number,
25605
- * }} TemplateElement
25606
- */
25607
-
25608
- /** @typedef {TemplatePart | TemplateElement} TemplateNode */
25609
-
25610
- /**
25611
- * @typedef {{
25612
- * type: 'Template',
25613
- * parts: TemplateNode[],
25614
- * length: number,
25615
- * }} TemplateIR
25616
- */
25617
-
25618
- /** @returns {TemplateIR} */
25619
- function createTemplateIr() {
25620
- return { type: 'Template', parts: [], length: 0 };
25621
- }
25622
-
25623
- /**
25624
- * @param {string} value
25625
- * @param {TemplatePartKind} [kind]
25626
- * @param {TemplateOrigin[] | null} [origins]
25627
- * @returns {TemplatePart}
25628
- */
25629
- function createTemplatePart(value, kind = 'raw', origins = null) {
25630
- return { type: 'TemplatePart', kind, value, origins, length: value.length };
25631
- }
25632
-
25633
- /** @param {TemplateIR} template @param {TemplateNode} node */
25634
- function appendTemplateNode(template, node) {
25635
- template.parts.push(node);
25636
- template.length += node.length;
25637
- }
25638
-
25639
- /**
25640
- * @param {TemplateIR} template
25641
- * @param {string} value
25642
- * @param {TemplatePartKind} [kind]
25643
- * @param {TemplateOrigin[] | null} [origins]
25644
- */
25645
- function appendTemplatePart(template, value, kind = 'raw', origins = null) {
25646
- if (value === '') return;
25647
- appendTemplateNode(template, createTemplatePart(value, kind, origins));
25648
- }
25649
-
25650
- /** @param {TemplateIR} into @param {TemplateIR} child */
25651
- function appendTemplateIr(into, child) {
25652
- if (child.length === 0) return;
25653
- for (const part of child.parts) into.parts.push(part);
25654
- into.length += child.length;
25655
- }
25656
-
25657
- /**
25658
- * @param {string} value
25659
- * @param {TemplatePartKind} [kind]
25660
- * @param {TemplateOrigin[] | null} [origins]
25661
- * @returns {TemplateIR}
25662
- */
25663
- function templatePart(value, kind = 'raw', origins = null) {
25664
- const template = createTemplateIr();
25665
- appendTemplatePart(template, value, kind, origins);
25666
- return template;
25667
- }
25668
-
25669
- /**
25670
- * @param {string} tag
25671
- * @param {string} namespace
25672
- * @param {boolean} voidElement
25673
- * @param {TemplateIR} attributes
25674
- * @param {TemplateIR} children
25675
- * @param {TemplateOrigin[] | null} openOrigins
25676
- * @param {TemplateOrigin[] | null} closeOrigins
25677
- * @param {boolean} [synthetic]
25678
- * @returns {TemplateIR}
25679
- */
25680
- function templateElement(
25681
- tag,
25682
- namespace,
25683
- voidElement,
25684
- attributes,
25685
- children,
25686
- openOrigins,
25687
- closeOrigins,
25688
- synthetic = false,
25689
- ) {
25690
- const opening = createTemplatePart(`<${tag}`, 'tag-open', openOrigins);
25691
- const openingEnd = createTemplatePart(voidElement ? '/>' : '>', 'syntax');
25692
- const closing = voidElement ? null : createTemplatePart(`</${tag}>`, 'tag-close', closeOrigins);
25693
- const node = {
25694
- type: 'TemplateElement',
25695
- tag,
25696
- namespace,
25697
- synthetic,
25698
- opening,
25699
- attributes: /** @type {TemplatePart[]} */ (attributes.parts),
25700
- openingEnd,
25701
- children: children.parts,
25702
- closing,
25703
- length:
25704
- opening.value.length +
25705
- attributes.length +
25706
- openingEnd.value.length +
25707
- children.length +
25708
- (closing === null ? 0 : closing.value.length),
25709
- };
25710
- const template = createTemplateIr();
25711
- appendTemplateNode(template, node);
25712
- return template;
25713
- }
25714
-
25715
- /**
25716
- * Serialize the completed template once. The IR already contains the exact
25717
- * runtime bytes; this pass only joins them and converts chunk-local origins to
25718
- * final logical-HTML offsets.
25719
- *
25720
- * @param {TemplateIR} template
25721
- */
25722
- function serializeTemplateIr(template) {
25723
- const values = [];
25724
- let origins = null;
25725
- let offset = 0;
25726
- const appendPart = (part) => {
25727
- values.push(part.value);
25728
- if (part.origins !== null) {
25729
- origins ??= [];
25730
- for (const origin of part.origins) {
25731
- origins.push({
25732
- start: origin.start + offset,
25733
- end: origin.end + offset,
25734
- srcStart: origin.srcStart,
25735
- srcEnd: origin.srcEnd,
25736
- kind: origin.kind,
25737
- });
25738
- }
25739
- }
25740
- offset += part.value.length;
25741
- };
25742
- const visit = (node) => {
25743
- if (node.type === 'TemplatePart') {
25744
- appendPart(node);
25745
- return;
25746
- }
25747
- appendPart(node.opening);
25748
- for (const attribute of node.attributes) appendPart(attribute);
25749
- appendPart(node.openingEnd);
25750
- for (const child of node.children) visit(child);
25751
- if (node.closing !== null) appendPart(node.closing);
25752
- };
25753
- for (const node of template.parts) {
25754
- visit(node);
25755
- }
25756
- return { html: values.join(''), origins };
25757
- }
27175
+ /** @typedef {import('./template-ir.js').TemplateIR} TemplateIR */
27176
+ /** @typedef {import('./template-ir.js').TemplateOrigin} TemplateOrigin */
25758
27177
 
25759
27178
  // Record the origin entries for one static-attribute chunk appended at `base`
25760
27179
  // (a bakeStaticAttr result or the inline ` style="…"` bakes, which share the
@@ -25826,15 +27245,36 @@ function emitNodeHtml(
25826
27245
  parentNs = 'html',
25827
27246
  cssHash = null,
25828
27247
  ) {
27248
+ if (node._octaneBindingValue || node._octaneBindingSlot) {
27249
+ const ch = makeChildCall(node.expression, ctx, componentName, inlinedSubs, cssHash, parentNs);
27250
+ ch.bindingMarker = bindingChildMarker(node);
27251
+ ch.hostPath = path.slice(0, -1);
27252
+ ch.anchorPath = path;
27253
+ compCalls.push(ch);
27254
+ return templatePart('<!>', 'anchor');
27255
+ }
25829
27256
  if (node.type === 'Text') {
25830
- if (isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)) {
25831
- bindings.push({
25832
- id: bindings.length,
25833
- kind: 'text',
25834
- expr: resolveStyleExpr(node.expression, cssHash),
25835
- path: path.slice(0, -1),
25836
- childIndex: path[path.length - 1],
25837
- });
27257
+ if (
27258
+ isDirectSignalHandleExpression(node.expression) ||
27259
+ isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)
27260
+ ) {
27261
+ bindings.push(
27262
+ markDirectSignalBinding(
27263
+ {
27264
+ id: bindings.length,
27265
+ kind: 'text',
27266
+ expr: resolveStyleExpr(node.expression, cssHash),
27267
+ bindingMarker: node._octaneBindingText
27268
+ ? bindingMarker(node._octaneBindingText, 't')
27269
+ : undefined,
27270
+ path: path.slice(0, -1),
27271
+ childIndex: path[path.length - 1],
27272
+ },
27273
+ ctx,
27274
+ node.expression,
27275
+ 'binding',
27276
+ ),
27277
+ );
25838
27278
  return templatePart('<!>', 'anchor');
25839
27279
  }
25840
27280
  // Bare `{expr}` (no string cast) → RENDERABLE hole at a top-level / multi-
@@ -25853,6 +27293,28 @@ function emitNodeHtml(
25853
27293
  // cannot represent a Fragment ref or template-only control flow in that body.
25854
27294
  // Ordinary root portals keep their existing descriptor/childSlot lowering.
25855
27295
  if (node.type === 'TSRXExpression') {
27296
+ if (isDirectSignalHandleExpression(node.expression)) {
27297
+ bindings.push(
27298
+ markDirectSignalBinding(
27299
+ {
27300
+ id: bindings.length,
27301
+ kind: 'text',
27302
+ expr: tsrxExprNode(
27303
+ resolveStyleExpr(node.expression, cssHash),
27304
+ ctx,
27305
+ componentName,
27306
+ inlinedSubs,
27307
+ ),
27308
+ path: path.slice(0, -1),
27309
+ childIndex: path[path.length - 1],
27310
+ },
27311
+ ctx,
27312
+ node.expression,
27313
+ 'binding',
27314
+ ),
27315
+ );
27316
+ return templatePart('<!>', 'anchor');
27317
+ }
25856
27318
  const fragmentBindings = ctx._fragRefStack;
25857
27319
  if (
25858
27320
  isCreatePortalCall(node.expression) &&
@@ -26162,7 +27624,9 @@ function emitElementHtml(
26162
27624
  const hostNs = nsForSelf(tag, parentNs);
26163
27625
  const childNs = nsForChildren(tag, parentNs);
26164
27626
  // Collect attributes.
26165
- const attrs = node.attributes || node.openingElement?.attributes || [];
27627
+ const attrs = expandKnownAttributeSpreads(
27628
+ node.attributes || node.openingElement?.attributes || [],
27629
+ );
26166
27630
  // A null/undefined child alongside direct raw HTML is semantically absent.
26167
27631
  // Suppress its child binding so hydration cannot clear the raw HTML that the
26168
27632
  // preceding binding just adopted/applied.
@@ -26178,7 +27642,7 @@ function emitElementHtml(
26178
27642
  // into the template HTML (the spread would clobber them at runtime) —
26179
27643
  // emit them as bindings in source order instead.
26180
27644
  const firstSpreadIdx = attrs.findIndex(
26181
- (a) => a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute',
27645
+ (a) => !a.knownSource && (a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute'),
26182
27646
  );
26183
27647
  const directPropIdentities = new Set();
26184
27648
  let hasDuplicateDirectProp = false;
@@ -26213,11 +27677,21 @@ function emitElementHtml(
26213
27677
  (tag === 'select' &&
26214
27678
  directPropNames.has('multiple') &&
26215
27679
  (directPropNames.has('value') || directPropNames.has('defaultValue')));
27680
+ const hasDirectSignalStyle =
27681
+ !ctx.nativeReads &&
27682
+ attrs.some((attr) => {
27683
+ if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') return false;
27684
+ if (normalizeJsxAttrName(jsxAttrRawName(attr), tag, hostNs) !== 'style') return false;
27685
+ const value = attr.value;
27686
+ const inner = value?.type === 'JSXExpressionContainer' ? value.expression : value;
27687
+ return spreadContainsDirectSignalHandle(inner);
27688
+ });
26216
27689
  const resolveHostPropsAcrossSources =
26217
27690
  firstSpreadIdx !== -1 ||
26218
27691
  hasDuplicateDirectProp ||
26219
27692
  hasDirectFormCascade ||
26220
- resolveDevCustomizedHost;
27693
+ resolveDevCustomizedHost ||
27694
+ hasDirectSignalStyle;
26221
27695
  const hostClientSources = [];
26222
27696
  let directChildrenClientBinding = null;
26223
27697
  let hostCommitClientBinding = null;
@@ -26258,9 +27732,45 @@ function emitElementHtml(
26258
27732
  typeWriters.length === 1 &&
26259
27733
  (staticInputType === 'checkbox' || staticInputType === 'radio');
26260
27734
  const attrTemplate = createTemplateIr();
27735
+ let directSignalControlSite = null;
27736
+ const ensureDirectControlSite = (site) => {
27737
+ if (directSignalControlSite !== null) return;
27738
+ if (directPropNames.has('data-octane-input')) {
27739
+ throw new Error(
27740
+ '`data-octane-input` is reserved for compiler-owned writable signal controls.',
27741
+ );
27742
+ }
27743
+ directSignalControlSite = site;
27744
+ appendTemplatePart(attrTemplate, ` data-octane-input="${escapeAttr(site)}"`, 'attribute', null);
27745
+ };
27746
+ const markDirectControl = (binding, expression, origin) => {
27747
+ if (!canCarryDirectSignalHandle(expression)) return binding;
27748
+ const site = directSignalSite(ctx, node, 'input');
27749
+ ensureDirectControlSite(site);
27750
+ ctx.signalBindingsUsed = true;
27751
+ ctx.signalBindingsEager = true;
27752
+ return { ...binding, signalDirect: true, signalSite: site };
27753
+ };
27754
+ const hostSignalSite = directSignalSite(ctx, node, 'binding');
27755
+ if (firstSpreadIdx !== -1 && (tag === 'input' || tag === 'textarea' || tag === 'select')) {
27756
+ ensureDirectControlSite(hostSignalSite);
27757
+ }
26261
27758
  let sawRef = false;
26262
27759
  for (let attrI = 0; attrI < attrs.length; attrI++) {
26263
27760
  const attr = attrs[attrI];
27761
+ if (attr._octaneKnownProjectionSource) continue;
27762
+ if (attr.knownSource) {
27763
+ const binding = {
27764
+ id: bindings.length,
27765
+ kind: attr.knownSource.projection ? 'nativeProjection' : 'hostValue',
27766
+ expr: tsrxExprNode(attr.argument, ctx, componentName, inlinedSubs),
27767
+ fields: attr.knownSource.fields,
27768
+ path,
27769
+ };
27770
+ attr.knownSource.binding = binding;
27771
+ bindings.push(binding);
27772
+ continue;
27773
+ }
26264
27774
  // `<div {...props}/>` — runtime spread. Emits one setSpread binding that
26265
27775
  // routes each key (class / style / on… / attr / ref) and diffs against
26266
27776
  // the prior spread object to clear removed keys.
@@ -26269,6 +27779,7 @@ function emitElementHtml(
26269
27779
  const binding = {
26270
27780
  id: bindings.length,
26271
27781
  kind: 'hostSpread',
27782
+ ...(attr._octaneBindingRest ? { bindingRest: attr._octaneBindingRest } : {}),
26272
27783
  // The INNER spread argument; the emit re-derives the
26273
27784
  // `_$snapshotSpread(…)` wrapper around it.
26274
27785
  expr: tsrxExprNode(attr.argument, ctx, componentName, inlinedSubs),
@@ -26281,6 +27792,24 @@ function emitElementHtml(
26281
27792
  }
26282
27793
  if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') continue;
26283
27794
  const rawAttrName = jsxAttrRawName(attr);
27795
+ if (attr._octaneBindingClassGroups) {
27796
+ const group = attr._octaneBindingClassGroups;
27797
+ bindings.push({
27798
+ id: bindings.length,
27799
+ kind: 'attr',
27800
+ name: group.receipt,
27801
+ attributeHelper: 'setBindingClass',
27802
+ expr: tsrxExprNode(
27803
+ b.array([b.array(group.baseline), b.array(group.groups)]),
27804
+ ctx,
27805
+ componentName,
27806
+ inlinedSubs,
27807
+ ),
27808
+ path,
27809
+ nameOrigin: attr.name,
27810
+ });
27811
+ continue;
27812
+ }
26284
27813
  if (resolveHostPropsAcrossSources) {
26285
27814
  if (rawAttrName === 'ref') {
26286
27815
  if (sawRef) {
@@ -26320,6 +27849,13 @@ function emitElementHtml(
26320
27849
  ctx.inspect,
26321
27850
  );
26322
27851
  }
27852
+ if (
27853
+ (tag === 'input' || tag === 'textarea' || tag === 'select') &&
27854
+ (rawAttrName === 'value' || (tag === 'input' && rawAttrName === 'checked')) &&
27855
+ isDirectSignalHandleExpression(inner)
27856
+ ) {
27857
+ ensureDirectControlSite(hostSignalSite);
27858
+ }
26323
27859
  expr = tsrxExprNode(inner, ctx, componentName, inlinedSubs);
26324
27860
  }
26325
27861
  const binding = {
@@ -26511,19 +28047,17 @@ function emitElementHtml(
26511
28047
  resolveFormControlsAcrossSpreads &&
26512
28048
  (ctlKind !== null || (tag === 'select' && attrName === 'multiple'))
26513
28049
  ) {
28050
+ const sourceExpr =
28051
+ val == null
28052
+ ? b.literal(true, 'true', attr)
28053
+ : val.type === 'JSXExpressionContainer'
28054
+ ? val.expression
28055
+ : val;
26514
28056
  const binding = {
26515
28057
  id: bindings.length,
26516
28058
  kind: 'formValue',
26517
28059
  name: attrName,
26518
- expr:
26519
- val == null
26520
- ? b.literal(true, 'true', attr)
26521
- : tsrxExprNode(
26522
- val.type === 'JSXExpressionContainer' ? val.expression : val,
26523
- ctx,
26524
- componentName,
26525
- inlinedSubs,
26526
- ),
28060
+ expr: tsrxExprNode(sourceExpr, ctx, componentName, inlinedSubs),
26527
28061
  path,
26528
28062
  };
26529
28063
  bindings.push(binding);
@@ -26541,22 +28075,28 @@ function emitElementHtml(
26541
28075
  } else if (ctlKind === 'checked' && leanChecked) {
26542
28076
  ctlKind = 'checkedCheckable';
26543
28077
  }
26544
- bindings.push({
28078
+ const sourceExpr =
28079
+ val == null
28080
+ ? b.literal(true, 'true', attr)
28081
+ : val.type === 'JSXExpressionContainer'
28082
+ ? val.expression
28083
+ : val;
28084
+ const binding = {
26545
28085
  id: bindings.length,
26546
28086
  kind: ctlKind,
26547
- expr:
26548
- val == null
26549
- ? b.literal(true, 'true', attr)
26550
- : tsrxExprNode(
26551
- val.type === 'JSXExpressionContainer' ? val.expression : val,
26552
- ctx,
26553
- componentName,
26554
- inlinedSubs,
26555
- ),
28087
+ expr: tsrxExprNode(sourceExpr, ctx, componentName, inlinedSubs),
26556
28088
  path,
26557
28089
  ns: hostNs,
26558
28090
  nameOrigin: attr.name,
26559
- });
28091
+ };
28092
+ bindings.push(
28093
+ ctlKind === 'value' ||
28094
+ ctlKind === 'selectValue' ||
28095
+ ctlKind === 'checked' ||
28096
+ ctlKind === 'checkedCheckable'
28097
+ ? markDirectControl(binding, sourceExpr, attr)
28098
+ : binding,
28099
+ );
26560
28100
  continue;
26561
28101
  }
26562
28102
  // `autoFocus` never bakes/writes an attribute — React parity: the
@@ -26613,6 +28153,13 @@ function emitElementHtml(
26613
28153
  // template (unless we're after a spread, which would clobber it); a fixed
26614
28154
  // literal prefix plus dynamic suffix can avoid a whole-object diff.
26615
28155
  if (attrName === 'style') {
28156
+ if (!ctx.nativeReads && isDirectSignalHandleExpression(inner)) {
28157
+ throw new Error(
28158
+ 'Direct signal style objects cannot install property-targeted bindings. ' +
28159
+ 'Use fixed style properties such as `style={{ color: color$ }}`, or use `.get()` ' +
28160
+ 'for a one-way whole-style value.',
28161
+ );
28162
+ }
26616
28163
  if (!isAfterSpread && inner.type === 'Literal' && typeof inner.value === 'string') {
26617
28164
  const chunk = ` style="${escapeAttr(inner.value)}"`;
26618
28165
  appendBakedAttribute(attrTemplate, chunk, attrName, attr.name, inner, ctx.inspect);
@@ -26837,7 +28384,7 @@ function emitElementHtml(
26837
28384
  } else if (attrName === 'class') {
26838
28385
  // (`className` was already normalized to `class` above.)
26839
28386
  const scalar = firstSpreadIdx === -1 ? scalarStateClass(inner, ctx) : null;
26840
- bindings.push({
28387
+ const binding = {
26841
28388
  id: bindings.length,
26842
28389
  kind: 'class',
26843
28390
  expr: scalar === null ? expr : tsrxExprNode(scalar, ctx, componentName, inlinedSubs),
@@ -26845,7 +28392,9 @@ function emitElementHtml(
26845
28392
  ns: hostNs,
26846
28393
  fresh: scalar === null && isFreshBindingExpr(inner),
26847
28394
  nameOrigin: attr.name,
26848
- });
28395
+ name: 'class',
28396
+ };
28397
+ bindings.push(markDirectSignalBinding(binding, ctx, inner, 'binding'));
26849
28398
  } else if (
26850
28399
  (tag === 'form' && attrName === 'action') ||
26851
28400
  ((tag === 'button' || tag === 'input') &&
@@ -26870,15 +28419,22 @@ function emitElementHtml(
26870
28419
  // namespace, URL, controlled-form, and validity routing. Their value
26871
28420
  // contract is uniform across native/custom HTML, SVG, and MathML:
26872
28421
  // nullish removes and everything else stringifies.
26873
- bindings.push({
26874
- id: bindings.length,
26875
- kind: 'ariaAttr',
26876
- name: attrName,
26877
- expr,
26878
- path,
26879
- ns: hostNs,
26880
- nameOrigin: attr.name,
26881
- });
28422
+ bindings.push(
28423
+ markDirectSignalBinding(
28424
+ {
28425
+ id: bindings.length,
28426
+ kind: 'ariaAttr',
28427
+ name: attrName,
28428
+ expr,
28429
+ path,
28430
+ ns: hostNs,
28431
+ nameOrigin: attr.name,
28432
+ },
28433
+ ctx,
28434
+ inner,
28435
+ 'binding',
28436
+ ),
28437
+ );
26882
28438
  } else if (
26883
28439
  !((hostNs === 'html' || hostNs === 'opaque') && tag.includes('-')) &&
26884
28440
  BOOLEAN_ATTR_PROPS.has(attrName.toLowerCase())
@@ -26886,15 +28442,22 @@ function emitElementHtml(
26886
28442
  // Exact native boolean props all share the canonical presence contract.
26887
28443
  // The lowercase name is the final DOM spelling (including camelCase JSX
26888
28444
  // aliases such as allowFullScreen -> allowfullscreen).
26889
- bindings.push({
26890
- id: bindings.length,
26891
- kind: 'booleanAttr',
26892
- name: attrName.toLowerCase(),
26893
- expr,
26894
- path,
26895
- ns: hostNs,
26896
- nameOrigin: attr.name,
26897
- });
28445
+ bindings.push(
28446
+ markDirectSignalBinding(
28447
+ {
28448
+ id: bindings.length,
28449
+ kind: 'booleanAttr',
28450
+ name: attrName.toLowerCase(),
28451
+ expr,
28452
+ path,
28453
+ ns: hostNs,
28454
+ nameOrigin: attr.name,
28455
+ },
28456
+ ctx,
28457
+ inner,
28458
+ 'binding',
28459
+ ),
28460
+ );
26898
28461
  } else if (
26899
28462
  /^data-[a-z][a-z0-9_-]*$/.test(attrName) &&
26900
28463
  isKnownStringExpression(inner, ctx.knownStringLocals)
@@ -26905,33 +28468,57 @@ function emitElementHtml(
26905
28468
  // data attribute in HTML, SVG, and MathML, so destination-opaque component
26906
28469
  // templates can retain this specialization. Production also admits unknown
26907
28470
  // scalar data values through staticAttributeWriter below.
26908
- bindings.push({
26909
- id: bindings.length,
26910
- kind: 'stringData',
26911
- name: attrName,
26912
- expr,
26913
- path,
26914
- ns: hostNs,
26915
- nameOrigin: attr.name,
26916
- });
28471
+ bindings.push(
28472
+ markDirectSignalBinding(
28473
+ {
28474
+ id: bindings.length,
28475
+ kind: 'stringData',
28476
+ name: attrName,
28477
+ expr,
28478
+ path,
28479
+ ns: hostNs,
28480
+ nameOrigin: attr.name,
28481
+ },
28482
+ ctx,
28483
+ inner,
28484
+ 'binding',
28485
+ ),
28486
+ );
26917
28487
  } else {
26918
28488
  const writer = ctx.dev ? null : staticAttributeWriter(tag, attrName);
26919
- bindings.push({
26920
- id: bindings.length,
26921
- kind: 'attr',
26922
- attributeHelper: writer?.helper,
26923
- name:
26924
- ctx.dev && (rawAttrName === 'tabIndex' || rawAttrName === 'htmlFor')
26925
- ? rawAttrName
26926
- : (writer?.name ?? attrName),
26927
- expr,
26928
- path,
26929
- ns: hostNs,
26930
- nameOrigin: attr.name,
26931
- });
28489
+ bindings.push(
28490
+ markDirectSignalBinding(
28491
+ {
28492
+ id: bindings.length,
28493
+ kind: 'attr',
28494
+ attributeHelper: writer?.helper,
28495
+ name:
28496
+ ctx.dev && (rawAttrName === 'tabIndex' || rawAttrName === 'htmlFor')
28497
+ ? rawAttrName
28498
+ : (writer?.name ?? attrName),
28499
+ expr,
28500
+ path,
28501
+ ns: hostNs,
28502
+ nameOrigin: attr.name,
28503
+ },
28504
+ ctx,
28505
+ inner,
28506
+ 'binding',
28507
+ ),
28508
+ );
26932
28509
  }
26933
28510
  }
26934
28511
  if (resolveHostPropsAcrossSources) {
28512
+ const signalHostSources =
28513
+ firstSpreadIdx !== -1 ||
28514
+ hasDirectSignalStyle ||
28515
+ hostClientSources.some(
28516
+ (source) => !source.spread && canCarryDirectSignalHandle(source.binding.expr),
28517
+ );
28518
+ if (signalHostSources) {
28519
+ ctx.signalBindingsUsed = true;
28520
+ ctx.signalBindingsEager = true;
28521
+ }
26935
28522
  hostCommitClientBinding = {
26936
28523
  id: bindings.length,
26937
28524
  kind: 'hostCommit',
@@ -26939,6 +28526,8 @@ function emitElementHtml(
26939
28526
  origin: node,
26940
28527
  sources: hostClientSources,
26941
28528
  hasNestedChildren: hasNestedJsxChildren,
28529
+ signalHostSources,
28530
+ signalSite: signalHostSources ? hostSignalSite : null,
26942
28531
  };
26943
28532
  bindings.push(hostCommitClientBinding);
26944
28533
  } else if (resolveDangerouslySetInnerHTMLAcrossSpreads) {
@@ -27014,13 +28603,16 @@ function emitElementHtml(
27014
28603
  }
27015
28604
  }
27016
28605
  if (
28606
+ !VOID_ELEMENTS.has(tag) &&
27017
28607
  !hasNestedJsxChildren &&
27018
28608
  (hostCommitClientBinding !== null || directChildrenClientBinding !== null)
27019
28609
  ) {
27020
28610
  compCalls.push({
27021
28611
  id: ctx.nextHelperId++,
27022
28612
  loc: devLoc(ctx, node),
28613
+ origin: node,
27023
28614
  isChild: true,
28615
+ signalSite: directSignalSite(ctx, node, 'binding'),
27024
28616
  hostPath: path,
27025
28617
  onlyChildText: true,
27026
28618
  potentialDangerouslySetInnerHTML,
@@ -27065,7 +28657,14 @@ function emitElementHtml(
27065
28657
  if (hasNestedHoist) children = children.filter((n) => n.type !== 'HeadHoist');
27066
28658
  }
27067
28659
  // Special case: a single Text child (only-child text fast path).
27068
- if (children.length === 1 && children[0].type === 'Text') {
28660
+ if (
28661
+ children.length === 1 &&
28662
+ children[0].type === 'Text' &&
28663
+ !children[0]._octaneBindingText &&
28664
+ !children[0]._octaneBindingOpaque &&
28665
+ !children[0]._octaneBindingValue &&
28666
+ !children[0]._octaneBindingSlot
28667
+ ) {
27069
28668
  const txtChild = children[0];
27070
28669
  const staticLit = staticTextLiteral(txtChild.expression);
27071
28670
  if (staticLit !== null) {
@@ -27089,15 +28688,23 @@ function emitElementHtml(
27089
28688
  ]
27090
28689
  : null;
27091
28690
  appendTemplatePart(html, escaped, 'text', origins);
27092
- } else if (isKnownTextChildExpression(txtChild.expression, ctx.knownStringChildLocals)) {
28691
+ } else if (
28692
+ isDirectSignalHandleExpression(txtChild.expression) ||
28693
+ isKnownTextChildExpression(txtChild.expression, ctx.knownStringChildLocals)
28694
+ ) {
28695
+ const binding = markDirectSignalBinding(
28696
+ {
28697
+ id: bindings.length,
28698
+ kind: 'textOnlyChild',
28699
+ expr: resolveStyleExpr(txtChild.expression, cssHash),
28700
+ path,
28701
+ },
28702
+ ctx,
28703
+ txtChild.expression,
28704
+ 'binding',
28705
+ );
27093
28706
  const seededText = tag !== 'template' && !tag.includes('-') && !directPropNames.has('is');
27094
- bindings.push({
27095
- id: bindings.length,
27096
- kind: 'textOnlyChild',
27097
- expr: resolveStyleExpr(txtChild.expression, cssHash),
27098
- path,
27099
- seededText,
27100
- });
28707
+ bindings.push({ ...binding, seededText });
27101
28708
  // A nonempty placeholder survives the HTML parser as one Text node. The
27102
28709
  // mount writes its actual value before the cloned host is inserted, so
27103
28710
  // even an empty value retains one stable text binding node. <template>
@@ -27239,8 +28846,23 @@ function emitElementHtml(
27239
28846
  childIdx++;
27240
28847
  continue;
27241
28848
  }
27242
- if (child.type === 'Text') {
27243
- const staticLit = staticTextLiteral(child.expression);
28849
+ if (child._octaneBindingValue || child._octaneBindingSlot) {
28850
+ const ch = makeChildCall(
28851
+ child.expression,
28852
+ ctx,
28853
+ componentName,
28854
+ inlinedSubs,
28855
+ cssHash,
28856
+ childNs,
28857
+ );
28858
+ ch.bindingMarker = bindingChildMarker(child);
28859
+ ch.hostPath = path;
28860
+ ch.anchorPath = [...path, childIdx];
28861
+ compCalls.push(ch);
28862
+ appendTemplatePart(html, '<!>', 'anchor');
28863
+ childIdx++;
28864
+ } else if (child.type === 'Text') {
28865
+ const staticLit = child._octaneBindingText ? null : staticTextLiteral(child.expression);
27244
28866
  if (staticLit === '') {
27245
28867
  // Renders nothing: bake nothing and consume no childIndex (an empty
27246
28868
  // text produces NO node when the template HTML is parsed, so counting
@@ -27272,17 +28894,30 @@ function emitElementHtml(
27272
28894
  appendTemplatePart(html, escaped, 'text', origins);
27273
28895
  prevBakedText = true;
27274
28896
  if (!prevBaked) childIdx++;
27275
- } else if (isKnownTextChildExpression(child.expression, ctx.knownStringChildLocals)) {
27276
- bindings.push({
27277
- id: bindings.length,
27278
- kind: 'text',
27279
- expr: resolveStyleExpr(child.expression, cssHash),
27280
- path,
27281
- childIndex: childIdx,
27282
- // A text-producing neighbour → the server emits a `<!-- -->`
27283
- // separator here; the walk must be hole-aware (see hasHoles).
27284
- adjacentText: hasTextNeighbor(adjKinds, childI),
27285
- });
28897
+ } else if (
28898
+ isDirectSignalHandleExpression(child.expression) ||
28899
+ isKnownTextChildExpression(child.expression, ctx.knownStringChildLocals)
28900
+ ) {
28901
+ bindings.push(
28902
+ markDirectSignalBinding(
28903
+ {
28904
+ id: bindings.length,
28905
+ kind: 'text',
28906
+ expr: resolveStyleExpr(child.expression, cssHash),
28907
+ bindingMarker: child._octaneBindingText
28908
+ ? bindingMarker(child._octaneBindingText, 't')
28909
+ : undefined,
28910
+ path,
28911
+ childIndex: childIdx,
28912
+ // A text-producing neighbour → the server emits a `<!-- -->`
28913
+ // separator here; the walk must be hole-aware (see hasHoles).
28914
+ adjacentText: hasTextNeighbor(adjKinds, childI),
28915
+ },
28916
+ ctx,
28917
+ child.expression,
28918
+ 'binding',
28919
+ ),
28920
+ );
27286
28921
  appendTemplatePart(html, '<!>', 'anchor'); // placeholder we'll replace at mount
27287
28922
  childIdx++;
27288
28923
  } else {
@@ -27461,24 +29096,36 @@ function emitElementHtml(
27461
29096
  } else if (isConditionalJsx(expr)) {
27462
29097
  // Lower `{cond ? A : B}` (where A or B is JSX) to an IfStatement so
27463
29098
  // each branch renders real DOM via the existing ifBlock machinery.
27464
- const asIf = {
27465
- ...b.if(expr.test, wrapAsBlockStmt(expr.consequent), wrapAsBlockStmt(expr.alternate)),
27466
- loc: expr.loc, // carry source position for dev hydration-mismatch LOC
27467
- };
29099
+ const asIf = conditionalJsxStatement(expr);
27468
29100
  const ic = makeIfCall(asIf, ctx, inlinedSubs, childNs, cssHash);
27469
29101
  ic.hostPath = path;
27470
29102
  ic.anchorPath = [...path, childIdx];
27471
29103
  ifCalls.push(ic);
27472
29104
  appendTemplatePart(html, '<!>', 'anchor');
27473
29105
  childIdx++;
27474
- } else if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
27475
- bindings.push({
27476
- id: bindings.length,
27477
- kind: 'text',
27478
- expr: tsrxExprNode(resolveStyleExpr(expr, cssHash), ctx, componentName, inlinedSubs),
27479
- path,
27480
- childIndex: childIdx,
27481
- });
29106
+ } else if (
29107
+ isDirectSignalHandleExpression(expr) ||
29108
+ isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
29109
+ ) {
29110
+ bindings.push(
29111
+ markDirectSignalBinding(
29112
+ {
29113
+ id: bindings.length,
29114
+ kind: 'text',
29115
+ expr: tsrxExprNode(
29116
+ resolveStyleExpr(expr, cssHash),
29117
+ ctx,
29118
+ componentName,
29119
+ inlinedSubs,
29120
+ ),
29121
+ path,
29122
+ childIndex: childIdx,
29123
+ },
29124
+ ctx,
29125
+ expr,
29126
+ 'binding',
29127
+ ),
29128
+ );
27482
29129
  appendTemplatePart(html, '<!>', 'anchor');
27483
29130
  childIdx++;
27484
29131
  } else {
@@ -27687,6 +29334,17 @@ function hoistBodyHelper(
27687
29334
  id: b.id(helperName, idOrigin ?? fakeOrigin ?? undefined),
27688
29335
  params: params || [],
27689
29336
  body: bodyStmts,
29337
+ ...(ctx.presentationHydration?.structural
29338
+ ? {
29339
+ _octanePresentationHydration: {
29340
+ id: ctx.presentationHydration.id,
29341
+ supported:
29342
+ ctx.presentationHydration.supported ||
29343
+ ctx.presentationHydration.conditionalRest === true,
29344
+ structural: true,
29345
+ },
29346
+ }
29347
+ : {}),
27690
29348
  },
27691
29349
  fakeOrigin,
27692
29350
  );
@@ -27831,6 +29489,7 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
27831
29489
  origin: node,
27832
29490
  condExpr: node.test, // the fold replaces this with a `props.hN` member read
27833
29491
  condTest: node.test, // raw test AST — the fold threads it as a `props.hN` hole
29492
+ bindingSite: node._octaneBindingSite,
27834
29493
  envNames,
27835
29494
  thenHelper: thenHelperName,
27836
29495
  elseHelper: elseHelperName,
@@ -28079,6 +29738,7 @@ function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash, parentNs
28079
29738
  loc: devLoc(ctx, expr),
28080
29739
  origin: expr,
28081
29740
  isChild: true,
29741
+ signalSite: directSignalSite(ctx, expr, 'binding'),
28082
29742
  valueExpr: tsrxExprNode(
28083
29743
  resolveStyleExpr(rewriteChildHoleValue(expr, ctx), cssHash),
28084
29744
  ctx,
@@ -28288,6 +29948,7 @@ function makeCompCall(
28288
29948
  cssHash = null,
28289
29949
  ) {
28290
29950
  const id = ctx.nextHelperId++;
29951
+ const invocationSite = componentInvocationSite(ctx, node);
28291
29952
  const compName = tagBindingName(node);
28292
29953
  const staticFragmentRenderer = node.openingElement?.metadata?.staticFragmentRenderer;
28293
29954
  const activityDescriptor = isActivityLongForm(node, ctx);
@@ -28427,23 +30088,31 @@ function makeCompCall(
28427
30088
  ctx.hoistedHelpers.push(inheritOriginLoc(b.const(bodyIdentity, b.object([])), node));
28428
30089
  childrenArgs.push(b.id(bodyIdentity));
28429
30090
  }
28430
- propNodes.push(
28431
- b.prop('init', b.literal('children'), b.call('_$markChildrenBlock', ...childrenArgs)),
28432
- );
30091
+ let childrenValue = b.call('_$markChildrenBlock', ...childrenArgs);
30092
+ if (ctx.presentationHydration?.structural && node._octaneBindingSite)
30093
+ childrenValue = b.call(
30094
+ requireRuntimeForContext(ctx, 'markBindingChildren'),
30095
+ childrenValue,
30096
+ b.literal(node._octaneBindingSite.id + ';' + node._octaneBindingSite.site),
30097
+ );
30098
+ propNodes.push(b.prop('init', b.literal('children'), childrenValue));
28433
30099
  }
28434
30100
  }
28435
30101
 
28436
30102
  // The props object as a node; the call-site emit embeds it directly.
28437
30103
  const propsExpr = staticFragmentRenderer?.props ?? inheritOriginLoc(b.object(propNodes), node);
28438
30104
  if (descriptorConfig) {
28439
- ctx.runtimeNeeded.add('createElement');
30105
+ ctx.runtimeNeeded.add('createElementFromConfig');
28440
30106
  return {
28441
30107
  id,
28442
30108
  loc: devLoc(ctx, node),
28443
30109
  origin: node,
28444
30110
  isChild: true,
28445
30111
  anchorlessAppendSafe: true,
28446
- valueExpr: inheritOriginLoc(b.call('_$createElement', compNode, propsExpr), node),
30112
+ valueExpr: inheritOriginLoc(
30113
+ b.call('_$createElementFromConfig', b.literal(invocationSite), compNode, propsExpr),
30114
+ node,
30115
+ ),
28447
30116
  };
28448
30117
  }
28449
30118
 
@@ -28619,11 +30288,13 @@ function makeCompCall(
28619
30288
 
28620
30289
  return {
28621
30290
  id,
30291
+ invocationSite,
28622
30292
  compNode,
28623
30293
  propsExpr,
28624
30294
  hostPath: null,
28625
30295
  keyExpr,
28626
- liteEligible,
30296
+ bindingSite: node._octaneBindingSite,
30297
+ liteEligible: node._octaneBindingSite ? false : liteEligible,
28627
30298
  anchorlessAppendSafe,
28628
30299
  autoMemoDeps,
28629
30300
  autoMemoDepNodes,
@@ -28632,8 +30303,8 @@ function makeCompCall(
28632
30303
  autoMemoLite,
28633
30304
  autoMemoPublicationWitnesses,
28634
30305
  voidComponent,
28635
- singleRoot,
28636
- maybeSingleRoot,
30306
+ singleRoot: node._octaneBindingSite ? false : singleRoot,
30307
+ maybeSingleRoot: node._octaneBindingSite ? false : maybeSingleRoot,
28637
30308
  loc: devLoc(ctx, node),
28638
30309
  origin: node,
28639
30310
  };
@@ -29411,6 +31082,10 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
29411
31082
  }
29412
31083
  }
29413
31084
 
31085
+ if (node._octaneBindingSite) {
31086
+ singleRoot = false;
31087
+ singleRootExpr = null;
31088
+ }
29414
31089
  const ssrMarkerless = isSsrMarkerlessForItem(node);
29415
31090
  const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
29416
31091
  const hostMountSafe =