@kudzujs/core 0.8.28 → 0.8.30

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.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.
5
+ The executable post-`0.8.30` 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
 
@@ -276,6 +276,18 @@ This queue orders the next investigations by general migration value. Start only
276
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
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
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
+
285
+ ### Completed In 0.8.30
286
+
287
+ - Reachable ordinary source validates relative runtime imports and re-exports before compilation and reports unresolved edges at the original importer file, line, column, and specifier.
288
+ - Ordinary dynamic `import()` fails during graph discovery rather than surviving into generated `.kudzu` modules; relative, package, template, and computed forms share one source-located boundary.
289
+ - Ordinary and Worker graph ownership remain separate, while type-only and unreachable source stay excluded. Export-name validation, ProjectSession, runtime behavior, and public APIs remain unchanged.
290
+
279
291
  ## Cross-Cutting Performance Gates
280
292
 
281
293
  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.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).
17
+ **Latest release: 0.8.30 - Graph failure diagnostics.** Reachable ordinary modules now validate relative runtime imports and re-exports and reject dynamic `import()` at the original importer location before generated module loading. Worker ownership, type-only edges, and unreachable source remain distinct. Read the [release notes](./RELEASES.md#0830---graph-failure-diagnostics), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.30), 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.30 - Graph failure diagnostics
4
+
5
+ Kudzu 0.8.30 completes P0.3 by making ordinary source-graph failures stop at the original importer before compilation, generated module loading, or `.kudzu` paths can obscure the source error.
6
+
7
+ ### Changed in 0.8.30
8
+
9
+ - Reachability validates every relative runtime import and re-export in an ordinary reachable module instead of silently dropping unresolved edges.
10
+ - Missing bound imports, side-effect imports, reachable helper imports, named/default forwarding, and `export *` report the importer file, line, column, and written specifier.
11
+ - Every ordinary dynamic `import()` form is rejected during graph discovery, including relative, package, template-literal, and computed specifiers.
12
+ - Ordinary and Worker traversal retain separate ownership, so Worker-only modules continue through Worker-specific graph diagnostics and cleanup rules.
13
+ - Type-only edges and unreachable migration source remain excluded and cannot block a build.
14
+
15
+ ### Compiler Boundary
16
+
17
+ - `ordinaryRuntimeDependencies()` is the single pre-codegen validator for ordinary runtime graph edges; downstream import rewriting remains a fail-closed assertion.
18
+ - This release validates whether a relative runtime edge resolves to exactly one TypeScript file. Whether an existing target exports a requested name remains deferred to the planned module-symbol graph.
19
+ - P0.3 adds no accepted source syntax, ProjectSession, export cache, public API, browser runtime, or generated capability bytes. P0.4 async native-handler invalidation is next.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm test`, `npm run test:package`, and all 189 tests pass.
24
+ - Focused in-memory and build fixtures cover missing page imports, reachable helper imports, re-exports, relative/package/computed dynamic imports, type-only exclusion, unreachable exclusion, and absence of generated `.kudzu` paths.
25
+ - Existing Worker graph diagnostics, React/Router/Zustand migrations, keyed ownership, effects, navigation, and static-route zero-JavaScript behavior remain covered.
26
+
27
+ ### Upgrade
28
+
29
+ ```bash
30
+ npm install @kudzujs/core@^0.8.30
31
+ ```
32
+
33
+ ## 0.8.29 - Symbol-aware descriptor discovery
34
+
35
+ 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.
36
+
37
+ ### Changed in 0.8.29
38
+
39
+ - 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.
40
+ - 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.
41
+ - Reactive conditionals, bindings, keyed-list evaluators, and list state rewrites distinguish outer state from same-named nested callback parameters.
42
+ - 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.
43
+ - Effect resource validation distinguishes real `IntersectionObserver`, `requestAnimationFrame()`, and `cancelAnimationFrame()` globals from shadowed application bindings and matches cleanup to the exact observer or frame declaration.
44
+
45
+ ### Compiler Boundary
46
+
47
+ - The binding index now recognizes normalized and original TypeScript node boundaries and exposes whether it directly owns an AST node.
48
+ - Imported, specialized, or compiler-synthesized trees continue through the established conservative shadow walker rather than borrowing an unrelated source index.
49
+ - P0.2 changes no public API, accepted source syntax, ModuleIR shape, browser runtime, or generated capability contract. P0.3 graph failure diagnostics is next.
50
+
51
+ ### Validation
52
+
53
+ - `npm run check`, `npm test`, `npm run test:package`, and all 187 tests pass.
54
+ - Focused tests cover shadowed imports, browser globals, state, setters, list parameters, JSON-safe HandlerIR/BindingIR round trips, and same-named effect resource cleanup.
55
+ - Existing Context actions, keyed component callbacks, React Router search parameters, native events, effects, Workers, navigation, and static-route zero-JavaScript behavior remain covered.
56
+
57
+ ### Upgrade
58
+
59
+ ```bash
60
+ npm install @kudzujs/core@^0.8.29
61
+ ```
62
+
3
63
  ## 0.8.28 - Source-local binding index
4
64
 
5
65
  Kudzu 0.8.28 completes the first large-application compiler foundation by resolving source-local lexical bindings before descriptor discovery and lowering.
@@ -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.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.
14
+ The active post-`0.8.30` 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.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.
3
+ This maps the current `0.8.30` 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
 
@@ -8,11 +8,11 @@ This maps the current `0.8.28` architecture built on the completed `0.8.23` Goal
8
8
  |---|---|---|
9
9
  | CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
10
10
  | Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteIR rendering, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
11
- | Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, and excludes unreachable migration source. |
11
+ | Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `ordinaryRuntimeDependencies()`, `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, excludes unreachable migration source, and fails unresolved ordinary edges or dynamic imports at the importer source location before code generation. |
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
+ | 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. |
16
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. |
17
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. |
18
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. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
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.
5
+ Active execution plan after `0.8.30`. 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
 
@@ -130,7 +130,9 @@ This is an incremental evolution of the current repository:
130
130
  ### Execution Status
131
131
 
132
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.
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
+ - [x] P0.3 Graph diagnostics is complete in `0.8.30`. Reachable ordinary modules validate relative runtime imports/re-exports and reject every dynamic `import()` at the importer source location before compilation or generated module loading. Ordinary and Worker traversal retain separate ownership, type-only and unreachable edges remain excluded, focused page/helper/re-export/dynamic fixtures pass with all 189 tests and packed-package smoke, and no export-symbol graph, ProjectSession, runtime, or public API is added.
135
+ - [ ] P0.4 Async native handler ownership is next. Pending handlers must not write after route or DOM ownership is released.
134
136
 
135
137
  ### P0: Semantic Correctness And Compiler Foundation
136
138
 
@@ -454,4 +456,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
454
456
 
455
457
  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.
456
458
 
457
- The next PR is **PR 2: Symbol-Aware Descriptor Discovery**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
459
+ The next PR is **PR 4: Async Native Handler Invalidation**. 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.28` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.30` 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
 
@@ -19,6 +19,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
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
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. |
23
+ | `0.8.30` | Validate ordinary runtime graph edges before code generation and reject dynamic imports at the importer source. | Page/helper/re-export/dynamic fixtures report original file/range/specifier without `.kudzu` paths; Worker, type-only, unreachable, output, and runtime behavior remain unchanged. |
22
24
 
23
25
  ## Sequence Rules
24
26
 
@@ -206,7 +206,7 @@ export function createBindingIndex(sourceFile) {
206
206
  if (binding.declarationKind.startsWith("import")) kind = "import"
207
207
  else if (binding.declarationKind === "global") kind = "global"
208
208
  else if (binding.declarationKind === "unresolved") kind = "unresolved"
209
- else if (inside(binding.declaration, ts.getOriginalNode(boundary))) kind = binding.declarationKind === "parameter" ? "parameter" : "local"
209
+ else if (inside(binding.declaration, boundary)) kind = binding.declarationKind === "parameter" ? "parameter" : "local"
210
210
  else kind = "capture"
211
211
  return {
212
212
  kind,
@@ -234,16 +234,21 @@ export function createBindingIndex(sourceFile) {
234
234
  return complete ? found : undefined
235
235
  }
236
236
 
237
- return { bindings: () => bindings.map(({ declaration, ...binding }) => ({ ...binding, ...(declaration ? { declarationRange: range(declaration) } : {}) })), references, resolveReference }
237
+ return { bindings: () => bindings.map(({ declaration, ...binding }) => ({ ...binding, ...(declaration ? { declarationRange: range(declaration) } : {}) })), hasNode: node => scopeByNode.has(node), references, resolveReference }
238
238
  }
239
239
 
240
240
  function inside(node, boundary) {
241
- for (let current = node; current; current = current.parent) if (current === boundary) return true
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
+ }
242
246
  return false
243
247
  }
244
248
 
245
249
  function range(node) {
246
- return node.pos >= 0 && node.end >= 0 ? { start: node.getStart(), end: node.end } : undefined
250
+ if (node.pos < 0 || node.end < 0) return undefined
251
+ return { start: node.getSourceFile() ? node.getStart() : node.pos, end: node.end }
247
252
  }
248
253
 
249
254
  function isFunctionLike(node) {
@@ -17,9 +17,9 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
17
17
  const listExpressions = []
18
18
  const clientModules = new Set()
19
19
 
20
- function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock) {
20
+ function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock, indexedBindingIndex) {
21
21
  const exportName = `listExpression${listExpressions.length}`
22
- 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 })
23
23
  const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
