@kudzujs/core 0.8.27 → 0.8.29

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
4
4
 
5
- The executable post-`0.8.27` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
5
+ The executable post-`0.8.29` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
6
6
 
7
7
  [`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
8
8
 
@@ -270,6 +270,18 @@ This queue orders the next investigations by general migration value. Start only
270
270
  - Unsupported page-level mutable value refs fail during source analysis with a source location and the existing effect-owned animation-frame exception; no resource API or runtime is added.
271
271
  - The large-application and AI-native execution plan orders symbol resolution, semantic operations, project/module analysis, IR authority, application foundations, ecosystem compatibility, AI tooling, and scale validation before broad feature work.
272
272
 
273
+ ### Completed In 0.8.28
274
+
275
+ - A source-local binding index assigns deterministic lexical identities to locals, parameters, imports, captures, known globals, and unresolved references after normalization.
276
+ - Reactive binding capture/import discovery and lowering distinguish outer application values from same-named browser globals and nested callback parameters without changing source syntax or adding browser bytes.
277
+ - Synthesized keyed expressions retain the existing fail-safe lowering path; native handler, effect, and remaining descriptor consumers stay ordered behind the next symbol-aware compiler patch.
278
+
279
+ ### Completed In 0.8.29
280
+
281
+ - Native handler, effect, remaining binding, keyed evaluator, optimized-command, and effect-resource discovery/lowering use source-local lexical identity when the binding index owns the complete AST.
282
+ - Same-named callback parameters, locals, imports, browser globals, state values, setters, reducers, observer handles, and animation-frame handles no longer alias through identifier spelling alone.
283
+ - Imported, specialized, and compiler-synthesized trees retain the existing conservative fallback; no public API, source syntax, ModuleIR contract, browser runtime, or capability bytes were added.
284
+
273
285
  ## Cross-Cutting Performance Gates
274
286
 
275
287
  Every migration feature must preserve:
package/README.md CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.27 - Large-application compiler roadmap.** A reduced E2B terminal fixture now establishes the first Goal C resource boundary with a source-located diagnostic, while the architecture packet fixes the ordered semantic-compiler, application-scale, compatibility, AI-tooling, and production-validation program. Read the [release notes](./RELEASES.md#0827---large-application-compiler-roadmap), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.27), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.29 - Symbol-aware descriptor discovery.** Native handlers, effects, reactive bindings, keyed evaluators, optimized commands, and effect-resource validation now use source-local lexical identity when the binding index owns the complete AST, while synthesized trees retain the established fail-safe path. Read the [release notes](./RELEASES.md#0829---symbol-aware-descriptor-discovery), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.29), or follow the [architecture packet](./docs/next-architecture/README.md).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.29 - Symbol-aware descriptor discovery
4
+
5
+ Kudzu 0.8.29 completes P0.2 by moving native handler, effect, binding, and list descriptor decisions from identifier spelling onto the source-local binding index introduced in 0.8.28.
6
+
7
+ ### Changed in 0.8.29
8
+
9
+ - Native handlers and effects discover state, setters, reducers, imports, captures, and nested snapshots by lexical reference identity when the complete callback belongs to the source index.
10
+ - Handler lowering consumes the same indexed identity used during discovery, so callback parameters and locals that shadow state, setters, reducers, imports, or captures remain untouched.
11
+ - Reactive conditionals, bindings, keyed-list evaluators, and list state rewrites distinguish outer state from same-named nested callback parameters.
12
+ - The command fast path accepts state and setter references only when they resolve outside the callback, and recognizes `console.log()` only when `console` is the actual browser global.
13
+ - Effect resource validation distinguishes real `IntersectionObserver`, `requestAnimationFrame()`, and `cancelAnimationFrame()` globals from shadowed application bindings and matches cleanup to the exact observer or frame declaration.
14
+
15
+ ### Compiler Boundary
16
+
17
+ - The binding index now recognizes normalized and original TypeScript node boundaries and exposes whether it directly owns an AST node.
18
+ - Imported, specialized, or compiler-synthesized trees continue through the established conservative shadow walker rather than borrowing an unrelated source index.
19
+ - P0.2 changes no public API, accepted source syntax, ModuleIR shape, browser runtime, or generated capability contract. P0.3 graph failure diagnostics is next.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm test`, `npm run test:package`, and all 187 tests pass.
24
+ - Focused tests cover shadowed imports, browser globals, state, setters, list parameters, JSON-safe HandlerIR/BindingIR round trips, and same-named effect resource cleanup.
25
+ - Existing Context actions, keyed component callbacks, React Router search parameters, native events, effects, Workers, navigation, and static-route zero-JavaScript behavior remain covered.
26
+
27
+ ### Upgrade
28
+
29
+ ```bash
30
+ npm install @kudzujs/core@^0.8.29
31
+ ```
32
+
33
+ ## 0.8.28 - Source-local binding index
34
+
35
+ Kudzu 0.8.28 completes the first large-application compiler foundation by resolving source-local lexical bindings before descriptor discovery and lowering.
36
+
37
+ ### Changed in 0.8.28
38
+
39
+ - A source-local binding index classifies value references as local declarations, parameters, imports, outer captures, known globals, or unresolved names with deterministic slots and source ranges.
40
+ - The index models module, function parameter/body, block, loop, catch, switch, class, computed method, namespace, import, destructuring, `var`, `let`, and `const` scopes without adding a TypeScript Program or cross-module graph.
41
+ - Reactive binding capture/import discovery uses lexical identity, so component locals named `document`, `location`, `history`, `navigator`, or `console` no longer become browser globals and shadowed callback parameters no longer become imports.
42
+ - Reactive binding lowering rewrites only the indexed occurrences that resolve to state or capture bindings. Same-named nested parameters remain local.
43
+ - Synthesized keyed expressions use the existing fail-safe path unless the complete expression can be indexed, preserving keyed row state and identity behavior.
44
+
45
+ ### Performance And Architecture
46
+
47
+ - A focused 1,000-reference guard verifies one index construction followed by direct indexed lookups and reports construction/lookup timing without making a cross-machine performance claim.
48
+ - The index is constructed after ordered normalization and parent repair, once per compiled source. It adds no public API, browser runtime, client metadata, or JavaScript bytes.
49
+ - Native handler, effect, and remaining descriptor discovery still use the existing analysis path; symbol-aware migration of those consumers remains the next P0.2 scope.
50
+
51
+ ### Validation
52
+
53
+ - `npm run check`, `npm run test:package`, and all 185 tests pass.
54
+ - Browser integration verifies shadowed browser-global names through initial HTML and reactive evaluator updates.
55
+ - React, Router, Zustand, keyed row, nested list, effect, Worker, navigation, and static-route zero-JavaScript behavior remain covered.
56
+
57
+ ### Upgrade
58
+
59
+ ```bash
60
+ npm install @kudzujs/core@^0.8.28
61
+ ```
62
+
3
63
  ## 0.8.27 - Large-application compiler roadmap
4
64
 
5
65
  Kudzu 0.8.27 records the evidence-backed path from the current static-first compiler to large production applications without adding a React runtime, generic client framework, or speculative resource API.
@@ -11,7 +11,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
11
11
  | C: state/resource model | Research only | Reduced fixtures expose a limitation |
12
12
  | D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
13
13
 
14
- The active post-`0.8.27` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). It orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
14
+ The active post-`0.8.29` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). It orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
15
15
 
