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
@@ -3,6 +3,9 @@
3
3
  // surgical plain-TS hook pass, keeping custom hooks and components aligned.
4
4
 
5
5
  import { builders as b } from '@tsrx/core';
6
+ import { hasInlineMemoDirectEval } from './inline-hook-memo.js';
7
+
8
+ export const STRONG_AUTOMATIC_MEMO_UNSUPPORTED = 'OCTANE_STRONG_AUTOMATIC_MEMO_UNSUPPORTED';
6
9
 
7
10
  const DEPENDENCY_HOOKS = new Map([
8
11
  ['useEffect', { callback: 0, deps: 1 }],
@@ -174,7 +177,13 @@ function collectHoistedVars(node, functionScope, root = true, bindingsOnly = fal
174
177
  for (const child of node) collectHoistedVars(child, functionScope, false, bindingsOnly);
175
178
  return;
176
179
  }
177
- if (!root && isFunction(node)) return;
180
+ if (!root && (isFunction(node) || node.type === 'JSXCodeBlock')) return;
181
+ if (
182
+ ['JSXIfExpression', 'JSXForExpression', 'JSXSwitchExpression', 'JSXTryExpression'].includes(
183
+ node.type,
184
+ )
185
+ )
186
+ return;
178
187
  if (
179
188
  bindingsOnly &&
180
189
  (isFunction(node) ||
@@ -216,6 +225,15 @@ function unwrapValue(node) {
216
225
  return node;
217
226
  }
218
227
 
228
+ function isUnshadowedUndefined(node, scope) {
229
+ const value = unwrapValue(node);
230
+ return (
231
+ value?.type === 'Identifier' &&
232
+ value.name === 'undefined' &&
233
+ resolveBinding(scope, value.name) === null
234
+ );
235
+ }
236
+
219
237
  function canonicalHookName(call, scope, onlyImported) {
220
238
  const callee = unwrapValue(call?.callee);
221
239
  if (!callee) return null;
@@ -305,6 +323,7 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
305
323
  const moduleScope = createScope(null, 'module');
306
324
  const nodeScopes = new WeakMap();
307
325
  const functionScopes = new WeakMap();
326
+ const functionBodies = new WeakSet();
308
327
  const declarators = [];
309
328
  const candidates = [];
310
329
  const calls = [];
@@ -358,6 +377,7 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
358
377
  }
359
378
 
360
379
  if (isFunction(node)) {
380
+ if (node.body) functionBodies.add(node.body);
361
381
  const fnScope = createScope(scope, 'function');
362
382
  const record = bindingsOnly
363
383
  ? null
@@ -455,10 +475,16 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
455
475
  ) {
456
476
  const blockScope = createScope(
457
477
  scope,
458
- bindingsOnly && node.type === 'StaticBlock' ? 'function' : 'block',
478
+ (bindingsOnly && node.type === 'StaticBlock') ||
479
+ (node.type === 'JSXCodeBlock' && !functionBodies.has(node))
480
+ ? 'function'
481
+ : 'block',
459
482
  );
460
- if (bindingsOnly && node.type === 'StaticBlock') {
461
- collectHoistedVars(node, blockScope, true, true);
483
+ if (
484
+ (bindingsOnly && node.type === 'StaticBlock') ||
485
+ (node.type === 'JSXCodeBlock' && !functionBodies.has(node))
486
+ ) {
487
+ collectHoistedVars(node, blockScope, true, bindingsOnly);
462
488
  }
463
489
  predeclareDirect(node.body, blockScope, hookRuntimeModules, bindingsOnly);
464
490
  for (const statement of node.body || []) walk(statement, blockScope);
@@ -638,7 +664,11 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
638
664
  config,
639
665
  trustedConfig,
640
666
  });
641
- if (trustedConfig && node.arguments.length === trustedConfig.deps) {
667
+ if (
668
+ trustedConfig &&
669
+ (node.arguments.length === trustedConfig.deps ||
670
+ node._octaneStrongOmittedDependency === true)
671
+ ) {
642
672
  candidates.push({ call: node, scope, name: trustedName, config: trustedConfig });
643
673
  }
644
674
  }
@@ -1482,8 +1512,27 @@ function cloneDependency(node) {
1482
1512
  /** @param {any} ast @param {{ onlyImported?: boolean, hookRuntimeModules?: readonly string[], filename?: string, inferDependencies?: boolean }} options */
1483
1513
  function analyzeInternal(ast, options) {
1484
1514
  const onlyImported = options.onlyImported === true;
1485
- const hookRuntimeModules = new Set(['octane', ...(options.hookRuntimeModules || [])]);
1486
- const analysis = buildScopes(ast, onlyImported, hookRuntimeModules);
1515
+ const shared = options.strongHookAnalysis ? analyzeStrongHookBindings(ast, options) : null;
1516
+ let analysis =
1517
+ shared?.analysis ??
1518
+ buildScopes(ast, onlyImported, new Set(['octane', ...(options.hookRuntimeModules || [])]));
1519
+ if (shared) {
1520
+ const callAnnotations = new Map(analysis.callAnnotations);
1521
+ const candidates = [];
1522
+ for (const { call, scope } of analysis.calls) {
1523
+ const name = shared.callNames.get(call);
1524
+ const config = DEPENDENCY_HOOKS.get(name);
1525
+ if (!config) continue;
1526
+ const annotation = { ...callAnnotations.get(call), _octaneImportedHook: name };
1527
+ delete annotation._octaneCustomHookCall;
1528
+ callAnnotations.set(call, annotation);
1529
+ const omitted = call.arguments[config.deps] === undefined;
1530
+ const replaceDependency = isUnshadowedUndefined(call.arguments[config.deps], scope);
1531
+ if (omitted || replaceDependency)
1532
+ candidates.push({ call, scope, name, config, replaceDependency });
1533
+ }
1534
+ analysis = { ...analysis, callAnnotations, candidates };
1535
+ }
1487
1536
  const inferred = new Map();
1488
1537
  // A hand-slotted module owns its authored dependency ABI. The production
1489
1538
  // memo-only pass still needs lexical import provenance, but must not infer
@@ -1514,6 +1563,9 @@ function analyzeInternal(ast, options) {
1514
1563
  inferred.set(candidate.call, {
1515
1564
  name: candidate.name,
1516
1565
  depsIndex: candidate.config.deps,
1566
+ ...(candidate.call._octaneStrongOmittedDependency === true || candidate.replaceDependency
1567
+ ? { replaceDependency: true }
1568
+ : {}),
1517
1569
  dependencies,
1518
1570
  });
1519
1571
  }
@@ -1549,6 +1601,245 @@ export function analyzeHookDependencies(ast, options = {}) {
1549
1601
  return analyzeInternal(ast, options).inferred;
1550
1602
  }
1551
1603
 
1604
+ /**
1605
+ * Infer callback captures for compiler-owned lifetimes without manufacturing a
1606
+ * hook call. Results retain the hook collector's receiver-aware method records;
1607
+ * null means the expression is not an analyzable callback, never an empty list.
1608
+ * The supplied callbacks must belong to this AST so lexical bindings are shared.
1609
+ */
1610
+ export function analyzeCallbackDependencies(ast, callbacks, options = {}) {
1611
+ const analysis = buildScopes(
1612
+ ast,
1613
+ options.onlyImported === true,
1614
+ new Set(['octane', ...(options.hookRuntimeModules || [])]),
1615
+ );
1616
+ markDependencyInvariantBindings(analysis);
1617
+ const inferred = new Map();
1618
+ for (const original of callbacks) {
1619
+ const callback = unwrapValue(original);
1620
+ inferred.set(
1621
+ original,
1622
+ isFunction(callback)
1623
+ ? collectDependencies(callback, analysis.functionScopes.get(callback) || null, analysis)
1624
+ : collectCallbackReference(callback, analysis),
1625
+ );
1626
+ }
1627
+ return inferred;
1628
+ }
1629
+
1630
+ // Strong dependency policy deliberately shares inference's lexical graph and
1631
+ // dependency collector. Comparing source strings, or independently collecting
1632
+ // free names here, would disagree on stable hooks and Effect Event exclusions.
1633
+ function strongHookNameResolver(analysis, onlyImported) {
1634
+ const aliases = new Map();
1635
+ function propertyName(property, computed) {
1636
+ const key = unwrapValue(property);
1637
+ return computed ? (key?.type === 'Literal' ? key.value : null) : key?.name;
1638
+ }
1639
+ for (const { decl, bindings, kind } of analysis.declarators) {
1640
+ if (kind !== 'const' || !decl.init) continue;
1641
+ const byPattern = new Map(bindings.map(({ pattern, binding }) => [pattern, binding]));
1642
+ function collect(pattern, path) {
1643
+ if (pattern?.type === 'Identifier') {
1644
+ const binding = byPattern.get(pattern);
1645
+ if (binding && !binding.reassigned) aliases.set(binding, { node: decl.init, path });
1646
+ } else if (pattern?.type === 'AssignmentPattern') {
1647
+ // Named namespace exports are defined, so their defaults cannot run.
1648
+ collect(pattern.left, path);
1649
+ } else if (pattern?.type === 'ObjectPattern') {
1650
+ for (const property of pattern.properties) {
1651
+ if (property.type === 'RestElement') continue;
1652
+ const name = propertyName(property.key, property.computed);
1653
+ if (typeof name === 'string') collect(property.value, [...path, name]);
1654
+ }
1655
+ }
1656
+ }
1657
+ collect(decl.id, []);
1658
+ }
1659
+ function resolve(node, scope, seen = new Set()) {
1660
+ const value = unwrapValue(node);
1661
+ if (value?.type === 'ChainExpression') return resolve(value.expression, scope, seen);
1662
+ if (value?.type === 'Identifier') {
1663
+ const binding = resolveBinding(scope, value.name);
1664
+ if (binding === null) return onlyImported ? null : value.name;
1665
+ if (binding.reassigned) return null;
1666
+ if (binding.hookRuntimeImport) return binding.hookRuntimeImport;
1667
+ if (binding.hookRuntimeNamespace) return '*';
1668
+ if (!aliases.has(binding) || seen.has(binding)) return null;
1669
+ seen.add(binding);
1670
+ const alias = aliases.get(binding);
1671
+ let name = resolve(alias.node, analysis.nodeScopes.get(alias.node) ?? binding.scope, seen);
1672
+ for (const property of alias.path) name = name === '*' ? property : null;
1673
+ return name;
1674
+ }
1675
+ if (value?.type === 'MemberExpression') {
1676
+ const name = propertyName(value.property, value.computed);
1677
+ return typeof name === 'string' && resolve(value.object, scope, seen) === '*' ? name : null;
1678
+ }
1679
+ return null;
1680
+ }
1681
+ // Optional calls still target defined, immutable Octane exports. Optional
1682
+ // syntax must not bypass Strong authoring policy for those proven imports.
1683
+ return (call, scope) => resolve(call.callee, scope);
1684
+ }
1685
+
1686
+ /** Shared authored hook identity and lexical proof for Strong analyses. */
1687
+ export function analyzeStrongHookBindings(ast, options = {}) {
1688
+ const onlyImported = options.onlyImported === true;
1689
+ const hookRuntimeModules = new Set([
1690
+ 'octane',
1691
+ 'octane/server',
1692
+ ...(options.nativeReads ? ['octane/signals/client', 'octane/signals/server'] : []),
1693
+ ...(options.hookRuntimeModules ?? []),
1694
+ ]);
1695
+ const shared = options.strongHookAnalysis;
1696
+ if (
1697
+ shared?.ast === ast &&
1698
+ shared.onlyImported === onlyImported &&
1699
+ shared.hookRuntimeModules.size === hookRuntimeModules.size &&
1700
+ [...hookRuntimeModules].every((request) => shared.hookRuntimeModules.has(request))
1701
+ )
1702
+ return shared;
1703
+ const analysis = buildScopes(ast, onlyImported, hookRuntimeModules);
1704
+ const nameFor = strongHookNameResolver(analysis, onlyImported);
1705
+ const callNames = new Map();
1706
+ for (const { call, scope } of analysis.calls) {
1707
+ const name = nameFor(call, scope);
1708
+ if (name !== null) callNames.set(call, name);
1709
+ }
1710
+ return { ast, analysis, callNames, onlyImported, hookRuntimeModules };
1711
+ }
1712
+
1713
+ function dependencyExpressionKey(expression, analysis) {
1714
+ const node = unwrapValue(expression);
1715
+ if (node?.type === 'Identifier') {
1716
+ const scope = analysis.nodeScopes.get(node);
1717
+ const binding = scope ? resolveBinding(scope, node.name) : null;
1718
+ return binding === null ? `global:${node.name}` : `binding:${binding.id}`;
1719
+ }
1720
+ if (node?.type === 'ChainExpression') return dependencyExpressionKey(node.expression, analysis);
1721
+ if (node?.type === 'MemberExpression') {
1722
+ const object = dependencyExpressionKey(node.object, analysis);
1723
+ const property = node.computed ? unwrapValue(node.property) : null;
1724
+ const name = node.computed
1725
+ ? property?.type === 'Literal' && ['string', 'number'].includes(typeof property.value)
1726
+ ? String(property.value)
1727
+ : null
1728
+ : node.property?.name;
1729
+ return object === null || name == null
1730
+ ? null
1731
+ : `${object}${node.optional ? '?' : ''}[${JSON.stringify(name)}]`;
1732
+ }
1733
+ return null;
1734
+ }
1735
+
1736
+ // Inference omits imported and module-invariant roots (including stable hook
1737
+ // results) because they cannot change between renders. An authored entry for
1738
+ // one adds nothing observable to a callback that ignores its arguments.
1739
+ function dependencyInvariantRoot(expression, analysis) {
1740
+ const node = unwrapValue(expression);
1741
+ if (node?.type !== 'Identifier') return false;
1742
+ const scope = analysis.nodeScopes.get(node);
1743
+ const binding = scope ? resolveBinding(scope, node.name) : null;
1744
+ return binding !== null && (binding.imported === true || binding.dependencyInvariant === true);
1745
+ }
1746
+
1747
+ function equivalentStrongDependencies(authored, callback, inferred, analysis) {
1748
+ if (authored?.type !== 'ArrayExpression' || inferred === null) return false;
1749
+ const expected = [];
1750
+ for (const dependency of inferred) {
1751
+ // Inference selects either an own method or its receiver at runtime.
1752
+ // A handwritten member cannot prove equality with that conditional value.
1753
+ if (dependency.method) return false;
1754
+ const key = dependencyExpressionKey(dependency.node, analysis);
1755
+ if (key === null) return false;
1756
+ expected.push(key);
1757
+ }
1758
+ const actual = [];
1759
+ const reactive = [];
1760
+ for (const dependency of authored.elements) {
1761
+ const key = dependencyExpressionKey(dependency, analysis);
1762
+ if (key === null) return false;
1763
+ actual.push(key);
1764
+ if (!dependencyInvariantRoot(dependency, analysis)) reactive.push(key);
1765
+ }
1766
+ // Dependency values are also callback arguments in Octane. For callbacks
1767
+ // that observe those arguments (including opaque references), position and
1768
+ // duplicate entries are observable and cannot be reduced to a set.
1769
+ if (callback?.type !== 'ArrowFunctionExpression' || callback.params.length > 0) {
1770
+ return (
1771
+ actual.length === expected.length && actual.every((key, index) => key === expected[index])
1772
+ );
1773
+ }
1774
+ const actualSet = new Set(reactive);
1775
+ return actualSet.size === new Set(expected).size && expected.every((key) => actualSet.has(key));
1776
+ }
1777
+
1778
+ /**
1779
+ * Read-only policy records for authored built-in hooks in an enabled Strong
1780
+ * module. The caller owns module opt-in, diagnostic locations, and severity.
1781
+ * Compatibility compilation never invokes this additional lexical analysis.
1782
+ * @param {any} ast
1783
+ * @param {{ onlyImported?: boolean, hookRuntimeModules?: readonly string[], nativeReads?: boolean }} [options]
1784
+ */
1785
+ export function analyzeStrongHookPolicies(ast, options = {}) {
1786
+ const { analysis, callNames } = analyzeStrongHookBindings(ast, options);
1787
+ const diagnostics = [];
1788
+ let markedInvariants = false;
1789
+ for (const { call, scope } of analysis.calls) {
1790
+ const name = callNames.get(call);
1791
+ const config = DEPENDENCY_HOOKS.get(name);
1792
+ if (config === undefined) continue;
1793
+ if (name === 'useMemo' || name === 'useCallback') {
1794
+ diagnostics.push({
1795
+ code: 'OCTANE_STRONG_MANUAL_MEMO',
1796
+ node: call,
1797
+ severity: 'error',
1798
+ message: `Strong mode owns calculation and callback caching. Replace ${name} with a const declaration and let the compiler track its inputs.`,
1799
+ });
1800
+ continue;
1801
+ }
1802
+ const argument = call.arguments[config.deps];
1803
+ if (argument === undefined || isUnshadowedUndefined(argument, scope)) continue;
1804
+ const authored = unwrapValue(argument);
1805
+ if (
1806
+ (authored?.type === 'Literal' && authored.value === null) ||
1807
+ authored?.type === 'NullLiteral'
1808
+ ) {
1809
+ diagnostics.push({
1810
+ code: 'OCTANE_STRONG_UNTRACKED_EFFECT',
1811
+ node: argument,
1812
+ severity: 'error',
1813
+ message:
1814
+ 'Strong mode does not allow null dependency lists. Omit the dependency argument so the compiler tracks the callback’s reactive inputs.',
1815
+ });
1816
+ continue;
1817
+ }
1818
+ let equivalent = false;
1819
+ if (authored?.type === 'ArrayExpression') {
1820
+ if (!markedInvariants) {
1821
+ markDependencyInvariantBindings(analysis);
1822
+ markedInvariants = true;
1823
+ }
1824
+ const callback = unwrapValue(call.arguments[config.callback]);
1825
+ const inferred = isFunction(callback)
1826
+ ? collectDependencies(callback, analysis.functionScopes.get(callback) ?? null, analysis)
1827
+ : collectCallbackReference(callback, analysis);
1828
+ equivalent = equivalentStrongDependencies(authored, callback, inferred, analysis);
1829
+ }
1830
+ diagnostics.push({
1831
+ code: 'OCTANE_STRONG_EXPLICIT_DEPENDENCIES',
1832
+ node: argument,
1833
+ severity: equivalent ? 'hint' : 'error',
1834
+ message: equivalent
1835
+ ? 'This dependency array is redundant: the compiler infers the same inputs. Omit the dependency argument.'
1836
+ : 'Strong mode owns dependency inference. Omit this dependency argument; its values are not proven equivalent to the callback’s inferred inputs.',
1837
+ });
1838
+ }
1839
+
1840
+ return diagnostics;
1841
+ }
1842
+
1552
1843
  /**
1553
1844
  * Copy-on-write rebuild carrying hook metadata: every call the scope walk
1554
1845
  * annotated is replaced by a shallow copy stamped with its `_octane*` props
@@ -1588,14 +1879,14 @@ function rebuildWithHookMetadata(ast, analysis, inferred, insertDeps, nativeRead
1588
1879
  if (out === null) out = { ...node };
1589
1880
  if (props !== undefined) Object.assign(out, props);
1590
1881
  if (result !== undefined) {
1591
- // Only an authored omission grants this capability. Explicit arrays,
1592
- // null, and dependency arguments forwarded by wrappers stay ordinary.
1882
+ // Only an omission (including Strong’s proven undefined placeholder) grants
1883
+ // this capability. Arrays, null, and forwarded dependencies stay ordinary.
1593
1884
  if (nativeReads && result.name === 'useMemo') out._octaneNativeInferredMemo = true;
1594
1885
  if (insertDeps) {
1595
1886
  const args = out.arguments.slice();
1596
1887
  // The synthesized array maps to the hook call it belongs to; each
1597
1888
  // dependency clone keeps its authored position.
1598
- args.splice(result.depsIndex, 0, {
1889
+ args.splice(result.depsIndex, result.replaceDependency ? 1 : 0, {
1599
1890
  ...b.array(
1600
1891
  result.dependencies.map((/** @type {any} */ dependency) =>
1601
1892
  dependency.method ? methodDepNode(dependency) : cloneDependency(dependency.node),
@@ -1651,3 +1942,461 @@ export function applyHookDependencies(ast, options = {}) {
1651
1942
  }
1652
1943
  return rebuildWithHookMetadata(ast, analysis, inferred, true, options.nativeReads === true).ast;
1653
1944
  }
1945
+
1946
+ /**
1947
+ * Candidates for Strong's declaration-level caching. Hook inference owns the
1948
+ * lexical and capture proof so generated caches use the same dependency ABI as
1949
+ * authored hooks. Mutable local objects and late-bound captures retain normal
1950
+ * JavaScript evaluation; caching them would change their lifetime or read TDZs.
1951
+ */
1952
+ export function analyzeStrongMemoCandidates(ast, options = {}) {
1953
+ const shared = analyzeStrongHookBindings(ast, options);
1954
+ const analysis = shared.analysis;
1955
+ const callNames = shared.callNames;
1956
+ markDependencyInvariantBindings(analysis);
1957
+ const hookName = (call) => callNames.get(call) ?? null;
1958
+ const hookCalls = new Map();
1959
+ const omittedDependencies = new Set();
1960
+ for (const { call, scope, trustedConfig } of analysis.calls) {
1961
+ const name = callNames.get(call);
1962
+ const config = DEPENDENCY_HOOKS.get(name);
1963
+ if (!config) continue;
1964
+ if (!trustedConfig || call.optional) hookCalls.set(call, name);
1965
+ if (isUnshadowedUndefined(call.arguments[config.deps], scope)) omittedDependencies.add(call);
1966
+ }
1967
+ const owners = new Map(analysis.functions.map((record) => [record.scope, record]));
1968
+ const functions = new Map(
1969
+ analysis.functions.filter((record) => record.binding).map((record) => [record.binding, record]),
1970
+ );
1971
+ const declarations = new Map();
1972
+ const aliases = new Map();
1973
+ function connectAlias(binding, target) {
1974
+ if (!binding || !target || binding === target) return;
1975
+ if (!aliases.has(binding)) aliases.set(binding, new Set());
1976
+ if (!aliases.has(target)) aliases.set(target, new Set());
1977
+ aliases.get(binding).add(target);
1978
+ aliases.get(target).add(binding);
1979
+ }
1980
+ function sharedBindings(node, targets) {
1981
+ const value = unwrapValue(node);
1982
+ if (!value) return;
1983
+ if (['Identifier', 'MemberExpression', 'ChainExpression'].includes(value.type)) {
1984
+ const target = rootBinding(value);
1985
+ if (target) targets.add(target);
1986
+ } else if (value.type === 'ObjectExpression')
1987
+ for (const prop of value.properties) sharedBindings(prop.argument ?? prop.value, targets);
1988
+ else if (value.type === 'ArrayExpression')
1989
+ for (const item of value.elements) sharedBindings(item?.argument ?? item, targets);
1990
+ else if (value.type === 'ConditionalExpression') {
1991
+ sharedBindings(value.consequent, targets);
1992
+ sharedBindings(value.alternate, targets);
1993
+ } else if (value.type === 'LogicalExpression') {
1994
+ sharedBindings(value.left, targets);
1995
+ sharedBindings(value.right, targets);
1996
+ } else if (['CallExpression', 'NewExpression'].includes(value.type))
1997
+ for (const arg of value.arguments) sharedBindings(arg.argument ?? arg, targets);
1998
+ }
1999
+
2000
+ for (const record of analysis.declarators) {
2001
+ for (const { binding } of record.bindings) declarations.set(binding, record.decl);
2002
+ const initial = unwrapValue(record.decl.init);
2003
+ if (record.kind === 'const') {
2004
+ const targets = new Set();
2005
+ sharedBindings(initial, targets);
2006
+ for (const { binding } of record.bindings)
2007
+ for (const target of targets) connectAlias(binding, target);
2008
+ }
2009
+ }
2010
+ const names = new Set();
2011
+ const referenced = new Set();
2012
+ const reads = new Map();
2013
+ const parents = new WeakMap();
2014
+ const bindingNodes = new WeakSet();
2015
+ function markBindingNodes(pattern) {
2016
+ if (!pattern) return;
2017
+ if (pattern.type === 'Identifier') bindingNodes.add(pattern);
2018
+ else if (pattern.type === 'ObjectPattern')
2019
+ for (const prop of pattern.properties) markBindingNodes(prop.argument ?? prop.value);
2020
+ else if (pattern.type === 'ArrayPattern')
2021
+ for (const item of pattern.elements) markBindingNodes(item);
2022
+ else if (pattern.type === 'AssignmentPattern') markBindingNodes(pattern.left);
2023
+ else if (pattern.type === 'RestElement') markBindingNodes(pattern.argument);
2024
+ }
2025
+ for (const { decl } of analysis.declarators) markBindingNodes(decl.id);
2026
+ for (const { node } of analysis.functions) {
2027
+ if (node.id) bindingNodes.add(node.id);
2028
+ for (const param of node.params) markBindingNodes(param);
2029
+ }
2030
+
2031
+ const mutated = new Set();
2032
+ const inLoop = new WeakSet();
2033
+ const mutationMethods = new Set([
2034
+ 'copyWithin',
2035
+ 'fill',
2036
+ 'pop',
2037
+ 'push',
2038
+ 'reverse',
2039
+ 'shift',
2040
+ 'sort',
2041
+ 'splice',
2042
+ 'unshift',
2043
+ 'set',
2044
+ 'delete',
2045
+ 'clear',
2046
+ 'add',
2047
+ ]);
2048
+ function rootBinding(node) {
2049
+ let value = unwrapValue(node);
2050
+ while (value?.type === 'MemberExpression' || value?.type === 'ChainExpression')
2051
+ value = unwrapValue(value.object ?? value.expression);
2052
+ return value?.type === 'Identifier'
2053
+ ? resolveBinding(analysis.nodeScopes.get(value) ?? null, value.name)
2054
+ : null;
2055
+ }
2056
+ function markMutation(target) {
2057
+ const value = unwrapValue(target);
2058
+ if (!value) return;
2059
+ if (value.type === 'ArrayPattern') for (const item of value.elements) markMutation(item);
2060
+ else if (value.type === 'ObjectPattern')
2061
+ for (const prop of value.properties) markMutation(prop.argument ?? prop.value);
2062
+ else if (value.type === 'AssignmentPattern') markMutation(value.left);
2063
+ else if (value.type === 'RestElement') markMutation(value.argument);
2064
+ else {
2065
+ const binding = rootBinding(value);
2066
+ if (binding) mutated.add(binding);
2067
+ }
2068
+ }
2069
+ function scan(node, loop = false, parent = null, parentKey = null) {
2070
+ if (!node || typeof node !== 'object') return;
2071
+ if (Array.isArray(node)) {
2072
+ for (const child of node) scan(child, loop, parent, parentKey);
2073
+ return;
2074
+ }
2075
+ if (parent) parents.set(node, parent);
2076
+ if (node.type === 'Identifier') {
2077
+ names.add(node.name);
2078
+ const structural =
2079
+ bindingNodes.has(node) ||
2080
+ parent?.type?.startsWith('Import') ||
2081
+ (parent?.type === 'MemberExpression' && parentKey === 'property' && !parent.computed) ||
2082
+ (['Property', 'MethodDefinition', 'PropertyDefinition'].includes(parent?.type) &&
2083
+ parentKey === 'key' &&
2084
+ !parent.computed) ||
2085
+ (parent?.type === 'LabeledStatement' && parentKey === 'label') ||
2086
+ ['BreakStatement', 'ContinueStatement', 'MetaProperty'].includes(parent?.type);
2087
+ if (!structural) {
2088
+ const binding = resolveBinding(analysis.nodeScopes.get(node) ?? null, node.name);
2089
+ if (binding) {
2090
+ referenced.add(binding);
2091
+ if (!reads.has(binding)) reads.set(binding, []);
2092
+ reads.get(binding).push(node);
2093
+ }
2094
+ }
2095
+ }
2096
+
2097
+ // A template `@for` body runs once per row, and this analysis does not
2098
+ // model the row bindings, so its declarations keep plain evaluation.
2099
+ if (
2100
+ [
2101
+ 'ForStatement',
2102
+ 'ForInStatement',
2103
+ 'ForOfStatement',
2104
+ 'WhileStatement',
2105
+ 'DoWhileStatement',
2106
+ 'JSXForExpression',
2107
+ ].includes(node.type)
2108
+ )
2109
+ loop = true;
2110
+ if (loop && node.type === 'VariableDeclarator') inLoop.add(node);
2111
+ if (node.type === 'ClassDeclaration' && node.id) {
2112
+ // A class binding stays in its temporal dead zone until the declaration
2113
+ // runs, so a cache input naming a later class would read it eagerly.
2114
+ const binding = resolveBinding(
2115
+ analysis.nodeScopes.get(node.id) ?? analysis.nodeScopes.get(node) ?? null,
2116
+ node.id.name,
2117
+ );
2118
+ if (binding) declarations.set(binding, node);
2119
+ }
2120
+ if (
2121
+ node.type === 'AssignmentExpression' ||
2122
+ node.type === 'UpdateExpression' ||
2123
+ (node.type === 'UnaryExpression' && node.operator === 'delete')
2124
+ ) {
2125
+ markMutation(node.left ?? node.argument);
2126
+ }
2127
+ if (
2128
+ ['ForInStatement', 'ForOfStatement'].includes(node.type) &&
2129
+ node.left?.type !== 'VariableDeclaration'
2130
+ )
2131
+ markMutation(node.left);
2132
+ if (node.type === 'CallExpression') {
2133
+ const member = unwrapValue(node.callee);
2134
+ const property = unwrapValue(member?.property);
2135
+ const receiver = unwrapValue(member?.object);
2136
+ const method = member?.computed
2137
+ ? property?.type === 'Literal'
2138
+ ? property.value
2139
+ : null
2140
+ : property?.name;
2141
+ // Preserve the conservative treatment of same-named local objects:
2142
+ // a shadowed Object/Reflect implementation can also mutate its target.
2143
+ if (
2144
+ receiver?.type === 'Identifier' &&
2145
+ ((receiver.name === 'Object' &&
2146
+ ['assign', 'defineProperty', 'defineProperties', 'setPrototypeOf'].includes(method)) ||
2147
+ (receiver.name === 'Reflect' &&
2148
+ ['set', 'deleteProperty', 'defineProperty', 'setPrototypeOf'].includes(method)))
2149
+ )
2150
+ markMutation(node.arguments[0]);
2151
+ if (member?.type === 'MemberExpression' && mutationMethods.has(method)) {
2152
+ const binding = rootBinding(member.object);
2153
+ if (binding) mutated.add(binding);
2154
+ }
2155
+ }
2156
+ for (const key in node)
2157
+ if (!AST_META_KEYS.has(key) && key !== 'typeAnnotation' && key !== 'returnType')
2158
+ scan(node[key], loop, node, key);
2159
+ }
2160
+ scan(ast);
2161
+ const mutationQueue = [...mutated];
2162
+ for (let index = 0; index < mutationQueue.length; index++) {
2163
+ for (const related of aliases.get(mutationQueue[index]) ?? []) {
2164
+ if (!mutated.has(related)) {
2165
+ mutated.add(related);
2166
+ mutationQueue.push(related);
2167
+ }
2168
+ }
2169
+ }
2170
+ const hookSummaries = new Map();
2171
+ // Nested functions count: a callback argument may run its hook synchronously
2172
+ // (`compute(() => useContext(Ctx))`), and caching that declaration would run
2173
+ // the hook only on cache misses.
2174
+ function executesHook(node, active = new Set(), root = true) {
2175
+ if (!node || typeof node !== 'object') return false;
2176
+ if (Array.isArray(node)) return node.some((child) => executesHook(child, active, false));
2177
+ if (node.type === 'CallExpression') {
2178
+ const name = hookName(node, analysis.nodeScopes.get(node));
2179
+ if (/^use(?:[A-Z]|$)/.test(name ?? '')) return true;
2180
+ const callee = unwrapValue(node.callee);
2181
+
2182
+ const binding = directCallBinding(node, analysis.nodeScopes.get(node));
2183
+ if (binding?.customHookImport) return true;
2184
+ const record = functions.get(binding);
2185
+ if (record && active.has(record)) return true;
2186
+ if (record) {
2187
+ if (!hookSummaries.has(record)) {
2188
+ active.add(record);
2189
+ hookSummaries.set(record, executesHook(record.node.body, active));
2190
+ active.delete(record);
2191
+ }
2192
+ if (hookSummaries.get(record)) return true;
2193
+ }
2194
+ if (isFunction(callee) && executesHook(callee.body, active)) return true;
2195
+ }
2196
+ for (const key in node)
2197
+ if (!AST_META_KEYS.has(key) && executesHook(node[key], active, false)) return true;
2198
+ return false;
2199
+ }
2200
+ function prohibited(node, root = true) {
2201
+ if (!node || typeof node !== 'object') return false;
2202
+ if (Array.isArray(node)) return node.some((child) => prohibited(child, false));
2203
+ if (!root && isFunction(node)) return false;
2204
+ if (
2205
+ [
2206
+ 'AwaitExpression',
2207
+ 'YieldExpression',
2208
+ 'AssignmentExpression',
2209
+ 'UpdateExpression',
2210
+ 'ThisExpression',
2211
+ 'Super',
2212
+ 'ImportExpression',
2213
+ 'CallExpression',
2214
+ 'NewExpression',
2215
+ 'TaggedTemplateExpression',
2216
+ 'SpreadElement',
2217
+ ].includes(node.type) ||
2218
+ node.type?.startsWith('JSX')
2219
+ )
2220
+ return true;
2221
+ if (node.type === 'Identifier' && node.name === 'arguments') return true;
2222
+ for (const key in node)
2223
+ if (!AST_META_KEYS.has(key) && prohibited(node[key], false)) return true;
2224
+ return false;
2225
+ }
2226
+ function capturesLexicalReceiver(node, root = true) {
2227
+ if (!node || typeof node !== 'object') return false;
2228
+ if (Array.isArray(node)) return node.some((child) => capturesLexicalReceiver(child, false));
2229
+ if (!root && ['FunctionExpression', 'FunctionDeclaration'].includes(node.type)) return false;
2230
+ if (['ThisExpression', 'Super', 'MetaProperty'].includes(node.type)) return true;
2231
+ for (const key in node)
2232
+ if (!AST_META_KEYS.has(key) && capturesLexicalReceiver(node[key], false)) return true;
2233
+ return false;
2234
+ }
2235
+ function createsIdentity(node) {
2236
+ const value = unwrapValue(node);
2237
+ if (!value) return false;
2238
+ if (
2239
+ [
2240
+ 'ObjectExpression',
2241
+ 'ArrayExpression',
2242
+ 'FunctionExpression',
2243
+ 'ArrowFunctionExpression',
2244
+ 'CallExpression',
2245
+ 'NewExpression',
2246
+ 'TaggedTemplateExpression',
2247
+ ].includes(value.type)
2248
+ )
2249
+ return true;
2250
+ if (value.type === 'ChainExpression') return createsIdentity(value.expression);
2251
+ if (value.type === 'ConditionalExpression')
2252
+ return createsIdentity(value.consequent) || createsIdentity(value.alternate);
2253
+ if (value.type === 'LogicalExpression')
2254
+ return createsIdentity(value.left) || createsIdentity(value.right);
2255
+ return false;
2256
+ }
2257
+ // A cache inherits an existing authored hook's render scope. Merely
2258
+ // producing JSX (or being named useX) never creates that ownership proof.
2259
+ function effectOwnerForRead(read) {
2260
+ let child = read;
2261
+ for (let node = parents.get(child); node; child = node, node = parents.get(node)) {
2262
+ if (node.type !== 'CallExpression') continue;
2263
+ const name = callNames.get(node);
2264
+ const config = DEPENDENCY_HOOKS.get(name);
2265
+ if (!config || ['useMemo', 'useCallback'].includes(name)) continue;
2266
+ if (node.arguments[config.callback] !== child && node.arguments[config.deps] !== child)
2267
+ continue;
2268
+ return owners.get(nearestFunctionScope(analysis.nodeScopes.get(node)));
2269
+ }
2270
+ return null;
2271
+ }
2272
+ const reflectiveOwners = new Map();
2273
+ function reflectiveOwner(record) {
2274
+ if (!reflectiveOwners.has(record))
2275
+ reflectiveOwners.set(record, hasInlineMemoDirectEval(record.node));
2276
+ return reflectiveOwners.get(record);
2277
+ }
2278
+ // A local custom hook can forward an input into an authored effect without
2279
+ // exposing that value during setup. Full compilation already supplies a
2280
+ // call-site boundary to direct useX calls. Plain modules deliberately do
2281
+ // not slot local custom calls, so only their own builtin effects qualify.
2282
+ const effectParameters = new Map();
2283
+ if (!shared.onlyImported)
2284
+ for (const record of analysis.functions) {
2285
+ if (
2286
+ !record.binding ||
2287
+ record.binding.reassigned ||
2288
+ !record.stableDefinition ||
2289
+ record.node.async ||
2290
+ record.node.generator ||
2291
+ reflectiveOwner(record) ||
2292
+ nearestFunctionScope(record.scope.parent)?.kind !== 'module'
2293
+ )
2294
+ continue;
2295
+ const indices = new Set();
2296
+ for (let index = 0; index < record.parameters.length; index++) {
2297
+ const binding = record.parameters[index];
2298
+ const uses = reads.get(binding) ?? [];
2299
+ if (
2300
+ binding &&
2301
+ !binding.reassigned &&
2302
+ !mutated.has(binding) &&
2303
+ uses.length &&
2304
+ uses.every((read) => effectOwnerForRead(read) === record)
2305
+ )
2306
+ indices.add(index);
2307
+ }
2308
+ if (indices.size) effectParameters.set(record.binding, indices);
2309
+ }
2310
+ function inputOwnerForRead(read) {
2311
+ const effectOwner = effectOwnerForRead(read);
2312
+ if (effectOwner) return effectOwner;
2313
+ let child = read;
2314
+ for (let node = parents.get(child); node; child = node, node = parents.get(node)) {
2315
+ if (node.type !== 'CallExpression') continue;
2316
+ if (
2317
+ node.optional ||
2318
+ node.callee?.type !== 'Identifier' ||
2319
+ !/^use[A-Z]/.test(node.callee.name)
2320
+ )
2321
+ return null;
2322
+ const indices = effectParameters.get(directCallBinding(node, analysis.nodeScopes.get(node)));
2323
+ if (!indices || !indices.has(node.arguments.indexOf(child)) || unwrapValue(child) !== read)
2324
+ return null;
2325
+ return owners.get(nearestFunctionScope(analysis.nodeScopes.get(node)));
2326
+ }
2327
+ return null;
2328
+ }
2329
+ function ownsReads(record, bindings) {
2330
+ if (record.node.async || record.node.generator || reflectiveOwner(record)) return false;
2331
+ // Nested callbacks and local JSX factories are value invocations. A
2332
+ // module function with its own authored slotted effect is already a hook
2333
+ // owner independently of how its name or returned values are spelled.
2334
+ if (nearestFunctionScope(record.scope.parent)?.kind !== 'module') return false;
2335
+ return bindings.every(({ binding }) =>
2336
+ (reads.get(binding) ?? []).every((read) => inputOwnerForRead(read) === record),
2337
+ );
2338
+ }
2339
+ const candidateDependencies = new Map();
2340
+ const candidates = new Map();
2341
+ for (const { decl, kind, bindings } of analysis.declarators) {
2342
+ const initial = unwrapValue(decl.init);
2343
+ if (
2344
+ !bindings.some(({ binding }) => referenced.has(binding)) ||
2345
+ kind !== 'const' ||
2346
+ !createsIdentity(initial) ||
2347
+ inLoop.has(decl) ||
2348
+ bindings.some(({ binding }) => binding.reassigned || mutated.has(binding))
2349
+ )
2350
+ continue;
2351
+ if (['Identifier', 'Literal', 'MemberExpression', 'ChainExpression'].includes(initial.type)) {
2352
+ if (
2353
+ initial.type !== 'ChainExpression' ||
2354
+ !['CallExpression', 'NewExpression'].includes(initial.expression?.type)
2355
+ )
2356
+ continue;
2357
+ }
2358
+ const owner = owners.get(nearestFunctionScope(analysis.nodeScopes.get(decl)));
2359
+ if (!owner || !ownsReads(owner, bindings) || hasInlineMemoDirectEval(initial)) continue;
2360
+ const callback = isFunction(initial);
2361
+ if (
2362
+ (!callback && prohibited(initial)) ||
2363
+ executesHook(initial) ||
2364
+ (initial.type === 'ArrowFunctionExpression' && capturesLexicalReceiver(initial))
2365
+ )
2366
+ continue;
2367
+ const dependencies = collectDependencies(
2368
+ initial,
2369
+ callback ? (analysis.functionScopes.get(initial) ?? null) : null,
2370
+ analysis,
2371
+ );
2372
+ if (
2373
+ dependencies.some(({ binding }) => {
2374
+ const enclosingFunction = owners.get(nearestFunctionScope(binding.scope));
2375
+ if (
2376
+ !callback &&
2377
+ enclosingFunction &&
2378
+ enclosingFunction.node.start >= initial.start &&
2379
+ enclosingFunction.node.end <= initial.end
2380
+ )
2381
+ return false;
2382
+ const declaration = declarations.get(binding);
2383
+ if (
2384
+ !callback &&
2385
+ declaration &&
2386
+ declaration.start >= initial.start &&
2387
+ declaration.end <= initial.end
2388
+ )
2389
+ return false;
2390
+ return (
2391
+ binding.reassigned ||
2392
+ mutated.has(binding) ||
2393
+ (declaration && declaration.start >= decl.start)
2394
+ );
2395
+ })
2396
+ )
2397
+ continue;
2398
+ candidates.set(decl, callback ? 'useCallback' : 'useMemo');
2399
+ candidateDependencies.set(decl, dependencies);
2400
+ }
2401
+ return { candidates, names, hookCalls, omittedDependencies, candidateDependencies };
2402
+ }