24
24
  if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
25
25
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
@@ -27,7 +27,8 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
27
27
 
28
28
  function compileListConditional(entry) {
29
29
  const exportName = `listExpression${listExpressions.length}`
30
- 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 })
31
32
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
32
33
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
33
34
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -36,21 +37,25 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
36
37
  }
37
38
 
38
39
  function compileListValue(expression, entry) {
40
+ const indexed = indexedReferences(bindingIndex, expression, expression)
39
41
  const rewrite = node => {
40
- if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
41
- 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")
42
47
  return ts.visitEachChild(node, rewrite, context)
43
48
  }
44
49
  const initial = entry.states?.size ? ts.visitNode(expression, rewrite) : expression
45
50
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
46
51
  return entry.field
47
52
  ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
48
- : 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)
49
54
  }
50
55
 
51
56
  function compileReactiveBinding(expression, { setters, importBindings = new Map(), keyedBlock }) {
52
57
  const parts = conditionalParts(expression)
53
- const state = parts && directStateIdentifier(parts.condition, setters)
58
+ const state = parts && directStateIdentifier(parts.condition, setters, bindingIndex)
54
59
  if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
55
60
  return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
56
61
  }
@@ -58,7 +63,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
58
63
  }
59
64
 
60
65
  function compileConditional(kind, expression, truthy, falsy, setters) {
61
- const state = directStateIdentifier(expression, setters)
66
+ const state = directStateIdentifier(expression, setters, bindingIndex)
62
67
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
63
68
  if (state) return factory.createCallExpression(factory.createIdentifier("__kStateConditional"), undefined, [factory.createStringLiteral(kind), state, thunk(truthy), thunk(falsy)])
64
69
  const [initial, ...descriptor] = compileReactiveExpression(expression, setters)
@@ -66,8 +71,8 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
66
71
  }