16
16
  ## Required Invariants
17
17
 
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the current `0.8.27` architecture built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
3
+ This maps the current `0.8.29` architecture built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -12,6 +12,7 @@ This maps the current `0.8.27` architecture built on the completed `0.8.23` Goal
12
12
  | Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every structural change. Imported source uses the same pipeline. |
13
13
  | Focused normalization passes | [`framework/compiler/`](../../framework/compiler/) | React, Router, browser signals, animation-frame refs, custom-hook timers, Zustand, and render control each validate and lower a narrow source shape. |
14
14
  | Shared AST/scope helpers | [`framework/compiler/ast-helpers.mjs`](../../framework/compiler/ast-helpers.mjs) | Binding, scope, reference, effect-return, and source-location analysis. |
15
+ | Source-local binding index | [`framework/compiler/analysis/binding-index.mjs`](../../framework/compiler/analysis/binding-index.mjs) | After normalization, assigns deterministic lexical slots and classifies local, parameter, import, capture, global, and unresolved references. Native handler, effect, binding, list evaluator, optimized-command, and effect-resource consumers use complete index-owned AST; synthesized expressions retain the existing fallback. |
15
16
  | Pure collection language | [`framework/compiler/collection-analysis.mjs`](../../framework/compiler/collection-analysis.mjs) | Analyzes collection roots/selectors and serializes the allowed pure expression language used by lists and derived dependencies. |
16
17
  | Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, keyed, and effect ownership results. |
17
18
  | Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Retains ordered JSON-safe owner and specialization records for state, setters, props, refs, IDs, direct signal links, and source provenance; AST identity remains private to its source-local session. |
@@ -38,7 +39,9 @@ This maps the current `0.8.27` architecture built on the completed `0.8.23` Goal
38
39
  src/pages entries + config
39
40
  -> project discovery and reachable relative graph
40
41
  -> compileSource()
41
- -> ordered normalization and main transformer semantic analysis
42
+ -> ordered normalization and parent repair
43
+ -> source-local binding index
44
+ -> main transformer semantic analysis
42
45
  -> TypeScript transpilation
43
46
  -> JSON-safe build module, ModuleIR, handler module, and imported assets
44
47
  -> build orchestration writes .kudzu executable and handler modules
@@ -64,6 +67,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
64
67
  - `build()` still owns explicit artifact selection and filesystem writes after generator results are produced.
65
68
  - Runtime generators intentionally specialize readable authored sources through exact anchors; every required anchor fails closed, but a future generator format may remove this transitional dependency.
66
69
  - Source reachability and source compilation share one module because both consume the same normalization and import graph contracts.
70
+ - Native handler, effect, state/setter, list, Router, React, and Zustand discovery still contains name-based analysis. The binding index is intentionally adopted one consumer family at a time; P0.2 moves the next descriptor consumers without changing cross-module semantics.
67
71
 
68
72
  These are future simplification opportunities, not incomplete Goal A contracts. Goal A changed no source support, browser output semantics, or browser architecture.
69
73
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Active execution plan after `0.8.27`. This document turns the current compiler audit into an ordered implementation program. It does not mark any planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
5
+ Active execution plan after `0.8.29`. This document turns the current compiler audit into an ordered implementation program. It does not mark any planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
6
6
 
