@kudzujs/core 0.8.27 → 0.8.28

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.28` 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,12 @@ 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
+
273
279
  ## Cross-Cutting Performance Gates
274
280
 
275
281
  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.28 - Source-local binding index.** The first large-application compiler foundation classifies locals, parameters, imports, captures, globals, and unresolved references by lexical identity, then uses that index for reactive binding capture/import discovery and lowering. Read the [release notes](./RELEASES.md#0828---source-local-binding-index), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.28), 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,35 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.28 - Source-local binding index
4
+
5
+ Kudzu 0.8.28 completes the first large-application compiler foundation by resolving source-local lexical bindings before descriptor discovery and lowering.
6
+
7
+ ### Changed in 0.8.28
8
+
9
+ - 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.
10
+ - 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.
11
+ - 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.
12
+ - Reactive binding lowering rewrites only the indexed occurrences that resolve to state or capture bindings. Same-named nested parameters remain local.
13
+ - Synthesized keyed expressions use the existing fail-safe path unless the complete expression can be indexed, preserving keyed row state and identity behavior.
14
+
15
+ ### Performance And Architecture
16
+
17
+ - 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.
18
+ - 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.
19
+ - 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.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm run test:package`, and all 185 tests pass.
24
+ - Browser integration verifies shadowed browser-global names through initial HTML and reactive evaluator updates.
25
+ - React, Router, Zustand, keyed row, nested list, effect, Worker, navigation, and static-route zero-JavaScript behavior remain covered.
26
+
27
+ ### Upgrade
28
+
29
+ ```bash
30
+ npm install @kudzujs/core@^0.8.28
31
+ ```
32
+
3
33
  ## 0.8.27 - Large-application compiler roadmap
4
34
 
5
35
  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.28` 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.28` 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. Reactive binding discovery/lowering consumes complete indexed expressions; 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.28`. 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,11 @@ 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
+ - [ ] P0.2 Symbol-aware descriptor discovery is next. It must migrate native handler, effect, and remaining binding discovery without broadening into cross-module symbols, semantic StateWrite IR, or ecosystem features.
134
+
130
135
  ### P0: Semantic Correctness And Compiler Foundation
131
136
 
132
137
  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.
@@ -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.28` 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,7 @@ 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. |
21
22
 
22
23
  ## Sequence Rules
23
24
 
@@ -0,0 +1,273 @@
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, ts.getOriginalNode(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) } : {}) })), references, resolveReference }
238
+ }
239
+
240
+ function inside(node, boundary) {
241
+ for (let current = node; current; current = current.parent) if (current === boundary) return true
242
+ return false
243
+ }
244
+
245
+ function range(node) {
246
+ return node.pos >= 0 && node.end >= 0 ? { start: node.getStart(), end: node.end } : undefined
247
+ }
248
+
249
+ function isFunctionLike(node) {
250
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)
251
+ }
252
+
253
+ function isValueReference(node) {
254
+ const parent = node.parent
255
+ if (!parent) return true
256
+ if (ts.isTypeNode(parent)) return false
257
+ if ((ts.isPropertyAccessExpression(parent) && parent.name === node)
258
+ || (ts.isPropertyAssignment(parent) && parent.name === node)
259
+ || ((ts.isMethodDeclaration(parent) || ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent) || ts.isMethodSignature(parent)) && parent.name === node)
260
+ || (ts.isVariableDeclaration(parent) && parent.name === node)
261
+ || (ts.isParameter(parent) && parent.name === node)
262
+ || ((ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(parent) || ts.isClassDeclaration(parent) || ts.isClassExpression(parent) || ts.isEnumDeclaration(parent) || ts.isModuleDeclaration(parent)) && parent.name === node)
263
+ || (ts.isJsxAttribute(parent) && parent.name === node)
264
+ || (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node))
265
+ || ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isImportEqualsDeclaration(parent)
266
+ || ts.isExportSpecifier(parent)
267
+ || ts.isLabeledStatement(parent) || ts.isBreakStatement(parent) || ts.isContinueStatement(parent)
268
+ || (ts.isEnumMember(parent) && parent.name === node)) return false
269
+ if ((ts.isTypeAliasDeclaration(parent) || ts.isInterfaceDeclaration(parent)) && parent.name === node) return false
270
+ if (ts.isJsxClosingElement(parent) && parent.tagName === node) return false
271
+ if ((ts.isJsxOpeningElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node) return node.text[0] === node.text[0].toUpperCase()
272
+ return true
273
+ }
@@ -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 = []
@@ -66,12 +67,16 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
66
67
 