67
72
 
68
73
  function compileReactiveExpression(expression, setters, importBindings = new Map(), keyedBlock) {
69
- const usedStates = referencedStateNames(expression, setters)
70
- const indexed = bindingIndex?.references(expression, expression)
74
+ const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
75
+ const indexed = indexedReferences(bindingIndex, expression, expression)
71
76
  const importedNames = indexed ? new Set(indexed.filter(reference => reference.kind === "import" && importBindings.has(reference.debugName)).map(reference => reference.debugName)) : referencedImportedBindings(expression, importBindings)
72
77
  const imports = [...importedNames].map(name => importBindings.get(name))
73
78
  registerClientImports(imports)
@@ -113,19 +118,20 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
113
118
  }
114
119
 
115
120
  function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
116
- const allCaptures = nativeCaptureNames(expression, setters)
117
- const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
118
- 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))
119
125
  imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
120
126
  const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
121
127
  registerClientImports(imports)
122
- const usedStates = referencedStateNames(expression.body, setters, expression)
128
+ const usedStates = referencedStateNames(expression.body, setters, expression, indexedBindingIndex)
123
129
  for (const name of usedReducers) {
124
130
  const reducer = reducers.get(name)
125
- 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)
126
132
  }
127
133
  const exportName = `${prefix}${entries.length}`