7
7
  [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection. This plan is authoritative for the order and completion evidence of compiler generalization, large-application foundations, compatibility boundaries, AI tooling, and scale validation. If implementation evidence changes a boundary, update this document before broadening a patch.
8
8
 
@@ -127,6 +127,12 @@ This is an incremental evolution of the current repository:
127
127
 
128
128
  ## Priority Program
129
129
 
130
+ ### Execution Status
131
+
132
+ - [x] P0.1 Source-local binding index is complete in `0.8.28`. Reactive binding capture/import discovery and lowering use the index only when the complete expression is indexed; synthesized keyed expressions retain the existing fail-safe path. Focused scope tests, a 1,000-reference guard, browser integration, all 185 tests, and packed-package smoke pass.
133
+ - [x] P0.2 Symbol-aware descriptor discovery is complete in `0.8.29`. Native handler, effect, remaining binding, list evaluator, optimized-command, and effect-resource discovery/lowering use the source-local binding index when it owns the complete AST; synthesized trees retain the existing fail-safe path. Focused lexical-shadow, JSON-safe IR, resource-ownership checks, all 187 tests, and packed-package smoke pass.
134
+ - [ ] P0.3 Graph diagnostics is next. It must fail unresolved relative imports/re-exports and ordinary dynamic imports at the importer source location without broadening into ProjectSession or cross-module symbol work.
135
+
130
136
  ### P0: Semantic Correctness And Compiler Foundation
131
137
 
132
138
  P0 creates the semantic base required by every later large-application capability. Do not begin broad ecosystem or application features before the relevant P0 dependencies are complete.
@@ -449,4 +455,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
449
455
 
450
456
  The first implementation PR is **PR 1: Source-Local Binding Index**. It is compile-time only, adds no browser bytes or public API, fixes an existing correctness class, and creates the stable semantic foundation required by component graphs, state operations, cross-module analysis, compatibility adapters, structured diagnostics, and AI explanation tooling.
451
457
 
452
- The next PR is **PR 2: Symbol-Aware Descriptor Discovery**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
458
+ The next PR is **PR 3: Graph Failure Diagnostics**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
@@ -1,6 +1,6 @@
1
1
  # Planned Version Sequence
2
2
 
3
- This is an execution sequence, not release history. `0.8.16` through `0.8.27` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.29` are completed scopes represented by package/release records.
4
4
 
5
5
  Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
6
6
 
@@ -18,6 +18,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
18
18
  | `0.8.25` | Reuse byte-identical generated native, parameter, and effect route-entry transforms within one build and strengthen compiler/release boundaries. | Alternating 1,011-page builds improve materially with identical deploy hashes; normalization and ModuleIR fail closed; Node, Chrome, package, and registry gates pass. |
19
19
  | `0.8.26` | Harden Goal B benchmark reproducibility and ordinary regression coverage without changing runtime behavior. | Commerce comparison defaults to exact output; route transform counts and keyed bulk/fallback guards are protected by the standard suite; baseline coverage and limits are explicit. |
20
20
  | `0.8.27` | Characterize the first Goal C browser-resource boundary and publish the ordered large-application and AI-native compiler execution plan. | The E2B-derived fixture fails with a source-located diagnostic, no resource runtime or API is added, and the next PR dependencies and completion evidence are explicit. |
21
+ | `0.8.28` | Add the source-local binding index and adopt it for reactive binding capture/import discovery and lowering. | Lexical shadowing is identity-based for complete indexed bindings, synthesized keyed expressions retain their fallback, the 1,000-reference guard passes, and no browser bytes or public API are added. |
22
+ | `0.8.29` | Move native handler, effect, remaining binding, list evaluator, optimized-command, and effect-resource discovery/lowering onto source-local lexical identity. | Shadowed imports/globals/state/resources remain distinct, HandlerIR/BindingIR round trips pass, synthesized trees retain the fallback, and no runtime or public API is added. |
21
23
 
22
24
  ## Sequence Rules
23
25
 
@@ -0,0 +1,278 @@
1
+ import ts from "typescript"
2
+
3
+ export const knownGlobalNames = new Set([
4
+ "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
5
+ ])
6
+
7
+ export function createBindingIndex(sourceFile) {
8
+ const scopeByNode = new WeakMap()
9
+ const referenceBindings = new WeakMap()
10
+ const originalReferenceBindings = new WeakMap()
11
+ const bindings = []
12
+ const rootScope = createScope(undefined, sourceFile, "module")
13
+ const pseudoBindings = new Map()
14
+
15
+ collect(sourceFile, rootScope)
16
+ resolveReferences(sourceFile)
17
+
18
+ function createScope(parent, node, kind) {
19
+ return { parent, node, kind, bindings: new Map() }
20
+ }
21
+
22
+ function mark(node, scope) {
23
+ scopeByNode.set(node, scope)
24
+ }
25
+
26
+ function declare(identifier, scope, declarationKind) {
27
+ mark(identifier, scope)
28
+ let binding = scope.bindings.get(identifier.text)
29
+ if (!binding) {
30
+ binding = { slot: bindings.length, debugName: identifier.text, declarationKind, declaration: identifier }
31
+ bindings.push(binding)
32
+ scope.bindings.set(identifier.text, binding)
33
+ }
34
+ return binding
35
+ }
36
+
37
+ function declareName(name, scope, declarationKind) {
38
+ mark(name, scope)
39
+ if (ts.isIdentifier(name)) {
40
+ declare(name, scope, declarationKind)
41
+ return
42
+ }
43
+ for (const element of name.elements) if (ts.isBindingElement(element)) declareName(element.name, scope, declarationKind)
44
+ }
45
+
46
+ function collectBindingExpressions(name, scope) {
47
+ if (ts.isIdentifier(name)) return
48
+ for (const element of name.elements) if (ts.isBindingElement(element)) {
49
+ if (element.propertyName && ts.isComputedPropertyName(element.propertyName)) collect(element.propertyName.expression, scope)
50
+ if (element.initializer) collect(element.initializer, scope)
51
+ collectBindingExpressions(element.name, scope)
52
+ }
53
+ }
54
+
55
+ function nearestVarScope(scope) {
56
+ for (let current = scope; current; current = current.parent) if (["function", "module"].includes(current.kind)) return current
57
+ return rootScope
58
+ }
59
+
60
+ function collectFunction(node, scope) {
61
+ if (ts.isFunctionDeclaration(node) && node.name) declare(node.name, scope, "function")
62
+ const parameterScope = createScope(scope, node, "parameters")
63
+ const functionScope = createScope(parameterScope, node.body ?? node, "function")
64
+ mark(node, parameterScope)
65
+ if (ts.isFunctionExpression(node) && node.name) declare(node.name, parameterScope, "function-name")
66
+ for (const parameter of node.parameters) {
67
+ mark(parameter, parameterScope)
68
+ declareName(parameter.name, parameterScope, "parameter")
69
+ collectBindingExpressions(parameter.name, parameterScope)
70
+ if (parameter.initializer) collect(parameter.initializer, parameterScope)
71
+ }
72
+ if (node.body) collect(node.body, functionScope)
73
+ }
74
+
75
+ function collectClass(node, scope) {
76
+ if (ts.isClassDeclaration(node) && node.name) declare(node.name, scope, "class")
77
+ const classScope = createScope(scope, node, "class")
78
+ mark(node, classScope)
79
+ if (ts.isClassExpression(node) && node.name) declare(node.name, classScope, "class-name")
80
+ for (const heritage of node.heritageClauses ?? []) for (const type of heritage.types) collect(type.expression, classScope)
81
+ for (const member of node.members) collect(member, classScope)
82
+ }
83
+
84
+ function collectLoop(node, scope) {
85
+ const loopScope = createScope(scope, node, "block")
86
+ mark(node, loopScope)
87
+ if (node.initializer) collect(node.initializer, loopScope)
88
+ if (ts.isForStatement(node)) {
89
+ if (node.condition) collect(node.condition, loopScope)
90
+ if (node.incrementor) collect(node.incrementor, loopScope)
91
+ } else collect(node.expression, loopScope)
92
+ collect(node.statement, loopScope)
93
+ }
94
+
95
+ function collect(node, scope) {
96
+ mark(node, scope)
97
+ if (ts.isTypeNode(node)) return
98
+ if (ts.isSourceFile(node)) {
99
+ for (const statement of node.statements) collect(statement, scope)
100
+ return
101
+ }
102
+ if (ts.isImportDeclaration(node)) {
103
+ const clause = node.importClause
104
+ if (!clause || clause.isTypeOnly) return
105
+ if (clause.name) declare(clause.name, scope, "import-default")
106
+ if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) declare(clause.namedBindings.name, scope, "import-namespace")
107
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) if (!element.isTypeOnly) declare(element.name, scope, "import-named")
108
+ return
109
+ }
110
+ if (ts.isImportEqualsDeclaration(node)) {
111
+ if (!node.isTypeOnly) declare(node.name, scope, "import-equals")
112
+ return
113
+ }
114
+ if (isFunctionLike(node)) {
115
+ if (node.name && ts.isComputedPropertyName(node.name)) collect(node.name.expression, scope)
116
+ collectFunction(node, scope)
117
+ return
118
+ }
119
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
120
+ collectClass(node, scope)
121
+ return
122
+ }
123
+ if (ts.isBlock(node)) {
124
+ const blockScope = createScope(scope, node, "block")
125
+ mark(node, blockScope)
126
+ for (const statement of node.statements) collect(statement, blockScope)
127
+ return
128
+ }
129
+ if (ts.isCaseBlock(node)) {
130
+ const caseScope = createScope(scope, node, "block")
131
+ mark(node, caseScope)
132
+ for (const clause of node.clauses) collect(clause, caseScope)
133
+ return
134
+ }
135
+ if (ts.isCatchClause(node)) {
136
+ const catchScope = createScope(scope, node, "block")
137
+ mark(node, catchScope)
138
+ if (node.variableDeclaration) {
139
+ declareName(node.variableDeclaration.name, catchScope, "catch")
140
+ collectBindingExpressions(node.variableDeclaration.name, catchScope)
141
+ }
142
+ collect(node.block, catchScope)
143
+ return
144
+ }
145
+ if (ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)) {
146
+ collectLoop(node, scope)
147
+ return
148
+ }
149
+ if (ts.isVariableDeclaration(node)) {
150
+ const list = node.parent
151
+ const declarationKind = ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) ? "const" : ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Let) ? "let" : "var"
152
+ const declarationScope = declarationKind === "var" ? nearestVarScope(scope) : scope
153
+ declareName(node.name, declarationScope, declarationKind)
154
+ collectBindingExpressions(node.name, scope)
155
+ if (node.initializer) collect(node.initializer, scope)
156
+ return
157
+ }
158
+ if (ts.isEnumDeclaration(node)) declare(node.name, scope, "enum")
159
+ if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
160
+ declare(node.name, scope, "namespace")
161
+ const moduleScope = createScope(scope, node, "module")
162
+ if (node.body && ts.isModuleBlock(node.body)) {
163
+ mark(node.body, moduleScope)
164
+ for (const statement of node.body.statements) collect(statement, moduleScope)
165
+ } else if (node.body) collect(node.body, moduleScope)
166
+ return
167
+ }
168
+ ts.forEachChild(node, child => collect(child, scope))
169
+ }
170
+
171
+ function lexicalBinding(scope, name) {
172
+ for (let current = scope; current; current = current.parent) {
173
+ const binding = current.bindings.get(name)
174
+ if (binding) return binding
175
+ }
176
+ }
177
+
178
+ function pseudoBinding(kind, name) {
179
+ const key = `${kind}:${name}`
180
+ let binding = pseudoBindings.get(key)
181
+ if (!binding) {
182
+ binding = { slot: bindings.length, debugName: name, declarationKind: kind }
183
+ bindings.push(binding)
184
+ pseudoBindings.set(key, binding)
185
+ }
186
+ return binding
187
+ }
188
+
189
+ function resolveReferences(node) {
190
+ if (ts.isTypeNode(node)) return
191
+ if (ts.isIdentifier(node) && isValueReference(node)) {
192
+ const lexical = lexicalBinding(scopeByNode.get(node) ?? rootScope, node.text)
193
+ const binding = lexical ?? pseudoBinding(knownGlobalNames.has(node.text) ? "global" : "unresolved", node.text)
194
+ referenceBindings.set(node, binding)
195
+ const original = ts.getOriginalNode(node)
196
+ if (original !== node) originalReferenceBindings.set(original, binding)
197
+ }
198
+ ts.forEachChild(node, resolveReferences)
199
+ }
200
+
201
+ function resolveReference(identifier, boundary = sourceFile) {
202
+ const original = ts.getOriginalNode(identifier)
203
+ const binding = referenceBindings.get(identifier) ?? referenceBindings.get(original) ?? originalReferenceBindings.get(original)
204
+ if (!binding) return undefined
205
+ let kind
206
+ if (binding.declarationKind.startsWith("import")) kind = "import"
207
+ else if (binding.declarationKind === "global") kind = "global"
208
+ else if (binding.declarationKind === "unresolved") kind = "unresolved"
209
+ else if (inside(binding.declaration, boundary)) kind = binding.declarationKind === "parameter" ? "parameter" : "local"
210
+ else kind = "capture"
211
+ return {
212
+ kind,
213
+ slot: binding.slot,
214
+ debugName: binding.debugName,
215
+ declarationKind: binding.declarationKind,
216
+ ...(binding.declaration ? { declaration: binding.declaration, declarationRange: range(binding.declaration) } : {}),
217
+ referenceRange: range(original)
218
+ }
219
+ }
220
+
221
+ function references(root, boundary = root) {
222
+ const found = []
223
+ let complete = true
224
+ const visit = node => {
225
+ if (ts.isTypeNode(node)) return
226
+ if (ts.isIdentifier(node) && isValueReference(node)) {
227
+ const resolution = resolveReference(node, boundary)
228
+ if (resolution) found.push(resolution)
229
+ else complete = false
230
+ }
231
+ ts.forEachChild(node, visit)
232
+ }
233
+ visit(root)
234
+ return complete ? found : undefined
235
+ }
236
+
237
+ return { bindings: () => bindings.map(({ declaration, ...binding }) => ({ ...binding, ...(declaration ? { declarationRange: range(declaration) } : {}) })), hasNode: node => scopeByNode.has(node), references, resolveReference }
238
+ }
239
+
240
+ function inside(node, boundary) {
241
+ const originalBoundary = ts.getOriginalNode(boundary)
242
+ for (let current = node; current; current = current.parent) {
243
+ const original = ts.getOriginalNode(current)
244
+ if (current === boundary || current === originalBoundary || original === boundary || original === originalBoundary) return true
245
+ }
246
+ return false
247
+ }
248
+
249
+ function range(node) {
250
+ if (node.pos < 0 || node.end < 0) return undefined
251
+ return { start: node.getSourceFile() ? node.getStart() : node.pos, end: node.end }
252
+ }
253
+
254
+ function isFunctionLike(node) {
255
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)
256
+ }
257
+
258
+ function isValueReference(node) {
259
+ const parent = node.parent
260
+ if (!parent) return true
261
+ if (ts.isTypeNode(parent)) return false
262
+ if ((ts.isPropertyAccessExpression(parent) && parent.name === node)
263
+ || (ts.isPropertyAssignment(parent) && parent.name === node)
264
+ || ((ts.isMethodDeclaration(parent) || ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent) || ts.isMethodSignature(parent)) && parent.name === node)
265
+ || (ts.isVariableDeclaration(parent) && parent.name === node)
266
+ || (ts.isParameter(parent) && parent.name === node)
267
+ || ((ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(parent) || ts.isClassDeclaration(parent) || ts.isClassExpression(parent) || ts.isEnumDeclaration(parent) || ts.isModuleDeclaration(parent)) && parent.name === node)
268
+ || (ts.isJsxAttribute(parent) && parent.name === node)
269
+ || (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node))
270
+ || ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isImportEqualsDeclaration(parent)
271
+ || ts.isExportSpecifier(parent)
272
+ || ts.isLabeledStatement(parent) || ts.isBreakStatement(parent) || ts.isContinueStatement(parent)
273
+ || (ts.isEnumMember(parent) && parent.name === node)) return false
274
+ if ((ts.isTypeAliasDeclaration(parent) || ts.isInterfaceDeclaration(parent)) && parent.name === node) return false
275
+ if (ts.isJsxClosingElement(parent) && parent.tagName === node) return false
276
+ if ((ts.isJsxOpeningElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node) return node.text[0] === node.text[0].toUpperCase()
277
+ return true
278
+ }
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript"
2
2
  import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
3
+ import { knownGlobalNames } from "./analysis/binding-index.mjs"
3
4
  import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
4
5
  import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
5
6
  import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
@@ -8,7 +9,7 @@ export function createSemanticArtifact(file) {
8
9
  return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
9
10
  }
10
11
 
11
- export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
12
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, bindingIndex, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
12
13
  const { moduleIR } = semantic
13
14
  const nativeHandlers = []
14
15
  const effectHandlers = []
@@ -16,9 +17,9 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
16
17
  const listExpressions = []
17
18
  const clientModules = new Set()
18
19
 
19
- function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock) {
20
+ function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock, indexedBindingIndex) {
20
21
  const exportName = `listExpression${listExpressions.length}`
21
- listExpressions.push({ exportName, expression, item, index, states, role: "list-expression", keyedBlock })
22
+ listExpressions.push({ exportName, expression, item, index, states, role: "list-expression", keyedBlock, bindingIndex: indexedBindingIndex })
22
23
  const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
23
24
  if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
24
25
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
@@ -26,7 +27,8 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
26
27
 
27
28
  function compileListConditional(entry) {
28
29
  const exportName = `listExpression${listExpressions.length}`
29
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional", keyedBlock: entry.keyedBlock })
30
+ const indexed = indexedReferences(bindingIndex, entry.condition, entry.condition)
31
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional", keyedBlock: entry.keyedBlock, bindingIndex: indexed ? bindingIndex : undefined })
30
32
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
31
33
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
32
34
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -35,21 +37,25 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
35
37
  }
36
38
 
37
39
  function compileListValue(expression, entry) {
40
+ const indexed = indexedReferences(bindingIndex, expression, expression)
38
41
  const rewrite = node => {
39
- if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
40
- if (ts.isIdentifier(node) && entry.states?.has(node.text) && isReferenceIdentifier(node)) return factory.createPropertyAccessExpression(node, "value")
42
+ const reference = ts.isShorthandPropertyAssignment(node) ? node.name : ts.isIdentifier(node) ? node : undefined
43
+ const resolution = reference && indexed ? bindingIndex.resolveReference(reference, expression) : undefined
44
+ const state = ["capture", "unresolved"].includes(resolution?.kind) && entry.states?.has(resolution.debugName)
45
+ if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text) && (!indexed || state)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
46
+ if (ts.isIdentifier(node) && entry.states?.has(node.text) && isReferenceIdentifier(node) && (!indexed || state)) return factory.createPropertyAccessExpression(node, "value")
41
47
  return ts.visitEachChild(node, rewrite, context)
42
48
  }
43
49
  const initial = entry.states?.size ? ts.visitNode(expression, rewrite) : expression
44
50
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
45
51
  return entry.field
46
52
  ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
47
- : compileListExpression(read, expression, entry.item, entry.index, entry.states, entry.keyedBlock)
53
+ : compileListExpression(read, expression, entry.item, entry.index, entry.states, entry.keyedBlock, indexed ? bindingIndex : undefined)
48
54
  }
49
55
 
50
56
  function compileReactiveBinding(expression, { setters, importBindings = new Map(), keyedBlock }) {
51
57
  const parts = conditionalParts(expression)
52
- const state = parts && directStateIdentifier(parts.condition, setters)
58
+ const state = parts && directStateIdentifier(parts.condition, setters, bindingIndex)
53
59
  if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
54
60
  return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
55
61
  }
@@ -57,7 +63,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
57
63
  }
58
64
 
59
65
  function compileConditional(kind, expression, truthy, falsy, setters) {
60
- const state = directStateIdentifier(expression, setters)
66
+ const state = directStateIdentifier(expression, setters, bindingIndex)
61
67
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
62
68
  if (state) return factory.createCallExpression(factory.createIdentifier("__kStateConditional"), undefined, [factory.createStringLiteral(kind), state, thunk(truthy), thunk(falsy)])
63
69
  const [initial, ...descriptor] = compileReactiveExpression(expression, setters)
@@ -65,13 +71,17 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
65
71
  }
66
72
 
67
73
  function compileReactiveExpression(expression, setters, importBindings = new Map(), keyedBlock) {
68
- const usedStates = referencedStateNames(expression, setters)
69
- const importedNames = referencedImportedBindings(expression, importBindings)
74
+ const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
75
+ const indexed = indexedReferences(bindingIndex, expression, expression)
76
+ const importedNames = indexed ? new Set(indexed.filter(reference => reference.kind === "import" && importBindings.has(reference.debugName)).map(reference => reference.debugName)) : referencedImportedBindings(expression, importBindings)
70
77
  const imports = [...importedNames].map(name => importBindings.get(name))
71
78
  registerClientImports(imports)
72
- const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
79
+ const allStateNames = new Set(setters.values())
80
+ const captures = indexed
81
+ ? new Set(indexed.filter(reference => ["capture", "unresolved"].includes(reference.kind) && !setters.has(reference.debugName) && !allStateNames.has(reference.debugName) && !importedNames.has(reference.debugName)).map(reference => reference.debugName))
82
+ : new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
73
83
  const exportName = `binding${reactiveBindings.length}`
74
- reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock })
84
+ reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock, ...(indexed ? { bindingIndex } : {}) })
75
85
  const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
76
86
  const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
77
87
  const stateNames = new Set(usedStates)
@@ -108,19 +118,20 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
108
118
  }
109
119
 
110
120
  function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
111
- const allCaptures = nativeCaptureNames(expression, setters)
112
- const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
113
- const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
121
+ const indexedBindingIndex = indexedReferences(bindingIndex, expression, expression) ? bindingIndex : undefined
122
+ const allCaptures = nativeCaptureNames(expression, setters, indexedBindingIndex)
123
+ const usedReducers = referencedReducerDispatches(expression.body, reducers, expression, indexedBindingIndex)
124
+ const imports = [...referencedImportedBindings(expression, importBindings, indexedBindingIndex)].map(name => importBindings.get(name))
114
125
  imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
115
126
  const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
116
127
  registerClientImports(imports)
117
- const usedStates = referencedStateNames(expression.body, setters, expression)
128
+ const usedStates = referencedStateNames(expression.body, setters, expression, indexedBindingIndex)
118
129
  for (const name of usedReducers) {
119
130
  const reducer = reducers.get(name)
120
- if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
131
+ if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction, bindingIndex)) usedStates.add(state)
121
132
  }
122
133
  const exportName = `${prefix}${entries.length}`
123
- entries.push({ exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates })
134
+ entries.push({ exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates, bindingIndex: indexedBindingIndex })
124
135
  const value = name => deferValues
125
136
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
126
137
  : factory.createIdentifier(name)
@@ -138,7 +149,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
138
149
 
139
150
  function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
140
151
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
141
- const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
152
+ const commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
142
153
  if (!commands.length || commands.some(command => !command)) return undefined
143
154
  const original = ts.getOriginalNode(expression)
144
155
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
@@ -147,6 +158,17 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
147
158
  return generateCommandBehavior(moduleIR, handler, factory)
148
159
  }
149
160
 
161
+ function canSpecializeCommand(command, expression, setters) {
162
+ const references = indexedReferences(bindingIndex, command, expression)
163
+ if (!references) return true
164
+ const stateNames = new Set(setters.values())
165
+ return references.every(reference => {
166
+ if (setters.has(reference.debugName) || stateNames.has(reference.debugName)) return ["capture", "unresolved"].includes(reference.kind)
167
+ if (reference.debugName === "console") return reference.kind === "global"
168
+ return true
169
+ })
170
+ }
171
+
150
172
  function registerClientImports(imports) {
151
173
  for (const entry of imports) if (!entry.package) clientModules.add(entry.target)
152
174
  }
@@ -236,9 +258,12 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
236
258
  return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult }
237
259
  }
238
260
 
239
- function directStateIdentifier(expression, setters) {
261
+ function directStateIdentifier(expression, setters, bindingIndex) {
240
262
  const value = unwrapExpression(expression)
241
- return ts.isIdentifier(value) && new Set(setters.values()).has(value.text) ? value : undefined
263
+ if (!ts.isIdentifier(value) || !new Set(setters.values()).has(value.text)) return undefined
264
+ const indexed = bindingIndex?.hasNode(expression) ? bindingIndex : undefined
265
+ const resolution = indexed?.resolveReference(value, expression)
266
+ return !indexed || ["capture", "unresolved"].includes(resolution?.kind) ? value : undefined
242
267
  }
243
268
 
244
269
  function conditionalParts(expression) {
@@ -249,7 +274,9 @@ function conditionalParts(expression) {
249
274
  return undefined
250
275
  }
251
276
 
252
- export function referencedReducerDispatches(root, reducers, scopeRoot = root) {
277
+ export function referencedReducerDispatches(root, reducers, scopeRoot = root, bindingIndex) {
278
+ const indexed = indexedReferences(bindingIndex, root, scopeRoot)
279
+ if (indexed) return new Set(indexed.filter(reference => ["capture", "unresolved"].includes(reference.kind) && reducers.has(reference.debugName)).map(reference => reference.debugName))
253
280
  const used = new Set()
254
281
  const visit = node => {
255
282
  if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(node.text)
@@ -259,8 +286,10 @@ export function referencedReducerDispatches(root, reducers, scopeRoot = root) {
259
286
  return used
260
287
  }
261
288
 
262
- export function referencedStateNames(root, setters, scopeRoot = root) {
289
+ export function referencedStateNames(root, setters, scopeRoot = root, bindingIndex) {
263
290
  const stateNames = new Set(setters.values())
291
+ const indexed = indexedReferences(bindingIndex, root, scopeRoot)
292
+ if (indexed) return new Set(indexed.filter(reference => ["capture", "unresolved"].includes(reference.kind) && (setters.has(reference.debugName) || stateNames.has(reference.debugName))).map(reference => setters.get(reference.debugName) ?? reference.debugName))
264
293
  const used = new Set()
265
294
  const visit = node => {
266
295
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, scopeRoot)) used.add(setters.get(node.expression.text))
@@ -272,11 +301,13 @@ export function referencedStateNames(root, setters, scopeRoot = root) {
272
301
  return used
273
302
  }
274
303
 
275
- export function nativeCaptureNames(expression, setters) {
276
- return captureNames(expression, expression.body, setters)
304
+ export function nativeCaptureNames(expression, setters, bindingIndex) {
305
+ return captureNames(expression, expression.body, setters, bindingIndex)
277
306
  }
278
307
 
279
- function referencedImportedBindings(expression, imports) {
308
+ function referencedImportedBindings(expression, imports, bindingIndex) {
309
+ const indexed = indexedReferences(bindingIndex, expression.body ?? expression, expression)
310
+ if (indexed) return new Set(indexed.filter(reference => reference.kind === "import" && imports.has(reference.debugName)).map(reference => reference.debugName))
280
311
  const names = new Set()
281
312
  const visit = node => {
282
313
  if (ts.isIdentifier(node) && imports.has(node.text) && isReferenceIdentifier(node)) names.add(node.text)
@@ -286,7 +317,12 @@ function referencedImportedBindings(expression, imports) {
286
317
  return names
287
318
  }
288
319
 
289
- export function captureNames(declarationRoot, referenceRoot, setters) {
320
+ export function captureNames(declarationRoot, referenceRoot, setters, bindingIndex) {
321
+ const indexed = indexedReferences(bindingIndex, referenceRoot, declarationRoot)
322
+ if (indexed) {
323
+ const stateNames = new Set(setters.values())
324
+ return new Set(indexed.filter(reference => ["capture", "unresolved"].includes(reference.kind) && !setters.has(reference.debugName) && !stateNames.has(reference.debugName)).map(reference => reference.debugName))
325
+ }
290
326
  const local = new Set()
291
327
  if (!isFunctionLike(declarationRoot)) {
292
328
  const collectDeclarations = node => {
@@ -303,7 +339,7 @@ export function captureNames(declarationRoot, referenceRoot, setters) {
303
339
  if (ts.isTypeNode(node)) return
304
340
  if (ts.isIdentifier(node)) {
305
341
  const declared = isFunctionLike(declarationRoot) ? isShadowedIdentifier(node, declarationRoot) : local.has(node.text)
306
- if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !nativeGlobals.has(node.text)) captures.add(node.text)
342
+ if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !knownGlobalNames.has(node.text)) captures.add(node.text)
307
343
  }
308
344
  ts.forEachChild(node, visit)
309
345
  }
@@ -311,6 +347,6 @@ export function captureNames(declarationRoot, referenceRoot, setters) {
311
347
  return captures
312
348
  }
313
349
 
314
- const nativeGlobals = new Set([
315
- "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
316
- ])
350
+ function indexedReferences(bindingIndex, root, boundary) {
351
+ return bindingIndex?.hasNode(root) && bindingIndex.hasNode(boundary) ? bindingIndex.references(root, boundary) : undefined
352
+ }
@@ -3,7 +3,7 @@ import { nearestFunction, referencesIdentifier, unwrapExpression } from "./ast-h
3
3
  import { collectionExpression } from "./collection-analysis.mjs"
4
4
  import { referencedStateNames } from "./descriptor-session.mjs"
5
5
 
6
- export function analyzeEffectDependencies({ dependencies, node, listEffect, keyedItem, setters, localDeclarations, factory, fail }) {
6
+ export function analyzeEffectDependencies({ dependencies, node, listEffect, keyedItem, setters, localDeclarations, factory, fail, bindingIndex }) {
7
7
  const itemDependencies = []
8
8
  const ordinaryDependencies = []
9
9
  let dependencyItem = listEffect ? keyedItem : undefined
@@ -32,8 +32,9 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
32
32
  for (const dependency of ordinaryDependencies) {
33
33
  const declarations = localDeclarations?.get(dependency.text)
34
34
  const initializer = declarations?.length === 1 ? declarations[0].initializer : undefined
35
- const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text)
36
- const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
35
+ const indexedInitializer = initializer && bindingIndex?.hasNode(initializer) ? bindingIndex : undefined
36
+ const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text) && (!indexedInitializer || ["capture", "unresolved"].includes(indexedInitializer.resolveReference(unwrapExpression(initializer), initializer)?.kind))
37
+ const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters, initializer, bindingIndex) : new Set()
37
38
  if (derivedStates.size) {
38
39
  const usedStates = new Set()
39
40
  const expression = collectionExpression(initializer, { fail, stateNames, selectorStates: usedStates })
@@ -55,7 +56,8 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
55
56
  return { dependencyItem, itemDependencies, ordinaryDependencies, entries, dependencyStates, substitutions, subscriptions, hasDerived }
56
57
  }
57
58
 
58
- export function validateEffectOwnedBrowserResources(callback, returns, fail) {
59
+ export function validateEffectOwnedBrowserResources(callback, returns, fail, bindingIndex) {
60
+ bindingIndex = bindingIndex?.hasNode(callback) ? bindingIndex : undefined
59
61
  const observers = []
60
62
  const frameAssignments = []
61
63
  const cancellations = new Set()
@@ -64,18 +66,20 @@ export function validateEffectOwnedBrowserResources(callback, returns, fail) {
64
66
  for (let current = node; current; current = current.parent) if (current === cleanup) return true
65
67
  return false
66
68
  })
69
+ const isGlobal = (identifier, name) => identifier.text === name && (!bindingIndex || bindingIndex.resolveReference(identifier, callback)?.kind === "global")
70
+ const resource = identifier => bindingIndex?.resolveReference(identifier, callback)?.declaration ?? identifier.text
67
71
  const visit = node => {
68
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
69
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
70
- if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
71
- if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
72
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && isGlobal(unwrapExpression(node.initializer).expression, "IntersectionObserver")) observers.push(node)
73
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && isGlobal(unwrapExpression(node.right).expression, "requestAnimationFrame")) frameAssignments.push(node)
74
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && isGlobal(node.expression, "cancelAnimationFrame") && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(resource(unwrapExpression(node.arguments[0])))
75
+ if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(resource(node.expression.expression))
72
76
  ts.forEachChild(node, visit)
73
77
  }
74
78
  visit(callback.body)
75
- for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
79
+ for (const observer of observers) if (!disconnected.has(bindingIndex ? observer.name : observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
76
80
  for (const assignment of frameAssignments) {
77
- const name = unwrapExpression(assignment.left).text
78
- if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
81
+ const left = unwrapExpression(assignment.left)
82
+ if (!cancellations.has(resource(left))) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(left.text)} in cleanup`)
79
83
  }
