octane 0.2.11 → 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 (264) 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/context-epoch.cjs +38 -0
  8. package/dist/cjs/css.cjs +6 -27
  9. package/dist/cjs/data-encoding.cjs +160 -0
  10. package/dist/cjs/dom-binding-handoff.cjs +93 -0
  11. package/dist/cjs/dom-binding-protocol.cjs +115 -0
  12. package/dist/cjs/dom-binding-styles.cjs +292 -0
  13. package/dist/cjs/dom-bindings.cjs +700 -0
  14. package/dist/cjs/dom-focused-move.cjs +66 -0
  15. package/dist/cjs/dom-stage.cjs +896 -0
  16. package/dist/cjs/dom-tables.cjs +4 -94
  17. package/dist/cjs/error-codes.client.generated.cjs +33 -0
  18. package/dist/cjs/error-codes.server.generated.cjs +24 -0
  19. package/dist/cjs/hydration/control-capture.cjs +244 -0
  20. package/dist/cjs/hydration/event-capture.cjs +125 -43
  21. package/dist/cjs/hydration/interaction-config.cjs +50 -2
  22. package/dist/cjs/hydration-markers.cjs +37 -0
  23. package/dist/cjs/independent-hydration-protocol.cjs +106 -0
  24. package/dist/cjs/index.cjs +9 -0
  25. package/dist/cjs/runtime.cjs +6397 -1683
  26. package/dist/cjs/runtime.server.cjs +1465 -213
  27. package/dist/cjs/server/early-signals.cjs +45 -0
  28. package/dist/cjs/server/index.cjs +47 -0
  29. package/dist/cjs/server/rpc-batch.cjs +149 -0
  30. package/dist/cjs/server/rpc-stream.cjs +146 -0
  31. package/dist/cjs/server/rpc.cjs +34 -15
  32. package/dist/cjs/server/signal-query-observation.cjs +158 -0
  33. package/dist/cjs/server/streamed-region.cjs +52 -0
  34. package/dist/cjs/server/streamed-signals.cjs +512 -0
  35. package/dist/cjs/server-call.cjs +103 -0
  36. package/dist/cjs/server-rpc-batch-client.cjs +141 -0
  37. package/dist/cjs/server-rpc-client.cjs +94 -27
  38. package/dist/cjs/server-rpc-protocol.cjs +141 -0
  39. package/dist/cjs/server-rpc-stream-client.cjs +118 -0
  40. package/dist/cjs/signals/actions.cjs +474 -0
  41. package/dist/cjs/signals/client.cjs +4 -1
  42. package/dist/cjs/signals/computations.cjs +460 -0
  43. package/dist/cjs/signals/control-binding.cjs +368 -0
  44. package/dist/cjs/signals/derived-facade.cjs +50 -0
  45. package/dist/cjs/signals/document-owner.cjs +71 -0
  46. package/dist/cjs/signals/early-values.cjs +155 -0
  47. package/dist/cjs/signals/encoding.cjs +6 -95
  48. package/dist/cjs/signals/engine.cjs +276 -12
  49. package/dist/cjs/signals/errors.cjs +18 -0
  50. package/dist/cjs/signals/facade.cjs +477 -0
  51. package/dist/cjs/signals/graph.cjs +166 -20
  52. package/dist/cjs/signals/handle-protocol.cjs +36 -0
  53. package/dist/cjs/signals/index.cjs +81 -2
  54. package/dist/cjs/signals/native-read-client.cjs +10 -0
  55. package/dist/cjs/signals/owner-context.cjs +112 -0
  56. package/dist/cjs/signals/query-attempt-observer.cjs +68 -0
  57. package/dist/cjs/signals/query-facade.cjs +72 -0
  58. package/dist/cjs/signals/read-protocol.cjs +54 -0
  59. package/dist/cjs/signals/requests.cjs +287 -14
  60. package/dist/cjs/signals/scalar-computations.cjs +76 -0
  61. package/dist/cjs/signals/server.cjs +4 -1
  62. package/dist/cjs/signals/transition-candidate.cjs +567 -0
  63. package/dist/cjs/signals/transition-state.cjs +106 -0
  64. package/dist/cjs/signals/types.cjs +50 -0
  65. package/dist/cjs/stream-protocol.cjs +3 -0
  66. package/dist/cjs/streamed-signals-protocol.cjs +159 -0
  67. package/dist/cjs/style-values.cjs +122 -0
  68. package/dist/cjs/trusted-html.cjs +30 -0
  69. package/dist/cjs/version.cjs +1 -1
  70. package/dist/class-names.d.ts +5 -0
  71. package/dist/class-names.js +25 -0
  72. package/dist/compiler/bundler.js +51 -22
  73. package/dist/compiler/compile.js +2534 -581
  74. package/dist/compiler/dom-binding-program.js +935 -0
  75. package/dist/compiler/dom-binding-request.js +57 -0
  76. package/dist/compiler/dom-bindings.js +2245 -0
  77. package/dist/compiler/hook-deps.js +759 -10
  78. package/dist/compiler/hydrate-boundaries.js +358 -15
  79. package/dist/compiler/index.d.ts +39 -2
  80. package/dist/compiler/index.js +1 -0
  81. package/dist/compiler/native-attribute-reads.js +127 -0
  82. package/dist/compiler/native-change-diagnostics.js +32 -38
  83. package/dist/compiler/native-read-diagnostics.js +69 -8
  84. package/dist/compiler/native-read-types.js +80 -34
  85. package/dist/compiler/parser.node.js +1 -1
  86. package/dist/compiler/renderer-boundaries.js +40 -0
  87. package/dist/compiler/server-context.js +118 -0
  88. package/dist/compiler/signal-attempt-reads.js +479 -0
  89. package/dist/compiler/signal-declarations.js +557 -0
  90. package/dist/compiler/signal-start-reads.js +413 -0
  91. package/dist/compiler/slot-hooks.d.ts +3 -1
  92. package/dist/compiler/slot-hooks.js +168 -34
  93. package/dist/compiler/strong-auto-memo.js +90 -0
  94. package/dist/compiler/strong-html.js +143 -0
  95. package/dist/compiler/strong-mode.js +764 -66
  96. package/dist/compiler/strong-template-policy.js +307 -0
  97. package/dist/compiler/template-ir.js +200 -0
  98. package/dist/compiler/typescript.d.ts +2 -0
  99. package/dist/compiler/typescript.js +2 -0
  100. package/dist/compiler/vite.d.ts +3 -1
  101. package/dist/compiler/vite.js +122 -4
  102. package/dist/compiler/volar.LICENSES.txt +1 -1
  103. package/dist/compiler/volar.d.ts +6 -1
  104. package/dist/compiler/volar.js +750 -1433
  105. package/dist/constants.d.ts +1 -0
  106. package/dist/constants.js +10 -0
  107. package/dist/context-epoch.d.ts +2 -0
  108. package/dist/context-epoch.js +13 -0
  109. package/dist/css.d.ts +2 -5
  110. package/dist/css.js +2 -23
  111. package/dist/data-encoding.d.ts +9 -0
  112. package/dist/data-encoding.js +134 -0
  113. package/dist/dom-binding-classes.d.ts +15 -0
  114. package/dist/dom-binding-classes.js +129 -0
  115. package/dist/dom-binding-controls.d.ts +2 -0
  116. package/dist/dom-binding-controls.js +6 -0
  117. package/dist/dom-binding-handoff.d.ts +55 -0
  118. package/dist/dom-binding-handoff.js +62 -0
  119. package/dist/dom-binding-program.d.ts +300 -0
  120. package/dist/dom-binding-program.js +1625 -0
  121. package/dist/dom-binding-projections.d.ts +15 -0
  122. package/dist/dom-binding-projections.js +187 -0
  123. package/dist/dom-binding-protocol.d.ts +13 -0
  124. package/dist/dom-binding-protocol.js +88 -0
  125. package/dist/dom-binding-signals.d.ts +21 -0
  126. package/dist/dom-binding-signals.js +109 -0
  127. package/dist/dom-binding-styles.d.ts +17 -0
  128. package/dist/dom-binding-styles.js +272 -0
  129. package/dist/dom-bindings.d.ts +89 -0
  130. package/dist/dom-bindings.js +670 -0
  131. package/dist/dom-focused-move.d.ts +6 -0
  132. package/dist/dom-focused-move.js +42 -0
  133. package/dist/dom-stage.d.ts +52 -0
  134. package/dist/dom-stage.js +872 -0
  135. package/dist/dom-tables.d.ts +1 -3
  136. package/dist/dom-tables.js +1 -91
  137. package/dist/error-codes.client.generated.d.ts +9 -0
  138. package/dist/error-codes.client.generated.js +33 -0
  139. package/dist/error-codes.server.generated.d.ts +6 -0
  140. package/dist/error-codes.server.generated.js +24 -0
  141. package/dist/hydration/control-capture.d.ts +62 -0
  142. package/dist/hydration/control-capture.js +223 -0
  143. package/dist/hydration/document-lifecycle.d.ts +23 -0
  144. package/dist/hydration/document-lifecycle.js +80 -0
  145. package/dist/hydration/event-capture.d.ts +20 -1
  146. package/dist/hydration/event-capture.js +128 -42
  147. package/dist/hydration/independent-island.d.ts +45 -0
  148. package/dist/hydration/independent-island.js +233 -0
  149. package/dist/hydration/index.d.ts +8 -1
  150. package/dist/hydration/index.js +55 -1
  151. package/dist/hydration/interaction-config.d.ts +5 -0
  152. package/dist/hydration/interaction-config.js +44 -1
  153. package/dist/hydration/stream-delivery.d.ts +32 -0
  154. package/dist/hydration/stream-delivery.js +333 -0
  155. package/dist/hydration/stream-receiver.d.ts +26 -0
  156. package/dist/hydration/stream-receiver.js +204 -0
  157. package/dist/hydration/stream-result-receiver.d.ts +70 -0
  158. package/dist/hydration/stream-result-receiver.js +246 -0
  159. package/dist/hydration/streamed-signals.d.ts +44 -0
  160. package/dist/hydration/streamed-signals.js +186 -0
  161. package/dist/hydration/types.d.ts +6 -0
  162. package/dist/hydration-markers.d.ts +7 -0
  163. package/dist/hydration-markers.js +10 -0
  164. package/dist/independent-hydration-protocol.d.ts +31 -0
  165. package/dist/independent-hydration-protocol.js +80 -0
  166. package/dist/index.d.ts +3 -0
  167. package/dist/index.js +6 -0
  168. package/dist/internal/signal-read.d.ts +1 -0
  169. package/dist/internal/signal-read.js +4 -0
  170. package/dist/jsx-runtime-strong.d.ts +33 -0
  171. package/dist/jsx-runtime.d.ts +58 -2
  172. package/dist/public-types.d.ts +15 -1
  173. package/dist/runtime.d.ts +297 -23
  174. package/dist/runtime.js +6405 -1679
  175. package/dist/runtime.server.d.ts +110 -27
  176. package/dist/runtime.server.js +1458 -213
  177. package/dist/server/early-signals.d.ts +14 -0
  178. package/dist/server/early-signals.js +34 -0
  179. package/dist/server/index.d.ts +11 -2
  180. package/dist/server/index.js +50 -1
  181. package/dist/server/rpc-batch.d.ts +17 -0
  182. package/dist/server/rpc-batch.js +129 -0
  183. package/dist/server/rpc-stream.d.ts +9 -0
  184. package/dist/server/rpc-stream.js +125 -0
  185. package/dist/server/rpc.d.ts +5 -1
  186. package/dist/server/rpc.js +37 -13
  187. package/dist/server/signal-query-observation.d.ts +3 -0
  188. package/dist/server/signal-query-observation.js +134 -0
  189. package/dist/server/streamed-region.d.ts +15 -0
  190. package/dist/server/streamed-region.js +30 -0
  191. package/dist/server/streamed-signals.d.ts +45 -0
  192. package/dist/server/streamed-signals.js +488 -0
  193. package/dist/server-call.d.ts +41 -0
  194. package/dist/server-call.js +75 -0
  195. package/dist/server-rpc-batch-client.d.ts +7 -0
  196. package/dist/server-rpc-batch-client.js +119 -0
  197. package/dist/server-rpc-client.d.ts +3 -1
  198. package/dist/server-rpc-client.js +99 -27
  199. package/dist/server-rpc-protocol.d.ts +47 -0
  200. package/dist/server-rpc-protocol.js +109 -0
  201. package/dist/server-rpc-stream-client.d.ts +5 -0
  202. package/dist/server-rpc-stream-client.js +98 -0
  203. package/dist/signals/actions.d.ts +12 -0
  204. package/dist/signals/actions.js +466 -0
  205. package/dist/signals/client.d.ts +2 -0
  206. package/dist/signals/client.js +1 -0
  207. package/dist/signals/computations.d.ts +37 -0
  208. package/dist/signals/computations.js +455 -0
  209. package/dist/signals/control-binding.d.ts +31 -0
  210. package/dist/signals/control-binding.js +353 -0
  211. package/dist/signals/derived-facade.d.ts +3 -0
  212. package/dist/signals/derived-facade.js +25 -0
  213. package/dist/signals/document-owner.d.ts +10 -0
  214. package/dist/signals/document-owner.js +43 -0
  215. package/dist/signals/early-values.d.ts +24 -0
  216. package/dist/signals/early-values.js +124 -0
  217. package/dist/signals/encoding.d.ts +1 -3
  218. package/dist/signals/encoding.js +3 -93
  219. package/dist/signals/engine.d.ts +53 -4
  220. package/dist/signals/engine.js +275 -16
  221. package/dist/signals/errors.d.ts +7 -0
  222. package/dist/signals/errors.js +16 -0
  223. package/dist/signals/facade.d.ts +74 -0
  224. package/dist/signals/facade.js +452 -0
  225. package/dist/signals/graph.d.ts +52 -6
  226. package/dist/signals/graph.js +174 -19
  227. package/dist/signals/handle-protocol.d.ts +4 -0
  228. package/dist/signals/handle-protocol.js +11 -0
  229. package/dist/signals/index.d.ts +12 -3
  230. package/dist/signals/index.js +71 -2
  231. package/dist/signals/native-read-client.d.ts +3 -0
  232. package/dist/signals/native-read-client.js +10 -0
  233. package/dist/signals/owner-context.d.ts +22 -0
  234. package/dist/signals/owner-context.js +77 -0
  235. package/dist/signals/query-attempt-observer.d.ts +49 -0
  236. package/dist/signals/query-attempt-observer.js +41 -0
  237. package/dist/signals/query-facade.d.ts +3 -0
  238. package/dist/signals/query-facade.js +49 -0
  239. package/dist/signals/read-protocol.d.ts +37 -2
  240. package/dist/signals/read-protocol.js +44 -0
  241. package/dist/signals/requests.d.ts +38 -5
  242. package/dist/signals/requests.js +298 -15
  243. package/dist/signals/scalar-computations.d.ts +2 -0
  244. package/dist/signals/scalar-computations.js +60 -0
  245. package/dist/signals/server.d.ts +2 -0
  246. package/dist/signals/server.js +1 -0
  247. package/dist/signals/transition-candidate.d.ts +71 -0
  248. package/dist/signals/transition-candidate.js +557 -0
  249. package/dist/signals/transition-state.d.ts +51 -0
  250. package/dist/signals/transition-state.js +70 -0
  251. package/dist/signals/types.d.ts +95 -5
  252. package/dist/signals/types.js +20 -0
  253. package/dist/stream-protocol.d.ts +0 -5
  254. package/dist/stream-protocol.js +3 -0
  255. package/dist/streamed-signals-protocol.d.ts +60 -0
  256. package/dist/streamed-signals-protocol.js +130 -0
  257. package/dist/style-values.d.ts +3 -0
  258. package/dist/style-values.js +96 -0
  259. package/dist/trusted-html.d.ts +17 -0
  260. package/dist/trusted-html.js +6 -0
  261. package/dist/universal-core.d.ts +6 -0
  262. package/dist/universal-core.js +214 -202
  263. package/dist/version.js +1 -1
  264. package/package.json +48 -5
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Architecture:
6
6
  * 1. Parse TSRX through the environment-selected @tsrx/core-compatible
7
- * parser (native oxc-tsrx in Node, pure JavaScript elsewhere), then run
7
+ * parser (native @tsrx/oxc in Node, pure JavaScript elsewhere), then run
8
8
  * @tsrx/core's target-neutral semantic analysis on the authored module.
9
9
  * 2. For each top-level node:
10
10
  * - Component (`@{ … }` body or a return-JSX function) → compile to a
@@ -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,
@@ -82,6 +89,7 @@ import { assertNoLiveClientOnlyImports } from './client-only-server.js';
82
89
  import { nsForChildren, nsForSelf } from './jsx-namespace.js';
83
90
  import { analyzeNativeChangeDiagnostics } from './native-change-diagnostics.js';
84
91
  import { assertStrongMode } from './strong-mode.js';