128
- 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 })
129
135
  const value = name => deferValues
130
136
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
131
137
  : factory.createIdentifier(name)
@@ -143,7 +149,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
143
149
 
144
150
  function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
145
151
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
146
- 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)
147
153
  if (!commands.length || commands.some(command => !command)) return undefined
148
154
  const original = ts.getOriginalNode(expression)
149
155
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
@@ -152,6 +158,17 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
152
158
  return generateCommandBehavior(moduleIR, handler, factory)
153
159
  }
154
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
+
155
172
  function registerClientImports(imports) {
156
173
  for (const entry of imports) if (!entry.package) clientModules.add(entry.target)
157
174
  }
@@ -241,9 +258,12 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
241
258
  return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult }
242
259
  }
243
260
 
244
- function directStateIdentifier(expression, setters) {
261
+ function directStateIdentifier(expression, setters, bindingIndex) {
245
262
  const value = unwrapExpression(expression)
246
- 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
247
267
  }
248
268
 
249
269
  function conditionalParts(expression) {
@@ -254,7 +274,9 @@ function conditionalParts(expression) {
254
274
  return undefined
255
275
  }
256
276
 
257
- 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))
258
280
  const used = new Set()
259
281
  const visit = node => {
260
282
  if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(node.text)
@@ -264,8 +286,10 @@ export function referencedReducerDispatches(root, reducers, scopeRoot = root) {
264
286
  return used
265
287
  }
266
288
 
267
- export function referencedStateNames(root, setters, scopeRoot = root) {
289
+ export function referencedStateNames(root, setters, scopeRoot = root, bindingIndex) {
268
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))
269
293
  const used = new Set()
270
294
  const visit = node => {
271
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))
@@ -277,11 +301,13 @@ export function referencedStateNames(root, setters, scopeRoot = root) {
277
301
  return used
278
302
  }
279
303
 
280
- export function nativeCaptureNames(expression, setters) {
281
- return captureNames(expression, expression.body, setters)
304
+ export function nativeCaptureNames(expression, setters, bindingIndex) {
305
+ return captureNames(expression, expression.body, setters, bindingIndex)
282
306
  }
283
307
 
284
- 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))
285
311
  const names = new Set()
286
312
  const visit = node => {
287
313
  if (ts.isIdentifier(node) && imports.has(node.text) && isReferenceIdentifier(node)) names.add(node.text)
@@ -291,7 +317,12 @@ function referencedImportedBindings(expression, imports) {
291
317
  return names
292
318
  }
293
319
 
294
- 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
+ }
295
326
  const local = new Set()
