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.
- package/README.md +5 -0
- package/dist/behavior-root.d.ts +13 -3
- package/dist/behavior-root.js +28 -5
- package/dist/cjs/behavior-root.cjs +32 -6
- package/dist/cjs/class-names.cjs +49 -0
- package/dist/cjs/constants.cjs +9 -0
- package/dist/cjs/context-epoch.cjs +38 -0
- package/dist/cjs/css.cjs +6 -27
- package/dist/cjs/data-encoding.cjs +160 -0
- package/dist/cjs/dom-binding-handoff.cjs +93 -0
- package/dist/cjs/dom-binding-protocol.cjs +115 -0
- package/dist/cjs/dom-binding-styles.cjs +292 -0
- package/dist/cjs/dom-bindings.cjs +700 -0
- package/dist/cjs/dom-focused-move.cjs +66 -0
- package/dist/cjs/dom-stage.cjs +896 -0
- package/dist/cjs/dom-tables.cjs +4 -94
- package/dist/cjs/error-codes.client.generated.cjs +33 -0
- package/dist/cjs/error-codes.server.generated.cjs +24 -0
- package/dist/cjs/hydration/control-capture.cjs +244 -0
- package/dist/cjs/hydration/event-capture.cjs +125 -43
- package/dist/cjs/hydration/interaction-config.cjs +50 -2
- package/dist/cjs/hydration-markers.cjs +37 -0
- package/dist/cjs/independent-hydration-protocol.cjs +106 -0
- package/dist/cjs/index.cjs +9 -0
- package/dist/cjs/runtime.cjs +6397 -1683
- package/dist/cjs/runtime.server.cjs +1465 -213
- package/dist/cjs/server/early-signals.cjs +45 -0
- package/dist/cjs/server/index.cjs +47 -0
- package/dist/cjs/server/rpc-batch.cjs +149 -0
- package/dist/cjs/server/rpc-stream.cjs +146 -0
- package/dist/cjs/server/rpc.cjs +34 -15
- package/dist/cjs/server/signal-query-observation.cjs +158 -0
- package/dist/cjs/server/streamed-region.cjs +52 -0
- package/dist/cjs/server/streamed-signals.cjs +512 -0
- package/dist/cjs/server-call.cjs +103 -0
- package/dist/cjs/server-rpc-batch-client.cjs +141 -0
- package/dist/cjs/server-rpc-client.cjs +94 -27
- package/dist/cjs/server-rpc-protocol.cjs +141 -0
- package/dist/cjs/server-rpc-stream-client.cjs +118 -0
- package/dist/cjs/signals/actions.cjs +474 -0
- package/dist/cjs/signals/client.cjs +4 -1
- package/dist/cjs/signals/computations.cjs +460 -0
- package/dist/cjs/signals/control-binding.cjs +368 -0
- package/dist/cjs/signals/derived-facade.cjs +50 -0
- package/dist/cjs/signals/document-owner.cjs +71 -0
- package/dist/cjs/signals/early-values.cjs +155 -0
- package/dist/cjs/signals/encoding.cjs +6 -95
- package/dist/cjs/signals/engine.cjs +276 -12
- package/dist/cjs/signals/errors.cjs +18 -0
- package/dist/cjs/signals/facade.cjs +477 -0
- package/dist/cjs/signals/graph.cjs +166 -20
- package/dist/cjs/signals/handle-protocol.cjs +36 -0
- package/dist/cjs/signals/index.cjs +81 -2
- package/dist/cjs/signals/native-read-client.cjs +10 -0
- package/dist/cjs/signals/owner-context.cjs +112 -0
- package/dist/cjs/signals/query-attempt-observer.cjs +68 -0
- package/dist/cjs/signals/query-facade.cjs +72 -0
- package/dist/cjs/signals/read-protocol.cjs +54 -0
- package/dist/cjs/signals/requests.cjs +287 -14
- package/dist/cjs/signals/scalar-computations.cjs +76 -0
- package/dist/cjs/signals/server.cjs +4 -1
- package/dist/cjs/signals/transition-candidate.cjs +567 -0
- package/dist/cjs/signals/transition-state.cjs +106 -0
- package/dist/cjs/signals/types.cjs +50 -0
- package/dist/cjs/stream-protocol.cjs +3 -0
- package/dist/cjs/streamed-signals-protocol.cjs +159 -0
- package/dist/cjs/style-values.cjs +122 -0
- package/dist/cjs/trusted-html.cjs +30 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/class-names.d.ts +5 -0
- package/dist/class-names.js +25 -0
- package/dist/compiler/bundler.js +51 -22
- package/dist/compiler/compile.js +2534 -581
- package/dist/compiler/dom-binding-program.js +935 -0
- package/dist/compiler/dom-binding-request.js +57 -0
- package/dist/compiler/dom-bindings.js +2245 -0
- package/dist/compiler/hook-deps.js +759 -10
- package/dist/compiler/hydrate-boundaries.js +358 -15
- package/dist/compiler/index.d.ts +39 -2
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/native-attribute-reads.js +127 -0
- package/dist/compiler/native-change-diagnostics.js +32 -38
- package/dist/compiler/native-read-diagnostics.js +69 -8
- package/dist/compiler/native-read-types.js +80 -34
- package/dist/compiler/parser.node.js +1 -1
- package/dist/compiler/renderer-boundaries.js +40 -0
- package/dist/compiler/server-context.js +118 -0
- package/dist/compiler/signal-attempt-reads.js +479 -0
- package/dist/compiler/signal-declarations.js +557 -0
- package/dist/compiler/signal-start-reads.js +413 -0
- package/dist/compiler/slot-hooks.d.ts +3 -1
- package/dist/compiler/slot-hooks.js +168 -34
- package/dist/compiler/strong-auto-memo.js +90 -0
- package/dist/compiler/strong-html.js +143 -0
- package/dist/compiler/strong-mode.js +764 -66
- package/dist/compiler/strong-template-policy.js +307 -0
- package/dist/compiler/template-ir.js +200 -0
- package/dist/compiler/typescript.d.ts +2 -0
- package/dist/compiler/typescript.js +2 -0
- package/dist/compiler/vite.d.ts +3 -1
- package/dist/compiler/vite.js +122 -4
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.d.ts +6 -1
- package/dist/compiler/volar.js +750 -1433
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +10 -0
- package/dist/context-epoch.d.ts +2 -0
- package/dist/context-epoch.js +13 -0
- package/dist/css.d.ts +2 -5
- package/dist/css.js +2 -23
- package/dist/data-encoding.d.ts +9 -0
- package/dist/data-encoding.js +134 -0
- package/dist/dom-binding-classes.d.ts +15 -0
- package/dist/dom-binding-classes.js +129 -0
- package/dist/dom-binding-controls.d.ts +2 -0
- package/dist/dom-binding-controls.js +6 -0
- package/dist/dom-binding-handoff.d.ts +55 -0
- package/dist/dom-binding-handoff.js +62 -0
- package/dist/dom-binding-program.d.ts +300 -0
- package/dist/dom-binding-program.js +1625 -0
- package/dist/dom-binding-projections.d.ts +15 -0
- package/dist/dom-binding-projections.js +187 -0
- package/dist/dom-binding-protocol.d.ts +13 -0
- package/dist/dom-binding-protocol.js +88 -0
- package/dist/dom-binding-signals.d.ts +21 -0
- package/dist/dom-binding-signals.js +109 -0
- package/dist/dom-binding-styles.d.ts +17 -0
- package/dist/dom-binding-styles.js +272 -0
- package/dist/dom-bindings.d.ts +89 -0
- package/dist/dom-bindings.js +670 -0
- package/dist/dom-focused-move.d.ts +6 -0
- package/dist/dom-focused-move.js +42 -0
- package/dist/dom-stage.d.ts +52 -0
- package/dist/dom-stage.js +872 -0
- package/dist/dom-tables.d.ts +1 -3
- package/dist/dom-tables.js +1 -91
- package/dist/error-codes.client.generated.d.ts +9 -0
- package/dist/error-codes.client.generated.js +33 -0
- package/dist/error-codes.server.generated.d.ts +6 -0
- package/dist/error-codes.server.generated.js +24 -0
- package/dist/hydration/control-capture.d.ts +62 -0
- package/dist/hydration/control-capture.js +223 -0
- package/dist/hydration/document-lifecycle.d.ts +23 -0
- package/dist/hydration/document-lifecycle.js +80 -0
- package/dist/hydration/event-capture.d.ts +20 -1
- package/dist/hydration/event-capture.js +128 -42
- package/dist/hydration/independent-island.d.ts +45 -0
- package/dist/hydration/independent-island.js +233 -0
- package/dist/hydration/index.d.ts +8 -1
- package/dist/hydration/index.js +55 -1
- package/dist/hydration/interaction-config.d.ts +5 -0
- package/dist/hydration/interaction-config.js +44 -1
- package/dist/hydration/stream-delivery.d.ts +32 -0
- package/dist/hydration/stream-delivery.js +333 -0
- package/dist/hydration/stream-receiver.d.ts +26 -0
- package/dist/hydration/stream-receiver.js +204 -0
- package/dist/hydration/stream-result-receiver.d.ts +70 -0
- package/dist/hydration/stream-result-receiver.js +246 -0
- package/dist/hydration/streamed-signals.d.ts +44 -0
- package/dist/hydration/streamed-signals.js +186 -0
- package/dist/hydration/types.d.ts +6 -0
- package/dist/hydration-markers.d.ts +7 -0
- package/dist/hydration-markers.js +10 -0
- package/dist/independent-hydration-protocol.d.ts +31 -0
- package/dist/independent-hydration-protocol.js +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/internal/signal-read.d.ts +1 -0
- package/dist/internal/signal-read.js +4 -0
- package/dist/jsx-runtime-strong.d.ts +33 -0
- package/dist/jsx-runtime.d.ts +58 -2
- package/dist/public-types.d.ts +15 -1
- package/dist/runtime.d.ts +297 -23
- package/dist/runtime.js +6405 -1679
- package/dist/runtime.server.d.ts +110 -27
- package/dist/runtime.server.js +1458 -213
- package/dist/server/early-signals.d.ts +14 -0
- package/dist/server/early-signals.js +34 -0
- package/dist/server/index.d.ts +11 -2
- package/dist/server/index.js +50 -1
- package/dist/server/rpc-batch.d.ts +17 -0
- package/dist/server/rpc-batch.js +129 -0
- package/dist/server/rpc-stream.d.ts +9 -0
- package/dist/server/rpc-stream.js +125 -0
- package/dist/server/rpc.d.ts +5 -1
- package/dist/server/rpc.js +37 -13
- package/dist/server/signal-query-observation.d.ts +3 -0
- package/dist/server/signal-query-observation.js +134 -0
- package/dist/server/streamed-region.d.ts +15 -0
- package/dist/server/streamed-region.js +30 -0
- package/dist/server/streamed-signals.d.ts +45 -0
- package/dist/server/streamed-signals.js +488 -0
- package/dist/server-call.d.ts +41 -0
- package/dist/server-call.js +75 -0
- package/dist/server-rpc-batch-client.d.ts +7 -0
- package/dist/server-rpc-batch-client.js +119 -0
- package/dist/server-rpc-client.d.ts +3 -1
- package/dist/server-rpc-client.js +99 -27
- package/dist/server-rpc-protocol.d.ts +47 -0
- package/dist/server-rpc-protocol.js +109 -0
- package/dist/server-rpc-stream-client.d.ts +5 -0
- package/dist/server-rpc-stream-client.js +98 -0
- package/dist/signals/actions.d.ts +12 -0
- package/dist/signals/actions.js +466 -0
- package/dist/signals/client.d.ts +2 -0
- package/dist/signals/client.js +1 -0
- package/dist/signals/computations.d.ts +37 -0
- package/dist/signals/computations.js +455 -0
- package/dist/signals/control-binding.d.ts +31 -0
- package/dist/signals/control-binding.js +353 -0
- package/dist/signals/derived-facade.d.ts +3 -0
- package/dist/signals/derived-facade.js +25 -0
- package/dist/signals/document-owner.d.ts +10 -0
- package/dist/signals/document-owner.js +43 -0
- package/dist/signals/early-values.d.ts +24 -0
- package/dist/signals/early-values.js +124 -0
- package/dist/signals/encoding.d.ts +1 -3
- package/dist/signals/encoding.js +3 -93
- package/dist/signals/engine.d.ts +53 -4
- package/dist/signals/engine.js +275 -16
- package/dist/signals/errors.d.ts +7 -0
- package/dist/signals/errors.js +16 -0
- package/dist/signals/facade.d.ts +74 -0
- package/dist/signals/facade.js +452 -0
- package/dist/signals/graph.d.ts +52 -6
- package/dist/signals/graph.js +174 -19
- package/dist/signals/handle-protocol.d.ts +4 -0
- package/dist/signals/handle-protocol.js +11 -0
- package/dist/signals/index.d.ts +12 -3
- package/dist/signals/index.js +71 -2
- package/dist/signals/native-read-client.d.ts +3 -0
- package/dist/signals/native-read-client.js +10 -0
- package/dist/signals/owner-context.d.ts +22 -0
- package/dist/signals/owner-context.js +77 -0
- package/dist/signals/query-attempt-observer.d.ts +49 -0
- package/dist/signals/query-attempt-observer.js +41 -0
- package/dist/signals/query-facade.d.ts +3 -0
- package/dist/signals/query-facade.js +49 -0
- package/dist/signals/read-protocol.d.ts +37 -2
- package/dist/signals/read-protocol.js +44 -0
- package/dist/signals/requests.d.ts +38 -5
- package/dist/signals/requests.js +298 -15
- package/dist/signals/scalar-computations.d.ts +2 -0
- package/dist/signals/scalar-computations.js +60 -0
- package/dist/signals/server.d.ts +2 -0
- package/dist/signals/server.js +1 -0
- package/dist/signals/transition-candidate.d.ts +71 -0
- package/dist/signals/transition-candidate.js +557 -0
- package/dist/signals/transition-state.d.ts +51 -0
- package/dist/signals/transition-state.js +70 -0
- package/dist/signals/types.d.ts +95 -5
- package/dist/signals/types.js +20 -0
- package/dist/stream-protocol.d.ts +0 -5
- package/dist/stream-protocol.js +3 -0
- package/dist/streamed-signals-protocol.d.ts +60 -0
- package/dist/streamed-signals-protocol.js +130 -0
- package/dist/style-values.d.ts +3 -0
- package/dist/style-values.js +96 -0
- package/dist/trusted-html.d.ts +17 -0
- package/dist/trusted-html.js +6 -0
- package/dist/universal-core.d.ts +6 -0
- package/dist/universal-core.js +214 -202
- package/dist/version.js +1 -1
- 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'
|
|
478
|
+
(bindingsOnly && node.type === 'StaticBlock') ||
|
|
479
|
+
(node.type === 'JSXCodeBlock' && !functionBodies.has(node))
|
|
480
|
+
? 'function'
|
|
481
|
+
: 'block',
|
|
459
482
|
);
|
|
460
|
-
if (
|
|
461
|
-
|
|
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 (
|
|
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
|
|
1486
|
-
|
|
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
|
|
1592
|
-
// null, and
|
|
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
|
+
}
|