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
@@ -17,14 +17,16 @@ import { parseModule, builders as b } from '@tsrx/core';
17
17
  import { parseModule as parseFallbackModule } from '#octane/compiler-parser';
18
18
  import { HOOK_NAMES, hookSlotHash } from './compile.js';
19
19
  import { NATIVE_SIGNAL_HOOK_NAMES } from './hook-names.js';
20
- import { METHOD_DEP_IMPORT, annotateHookCalls } from './hook-deps.js';
20
+ import { METHOD_DEP_IMPORT, annotateHookCalls, analyzeStrongMemoCandidates } from './hook-deps.js';
21
21
  import { inlinePlainHookMemos } from './plain-hook-memo.js';
22
22
  import { assertStrongMode } from './strong-mode.js';
23
+ import { unsupportedStrongAutomaticMemo } from './strong-auto-memo.js';
23
24
  import { assertNativeReadDiagnostics, nativeReadOptions } from './native-read-diagnostics.js';
24
25
  import { nativeReadActivationIndex } from './native-read-codegen.js';
25
26
  import { findManualHookProviders, manualHookWrapperParameters } from './manual-hooks.js';
26
27
  import { findLeadingJsxImportSourcePragma } from './pragma.js';
27
28
  import { collectProvenContextBindings, isProvenContextUse } from './context-use.js';