296
327
  if (!isFunctionLike(declarationRoot)) {
297
328
  const collectDeclarations = node => {
@@ -315,3 +346,7 @@ export function captureNames(declarationRoot, referenceRoot, setters) {
315
346
  visit(referenceRoot)
316
347
  return captures
317
348
  }
349
+
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,
@@ -195,7 +200,7 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
195
200
  const visitor = node => {
196
201
  const reference = ts.isShorthandPropertyAssignment(node) ? node.name : ts.isIdentifier(node) ? node : undefined
197
202
  const resolution = reference ? bindingIndex?.resolveReference(reference, expression) : undefined
198
- const indexedState = resolution?.kind === "capture" && states.has(resolution.debugName)
203
+ const indexedState = ["capture", "unresolved"].includes(resolution?.kind) && states.has(resolution.debugName)
199
204
  const indexedCapture = ["capture", "unresolved"].includes(resolution?.kind) && captures.has(resolution.debugName)
200
205
  if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text) && (!bindingIndex || indexedState)) {
201
206
  return factory.createPropertyAssignment(
@@ -241,14 +246,14 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
241
246
  }
242
247
  }
243
248
 
244
- function printListExpression({ exportName, expression, item, index, states = new Set() }) {
249
+ function printListExpression({ exportName, expression, item, index, states = new Set(), bindingIndex }) {
245
250
  const factory = ts.factory
246
251
  const transformer = context => root => {
247
252
  const visitor = node => {
248
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
253
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
249
254
  return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
250
255
  }
251
- 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))) {
252
257
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
253
258
  }
254
259
  return ts.visitEachChild(node, visitor, context)
@@ -18,7 +18,7 @@ import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
18
18
  import { createReactMigrationPass, reactMemoExpression } from "./react-migration-pass.mjs"
19
19
  import { normalizeRenderControlFlow } from "./render-control-pass.mjs"
20
20
  import { createRouterPass } from "./router-pass.mjs"
21
- import { parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
21
+ import { ordinaryRuntimeDependencies, parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
22
22
  import { createWorkerCompiler } from "./worker-compiler.mjs"
23
23
  import { createZustandPass } from "./zustand-pass.mjs"
24
24
 
@@ -92,20 +92,27 @@ function emittedPackageReference(source, file, packages) {
92
92
 
93
93
  export function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
94
94
  const reachable = new Set()
95
- const queue = [...entries]
95
+ const ordinary = new Set()
96
+ const workers = new Set()
97
+ const queue = entries.map(file => ({ file, owner: "ordinary" }))
96
98
  while (queue.length) {
97
- const file = queue.pop()
98
- if (reachable.has(file)) continue
99
+ const { file, owner } = queue.pop()
100
+ const visited = owner === "ordinary" ? ordinary : workers
101
+ if (visited.has(file)) continue
102
+ visited.add(file)
99
103
  reachable.add(file)
100
104
  const sourceFile = parseSourceFile(file, sourceIndex.get(file))
105
+ if (owner === "ordinary") for (const target of ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport)) queue.push({ file: target, owner: target.endsWith(".worker.ts") ? "worker" : owner })
101
106
  const visit = node => {
102
- const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
103
- if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
104
- try { queue.push(resolveSourceImport(file, specifier.text, sourceFiles)) } catch {}
107
+ if (owner === "worker") {
108
+ const specifier = (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && runtimeModuleReference(node) && node.moduleSpecifier
109
+ if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith(".") && !isStaticImport(specifier.text)) {
110
+ try { queue.push({ file: resolveSourceImport(file, specifier.text, sourceFiles), owner }) } catch {}
111
+ }
105
112
  }
106
113
  const worker = workerCompiler.candidate(node, sourceFile)
107
114
  if (worker && ts.isStringLiteral(worker.url.arguments[0]) && worker.url.arguments[0].text.endsWith(".worker.ts")) {
108
- try { queue.push(resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles)) } catch {}
115
+ try { queue.push({ file: resolveSourceImport(file, worker.url.arguments[0].text, sourceFiles), owner: "worker" }) } catch {}
109
116
  }
110
117
  ts.forEachChild(node, visit)
111
118
  }
@@ -795,9 +802,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
795
802
  const expanded = substituteClone(expression, substitutions, factory, context)
796
803
  ts.setParentRecursive(expanded, false)
797
804
  expanded.parent = expression.parent
798
- const usedStates = referencedStateNames(expanded, setters)
805
+ const usedStates = referencedStateNames(expanded, setters, expanded, bindingIndex)
799
806
  if (!usedStates.size) return expression
800
- const captures = captureNames(expanded, expanded, setters)
807
+ const captures = captureNames(expanded, expanded, setters, bindingIndex)
801
808
  const allowedNames = new Set([...setters.values(), ...captures])
802
809
  validateReactiveJsxExpression(expanded, allowedNames)
803
810
  return expanded
@@ -822,7 +829,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
822
829
  const signals = new Set()
823
830
  if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
824
831
  const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
825
- for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
832
+ for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression, bindingIndex)) signals.add(state)
826
833
  return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
827
834
  }