80
84
  }
81
85
 
@@ -4,16 +4,16 @@ import { isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowe
4
4
  export function createHandlerLowering({ cloneAst, synthesizeTree }) {
5
5
  return { lowerListExpression: printListExpression, lowerNativeHandler: printNativeHandler, lowerReactiveBinding: printReactiveBinding }
6
6
 
7
- function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set() }) {
7
+ function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set(), bindingIndex }) {
8
8
  const factory = ts.factory
9
9
  const stateNames = new Set(setters.values())
10
- const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates) : new Set()
10
+ const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates, bindingIndex) : new Set()
11
11
  const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
12
- const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures) : new Set()
12
+ const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures, bindingIndex) : new Set()
13
13
  const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
14
14
  const transformer = context => root => {
15
15
  const visitor = node => {
16
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
16
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && matchesExternalReference(node.expression, expression, bindingIndex)) {
17
17
  const reducer = reducers.get(node.expression.text)
18
18
  if (reducer.contextAction) {
19
19
  const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
@@ -25,34 +25,34 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
25
25
  if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
26
26
  return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
27
27
  }
28
- if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
28
+ if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
29
29
  if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
30
30
  if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
31
31
  return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
32
32
  }
33
- if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
33
+ if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && matchesExternalReference(node, expression, bindingIndex)) {
34
34
  if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
35
35
  if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
36
36
  return reducerReference(factory, reducers.get(node.text))
37
37
  }