92
+ import { applyStrongAutomaticMemo } from './strong-auto-memo.js';
85
93
  import {
86
94
  assertNativeReadDiagnostics,
87
95
  assertNativeReadOptions,
@@ -92,7 +100,24 @@ import {
92
100
  wrapNativeReadScope,
93
101
  wrapNativeWarmScope,
94
102
  } from './native-read-codegen.js';
95
- 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';
96
121
  import { collectProvenContextBindings, isProvenContextUse } from './context-use.js';
97
122
  import { applyCssModuleConstants } from './css-module-constants.js';
98
123
  import { assertUniversalRuntimeTarget, normalizeUniversalRuntime } from './universal-runtime.js';
@@ -438,7 +463,7 @@ function attrBindingHelper(bind) {
438
463
  case 'styleProperty':
439
464
  return 'setStyleProperty';
440
465
  case 'styleProperties':
441
- return 'setStyleProperties';
466
+ return bind.spread ? 'setStyle' : 'setStyleProperties';
442
467
  // SVG/MathML `className` is read-only — setClassAttr sets the attribute and
443
468
  // clsx-composes (setClassName composes on HTML).
444
469
  case 'class':
@@ -506,6 +531,135 @@ function attrBindingUpdateHelper(bind, inlineBindingGuards = false) {
506
531
  }
507
532
  }
508
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
+
509
663
  /**
510
664
  * An ALIAS: an authored range that emits nothing of its own but means the same
511
665
  * thing as one that does. A component's closing tag is the case — `</Card>` is
@@ -877,8 +1031,8 @@ function hasPotentialDangerouslySetInnerHTML(node) {
877
1031
  const attrs = node.attributes || node.openingElement?.attributes || [];
878
1032
  return attrs.some(
879
1033
  (attr) =>
880
- attr.type === 'SpreadAttribute' ||
881
- attr.type === 'JSXSpreadAttribute' ||
1034
+ ((attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') &&
1035
+ !attr._octaneKnownAttributeSpread) ||
882
1036
  ((attr.type === 'Attribute' || attr.type === 'JSXAttribute') &&
883
1037
  jsxAttrRawName(attr) === 'dangerouslySetInnerHTML'),
884
1038
  );
@@ -888,6 +1042,7 @@ function hasDefinitelyPresentDirectDangerouslySetInnerHTML(node) {
888
1042
  const attrs = node.attributes || node.openingElement?.attributes || [];
889
1043
  let state = false;
890
1044
  for (const attr of attrs) {
1045
+ if (attr._octaneKnownAttributeSpread) continue;
891
1046
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
892
1047
  // A later spread may overwrite or introduce the prop, so the effective
893
1048
  // writer is no longer statically known. A following direct writer can
@@ -914,6 +1069,7 @@ function hasDefinitelyPresentDirectChildrenProp(node) {
914
1069
  const attrs = node.attributes || node.openingElement?.attributes || [];
915
1070
  let state = false;
916
1071
  for (const attr of attrs) {
1072
+ if (attr._octaneKnownAttributeSpread) continue;
917
1073
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
918
1074
  state = null;
919
1075
  continue;
@@ -1190,6 +1346,10 @@ const HOOK_MEMO_RUNTIME_HELPERS = new Set([
1190
1346
  'memoPublishAlways',
1191
1347
  ]);
1192
1348
  const NATIVE_READ_RUNTIME_HELPERS = new Set([
1349
+ 'nativeStyleBinding',
1350
+ 'nativeProjectionBinding',
1351
+ 'readNativeDomStyle',
1352
+ 'readNativeDomProps',
1193
1353
  'enableNativeReadCollection',
1194
1354
  'beginNativeReadScope',
1195
1355
  'endNativeReadScope',
@@ -1209,6 +1369,29 @@ const NATIVE_READ_RUNTIME_HELPERS = new Set([
1209
1369
  'nativeCreateScopedElement',
1210
1370
  ]);
1211
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',
1393
+ 'canSplitStyleProperties',
1394
+ 'styleObjectPrototype',
1212
1395
  'replaceRef',
1213
1396
  'queueOwnRefDetach',
1214
1397
  'setPlainAttribute',
@@ -1229,6 +1412,18 @@ const INTERNAL_CLIENT_RUNTIME_HELPERS = new Set([
1229
1412
  ...NATIVE_READ_RUNTIME_HELPERS,
1230
1413
  ]);
1231
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',
1232
1427
  'ssrSpreadContent',
1233
1428
  ...NATIVE_READ_RUNTIME_HELPERS,
1234
1429
  ]);
@@ -1611,6 +1806,7 @@ function lowerImportedErrorBoundaries(ast) {
1611
1806
  function inheritSoleCompRoot(bodyNodes, ctx) {
1612
1807
  if (bodyNodes.length !== 1) return false;
1613
1808
  const n = bodyNodes[0];
1809
+ if (n._octaneBindingSite !== undefined) return false;
1614
1810
  if (n.type !== 'Element' || !isComponentTag(n)) return false;
1615
1811
  // Returned-JSX extraction can merge an authored key/spread config into one
1616
1812
  // spread hole. Its descriptor still needs its own keyed reconciliation range.
@@ -2208,15 +2404,23 @@ function staticObjectToCssString(obj) {
2208
2404
  // A literal prefix can live in the template when all following properties have
2209
2405
  // fixed, unique names. The values still evaluate together at the authored
2210
2406
  // style attribute, before any DOM writes. Fixed literal values
2211
- // after the first dynamic property stay in the ordered suffix. Spreads,
2212
- // computed names, and accessors retain ordinary object evaluation and diffing.
2407
+ // after the first dynamic property stay in the ordered suffix. Leading spreads
2408
+ // retain an object snapshot; only their fixed trailing properties are split.
2409
+ // Computed names and accessors retain ordinary object evaluation and diffing.
2213
2410
  function mixedStaticStyle(obj) {
2214
2411
  if (obj?.type !== 'ObjectExpression' || obj.properties.length === 0) return null;
2412
+ let spreadCount = 0;
2413
+ while (obj.properties[spreadCount]?.type === 'SpreadElement') spreadCount++;
2414
+ if (spreadCount === obj.properties.length) return null;
2415
+ const spread = spreadCount
2416
+ ? inheritOriginLoc(b.object(obj.properties.slice(0, spreadCount)), obj)
2417
+ : null;
2418
+ const properties = spreadCount ? obj.properties.slice(spreadCount) : obj.properties;
2215
2419
  const seen = new Map();
2216
2420
  let duplicates = false;
2217
2421
  const prefix = [];
2218
2422
  const dynamics = [];
2219
- for (const property of obj.properties) {
2423
+ for (const property of properties) {
2220
2424
  if (
2221
2425
  (property.type !== 'Property' && property.type !== 'ObjectProperty') ||
2222
2426
  property.computed ||
@@ -2234,6 +2438,9 @@ function mixedStaticStyle(obj) {
2234
2438
  if (
2235
2439
  name === '__proto__' ||
2236
2440
  name === 'dangerouslySetInnerHTML' ||
2441
+ // Integer keys enumerate before the spread's string keys, regardless
2442
+ // of their authored position. Keep their coercion order on the object path.
2443
+ (spread !== null && String(Number(name) >>> 0) === name && name !== '4294967295') ||
2237
2444
  (seen.has(normalized) && seen.get(normalized) !== name)
2238
2445
  ) {
2239
2446
  return null;
@@ -2255,6 +2462,7 @@ function mixedStaticStyle(obj) {
2255
2462
  return null;
2256
2463
  }
2257
2464
  if (
2465
+ spread === null &&
2258
2466
  value?.type === 'Literal' &&
2259
2467
  dynamics.length === 0 &&
2260
2468
  (typeof value.value === 'string' || typeof value.value === 'number') &&
@@ -2271,7 +2479,8 @@ function mixedStaticStyle(obj) {
2271
2479
  if (duplicates) {
2272
2480
  return {
2273
2481
  css: '',
2274
- dynamics: obj.properties.map((property) => ({
2482
+ spread,
2483
+ dynamics: properties.map((property) => ({
2275
2484
  name: property.key.type === 'Identifier' ? property.key.name : property.key.value,
2276
2485
  key: property.key,
2277
2486
  value: property.value,
@@ -2280,7 +2489,7 @@ function mixedStaticStyle(obj) {
2280
2489
  }
2281
2490
  if (dynamics.length === 0) return null;
2282
2491
  const css = staticObjectToCssString({ properties: prefix });
2283
- return prefix.length !== 0 && css === '' ? null : { css, dynamics };
2492
+ return prefix.length !== 0 && css === '' ? null : { css, dynamics, spread };
2284
2493
  }
2285
2494
 
2286
2495
  // ===========================================================================
@@ -4467,6 +4676,7 @@ function containsAutoMemoContextRead(root, ctx) {
4467
4676
  }
4468
4677
  if (seen.has(node)) return;
4469
4678
  seen.add(node);
4679
+
4470
4680
  if (
4471
4681
  node.type === 'ArrowFunctionExpression' ||
4472
4682
  node.type === 'FunctionExpression' ||
@@ -6365,6 +6575,26 @@ function containsAutoMemoUnsafeStructure(stmts, ctx = null) {
6365
6575
  if (containsDeferredRefRead(n)) found = true;
6366
6576
  return;
6367
6577
  }
6578
+ // Native styles perform implicit reads, including styles carried by a host
6579
+ // prop spread. Equal props cannot prove the subtree complete during a
6580
+ // boundary retry: these reads are outside the parent call's memo witness.
6581
+ if (
6582
+ ctx?.nativeReads &&
6583
+ n.type === 'JSXOpeningElement' &&
6584
+ n.name?.type === 'JSXIdentifier' &&
6585
+ /^[a-z]/.test(n.name.name) &&
6586
+ n.attributes.some((attribute) => {
6587
+ if (attribute.type === 'JSXSpreadAttribute' || attribute.type === 'SpreadAttribute')
6588
+ return true;
6589
+ if (attribute.name?.name !== 'style' || attribute.value?.type !== 'JSXExpressionContainer')
6590
+ return false;
6591
+ const value = unwrapTsExpr(attribute.value.expression);
6592
+ return value.type !== 'ObjectExpression' || !objectExprIsStaticLiteral(value);
6593
+ })
6594
+ ) {
6595
+ found = true;
6596
+ return;
6597
+ }
6368
6598
  if (
6369
6599
  t === 'AssignmentExpression' ||
6370
6600
  t === 'AssignmentPattern' ||
@@ -6795,6 +7025,19 @@ function wrapAsBlockStmt(node) {
6795
7025
  return b.block([node]);
6796
7026
  }
6797
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
+
6798
7041
  /** `xs.map(x => <li/>)` — detect so we can throw a useful "use for-of" error. */
6799
7042
  function isJsxReturningMapCall(node) {
6800
7043
  if (!node || node.type !== 'CallExpression') return false;
@@ -7298,6 +7541,7 @@ function anchorlessRootShape(node) {
7298
7541
  * the hydrator can use it as the keyed item boundary without an item pair.
7299
7542
  */
7300
7543
  function isSsrMarkerlessForItem(node) {
7544
+ if (node?._octaneBindingSite !== undefined) return false;
7301
7545
  const body = node?.body?.body || [];
7302
7546
  const jsxChildren = body.filter((s) => isJsxNode(s));
7303
7547
  return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
@@ -7878,21 +8122,6 @@ function identifierName(node) {
7878
8122
  return null;
7879
8123
  }
7880
8124
 
7881
- function unwrapServerFunctionInitializer(node) {
7882
- let current = node;
7883
- while (
7884
- current &&
7885
- (current.type === 'TSAsExpression' ||
7886
- current.type === 'TSSatisfiesExpression' ||
7887
- current.type === 'TSNonNullExpression' ||
7888
- current.type === 'TypeCastExpression' ||
7889
- current.type === 'ParenthesizedExpression')
7890
- ) {
7891
- current = current.expression;
7892
- }
7893
- return current;
7894
- }
7895
-
7896
8125
  function collectServerFunctionBindings(statements) {
7897
8126
  const functions = new Set();
7898
8127
  const aliases = new Map();
@@ -8055,6 +8284,8 @@ function analyzeServerModule(ast, filename) {
8055
8284
  const statements = declaration.body?.body || [];
8056
8285
  validateServerModuleIsolation(ast, declaration, filename);
8057
8286
  const functionBindings = collectServerFunctionBindings(statements);
8287
+ const functionNodes = collectServerFunctionNodes(statements);
8288
+ const contextImports = serverContextTypeImports(statements);
8058
8289
  for (const statement of statements) {
8059
8290
  if (statement.type === 'ExportDefaultDeclaration') {
8060
8291
  throw new Error('`module server` does not support default exports; use named functions.');
@@ -8111,7 +8342,15 @@ function analyzeServerModule(ast, filename) {
8111
8342
  }
8112
8343
  }
8113
8344
 
8114
- 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 };
8115
8354
  }
8116
8355
 
8117
8356
  /**
@@ -8129,15 +8368,19 @@ function emitServerModuleClientStubs(info, ctx) {
8129
8368
  if (!imported || !local) continue;
8130
8369
  ctx.runtimeNeeded.add('__serverRpc');
8131
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
+ ];
8132
8381
  nodes.push(
8133
8382
  inheritOriginLoc(
8134
- b.const(
8135
- local,
8136
- b.arrow(
8137
- [b.rest(b.id('args'))],
8138
- b.call('_$__serverRpc', b.literal(hash, JSON.stringify(hash)), b.id('args')),
8139
- ),
8140
- ),
8383
+ b.const(local, b.arrow([b.rest(b.id('args'))], b.call('_$__serverRpc', ...rpcArgs))),
8141
8384
  specifier,
8142
8385
  ),
8143
8386
  );
@@ -8204,13 +8447,29 @@ function emitServerModuleServerNodes(info, ctx) {
8204
8447
  ...namespaceBody,
8205
8448
  ];
8206
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
+ }
8207
8466
  serverStatements.push(
8208
8467
  inheritOriginLoc(
8209
8468
  b.stmt(
8210
8469
  b.assignment(
8211
8470
  '=',
8212
8471
  b.member(b.id('server'), b.literal(exported, JSON.stringify(exported)), true),
8213
- b.id(local),
8472
+ value,
8214
8473
  ),
8215
8474
  ),
8216
8475
  origin,
@@ -8849,11 +9108,326 @@ export function compile(source, filename, options) {
8849
9108
  return compileAuthored(source, filename, options, null);
8850
9109
  }
8851
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
+
8852
9426
  // Internal bundler entry point. Unlike the old __hydratePrepared handoff, this
8853
9427
  // always validates authored source and only exposes the hydrate slice that the
8854
9428
  // same compilation already prepared for production void-export classification.
8855
9429
  export function compileForBundler(source, filename, options) {
8856
- const metadata = { hydrateAst: null, cssModuleConstantImports: [] };
9430
+ const metadata = { hydrateAst: null, cssModuleConstantImports: [], independentWidgets: [] };
8857
9431
  const result = compileAuthored(source, filename, options, metadata);
8858
9432
  return { result, ...metadata };
8859
9433
  }
@@ -8864,22 +9438,98 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8864
9438
  throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
8865
9439
  }
8866
9440
  const cleanFilename = cleanCompileFilename(filename);
8867
- const analyzedAst = markParserSensitiveHosts(
9441
+ let analyzedAst = markParserSensitiveHosts(
8868
9442
  normalizeAuthoredJsxLiterals(parseModule(source, cleanFilename)),
8869
9443
  );
8870
9444
  analyzeTsrx(analyzedAst, cleanFilename);
8871
9445
  adoptParserAst(analyzedAst);
8872
9446
  options = nativeReadOptions(analyzedAst, options);
8873
9447
  assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
8874
- const strongModeEnabled =
8875
- assertStrongMode(analyzedAst, source, cleanFilename, options)?.enabled === true;
8876
- if (bundlerMetadata !== null) bundlerMetadata.hydrateAst = analyzedAst;
8877
- const textTypedAst = applyStringChildProofs(
8878
- analyzedAst,
8879
- source,
9448
+ const strongAnalysis = assertStrongMode(analyzedAst, source, cleanFilename, options);
9449
+ const strongModeEnabled = strongAnalysis?.enabled === true;
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
+ ),
8880
9520
  cleanFilename,
8881
- options?.textTypeFacts,
8882
9521
  );
9522
+ if (bundlerMetadata !== null) bundlerMetadata.hydrateAst = signalAst;
9523
+ const memoizedAst = strongModeEnabled
9524
+ ? applyStrongAutomaticMemo(signalAst, {
9525
+ ...options,
9526
+ filename: cleanFilename,
9527
+ strongHookAnalysis: strongAnalysis.strongHookAnalysis,
9528
+ })
9529
+ : signalAst;
9530
+ const textTypedAst = hasDomBindings
9531
+ ? memoizedAst
9532
+ : applyStringChildProofs(memoizedAst, source, cleanFilename, options?.textTypeFacts);
8883
9533
  let constantAst = textTypedAst;
8884
9534
  if (typeof options?.resolveCssModuleConstant === 'function') {
8885
9535
  const constants = applyCssModuleConstants(
@@ -8892,16 +9542,29 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8892
9542
  bundlerMetadata.cssModuleConstantImports = constants.imports;
8893
9543
  }
8894
9544
  }
8895
- return compileInternal(
9545
+ const result = compileInternal(
8896
9546
  source,
8897
9547
  filename,
8898
- options,
9548
+ strongAnalysis?.nativeChangeAnalysis
9549
+ ? {
9550
+ ...options,
9551
+ // Strong already analyzed the authored hosts. The classifications use
9552
+ // source offsets, which the copy-on-write transforms preserve.
9553
+ __nativeChangeAnalysis: strongAnalysis.nativeChangeAnalysis,
9554
+ __nativeChangeDiagnostics: strongAnalysis.nativeChangeAnalysis.diagnostics,
9555
+ }
9556
+ : options,
8899
9557
  constantAst,
8900
9558
  mode,
8901
9559
  bundlerMetadata,
8902
- textTypedAst !== analyzedAst,
9560
+ textTypedAst !== memoizedAst,
8903
9561
  strongModeEnabled,
8904
9562
  );
9563
+ if (strongAnalysis?.diagnostics.length > 0) {
9564
+ result.diagnostics = [...strongAnalysis.diagnostics, ...(result.diagnostics ?? [])];
9565
+ }
9566
+ if (bindingConstants !== undefined) result.bindingConstants = bindingConstants;
9567
+ return result;
8905
9568
  }
8906
9569
 
8907
9570
  function compileInternal(
@@ -8992,6 +9655,7 @@ function compileInternal(
8992
9655
  ).diagnostics;
8993
9656
  if (bundlerMetadata !== null) {
8994
9657
  bundlerMetadata.hydrateAst = hydratePreparation.ast;
9658
+ bundlerMetadata.independentWidgets = hydratePreparation.independentWidgets ?? [];
8995
9659
  }
8996
9660
  const compiled = compileInternal(
8997
9661
  source,
@@ -9341,6 +10005,8 @@ function compileInternal(
9341
10005
  ownedCssInjections: new Set(),
9342
10006
  componentOwners: [],
9343
10007
  currentComponentOwner: null,
10008
+ signalBindingsUsed: ast._octaneSignalDeclarations === true,
10009
+ signalBindingsEager: ast._octaneSignalDeclarations === true,
9344
10010
  currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
9345
10011
  currentMapTemps: null, // expression temps (map receiver/method, folded condition) owned by this function
9346
10012
  currentAutoMemoOffset: 0, // flat compiler-cache cell offset for the body being emitted
@@ -9871,18 +10537,18 @@ function compileInternal(
9871
10537
  ).filter((n) => n.type !== 'HeadHoist');
9872
10538
  if (inheritSoleCompRoot(bodyNodes, ctx)) eligible = false;
9873
10539
  }
9874
- info.eligible = eligible;
10540
+ info.eligible = eligible && !compNode._octaneBindingView;
9875
10541
  // Single-ELEMENT-root output: the component's body renders exactly one plain
9876
10542
  // DOM element (not a component tag, fragment, or control-flow). Such a
9877
10543
  // component self-delimits via that element on CLIENT mount — its
9878
10544
  // `componentSlot` needs no `comp`/`/comp` markers (singleRoot path), exactly
9879
10545
  // like a single-root `@for` item. (Output-shape based — independent of which
9880
10546
  // hooks it calls.)
9881
- info.singleRoot = singleHostComponentRoot(compNode);
10547
+ info.singleRoot = !compNode._octaneBindingView && singleHostComponentRoot(compNode);
9882
10548
  // Transitive @if-arm form of the same proof — resolved by the fixed point
9883
10549
  // below once every same-module callee an arm depends on is itself proven.
9884
10550
  info.singleRootIfDeps =
9885
- !info.singleRoot && isVoidJsxCodeBlockFunction(compNode)
10551
+ !compNode._octaneBindingView && !info.singleRoot && isVoidJsxCodeBlockFunction(compNode)
9886
10552
  ? collectSingleRootIfDeps(compNode.body.render, locals, ctx)
9887
10553
  : null;
9888
10554
  }
@@ -10442,6 +11108,22 @@ function compileInternal(
10442
11108
  ctx.runtimeNeeded.add('__vtSeen');
10443
11109
  vtHintNodes.push(inheritOriginLoc(b.stmt(b.call(rtAlias('__vtSeen'))), moduleOrigin));
10444
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
+ }
10445
11127
 
10446
11128
  // All remaining authored memo sites have now received their real hook slots.
10447
11129
  // Lower callable helpers, returned-JSX functions, explicit-slot calls and
@@ -10450,6 +11132,7 @@ function compileInternal(
10450
11132
  // accidentally import DOM memo helpers for a universal renderer's scope.
10451
11133
  let moduleBody = [
10452
11134
  ...nativeReadActivationNodes(ctx, moduleOrigin),
11135
+ ...signalBindingNodes,
10453
11136
  ...vtHintNodes,
10454
11137
  ...delegateNodes,
10455
11138
  ...styleNodes,
@@ -10502,9 +11185,12 @@ function compileInternal(
10502
11185
  };
10503
11186
  const printed = printNodeWithMap(program, ctx);
10504
11187
 
10505
- /** @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 }} */
10506
11189
  const result = {
10507
11190
  code: printed.code,
11191
+ ...(ast._octaneSignalDeclarations === true || ctx.nativeReads
11192
+ ? { streamedSignals: true }
11193
+ : null),
10508
11194
  map: {
10509
11195
  version: 3,
10510
11196
  sources: [ctx.mapSourceName],
@@ -10625,6 +11311,8 @@ function compileServer(
10625
11311
  nativeModuleStyles: options?.nativeReads === true && hasModuleStyleMaps(ast.body),
10626
11312
  hmr: false, // SSR never hot-swaps in place; client/server production slot shapes stay aligned
10627
11313
  dev: !!(options && options.dev),
11314
+ signalBindingsUsed: ast._octaneSignalDeclarations === true,
11315
+ signalBindingsEager: ast._octaneSignalDeclarations === true || options?.nativeReads === true,
10628
11316
  // SSR MIRROR of the parallel-`use()` pipeline (docs/suspense-parallel-use-
10629
11317
  // plan.md Phase 5): the same memoize (Pass A) + hoist/batch (Pass B)
10630
11318
  // transforms run on server bodies, emitting `_$puMemo`/`_$puBatch` — the
@@ -10803,12 +11491,34 @@ function compileServer(
10803
11491
  // the flush may register `hookSlots` as a needed runtime import.
10804
11492
  flushTailHookSymbols(ctx);
10805
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
+ }
10806
11510
  const runtimeImportNodes = buildRuntimeImportNodes(ctx, 'octane/server', ctx._moduleOrigin);
10807
11511
  const helperNodes = hoistedHelperNodes(ctx);
10808
11512
  const program = {
10809
11513
  type: 'Program',
10810
11514
  sourceType: 'module',
10811
- body: [...runtimeImportNodes, ...nativeActivation, ...helperNodes, ...bodyNodes],
11515
+ body: [
11516
+ ...runtimeImportNodes,
11517
+ ...nativeActivation,
11518
+ ...signalActivation,
11519
+ ...helperNodes,
11520
+ ...bodyNodes,
11521
+ ],
10812
11522
  metadata: { path: [] },
10813
11523
  start: ast.start,
10814
11524
  end: ast.end,
@@ -10817,6 +11527,9 @@ function compileServer(
10817
11527
  const printed = printNodeWithMap(program, ctx);
10818
11528
  const result = {
10819
11529
  code: printed.code,
11530
+ ...(ast._octaneSignalDeclarations === true || ctx.nativeReads
11531
+ ? { streamedSignals: true }
11532
+ : null),
10820
11533
  map: {
10821
11534
  version: 3,
10822
11535
  sources: [ctx.mapSourceName],
@@ -10939,6 +11652,20 @@ function compileServerComponent(node, ctx) {
10939
11652
  if (componentReferencedAboveDeclaration(ctx, node, name)) {
10940
11653
  const declaration = isDefault ? b.export_default(fn) : isExported ? b.export(fn) : fn;
10941
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
+ }
10942
11669
  if (warmNode !== null) {
10943
11670
  nodes.push(
10944
11671
  inheritOriginLoc(
@@ -10956,7 +11683,19 @@ function compileServerComponent(node, ctx) {
10956
11683
  return nodes;
10957
11684
  }
10958
11685
 
10959
- 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
+ }
10960
11699
  const declaration = inheritOriginLoc(b.const(name, initializer), node);
10961
11700
  const nodes = [
10962
11701
  isExported && !isDefault ? inheritOriginLoc(b.export(declaration), node) : declaration,
@@ -11262,8 +12001,19 @@ function ssrCompileBodyWithMapTemps(
11262
12001
  // needs no carrier. Other bodies can cross a component/value boundary, where
11263
12002
  // the carrier distinguishes compiled HTML from authored text returns. This
11264
12003
  // flag belongs to this body only; nested callbacks keep their own contract.
11265
- if (!unboxedHtml) ctx.runtimeNeeded.add('ssrHtml');
11266
- 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
+ );
11267
12017
  const origin =
11268
12018
  node.loc != null
11269
12019
  ? node
@@ -11294,10 +12044,20 @@ function ssrCompileBodyWithMapTemps(
11294
12044
  // flow — all serialize elements or `<!--[-->…<!--]-->` ranges
11295
12045
  // that break text-node adjacency on their own)
11296
12046
  function textAdjacencyKind(node, ctx) {
12047
+ if (
12048
+ node._octaneBindingText ||
12049
+ node._octaneBindingOpaque ||
12050
+ node._octaneBindingValue ||
12051
+ node._octaneBindingSlot
12052
+ )
12053
+ return 'other';
11297
12054
  if (node.type !== 'Text') return 'other';
11298
12055
  const lit = staticTextLiteral(node.expression);
11299
12056
  if (lit !== null) return lit === '' ? 'empty' : 'static';
11300
- return isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals) ? 'dyn' : 'other';
12057
+ return isDirectSignalHandleExpression(node.expression) ||
12058
+ isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)
12059
+ ? 'dyn'
12060
+ : 'other';
11301
12061
  }
11302
12062
 
11303
12063
  // Does the child at `i` have a text-producing sibling next to it (looking
@@ -11563,10 +12323,17 @@ function ssrEmitNode(
11563
12323
  componentNs,
11564
12324
  nlGuard = false,
11565
12325
  ) {
12326
+ if (node._octaneBindingValue || node._octaneBindingSlot)
12327
+ return ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
11566
12328
  switch (node.type) {
11567
12329
  case 'Text': {
11568
12330
  const expr = node.expression;
11569
- 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
+ ) {
11570
12337
  if (ctx.dev && ctx._ssrNestingText === true) {
11571
12338
  ctx.runtimeNeeded.add('ssrNestingText');
11572
12339
  return ssrCall(
@@ -11604,30 +12371,44 @@ function ssrEmitNode(
11604
12371
  // primitive to text) — mirrors Ripple's `{expr}` vs `{expr as string}`.
11605
12372
  // rewriteHookCalls: a `use(thenable)` in this hole bypasses the setup
11606
12373
  // rewrite, so key it here too (else it collides with sibling/nested use()).
11607
- if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
12374
+ if (
12375
+ isDirectSignalHandleExpression(expr) ||
12376
+ isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
12377
+ ) {
11608
12378
  // ssrTextPre = ssrText + the runtime leading-'\n' protection (the value
11609
12379
  // isn't known at compile time here).
11610
12380
  const fn =
11611
12381
  ctx.dev && ctx._ssrNestingText === true
11612
12382
  ? 'ssrNestingText'
11613
- : nlGuard
12383
+ : nlGuard && !node._octaneBindingText
11614
12384
  ? 'ssrTextPre'
11615
12385
  : 'ssrText';
11616
12386
  ctx.runtimeNeeded.add(fn);
11617
- 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;
11618
12392
  }
11619
12393
  // rewriteJsxValues lowers any JSX embedded in the expression (e.g.
11620
12394
  // `{cond && <div/>}`, a ternary, a `.map(x => <Row/>)`) to printable
11621
12395
  // createElement(...) descriptors — exactly like ssrEmitTsrxExpression and
11622
12396
  // the client makeChildCall. Without it the raw JSX leaks into the emitted
11623
12397
  // ssrChild(...) call as unparseable source.
11624
- const childHelper = nlGuard ? 'ssrChildPre' : 'ssrChild';
12398
+ const childHelper = node._octaneBindingOpaque
12399
+ ? 'ssrBindingChild'
12400
+ : nlGuard
12401
+ ? 'ssrChildPre'
12402
+ : 'ssrChild';
11625
12403
  ctx.runtimeNeeded.add(childHelper);
11626
12404
  const childExpr = ssrCall(
11627
12405
  childHelper,
11628
12406
  [
11629
12407
  resolveStyleExpr(rewriteJsxValues(rewriteHookCalls(expr, ctx, name), ctx), cssHash),
11630
12408
  b.id('__s'),
12409
+ ...(node._octaneBindingOpaque
12410
+ ? [b.literal(bindingMarker(node._octaneBindingOpaque, 'o'))]
12411
+ : []),
11631
12412
  ],
11632
12413
  node,
11633
12414
  );
@@ -11708,7 +12489,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11708
12489
  rejectVoidElementContent(tag, node, ctx);
11709
12490
  rejectTextareaValueChildren(tag, node, ctx);
11710
12491
  rejectDangerouslySetInnerHTMLChildren(tag, node, ctx);
11711
- const attrs = node.attributes || node.openingElement?.attributes || [];
12492
+ const attrs = expandKnownAttributeSpreads(
12493
+ node.attributes || node.openingElement?.attributes || [],
12494
+ );
11712
12495
  const devFormActionAttribute =
11713
12496
  ctx.dev && (tag === 'form' || tag === 'button' || tag === 'input')
11714
12497
  ? attrs.find(
@@ -11808,15 +12591,42 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11808
12591
  else directAttributeIdentities.add(identity);
11809
12592
  }
11810
12593
  const firstSpreadIdx = attrs.findIndex(
11811
- (a) => a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute',
12594
+ (a) => !a.knownSource && (a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute'),
11812
12595
  );
11813
- // Customized built-ins suppress native unknown-property diagnostics based on
11814
- // the final `is` prop, regardless of where that prop appears in JSX. Resolve
11815
- // directly authored DEV hosts through the existing whole-element attribute
11816
- // snapshot so sibling validators see that final value before serialization.
11817
- // Production keeps its original fully baked/direct-serializer paths.
11818
- const resolveDevCustomizedHost =
11819
- ctx.dev &&
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
+ }
12623
+ // Customized built-ins suppress native unknown-property diagnostics based on
12624
+ // the final `is` prop, regardless of where that prop appears in JSX. Resolve
12625
+ // directly authored DEV hosts through the existing whole-element attribute
12626
+ // snapshot so sibling validators see that final value before serialization.
12627
+ // Production keeps its original fully baked/direct-serializer paths.
12628
+ const resolveDevCustomizedHost =
12629
+ ctx.dev &&
11820
12630
  (selfNs === 'html' || selfNs === 'opaque') &&
11821
12631
  !tag.includes('-') &&
11822
12632
  directAttributeIdentities.has('is');
@@ -11964,13 +12774,40 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11964
12774
  };
11965
12775
 
11966
12776
  for (let attrI = 0; attrI < attrs.length; attrI++) {
11967
- 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
+ }
11968
12792
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
11969
12793
  ensureAttrPart();
12794
+ ctx.signalBindingsUsed = true;
11970
12795
  ctx.runtimeNeeded.add('ssrAttrs');
11971
12796
  ctx.runtimeNeeded.add('ssrSnapshotSpread');
11972
12797
  const tmp = bindAttributeEvaluation(
11973
- 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
+ ),
11974
12811
  );
11975
12812
  if (resolveFormControlsAcrossSpreads) {
11976
12813
  if (tag !== 'textarea' && formControlPart === -1) {
@@ -12013,6 +12850,29 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12013
12850
  }
12014
12851
  if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') continue;
12015
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
+ }
12016
12876
  if (rawAttrName === 'key') {
12017
12877
  bindDiscardedAttributeValue(attr.value);
12018
12878
  continue;
@@ -12084,10 +12944,13 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12084
12944
  const ctlInner =
12085
12945
  val == null ? null : val.type === 'JSXExpressionContainer' ? val.expression : val;
12086
12946
  if (tag === 'input') {
12087
- const ctlExpr =
12947
+ let ctlExpr =
12088
12948
  ctlInner === null
12089
12949
  ? inheritOriginLoc(b.literal(true, 'true'), attr)
12090
12950
  : tsrxExprNode(ctlInner, ctx, name, inlinedSubs);
12951
+ if (attrName === 'value' || attrName === 'checked') {
12952
+ ctlExpr = markDirectControl(ctlExpr, attr);
12953
+ }
12091
12954
  if (formControlPart === -1) {
12092
12955
  flush();
12093
12956
  formControlPart = parts.length;
@@ -12102,11 +12965,12 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12102
12965
  }
12103
12966
  // textarea / select: value/defaultValue never serialize as attributes —
12104
12967
  // captured for the content position (textarea) / projection scope (select).
12105
- const ctlExpr = bindAttributeEvaluation(
12968
+ let ctlExpr =
12106
12969
  ctlInner === null
12107
12970
  ? inheritOriginLoc(b.literal(true, 'true'), attr)
12108
- : tsrxExprNode(ctlInner, ctx, name, inlinedSubs),
12109
- );
12971
+ : tsrxExprNode(ctlInner, ctx, name, inlinedSubs);
12972
+ if (attrName === 'value') ctlExpr = markDirectControl(ctlExpr, attr);
12973
+ ctlExpr = bindAttributeEvaluation(ctlExpr);
12110
12974
  if (resolveFormControlsAcrossSpreads) {
12111
12975
  if (tag === 'select' && formControlPart === -1) {
12112
12976
  flush();
@@ -12115,6 +12979,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12115
12979
  }
12116
12980
  formControlSources.push(ssrDirectSource(ctx, attrName, ctlExpr, attr));
12117
12981
  } else if (attrName === 'value') {
12982
+ if (directSignalControlSite !== null) {
12983
+ formControlSources.push(ssrDirectSource(ctx, attrName, ctlExpr, attr));
12984
+ }
12118
12985
  ctlValue = ctlExpr;
12119
12986
  ctlValueAttr = attr;
12120
12987
  } else {
@@ -12238,6 +13105,8 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12238
13105
  cssHash,
12239
13106
  );
12240
13107
  attrExpr = tsrxExprNode(attrInner, ctx, name, inlinedSubs);
13108
+ if (!(ctx.nativeReads && attrName === 'style'))
13109
+ attrExpr = ssrSignalValue(attrExpr, ctx, attr, true);
12241
13110
  }
12242
13111
  attrSources.push(
12243
13112
  ssrDirectSource(
@@ -12279,6 +13148,13 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12279
13148
  let inner = val.type === 'JSXExpressionContainer' ? val.expression : val;
12280
13149
 
12281
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
+ }
12282
13158
  inner = resolveStyleExpr(inner, cssHash);
12283
13159
  if (!isAfterSpread && inner.type === 'Literal' && typeof inner.value === 'string') {
12284
13160
  bakeLit(` style="${escapeAttr(inner.value)}"`, attrName, attr.name, inner);
@@ -12297,13 +13173,11 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12297
13173
  flush();
12298
13174
  ctx.runtimeNeeded.add('ssrStyle');
12299
13175
  registerAttrLoweringOrigin(ctx, attr.name, 'ssrStyle', null);
12300
- parts.push(
12301
- ssrCall(
12302
- 'ssrStyle',
12303
- [bindAttributeEvaluation(tsrxExprNode(inner, ctx, name, inlinedSubs))],
12304
- attr,
12305
- ),
12306
- );
13176
+ inner = tsrxExprNode(inner, ctx, name, inlinedSubs);
13177
+ if (ctx.nativeReads)
13178
+ inner = b.call(requireRuntimeForContext(ctx, 'readNativeDomStyle'), inner);
13179
+ else inner = ssrSignalStyleObject(inner, ctx, attr);
13180
+ parts.push(ssrCall('ssrStyle', [bindAttributeEvaluation(inner)], attr));
12307
13181
  continue;
12308
13182
  }
12309
13183
 
@@ -12385,7 +13259,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12385
13259
  flush();
12386
13260
  ctx.runtimeNeeded.add('ssrAttr');
12387
13261
  registerAttrLoweringOrigin(ctx, attr.name, 'ssrAttr', attrName);
12388
- const valueExpr = bindAttributeEvaluation(tsrxExprNode(inner, ctx, name, inlinedSubs));
13262
+ const valueExpr = bindAttributeEvaluation(
13263
+ ssrSignalValue(tsrxExprNode(inner, ctx, name, inlinedSubs), ctx, attr, true),
13264
+ );
12389
13265
  if (
12390
13266
  devFormActionSources !== null &&
12391
13267
  (attrName === 'type' ||
@@ -12413,10 +13289,18 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12413
13289
  );
12414
13290
  parts.push(singleDirectAttribute);
12415
13291
  }
13292
+ let signalControlAttrs = null;
13293
+ if (directSignalControlSite !== null) {
13294
+ ctx.runtimeNeeded.add('ssrSignalControlAttrs');
13295
+ signalControlAttrs = ssrCall('ssrSignalControlAttrs', [b.array(formControlSources)], node);
13296
+ }
12416
13297
  if (formControlPart !== -1) {
12417
13298
  if (tag === 'input') {
12418
13299
  ctx.runtimeNeeded.add('ssrInputAttrs');
12419
- 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
+ }
12420
13304
  // React serializes/coerces ordinary attributes before projecting the
12421
13305
  // effective checked/value state. Expressions and spread getters have
12422
13306
  // already run into temps in authored order; move only this serialization
@@ -12427,8 +13311,18 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12427
13311
  parts.push(inputAttrs);
12428
13312
  } else {
12429
13313
  ctx.runtimeNeeded.add('ssrSelectAttrs');
12430
- parts[formControlPart] = ssrCall('ssrSelectAttrs', [b.array(formControlSources)], node);
12431
- }
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);
12432
13326
  }
12433
13327
  if (attrPart !== -1) {
12434
13328
  ctx.runtimeNeeded.add('ssrAttrs');
@@ -12439,6 +13333,7 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12439
13333
  b.literal(tag, JSON.stringify(tag)),
12440
13334
  b.literal(selfNs, JSON.stringify(selfNs)),
12441
13335
  b.literal(resolveFormControlsAcrossSpreads),
13336
+ ...(ctx.nativeReads ? [b.id(requireRuntimeForContext(ctx, 'readNativeDomStyle'))] : []),
12442
13337
  ],
12443
13338
  node,
12444
13339
  );
@@ -12527,6 +13422,9 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12527
13422
  } else if (
12528
13423
  htmlSources.length === 0 &&
12529
13424
  onlyChild0 !== null &&
13425
+ !onlyChild0._octaneBindingOpaque &&
13426
+ !onlyChild0._octaneBindingValue &&
13427
+ !onlyChild0._octaneBindingSlot &&
12530
13428
  staticTextLiteral(onlyChild0.expression) === null &&
12531
13429
  !isKnownTextChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
12532
13430
  ) {
@@ -12746,11 +13644,12 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12746
13644
 
12747
13645
  function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
12748
13646
  registerOriginAlias(ctx, node.closingElement?.name, node.id || node.openingElement?.name);
13647
+ const invocationSite = componentInvocationSite(ctx, node);
12749
13648
  // M3 inherit-range: consume the body-root flag ONCE, before this component's
12750
13649
  // props/children compile below (they recurse into ssrEmitNodes/ssrCompileSub
12751
13650
  // and must not inherit it). Set by ssrCompileBody only for the sole
12752
13651
  // comp-call root of a `@{}` body — which is exactly this emit.
12753
- const inherit = ctx._ssrInheritRoot === true;
13652
+ const inherit = ctx._ssrInheritRoot === true && !node._octaneBindingSite;
12754
13653
  ctx._ssrInheritRoot = false;
12755
13654
  // Capture before compiling attributes/children: nested subs temporarily mutate
12756
13655
  // this context flag. Only this component's immediate children sub inherits the
@@ -12905,11 +13804,11 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
12905
13804
  // The direct mode-only Activity was lowered to ActivityStatement. Richer
12906
13805
  // Activity configs and generic explicit-key + spread sites keep one ordered
12907
13806
  // element config, matching createElement without changing simple call sites.
12908
- ctx.runtimeNeeded.add('createElement');
13807
+ ctx.runtimeNeeded.add('createElementFromConfig');
12909
13808
  ctx.runtimeNeeded.add('ssrChild');
12910
13809
  const descriptor = ssrCall(
12911
- 'createElement',
12912
- [compExpr, inheritOriginLoc(b.object(propNodes), node)],
13810
+ 'createElementFromConfig',
13811
+ [b.literal(invocationSite), compExpr, inheritOriginLoc(b.object(propNodes), node)],
12913
13812
  node,
12914
13813
  );
12915
13814
  const child = ssrCall('ssrChild', [descriptor, b.id('__s')], node);
@@ -12929,16 +13828,38 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
12929
13828
  args.push(
12930
13829
  b.literal(componentNs, JSON.stringify(componentNs)),
12931
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),
12932
13840
  );
12933
- if (keyExpr !== null) args.push(keyExpr);
12934
- } else if (keyExpr !== null) {
12935
- args.push(b.literal(inherit, inherit ? 'true' : 'false'), keyExpr);
12936
- } else if (inherit) {
12937
- args.push(b.literal(true, 'true'));
13841
+ }
13842
+ if (node._octaneBindingSite) {
13843
+ args.push(b.literal(bindingMarker(node._octaneBindingSite, 'v')));
12938
13844
  }
12939
13845
  return ssrCall(helper, args, node);
12940
13846
  }
12941
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
+
12942
13863
  // ---------------------------------------------------------------------------
12943
13864
  // Server control flow — @if/@for/@switch/@try lowered to HTML-string builders.
12944
13865
  // Each branch/item/case body is compiled (via ssrCompileSub) into a server
@@ -13139,6 +14060,29 @@ function ssrEmitIf(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs)
13139
14060
  node.alternate,
13140
14061
  )
13141
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
+ }
13142
14086
  return ssrCall(
13143
14087
  'ssrBlock',
13144
14088
  [
@@ -13204,6 +14148,7 @@ function ssrEmitActivity(node, ctx, name, inlinedSubs, parentNs, cssHash, compon
13204
14148
  }
13205
14149
 
13206
14150
  function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
14151
+ const bindingSite = node._octaneBindingSite;
13207
14152
  // rewriteHookCalls: key any `use(thenable)` in the @for iterable expression.
13208
14153
  const itemsExpr = rewriteHookCalls(node.right, ctx, name);
13209
14154
  const mapCall = node.nativeArrayMap || null;
@@ -13307,22 +14252,37 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13307
14252
  node.index ? [b.id('__it'), b.id('__i')] : [b.id('__it')],
13308
14253
  node,
13309
14254
  );
13310
- const sharedItemRange = markerlessItem || canShareSsrComponentItemRange(node, ctx);
13311
- const itemHtml = sharedItemRange ? itemCall : ssrCall('ssrBlock', [itemCall], node);
13312
- 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
13313
14260
  ? ssrCall(
13314
14261
  'ssrForItem',
13315
14262
  [
13316
- itemKey,
14263
+ bindingSite ? b.id('__bindingKeyValue') : itemKey,
13317
14264
  b.id(itemSub.fnName),
13318
14265
  b.id('__it'),
13319
14266
  node.index ? b.id('__i') : ssrVoid(node),
13320
14267
  b.id('__s'),
13321
- b.literal(!sharedItemRange),
14268
+ b.literal(!sharedItemRange && !bindingSite),
14269
+ ...(mapCall === null ? [] : [b.literal(true)]),
13322
14270
  ],
13323
14271
  node,
13324
14272
  )
13325
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
+ }
13326
14286
  if (node.empty) ctx.runtimeNeeded.add('ssrArm');
13327
14287
  if (itemNeedsIdentity) ctx.runtimeNeeded.add('ssrForItem');
13328
14288
  // Render every item into one incrementally-built string. Avoid map().join():
@@ -13336,8 +14296,30 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13336
14296
  const index = b.id('__i');
13337
14297
  const loopBody = b.block([
13338
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
+ : []),
13339
14308
  b.stmt(b.assignment('+=', b.id('__html'), renderItem)),
13340
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);
13341
14323
  const render = b.arrow(
13342
14324
  [],
13343
14325
  b.block([
@@ -13349,10 +14331,11 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13349
14331
  ),
13350
14332
  b.if(
13351
14333
  b.binary('===', b.member(items, 'length'), b.literal(0)),
13352
- b.return(ssrCall('ssrForBlock', [emptyCall, b.literal(false, 'false')], node)),
14334
+ b.return(forRange(emptyCall, false)),
13353
14335
  null,
13354
14336
  ),
13355
- ...(itemNeedsIdentity && keyDeclaration !== null ? [keyDeclaration] : []),
14337
+ ...((itemNeedsIdentity || bindingSite) && keyDeclaration !== null ? [keyDeclaration] : []),
14338
+ ...(bindingSite ? [b.const('__bindingKeys', b.new(b.id('Set'), node))] : []),
13356
14339
  b.let('__html', b.literal('')),
13357
14340
  b.for(
13358
14341
  b.let('__i', b.literal(0)),
@@ -13360,7 +14343,7 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
13360
14343
  b.update('++', index),
13361
14344
  loopBody,
13362
14345
  ),
13363
- b.return(ssrCall('ssrForBlock', [b.id('__html'), b.literal(true, 'true')], node)),
14346
+ b.return(forRange(b.id('__html'), true)),
13364
14347
  ]),
13365
14348
  );
13366
14349
  const fast = ssrCall('ssrControl', [b.literal(ssrControlKey('for', node)), render], node);
@@ -13570,6 +14553,33 @@ function ssrControlKey(kind, node) {
13570
14553
  // renderer component so its template scopes and hydration range both survive.
13571
14554
  function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
13572
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
+ }
13573
14583
  if (node.returnedJsxValue === true && requiresTemplateNormalization(expr, 'html', true, ctx)) {
13574
14584
  // A returned JSX value that contains template-only syntax is one compiled
13575
14585
  // renderer on the client (rather than a descriptor array whose value
@@ -13594,7 +14604,19 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
13594
14604
  );
13595
14605
  inlinedSubs.push(sub.fn);
13596
14606
  ctx.runtimeNeeded.add('ssrComponent');
13597
- 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
+ );
13598
14620
  }
13599
14621
  if (
13600
14622
  expr &&
@@ -13624,10 +14646,7 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
13624
14646
  // position — body/arm/fragment roots and component-children roots,
13625
14647
  // where a rich hole rides the de-opt value path because a portal there
13626
14648
  // has no host to stamp (`_ssrHostChildPos`).
13627
- const asIf = {
13628
- ...b.if(expr.test, wrapAsBlockStmt(expr.consequent), wrapAsBlockStmt(expr.alternate)),
13629
- loc: expr.loc, // same devLoc/control-key position as the client's claim
13630
- };
14649
+ const asIf = conditionalJsxStatement(expr);
13631
14650
  return ssrEmitIf(asIf, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
13632
14651
  }
13633
14652
  ctx.runtimeNeeded.add('ssrChild');
@@ -14100,6 +15119,17 @@ function compileComponent(node, ctx, options) {
14100
15119
  // back to a full reload — the capture predates the wrapper).
14101
15120
  statements.push(guarded(b.assignment('=', b.id(name), b.call('_$hmr', b.id(name)))));
14102
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
+ }
14103
15133
  return { nodes: statements };
14104
15134
  }
14105
15135
 
@@ -14129,6 +15159,18 @@ function compileComponent(node, ctx, options) {
14129
15159
  // its fresh body to the previous canonical wrapper and re-export that same
14130
15160
  // identity. Production components retain their tree-shakeable const binding.
14131
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
+ }
14132
15174
  if (!hmrWrap && componentInfo?.singleRoot === true) {
14133
15175
  valueExpr = inheritOriginLoc(singleRootInitializer(ctx, valueExpr), node);
14134
15176
  componentInfo.singleRootInitialized = true;
@@ -14159,18 +15201,204 @@ function compileComponent(node, ctx, options) {
14159
15201
  return { nodes: [declNode] };
14160
15202
  }
14161
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
+
14162
15394
  /**
14163
- * Generate just the `function (...) { ... }` text for a component-shaped node.
14164
- * Used both for top-level components and for inlined for-of item bodies.
14165
- *
14166
- * `parentNs` is the namespace this body's JSX is rendered into. Top-level
14167
- * components use 'opaque' because their call site can select HTML, SVG, or
14168
- * MathML; an if/for/try body inherits its known host namespace.
14169
- *
14170
- * `cssHash` is the enclosing component's scoped-style hash (or null) — used to
14171
- * 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.
14172
15398
  */
14173
15399
  function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null, options = null) {
15400
+ const previousPresentationHydration = ctx.presentationHydration;
15401
+ ctx.presentationHydration = node._octanePresentationHydration;
14174
15402
  const returnedOutput = options?.returnedOutput === true;
14175
15403
  const previousAutoCalculatedRenderableRefs = ctx.currentAutoCalculatedRenderableRefs;
14176
15404
  let autoCalculatedDeclarations = null;
@@ -14650,7 +15878,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14650
15878
  b.member(b.member(b.id('__s'), 'block'), 'pending'),
14651
15879
  b.unary('!', b.member(b.member(b.id('__s'), 'block'), 'crossRenderUpdate')),
14652
15880
  ),
14653
- b.return(null),
15881
+ { ...b.return(null), _octanePendingOutputGuard: true },
14654
15882
  null,
14655
15883
  ),
14656
15884
  node,
@@ -14773,13 +16001,15 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14773
16001
  ctx.currentMapTemps = prevMapTemps;
14774
16002
  // ONE FunctionDeclaration node — the caller prints it (once, with the full
14775
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;
14776
16006
  const emittedFunction = b.function_declaration(
14777
16007
  b.id(name, node.id ?? node),
14778
16008
  fnParams,
14779
16009
  b.block(
14780
16010
  ctx.nativeReads
14781
- ? wrapNativeReadScope(bodyStatements, b.id('__s'), nativeReadNames(ctx))
14782
- : bodyStatements,
16011
+ ? wrapNativeReadScope(presentationBody, b.id('__s'), nativeReadNames(ctx))
16012
+ : presentationBody,
14783
16013
  ),
14784
16014
  );
14785
16015
  return inheritOriginLoc(
@@ -15351,6 +16581,10 @@ function collectDepPaths(expr) {
15351
16581
  return;
15352
16582
  }
15353
16583
  switch (n.type) {
16584
+ case 'MetaProperty':
16585
+ // `import.meta` and `new.target` contain syntax tokens, not free
16586
+ // bindings. Visiting their Identifier children creates invalid deps.
16587
+ return;
15354
16588
  case 'Identifier':
15355
16589
  if (!bound.has(n.name)) push(b.id(n.name), n.name);
15356
16590
  return;
@@ -18219,10 +19453,29 @@ function compileReturnJsxFunction(node, ctx, options) {
18219
19453
  // export follow-up statements — compile() embeds them in the single module
18220
19454
  // AST, whose one esrap print yields the whole module map (chained consumers
18221
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
+ : [];
18222
19474
  if (options && options.hmrWrap) {
18223
19475
  const nodes = [
18224
19476
  fn,
18225
19477
  inheritOriginLoc(b.stmt(b.assignment('=', b.id(name), b.call('_$hmr', b.id(name)))), node),
19478
+ ...bindingStamp,
18226
19479
  ];
18227
19480
  if (options.default) {
18228
19481
  nodes.push(
@@ -18239,10 +19492,11 @@ function compileReturnJsxFunction(node, ctx, options) {
18239
19492
  return { nodes };
18240
19493
  }
18241
19494
  if (options && options.default) {
18242
- return { nodes: [fn, inheritOriginLoc(b.export_default(b.id(name)), node)] };
19495
+ return { nodes: [fn, ...bindingStamp, inheritOriginLoc(b.export_default(b.id(name)), node)] };
18243
19496
  }
18244
- if (options && options.export) return { nodes: [inheritOriginLoc(b.export(fn), node)] };
18245
- return { nodes: [fn] };
19497
+ if (options && options.export)
19498
+ return { nodes: [inheritOriginLoc(b.export(fn), node), ...bindingStamp] };
19499
+ return { nodes: [fn, ...bindingStamp] };
18246
19500
  }
18247
19501
 
18248
19502
  function nativeReturnedJsxValue(
@@ -18719,6 +19973,23 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18719
19973
  newAttrs.push(attr);
18720
19974
  continue;
18721
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
+ }
18722
19993
  const v = attr.value;
18723
19994
  const inner = v && v.type === 'JSXExpressionContainer' ? v.expression : v;
18724
19995
  // Dynamic attr = an expression value that isn't a static literal/string.
@@ -18787,9 +20058,10 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18787
20058
  const annotation = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18788
20059
  ? b.ts_keyword_type('string')
18789
20060
  : expr.typeAnnotation;
18790
- newChildren.push(
18791
- b.jsx_expression_container(b.ts_as(memberProps(hn, expr.expression), annotation)),
18792
- );
20061
+ newChildren.push({
20062
+ ...child,
20063
+ expression: b.ts_as(memberProps(hn, expr.expression), annotation),
20064
+ });
18793
20065
  } else {
18794
20066
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(expr, ctx, childNs)));
18795
20067
  // The renderer only sees `props.hN`, so it cannot rediscover an inline
@@ -18800,15 +20072,16 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18800
20072
  const rendered = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18801
20073
  ? b.ts_as(member, b.ts_keyword_type('string'))
18802
20074
  : member;
18803
- newChildren.push(b.jsx_expression_container(rendered));
20075
+ newChildren.push({ ...child, expression: rendered });
18804
20076
  }
18805
20077
  } else if (t === 'Element' || t === 'JSXElement') {
18806
20078
  if (isLongFormTemplateSentinel(child, childNs, true, ctx)) {
18807
20079
  newChildren.push(extractFragment(child, ctx, holeProps, childNs));
18808
20080
  } else if (
18809
20081
  isComponentTag(child) &&
18810
- ctx._foldCtx?.templateComponentChildren === true &&
18811
- requiresTemplateNormalization(child, childNs, true, ctx)
20082
+ (child._octaneBindingSite ||
20083
+ (ctx._foldCtx?.templateComponentChildren === true &&
20084
+ requiresTemplateNormalization(child, childNs, true, ctx)))
18812
20085
  ) {
18813
20086
  // Component children normally become descriptor children at return-value
18814
20087
  // position. A directive cannot be represented by lowerJsxChild, though, so
@@ -18877,7 +20150,12 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18877
20150
  // before fragment extraction. It is still an owner-side renderable hole.
18878
20151
  const hn = `h${holeProps.length}`;
18879
20152
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(child.expression, ctx, childNs)));
18880
- 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
+ });
18881
20159
  } else if ((t === 'IfStatement' || t === 'JSXIfExpression') && ctx._foldCtx) {
18882
20160
  // FOLD a directive: lower its branch bodies on the COMPONENT side (so the
18883
20161
  // `__then$N`/`__else$N` helpers keep their closure over setup locals/props),
@@ -18894,7 +20172,12 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18894
20172
  inheritOriginLoc(b.if(child.test, child.consequent, child.alternate || null), child),
18895
20173
  // The clause keyword's own span — `@else` starts before its
18896
20174
  // block, so it is the only authored range that names the arm.
18897
- { alternateKeyword: child.alternateKeyword ?? null },
20175
+ {
20176
+ alternateKeyword: child.alternateKeyword ?? null,
20177
+ ...(child._octaneBindingSite
20178
+ ? { _octaneBindingSite: child._octaneBindingSite }
20179
+ : {}),
20180
+ },
18898
20181
  )
18899
20182
  : child;
18900
20183
  const ic = makeIfCall(ifNode, ctx, fc.compInlinedSubs, fc.parentNs, fc.cssHash);
@@ -18977,6 +20260,9 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18977
20260
  index: child.index || null,
18978
20261
  empty: child.empty || null,
18979
20262
  emptyKeyword: child.emptyKeyword ?? null,
20263
+ ...(child._octaneBindingSite
20264
+ ? { _octaneBindingSite: child._octaneBindingSite }
20265
+ : {}),
18980
20266
  },
18981
20267
  )
18982
20268
  : child;
@@ -19284,9 +20570,17 @@ function lowerHostFragment(
19284
20570
  } else {
19285
20571
  ctx.hoistedHelpers.push(renderer);
19286
20572
  }
19287
- ctx.runtimeNeeded.add('createElement');
20573
+ ctx.runtimeNeeded.add('createElementFromConfig');
19288
20574
  // The fragment-renderer call maps to the authored fragment root.
19289
- 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
+ );
19290
20584
  }
19291
20585
 
19292
20586
  /**
@@ -19591,7 +20885,7 @@ function serverValueDirectiveFold(
19591
20885
  directive,
19592
20886
  ),
19593
20887
  );
19594
- ctx.runtimeNeeded.add('createElement');
20888
+ ctx.runtimeNeeded.add('createElementFromConfig');
19595
20889
  // The wrapper call stands where the directive was authored, and every printed
19596
20890
  // node needs an origin (assertNodeLocs). Inside a component body the enclosing
19597
20891
  // statement supplied one; a module-level statement does not.
@@ -19603,7 +20897,12 @@ function serverValueDirectiveFold(
19603
20897
  );
19604
20898
  }
19605
20899
  return inheritOriginLoc(
19606
- 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
+ ),
19607
20906
  directive,
19608
20907
  );
19609
20908
  };
@@ -20144,6 +21443,7 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20144
21443
  const nameNode = node.openingElement?.name || node.id;
20145
21444
  const activity = isActivityLongForm(node, ctx);
20146
21445
  const componentTag = activity || isComponentTag(node);
21446
+ const invocationSite = componentTag ? componentInvocationSite(ctx, node) : null;
20147
21447
  // Host (lowercase) tag → string literal (`'li'`) for the de-opt renderer;
20148
21448
  // component (capitalized / member / dynamic) → the identifier/member ref.
20149
21449
  const compNode = activity
@@ -20161,6 +21461,15 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20161
21461
  const attrs = node.attributes || node.openingElement?.attributes || [];
20162
21462
  const properties = [];
20163
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
+ );
20164
21473
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
20165
21474
  properties.push(b.spread(rewriteJsxValues(attr.argument, ctx)));
20166
21475
  continue;
@@ -20217,7 +21526,21 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20217
21526
  );
20218
21527
  }
20219
21528
  }
20220
- const propsNode = inheritOriginLoc(b.object(properties), node);
21529
+ const propsObject = inheritOriginLoc(b.object(properties), node);
21530
+ const propsNode =
21531
+ ctx.nativeReads &&
21532
+ !componentTag &&
21533
+ attrs.some(
21534
+ (attr) =>
21535
+ attr.type === 'SpreadAttribute' ||
21536
+ attr.type === 'JSXSpreadAttribute' ||
21537
+ (attr.name?.name ?? attr.name) === 'style',
21538
+ )
21539
+ ? inheritOriginLoc(
21540
+ b.call(requireRuntimeForContext(ctx, 'readNativeDomProps'), propsObject),
21541
+ node,
21542
+ )
21543
+ : propsObject;
20221
21544
  const childrenNeedRenderScope = jsxValueChildrenNeedRenderScope(node);
20222
21545
  const eagerProviderChildren =
20223
21546
  eagerRoot &&
@@ -20344,13 +21667,30 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
20344
21667
  ? requireRuntimeForContext(ctx, 'nativeCreateScopedElement')
20345
21668
  : rtAlias('createScopedElement');
20346
21669
  const readChildren = inheritOriginLoc(b.arrow([], memoizedChildrenBody ?? childrenValue), node);
20347
- 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
+ );
20348
21680
  } else {
20349
- ctx.runtimeNeeded.add('createElement');
21681
+ ctx.runtimeNeeded.add(componentTag ? 'createElementFromConfig' : 'createElement');
20350
21682
  // Remaining scaffolding (callee, props object, spread/diagnostic wrappers,
20351
21683
  // static-content literals) maps to the authored JSX element.
20352
21684
  descriptor = inheritOriginLoc(
20353
- 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),
20354
21694
  node,
20355
21695
  );
20356
21696
  }
@@ -20994,6 +22334,14 @@ function rewriteOpaqueTitles(node, ctx, namespace = 'html') {
20994
22334
  function headAttrsExpression(el) {
20995
22335
  const attrProps = [];
20996
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
+ );
20997
22345
  if (a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute') {
20998
22346
  attrProps.push(b.spread(a.argument));
20999
22347
  continue;
@@ -21377,6 +22725,14 @@ function normalizeChildren(
21377
22725
  out.push({
21378
22726
  type: needsRichDispatch(expression) ? 'TSRXExpression' : 'Text',
21379
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 }),
21380
22736
  });
21381
22737
  } else if (n.type === 'JSXElement') {
21382
22738
  // Long-form `<Fragment>…</Fragment>` (canary `enableFragmentRefs`
@@ -21493,6 +22849,7 @@ function normalizeChildren(
21493
22849
  selfClosing: n.openingElement.selfClosing,
21494
22850
  loc: n.loc, // preserve element position for dev hydration LOC (component slots)
21495
22851
  };
22852
+ if (n._octaneBindingSite !== undefined) element._octaneBindingSite = n._octaneBindingSite;
21496
22853
  // `unstable_Activity` starts with a lowercase letter but is a component
21497
22854
  // when it resolves to the builtin. Keep that fact on this rare node only.
21498
22855
  if (isActivityLongForm(n, ctx)) element.activityDescriptor = true;
@@ -21544,6 +22901,9 @@ function normalizeChildren(
21544
22901
  out.push(
21545
22902
  Object.assign(inheritOriginLoc(b.if(n.test, n.consequent, n.alternate || null), n), {
21546
22903
  alternateKeyword: n.alternateKeyword ?? null,
22904
+ ...(n._octaneBindingSite === undefined
22905
+ ? {}
22906
+ : { _octaneBindingSite: n._octaneBindingSite }),
21547
22907
  }),
21548
22908
  );
21549
22909
  } else if (n.type === 'JSXForExpression') {
@@ -21557,6 +22917,9 @@ function normalizeChildren(
21557
22917
  index: n.index || null,
21558
22918
  empty: n.empty || null,
21559
22919
  emptyKeyword: n.emptyKeyword ?? null,
22920
+ ...(n._octaneBindingSite === undefined
22921
+ ? {}
22922
+ : { _octaneBindingSite: n._octaneBindingSite }),
21560
22923
  }),
21561
22924
  );
21562
22925
  } else if (n.type === 'JSXTryExpression') {
@@ -22278,8 +23641,24 @@ function stripTsOnlyWrappers(node) {
22278
23641
  function nativeReadActivationNodes(ctx, origin) {
22279
23642
  // Install the graph-free driver before any authored root invocation, not
22280
23643
  // after parameter evaluation inside a syntactically recognized component.
22281
- // Plain data modules do not pass through this renderer compilation path.
22282
- 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 [];
22283
23662
  return [
22284
23663
  inheritOriginLoc(
22285
23664
  b.stmt(b.call(requireRuntimeForContext(ctx, 'enableNativeReadCollection'), b.literal(1))),
@@ -22874,7 +24253,7 @@ function planJsx(
22874
24253
  // A dynamic text hole with a text-producing neighbour: the server emits a
22875
24254
  // `<!-- -->` separator between the two texts (else the parser would merge
22876
24255
  // them), and only the hole-aware sibling() walk knows to step across it.
22877
- elementBindings.some((b) => b.kind === 'text' && b.adjacentText);
24256
+ elementBindings.some((b) => b.kind === 'text' && (b.adjacentText || b.bindingMarker));
22878
24257
  if (hasHoles) {
22879
24258
  ctx.runtimeNeeded.add('child');
22880
24259
  ctx.runtimeNeeded.add('sibling');
@@ -23049,6 +24428,7 @@ function planJsx(
23049
24428
  // happens on the intact template. (Regression: StoryRow's `.meta` interleaves text holes
23050
24429
  // with `<Link>` components.)
23051
24430
  const deferredTextMounts = [];
24431
+ let nativeStyleSlots = 0;
23052
24432
  // Emit per-binding mount code.
23053
24433
  for (const b of elementBindings) {
23054
24434
  // A sibling-position `{x as string}` text hole resolves to its POSITION node
@@ -23063,14 +24443,21 @@ function planJsx(
23063
24443
  // htext/htextSwap/setAttribute (which only have the element) can report `file:line:col`.
23064
24444
  if (_locStamped && b.kind !== 'text') stampHostLoc(elVar, b.path, undefined);
23065
24445
  else if (_locStamped) stampHostLoc(ensureVar(b.path), b.path, undefined);
23066
- if (b.kind === 'text' || b.kind === 'textOnlyChild') ctx.runtimeNeeded.add('setText');
23067
- 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');
23068
24455
  if (b.kind === 'textOnlyChild') ctx.runtimeNeeded.add('htext');
23069
24456
  // Mounts use the unconditional writer; reactive scalar bindings use either
23070
24457
  // that writer behind an inline guard or its compact comparison helper.
23071
24458
  // Claim both authored-name tokens when both paths exist, and do not retain
23072
24459
  // a mount-only import for deferred writes.
23073
- const attrHelper = attrBindingHelper(b);
24460
+ const attrHelper = b.signalDirect ? null : attrBindingHelper(b);
23074
24461
  if (attrHelper !== null) {
23075
24462
  if (b.kind === 'class' && b.fresh && (!b.deferred || (!b.mountOnly && inlineBindingGuards)))
23076
24463
  ctx.runtimeNeeded.add('normalizeClass');
@@ -23088,8 +24475,12 @@ function planJsx(
23088
24475
  if (b.kind === 'dangerCommit') ctx.runtimeNeeded.add('setDangerouslySetInnerHTMLSources');
23089
24476
  if (b.kind === 'formCommit') ctx.runtimeNeeded.add('setFormControlSources');
23090
24477
  if (b.kind === 'hostCommit') {
23091
- ctx.runtimeNeeded.add('setHostPropSources');
23092
- ctx.runtimeNeeded.add('queueOwnRefDetach');
24478
+ if (ctx.nativeReads) b.readStyle = requireRuntimeForContext(ctx, 'readNativeDomStyle');
24479
+ if (b.signalHostSources) ctx.runtimeNeeded.add('bindSignalHostPropSources');
24480
+ else {
24481
+ ctx.runtimeNeeded.add('setHostPropSources');
24482
+ ctx.runtimeNeeded.add('queueOwnRefDetach');
24483
+ }
23093
24484
  }
23094
24485
  // A commit-phase collector takes the element's props as one grouped call,
23095
24486
  // so each source's name literal — not the shared helper — is what claims
@@ -23121,7 +24512,15 @@ function planJsx(
23121
24512
  }
23122
24513
  if (b.kind === 'styleProperties') {
23123
24514
  ctx.runtimeNeeded.add('setStyleProperty');
23124
- ctx.runtimeNeeded.add('isHydratingStyle');
24515
+ ctx.runtimeNeeded.add(b.spread ? 'canSplitStyleProperties' : 'isHydratingStyle');
24516
+ if (b.spread) ctx.runtimeNeeded.add('styleObjectPrototype');
24517
+ }
24518
+ if (b.kind === 'nativeStyle' || b.kind === 'nativeProjection') {
24519
+ b.slotIndex = ++nativeStyleSlots;
24520
+ b.helper = requireRuntimeForContext(
24521
+ ctx,
24522
+ b.kind === 'nativeStyle' ? 'nativeStyleBinding' : 'nativeProjectionBinding',
24523
+ );
23125
24524
  }
23126
24525
  if (b.kind === 'spread') {
23127
24526
  ctx.runtimeNeeded.add('setSpread');
@@ -23383,7 +24782,8 @@ function planJsx(
23383
24782
  const afterCalls = [];
23384
24783
  const pushAfter = (id, line) => afterCalls.push({ id, line });
23385
24784
  // Dense per-body slot indices. Slot 0 is this body's binding bag (`__s.slots[0]`);
23386
- // each control-flow / component / child construct gets index 1..N. The runtime
24785
+ // native styles fill the following indices during binding mount. Each remaining
24786
+ // control-flow / component / child construct gets the next index. The runtime
23387
24787
  // runs the slot calls in `afterCalls` SORTED by source id, so we assign indices in
23388
24788
  // that same id order — the scope's `slots` array is then written 0,1,2,… and stays
23389
24789
  // PACKED (a holey array, written out of order, would be a slower elements-kind).
@@ -23398,7 +24798,7 @@ function planJsx(
23398
24798
  allConstructs.sort((a, b) => a.id - b.id);
23399
24799
  // Slot 0 is the binding bag for template bodies; control-flow-only (noTemplate)
23400
24800
  // bodies have no bag, so their constructs start at slot 0.
23401
- const slotBase = noTemplate ? 0 : 1;
24801
+ const slotBase = noTemplate ? 0 : 1 + nativeStyleSlots;
23402
24802
  for (let i = 0; i < allConstructs.length; i++) allConstructs[i].slotIndex = i + slotBase;
23403
24803
  // Hoisted head elements take the slots AFTER the constructs (and `plan.head` runs
23404
24804
  // after `plan.after`), so the scope's `slots` array fills 0,1,…,N,N+1,… packed.
@@ -23743,26 +25143,37 @@ function planJsx(
23743
25143
  ctx.runtimeNeeded.add('ifBlock');
23744
25144
  registerDirectiveOrigin(ctx, org, ['_$ifBlock', ic.thenHelper, ic.elseHelper]);
23745
25145
  registerClauseOrigin(ctx, ic.alternateKeyword, [ic.elseHelper]);
23746
- const call = b.stmt(
23747
- b.call(
23748
- '_$ifBlock',
23749
- b.id('__s'),
23750
- b.literal(slotIndex),
23751
- hostExpr,
23752
- condition,
23753
- helperRefNode(ic.thenHelper),
23754
- inheritOriginLoc(helperRefNode(ic.elseHelper ?? 'null'), ic.alternateKeyword),
23755
- ...trailing,
23756
- ),
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,
23757
25155
  );
23758
- pushAfterStmt(ic.id, org, conditionDeclaration ? b.block([conditionDeclaration, call]) : call);
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);
25166
+ pushAfterStmt(ic.id, org, conditionDeclaration ? b.block([conditionDeclaration, call]) : call);
23759
25167
  }
23760
25168
  for (const cc of compCalls) {
23761
25169
  const slotIndex = cc.slotIndex;
23762
25170
  const org = cc.origin ?? planOrigin;
23763
25171
  const hostExpr = () => hostNodeFor(`_compHost$${cc.id}`);
23764
25172
  if (cc.hostChildrenBinding != null) {
23765
- 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
+ };
23766
25177
  cc.valueExpr = b.conditional(
23767
25178
  propertyIsEnumerableCall(props(), 'children'),
23768
25179
  b.member(props(), 'children'),
@@ -23777,6 +25188,59 @@ function planJsx(
23777
25188
  // component calls; only the emitted runtime call differs.
23778
25189
  if (cc.isChild) {
23779
25190
  const V = () => b.id('_v');
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
+ }
23780
25244
  // MARKERLESS only-child renderable: append a primitive as a single Text
23781
25245
  // node (no `<!>`, no slot state). Either the shared helper or the inline
23782
25246
  // row guard compares its cached value/node before the ordinary child path.
@@ -23883,7 +25347,6 @@ function planJsx(
23883
25347
  }
23884
25348
  // Anchor node (null = append into an in-template element host) — see
23885
25349
  // anchorNodeFor.
23886
- const childAnchor = anchorNodeFor(cc, 'compAnchor');
23887
25350
  if (noTemplate) {
23888
25351
  // No bag to cache on → the small `textSlot` wrapper (fast inline for a
23889
25352
  // primitive into a text slot; delegates to `childSlot` otherwise).
@@ -23968,17 +25431,36 @@ function planJsx(
23968
25431
  liteMemo ? 'componentSlotLite' : cc.voidComponent ? 'componentSlotVoid' : 'componentSlot',
23969
25432
  );
23970
25433
  const memoAnchor = anchorNodeFor(cc, 'compAnchor');
23971
- const trailing = memoAnchor ? [memoAnchor] : [];
23972
- if (!liteMemo && cc.inheritRange) {
23973
- if (trailing.length === 0) trailing.push(undefinedNode());
23974
- trailing.push(undefinedNode(), undefinedNode(), b.literal(true));
23975
- } else if (!liteMemo && cc.singleRoot) {
23976
- if (trailing.length === 0) trailing.push(undefinedNode());
23977
- trailing.push(undefinedNode(), b.literal(true));
23978
- }
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
+ ];
23979
25444
  const witnessMiss = cc.autoMemoWitnesses.length
23980
25445
  ? witnessMissChain(cc.autoMemoWitnesses)
23981
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
+ );
23982
25464
  pushAfterStmt(
23983
25465
  cc.id,
23984
25466
  org,
@@ -23986,17 +25468,7 @@ function planJsx(
23986
25468
  ctx,
23987
25469
  cc.autoMemoDeps,
23988
25470
  slotIndex,
23989
- b.stmt(
23990
- b.call(
23991
- componentHelper,
23992
- b.id('__s'),
23993
- b.literal(slotIndex),
23994
- hostExpr(),
23995
- cc.compNode,
23996
- cc.propsExpr,
23997
- ...trailing,
23998
- ),
23999
- ),
25471
+ b.stmt(invocation),
24000
25472
  witnessMiss,
24001
25473
  cc.autoMemoContextAware,
24002
25474
  depNodeFor(cc),
@@ -24036,6 +25508,8 @@ function planJsx(
24036
25508
  undefinedNode(),
24037
25509
  undefinedNode(),
24038
25510
  b.literal(true),
25511
+ undefinedNode(),
25512
+ b.literal(cc.invocationSite),
24039
25513
  ),
24040
25514
  ),
24041
25515
  );
@@ -24062,7 +25536,8 @@ function planJsx(
24062
25536
  hostExpr(),
24063
25537
  cc.compNode,
24064
25538
  cc.propsExpr,
24065
- ...(liteAnchor ? [liteAnchor] : []),
25539
+ liteAnchor ?? undefinedNode(),
25540
+ b.literal(cc.invocationSite),
24066
25541
  ),
24067
25542
  ),
24068
25543
  );
@@ -24075,52 +25550,32 @@ function planJsx(
24075
25550
  // unmount move the slot DOM along with the block; an element host with
24076
25551
  // no in-template anchor can safely append).
24077
25552
  const compAnchor = anchorNodeFor(cc, 'compAnchor');
24078
- const trailing = [];
24079
- let anchorFilled = false;
24080
- if (compAnchor) {
24081
- trailing.push(compAnchor);
24082
- anchorFilled = true;
24083
- }
24084
- // key arg is positional AFTER anchor in componentSlot's signature. When a
24085
- // key is present but anchor isn't, supply `undefined` for the anchor slot
24086
- // so the key lands in the right argument position — the runtime's
24087
- // `anchor ?? null` still routes through appendChild as before.
24088
- if (cc.keyExpr != null) {
24089
- if (!anchorFilled) {
24090
- trailing.push(undefinedNode());
24091
- anchorFilled = true;
24092
- }
24093
- trailing.push(cc.keyExpr);
24094
- }
24095
- // singleRoot is the 8th positional arg (after anchor, key). It's gated on
24096
- // no-key, so backfill anchor + key placeholders to land it in the right
24097
- // slot. `true` = proven same-module single-element root; `2` = the
24098
- // cross-module sentinel (runtime checks the callee's $$singleRoot stamp).
24099
- if (cc.singleRoot || cc.maybeSingleRoot) {
24100
- if (!anchorFilled) {
24101
- trailing.push(undefinedNode());
24102
- anchorFilled = true;
24103
- }
24104
- trailing.push(undefinedNode(), cc.singleRoot ? b.literal(true) : b.literal(2));
24105
- }
24106
- // Persist key ownership separately from the current key VALUE so
24107
- // `key={undefined}` remains an independent reconciliation boundary.
24108
- if (cc.keyExpr != null) trailing.push(undefinedNode(), undefinedNode(), b.literal(true));
24109
- pushAfterStmt(
24110
- cc.id,
24111
- org,
24112
- b.stmt(
24113
- b.call(
24114
- componentHelper,
24115
- b.id('__s'),
24116
- b.literal(slotIndex),
24117
- hostExpr(),
24118
- cc.compNode,
24119
- cc.propsExpr,
24120
- ...trailing,
24121
- ),
24122
- ),
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,
24123
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));
24124
25579
  }
24125
25580
  for (const pc of ctx._portalCalls) {
24126
25581
  const slotIndex = pc.slotIndex;
@@ -24497,6 +25952,70 @@ function commitSourceRows(sources, valueOf) {
24497
25952
  return b.array(rows);
24498
25953
  }
24499
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
+
25965
+ // Recombine a native spread snapshot only for mount/hydration or a key collision.
25966
+ // Spreading the snapshot cannot replay getters: the authored spreads already
25967
+ // copied their values before any of the trailing expressions were evaluated.
25968
+ function styleSpreadObject(bind, spread, valueOf) {
25969
+ return b.object([
25970
+ b.spread(spread),
25971
+ ...bind.properties.map((property, i) =>
25972
+ b.prop(
25973
+ 'init',
25974
+ inheritOriginLoc(b.literal(property.name), property.key),
25975
+ valueOf(property, i),
25976
+ ),
25977
+ ),
25978
+ ]);
25979
+ }
25980
+
25981
+ function styleSpreadNeedsFullStyle(bind, spread) {
25982
+ return orChain([
25983
+ ...bind.properties.map((property) => b.binary('in', b.literal(property.name), spread)),
25984
+ b.unary('!', b.call('_$canSplitStyleProperties')),
25985
+ ]);
25986
+ }
25987
+
25988
+ // Native spreads create writable own data properties. Complete that fresh
25989
+ // snapshot in place only when it will be retained as the full style, and no
25990
+ // inherited setter or read-only property could intercept a trailing assignment.
25991
+ function completeStyleSpread(bind, spread, valueOf, needsFull) {
25992
+ return b.conditional(
25993
+ b.logical(
25994
+ '&&',
25995
+ needsFull,
25996
+ b.unary(
25997
+ '!',
25998
+ orChain(
25999
+ bind.properties.map((property) =>
26000
+ b.binary('in', b.literal(property.name), b.id('_$styleObjectPrototype')),
26001
+ ),
26002
+ ),
26003
+ ),
26004
+ ),
26005
+ b.sequence([
26006
+ ...bind.properties.map((property, i) =>
26007
+ b.assignment(
26008
+ '=',
26009
+ b.member(spread, inheritOriginLoc(b.literal(property.name), property.key), true),
26010
+ valueOf(property, i),
26011
+ ),
26012
+ ),
26013
+ spread,
26014
+ ]),
26015
+ styleSpreadObject(bind, spread, valueOf),
26016
+ );
26017
+ }
26018
+
24500
26019
  // Mount for a DEFERRED property-write binding: store the element ref + seed the
24501
26020
  // diff field to `undefined`. The every-render diff then performs the actual
24502
26021
  // write — including on the first render, since the `undefined` seed makes its
@@ -24504,15 +26023,18 @@ function commitSourceRows(sources, valueOf) {
24504
26023
  // `setClassName(el, undefined)` no-op on a freshly-cloned element (so the output
24505
26024
  // is byte-identical to the old unconditional mount write).
24506
26025
  function emitDeferredMount(bind, elVar, bag) {
24507
- // Whole-object styles diff on `_sty`; grouped styles use it only as a
24508
- // first-render marker. Each grouped property keeps a scalar previous value.
26026
+ // Whole-object and spread-prefix styles diff on `_sty`; other grouped styles
26027
+ // use it only as a first-render marker. Each trailing property keeps a scalar.
24509
26028
  bag.constField(
24510
- bind.kind === 'style' || bind.kind === 'styleProperties'
24511
- ? `_sty$${bind.id}`
24512
- : `_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}`,
24513
26034
  bind.kind === 'styleProperty' || bind.kind === 'styleProperties' ? 'style-unset' : 'undefined',
24514
26035
  );
24515
26036
  if (bind.kind === 'styleProperties') {
26037
+ if (bind.spread) bag.constField(`_styFull$${bind.id}`, 'undefined');
24516
26038
  for (let i = 0; i < bind.properties.length; i++) {
24517
26039
  bag.constField(`_prev$${bind.id}_${i}`, 'undefined');
24518
26040
  }
@@ -24532,7 +26054,57 @@ function bindingOrigin(bind) {
24532
26054
  return bind.expr && bind.expr.loc ? bind.expr : bind.origin;
24533
26055
  }
24534
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
+
24535
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 };
24536
26108
  if (bind.deferred) return emitDeferredMount(bind, elVar, bag);
24537
26109
  const org = bindingOrigin(bind);
24538
26110
  const st = (node) => inheritOriginLoc(node, org);
@@ -24546,6 +26118,46 @@ function emitBindingMount(bind, elVar, bag) {
24546
26118
  // name; everything else in the call maps to the value expression.
24547
26119
  const callee = () => attrLoweringToken(b.id(`_$${attrBindingHelper(bind)}`), bind);
24548
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
+ }
24549
26161
  // `suppressHydrationWarning`: stamp a JS flag (NOT a DOM attribute) the runtime reads to
24550
26162
  // keep the server value + skip the warning on a hydration mismatch for this element.
24551
26163
  if (bind.kind === 'suppress') {
@@ -24571,11 +26183,42 @@ function emitBindingMount(bind, elVar, bag) {
24571
26183
  if (bind.kind === 'formDiagnostic') {
24572
26184
  const args = [el(), b.literal(bind.diagnosticKind)];
24573
26185
  if (bind.selectedBinding !== undefined) {
24574
- 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
+ );
24575
26193
  }
24576
26194
  return st(b.stmt(b.call('_$queueFormAuthoringDiagnostic', ...args)));
24577
26195
  }
24578
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
+ }
26214
+ case 'nativeStyle': {
26215
+ return st(
26216
+ b.block([
26217
+ ...mountHost(),
26218
+ b.stmt(b.call(bind.helper, b.id('__s'), b.literal(bind.slotIndex), el(), bind.expr)),
26219
+ ]),
26220
+ );
26221
+ }
24579
26222
  case 'textOnlyChild': {
24580
26223
  // Only compiler-admitted native placeholders may be reused. An old
24581
26224
  // two-argument caller or an excluded custom/template host must retain
@@ -24649,6 +26292,29 @@ function emitBindingMount(bind, elVar, bag) {
24649
26292
  const sources = commitSourceRows(bind.sources, (binding, spread) =>
24650
26293
  b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
24651
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
+ }
24652
26318
  // The cleanup closure reads the bag through the captured `_b` — the bag
24653
26319
  // exists by the time any cleanup runs (committed at mount end), and the
24654
26320
  // `_host$` field is re-written by updates, so the read must be live.
@@ -24674,6 +26340,7 @@ function emitBindingMount(bind, elVar, bag) {
24674
26340
  undefinedNode(),
24675
26341
  b.id('__s'),
24676
26342
  b.literal(bind.hasNestedChildren === true),
26343
+ ...(bind.readStyle ? [b.id(bind.readStyle)] : []),
24677
26344
  ),
24678
26345
  ),
24679
26346
  ),
@@ -24692,7 +26359,18 @@ function emitBindingMount(bind, elVar, bag) {
24692
26359
  return st(
24693
26360
  b.block([
24694
26361
  b.const('_v', bind.expr),
24695
- 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
+ ),
24696
26374
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
24697
26375
  ]),
24698
26376
  );
@@ -24754,6 +26432,34 @@ function emitBindingMount(bind, elVar, bag) {
24754
26432
  }
24755
26433
  case 'styleProperties': {
24756
26434
  for (let i = 0; i < bind.properties.length; i++) bag.local(`_prev$${bind.id}_${i}`);
26435
+ if (bind.spread) {
26436
+ const spreadValues = () => b.id(bind.spreadName);
26437
+ const spread = () => b.member(spreadValues(), b.literal(0), true);
26438
+ const full = () => b.id(bind.fullName);
26439
+ const merged = () => b.id(bind.mergedName);
26440
+ const valueOf = (property) =>
26441
+ b.member(spreadValues(), b.literal(property.evaluationIndex + 1), true);
26442
+ return st(
26443
+ b.block([
26444
+ b.const(bind.spreadName, bind.expr),
26445
+ b.const(bind.fullName, styleSpreadNeedsFullStyle(bind, spread())),
26446
+ b.const(bind.mergedName, completeStyleSpread(bind, spread(), valueOf, full())),
26447
+ b.stmt(b.call(callee(), el(), merged(), undefinedNode())),
26448
+ ...mountHost(),
26449
+ b.stmt(
26450
+ b.assignment(
26451
+ '=',
26452
+ local(`_sty$${bind.id}`),
26453
+ b.conditional(full(), merged(), spread()),
26454
+ ),
26455
+ ),
26456
+ b.stmt(b.assignment('=', local(`_styFull$${bind.id}`), full())),
26457
+ ...bind.properties.map((property, i) =>
26458
+ b.stmt(b.assignment('=', local(`_prev$${bind.id}_${i}`), valueOf(property))),
26459
+ ),
26460
+ ]),
26461
+ );
26462
+ }
24757
26463
  const entries =
24758
26464
  bind.properties.length === bind.evaluations.length
24759
26465
  ? V()
@@ -24920,6 +26626,10 @@ function emitBindingMount(bind, elVar, bag) {
24920
26626
  }
24921
26627
 
24922
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 };
24923
26633
  if (bind.mountOnly) return null;
24924
26634
  const org = bindingOrigin(bind);
24925
26635
  const st = (node) => inheritOriginLoc(node, org);
@@ -24932,7 +26642,79 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
24932
26642
  const callee = () =>
24933
26643
  attrLoweringToken(b.id(`_$${attrBindingUpdateHelper(bind, inlineBindingGuards)}`), bind);
24934
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
+ }
24935
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
+ }
26713
+ case 'nativeStyle': {
26714
+ return st(
26715
+ b.stmt(b.call(bind.helper, b.id('__s'), b.literal(bind.slotIndex), F('_el'), bind.expr)),
26716
+ );
26717
+ }
24936
26718
  case 'nativeChangeRuntime': {
24937
26719
  return st(b.stmt(b.call('_$queueNativeChangeDiagnostic', F('_el'))));
24938
26720
  }
@@ -25002,6 +26784,26 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25002
26784
  const sources = commitSourceRows(bind.sources, (binding, spread) =>
25003
26785
  bagFieldNode(bag, `${spread ? '_sp' : '_prev'}$${binding.id}`),
25004
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
+ }
25005
26807
  return st(
25006
26808
  b.stmt(
25007
26809
  b.assignment(
@@ -25014,6 +26816,7 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25014
26816
  F('_host'),
25015
26817
  b.id('__s'),
25016
26818
  b.literal(bind.hasNestedChildren === true),
26819
+ ...(bind.readStyle ? [b.id(bind.readStyle)] : []),
25017
26820
  ),
25018
26821
  ),
25019
26822
  ),
@@ -25131,8 +26934,76 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25131
26934
  // Evaluate every authored value before the first CSS write, including
25132
26935
  // fixed literal suffix values. Mount and hydration compare one complete
25133
26936
  // style; ordinary updates only call the scalar setter for changed cells.
25134
- const values = bind.evaluations.map((value, i) => b.const(`_v${i}`, value));
25135
- const valueOf = (property) => b.id(`_v${property.evaluationIndex}`);
26937
+ const valueName = (i) => bind.valueNames?.[i] ?? `_v${i}`;
26938
+ const values = bind.evaluations.map((value, i) => b.const(valueName(i), value));
26939
+ const valueOf = (property) => b.id(valueName(property.evaluationIndex));
26940
+ if (bind.spread) {
26941
+ const spread = b.id(bind.spreadName);
26942
+ const needsFull = b.id(bind.fullName);
26943
+ const merged = b.id(bind.mergedName);
26944
+ const previous = (property, i) => bagFieldNode(bag, `_prev$${bind.id}_${i}`);
26945
+ const initial = bind.deferred ? b.binary('===', F('_sty'), b.id('__s')) : b.literal(false);
26946
+ const previousStyle = b.conditional(
26947
+ F('_styFull'),
26948
+ F('_sty'),
26949
+ styleSpreadObject(bind, F('_sty'), previous),
26950
+ );
26951
+ const full = b.stmt(
26952
+ b.call(
26953
+ callee(),
26954
+ F('_el'),
26955
+ merged,
26956
+ bind.deferred ? b.conditional(initial, undefinedNode(), previousStyle) : previousStyle,
26957
+ ),
26958
+ );
26959
+ const scalar = bind.properties.map((property, i) =>
26960
+ b.if(
26961
+ b.binary('!==', previous(property, i), valueOf(property)),
26962
+ b.block([
26963
+ b.stmt(
26964
+ b.call(
26965
+ attrLoweringToken(b.id('_$setStyleProperty'), bind),
26966
+ F('_el'),
26967
+ inheritOriginLoc(b.literal(property.name), property.key),
26968
+ valueOf(property),
26969
+ b.literal(''),
26970
+ previous(property, i),
26971
+ ),
26972
+ ),
26973
+ ]),
26974
+ null,
26975
+ ),
26976
+ );
26977
+ // A spread can insert a trailing key before a shorthand. Retain the
26978
+ // complete object when required, so consecutive full diffs reuse it.
26979
+ // This also preserves mutations by inherited getters on the complete
26980
+ // receiver. Leaving full mode needs one complete diff before returning
26981
+ // to prefix/scalar updates.
26982
+ return st(
26983
+ b.block([
26984
+ b.const(bind.spreadName, bind.spread),
26985
+ ...values,
26986
+ b.const(bind.fullName, styleSpreadNeedsFullStyle(bind, spread)),
26987
+ b.if(
26988
+ orChain([initial, needsFull, F('_styFull')]),
26989
+ b.block([
26990
+ b.const(bind.mergedName, completeStyleSpread(bind, spread, valueOf, needsFull)),
26991
+ full,
26992
+ b.stmt(b.assignment('=', F('_sty'), b.conditional(needsFull, merged, spread))),
26993
+ ]),
26994
+ b.block([
26995
+ b.stmt(b.call(callee(), F('_el'), spread, F('_sty'))),
26996
+ ...scalar,
26997
+ b.stmt(b.assignment('=', F('_sty'), spread)),
26998
+ ]),
26999
+ ),
27000
+ b.stmt(b.assignment('=', F('_styFull'), needsFull)),
27001
+ ...bind.properties.map((property, i) =>
27002
+ b.stmt(b.assignment('=', previous(property, i), valueOf(property))),
27003
+ ),
27004
+ ]),
27005
+ );
27006
+ }
25136
27007
  const entries = b.array(
25137
27008
  bind.properties.flatMap((property) => [
25138
27009
  inheritOriginLoc(b.literal(property.name), property.key),
@@ -25275,233 +27146,35 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
25275
27146
  // detaches the new ref.
25276
27147
  const fi = () => b.id('_fi');
25277
27148
  const cur = () => b.member(fi(), '_currentRef');
25278
- return st(
25279
- b.block([
25280
- b.const('_r', bind.expr),
25281
- b.const('_fi', F('_fi')),
25282
- b.if(
25283
- andChain([fi(), b.binary('!==', b.id('_r'), cur())]),
25284
- b.stmt(
25285
- b.assignment(
25286
- '=',
25287
- cur(),
25288
- b.call('_$replaceRef', b.id('__s'), cur(), b.id('_r'), fi()),
25289
- ),
25290
- ),
25291
- null,
25292
- ),
25293
- ]),
25294
- );
25295
- }
25296
- }
25297
- return null;
25298
- }
25299
-
25300
- // ===========================================================================
25301
- // HTML emission
25302
- // ===========================================================================
25303
-
25304
- // ---------------------------------------------------------------------------
25305
- // Template IR + origin recording (`inspect: true`). Static template HTML is
25306
- // runtime DATA, not JavaScript syntax: the browser-facing `template(html)` ABI
25307
- // intentionally stays a string. The compiler represents that data as an
25308
- // structured tree of elements and typed data chunks until allocTemplate
25309
- // serializes the completed template exactly once. Child templates compose by
25310
- // transferring nodes, so no intermediate HTML is joined and no origin offsets
25311
- // need to be shifted during construction. Origin spans remain local to their
25312
- // chunk; the one serializer shifts them into final logical-HTML coordinates.
25313
- //
25314
- // Origin collection stays gated on `ctx.inspect`. The normal compile path pays
25315
- // for the template chunks (the structural representation replacing string
25316
- // concatenation) but not for source-span arrays.
25317
- // ---------------------------------------------------------------------------
25318
-
25319
- /**
25320
- * @typedef {{ start: number, end: number, srcStart: number, srcEnd: number,
25321
- * kind: 'tag-open' | 'tag-close' | 'attr-name' | 'attr-value' | 'text' }} TemplateOrigin
25322
- */
25323
-
25324
- /**
25325
- * @typedef {'syntax' | 'tag-open' | 'tag-close' | 'attribute' | 'text' |
25326
- * 'anchor' | 'fragment-open' | 'fragment-close' | 'raw'} TemplatePartKind
25327
- */
25328
-
25329
- /**
25330
- * @typedef {{
25331
- * type: 'TemplatePart',
25332
- * kind: TemplatePartKind,
25333
- * value: string,
25334
- * origins: TemplateOrigin[] | null,
25335
- * length: number,
25336
- * }} TemplatePart
25337
- */
25338
-
25339
- /**
25340
- * @typedef {{
25341
- * type: 'TemplateElement',
25342
- * tag: string,
25343
- * namespace: string,
25344
- * synthetic: boolean,
25345
- * opening: TemplatePart,
25346
- * attributes: TemplatePart[],
25347
- * openingEnd: TemplatePart,
25348
- * children: TemplateNode[],
25349
- * closing: TemplatePart | null,
25350
- * length: number,
25351
- * }} TemplateElement
25352
- */
25353
-
25354
- /** @typedef {TemplatePart | TemplateElement} TemplateNode */
25355
-
25356
- /**
25357
- * @typedef {{
25358
- * type: 'Template',
25359
- * parts: TemplateNode[],
25360
- * length: number,
25361
- * }} TemplateIR
25362
- */
25363
-
25364
- /** @returns {TemplateIR} */
25365
- function createTemplateIr() {
25366
- return { type: 'Template', parts: [], length: 0 };
25367
- }
25368
-
25369
- /**
25370
- * @param {string} value
25371
- * @param {TemplatePartKind} [kind]
25372
- * @param {TemplateOrigin[] | null} [origins]
25373
- * @returns {TemplatePart}
25374
- */
25375
- function createTemplatePart(value, kind = 'raw', origins = null) {
25376
- return { type: 'TemplatePart', kind, value, origins, length: value.length };
25377
- }
25378
-
25379
- /** @param {TemplateIR} template @param {TemplateNode} node */
25380
- function appendTemplateNode(template, node) {
25381
- template.parts.push(node);
25382
- template.length += node.length;
25383
- }
25384
-
25385
- /**
25386
- * @param {TemplateIR} template
25387
- * @param {string} value
25388
- * @param {TemplatePartKind} [kind]
25389
- * @param {TemplateOrigin[] | null} [origins]
25390
- */
25391
- function appendTemplatePart(template, value, kind = 'raw', origins = null) {
25392
- if (value === '') return;
25393
- appendTemplateNode(template, createTemplatePart(value, kind, origins));
25394
- }
25395
-
25396
- /** @param {TemplateIR} into @param {TemplateIR} child */
25397
- function appendTemplateIr(into, child) {
25398
- if (child.length === 0) return;
25399
- for (const part of child.parts) into.parts.push(part);
25400
- into.length += child.length;
25401
- }
25402
-
25403
- /**
25404
- * @param {string} value
25405
- * @param {TemplatePartKind} [kind]
25406
- * @param {TemplateOrigin[] | null} [origins]
25407
- * @returns {TemplateIR}
25408
- */
25409
- function templatePart(value, kind = 'raw', origins = null) {
25410
- const template = createTemplateIr();
25411
- appendTemplatePart(template, value, kind, origins);
25412
- return template;
25413
- }
25414
-
25415
- /**
25416
- * @param {string} tag
25417
- * @param {string} namespace
25418
- * @param {boolean} voidElement
25419
- * @param {TemplateIR} attributes
25420
- * @param {TemplateIR} children
25421
- * @param {TemplateOrigin[] | null} openOrigins
25422
- * @param {TemplateOrigin[] | null} closeOrigins
25423
- * @param {boolean} [synthetic]
25424
- * @returns {TemplateIR}
25425
- */
25426
- function templateElement(
25427
- tag,
25428
- namespace,
25429
- voidElement,
25430
- attributes,
25431
- children,
25432
- openOrigins,
25433
- closeOrigins,
25434
- synthetic = false,
25435
- ) {
25436
- const opening = createTemplatePart(`<${tag}`, 'tag-open', openOrigins);
25437
- const openingEnd = createTemplatePart(voidElement ? '/>' : '>', 'syntax');
25438
- const closing = voidElement ? null : createTemplatePart(`</${tag}>`, 'tag-close', closeOrigins);
25439
- const node = {
25440
- type: 'TemplateElement',
25441
- tag,
25442
- namespace,
25443
- synthetic,
25444
- opening,
25445
- attributes: /** @type {TemplatePart[]} */ (attributes.parts),
25446
- openingEnd,
25447
- children: children.parts,
25448
- closing,
25449
- length:
25450
- opening.value.length +
25451
- attributes.length +
25452
- openingEnd.value.length +
25453
- children.length +
25454
- (closing === null ? 0 : closing.value.length),
25455
- };
25456
- const template = createTemplateIr();
25457
- appendTemplateNode(template, node);
25458
- return template;
25459
- }
25460
-
25461
- /**
25462
- * Serialize the completed template once. The IR already contains the exact
25463
- * runtime bytes; this pass only joins them and converts chunk-local origins to
25464
- * final logical-HTML offsets.
25465
- *
25466
- * @param {TemplateIR} template
25467
- */
25468
- function serializeTemplateIr(template) {
25469
- const values = [];
25470
- let origins = null;
25471
- let offset = 0;
25472
- const appendPart = (part) => {
25473
- values.push(part.value);
25474
- if (part.origins !== null) {
25475
- origins ??= [];
25476
- for (const origin of part.origins) {
25477
- origins.push({
25478
- start: origin.start + offset,
25479
- end: origin.end + offset,
25480
- srcStart: origin.srcStart,
25481
- srcEnd: origin.srcEnd,
25482
- kind: origin.kind,
25483
- });
25484
- }
25485
- }
25486
- offset += part.value.length;
25487
- };
25488
- const visit = (node) => {
25489
- if (node.type === 'TemplatePart') {
25490
- appendPart(node);
25491
- return;
27149
+ return st(
27150
+ b.block([
27151
+ b.const('_r', bind.expr),
27152
+ b.const('_fi', F('_fi')),
27153
+ b.if(
27154
+ andChain([fi(), b.binary('!==', b.id('_r'), cur())]),
27155
+ b.stmt(
27156
+ b.assignment(
27157
+ '=',
27158
+ cur(),
27159
+ b.call('_$replaceRef', b.id('__s'), cur(), b.id('_r'), fi()),
27160
+ ),
27161
+ ),
27162
+ null,
27163
+ ),
27164
+ ]),
27165
+ );
25492
27166
  }
25493
- appendPart(node.opening);
25494
- for (const attribute of node.attributes) appendPart(attribute);
25495
- appendPart(node.openingEnd);
25496
- for (const child of node.children) visit(child);
25497
- if (node.closing !== null) appendPart(node.closing);
25498
- };
25499
- for (const node of template.parts) {
25500
- visit(node);
25501
27167
  }
25502
- return { html: values.join(''), origins };
27168
+ return null;
25503
27169
  }
25504
27170
 
27171
+ // ===========================================================================
27172
+ // HTML emission
27173
+ // ===========================================================================
27174
+
27175
+ /** @typedef {import('./template-ir.js').TemplateIR} TemplateIR */
27176
+ /** @typedef {import('./template-ir.js').TemplateOrigin} TemplateOrigin */
27177
+
25505
27178
  // Record the origin entries for one static-attribute chunk appended at `base`
25506
27179
  // (a bakeStaticAttr result or the inline ` style="…"` bakes, which share the
25507
27180
  // same layout). Uses bakeStaticAttr's documented LAYOUT INVARIANT — ` name`
@@ -25572,15 +27245,36 @@ function emitNodeHtml(
25572
27245
  parentNs = 'html',
25573
27246
  cssHash = null,
25574
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
+ }
25575
27256
  if (node.type === 'Text') {
25576
- if (isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)) {
25577
- bindings.push({
25578
- id: bindings.length,
25579
- kind: 'text',
25580
- expr: resolveStyleExpr(node.expression, cssHash),
25581
- path: path.slice(0, -1),
25582
- childIndex: path[path.length - 1],
25583
- });
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
+ );
25584
27278
  return templatePart('<!>', 'anchor');
25585
27279
  }
25586
27280
  // Bare `{expr}` (no string cast) → RENDERABLE hole at a top-level / multi-
@@ -25599,6 +27293,28 @@ function emitNodeHtml(
25599
27293
  // cannot represent a Fragment ref or template-only control flow in that body.
25600
27294
  // Ordinary root portals keep their existing descriptor/childSlot lowering.
25601
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
+ }
25602
27318
  const fragmentBindings = ctx._fragRefStack;
25603
27319
  if (
25604
27320
  isCreatePortalCall(node.expression) &&
@@ -25908,7 +27624,9 @@ function emitElementHtml(
25908
27624
  const hostNs = nsForSelf(tag, parentNs);
25909
27625
  const childNs = nsForChildren(tag, parentNs);
25910
27626
  // Collect attributes.
25911
- const attrs = node.attributes || node.openingElement?.attributes || [];
27627
+ const attrs = expandKnownAttributeSpreads(
27628
+ node.attributes || node.openingElement?.attributes || [],
27629
+ );
25912
27630
  // A null/undefined child alongside direct raw HTML is semantically absent.
25913
27631
  // Suppress its child binding so hydration cannot clear the raw HTML that the
25914
27632
  // preceding binding just adopted/applied.
@@ -25924,7 +27642,7 @@ function emitElementHtml(
25924
27642
  // into the template HTML (the spread would clobber them at runtime) —
25925
27643
  // emit them as bindings in source order instead.
25926
27644
  const firstSpreadIdx = attrs.findIndex(
25927
- (a) => a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute',
27645
+ (a) => !a.knownSource && (a.type === 'SpreadAttribute' || a.type === 'JSXSpreadAttribute'),
25928
27646
  );
25929
27647
  const directPropIdentities = new Set();
25930
27648
  let hasDuplicateDirectProp = false;
@@ -25959,11 +27677,21 @@ function emitElementHtml(
25959
27677
  (tag === 'select' &&
25960
27678
  directPropNames.has('multiple') &&
25961
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
+ });
25962
27689
  const resolveHostPropsAcrossSources =
25963
27690
  firstSpreadIdx !== -1 ||
25964
27691
  hasDuplicateDirectProp ||
25965
27692
  hasDirectFormCascade ||
25966
- resolveDevCustomizedHost;
27693
+ resolveDevCustomizedHost ||
27694
+ hasDirectSignalStyle;
25967
27695
  const hostClientSources = [];
25968
27696
  let directChildrenClientBinding = null;
25969
27697
  let hostCommitClientBinding = null;
@@ -26004,9 +27732,45 @@ function emitElementHtml(
26004
27732
  typeWriters.length === 1 &&
26005
27733
  (staticInputType === 'checkbox' || staticInputType === 'radio');
26006
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
+ }
26007
27758
  let sawRef = false;
26008
27759
  for (let attrI = 0; attrI < attrs.length; attrI++) {
26009
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
+ }
26010
27774
  // `<div {...props}/>` — runtime spread. Emits one setSpread binding that
26011
27775
  // routes each key (class / style / on… / attr / ref) and diffs against
26012
27776
  // the prior spread object to clear removed keys.
@@ -26015,6 +27779,7 @@ function emitElementHtml(
26015
27779
  const binding = {
26016
27780
  id: bindings.length,
26017
27781
  kind: 'hostSpread',
27782
+ ...(attr._octaneBindingRest ? { bindingRest: attr._octaneBindingRest } : {}),
26018
27783
  // The INNER spread argument; the emit re-derives the
26019
27784
  // `_$snapshotSpread(…)` wrapper around it.
26020
27785
  expr: tsrxExprNode(attr.argument, ctx, componentName, inlinedSubs),
@@ -26027,6 +27792,24 @@ function emitElementHtml(
26027
27792
  }
26028
27793
  if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') continue;
26029
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
+ }
26030
27813
  if (resolveHostPropsAcrossSources) {
26031
27814
  if (rawAttrName === 'ref') {
26032
27815
  if (sawRef) {
@@ -26066,6 +27849,13 @@ function emitElementHtml(
26066
27849
  ctx.inspect,
26067
27850
  );
26068
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
+ }
26069
27859
  expr = tsrxExprNode(inner, ctx, componentName, inlinedSubs);
26070
27860
  }
26071
27861
  const binding = {
@@ -26257,19 +28047,17 @@ function emitElementHtml(
26257
28047
  resolveFormControlsAcrossSpreads &&
26258
28048
  (ctlKind !== null || (tag === 'select' && attrName === 'multiple'))
26259
28049
  ) {
28050
+ const sourceExpr =
28051
+ val == null
28052
+ ? b.literal(true, 'true', attr)
28053
+ : val.type === 'JSXExpressionContainer'
28054
+ ? val.expression
28055
+ : val;
26260
28056
  const binding = {
26261
28057
  id: bindings.length,
26262
28058
  kind: 'formValue',
26263
28059
  name: attrName,
26264
- expr:
26265
- val == null
26266
- ? b.literal(true, 'true', attr)
26267
- : tsrxExprNode(
26268
- val.type === 'JSXExpressionContainer' ? val.expression : val,
26269
- ctx,
26270
- componentName,
26271
- inlinedSubs,
26272
- ),
28060
+ expr: tsrxExprNode(sourceExpr, ctx, componentName, inlinedSubs),
26273
28061
  path,
26274
28062
  };
26275
28063
  bindings.push(binding);
@@ -26287,22 +28075,28 @@ function emitElementHtml(
26287
28075
  } else if (ctlKind === 'checked' && leanChecked) {
26288
28076
  ctlKind = 'checkedCheckable';
26289
28077
  }
26290
- 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 = {
26291
28085
  id: bindings.length,
26292
28086
  kind: ctlKind,
26293
- expr:
26294
- val == null
26295
- ? b.literal(true, 'true', attr)
26296
- : tsrxExprNode(
26297
- val.type === 'JSXExpressionContainer' ? val.expression : val,
26298
- ctx,
26299
- componentName,
26300
- inlinedSubs,
26301
- ),
28087
+ expr: tsrxExprNode(sourceExpr, ctx, componentName, inlinedSubs),
26302
28088
  path,
26303
28089
  ns: hostNs,
26304
28090
  nameOrigin: attr.name,
26305
- });
28091
+ };
28092
+ bindings.push(
28093
+ ctlKind === 'value' ||
28094
+ ctlKind === 'selectValue' ||
28095
+ ctlKind === 'checked' ||
28096
+ ctlKind === 'checkedCheckable'
28097
+ ? markDirectControl(binding, sourceExpr, attr)
28098
+ : binding,
28099
+ );
26306
28100
  continue;
26307
28101
  }
26308
28102
  // `autoFocus` never bakes/writes an attribute — React parity: the
@@ -26359,11 +28153,32 @@ function emitElementHtml(
26359
28153
  // template (unless we're after a spread, which would clobber it); a fixed
26360
28154
  // literal prefix plus dynamic suffix can avoid a whole-object diff.
26361
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
+ }
26362
28163
  if (!isAfterSpread && inner.type === 'Literal' && typeof inner.value === 'string') {
26363
28164
  const chunk = ` style="${escapeAttr(inner.value)}"`;
26364
28165
  appendBakedAttribute(attrTemplate, chunk, attrName, attr.name, inner, ctx.inspect);
26365
28166
  continue;
26366
28167
  }
28168
+ if (
28169
+ ctx.nativeReads &&
28170
+ (inner.type !== 'ObjectExpression' || !objectExprIsStaticLiteral(inner))
28171
+ ) {
28172
+ bindings.push({
28173
+ id: bindings.length,
28174
+ kind: 'nativeStyle',
28175
+ expr: tsrxExprNode(inner, ctx, componentName, inlinedSubs),
28176
+ path,
28177
+ ns: hostNs,
28178
+ nameOrigin: attr.name,
28179
+ });
28180
+ continue;
28181
+ }
26367
28182
  if (
26368
28183
  !isAfterSpread &&
26369
28184
  inner.type === 'ObjectExpression' &&
@@ -26398,7 +28213,7 @@ function emitElementHtml(
26398
28213
  for (const entry of mixed.dynamics) {
26399
28214
  registerAttrLoweringOrigin(ctx, entry.key, null, entry.name);
26400
28215
  }
26401
- if (mixed.dynamics.length === 1) {
28216
+ if (mixed.dynamics.length === 1 && mixed.spread === null) {
26402
28217
  const entry = mixed.dynamics[0];
26403
28218
  bindings.push({
26404
28219
  id: bindings.length,
@@ -26413,6 +28228,9 @@ function emitElementHtml(
26413
28228
  staticCss: mixed.css,
26414
28229
  });
26415
28230
  } else {
28231
+ const spread = mixed.spread
28232
+ ? tsrxExprNode(mixed.spread, ctx, componentName, inlinedSubs)
28233
+ : null;
26416
28234
  const entries = [];
26417
28235
  const propertyBindings = new Map();
26418
28236
  const evaluations = [];
@@ -26429,7 +28247,14 @@ function emitElementHtml(
26429
28247
  bindings.push({
26430
28248
  id: bindings.length,
26431
28249
  kind: 'styleProperties',
26432
- expr: inheritOriginLoc(b.array(entries), inner),
28250
+ expr: inheritOriginLoc(b.array(spread ? [spread, ...evaluations] : entries), inner),
28251
+ spread,
28252
+ spreadName: spread ? allocCompilerName(ctx, '__styleSpread') : null,
28253
+ fullName: spread ? allocCompilerName(ctx, '__styleFull') : null,
28254
+ mergedName: spread ? allocCompilerName(ctx, '__styleObject') : null,
28255
+ valueNames: spread
28256
+ ? evaluations.map(() => allocCompilerName(ctx, '__styleValue'))
28257
+ : null,
26433
28258
  properties: [...propertyBindings.values()],
26434
28259
  evaluations,
26435
28260
  path,
@@ -26559,7 +28384,7 @@ function emitElementHtml(
26559
28384
  } else if (attrName === 'class') {
26560
28385
  // (`className` was already normalized to `class` above.)
26561
28386
  const scalar = firstSpreadIdx === -1 ? scalarStateClass(inner, ctx) : null;
26562
- bindings.push({
28387
+ const binding = {
26563
28388
  id: bindings.length,
26564
28389
  kind: 'class',
26565
28390
  expr: scalar === null ? expr : tsrxExprNode(scalar, ctx, componentName, inlinedSubs),
@@ -26567,7 +28392,9 @@ function emitElementHtml(
26567
28392
  ns: hostNs,
26568
28393
  fresh: scalar === null && isFreshBindingExpr(inner),
26569
28394
  nameOrigin: attr.name,
26570
- });
28395
+ name: 'class',
28396
+ };
28397
+ bindings.push(markDirectSignalBinding(binding, ctx, inner, 'binding'));
26571
28398
  } else if (
26572
28399
  (tag === 'form' && attrName === 'action') ||
26573
28400
  ((tag === 'button' || tag === 'input') &&
@@ -26592,15 +28419,22 @@ function emitElementHtml(
26592
28419
  // namespace, URL, controlled-form, and validity routing. Their value
26593
28420
  // contract is uniform across native/custom HTML, SVG, and MathML:
26594
28421
  // nullish removes and everything else stringifies.
26595
- bindings.push({
26596
- id: bindings.length,
26597
- kind: 'ariaAttr',
26598
- name: attrName,
26599
- expr,
26600
- path,
26601
- ns: hostNs,
26602
- nameOrigin: attr.name,
26603
- });
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
+ );
26604
28438
  } else if (
26605
28439
  !((hostNs === 'html' || hostNs === 'opaque') && tag.includes('-')) &&
26606
28440
  BOOLEAN_ATTR_PROPS.has(attrName.toLowerCase())
@@ -26608,15 +28442,22 @@ function emitElementHtml(
26608
28442
  // Exact native boolean props all share the canonical presence contract.
26609
28443
  // The lowercase name is the final DOM spelling (including camelCase JSX
26610
28444
  // aliases such as allowFullScreen -> allowfullscreen).
26611
- bindings.push({
26612
- id: bindings.length,
26613
- kind: 'booleanAttr',
26614
- name: attrName.toLowerCase(),
26615
- expr,
26616
- path,
26617
- ns: hostNs,
26618
- nameOrigin: attr.name,
26619
- });
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
+ );
26620
28461
  } else if (
26621
28462
  /^data-[a-z][a-z0-9_-]*$/.test(attrName) &&
26622
28463
  isKnownStringExpression(inner, ctx.knownStringLocals)
@@ -26627,33 +28468,57 @@ function emitElementHtml(
26627
28468
  // data attribute in HTML, SVG, and MathML, so destination-opaque component
26628
28469
  // templates can retain this specialization. Production also admits unknown
26629
28470
  // scalar data values through staticAttributeWriter below.
26630
- bindings.push({
26631
- id: bindings.length,
26632
- kind: 'stringData',
26633
- name: attrName,
26634
- expr,
26635
- path,
26636
- ns: hostNs,
26637
- nameOrigin: attr.name,
26638
- });
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
+ );
26639
28487
  } else {
26640
28488
  const writer = ctx.dev ? null : staticAttributeWriter(tag, attrName);
26641
- bindings.push({
26642
- id: bindings.length,
26643
- kind: 'attr',
26644
- attributeHelper: writer?.helper,
26645
- name:
26646
- ctx.dev && (rawAttrName === 'tabIndex' || rawAttrName === 'htmlFor')
26647
- ? rawAttrName
26648
- : (writer?.name ?? attrName),
26649
- expr,
26650
- path,
26651
- ns: hostNs,
26652
- nameOrigin: attr.name,
26653
- });
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
+ );
26654
28509
  }
26655
28510
  }
26656
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
+ }
26657
28522
  hostCommitClientBinding = {
26658
28523
  id: bindings.length,
26659
28524
  kind: 'hostCommit',
@@ -26661,6 +28526,8 @@ function emitElementHtml(
26661
28526
  origin: node,
26662
28527
  sources: hostClientSources,
26663
28528
  hasNestedChildren: hasNestedJsxChildren,
28529
+ signalHostSources,
28530
+ signalSite: signalHostSources ? hostSignalSite : null,
26664
28531
  };
26665
28532
  bindings.push(hostCommitClientBinding);
26666
28533
  } else if (resolveDangerouslySetInnerHTMLAcrossSpreads) {
@@ -26736,13 +28603,16 @@ function emitElementHtml(
26736
28603
  }
26737
28604
  }
26738
28605
  if (
28606
+ !VOID_ELEMENTS.has(tag) &&
26739
28607
  !hasNestedJsxChildren &&
26740
28608
  (hostCommitClientBinding !== null || directChildrenClientBinding !== null)
26741
28609
  ) {
26742
28610
  compCalls.push({
26743
28611
  id: ctx.nextHelperId++,
26744
28612
  loc: devLoc(ctx, node),
28613
+ origin: node,
26745
28614
  isChild: true,
28615
+ signalSite: directSignalSite(ctx, node, 'binding'),
26746
28616
  hostPath: path,
26747
28617
  onlyChildText: true,
26748
28618
  potentialDangerouslySetInnerHTML,
@@ -26787,7 +28657,14 @@ function emitElementHtml(
26787
28657
  if (hasNestedHoist) children = children.filter((n) => n.type !== 'HeadHoist');
26788
28658
  }
26789
28659
  // Special case: a single Text child (only-child text fast path).
26790
- 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
+ ) {
26791
28668
  const txtChild = children[0];
26792
28669
  const staticLit = staticTextLiteral(txtChild.expression);
26793
28670
  if (staticLit !== null) {
@@ -26811,15 +28688,23 @@ function emitElementHtml(
26811
28688
  ]
26812
28689
  : null;
26813
28690
  appendTemplatePart(html, escaped, 'text', origins);
26814
- } 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
+ );
26815
28706
  const seededText = tag !== 'template' && !tag.includes('-') && !directPropNames.has('is');
26816
- bindings.push({
26817
- id: bindings.length,
26818
- kind: 'textOnlyChild',
26819
- expr: resolveStyleExpr(txtChild.expression, cssHash),
26820
- path,
26821
- seededText,
26822
- });
28707
+ bindings.push({ ...binding, seededText });
26823
28708
  // A nonempty placeholder survives the HTML parser as one Text node. The
26824
28709
  // mount writes its actual value before the cloned host is inserted, so
26825
28710
  // even an empty value retains one stable text binding node. <template>
@@ -26961,8 +28846,23 @@ function emitElementHtml(
26961
28846
  childIdx++;
26962
28847
  continue;
26963
28848
  }
26964
- if (child.type === 'Text') {
26965
- 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);
26966
28866
  if (staticLit === '') {
26967
28867
  // Renders nothing: bake nothing and consume no childIndex (an empty
26968
28868
  // text produces NO node when the template HTML is parsed, so counting
@@ -26994,17 +28894,30 @@ function emitElementHtml(
26994
28894
  appendTemplatePart(html, escaped, 'text', origins);
26995
28895
  prevBakedText = true;
26996
28896
  if (!prevBaked) childIdx++;
26997
- } else if (isKnownTextChildExpression(child.expression, ctx.knownStringChildLocals)) {
26998
- bindings.push({
26999
- id: bindings.length,
27000
- kind: 'text',
27001
- expr: resolveStyleExpr(child.expression, cssHash),
27002
- path,
27003
- childIndex: childIdx,
27004
- // A text-producing neighbour → the server emits a `<!-- -->`
27005
- // separator here; the walk must be hole-aware (see hasHoles).
27006
- adjacentText: hasTextNeighbor(adjKinds, childI),
27007
- });
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
+ );
27008
28921
  appendTemplatePart(html, '<!>', 'anchor'); // placeholder we'll replace at mount
27009
28922
  childIdx++;
27010
28923
  } else {
@@ -27183,24 +29096,36 @@ function emitElementHtml(
27183
29096
  } else if (isConditionalJsx(expr)) {
27184
29097
  // Lower `{cond ? A : B}` (where A or B is JSX) to an IfStatement so
27185
29098
  // each branch renders real DOM via the existing ifBlock machinery.
27186
- const asIf = {
27187
- ...b.if(expr.test, wrapAsBlockStmt(expr.consequent), wrapAsBlockStmt(expr.alternate)),
27188
- loc: expr.loc, // carry source position for dev hydration-mismatch LOC
27189
- };
29099
+ const asIf = conditionalJsxStatement(expr);
27190
29100
  const ic = makeIfCall(asIf, ctx, inlinedSubs, childNs, cssHash);
27191
29101
  ic.hostPath = path;
27192
29102
  ic.anchorPath = [...path, childIdx];
27193
29103
  ifCalls.push(ic);
27194
29104
  appendTemplatePart(html, '<!>', 'anchor');
27195
29105
  childIdx++;
27196
- } else if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
27197
- bindings.push({
27198
- id: bindings.length,
27199
- kind: 'text',
27200
- expr: tsrxExprNode(resolveStyleExpr(expr, cssHash), ctx, componentName, inlinedSubs),
27201
- path,
27202
- childIndex: childIdx,
27203
- });
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
+ );
27204
29129
  appendTemplatePart(html, '<!>', 'anchor');
27205
29130
  childIdx++;
27206
29131
  } else {
@@ -27409,6 +29334,17 @@ function hoistBodyHelper(
27409
29334
  id: b.id(helperName, idOrigin ?? fakeOrigin ?? undefined),
27410
29335
  params: params || [],
27411
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
+ : {}),
27412
29348
  },
27413
29349
  fakeOrigin,
27414
29350
  );
@@ -27553,6 +29489,7 @@ function makeIfCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
27553
29489
  origin: node,
27554
29490
  condExpr: node.test, // the fold replaces this with a `props.hN` member read
27555
29491
  condTest: node.test, // raw test AST — the fold threads it as a `props.hN` hole
29492
+ bindingSite: node._octaneBindingSite,
27556
29493
  envNames,
27557
29494
  thenHelper: thenHelperName,
27558
29495
  elseHelper: elseHelperName,
@@ -27801,6 +29738,7 @@ function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash, parentNs
27801
29738
  loc: devLoc(ctx, expr),
27802
29739
  origin: expr,
27803
29740
  isChild: true,
29741
+ signalSite: directSignalSite(ctx, expr, 'binding'),
27804
29742
  valueExpr: tsrxExprNode(
27805
29743
  resolveStyleExpr(rewriteChildHoleValue(expr, ctx), cssHash),
27806
29744
  ctx,
@@ -28010,6 +29948,7 @@ function makeCompCall(
28010
29948
  cssHash = null,
28011
29949
  ) {
28012
29950
  const id = ctx.nextHelperId++;
29951
+ const invocationSite = componentInvocationSite(ctx, node);
28013
29952
  const compName = tagBindingName(node);
28014
29953
  const staticFragmentRenderer = node.openingElement?.metadata?.staticFragmentRenderer;
28015
29954
  const activityDescriptor = isActivityLongForm(node, ctx);
@@ -28149,23 +30088,31 @@ function makeCompCall(
28149
30088
  ctx.hoistedHelpers.push(inheritOriginLoc(b.const(bodyIdentity, b.object([])), node));
28150
30089
  childrenArgs.push(b.id(bodyIdentity));
28151
30090
  }
28152
- propNodes.push(
28153
- b.prop('init', b.literal('children'), b.call('_$markChildrenBlock', ...childrenArgs)),
28154
- );
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));
28155
30099
  }
28156
30100
  }
28157
30101
 
28158
30102
  // The props object as a node; the call-site emit embeds it directly.
28159
30103
  const propsExpr = staticFragmentRenderer?.props ?? inheritOriginLoc(b.object(propNodes), node);
28160
30104
  if (descriptorConfig) {
28161
- ctx.runtimeNeeded.add('createElement');
30105
+ ctx.runtimeNeeded.add('createElementFromConfig');
28162
30106
  return {
28163
30107
  id,
28164
30108
  loc: devLoc(ctx, node),
28165
30109
  origin: node,
28166
30110
  isChild: true,
28167
30111
  anchorlessAppendSafe: true,
28168
- valueExpr: inheritOriginLoc(b.call('_$createElement', compNode, propsExpr), node),
30112
+ valueExpr: inheritOriginLoc(
30113
+ b.call('_$createElementFromConfig', b.literal(invocationSite), compNode, propsExpr),
30114
+ node,
30115
+ ),
28169
30116
  };
28170
30117
  }
28171
30118
 
@@ -28341,11 +30288,13 @@ function makeCompCall(
28341
30288
 
28342
30289
  return {
28343
30290
  id,
30291
+ invocationSite,
28344
30292
  compNode,
28345
30293
  propsExpr,
28346
30294
  hostPath: null,
28347
30295
  keyExpr,
28348
- liteEligible,
30296
+ bindingSite: node._octaneBindingSite,
30297
+ liteEligible: node._octaneBindingSite ? false : liteEligible,
28349
30298
  anchorlessAppendSafe,
28350
30299
  autoMemoDeps,
28351
30300
  autoMemoDepNodes,
@@ -28354,8 +30303,8 @@ function makeCompCall(
28354
30303
  autoMemoLite,
28355
30304
  autoMemoPublicationWitnesses,
28356
30305
  voidComponent,
28357
- singleRoot,
28358
- maybeSingleRoot,
30306
+ singleRoot: node._octaneBindingSite ? false : singleRoot,
30307
+ maybeSingleRoot: node._octaneBindingSite ? false : maybeSingleRoot,
28359
30308
  loc: devLoc(ctx, node),
28360
30309
  origin: node,
28361
30310
  };
@@ -28662,7 +30611,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28662
30611
  '`use(promise)` first.',
28663
30612
  );
28664
30613
  }
28665
- // node.left = const x OR const &{x,y} / const [a,b] (destructured)
30614
+ // node.left = const x OR const {x,y} / const [a,b] (destructured)
28666
30615
  // node.right = expr, node.body = BlockStatement,
28667
30616
  // node.key = optional `key …` expression, node.index = optional `index <id>`.
28668
30617
  // `@for (...) { ... } @empty { ... }` — hoist the empty branch as its own
@@ -28678,7 +30627,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28678
30627
  const leftDeclId = node.left.declarations[0].id;
28679
30628
  const isDestructured = leftDeclId.type !== 'Identifier';
28680
30629
  // `itemName` is the identifier used in the body signature + keyFn. For a
28681
- // plain `const x of …`, that's `x`. For a destructured `const &{id} of …`,
30630
+ // plain `const x of …`, that's `x`. For a destructured `const {id} of …`,
28682
30631
  // we synthesize a fresh name and emit the destructuring inside the body so
28683
30632
  // the keyFn still gets the whole item and the body still sees the fields.
28684
30633
  const itemName = isDestructured ? '_item' : leftDeclId.name;
@@ -28785,7 +30734,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28785
30734
  ]
28786
30735
  : [];
28787
30736
 
28788
- // Destructured header `const &{x,y} of …` — synthesize a destructure stmt
30737
+ // Destructured header `const {x,y} of …` — synthesize a destructure stmt
28789
30738
  // at the top of the body so the user fields bind from the synthetic item.
28790
30739
  const destructureInjection = isDestructured
28791
30740
  ? [
@@ -29133,6 +31082,10 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
29133
31082
  }
29134
31083
  }
29135
31084
 
31085
+ if (node._octaneBindingSite) {
31086
+ singleRoot = false;
31087
+ singleRootExpr = null;
31088
+ }
29136
31089
  const ssrMarkerless = isSsrMarkerlessForItem(node);
29137
31090
  const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
29138
31091
  const hostMountSafe =