828
835
  result.analysis = componentAnalysis.registerSpecialization({
@@ -1371,7 +1378,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1371
1378
  calculation.parent = callback
1372
1379
  validateListExpression(calculation, parts.item, originalParts.root, fail)
1373
1380
  }
1374
- const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
1381
+ const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex)
1375
1382
  preparedRenderedLists.push({ node, parts, analysis })
1376
1383
  }
1377
1384
 
@@ -1379,8 +1386,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1379
1386
  const parts = conditionalParts(expression)
1380
1387
  if (!parts) return ts.visitNode(expression, visitor)
1381
1388
  const setters = settersForNode(anchor, settersByFunction)
1382
- const usedStates = referencedStateNames(parts.condition, setters)
1383
- const captures = captureNames(parts.condition, parts.condition, setters)
1389
+ const usedStates = referencedStateNames(parts.condition, setters, parts.condition, bindingIndex)
1390
+ const captures = captureNames(parts.condition, parts.condition, setters, bindingIndex)
1384
1391
  if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
1385
1392
  usesBehavior = true
1386
1393
  usesConditional = true
@@ -1534,7 +1541,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1534
1541
  setters,
1535
1542
  localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
1536
1543
  factory,
1537
- fail: effectFail
1544
+ fail: effectFail,
1545
+ bindingIndex
1538
1546
  })
1539
1547
  const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
1540
1548
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
@@ -1559,7 +1567,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1559
1567
  const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
1560
1568
  if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
1561
1569
  if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
1562
- validateEffectOwnedBrowserResources(callback, returns, effectFail)
1570
+ validateEffectOwnedBrowserResources(callback, returns, effectFail, bindingIndex)
1563
1571
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
1564
1572
  const callbackFile = callbackSource.fileName
1565
1573
  let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
@@ -1688,8 +1696,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1688
1696
  }
1689
1697
  const setters = settersForNode(node, settersByFunction)
1690
1698
  const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
1691
- const usedStates = referencedStateNames(expression, setters)
1692
- const captures = captureNames(expression, expression, setters)
1699
+ const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
1700
+ const captures = captureNames(expression, expression, setters, bindingIndex)
1693
1701
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
1694
1702
  usesBehavior = true
1695
1703
  usesBinding = true
@@ -1701,8 +1709,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1701
1709
  const sourceExpression = node.initializer.expression
1702
1710
  const setters = settersForNode(node, settersByFunction)
1703
1711
  const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
1704
- const usedStates = referencedStateNames(expression, setters)
1705
- const captures = captureNames(expression, expression, setters)
1712
+ const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
1713
+ const captures = captureNames(expression, expression, setters, bindingIndex)
1706
1714
  if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
1707
1715
  usesBehavior = true
1708
1716
  usesBinding = true