38
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
38
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && matchesExternalReference(node.expression, expression, bindingIndex)) {
39
39
  return factory.createCallExpression(
40
40
  factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
41
41
  undefined,
42
42
  [factory.createStringLiteral(setters.get(node.expression.text)), ...node.arguments.map(argument => ts.visitNode(argument, visitor))]
43
43
  )
44
44
  }
45
- if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
45
+ if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
46
46
  return factory.createPropertyAssignment(node.name, setterReference(factory, setters.get(node.name.text)))
47
47
  }
48
- if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
48
+ if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && matchesExternalReference(node, expression, bindingIndex)) {
49
49
  return setterReference(factory, setters.get(node.text))
50
50
  }
51
- if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
51
+ if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
52
52
  if (snapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, snapshots.get(node.name.text))
53
53
  return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
54
54
  }
55
- if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
55
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && matchesExternalReference(node, expression, bindingIndex)) {
56
56
  if (snapshots.has(node.text) && insideNestedFunction(node, expression)) return snapshots.get(node.text)
57
57
  return factory.createCallExpression(
58
58
  factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
@@ -60,11 +60,11 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
60
60
  [factory.createStringLiteral(node.text)]
61
61
  )
62
62
  }
63
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
63
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
64
64
  if (captureSnapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, captureSnapshots.get(node.name.text))