67
68
  function compileReactiveExpression(expression, setters, importBindings = new Map(), keyedBlock) {
68
69
  const usedStates = referencedStateNames(expression, setters)
69
- const importedNames = referencedImportedBindings(expression, importBindings)
70
+ const indexed = bindingIndex?.references(expression, expression)
71
+ const importedNames = indexed ? new Set(indexed.filter(reference => reference.kind === "import" && importBindings.has(reference.debugName)).map(reference => reference.debugName)) : referencedImportedBindings(expression, importBindings)
70
72
  const imports = [...importedNames].map(name => importBindings.get(name))
71
73
  registerClientImports(imports)
72
- const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
74
+ const allStateNames = new Set(setters.values())
75
+ const captures = indexed
76
+ ? 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))
77
+ : new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
73
78
  const exportName = `binding${reactiveBindings.length}`
74
- reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock })
79
+ reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock, ...(indexed ? { bindingIndex } : {}) })
75
80
  const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
76
81
  const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
77
82
  const stateNames = new Set(usedStates)
@@ -303,14 +308,10 @@ export function captureNames(declarationRoot, referenceRoot, setters) {
303
308
  if (ts.isTypeNode(node)) return
304
309
  if (ts.isIdentifier(node)) {
305
310
  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)
311
+ if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !knownGlobalNames.has(node.text)) captures.add(node.text)
307
312
  }
308
313
  ts.forEachChild(node, visit)
309
314
  }
310
315
  visit(referenceRoot)
311
316
  return captures
312
317
  }
313
-
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
- ])
@@ -189,11 +189,15 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
189
189
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
190
190
  }
191
191
 
192
- function printReactiveBinding({ exportName, expression, captures, states }) {
192
+ function printReactiveBinding({ exportName, expression, captures, states, bindingIndex }) {
193
193
  const factory = ts.factory
194
194
  const transformer = context => root => {
195
195
  const visitor = node => {
196
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
196
+ const reference = ts.isShorthandPropertyAssignment(node) ? node.name : ts.isIdentifier(node) ? node : undefined
197
+ const resolution = reference ? bindingIndex?.resolveReference(reference, expression) : undefined
198
+ const indexedState = resolution?.kind === "capture" && states.has(resolution.debugName)
199
+ const indexedCapture = ["capture", "unresolved"].includes(resolution?.kind) && captures.has(resolution.debugName)
200
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text) && (!bindingIndex || indexedState)) {
197
201
  return factory.createPropertyAssignment(
198
202
  node.name,
199
203
  factory.createCallExpression(
@@ -203,17 +207,17 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
203
207
  )
204
208
  )
205
209
  }
206
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
210
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && (bindingIndex ? indexedState : !isShadowedByParameter(node, expression))) {
207
211
  return factory.createCallExpression(
208
212
  factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
209
213
  undefined,
210
214
  [factory.createStringLiteral(node.text)]
211
215
  )
212
216
  }
213
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
217
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && (!bindingIndex || indexedCapture)) {
214
218
  return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
215
219
  }
216
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
220
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && (!bindingIndex || indexedCapture)) {
217
221
  return scopeRead(factory, node.text)
218
222
  }
219
223
  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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.27",
3
+ "version": "0.8.28",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",