@kudzujs/core 0.8.28 → 0.8.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +7 -1
- package/README.md +1 -1
- package/RELEASES.md +30 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +2 -2
- package/docs/next-architecture/large-application-ai-native-roadmap.md +4 -3
- package/docs/next-architecture/versioning.md +2 -1
- package/framework/compiler/analysis/binding-index.mjs +9 -4
- package/framework/compiler/descriptor-session.mjs +60 -25
- package/framework/compiler/effect-analysis.mjs +15 -11
- package/framework/compiler/handler-lowering.mjs +26 -21
- package/framework/compiler/source-compiler.mjs +16 -15
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -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.
|
|
5
|
+
The executable post-`0.8.29` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
|
|
6
6
|
|
|
7
7
|
[`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
|
|
8
8
|
|
|
@@ -276,6 +276,12 @@ 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
|
+
|
|
279
285
|
## Cross-Cutting Performance Gates
|
|
280
286
|
|
|
281
287
|
Every migration feature must preserve:
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
14
14
|
|
|
15
15
|
> Experimental `0.8.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.8.
|
|
17
|
+
**Latest release: 0.8.29 - Symbol-aware descriptor discovery.** Native handlers, effects, reactive bindings, keyed evaluators, optimized commands, and effect-resource validation now use source-local lexical identity when the binding index owns the complete AST, while synthesized trees retain the established fail-safe path. Read the [release notes](./RELEASES.md#0829---symbol-aware-descriptor-discovery), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.29), or follow the [architecture packet](./docs/next-architecture/README.md).
|
|
18
18
|
|
|
19
19
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
20
20
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.29 - Symbol-aware descriptor discovery
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.29 completes P0.2 by moving native handler, effect, binding, and list descriptor decisions from identifier spelling onto the source-local binding index introduced in 0.8.28.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.29
|
|
8
|
+
|
|
9
|
+
- Native handlers and effects discover state, setters, reducers, imports, captures, and nested snapshots by lexical reference identity when the complete callback belongs to the source index.
|
|
10
|
+
- Handler lowering consumes the same indexed identity used during discovery, so callback parameters and locals that shadow state, setters, reducers, imports, or captures remain untouched.
|
|
11
|
+
- Reactive conditionals, bindings, keyed-list evaluators, and list state rewrites distinguish outer state from same-named nested callback parameters.
|
|
12
|
+
- The command fast path accepts state and setter references only when they resolve outside the callback, and recognizes `console.log()` only when `console` is the actual browser global.
|
|
13
|
+
- Effect resource validation distinguishes real `IntersectionObserver`, `requestAnimationFrame()`, and `cancelAnimationFrame()` globals from shadowed application bindings and matches cleanup to the exact observer or frame declaration.
|
|
14
|
+
|
|
15
|
+
### Compiler Boundary
|
|
16
|
+
|
|
17
|
+
- The binding index now recognizes normalized and original TypeScript node boundaries and exposes whether it directly owns an AST node.
|
|
18
|
+
- Imported, specialized, or compiler-synthesized trees continue through the established conservative shadow walker rather than borrowing an unrelated source index.
|
|
19
|
+
- P0.2 changes no public API, accepted source syntax, ModuleIR shape, browser runtime, or generated capability contract. P0.3 graph failure diagnostics is next.
|
|
20
|
+
|
|
21
|
+
### Validation
|
|
22
|
+
|
|
23
|
+
- `npm run check`, `npm test`, `npm run test:package`, and all 187 tests pass.
|
|
24
|
+
- Focused tests cover shadowed imports, browser globals, state, setters, list parameters, JSON-safe HandlerIR/BindingIR round trips, and same-named effect resource cleanup.
|
|
25
|
+
- Existing Context actions, keyed component callbacks, React Router search parameters, native events, effects, Workers, navigation, and static-route zero-JavaScript behavior remain covered.
|
|
26
|
+
|
|
27
|
+
### Upgrade
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @kudzujs/core@^0.8.29
|
|
31
|
+
```
|
|
32
|
+
|
|
3
33
|
## 0.8.28 - Source-local binding index
|
|
4
34
|
|
|
5
35
|
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.
|
|
14
|
+
The active post-`0.8.29` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). It orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
|
|
15
15
|
|
|
16
16
|
## Required Invariants
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.8.
|
|
3
|
+
This maps the current `0.8.29` architecture built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ This maps the current `0.8.28` 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.
|
|
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.
|
|
5
|
+
Active execution plan after `0.8.29`. This document turns the current compiler audit into an ordered implementation program. It does not mark any planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
|
|
6
6
|
|
|
7
7
|
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection. This plan is authoritative for the order and completion evidence of compiler generalization, large-application foundations, compatibility boundaries, AI tooling, and scale validation. If implementation evidence changes a boundary, update this document before broadening a patch.
|
|
8
8
|
|
|
@@ -130,7 +130,8 @@ 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
|
-
- [
|
|
133
|
+
- [x] P0.2 Symbol-aware descriptor discovery is complete in `0.8.29`. Native handler, effect, remaining binding, list evaluator, optimized-command, and effect-resource discovery/lowering use the source-local binding index when it owns the complete AST; synthesized trees retain the existing fail-safe path. Focused lexical-shadow, JSON-safe IR, resource-ownership checks, all 187 tests, and packed-package smoke pass.
|
|
134
|
+
- [ ] P0.3 Graph diagnostics is next. It must fail unresolved relative imports/re-exports and ordinary dynamic imports at the importer source location without broadening into ProjectSession or cross-module symbol work.
|
|
134
135
|
|
|
135
136
|
### P0: Semantic Correctness And Compiler Foundation
|
|
136
137
|
|
|
@@ -454,4 +455,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
454
455
|
|
|
455
456
|
The first implementation PR is **PR 1: Source-Local Binding Index**. It is compile-time only, adds no browser bytes or public API, fixes an existing correctness class, and creates the stable semantic foundation required by component graphs, state operations, cross-module analysis, compatibility adapters, structured diagnostics, and AI explanation tooling.
|
|
456
457
|
|
|
457
|
-
The next PR is **PR
|
|
458
|
+
The next PR is **PR 3: Graph Failure Diagnostics**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Planned Version Sequence
|
|
2
2
|
|
|
3
|
-
This is an execution sequence, not release history. `0.8.16` through `0.8.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.29` are completed scopes represented by package/release records.
|
|
4
4
|
|
|
5
5
|
Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
|
|
6
6
|
|
|
@@ -19,6 +19,7 @@ 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. |
|
|
22
23
|
|
|
23
24
|
## Sequence Rules
|
|
24
25
|
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
117
|
-
const
|
|
118
|
-
const
|
|
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
|
-
|
|
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
|
|
36
|
-
const
|
|
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
|
|
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
|
|
70
|
-
if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression
|
|
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
|
|
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
|
|
78
|
-
if (!cancellations.has(
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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
|
|
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)
|
|
@@ -795,9 +795,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
795
795
|
const expanded = substituteClone(expression, substitutions, factory, context)
|
|
796
796
|
ts.setParentRecursive(expanded, false)
|
|
797
797
|
expanded.parent = expression.parent
|
|
798
|
-
const usedStates = referencedStateNames(expanded, setters)
|
|
798
|
+
const usedStates = referencedStateNames(expanded, setters, expanded, bindingIndex)
|
|
799
799
|
if (!usedStates.size) return expression
|
|
800
|
-
const captures = captureNames(expanded, expanded, setters)
|
|
800
|
+
const captures = captureNames(expanded, expanded, setters, bindingIndex)
|
|
801
801
|
const allowedNames = new Set([...setters.values(), ...captures])
|
|
802
802
|
validateReactiveJsxExpression(expanded, allowedNames)
|
|
803
803
|
return expanded
|
|
@@ -822,7 +822,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
822
822
|
const signals = new Set()
|
|
823
823
|
if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
|
|
824
824
|
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)
|
|
825
|
+
for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression, bindingIndex)) signals.add(state)
|
|
826
826
|
return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
|
|
827
827
|
}
|
|
828
828
|
result.analysis = componentAnalysis.registerSpecialization({
|
|
@@ -1371,7 +1371,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1371
1371
|
calculation.parent = callback
|
|
1372
1372
|
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
1373
1373
|
}
|
|
1374
|
-
const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
1374
|
+
const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex)
|
|
1375
1375
|
preparedRenderedLists.push({ node, parts, analysis })
|
|
1376
1376
|
}
|
|
1377
1377
|
|
|
@@ -1379,8 +1379,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1379
1379
|
const parts = conditionalParts(expression)
|
|
1380
1380
|
if (!parts) return ts.visitNode(expression, visitor)
|
|
1381
1381
|
const setters = settersForNode(anchor, settersByFunction)
|
|
1382
|
-
const usedStates = referencedStateNames(parts.condition, setters)
|
|
1383
|
-
const captures = captureNames(parts.condition, parts.condition, setters)
|
|
1382
|
+
const usedStates = referencedStateNames(parts.condition, setters, parts.condition, bindingIndex)
|
|
1383
|
+
const captures = captureNames(parts.condition, parts.condition, setters, bindingIndex)
|
|
1384
1384
|
if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
|
|
1385
1385
|
usesBehavior = true
|
|
1386
1386
|
usesConditional = true
|
|
@@ -1534,7 +1534,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1534
1534
|
setters,
|
|
1535
1535
|
localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
|
|
1536
1536
|
factory,
|
|
1537
|
-
fail: effectFail
|
|
1537
|
+
fail: effectFail,
|
|
1538
|
+
bindingIndex
|
|
1538
1539
|
})
|
|
1539
1540
|
const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
|
|
1540
1541
|
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
@@ -1559,7 +1560,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1559
1560
|
const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
|
|
1560
1561
|
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
1561
1562
|
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)
|
|
1563
|
+
validateEffectOwnedBrowserResources(callback, returns, effectFail, bindingIndex)
|
|
1563
1564
|
const callbackSource = specializedEffect?.sourceFile ?? sourceFile
|
|
1564
1565
|
const callbackFile = callbackSource.fileName
|
|
1565
1566
|
let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
|
|
@@ -1688,8 +1689,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1688
1689
|
}
|
|
1689
1690
|
const setters = settersForNode(node, settersByFunction)
|
|
1690
1691
|
const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
|
|
1691
|
-
const usedStates = referencedStateNames(expression, setters)
|
|
1692
|
-
const captures = captureNames(expression, expression, setters)
|
|
1692
|
+
const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
|
|
1693
|
+
const captures = captureNames(expression, expression, setters, bindingIndex)
|
|
1693
1694
|
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
|
|
1694
1695
|
usesBehavior = true
|
|
1695
1696
|
usesBinding = true
|
|
@@ -1701,8 +1702,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1701
1702
|
const sourceExpression = node.initializer.expression
|
|
1702
1703
|
const setters = settersForNode(node, settersByFunction)
|
|
1703
1704
|
const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
|
|
1704
|
-
const usedStates = referencedStateNames(expression, setters)
|
|
1705
|
-
const captures = captureNames(expression, expression, setters)
|
|
1705
|
+
const usedStates = referencedStateNames(expression, setters, expression, bindingIndex)
|
|
1706
|
+
const captures = captureNames(expression, expression, setters, bindingIndex)
|
|
1706
1707
|
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
|
|
1707
1708
|
usesBehavior = true
|
|
1708
1709
|
usesBinding = true
|
|
@@ -1955,7 +1956,7 @@ function insideJsxEventHandler(node, root) {
|
|
|
1955
1956
|
return false
|
|
1956
1957
|
}
|
|
1957
1958
|
|
|
1958
|
-
function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
|
|
1959
|
+
function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex) {
|
|
1959
1960
|
const fail = (node, message) => {
|
|
1960
1961
|
throw sourceNodeError(node, sourceFile, message)
|
|
1961
1962
|
}
|
|
@@ -2016,7 +2017,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
|
|
|
2016
2017
|
calculation.parent = callback
|
|
2017
2018
|
validateListExpression(calculation, nested.item, nested.root, fail)
|
|
2018
2019
|
}
|
|
2019
|
-
const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
2020
|
+
const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex)
|
|
2020
2021
|
analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
|
|
2021
2022
|
return
|
|
2022
2023
|
}
|
|
@@ -2044,7 +2045,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
|
|
|
2044
2045
|
return
|
|
2045
2046
|
}
|
|
2046
2047
|
if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
|
|
2047
|
-
const states = referencedStateNames(expression, setters)
|
|
2048
|
+
const states = referencedStateNames(expression, setters, expression, bindingIndex)
|
|
2048
2049
|
for (const rowState of rowStates) states.delete(rowState.state)
|
|
2049
2050
|
if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
|
|
2050
2051
|
validateListExpression(expression, item, node, fail, parts.index, states)
|