65
65
  return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
66
66
  }
67
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
67
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && matchesExternalReference(node, expression, bindingIndex)) {
68
68
  if (captureSnapshots.has(node.text) && insideNestedFunction(node, expression)) return captureSnapshots.get(node.text)
69
69
  return scopeRead(factory, node.text)
70
70
  }
@@ -106,21 +106,21 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
106
106
  }
107
107
  }
108
108
 
109
- function nestedCaptureNames(expression, captures) {
109
+ function nestedCaptureNames(expression, captures, bindingIndex) {
110
110
  const names = new Set()
111
111
  const visit = node => {
112
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
112
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && matchesExternalReference(node, expression, bindingIndex)) names.add(node.text)
113
113
  ts.forEachChild(node, visit)
114
114
  }
115
115
  visit(expression.body)
116
116
  return names
117
117
  }
118
118
 
119
- function nestedStateNames(expression, setters, liveStates = new Set()) {
119
+ function nestedStateNames(expression, setters, liveStates = new Set(), bindingIndex) {
120
120
  const states = new Set(setters.values())
121
121
  const names = new Set()
122
122
  const visit = node => {
123
- if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
123
+ if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && matchesExternalReference(node, expression, bindingIndex)) names.add(node.text)
124
124
  ts.forEachChild(node, visit)
125
125
  }
126
126
  visit(expression.body)
@@ -134,6 +134,11 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
134
134
  return false
135
135
  }