@@ -1955,7 +1963,7 @@ function insideJsxEventHandler(node, root) {
1955
1963
  return false
1956
1964
  }
1957
1965
 
1958
- function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
1966
+ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex) {
1959
1967
  const fail = (node, message) => {
1960
1968
  throw sourceNodeError(node, sourceFile, message)
1961
1969
  }
@@ -2016,7 +2024,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2016
2024
  calculation.parent = callback
2017
2025
  validateListExpression(calculation, nested.item, nested.root, fail)
2018
2026
  }
2019
- const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
2027
+ const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex)
2020
2028
  analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
2021
2029
  return
2022
2030
  }
@@ -2044,7 +2052,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2044
2052
  return
2045
2053
  }
2046
2054
  if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
2047
- const states = referencedStateNames(expression, setters)
2055
+ const states = referencedStateNames(expression, setters, expression, bindingIndex)
2048
2056
  for (const rowState of rowStates) states.delete(rowState.state)
2049
2057
  if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
2050
2058
  validateListExpression(expression, item, node, fail, parts.index, states)
@@ -2666,15 +2674,6 @@ function hasFrameworkImport(sourceFile, name) {
2666
2674
 
2667
2675
  function packageImportBindings(sourceFile) {
2668
2676
  const bindings = new Map()
2669
- const rejectDynamic = node => {
2670
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
2671
- const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0]) ? node.arguments[0].text : null
2672
- if (specifier === null) throw sourceNodeError(node, sourceFile, "Dynamic import specifiers are not supported")
2673
- if (!specifier.startsWith(".")) throw sourceNodeError(node, sourceFile, `Dynamic package import ${JSON.stringify(specifier)} is not supported`)
2674
- }
2675
- ts.forEachChild(node, rejectDynamic)
2676
- }
2677
- rejectDynamic(sourceFile)
2678
2677
  for (const node of sourceFile.statements) {
2679
2678
  if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue
2680
2679
  const target = node.moduleSpecifier.text
@@ -1,5 +1,6 @@
1
1
  import { dirname, extname, join, relative, resolve } from "node:path"
2
2
  import ts from "typescript"
3
+ import { sourceNodeError } from "./ast-helpers.mjs"
3
4
 
4
5
  const root = process.cwd()
5
6
 
@@ -24,6 +25,32 @@ export function runtimeModuleReference(node) {
24
25
  return clause.namedBindings?.elements.some(entry => !entry.isTypeOnly) ?? false
25
26
  }
26
27
 
28
+ export function ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport) {
29
+ const dependencies = []
30
+ const rejectDynamicImports = node => {
31
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
32
+ const argument = node.arguments[0]
33
+ const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(argument) ? JSON.stringify(argument.text) : argument?.getText(sourceFile) ?? "<missing>"
34
+ throw sourceNodeError(node, sourceFile, `Dynamic import ${specifier} is not supported in ordinary source modules`)
35
+ }
36
+ ts.forEachChild(node, rejectDynamicImports)
37
+ }
38
+ rejectDynamicImports(sourceFile)
39
+ for (const node of sourceFile.statements) {
40
+ if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !runtimeModuleReference(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier)) continue
41
+ const specifier = node.moduleSpecifier
42
+ if (!specifier.text.startsWith(".") || isStaticImport(specifier.text)) continue
43
+ try {
44
+ dependencies.push(resolveSourceImport(file, specifier.text, sourceFiles))
45
+ } catch (error) {
46
+ const detail = error.message.slice(error.message.indexOf("Relative import"))
47
+ const edge = ts.isExportDeclaration(node) ? "re-export" : "import"
48
+ throw sourceNodeError(specifier, sourceFile, detail.replace("Relative import", `Relative runtime ${edge}`))
49
+ }
50
+ }
51
+ return dependencies
52
+ }
53
+
27
54
  export function parseSourceFile(file, source) {
28
55
  return ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
29
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.28",
3
+ "version": "0.8.30",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",