29
+ import { signalDeclarationSourceEdits } from './signal-declarations.js';
28
30
  import {
29
31
  hookMethodName,
30
32
  hasHookMethods,
@@ -1109,6 +1111,43 @@ function emitHookMethodChain(node, owner, st) {
1109
1111
  return true;
1110
1112
  }
1111
1113
 
1114
+ function strongCallSeparator(node, st) {
1115
+ // Parser expression ranges omit grouping parentheses. Append at the CALL
1116
+ // delimiter so a new slot never becomes part of an authored comma expression.
1117
+ const tail = st.source
1118
+ .slice(node.arguments.at(-1)?.end ?? node.start, node.end - 1)
1119
+ .replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '')
1120
+ .trimEnd();
1121
+ return tail.endsWith(',') ? ' ' : ', ';
1122
+ }
1123
+
1124
+ function emitInferredDependencies(dependencies, st) {
1125
+ return dependencies
1126
+ .map((dependency) =>
1127
+ dependency.method
1128
+ ? `${requireParallelHelper(st, METHOD_DEP_IMPORT)}(${dependency.method.root.name}, ${JSON.stringify(dependency.method.name)})`
1129
+ : st.source.slice(dependency.node.start, dependency.node.end),
1130
+ )
1131
+ .join(', ');
1132
+ }
1133
+
1134
+ function emitStrongMemo(node, owner, st) {
1135
+ if (node.type !== 'VariableDeclarator') return;
1136
+ const memo = st.strongMemos?.get(node.start);
1137
+ if (!memo) return;
1138
+ const slot = allocHookSymbol(st, owner, node.id.name, memo.hook, node.init);
1139
+ const helper = requireParallelHelper(
1140
+ st,
1141
+ st.nativeReads && memo.hook === 'useMemo' ? 'nativePuMemo' : memo.hook,
1142
+ );
1143
+ const dependencyText = emitInferredDependencies(memo.dependencies, st);
1144
+ st.edits.push({
1145
+ pos: node.init.start,
1146
+ text: `${helper}(${memo.hook === 'useMemo' ? '() => ' : ''}(`,
1147
+ });
1148
+ st.edits.push({ pos: node.init.end, text: `), [${dependencyText}], ${slot})` });
1149
+ }
1150
+
1112
1151
  function walk(node, owner, st) {
1113
1152
  if (!node || typeof node !== 'object') return;
1114
1153
  if (Array.isArray(node)) {
@@ -1116,6 +1155,8 @@ function walk(node, owner, st) {
1116
1155
  return;
1117
1156
  }
1118
1157
 
1158
+ emitStrongMemo(node, owner, st);
1159
+
1119
1160
  // A `const x = (…) => …` / `function …` gives the enclosing name used in the
1120
1161
  // (debug-only) slot key. For a named declaration the id is on the node; for an
1121
1162
  // arrow/expr assigned to a `const`, take the declarator name.
@@ -1195,17 +1236,17 @@ function walk(node, owner, st) {
1195
1236
  // dependencies are the one synthesized form: the helper call's root is
1196
1237
  // a bare identifier and its name a JSON string, so no arbitrary TS
1197
1238
  // syntax needs reprinting there either.
1198
- const deps = inferred.dependencies
1199
- .map((dependency) =>
1200
- dependency.method
1201
- ? `${requireParallelHelper(st, METHOD_DEP_IMPORT)}(${dependency.method.root.name}, ${JSON.stringify(dependency.method.name)})`
1202
- : st.source.slice(dependency.node.start, dependency.node.end),
1203
- )
1204
- .join(', ');
1205
- st.edits.push({
1206
- pos: node.arguments[node.arguments.length - 1].end,
1207
- text: `, [${deps}], ${sym}`,
1208
- });
1239
+ const deps = emitInferredDependencies(inferred.dependencies, st);
1240
+ if (inferred.replaceDependency) {
1241
+ const argument = node.arguments[inferred.depsIndex];
1242
+ st.edits.push({ pos: argument.start, end: argument.end, text: `[${deps}]` });
1243
+ st.edits.push({ pos: node.end - 1, text: `${strongCallSeparator(node, st)}${sym}` });
1244
+ } else {
1245
+ st.edits.push({
1246
+ pos: st.strong ? node.end - 1 : node.arguments[node.arguments.length - 1].end,
1247
+ text: `${st.strong ? strongCallSeparator(node, st) : ', '}[${deps}], ${sym}`,
1248
+ });
1249
+ }
1209
1250
  } else if (node.arguments.length === 0) {
1210
1251
  // State/ref initializers may themselves be Symbols. Reserve their
1211
1252
  // authored position even when empty; other hooks keep their ABI.
@@ -1217,8 +1258,8 @@ function walk(node, owner, st) {
1217
1258
  // `useState(0)` → `useState(0, _h$N)` — insert AFTER the last arg's end so
1218
1259
  // trailing commas / whitespace before `)` stay valid.
1219
1260
  st.edits.push({
1220
- pos: node.arguments[node.arguments.length - 1].end,
1221
- text: ', ' + sym,
1261
+ pos: st.strong ? node.end - 1 : node.arguments[node.arguments.length - 1].end,
1262
+ text: (st.strong ? strongCallSeparator(node, st) : ', ') + sym,
1222
1263
  });
1223
1264
  }
1224
1265
  }
@@ -1297,7 +1338,7 @@ function parseHookSource(source, id) {
1297
1338
  * `inlineHookMemo: true` enables the production-client whole-AST memo path;
1298
1339
  * the default remains surgical. `manualSlots: true` permits memo and observed
1299
1340
  * getter rewrites without injecting or changing the authored slot policy.
1300
- * @returns {{ code: string, map: any } | null}
1341
+ * @returns {{ code: string, map: any, streamedSignals?: true, diagnostics?: any[] } | null}
1301
1342
  */
1302
1343
  export function slotHooks(source, id, options) {
1303
1344
  const environment = options?.environment ?? 'client';
@@ -1314,15 +1355,34 @@ export function slotHooks(source, id, options) {
1314
1355
  return null; // let the normal pipeline surface the parse error
1315
1356
  }
1316
1357
  options = nativeReadOptions(ast, options);
1317
- assertStrongMode(ast, source, id, options);
1358
+ const strongAnalysis = assertStrongMode(ast, source, id, { ...options, onlyImported: true });
1359
+ const strongHints = strongAnalysis?.diagnostics.length
1360
+ ? { diagnostics: strongAnalysis.diagnostics }
1361
+ : {};
1318
1362
  assertNativeReadDiagnostics(ast, source, id, options);
1363
+ const strongHookAnalysis = strongAnalysis?.strongHookAnalysis ?? null;
1364
+ const strongPlan = strongHookAnalysis
1365
+ ? analyzeStrongMemoCandidates(ast, { ...options, onlyImported: true, strongHookAnalysis })
1366
+ : null;
1367
+ if (strongPlan?.candidates.size && options?.manualSlots)
1368
+ throw unsupportedStrongAutomaticMemo(
1369
+ ast.body[0],
1370
+ id,
1371
+ 'Strong declaration caching cannot add cache slots to a manually slotted module.',
1372
+ );
1319
1373
  const importInfo = octaneHookLocals(
1320
1374
  ast,
1321
1375
  options?.nativeReads === true,
1322
- findLeadingJsxImportSourcePragma(source) === 'octane',
1376
+ // The bundler claims both pragmas for Octane (see pragmaOwnedModules).
1377
+ ['octane', 'octane/strong'].includes(findLeadingJsxImportSourcePragma(source)),
1323
1378
  );
1379
+ if (strongHookAnalysis?.callNames.size)
1380
+ importInfo.importsHook ||= [...strongHookAnalysis.callNames.values()].some((name) =>
1381
+ HOOK_NAMES.has(name),
1382
+ );
1324
1383
  const manualProviders = options?.manualSlots ? findManualHookProviders(ast) : new Map();
1325
1384
  const nativeReadActivation = options?.nativeReads === true && importsNativeRenderer(ast);
1385
+ const signalLowering = signalDeclarationSourceEdits(ast, id, source);
1326
1386
  const canSpecializeRoot =
1327
1387
  !options?.manualSlots &&
1328
1388
  !options?.hmr &&
@@ -1333,9 +1393,10 @@ export function slotHooks(source, id, options) {
1333
1393
  !importInfo.importsHook &&
1334
1394
  !canSpecializeRoot &&
1335
1395
  !nativeReadActivation &&
1396
+ !signalLowering.usesSignals &&
1336
1397
  !manualProviders.size
1337
1398
  ) {
1338
- return null;
1399
+ return strongAnalysis?.diagnostics.length ? { code: source, map: null, ...strongHints } : null;
1339
1400
  }
1340
1401
  // The parsed tree is never mutated: annotateHookCalls returns a COW-rebuilt
1341
1402
  // module whose hook calls carry their `_octane*` props (start/end offsets are
@@ -1346,6 +1407,7 @@ export function slotHooks(source, id, options) {
1346
1407
  const annotated = annotateHookCalls(ast, {
1347
1408
  filename: id,
1348
1409
  onlyImported: true,
1410
+ ...(strongHookAnalysis ? { strongHookAnalysis } : {}),
1349
1411
  nativeReads: options?.nativeReads === true,
1350
1412
  ...(options?.nativeReads === true
1351
1413
  ? { hookRuntimeModules: ['octane/signals/client', 'octane/signals/server'] }
@@ -1357,6 +1419,9 @@ export function slotHooks(source, id, options) {
1357
1419
  }
1358
1420
  const getterCalls = importInfo.importsHook ? collectStateGetterCalls(ast) : new WeakSet();
1359
1421
  if (
1422
+ !strongAnalysis?.enabled &&
1423
+ // The whole-AST memo path does not apply declaration identity edits.
1424
+ !signalLowering.usesSignals &&
1360
1425
  canPrint &&
1361
1426
  options?.inlineHookMemo === true &&
1362
1427
  environment === 'client' &&
@@ -1380,9 +1445,16 @@ export function slotHooks(source, id, options) {
1380
1445
  getterCalls,
1381
1446
  stateGetterHelpers: STATE_GETTER_HELPERS,
1382
1447
  });
1383
- if (inlined !== null) return inlined;
1448
+ if (inlined !== null) return { ...inlined, ...strongHints };
1384
1449
  }
1385
1450
  const st = {
1451
+ strong: strongAnalysis?.enabled === true,
1452
+ strongMemos: new Map(
1453
+ [...(strongPlan?.candidates ?? [])].map(([node, hook]) => [
1454
+ node.start,
1455
+ { hook, dependencies: strongPlan.candidateDependencies.get(node) },
1456
+ ]),
1457
+ ),
1386
1458
  manualSlots: options?.manualSlots === true,
1387
1459
  nativeReads: options?.nativeReads === true,
1388
1460
  locals: importInfo.locals,
@@ -1398,7 +1470,7 @@ export function slotHooks(source, id, options) {
1398
1470
  hash: hookSlotHash(id),
1399
1471
  nextId: 0,
1400
1472
  nextPuId: 0,
1401
- edits: [],
1473
+ edits: [...signalLowering.edits],
1402
1474
  decls: [],
1403
1475
  parallelHelpers: new Map(),
1404
1476
  provenContextBindings: collectProvenContextBindings(ast),
@@ -1418,7 +1490,8 @@ export function slotHooks(source, id, options) {
1418
1490
  if (canSpecializeRoot) {
1419
1491
  collectVoidRootEdits(ast, st, options.isVoidComponentImport);
1420
1492
  }
1421
- if (st.edits.length === 0 && !nativeReadActivation) return null;
1493
+ if (st.edits.length === 0 && !nativeReadActivation && !signalLowering.usesSignals)
1494
+ return strongAnalysis?.diagnostics.length ? { code: source, map: null, ...strongHints } : null;
1422
1495
  const activation = nativeReadActivation
1423
1496
  ? requireParallelHelper(st, 'enableNativeReadCollection')
1424
1497
  : null;
@@ -1458,6 +1531,35 @@ export function slotHooks(source, id, options) {
1458
1531
  const otherHelperImports = [...otherHelpers]
1459
1532
  .map(([request, specifiers]) => `import { ${specifiers.join(', ')} } from '${request}';\n`)
1460
1533
  .join('');
1534
+ const signalHelperImports = new Map();
1535
+ for (const helper of signalLowering.imports) {
1536
+ let specifiers = signalHelperImports.get(helper.source);
1537
+ if (specifiers === undefined) signalHelperImports.set(helper.source, (specifiers = []));
1538
+ specifiers.push(`${helper.imported} as ${helper.local}`);
1539
+ }
1540
+ let signalActivation = '';
1541
+ if (signalLowering.usesSignals) {
1542
+ const request =
1543
+ environment === 'server'
1544
+ ? 'octane/internal/server'
1545
+ : nativeReadActivation
1546
+ ? 'octane/internal/client'
1547
+ : 'octane/signals';
1548
+ const imported =
1549
+ environment === 'server'
1550
+ ? 'enableServerSignalBindings'
1551
+ : nativeReadActivation
1552
+ ? 'enableSignalBindings'
1553
+ : '__enableSignalDocument';
1554
+ const local = allocSlotName(st, `_$${imported}`);
1555
+ let specifiers = signalHelperImports.get(request);
1556
+ if (specifiers === undefined) signalHelperImports.set(request, (specifiers = []));
1557
+ specifiers.push(`${imported} as ${local}`);
1558
+ signalActivation = `${local}(1);\n`;
1559
+ }
1560
+ const signalImports = [...signalHelperImports]
1561
+ .map(([request, specifiers]) => `import { ${specifiers.join(', ')} } from '${request}';\n`)
1562
+ .join('');
1461
1563
  const profileImport =
1462
1564
  st.profile && !st.manualSlots
1463
1565
  ? "import { __profileHook as _$__profileHook } from 'octane/profiling';\n"
@@ -1467,26 +1569,58 @@ export function slotHooks(source, id, options) {
1467
1569
  ? `const ${st.slotBaseName} = /* @__PURE__ */ ${st.hookSlotsName}(${st.nextId});\n`
1468
1570
  : '';
1469
1571
  const block =
1470
- helperImport + otherHelperImports + profileImport + slotBase + st.decls.join('\n') + '\n';
1471
- if (activation !== null) {
1472
- // Native plain modules can render a local function during evaluation.
1473
- // Its slots and invocation collector must already exist at that call.
1572
+ helperImport +
1573
+ otherHelperImports +
1574
+ signalImports +
1575
+ profileImport +
1576
+ slotBase +
1577
+ st.decls.join('\n') +
1578
+ '\n' +
1579
+ signalActivation;
1580
+ if (activation !== null || signalLowering.usesSignals) {
1581
+ // Plain modules may read global signals or render during evaluation.
1582
+ // Their document capability and any render slots must already exist.
1474
1583
  // No newline is inserted, retaining the surgical pass's line mapping.
1475
1584
  const index = nativeReadActivationIndex(ast.body);
1476
1585
  st.edits.push({
1477
1586
  pos: ast.body[index]?.start ?? source.length,
1478
- text: `;${block.replace(/\n/g, ' ')}${activation}(1); `,
1587
+ text: `;${block.replace(/\n/g, ' ')}${activation === null ? '' : `${activation}(1); `}`,
1479
1588
  });
1480
1589
  }
1481
- // Apply insertions right-to-left so earlier offsets stay valid.
1590
+ // Assemble disjoint edits once instead of copying the growing module for
1591
+ // every insertion. Descending, stable order retains the existing rule that
1592
+ // later insertions at the same offset appear before earlier insertions.
1482
1593
  st.edits.sort((a, b) => b.pos - a.pos);
1483
- let code = source;
1594
+ const chunks = [];
1595
+ let cursor = source.length;
1596
+ let overlaps = false;
1484
1597
  for (const edit of st.edits) {
1485
- code =
1486
- code.slice(0, edit.pos) +
1487
- edit.text +
1488
- code.slice(edit.end === undefined ? edit.pos : edit.end);
1598
+ const end = edit.end ?? edit.pos;
1599
+ if (end > cursor) {
1600
+ overlaps = true;
1601
+ break;
1602
+ }
1603
+ chunks.push(source.slice(end, cursor), edit.text);
1604
+ cursor = edit.pos;
1489
1605
  }
1490
- if (activation === null) code = code.endsWith('\n') ? code + block : code + '\n' + block;
1491
- return { code, map: null };
1606
+ let code;
1607
+ if (overlaps) {
1608
+ // Overlapping replacements refer to the already edited text. Preserve
1609
+ // that sequential behavior, including insertion/replacement ties.
1610
+ code = source;
1611
+ for (const edit of st.edits) {
1612
+ code = code.slice(0, edit.pos) + edit.text + code.slice(edit.end ?? edit.pos);
1613
+ }
1614
+ } else {
1615
+ chunks.push(source.slice(0, cursor));
1616
+ code = chunks.reverse().join('');
1617
+ }
1618
+ if (activation === null && !signalLowering.usesSignals)
1619
+ code = code.endsWith('\n') ? code + block : code + '\n' + block;
1620
+ return {
1621
+ code,
1622
+ map: null,
1623
+ ...(signalLowering.usesSignals || nativeReadActivation ? { streamedSignals: true } : null),
1624
+ ...strongHints,
1625
+ };
1492
1626
  }
@@ -0,0 +1,90 @@
1
+ import { builders as b } from '@tsrx/core';
2
+ import { inheritHookMemoOrigin as origin } from './inline-hook-memo.js';
3
+ import { analyzeStrongMemoCandidates, STRONG_AUTOMATIC_MEMO_UNSUPPORTED } from './hook-deps.js';
4
+
5
+ const SKIP_KEYS = new Set(['loc', 'start', 'end', 'range', 'metadata', 'parent']);
6
+
7
+ export function unsupportedStrongAutomaticMemo(node, filename, message) {
8
+ const line = node?.loc?.start?.line ?? 1;
9
+ const column = node?.loc?.start?.column ?? 0;
10
+ const error = new SyntaxError(
11
+ `${filename ?? '<anonymous>'}:${line}:${column + 1}: [${STRONG_AUTOMATIC_MEMO_UNSUPPORTED}] ${message}`,
12
+ );
13
+ Object.assign(error, {
14
+ code: STRONG_AUTOMATIC_MEMO_UNSUPPORTED,
15
+ filename,
16
+ loc: { line, column },
17
+ pos: node?.start ?? 0,
18
+ end: node?.end ?? 0,
19
+ });
20
+ return error;
21
+ }
22
+
23
+ /**
24
+ * Cache eligible Strong declarations before dependency inference. These hooks
25
+ * retain effect/subscription identity in development as well as production;
26
+ * later production passes may inline the same runtime cache semantics.
27
+ * The caller invokes this only after validating authored Strong source.
28
+ */
29
+ export function applyStrongAutomaticMemo(ast, options = {}) {
30
+ const { candidates, names, hookCalls, omittedDependencies } = analyzeStrongMemoCandidates(
31
+ ast,
32
+ options,
33
+ );
34
+ if (candidates.size === 0 && hookCalls.size === 0 && omittedDependencies.size === 0) return ast;
35
+ const aliases = new Map();
36
+ for (const hook of new Set([...candidates.values(), ...hookCalls.values()])) {
37
+ let alias = `_$strong${hook}`;
38
+ while (names.has(alias)) alias += '_';
39
+ names.add(alias);
40
+ aliases.set(hook, alias);
41
+ }
42
+ function rebuild(node) {
43
+ if (!node || typeof node !== 'object') return node;
44
+ if (Array.isArray(node)) {
45
+ const children = node.map(rebuild);
46
+ return children.some((child, index) => child !== node[index]) ? children : node;
47
+ }
48
+ const importedHook = hookCalls.get(node);
49
+ const omittedDependency = omittedDependencies.has(node);
50
+ if (importedHook || omittedDependency) {
51
+ // Built-in exports are immutable and defined: normalize proven aliases
52
+ // and optional calls so existing dependency/slot passes see their ABI.
53
+ return {
54
+ ...node,
55
+ callee: importedHook ? b.id(aliases.get(importedHook), node.callee) : rebuild(node.callee),
56
+ optional: false,
57
+ arguments: rebuild(node.arguments),
58
+ // Replace the proven placeholder during inference, keeping later slot
59
+ // arguments at their authored positions. Never rewrite an array.
60
+ ...(omittedDependency ? { _octaneStrongOmittedDependency: true } : {}),
61
+ };
62
+ }
63
+ const hook = candidates.get(node);
64
+ if (hook) {
65
+ const initial = rebuild(node.init);
66
+ const callback = hook === 'useCallback' ? initial : origin(b.arrow([], initial), node.init);
67
+ return { ...node, init: origin(b.call(b.id(aliases.get(hook)), callback), node.init) };
68
+ }
69
+ let output = node;
70
+ for (const key in node) {
71
+ if (SKIP_KEYS.has(key)) continue;
72
+ const value = rebuild(node[key]);
73
+ if (value !== node[key]) {
74
+ if (output === node) output = { ...node };
75
+ output[key] = value;
76
+ }
77
+ }
78
+ return output;
79
+ }
80
+ const result = rebuild(ast);
81
+ if (aliases.size === 0) return result;
82
+ const imports = origin(
83
+ b.imports(
84
+ [...aliases].map(([hook, alias]) => [hook, alias]),
85
+ 'octane',
86
+ ),
87
+ ast.body.find((node) => node.loc) ?? ast,
88
+ );
89
+ return { ...result, body: [...result.body, imports] };
90
+ }
@@ -0,0 +1,143 @@
1
+ import { createRendererRegionResolver } from './renderer-boundaries.js';
2
+
3
+ export const STRONG_UNTRUSTED_HTML = 'OCTANE_STRONG_UNTRUSTED_HTML';
4
+ export const STRONG_HTML_MESSAGE =
5
+ 'Strong mode requires a TrustedHTML value for dangerouslySetInnerHTML. Use trustHTML() only for trusted or already sanitized HTML; trustHTML() does not sanitize its input.';
6
+ const SKIP_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'metadata', 'parent']);
7
+ const TRANSPARENT = new Set([
8
+ 'ParenthesizedExpression',
9
+ 'TSAsExpression',
10
+ 'TSSatisfiesExpression',
11
+ 'TSTypeAssertion',
12
+ 'TSNonNullExpression',
13
+ 'TSInstantiationExpression',
14
+ ]);
15
+
16
+ // Identifier escapes use Unicode; quoted property names also admit hex and
17
+ // escaped letters. Ordinary escapes such as a newline cannot spell this key.
18
+ export function mayHaveStrongHTML(source) {
19
+ return (
20
+ source.includes('dangerouslySetInnerHTML') ||
21
+ /\\(?:u|x|[dageoslySIHTML]|[\r\n\u2028\u2029])/.test(source)
22
+ );
23
+ }
24
+
25
+ function unwrap(node) {
26
+ while (node && TRANSPARENT.has(node.type)) node = node.expression;
27
+ return node;
28
+ }
29
+
30
+ function propertyName(node) {
31
+ return node?.type === 'Identifier' || node?.type === 'JSXIdentifier' ? node.name : node?.value;
32
+ }
33
+
34
+ export function checkStrongHTMLValue(expression, report) {
35
+ const node = unwrap(expression);
36
+ if (!node) return;
37
+ switch (node.type) {
38
+ case 'ObjectExpression':
39
+ // A spread of an existing branded value retains its type. The type
40
+ // checker verifies that value rather than rejecting all object syntax.
41
+ if (!node.properties.some((property) => property.type === 'SpreadElement')) report(node);
42
+ return;
43
+ case 'Literal':
44
+ if (node.value !== null) report(node);
45
+ return;
46
+ case 'TemplateLiteral':
47
+ case 'ArrayExpression':
48
+ case 'ArrowFunctionExpression':
49
+ case 'FunctionExpression':
50
+ report(node);
51
+ return;
52
+ case 'ConditionalExpression':
53
+ checkStrongHTMLValue(node.consequent, report);
54
+ checkStrongHTMLValue(node.alternate, report);
55
+ return;
56
+ case 'LogicalExpression':
57
+ checkStrongHTMLValue(node.left, report);
58
+ checkStrongHTMLValue(node.right, report);
59
+ return;
60
+ case 'SequenceExpression':
61
+ checkStrongHTMLValue(node.expressions.at(-1), report);
62
+ }
63
+ }
64
+ // Resolve only a statically visible final writer. An unknown later spread
65
+ // may replace an earlier raw value, so its actual type belongs to TypeScript.
66
+ export function strongHTMLSpreadWriter(expression) {
67
+ const node = unwrap(expression);
68
+ if (node?.type !== 'ObjectExpression') return undefined;
69
+ for (let index = node.properties.length - 1; index >= 0; index--) {
70
+ const property = node.properties[index];
71
+ if (property.type === 'SpreadElement') {
72
+ const writer = strongHTMLSpreadWriter(property.argument);
73
+ if (writer !== null) return writer;
74
+ } else if (property.type === 'Property') {
75
+ if (property.computed && property.key.type !== 'Literal') return undefined;
76
+ if (propertyName(property.key) === 'dangerouslySetInnerHTML')
77
+ return property.kind === 'init' ? property.value : undefined;
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * A syntax-only compiler can reject visibly unbranded values. Imported values,
85
+ * props, aliases and function results are checked by Strong's nominal JSX types
86
+ * in Volar/tsrx-tsc; guessing their types here would reject valid trust boundaries.
87
+ */
88
+ export function analyzeStrongHTML(ast, source, filename, options = {}) {
89
+ if (!mayHaveStrongHTML(source)) return [];
90
+ const diagnostics = [];
91
+ const reported = new WeakSet();
92
+ const isDOM =
93
+ options.rendererRegionResolver ?? createRendererRegionResolver(ast, source, filename, options);
94
+ function report(node) {
95
+ if (reported.has(node)) return;
96
+ reported.add(node);
97
+ const position = (edge) => ({
98
+ offset: node[edge] ?? 0,
99
+ line: node.loc?.[edge]?.line ?? 1,
100
+ column: node.loc?.[edge]?.column ?? 0,
101
+ });
102
+ diagnostics.push({
103
+ code: STRONG_UNTRUSTED_HTML,
104
+ severity: 'error',
105
+ filename,
106
+ start: position('start'),
107
+ end: position('end'),
108
+ message: STRONG_HTML_MESSAGE,
109
+ suggestions: [],
110
+ });
111
+ }
112
+ function visit(node) {
113
+ if (!node || typeof node !== 'object') return;
114
+ if (Array.isArray(node)) {
115
+ for (const item of node) visit(item);
116
+ return;
117
+ }
118
+ if (
119
+ node.type === 'JSXOpeningElement' &&
120
+ node.name?.type === 'JSXIdentifier' &&
121
+ /^[a-z]/.test(node.name.name) &&
122
+ isDOM(node)
123
+ ) {
124
+ for (let index = node.attributes.length - 1; index >= 0; index--) {
125
+ const attribute = node.attributes[index];
126
+ if (attribute.type === 'JSXSpreadAttribute') {
127
+ const writer = strongHTMLSpreadWriter(attribute.argument);
128
+ if (writer === null) continue;
129
+ checkStrongHTMLValue(writer, report);
130
+ break;
131
+ } else if (propertyName(attribute.name) === 'dangerouslySetInnerHTML') {
132
+ if (attribute.value?.type === 'JSXExpressionContainer')
133
+ checkStrongHTMLValue(attribute.value.expression, report);
134
+ else report(attribute.value ?? attribute);
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ for (const key of Object.keys(node)) if (!SKIP_KEYS.has(key)) visit(node[key]);
140
+ }
141
+ visit(ast);
142
+ return diagnostics;
143
+ }