136
136
 
137
+ function matchesExternalReference(node, boundary, bindingIndex) {
138
+ if (!bindingIndex?.hasNode(node)) return !isShadowedIdentifier(node, boundary)
139
+ return ["capture", "unresolved"].includes(bindingIndex.resolveReference(node, boundary)?.kind)
140
+ }
141
+
137
142
  function setterReference(factory, stateName) {
138
143
  return factory.createArrowFunction(
139
144
  undefined,
@@ -189,11 +194,15 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
189
194
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
190
195
  }
191
196
 
192
- function printReactiveBinding({ exportName, expression, captures, states }) {
197
+ function printReactiveBinding({ exportName, expression, captures, states, bindingIndex }) {
193
198
  const factory = ts.factory
194
199
  const transformer = context => root => {
195
200
  const visitor = node => {
196
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
201
+ const reference = ts.isShorthandPropertyAssignment(node) ? node.name : ts.isIdentifier(node) ? node : undefined
202
+ const resolution = reference ? bindingIndex?.resolveReference(reference, expression) : undefined
203
+ const indexedState = ["capture", "unresolved"].includes(resolution?.kind) && states.has(resolution.debugName)
204
+ const indexedCapture = ["capture", "unresolved"].includes(resolution?.kind) && captures.has(resolution.debugName)
205
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text) && (!bindingIndex || indexedState)) {
197
206
  return factory.createPropertyAssignment(
198
207
  node.name,
199
208
  factory.createCallExpression(
@@ -203,17 +212,17 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
203
212
  )
204
213
  )
205
214
  }
206
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
215
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && (bindingIndex ? indexedState : !isShadowedByParameter(node, expression))) {
207
216
  return factory.createCallExpression(
208
217
  factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
209
218
  undefined,
210
219
  [factory.createStringLiteral(node.text)]
211
220
  )
212
221
  }
213
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
222
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && (!bindingIndex || indexedCapture)) {
214
223
  return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
215
224
  }
216
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
225
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && (!bindingIndex || indexedCapture)) {
217
226
  return scopeRead(factory, node.text)
218
227
  }
