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
@@ -1,7 +1,24 @@
1
- import { collectReassignedBindings } from './hook-deps.js';
1
+ import {
2
+ analyzeStrongHookBindings,
3
+ analyzeStrongHookPolicies,
4
+ collectReassignedBindings,
5
+ } from './hook-deps.js';
6
+ import { createRendererRegionResolver } from './renderer-boundaries.js';
7
+ import { analyzeStrongHTML } from './strong-html.js';
8
+ import { analyzeNativeChangeDiagnostics } from './native-change-diagnostics.js';
9
+ import { createStrongTemplatePolicy } from './strong-template-policy.js';
2
10
 
3
11
  const STATE_HOOKS = new Set(['useState', 'useReducer', 'useLinkedState']);
4
12
  const EFFECT_HOOKS = new Set(['useEffect', 'useLayoutEffect', 'useInsertionEffect']);
13
+ const EFFECT_CONTROL_TYPES = new Set([
14
+ 'ForStatement',
15
+ 'ForInStatement',
16
+ 'ForOfStatement',
17
+ 'WhileStatement',
18
+ 'DoWhileStatement',
19
+ 'SwitchStatement',
20
+ 'LabeledStatement',
21
+ ]);
5
22
  const ARRAY_MUTATORS = new Set([
6
23
  'copyWithin',
7
24
  'fill',
@@ -50,20 +67,12 @@ const TRUTHY_VALUE = 8;
50
67
  const UNKNOWN_VALUE = NULLISH_VALUE | FALSY_VALUE | TRUTHY_VALUE;
51
68
  const UNKNOWN_PRIMITIVE = Symbol('unknown primitive');
52
69
  const NO_RETURN_VALUE = Symbol('no return value');
70
+ const NORMAL_COMPLETIONS = new Set(['normal']);
53
71
  const OTHER_BINDING = { kind: 'other' };
54
72
  const SNAPSHOT_BINDING = { kind: 'snapshot' };
55
- const ARRAY_SNAPSHOT_BINDING = { kind: 'snapshot', array: true };
56
73
  const STATE_GETTER_BINDING = { kind: 'getter' };
57
- const STATE_TUPLE_BINDING = {
58
- kind: 'state-tuple',
59
- snapshot: SNAPSHOT_BINDING,
60
- getter: STATE_GETTER_BINDING,
61
- };
62
- const ARRAY_STATE_TUPLE_BINDING = {
63
- kind: 'state-tuple',
64
- snapshot: ARRAY_SNAPSHOT_BINDING,
65
- getter: STATE_GETTER_BINDING,
66
- };
74
+ const PROP_BINDING = { kind: 'prop' };
75
+ const FETCH_BINDING = { kind: 'fetch' };
67
76
  const UNDEFINED_BINDING = { kind: 'constant', value: UNDEFINED_VALUE, primitive: undefined };
68
77
  const GLOBAL_OBJECT_BINDING = { kind: 'global-object' };
69
78
  const AMBIENT_GLOBAL_BINDINGS = new Map(
@@ -172,6 +181,9 @@ export const STRONG_RETAINED_ROW_MUTATION = 'OCTANE_STRONG_RETAINED_ROW_MUTATION
172
181
  export const STRONG_RENDER_IMPURE_CALL = 'OCTANE_STRONG_RENDER_IMPURE_CALL';
173
182
  export const STRONG_RENDER_EFFECT_EVENT_CALL = 'OCTANE_STRONG_RENDER_EFFECT_EVENT_CALL';
174
183
  export const STRONG_EFFECT_EVENT_DEPENDENCY = 'OCTANE_STRONG_EFFECT_EVENT_DEPENDENCY';
184
+ export const STRONG_EFFECT_DATA_FETCH = 'OCTANE_STRONG_EFFECT_DATA_FETCH';
185
+ export const STRONG_EFFECT_CHAIN = 'OCTANE_STRONG_EFFECT_CHAIN';
186
+ export const STRONG_UNLINKED_PROP_STATE = 'OCTANE_STRONG_UNLINKED_PROP_STATE';
175
187
  export const STRONG_DIRECTIVE_PLACEMENT = 'OCTANE_STRONG_DIRECTIVE_PLACEMENT';
176
188
  export const STRONG_HOOK_LOCALITY = 'OCTANE_STRONG_HOOK_LOCALITY';
177
189
  export const STRONG_EVENT_HANDLER_LOCALITY = 'OCTANE_STRONG_EVENT_HANDLER_LOCALITY';
@@ -240,12 +252,20 @@ function bindSnapshotPattern(pattern, value, bind) {
240
252
  } else if (pattern?.type === 'ObjectPattern') {
241
253
  for (const property of pattern.properties ?? []) {
242
254
  if (property.type === 'Property') {
243
- bindSnapshotPattern(property.value, SNAPSHOT_BINDING, bind);
255
+ bindSnapshotPattern(
256
+ property.value,
257
+ value.state ? { kind: 'snapshot', state: value.state } : SNAPSHOT_BINDING,
258
+ bind,
259
+ );
244
260
  }
245
261
  }
246
262
  } else if (pattern?.type === 'ArrayPattern') {
247
263
  for (const element of pattern.elements ?? []) {
248
- bindSnapshotPattern(element, SNAPSHOT_BINDING, bind);
264
+ bindSnapshotPattern(
265
+ element,
266
+ value.state ? { kind: 'snapshot', state: value.state } : SNAPSHOT_BINDING,
267
+ bind,
268
+ );
249
269
  }
250
270
  }
251
271
  // Rest copies and default expressions can produce new mutable values.
@@ -306,7 +326,9 @@ function predeclareStatements(statements, scope) {
306
326
  for (const original of statements ?? []) {
307
327
  const node = declarationOf(original);
308
328
  if (node?.type === 'ImportDeclaration') {
309
- const octane = node.source?.value === 'octane' && node.importKind !== 'type';
329
+ const octane =
330
+ (node.source?.value === 'octane' || node.source?.value === 'octane/server') &&
331
+ node.importKind !== 'type';
310
332
  for (const specifier of node.specifiers ?? []) {
311
333
  if (!specifier.local?.name || specifier.importKind === 'type') continue;
312
334
  if (octane && specifier.type === 'ImportNamespaceSpecifier') {
@@ -343,6 +365,7 @@ function createScope(
343
365
  kind,
344
366
  bindings: new BindingMap(parent?.bindings.clock ?? { value: 0 }),
345
367
  isReassigned,
368
+ hookNames: parent?.hookNames,
346
369
  };
347
370
  for (const param of params) addPatternNames(param, scope.bindings, { kind: 'other' });
348
371
  predeclareStatements(statements, scope);
@@ -375,12 +398,15 @@ function resolveScope(scope, name) {
375
398
  }
376
399
 
377
400
  function importedHook(callee, scope) {
401
+ if (scope.hookNames !== undefined) return scope.hookNames.get(callee) ?? null;
378
402
  const value = unwrap(callee);
379
403
  if (value?.type === 'Identifier') {
380
404
  const binding = resolve(scope, value.name);
381
405
  return binding?.kind === 'hook' ? binding.hook : null;
382
406
  }
383
- if (value?.type !== 'MemberExpression' || value.optional === true) return null;
407
+ // An imported namespace is never nullish. Optional access to its known
408
+ // hooks has the same execution phase as ordinary namespace access.
409
+ if (value?.type !== 'MemberExpression') return null;
384
410
  const object = unwrap(value.object);
385
411
  if (object?.type !== 'Identifier' || resolve(scope, object.name)?.kind !== 'namespace') {
386
412
  return null;
@@ -456,7 +482,7 @@ function strongDirectives(ast) {
456
482
  // can be declared there; a value used by siblings belongs to their parent.
457
483
  // Keep this source-level check separate from the phase analysis above: it never
458
484
  // annotates the parser tree or changes the code emitted for a valid module.
459
- function strongLocalityDiagnostics(ast, source, filename, isReassigned) {
485
+ function strongLocalityDiagnostics(ast, source, filename, isReassigned, hookNames) {
460
486
  const diagnostics = [];
461
487
  const values = [];
462
488
  const effects = [];
@@ -466,6 +492,7 @@ function strongLocalityDiagnostics(ast, source, filename, isReassigned) {
466
492
  const hostAttributes = new WeakSet();
467
493
  const componentHandlers = new WeakSet();
468
494
  const moduleScope = createScope(null, 'module', ast.body ?? []);
495
+ moduleScope.hookNames = hookNames;
469
496
  const mayHaveHoistedVars = source.includes('var');
470
497
  let activeHandler = null;
471
498
  let activeDerivedValue = null;
@@ -1286,6 +1313,25 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1286
1313
  }
1287
1314
  const enabled = options.strong === true || directives.enabled;
1288
1315
  if (!enabled) return { enabled: false, diagnostics };
1316
+ const strongHookAnalysis = analyzeStrongHookBindings(ast, options);
1317
+ const hookNames = new WeakMap();
1318
+ for (const [call, name] of strongHookAnalysis.callNames) {
1319
+ if (
1320
+ LOCAL_VALUE_HOOKS.has(name) ||
1321
+ EFFECT_HOOKS.has(name) ||
1322
+ name === 'useImperativeHandle' ||
1323
+ name === 'memo' ||
1324
+ name === 'lazy'
1325
+ )
1326
+ hookNames.set(call.callee, name);
1327
+ }
1328
+ options = {
1329
+ ...options,
1330
+ strongHookAnalysis,
1331
+ rendererRegionResolver:
1332
+ options.rendererRegionResolver ??
1333
+ createRendererRegionResolver(ast, source, filename, options),
1334
+ };
1289
1335
 
1290
1336
  let reassignedBindings;
1291
1337
  function isReassigned(identifier) {
@@ -1293,6 +1339,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1293
1339
  return reassignedBindings.has(identifier);
1294
1340
  }
1295
1341
  const moduleScope = createScope(null, 'module', ast.body ?? [], [], isReassigned);
1342
+ moduleScope.hookNames = hookNames;
1296
1343
  const mutableModuleDeclarations = new Map();
1297
1344
  const reassignedModuleNames = new Map();
1298
1345
  const moduleAmbientAliases = new Map();
@@ -1330,6 +1377,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1330
1377
  const activeCallbacks = new Set();
1331
1378
  const activeReturnCallbacks = new Set();
1332
1379
  const callResults = new WeakMap();
1380
+ const stateTuples = new WeakMap();
1381
+ const effectRecords = [];
1382
+ const effectBranchExits = new WeakMap();
1383
+ const unlinkedPropInitializers = new Set();
1384
+ let renderOwner = null;
1385
+ let currentEffect = null;
1386
+ let currentEffectControl = null;
1387
+ let currentFunction = null;
1388
+ let fetchContinuation = false;
1389
+ let collectEffectReads = true;
1390
+ let effectOwnsWrites = true;
1333
1391
  const reportedDiagnostics = new WeakMap();
1334
1392
  const hoistedVarNames = new WeakMap();
1335
1393
  const mayHaveHoistedVars = source.includes('var');
@@ -1367,7 +1425,13 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1367
1425
  renderOutputs.set(fn, result);
1368
1426
  return result;
1369
1427
  }
1428
+ // Hook-named module functions keep unknown arguments even when they build
1429
+ // JSX: their callers, not props, supply the values.
1430
+ const hookNamedRoots = new WeakSet();
1370
1431
  function addNamedRenderRoot(fn, name) {
1432
+ if (FUNCTION_TYPES.has(fn?.type) && /^(?:unstable_)?use[A-Z0-9]/.test(name)) {
1433
+ hookNamedRoots.add(fn);
1434
+ }
1371
1435
  if (
1372
1436
  FUNCTION_TYPES.has(fn?.type) &&
1373
1437
  (/^(?:unstable_)?use[A-Z0-9]/.test(name) || (/^[A-Z]/.test(name) && hasRenderOutput(fn)))
@@ -1411,6 +1475,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
1411
1475
  let currentFunctionChecksImpureCalls = false;
1412
1476
  let currentFunctionChecksRenderReads = false;
1413
1477
  let currentRetainedRowScope = null;
1478
+ const templatePolicy = createStrongTemplatePolicy({
1479
+ ast,
1480
+ report,
1481
+ resolve,
1482
+ unwrap,
1483
+ callableValue,
1484
+ staticPrimitiveValue,
1485
+ source,
1486
+ filename,
1487
+ options,
1488
+ });
1414
1489
 
1415
1490
  function predeclareHoistedVars(node, scope) {
1416
1491
  if (!mayHaveHoistedVars || node == null) return;
@@ -2071,7 +2146,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2071
2146
  statement.type === 'ReturnStatement' ||
2072
2147
  statement.type === 'ThrowStatement' ||
2073
2148
  statement.type === 'BreakStatement' ||
2074
- statement.type === 'ContinueStatement'
2149
+ statement.type === 'ContinueStatement' ||
2150
+ (currentEffect !== null && branchAlwaysExits(statement))
2075
2151
  ) {
2076
2152
  break;
2077
2153
  }
@@ -2145,16 +2221,18 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2145
2221
  ? expressionBinding(parameter.right, parameterScope)
2146
2222
  : null;
2147
2223
  value =
2148
- value.kind === 'constant' && value.primitive === undefined
2149
- ? defaultValue
2150
- : definitelyDefined(value)
2151
- ? value
2152
- : defaultValue?.kind === 'getter' ||
2153
- defaultValue?.kind === 'state-tuple' ||
2154
- defaultValue === GLOBAL_OBJECT_BINDING ||
2155
- defaultValue?.kind === 'ambient'
2156
- ? defaultValue
2157
- : OTHER_BINDING;
2224
+ value?.kind === 'prop'
2225
+ ? value
2226
+ : value.kind === 'constant' && value.primitive === undefined
2227
+ ? defaultValue
2228
+ : definitelyDefined(value)
2229
+ ? value
2230
+ : defaultValue?.kind === 'getter' ||
2231
+ defaultValue?.kind === 'state-tuple' ||
2232
+ defaultValue === GLOBAL_OBJECT_BINDING ||
2233
+ defaultValue?.kind === 'ambient'
2234
+ ? defaultValue
2235
+ : OTHER_BINDING;
2158
2236
  visitPatternExpressions(
2159
2237
  parameter.left,
2160
2238
  parameterScope,
@@ -2166,11 +2244,13 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2166
2244
  );
2167
2245
  bindStateGetterPattern(parameter.left, value, parameterScope, bindGetter);
2168
2246
  bindAmbientPattern(parameter.left, value, parameterScope, bindGetter);
2247
+ if (value?.kind === 'prop') bindPropPattern(parameter.left, bindGetter);
2169
2248
  parameter = parameter.left;
2170
2249
  } else {
2171
2250
  visitPatternExpressions(parameter, parameterScope, phase, value?.kind === 'ref', value);
2172
2251
  bindStateGetterPattern(parameter, value, parameterScope, bindGetter);
2173
2252
  bindAmbientPattern(parameter, value, parameterScope, bindGetter);
2253
+ if (value?.kind === 'prop') bindPropPattern(parameter, bindGetter);
2174
2254
  }
2175
2255
  if (parameter.type === 'Identifier' && !isReassigned(parameter)) {
2176
2256
  parameterScope.bindings.set(parameter.name, value);
@@ -2180,6 +2260,20 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2180
2260
  }
2181
2261
 
2182
2262
  function visitFunction(node, parentScope, phase, args = null) {
2263
+ const enclosingFunction = currentFunction;
2264
+ const enclosingOwner = renderOwner;
2265
+ const enclosingFetchContinuation = fetchContinuation;
2266
+ currentFunction = node;
2267
+ if (
2268
+ (phase === 'render' && renderOwner === null) ||
2269
+ (!activeCallbacks.has(node) && hasRenderOutput(node))
2270
+ )
2271
+ renderOwner = node;
2272
+ if (args === null && phase === 'render' && hasRenderOutput(node) && !hookNamedRoots.has(node)) {
2273
+ // Component arguments are immutable prop projections. Custom hook
2274
+ // arguments remain unknown unless a caller supplies a proven value.
2275
+ args = (node.params ?? []).map(() => PROP_BINDING);
2276
+ }
2183
2277
  const enclosingFunctionIsAsync = currentFunctionIsAsync;
2184
2278
  const enclosingFunctionChecksImpureCalls = currentFunctionChecksImpureCalls;
2185
2279
  const enclosingFunctionChecksRenderReads = currentFunctionChecksRenderReads;
@@ -2214,9 +2308,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2214
2308
  const executionPhase = visitStatements(body.body, functionScope, phase);
2215
2309
  if (body.render) visit(body.render, functionScope, executionPhase);
2216
2310
  } else {
2311
+ // An expression body returns its value, so it can supply the cleanup
2312
+ // exactly like a block body's return statement.
2313
+ if (currentEffect !== null && effectCallbackIsCurrent(currentEffect.callback)) {
2314
+ markEffectCleanup(body, functionScope);
2315
+ }
2217
2316
  visit(body, functionScope, phase);
2218
2317
  }
2219
2318
  } finally {
2319
+ currentFunction = enclosingFunction;
2320
+ renderOwner = enclosingOwner;
2321
+ fetchContinuation = enclosingFetchContinuation;
2220
2322
  currentFunctionIsAsync = enclosingFunctionIsAsync;
2221
2323
  currentFunctionChecksImpureCalls = enclosingFunctionChecksImpureCalls;
2222
2324
  currentFunctionChecksRenderReads = enclosingFunctionChecksRenderReads;
@@ -2331,6 +2433,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2331
2433
  bindAmbientPattern(declaration.id, ambient, scope, bind);
2332
2434
  return;
2333
2435
  }
2436
+ if (declarationKind === 'const' || !isReassigned(declaration.id)) {
2437
+ if (fetchFunction(initial, scope)) {
2438
+ bind(declaration.id, FETCH_BINDING);
2439
+ return;
2440
+ }
2441
+ const request = currentEffect === null ? null : fetchRequest(initial, scope);
2442
+ if (request !== null) {
2443
+ bind(declaration.id, { kind: 'fetch-request', request });
2444
+ return;
2445
+ }
2446
+ }
2334
2447
  const stateTuple = stateTupleBinding(initial, scope);
2335
2448
  const snapshot =
2336
2449
  stateTuple === null && declarationKind === 'const' ? snapshotBinding(initial, scope) : null;
@@ -2338,7 +2451,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2338
2451
  bindSnapshotPattern(declaration.id.elements?.[0], stateTuple.snapshot, bind);
2339
2452
  const element = declaration.id.elements?.[1];
2340
2453
  const setter = element?.type === 'AssignmentPattern' ? element.left : element;
2341
- bind(setter, { kind: 'setter' });
2454
+ bind(setter, stateTuple.setter);
2342
2455
  bindStateGetterPattern(declaration.id, stateTuple, scope, bind);
2343
2456
  } else if (declaration.id?.type === 'ObjectPattern' && stateTuple) {
2344
2457
  for (const property of declaration.id.properties ?? []) {
@@ -2351,7 +2464,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2351
2464
  const value = property.value;
2352
2465
  const setter = value?.type === 'AssignmentPattern' ? value.left : value;
2353
2466
  if ((key === 1 || key === '1') && setter?.type === 'Identifier') {
2354
- bind(setter, { kind: 'setter' });
2467
+ bind(setter, stateTuple.setter);
2355
2468
  } else if (key === 0 || key === '0') {
2356
2469
  bindSnapshotPattern(value, stateTuple.snapshot, bind);
2357
2470
  }
@@ -2366,7 +2479,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2366
2479
  ) {
2367
2480
  bind(declaration.id, { kind: 'ref' });
2368
2481
  } else if (declarationKind === 'const' && stateTupleUpdater(initial, scope)) {
2369
- target.bindings.set(declaration.id.name, { kind: 'setter' });
2482
+ target.bindings.set(declaration.id.name, stateTupleUpdater(initial, scope));
2370
2483
  } else if (declarationKind === 'const' && stateTupleGetter(initial, scope)) {
2371
2484
  target.bindings.set(declaration.id.name, STATE_GETTER_BINDING);
2372
2485
  } else if (snapshot !== null) {
@@ -2433,6 +2546,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2433
2546
  } else if (snapshot !== null) {
2434
2547
  bindSnapshotPattern(declaration.id, snapshot, bind);
2435
2548
  }
2549
+ // Proven callable, tuple and option bindings must keep their original
2550
+ // semantics. Projection provenance fills only otherwise unknown values.
2551
+ if (
2552
+ stateTuple === null &&
2553
+ snapshot === null &&
2554
+ (declarationKind === 'const' || !isReassigned(declaration.id))
2555
+ ) {
2556
+ const known =
2557
+ declaration.id?.type === 'Identifier' ? target.bindings.get(declaration.id.name) : null;
2558
+ if (known == null || known.kind === 'other') {
2559
+ const prop = propProjection(initial, scope);
2560
+ // A value mixing props and state keeps its state provenance so effect
2561
+ // reads still link to the writer; the prop flag keeps the eager
2562
+ // initializer check.
2563
+ const states =
2564
+ declaration.id?.type === 'Identifier' ? projectedStates(initial, scope) : null;
2565
+ if (states !== null) bind(declaration.id, { kind: 'derived-state', states, prop });
2566
+ else if (prop) bindPropPattern(declaration.id, bind);
2567
+ }
2568
+ }
2436
2569
  }
2437
2570
 
2438
2571
  function stateTupleBinding(expression, scope) {
@@ -2446,9 +2579,21 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2446
2579
  if (!STATE_HOOKS.has(hook)) return null;
2447
2580
  // A known array initializer is a narrow mutator check, not a judgment
2448
2581
  // about arbitrary methods on object snapshots or opaque hook outputs.
2449
- return hook === 'useState' && unwrap(node.arguments?.[0])?.type === 'ArrayExpression'
2450
- ? ARRAY_STATE_TUPLE_BINDING
2451
- : STATE_TUPLE_BINDING;
2582
+ let tuple = stateTuples.get(node);
2583
+ if (tuple === undefined) {
2584
+ tuple = {
2585
+ kind: 'state-tuple',
2586
+ snapshot: {
2587
+ kind: 'snapshot',
2588
+ state: node,
2589
+ array: hook === 'useState' && unwrap(node.arguments?.[0])?.type === 'ArrayExpression',
2590
+ },
2591
+ setter: { kind: 'setter', state: node },
2592
+ getter: STATE_GETTER_BINDING,
2593
+ };
2594
+ stateTuples.set(node, tuple);
2595
+ }
2596
+ return tuple;
2452
2597
  }
2453
2598
 
2454
2599
  function snapshotBinding(expression, scope) {
@@ -2463,7 +2608,12 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2463
2608
  const key = staticPrimitiveValue(node.property, scope);
2464
2609
  if (key === 0 || key === '0') return tuple.snapshot;
2465
2610
  }
2466
- return snapshotBinding(node.object, scope) !== null ? SNAPSHOT_BINDING : null;
2611
+ const parent = snapshotBinding(node.object, scope);
2612
+ return parent === null
2613
+ ? null
2614
+ : parent.state
2615
+ ? { kind: 'snapshot', state: parent.state }
2616
+ : SNAPSHOT_BINDING;
2467
2617
  }
2468
2618
 
2469
2619
  function bindDeclaration(declaration, declarationKind, scope, phase) {
@@ -2491,13 +2641,11 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2491
2641
 
2492
2642
  function stateTupleUpdater(value, scope) {
2493
2643
  const member = unwrap(value);
2494
- if (member?.type !== 'MemberExpression' || member.computed !== true) return false;
2644
+ if (member?.type !== 'MemberExpression' || member.computed !== true) return null;
2495
2645
  const property = staticPrimitiveValue(member.property, scope);
2496
- return (
2497
- (property === 1 || property === '1') &&
2498
- unwrap(member.object)?.type === 'Identifier' &&
2499
- resolve(scope, unwrap(member.object).name)?.kind === 'state-tuple'
2500
- );
2646
+ return property === 1 || property === '1'
2647
+ ? (stateTupleBinding(member.object, scope)?.setter ?? null)
2648
+ : null;
2501
2649
  }
2502
2650
 
2503
2651
  function stateTupleGetter(value, scope) {
@@ -2786,8 +2934,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2786
2934
  if (scopes === undefined) functions.set(callback.node, (scopes = new Set()));
2787
2935
  scopes.add(callback.scope);
2788
2936
  } else {
2789
- const identity =
2790
- callback.kind === 'setter' || callback.kind === 'getter' ? callback.kind : callback;
2937
+ const identity = callback.kind === 'getter' ? callback.kind : callback;
2791
2938
  if (seen.has(identity)) return;
2792
2939
  seen.add(identity);
2793
2940
  }
@@ -2850,7 +2997,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2850
2997
  const binding = resolve(scope, node.name);
2851
2998
  return isCallableValue(binding) ? binding : null;
2852
2999
  }
2853
- if (stateTupleUpdater(node, scope)) return { kind: 'setter' };
3000
+ const setter = stateTupleUpdater(node, scope);
3001
+ if (setter !== null) return setter;
2854
3002
  if (stateTupleGetter(node, scope)) return STATE_GETTER_BINDING;
2855
3003
  if (node?.type === 'SequenceExpression') {
2856
3004
  return callableValue(node.expressions?.[node.expressions.length - 1], scope);
@@ -2962,6 +3110,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2962
3110
  isCallableValue(binding) ||
2963
3111
  binding?.kind === 'ref' ||
2964
3112
  binding?.kind === 'snapshot' ||
3113
+ binding?.kind === 'prop' ||
3114
+ binding?.kind === 'derived-state' ||
2965
3115
  binding?.kind === 'state-tuple' ||
2966
3116
  binding?.kind === 'constant' ||
2967
3117
  binding?.kind === 'linked-key'
@@ -2982,7 +3132,14 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
2982
3132
 
2983
3133
  function argumentValues(args, scope) {
2984
3134
  if (args?.some((argument) => argument.type === 'SpreadElement')) return null;
2985
- return (args ?? []).map((argument) => expressionBinding(argument, scope));
3135
+ return (args ?? []).map((argument) => {
3136
+ const value = expressionBinding(argument, scope);
3137
+ // Prop-state intent belongs to the component's own initializer. A
3138
+ // helper's parameter is supplied by callers, independently of spelling.
3139
+ if (value?.kind === 'prop') return OTHER_BINDING;
3140
+ if (value?.kind === 'derived-state' && value.prop) return { ...value, prop: false };
3141
+ return value;
3142
+ });
2986
3143
  }
2987
3144
 
2988
3145
  function mergePrimitive(left, right) {
@@ -3129,6 +3286,316 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3129
3286
  return result;
3130
3287
  }
3131
3288
 
3289
+ function bindPropPattern(pattern, bind) {
3290
+ if (pattern?.type === 'Identifier') bind(pattern, PROP_BINDING);
3291
+ else if (pattern?.type === 'AssignmentPattern') bindPropPattern(pattern.left, bind);
3292
+ else if (pattern?.type === 'RestElement') bindPropPattern(pattern.argument, bind);
3293
+ else if (pattern?.type === 'ObjectPattern') {
3294
+ for (const property of pattern.properties ?? [])
3295
+ bindPropPattern(property.value ?? property.argument, bind);
3296
+ } else if (pattern?.type === 'ArrayPattern') {
3297
+ for (const element of pattern.elements ?? []) bindPropPattern(element, bind);
3298
+ }
3299
+ }
3300
+
3301
+ function propProjection(expression, scope) {
3302
+ const node = unwrap(expression);
3303
+ if (node == null || FUNCTION_TYPES.has(node.type)) return false;
3304
+ if (node.type === 'Identifier') {
3305
+ const binding = resolve(scope, node.name);
3306
+ return binding?.kind === 'prop' || binding?.prop === true;
3307
+ }
3308
+ if (node.type === 'MemberExpression')
3309
+ return (
3310
+ propProjection(node.object, scope) ||
3311
+ (node.computed && propProjection(node.property, scope))
3312
+ );
3313
+ if (node.type === 'CallExpression' || node.type === 'NewExpression')
3314
+ return (
3315
+ (unwrap(node.callee)?.type === 'MemberExpression' &&
3316
+ propProjection(unwrap(node.callee).object, scope)) ||
3317
+ (node.arguments ?? []).some((argument) => propProjection(argument, scope))
3318
+ );
3319
+ if (node.type === 'Property')
3320
+ return (
3321
+ propProjection(node.value, scope) || (node.computed && propProjection(node.key, scope))
3322
+ );
3323
+ if (node.type === 'ObjectExpression')
3324
+ return (node.properties ?? []).some((property) => propProjection(property, scope));
3325
+ if (node.type === 'ArrayExpression')
3326
+ return (node.elements ?? []).some((element) => propProjection(element, scope));
3327
+ if (node.type === 'TemplateLiteral')
3328
+ return (node.expressions ?? []).some((value) => propProjection(value, scope));
3329
+ if (node.type === 'SequenceExpression') return propProjection(node.expressions?.at(-1), scope);
3330
+ if (node.type === 'ConditionalExpression')
3331
+ return (
3332
+ propProjection(node.test, scope) ||
3333
+ propProjection(node.consequent, scope) ||
3334
+ propProjection(node.alternate, scope)
3335
+ );
3336
+ if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression')
3337
+ return propProjection(node.left, scope) || propProjection(node.right, scope);
3338
+ if (node.type === 'UnaryExpression' || node.type === 'SpreadElement')
3339
+ return propProjection(node.argument, scope);
3340
+ return false;
3341
+ }
3342
+
3343
+ function projectedStates(expression, scope, states = null) {
3344
+ const node = unwrap(expression);
3345
+ if (node == null || typeof node !== 'object' || FUNCTION_TYPES.has(node.type)) return states;
3346
+ if (node.type === 'Identifier') {
3347
+ const binding = resolve(scope, node.name);
3348
+ if (binding?.state && binding.kind === 'snapshot') (states ??= new Set()).add(binding.state);
3349
+ if (binding?.kind === 'derived-state') {
3350
+ for (const state of binding.states) (states ??= new Set()).add(state);
3351
+ }
3352
+ return states;
3353
+ }
3354
+ if (node.type === 'MemberExpression') {
3355
+ const snapshot = snapshotBinding(node, scope);
3356
+ if (snapshot?.state) (states ??= new Set()).add(snapshot.state);
3357
+ states = projectedStates(node.object, scope, states);
3358
+ return node.computed ? projectedStates(node.property, scope, states) : states;
3359
+ }
3360
+ if (node.type === 'Property') {
3361
+ states = projectedStates(node.value, scope, states);
3362
+ return node.computed ? projectedStates(node.key, scope, states) : states;
3363
+ }
3364
+ if (node.type === 'CallExpression' || node.type === 'NewExpression') {
3365
+ if (unwrap(node.callee)?.type === 'MemberExpression')
3366
+ states = projectedStates(unwrap(node.callee).object, scope, states);
3367
+ for (const argument of node.arguments ?? [])
3368
+ states = projectedStates(argument, scope, states);
3369
+ return states;
3370
+ }
3371
+ if (Array.isArray(node)) {
3372
+ for (const child of node) states = projectedStates(child, scope, states);
3373
+ } else if (!node.type?.startsWith('TS')) {
3374
+ for (const key in node) {
3375
+ if (!SKIP_KEYS.has(key) && !key.startsWith('_octane'))
3376
+ states = projectedStates(node[key], scope, states);
3377
+ }
3378
+ }
3379
+ return states;
3380
+ }
3381
+
3382
+ function fetchFunction(expression, scope) {
3383
+ const node = unwrap(expression);
3384
+ if (node?.type === 'Identifier') {
3385
+ const value = resolve(scope, node.name);
3386
+ return value === FETCH_BINDING || (node.name === 'fetch' && value === null);
3387
+ }
3388
+ if (node?.type !== 'MemberExpression') return false;
3389
+ const object = ambientReference(node.object, scope);
3390
+ return (
3391
+ (object === GLOBAL_OBJECT_BINDING || object === AMBIENT_GLOBAL_BINDINGS.get('window')) &&
3392
+ ambientPropertyKey(node, scope) === 'fetch'
3393
+ );
3394
+ }
3395
+
3396
+ function fetchRequest(expression, scope) {
3397
+ const node = unwrap(expression);
3398
+ if (node?.type === 'Identifier') return resolve(scope, node.name)?.request ?? null;
3399
+ if (node?.type !== 'CallExpression') return null;
3400
+ if (fetchFunction(node.callee, scope)) return node;
3401
+ const callee = unwrap(node.callee);
3402
+ if (callee?.type !== 'MemberExpression') return null;
3403
+ const method = ambientPropertyKey(callee, scope);
3404
+ return method === 'then' || method === 'catch' || method === 'finally'
3405
+ ? fetchRequest(callee.object, scope)
3406
+ : null;
3407
+ }
3408
+
3409
+ function visitEffect(node, scope) {
3410
+ const enclosingEffect = currentEffect;
3411
+ const enclosingContinuation = fetchContinuation;
3412
+ const enclosingOwnsWrites = effectOwnsWrites;
3413
+ const callback = callableValue(node.arguments?.[0], scope);
3414
+ const record = {
3415
+ node,
3416
+ owner: renderOwner,
3417
+ callback,
3418
+ reads: new Set(),
3419
+ writes: new Set(),
3420
+ started: new Set(),
3421
+ cleanup: false,
3422
+ fetchUpdater: null,
3423
+ };
3424
+ currentEffect = record;
3425
+ fetchContinuation = false;
3426
+ effectOwnsWrites = true;
3427
+ effectRecords.push(record);
3428
+ try {
3429
+ visitExplicitStateDependencies(node.arguments?.[1], scope, record.reads);
3430
+ visitCallable(callback, unwrap(node.arguments?.[0]), 'effect');
3431
+ } finally {
3432
+ currentEffect = enclosingEffect;
3433
+ fetchContinuation = enclosingContinuation;
3434
+ effectOwnsWrites = enclosingOwnsWrites;
3435
+ }
3436
+ if (record.fetchUpdater !== null && !record.cleanup) {
3437
+ report(
3438
+ STRONG_EFFECT_DATA_FETCH,
3439
+ record.fetchUpdater,
3440
+ 'Strong mode does not allow fetching data into state from an effect without cleanup. Read data with use() or a query binding; keep effects for external synchronization with cleanup.',
3441
+ );
3442
+ }
3443
+ }
3444
+
3445
+ function visitExplicitStateDependencies(expression, scope, dependencies) {
3446
+ const node = unwrap(expression);
3447
+ if (node?.type === 'ArrayExpression') {
3448
+ for (const element of node.elements ?? [])
3449
+ visitExplicitStateDependencies(element, scope, dependencies);
3450
+ } else {
3451
+ const states = projectedStates(node, scope);
3452
+ if (states !== null) for (const state of states) dependencies.add(state);
3453
+ }
3454
+ }
3455
+
3456
+ // Keep abrupt completions distinct until their owning statement consumes them.
3457
+ // A break exits a switch/loop, while a return exits the surrounding callback.
3458
+ function statementCompletions(statement, loopLabel = null) {
3459
+ if (statement == null) return NORMAL_COMPLETIONS;
3460
+ const known = loopLabel === null ? effectBranchExits.get(statement) : undefined;
3461
+ if (known !== undefined) return known;
3462
+ let completions;
3463
+ switch (statement.type) {
3464
+ case 'ReturnStatement':
3465
+ completions = new Set(['return']);
3466
+ break;
3467
+ case 'ThrowStatement':
3468
+ completions = new Set(['throw']);
3469
+ break;
3470
+ case 'BreakStatement':
3471
+ case 'ContinueStatement':
3472
+ completions = new Set([
3473
+ `${statement.type === 'BreakStatement' ? 'break' : 'continue'}:${statement.label?.name ?? ''}`,
3474
+ ]);
3475
+ break;
3476
+ case 'BlockStatement':
3477
+ completions = statementsCompletions(statement.body);
3478
+ break;
3479
+ case 'IfStatement':
3480
+ completions = new Set([
3481
+ ...statementCompletions(statement.consequent),
3482
+ ...statementCompletions(statement.alternate),
3483
+ ]);
3484
+ break;
3485
+ case 'SwitchStatement': {
3486
+ completions = new Set();
3487
+ let suffix = new Set(['normal']);
3488
+ let hasDefault = false;
3489
+ for (let index = statement.cases.length - 1; index >= 0; index--) {
3490
+ const branch = statement.cases[index];
3491
+ hasDefault ||= branch.test === null;
3492
+ const branchCompletions = statementsCompletions(branch.consequent);
3493
+ if (branchCompletions.delete('normal'))
3494
+ for (const completion of suffix) branchCompletions.add(completion);
3495
+ suffix = branchCompletions;
3496
+ for (const completion of suffix) completions.add(completion);
3497
+ }
3498
+ const breaks = completions.delete('break:');
3499
+ if (!hasDefault || breaks) completions.add('normal');
3500
+ break;
3501
+ }
3502
+ case 'ForStatement':
3503
+ case 'ForInStatement':
3504
+ case 'ForOfStatement':
3505
+ case 'WhileStatement':
3506
+ case 'DoWhileStatement': {
3507
+ const test = unwrap(statement.test);
3508
+ const indefinite =
3509
+ (statement.type === 'ForStatement' && test == null) ||
3510
+ (test?.type === 'Literal' && test.value === true);
3511
+ const neverStarts =
3512
+ statement.type !== 'DoWhileStatement' && test?.type === 'Literal' && test.value === false;
3513
+ completions = neverStarts
3514
+ ? new Set(['normal'])
3515
+ : new Set(statementCompletions(statement.body));
3516
+ const repeats = completions.delete('normal');
3517
+ let continues = completions.delete('continue:');
3518
+ if (loopLabel !== null)
3519
+ continues = completions.delete(`continue:${loopLabel}`) || continues;
3520
+ const breaks = completions.delete('break:');
3521
+ if (
3522
+ breaks ||
3523
+ (!indefinite && (statement.type !== 'DoWhileStatement' || repeats || continues))
3524
+ )
3525
+ completions.add('normal');
3526
+ break;
3527
+ }
3528
+ case 'LabeledStatement':
3529
+ completions = new Set(statementCompletions(statement.body, statement.label.name));
3530
+ if (completions.delete(`break:${statement.label.name}`)) completions.add('normal');
3531
+ break;
3532
+ case 'TryStatement': {
3533
+ completions = new Set(statementCompletions(statement.block));
3534
+ if (statement.handler != null) {
3535
+ completions.delete('throw');
3536
+ for (const completion of statementCompletions(statement.handler.body))
3537
+ completions.add(completion);
3538
+ }
3539
+ if (statement.finalizer != null) {
3540
+ const finalizer = statementCompletions(statement.finalizer);
3541
+ if (!finalizer.has('normal')) completions.clear();
3542
+ for (const completion of finalizer)
3543
+ if (completion !== 'normal') completions.add(completion);
3544
+ }
3545
+ break;
3546
+ }
3547
+ default:
3548
+ return NORMAL_COMPLETIONS;
3549
+ }
3550
+ if (loopLabel === null) effectBranchExits.set(statement, completions);
3551
+ return completions;
3552
+ }
3553
+
3554
+ function statementsCompletions(statements) {
3555
+ const completions = new Set(['normal']);
3556
+ for (const statement of statements ?? []) {
3557
+ if (!completions.delete('normal')) break;
3558
+ for (const completion of statementCompletions(statement)) completions.add(completion);
3559
+ }
3560
+ return completions;
3561
+ }
3562
+
3563
+ function branchAlwaysExits(statement) {
3564
+ return !statementCompletions(statement).has('normal');
3565
+ }
3566
+
3567
+ // A returned value counts as cleanup unless it is provably not callable: a
3568
+ // fetch request or a known non-function value.
3569
+ function markEffectCleanup(argument, scope) {
3570
+ // Async callbacks return a Promise, even when their resolved value is a function.
3571
+ if (currentFunctionIsAsync) return;
3572
+ const value = unwrap(argument);
3573
+ if (value?.type === 'CallExpression') {
3574
+ const callback = callableValue(value.callee, scope);
3575
+ const callbacks =
3576
+ callback?.kind === 'callback-choice' && callback.complete ? callback.values : [callback];
3577
+ if (
3578
+ callbacks.length > 0 &&
3579
+ callbacks.every((value) => value?.kind === 'callback' && value.node.async)
3580
+ )
3581
+ return;
3582
+ }
3583
+ if (
3584
+ value != null &&
3585
+ (callableValue(value, scope) !== null ||
3586
+ (staticExpressionValue(value, scope) === UNKNOWN_VALUE &&
3587
+ fetchRequest(value, scope) === null))
3588
+ ) {
3589
+ currentEffect.cleanup = true;
3590
+ }
3591
+ }
3592
+
3593
+ function effectCallbackIsCurrent(value) {
3594
+ return value?.kind === 'callback'
3595
+ ? value.node === currentFunction
3596
+ : value?.kind === 'callback-choice' && value.values.some(effectCallbackIsCurrent);
3597
+ }
3598
+
3132
3599
  function visitCallable(value, origin, phase, args = null) {
3133
3600
  if (value?.kind === 'callback') {
3134
3601
  visitCallback(value.node, value.scope, phase, args);
@@ -3136,9 +3603,23 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3136
3603
  for (const callback of value.values) visitCallable(callback, origin, phase, args);
3137
3604
  } else if (value?.kind === 'effect-event') {
3138
3605
  if (phase === 'render') reportEffectEventCall(origin);
3139
- else if (phase === 'effect') visitCallable(value.callback, origin, phase, args);
3140
- } else if (value?.kind === 'setter' && (phase === 'render' || phase === 'effect')) {
3141
- reportSetter(origin, phase);
3606
+ else if (phase === 'effect' || currentEffect !== null) {
3607
+ const enclosingCollectEffectReads = collectEffectReads;
3608
+ // Effect Events observe current values without subscribing the calling effect.
3609
+ // Their writes still participate in fetch and cross-effect state flow checks.
3610
+ collectEffectReads = false;
3611
+ try {
3612
+ visitCallable(value.callback, origin, phase, args);
3613
+ } finally {
3614
+ collectEffectReads = enclosingCollectEffectReads;
3615
+ }
3616
+ }
3617
+ } else if (value?.kind === 'setter') {
3618
+ if (currentEffect !== null && effectOwnsWrites && value.state) {
3619
+ currentEffect.writes.add(value.state);
3620
+ if (fetchContinuation) currentEffect.fetchUpdater ??= origin;
3621
+ }
3622
+ if (phase === 'render' || phase === 'effect') reportSetter(origin, phase);
3142
3623
  } else if (value?.kind === 'getter' && phase === 'render' && currentFunctionChecksRenderReads) {
3143
3624
  reportStateGetterCall(origin);
3144
3625
  }
@@ -3324,18 +3805,75 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3324
3805
  return;
3325
3806
  }
3326
3807
  if (node.type?.startsWith('TS')) return;
3808
+ if (
3809
+ currentEffect !== null &&
3810
+ EFFECT_CONTROL_TYPES.has(node.type) &&
3811
+ currentEffectControl?.node !== node
3812
+ ) {
3813
+ const parent = currentEffectControl;
3814
+ const control = {
3815
+ node,
3816
+ parent,
3817
+ loop: node.type !== 'SwitchStatement' && node.type !== 'LabeledStatement',
3818
+ label:
3819
+ node.type === 'LabeledStatement'
3820
+ ? node.label.name
3821
+ : parent?.node.type === 'LabeledStatement' && parent.node.body === node
3822
+ ? parent.label
3823
+ : null,
3824
+ breakContinuation: false,
3825
+ continueContinuation: false,
3826
+ };
3827
+ const before = fetchContinuation;
3828
+ currentEffectControl = control;
3829
+ try {
3830
+ visit(node, scope, phase, readAccess);
3831
+ if (fetchContinuation && control.loop && branchAlwaysExits(node.body))
3832
+ fetchContinuation = before;
3833
+ // Break and continue leave the current branch, but their asynchronous
3834
+ // path can still reach an update after the target loop or switch.
3835
+ fetchContinuation ||= control.breakContinuation || control.continueContinuation;
3836
+ } finally {
3837
+ currentEffectControl = parent;
3838
+ }
3839
+ return;
3840
+ }
3841
+ templatePolicy.visit(node, scope, readAccess);
3327
3842
 
3328
3843
  switch (node.type) {
3329
3844
  case 'ImportDeclaration':
3330
3845
  case 'MetaProperty':
3331
- case 'BreakStatement':
3332
- case 'ContinueStatement':
3333
3846
  case 'JSXIdentifier':
3334
3847
  case 'Literal':
3335
3848
  case 'ThisExpression':
3336
3849
  case 'Super':
3337
3850
  return;
3851
+ case 'BreakStatement':
3852
+ case 'ContinueStatement': {
3853
+ if (currentEffect !== null && fetchContinuation) {
3854
+ const continuation = node.type === 'ContinueStatement';
3855
+ for (let control = currentEffectControl; control !== null; control = control.parent) {
3856
+ if (
3857
+ node.label
3858
+ ? control.label !== node.label.name || (continuation && !control.loop)
3859
+ : continuation
3860
+ ? !control.loop
3861
+ : control.node.type === 'LabeledStatement'
3862
+ )
3863
+ continue;
3864
+ control[continuation ? 'continueContinuation' : 'breakContinuation'] = true;
3865
+ break;
3866
+ }
3867
+ }
3868
+ return;
3869
+ }
3338
3870
  case 'Identifier':
3871
+ if (readAccess && collectEffectReads && currentEffect !== null) {
3872
+ const binding = resolve(scope, node.name);
3873
+ if (binding?.kind === 'snapshot' && binding.state) currentEffect.reads.add(binding.state);
3874
+ if (binding?.kind === 'derived-state')
3875
+ for (const state of binding.states) currentEffect.reads.add(state);
3876
+ }
3339
3877
  if (
3340
3878
  readAccess &&
3341
3879
  phase === 'render' &&
@@ -3405,9 +3943,33 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3405
3943
  return;
3406
3944
  case 'FunctionDeclaration':
3407
3945
  case 'FunctionExpression':
3408
- case 'ArrowFunctionExpression':
3409
- visitFunction(node, scope, phase === 'module' ? 'render' : 'deferred');
3946
+ case 'ArrowFunctionExpression': {
3947
+ // A declaration does not execute a callback. Calls and callbacks
3948
+ // passed to external work are visited separately with the effect.
3949
+ const enclosingEffect = currentEffect;
3950
+ currentEffect = null;
3951
+ try {
3952
+ visitFunction(node, scope, phase === 'module' ? 'render' : 'deferred');
3953
+ } finally {
3954
+ currentEffect = enclosingEffect;
3955
+ }
3410
3956
  return;
3957
+ }
3958
+ case 'AwaitExpression': {
3959
+ visit(node.argument, scope, phase);
3960
+ if (currentEffect !== null) {
3961
+ const request = fetchRequest(node.argument, scope);
3962
+ if (request !== null && currentEffect.started.has(request)) fetchContinuation = true;
3963
+ }
3964
+ return;
3965
+ }
3966
+ case 'ReturnStatement': {
3967
+ if (currentEffect !== null && effectCallbackIsCurrent(currentEffect.callback)) {
3968
+ markEffectCleanup(node.argument, scope);
3969
+ }
3970
+ visit(node.argument, scope, phase);
3971
+ return;
3972
+ }
3411
3973
  case 'ClassDeclaration':
3412
3974
  case 'ClassExpression': {
3413
3975
  const classScope = createScope(scope, 'block', [], node.id ? [node.id] : []);
@@ -3435,20 +3997,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3435
3997
  if (node.render) visit(node.render, block, executionPhase);
3436
3998
  return;
3437
3999
  }
3438
- case 'IfStatement': {
3439
- visit(node.test, scope, phase);
3440
- const branchPhase = phaseAfter(node.test, phase);
3441
- const branches = conditionalExpressionBranches(node, scope);
3442
- if ((branches & 1) !== 0) visit(node.consequent, scope, branchPhase);
3443
- if ((branches & 2) !== 0) visit(node.alternate, scope, branchPhase);
3444
- return;
3445
- }
4000
+ case 'IfStatement':
3446
4001
  case 'ConditionalExpression': {
3447
4002
  visit(node.test, scope, phase);
3448
4003
  const branchPhase = phaseAfter(node.test, phase);
3449
4004
  const branches = conditionalExpressionBranches(node, scope);
4005
+ const before = fetchContinuation;
3450
4006
  if ((branches & 1) !== 0) visit(node.consequent, scope, branchPhase);
4007
+ const consequent =
4008
+ (branches & 1) !== 0 &&
4009
+ fetchContinuation &&
4010
+ (node.type !== 'IfStatement' || !branchAlwaysExits(node.consequent));
4011
+ fetchContinuation = before;
3451
4012
  if ((branches & 2) !== 0) visit(node.alternate, scope, branchPhase);
4013
+ // An abrupt branch cannot reach a later state update. Inspect all writes
4014
+ // inside it (including finally) before excluding its continuation.
4015
+ fetchContinuation =
4016
+ consequent ||
4017
+ ((branches & 2) !== 0 &&
4018
+ fetchContinuation &&
4019
+ (node.type !== 'IfStatement' || !branchAlwaysExits(node.alternate)));
3452
4020
  return;
3453
4021
  }
3454
4022
  case 'LogicalExpression':
@@ -3545,11 +4113,41 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3545
4113
  reportAmbientRead(node);
3546
4114
  }
3547
4115
  }
4116
+ if (readAccess && collectEffectReads && currentEffect !== null) {
4117
+ const snapshot = snapshotBinding(node, scope);
4118
+ if (snapshot?.state) currentEffect.reads.add(snapshot.state);
4119
+ }
3548
4120
  return;
3549
4121
  }
3550
4122
  case 'LabeledStatement':
3551
4123
  visit(node.body, scope, phase);
3552
4124
  return;
4125
+ case 'TryStatement': {
4126
+ let pendingControls = null;
4127
+ if (
4128
+ currentEffect !== null &&
4129
+ currentEffectControl !== null &&
4130
+ branchAlwaysExits(node.finalizer)
4131
+ ) {
4132
+ pendingControls = [];
4133
+ for (let control = currentEffectControl; control !== null; control = control.parent)
4134
+ pendingControls.push([
4135
+ control,
4136
+ control.breakContinuation,
4137
+ control.continueContinuation,
4138
+ ]);
4139
+ }
4140
+ visit(node.block, scope, phase);
4141
+ visit(node.handler, scope, phase);
4142
+ // An abrupt finally replaces pending exits from try/catch. Restore the
4143
+ // earlier paths before recording any new exits from finally itself.
4144
+ for (const [control, breaks, continues] of pendingControls ?? []) {
4145
+ control.breakContinuation = breaks;
4146
+ control.continueContinuation = continues;
4147
+ }
4148
+ visit(node.finalizer, scope, phase);
4149
+ return;
4150
+ }
3553
4151
  case 'JSXSwitchExpression': {
3554
4152
  visit(node.discriminant, scope, phase);
3555
4153
  let searchPhase = phaseAfter(node.discriminant, phase);
@@ -3583,7 +4181,10 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3583
4181
  const fallbackPhase = searchPhase;
3584
4182
  searchPhase = initialPhase;
3585
4183
  let fallthroughPhase = null;
4184
+ const matchingContinuation = fetchContinuation;
4185
+ let fallthroughContinuation = false;
3586
4186
  for (const branch of branches) {
4187
+ fetchContinuation = matchingContinuation || fallthroughContinuation;
3587
4188
  if (currentFunctionIsAsync && searchPhase !== 'deferred' && alwaysAwaits(branch.test)) {
3588
4189
  searchPhase = 'deferred';
3589
4190
  }
@@ -3594,6 +4195,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3594
4195
  ? 'deferred'
3595
4196
  : phase;
3596
4197
  const executionPhase = visitStatements(branch.consequent, switchScope, branchPhase);
4198
+ fallthroughContinuation = fetchContinuation && !branch.consequent.some(branchAlwaysExits);
3597
4199
  const last = branch.consequent?.[branch.consequent.length - 1];
3598
4200
  fallthroughPhase =
3599
4201
  last?.type === 'BreakStatement' ||
@@ -3603,6 +4205,7 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3603
4205
  ? null
3604
4206
  : executionPhase;
3605
4207
  }
4208
+ fetchContinuation = matchingContinuation || fallthroughContinuation;
3606
4209
  return;
3607
4210
  }
3608
4211
  case 'VariableDeclaration': {
@@ -3665,10 +4268,26 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3665
4268
  visitExplicitDependencies(node.arguments?.[dependencyIndex], scope);
3666
4269
  }
3667
4270
  if (EFFECT_HOOKS.has(hook)) {
3668
- visitSynchronousHookCallback(node.arguments?.[0], scope, 'effect');
4271
+ visitEffect(node, scope);
3669
4272
  return;
3670
4273
  }
4274
+ if (
4275
+ hook === 'useReducer' &&
4276
+ executionPhase === 'render' &&
4277
+ (node.arguments?.[2] == null ||
4278
+ staticExpressionValue(node.arguments[2], scope) === UNDEFINED_VALUE) &&
4279
+ propProjection(node.arguments?.[1], scope)
4280
+ )
4281
+ unlinkedPropInitializers.add(unwrap(node.arguments[1]));
3671
4282
  if (hook === 'useState' || hook === 'useMemo') {
4283
+ if (
4284
+ hook === 'useState' &&
4285
+ executionPhase === 'render' &&
4286
+ propProjection(node.arguments?.[0], scope) &&
4287
+ callableValue(node.arguments?.[0], scope) === null
4288
+ ) {
4289
+ unlinkedPropInitializers.add(unwrap(node.arguments[0]));
4290
+ }
3672
4291
  visitSynchronousHookCallback(
3673
4292
  node.arguments?.[0],
3674
4293
  scope,
@@ -3717,9 +4336,31 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3717
4336
  }
3718
4337
  }
3719
4338
  }
4339
+ if (currentEffect !== null && hook === null) {
4340
+ if (effectOwnsWrites && fetchFunction(callee, scope)) currentEffect.started.add(node);
4341
+ const callback = callableValue(callee, scope);
4342
+ if (callback === null) {
4343
+ const promiseContinuation =
4344
+ callee?.type === 'MemberExpression' &&
4345
+ ['then', 'catch', 'finally'].includes(ambientPropertyKey(callee, scope));
4346
+ const request = promiseContinuation ? fetchRequest(callee.object, scope) : null;
4347
+ const before = fetchContinuation;
4348
+ const beforeOwnsWrites = effectOwnsWrites;
4349
+ effectOwnsWrites &&= promiseContinuation;
4350
+ if (request !== null && currentEffect.started.has(request)) fetchContinuation = true;
4351
+ try {
4352
+ for (const argument of node.arguments ?? [])
4353
+ visitCallable(callableValue(argument, scope), unwrap(argument), 'deferred');
4354
+ } finally {
4355
+ fetchContinuation = before;
4356
+ effectOwnsWrites = beforeOwnsWrites;
4357
+ }
4358
+ }
4359
+ }
3720
4360
  if (
3721
4361
  executionPhase === 'render' ||
3722
4362
  executionPhase === 'effect' ||
4363
+ currentEffect !== null ||
3723
4364
  FUNCTION_TYPES.has(callee?.type)
3724
4365
  ) {
3725
4366
  const callback = callableValue(callee, scope);
@@ -3917,6 +4558,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3917
4558
  executionPhase = 'deferred';
3918
4559
  }
3919
4560
  visit(node.body, loop, executionPhase);
4561
+ if (currentEffectControl?.node === node)
4562
+ fetchContinuation ||= currentEffectControl.continueContinuation;
3920
4563
  visit(node.update, loop, executionPhase);
3921
4564
  return;
3922
4565
  }
@@ -3934,12 +4577,17 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3934
4577
  addPatternNames(declaration.id, row.bindings, OTHER_BINDING);
3935
4578
  }
3936
4579
  }
3937
- addPatternNames(node.index, row.bindings, OTHER_BINDING);
4580
+ addPatternNames(node.index, row.bindings, { kind: 'other', strongIndex: true });
3938
4581
  visit(node.left, row, executionPhase);
3939
4582
  const enclosingRetainedRowScope = currentRetainedRowScope;
3940
4583
  currentRetainedRowScope = row;
3941
4584
  try {
3942
- visit(node.key, row, executionPhase);
4585
+ templatePolicy.enterKey(node.key, row);
4586
+ try {
4587
+ visit(node.key, row, executionPhase);
4588
+ } finally {
4589
+ templatePolicy.exitKey();
4590
+ }
3943
4591
  visit(node.body, row, executionPhase);
3944
4592
  } finally {
3945
4593
  currentRetainedRowScope = enclosingRetainedRowScope;
@@ -3966,6 +4614,9 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3966
4614
  : phase;
3967
4615
  if (node.left?.type === 'ArrayPattern' || node.left?.type === 'ObjectPattern') {
3968
4616
  visitPatternExpressions(node.left, loop, executionPhase);
4617
+ } else if (node.left?.type === 'VariableDeclaration') {
4618
+ for (const declaration of node.left.declarations)
4619
+ visitPatternExpressions(declaration.id, loop, executionPhase);
3969
4620
  } else {
3970
4621
  visit(node.left, loop, executionPhase, false);
3971
4622
  }
@@ -3983,6 +4634,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
3983
4634
  }
3984
4635
  case 'DoWhileStatement': {
3985
4636
  visit(node.body, scope, phase);
4637
+ if (currentEffectControl?.node === node)
4638
+ fetchContinuation ||= currentEffectControl.continueContinuation;
3986
4639
  const executionPhase =
3987
4640
  currentFunctionIsAsync &&
3988
4641
  phase !== 'deferred' &&
@@ -4001,17 +4654,62 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
4001
4654
  }
4002
4655
 
4003
4656
  for (const statement of ast.body ?? []) visit(statement, moduleScope, 'module');
4657
+ for (const initial of unlinkedPropInitializers) {
4658
+ report(
4659
+ STRONG_UNLINKED_PROP_STATE,
4660
+ initial,
4661
+ 'Strong mode requires explicit intent for prop-derived state. Use useLinkedState(value, () => value) to follow the prop, or a lazy useState initializer (or useReducer third initializer) to capture only its initial value.',
4662
+ );
4663
+ }
4664
+ // Match state identity, not its local spelling. The graph is local to one
4665
+ // analyzed module and compares only distinct effects in the same render root.
4666
+ const effectWriters = new Map();
4667
+ for (const effect of effectRecords) {
4668
+ for (const state of effect.writes) {
4669
+ let writers = effectWriters.get(state);
4670
+ if (writers === undefined) effectWriters.set(state, (writers = []));
4671
+ writers.push(effect);
4672
+ }
4673
+ }
4674
+ for (const effect of effectRecords) {
4675
+ if (
4676
+ [...effect.reads].some((state) =>
4677
+ effectWriters
4678
+ .get(state)
4679
+ ?.some((writer) => writer.node !== effect.node && writer.owner === effect.owner),
4680
+ )
4681
+ ) {
4682
+ report(
4683
+ STRONG_EFFECT_CHAIN,
4684
+ effect.node,
4685
+ 'Strong mode does not allow an effect to depend on state written by another effect in the same component. Derive the value during render, use useLinkedState, or keep the external synchronization in one effect.',
4686
+ );
4687
+ }
4688
+ }
4004
4689
  if (hasNestedCodeBlock) {
4005
- diagnostics.push(...strongLocalityDiagnostics(ast, source, filename, isReassigned));
4690
+ diagnostics.push(...strongLocalityDiagnostics(ast, source, filename, isReassigned, hookNames));
4691
+ }
4692
+ templatePolicy.finish();
4693
+ for (const policy of analyzeStrongHookPolicies(ast, options)) {
4694
+ diagnostics.push({
4695
+ ...diagnostic(policy.code, filename, policy.node, policy.message),
4696
+ severity: policy.severity,
4697
+ });
4006
4698
  }
4007
- return { enabled, diagnostics };
4699
+ diagnostics.push(...analyzeStrongHTML(ast, source, filename, options));
4700
+ const nativeChangeAnalysis = analyzeNativeChangeDiagnostics(ast, source, filename, {
4701
+ ...options,
4702
+ strong: true,
4703
+ });
4704
+ diagnostics.push(...nativeChangeAnalysis.diagnostics);
4705
+ return { enabled, diagnostics, nativeChangeAnalysis, strongHookAnalysis };
4008
4706
  }
4009
4707
 
4010
4708
  /** Throw the first Strong-mode violation using the original authored location. */
4011
4709
  export function assertStrongMode(ast, source, filename, options) {
4012
4710
  if (options?.strong !== true && !source.includes('use strong')) return;
4013
4711
  const result = analyzeStrongMode(ast, source, filename, options);
4014
- const violation = result.diagnostics[0];
4712
+ const violation = result.diagnostics.find((item) => item.severity === 'error');
4015
4713
  if (violation === undefined) return result;
4016
4714
  const { code, message } = violation;
4017
4715
  const { line, column } = violation.start;