219
228
  return ts.visitEachChild(node, visitor, context)
@@ -237,14 +246,14 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
237
246
  }
238
247
  }
239
248
 
240
- function printListExpression({ exportName, expression, item, index, states = new Set() }) {
249
+ function printListExpression({ exportName, expression, item, index, states = new Set(), bindingIndex }) {
241
250
  const factory = ts.factory
242
251
  const transformer = context => root => {
243
252
  const visitor = node => {
244
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
253
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
245
254
  return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
246
255
  }
247
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
256
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && (bindingIndex ? matchesExternalReference(node, expression, bindingIndex) : !isShadowedByParameter(node, expression))) {
248
257
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
249
258
  }
250
259
  return ts.visitEachChild(node, visitor, context)
@@ -1,6 +1,7 @@
1
1
  import { readFile, realpath, stat } from "node:fs/promises"
2
2
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
3
3
  import ts from "typescript"
4
+ import { createBindingIndex } from "./analysis/binding-index.mjs"
4
5
  import { createComponentAnalysisSession } from "./analysis/component-analysis.mjs"
5
6
  import { normalizeEffectAnimationFrameRefs } from "./animation-frame-pass.mjs"
6
7
  import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
@@ -248,6 +249,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
248
249
  const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex })
249
250
  sourceFile = normalized.sourceFile
250
251
  const { customHookTimerStates } = normalized
252
+ const bindingIndex = createBindingIndex(sourceFile)
251
253
  const factory = context.factory
252
254
  const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
253
255
  const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
@@ -256,6 +258,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
256
258
  handlerUrl,
257
259
  factory,
258
260
  context,
261
+ bindingIndex,
259
262
  compileEventCommand,
260
263
  handlerLowering,
261
264
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
@@ -792,9 +795,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
792
795
  const expanded = substituteClone(expression, substitutions, factory, context)
793
796
  ts.setParentRecursive(expanded, false)
794
797
  expanded.parent = expression.parent
795
- const usedStates = referencedStateNames(expanded, setters)
798
+ const usedStates = referencedStateNames(expanded, setters, expanded, bindingIndex)
796
799
  if (!usedStates.size) return expression
797
- const captures = captureNames(expanded, expanded, setters)
800
+ const captures = captureNames(expanded, expanded, setters, bindingIndex)
798
801
  const allowedNames = new Set([...setters.values(), ...captures])
799
802
  validateReactiveJsxExpression(expanded, allowedNames)
800
803
  return expanded
@@ -819,7 +822,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
819
822
  const signals = new Set()
820
823
  if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
821
824
  const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
822
- for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
825
+ for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression, bindingIndex)) signals.add(state)
823
826
  return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
824
827
  }
825
828
  result.analysis = componentAnalysis.registerSpecialization({
@@ -1368,7 +1371,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1368
1371
  calculation.parent = callback
1369
1372
  validateListExpression(calculation, parts.item, originalParts.root, fail)
1370
1373
  }
1371
- const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
1374
+ const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex)
1372
1375
  preparedRenderedLists.push({ node, parts, analysis })
1373
1376
  }
1374
1377
 
@@ -1376,8 +1379,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1376
1379
  const parts = conditionalParts(expression)
1377
1380
  if (!parts) return ts.visitNode(expression, visitor)
1378
1381
  const setters = settersForNode(anchor, settersByFunction)
1379
- const usedStates = referencedStateNames(parts.condition, setters)
1380
- const captures = captureNames(parts.condition, parts.condition, setters)
1382
+ const usedStates = referencedStateNames(parts.condition, setters, parts.condition, bindingIndex)
1383
+ const captures = captureNames(parts.condition, parts.condition, setters, bindingIndex)
1381
1384
  if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
1382
1385
  usesBehavior = true
1383
1386
  usesConditional = true
@@ -1531,7 +1534,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1531
1534
  setters,
1532
1535
  localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
1533
1536
  factory,
1534
- fail: effectFail
1537
+ fail: effectFail,
1538
+ bindingIndex
1535
1539
  })
1536
1540
  const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
1537
1541
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
@@ -1556,7 +1560,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1556
1560
  const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
1557
1561
  if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
1558
1562
  if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
1559
- validateEffectOwnedBrowserResources(callback, returns, effectFail)
1563
+ validateEffectOwnedBrowserResources(callback, returns, effectFail, bindingIndex)
1560
1564
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
1561
1565
  const callbackFile = callbackSource.fileName
1562
1566
  let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
@@ -1685,8 +1689,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1685
1689
  }
1686
1690
  const setters = settersForNode(node, settersByFunction)
1687
1691
  const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
1688
- const usedStates = referencedStateNames(expression, setters)
1689
- const captures = captureNames(expression, expression, setters)
1692
+ const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
1693
+ const captures = captureNames(expression, expression, setters, bindingIndex)
1690
1694
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
1691
1695
  usesBehavior = true
1692
1696
  usesBinding = true
@@ -1698,8 +1702,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1698
1702
  const sourceExpression = node.initializer.expression
1699
1703
  const setters = settersForNode(node, settersByFunction)
1700
1704
  const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
1701
- const usedStates = referencedStateNames(expression, setters)
1702
- const captures = captureNames(expression, expression, setters)
1705
+ const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
1706
+ const captures = captureNames(expression, expression, setters, bindingIndex)
1703
1707
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
1704
1708
  usesBehavior = true
1705
1709
  usesBinding = true
@@ -1952,7 +1956,7 @@ function insideJsxEventHandler(node, root) {
1952
1956
  return false
1953
1957
  }
1954
1958
 
1955
- function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
1959
+ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex) {
1956
1960
  const fail = (node, message) => {
1957
1961
  throw sourceNodeError(node, sourceFile, message)
1958
1962
  }
@@ -2013,7 +2017,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2013
2017
  calculation.parent = callback
2014
2018
  validateListExpression(calculation, nested.item, nested.root, fail)
2015
2019
  }
2016
- const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2020
+ const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex)
2017
2021
  analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
2018
2022
  return
2019
2023
  }
@@ -2041,7 +2045,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2041
2045
  return
2042
2046
  }
2043
2047
  if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
2044
- const states = referencedStateNames(expression, setters)
2048
+ const states = referencedStateNames(expression, setters, expression, bindingIndex)
2045
2049
  for (const rowState of rowStates) states.delete(rowState.state)
2046
2050
  if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
2047
2051
  validateListExpression(expression, item, node, fail, parts.index, states)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.27",
3
+ "version": "0.8.29",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",