@kudzujs/core 0.8.34 → 0.8.36

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.34` 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.36` 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
 
@@ -314,6 +314,13 @@ This queue orders the next investigations by general migration value. Start only
314
314
  - Read-only graph consumers share a canonical AST while importer normalization deep-clones every node and repairs independent parent links before transformation.
315
315
  - A 100-importer fixture proves bounded parse/summary work and identical source results without changing accepted migration syntax or browser output.
316
316
 
317
+ ### Completed In 0.8.35
318
+
319
+ - Cross-module declarations resolve through stable project-relative ModuleSymbol records rather than transformed AST identity or readable names.
320
+ - Source-local SiteIds identify imports, re-exports, component calls, hooks, keyed lists, effects, and ownership records across private transformer clones.
321
+ - Default/named exports, aliases, barrel chains, `export *`, cycles, ambiguity, repeated compilation, and source-dependent cache invalidation are covered without changing migration syntax or browser runtime behavior.
322
+ - The 100-importer fixture reduces private clones from 200 to 100 and records a small directional build improvement; current React/Vue/Svelte/Astro whole-build and artifact context is retained in `PERFORMANCE.md`.
323
+
317
324
  ## Cross-Cutting Performance Gates
318
325
 
319
326
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, and `npm run benchmark:module-cache` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
4
4
 
5
+ ## P0.9 Semantic State Operations
6
+
7
+ Measured UTC 2026-08-11 on the Intel Core i5-9500 Linux x64 host with Node 24.14.0. The baseline was clean tag `v0.8.35` at `f25700d9d2b247c01db19f0e8c95f16cb1fa81a5`. The compiler and focused-check patch had SHA-256 `4c3c8a3de18b1e792ea84cf7608a89971850195036a58f37dd76e359bfc8a58d`, produced by:
8
+
9
+ ```bash
10
+ git diff --binary v0.8.35 -- framework/compiler/optimize/command-specialization.mjs framework/compiler/descriptor-session.mjs test/compiler-passes.test.mjs test/fixtures/effect-isolation/src/pages/command.tsx test/framework.test.mjs | sha256sum
11
+ ```
12
+
13
+ The maintained 100-importer fixture used three warm-ups and 21 alternating fresh-process samples. P0.9 keeps the existing direct command fast path first and invokes whole-handler semantic analysis only after direct specialization fails. Baseline and candidate produced the same normalized 382,603-byte graph, SHA-256 `8c35b3f6d2c571306bd97c4d51d4af76ca244badd36c57363bf579ef961f41aa`, 395,346-byte source result, and 103 / 103 / 100 parse, summary, and clone counts.
14
+
15
+ | Target | Compiler median | Range | Peak RSS median | Source-result bytes |
16
+ |---|---:|---:|---:|---:|
17
+ | `v0.8.35` | 947.100 ms | 839.270-1,064.444 ms | 258.0 MiB | 395,346 B |
18
+ | P0.9 candidate | 942.596 ms | 849.656-1,215.997 ms | 258.1 MiB | 395,346 B |
19
+
20
+ The candidate's unpaired median is 0.48% lower. Round-paired candidate-minus-baseline differences had a +1.754 ms median with the candidate faster in 10/21 pairs and the baseline faster in 11/21. Timing and peak-RSS ranges overlap, so no material improvement or regression is claimed. An initial implementation that routed every direct handler through whole-handler analysis measured a +12.584 ms paired median and 10.6 MiB higher RSS median; restoring the direct fast path and narrowing the analyzer removed that regression before this final record.
21
+
22
+ ```text
23
+ v0.8.35: [955.382,927.389,935.312,852.622,839.270,918.424,943.013,1026.033,936.514,1019.725,921.591,1064.444,949.671,975.729,933.380,896.773,949.751,951.418,947.100,998.950,1041.266]
24
+ candidate: [942.596,1215.997,965.312,849.656,877.468,928.334,971.812,966.482,938.268,1016.268,932.327,939.217,928.284,1082.782,1086.319,976.534,881.665,915.266,962.642,905.596,1000.939]
25
+ paired candidate-baseline: [-12.786,288.608,30.000,-2.966,38.198,9.910,28.799,-59.551,1.754,-3.457,10.736,-125.227,-21.387,107.053,152.939,79.761,-68.086,-36.152,15.542,-93.354,-40.327]
26
+ ```
27
+
28
+ The four required source forms lower to the same existing command HandlerIR and command-only browser path. Alias/helper forms therefore avoid the native handler module and native runtime they previously required; no command ABI, state batching, ownership, runtime source, or unaffected route artifact changes.
29
+
30
+ ## P0.8 Stable ModuleSymbol And SiteId
31
+
32
+ Measured UTC 2026-08-11 on the same Intel Core i5-9500 Linux x64 host with Node 24.14.0. The baseline was clean tag `v0.8.34` at `007fcb6e23c7d5bc742fa37c28388d070da9f598`. The compiler and maintained-check patch had SHA-256 `10ed6beb448b9e86961afab0b798f010932b8a29f98475d41f7bd8cfad04a872`, produced by:
33
+
34
+ ```bash
35
+ git diff --binary v0.8.34 -- framework/compiler/project-session.mjs framework/compiler/source-compiler.mjs framework/compiler/analysis/component-analysis.mjs test/compiler-passes.test.mjs test/module-cache-performance.mjs | sha256sum
36
+ ```
37
+
38
+ The maintained 100-importer fixture used three warm-ups and 21 alternating fresh-process samples. Because P0.8 intentionally adds source-local `site` metadata, the runner removes only `site` keys for output-equivalence hashing and reports the unmodified source-result size separately. The normalized 382,603-byte graph retained SHA-256 `8c35b3f6d2c571306bd97c4d51d4af76ca244badd36c57363bf579ef961f41aa`; parse and summary misses remained 103 each. Stable symbol traversal removed unnecessary normalization of intermediate barrel modules, reducing importer-local clones from 200 to 100.
39
+
40
+ | Target | Compiler median | Range | Peak RSS median | Source-result bytes | Parse / summary / clone misses |
41
+ |---|---:|---:|---:|---:|---:|
42
+ | `v0.8.34` | 760.967 ms | 696.626-796.908 ms | 256.9 MiB | 382,603 B | not instrumented |
43
+ | `0.8.35` | 755.595 ms | 692.365-804.376 ms | 257.8 MiB | 395,346 B | 103 / 103 / 100 |
44
+
45
+ The candidate's unpaired median is 0.71% lower. Round-paired candidate-minus-baseline differences had a -2.971 ms median with the candidate faster in 13/21 pairs and the baseline faster in 8/21. This is a small directional improvement, not a material speedup claim. Peak RSS differs by 0.9 MiB and ranges overlap. The 12,743-byte source-result increase is deterministic SiteId metadata in build scratch, not deployed browser JavaScript.
46
+
47
+ Two measurements before removing intermediate barrel clones showed paired medians of +12.142 ms and +21.954 ms and RSS medians 11.7-12.3 MiB above baseline. Removing those clones reduced the paired median to +4.951 ms; caching repeated ModuleSymbol resolutions produced the final -2.971 ms result. The earlier measurements are not the final candidate record.
48
+
49
+ ```text
50
+ v0.8.34: [791.247,780.543,796.908,783.896,786.968,790.739,784.589,753.377,697.477,696.626,701.336,718.764,709.913,743.865,749.294,791.943,743.784,758.566,783.665,763.691,760.967]
51
+ candidate: [783.482,799.855,760.681,768.299,804.376,802.390,783.124,799.484,692.365,720.229,708.652,707.073,712.554,708.853,750.750,756.400,742.668,755.595,776.556,747.370,751.392]
52
+ paired candidate-baseline: [-7.765,19.312,-36.227,-15.597,17.408,11.651,-1.465,46.107,-5.112,23.603,7.316,-11.691,2.641,-35.012,1.456,-35.543,-1.116,-2.971,-7.109,-16.321,-9.575]
53
+ ```
54
+
55
+ ```bash
56
+ git worktree add --detach /tmp/opencode/kudzu-v0.8.34 v0.8.34
57
+ ln -s "$PWD/node_modules" /tmp/opencode/kudzu-v0.8.34/node_modules
58
+ BASELINE_ROOT=/tmp/opencode/kudzu-v0.8.34 CANDIDATE_ROOT="$PWD" WARMUPS=3 RUNS=21 npm run benchmark:module-cache
59
+ ```
60
+
5
61
  ## P0.7 Parsed Module And Export Summary Cache
6
62
 
7
63
  Measured UTC 2026-08-11 on an Intel Core i5-9500 with 6 physical/logical cores, 31.2 GiB RAM, Linux 6.17.0-19-generic x64, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.33` at `65c96b13802c73e9c9a109cebbaac88bae7704a7`; both targets used the same installed dependencies. The implementation and maintained benchmark patch had SHA-256 `bca61b37f14c77488c4a35b62856faa6b471dd01081abcb69c47027b3b4615d9`, produced by:
@@ -33,25 +89,32 @@ BASELINE_ROOT=/tmp/opencode/kudzu-v0.8.33 CANDIDATE_ROOT="$PWD" WARMUPS=3 RUNS=2
33
89
 
34
90
  ### Current Cross-Framework Whole-Build Context
35
91
 
36
- This separate local check is not a parsed-module-cache comparison or maintained ranking. The external `/tmp/opencode/kudzu-dependency-benchmark` workspace is not Git-provenanced. It used Kudzu's current candidate, React 19.2.7, Vue 3.5.40, Svelte 5.56.6, Astro 7.1.1, and Vite 7.3.6. Every target emitted matched initial device content, and headless Chrome passed detail fetch, list filters, empty state, superseded async command, stale-result rejection, and HTTP error behavior. React, Vue, and Svelte perform separate client, SSR, and prerender builds; Astro uses its native build; Kudzu runs one static compiler build, so build stages and artifact architectures differ.
92
+ Measured again after the P0.8 optimization on 2026-08-11. This separate local check is not a parsed-module-cache comparison or maintained ranking. The external `/tmp/opencode/kudzu-dependency-benchmark` workspace is not Git-provenanced. It used Kudzu 0.8.35, React 19.2.7, Vue 3.5.40, Svelte 5.56.6, Astro 7.1.1, and Vite 7.3.6. Every target emitted matched initial device content, and fresh headless Chrome runs passed detail fetch, list filters, empty state, superseded async command, stale-result rejection, and HTTP error behavior. One warm-up preceded seven rotated clean builds. React, Vue, and Svelte perform separate client, SSR, and prerender builds; Astro uses its native build and an application-specific imperative script; Kudzu runs one static compiler build, so build stages and artifact architectures differ.
37
93
 
38
94
  | Target | Build median | HTML raw / gzip | JavaScript raw / gzip | Total output |
39
95
  |---|---:|---:|---:|---:|
40
- | Kudzu | 856.2 ms | 5,647 B / 1,324 B | 30,712 B / 12,233 B | 36,359 B |
41
- | React SSR | 2,802.0 ms | 1,338 B / 562 B | 195,864 B / 61,488 B | 197,202 B |
42
- | Vue SSR | 2,295.3 ms | 1,348 B / 567 B | 69,643 B / 27,756 B | 70,991 B |
43
- | Svelte SSR | 2,972.9 ms | 1,399 B / 582 B | 40,781 B / 15,898 B | 42,180 B |
44
- | Astro native | 1,482.9 ms | 3,307 B / 1,229 B | 2,201 B / 883 B | 3,307 B |
96
+ | Kudzu | 495.4 ms | 5,647 B / 1,324 B | 30,712 B / 12,233 B | 36,359 B |
97
+ | React SSR | 1,639.5 ms | 1,338 B / 562 B | 195,864 B / 61,488 B | 197,202 B |
98
+ | Vue SSR | 1,477.3 ms | 1,348 B / 567 B | 69,643 B / 27,756 B | 70,991 B |
99
+ | Svelte SSR | 2,033.1 ms | 1,399 B / 582 B | 40,781 B / 15,898 B | 42,180 B |
100
+ | Astro native | 974.5 ms | 3,307 B / 1,229 B | 2,201 B / 883 B | 3,307 B |
45
101
 
46
102
  ```text
47
- Kudzu: [672.2,697.5,796.0,856.2,884.2,937.9,985.9]
48
- React SSR: [2238.5,2345.7,2598.5,2802.0,2843.9,2848.5,3253.6]
49
- Vue SSR: [1968.1,2068.9,2150.7,2295.3,2494.6,2617.1,3380.0]
50
- Svelte SSR: [2615.6,2922.0,2926.6,2972.9,3020.5,3168.3,3739.9]
51
- Astro native: [1444.9,1445.7,1451.2,1482.9,1732.5,1744.0,2236.9]
103
+ Kudzu: [474.3,482.6,493.4,495.4,498.0,503.5,504.9]
104
+ React SSR: [1589.6,1617.1,1638.4,1639.5,1640.7,1662.7,1667.5]
105
+ Vue SSR: [1433.8,1448.2,1450.0,1477.3,1486.0,1498.2,1500.5]
106
+ Svelte SSR: [1956.3,1993.7,2025.4,2033.1,2036.9,2060.2,2072.8]
107
+ Astro native: [943.3,945.9,964.7,974.5,988.8,996.4,999.5]
52
108
  ```
53
109
 
54
- The cross-framework result provides current whole-build context only. It does not show that Kudzu's module cache is faster than another framework's cache: this fixture has one application module per target and exposes no equivalent parser/cache counters. A cache-specific cross-framework claim would require matched 100-importer module graphs, equivalent output, equal cold/warm policy, and framework-specific cache instrumentation.
110
+ ```bash
111
+ node /tmp/opencode/kudzu-dependency-benchmark/device-run.mjs
112
+ node /tmp/opencode/kudzu-dependency-benchmark/device-verify.mjs
113
+ ```
114
+
115
+ Kudzu's build median was 69.8% lower than React SSR, 66.5% lower than Vue SSR, 75.6% lower than Svelte SSR, and 49.2% lower than Astro native in this protocol. Kudzu shipped 80.1%, 55.9%, and 23.1% less JavaScript gzip than React, Vue, and Svelte respectively. Astro shipped only 883 B JavaScript gzip and a 3,307 B single-file output, so Kudzu's JavaScript gzip was 13.9 times and total output 11.0 times larger than Astro's direct imperative implementation. Kudzu's larger HTML carries static content and capability descriptors; its 1,324 B gzip was close to Astro's 1,229 B but larger than the hydrated SSR controls' 562-582 B HTML.
116
+
117
+ The cross-framework result provides current whole-build and deploy-artifact context only. It does not show that Kudzu's ModuleSymbol cache is faster than another framework's module cache: this fixture has one application module per target and exposes no equivalent parser/cache counters. A cache-specific cross-framework claim would require matched 100-importer module graphs, equivalent output, equal cold/warm policy, and framework-specific cache instrumentation. The browser run verifies behavior rather than interaction latency; the maintained larger keyed benchmarks below remain the runtime-performance evidence.
55
118
 
56
119
  ## 2026-08-11 React, Vue, And Svelte Check
57
120
 
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.34 - Session-local module cache.** Each unchanged source module is parsed and export-summarized once per ProjectSession, while every transformer receives an independent deep clone so mutable TypeScript AST state cannot leak across importers or projects. Read the [release notes](./RELEASES.md#0834---session-local-module-cache), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.34), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.36 - Semantic state operations.** Equivalent direct, aliased, and local-helper state updates now lower to the same command IR without adding handler JavaScript. Read the [release notes](./RELEASES.md#0836---semantic-state-operations), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.36), 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)
@@ -32,6 +32,7 @@ Kudzu requires Node.js 22 or newer.
32
32
  ```bash
33
33
  npm create kudzu@latest my-app
34
34
  cd my-app
35
+ npm install
35
36
  npm run dev
36
37
  ```
37
38
 
package/RELEASES.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.36 - Semantic state operations
4
+
5
+ Kudzu 0.8.36 completes P0.9 by proving equivalent direct, aliased, and local-helper state updates and lowering them through the existing command HandlerIR path.
6
+
7
+ ### Changed in 0.8.36
8
+
9
+ - Direct `setCount(count + 1)`, an immutable result alias, a zero-argument arrow helper, and a one-parameter function helper emit the same structured `add` command.
10
+ - Binding identity proves state, setter, helper, and parameter ownership before specialization.
11
+ - Recursion, helper or alias escape, mutation, optional calls, and object-based dynamic dispatch fail at authored source locations.
12
+ - The existing direct command fast path remains first; whole-handler analysis runs only when direct specialization fails.
13
+ - Quick Start instructions now include an explicit `npm install`, and `create-kudzu@0.1.102` reports install-aware next steps and includes the install command in generated READMEs.
14
+
15
+ ### Performance
16
+
17
+ - The maintained 100-importer fixture preserves the same normalized graph, source-result bytes, digest, and 103 / 103 / 100 parse, summary, and clone counts.
18
+ - Twenty-one alternating `v0.8.35`/candidate samples measured compiler medians of 947.100 ms and 942.596 ms. The +1.754 ms paired candidate-minus-baseline median and overlapping RSS ranges establish no material improvement or regression.
19
+ - Alias and helper forms avoid native handler ESM and reuse the existing command runtime; direct command artifacts remain unchanged.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm test`, and `npm run test:package` pass with all 197 tests and 151 generated pages.
24
+ - Focused coverage proves identical JSON-safe command IR, signal ownership, build-module behavior calls, RouteIR commands, and zero native-handler artifacts for all four source forms.
25
+ - Existing command ABI, synchronous batching, ownership, browser runtime, and unrelated native handlers remain unchanged.
26
+ - P0.10 ModuleIR reference unification is next.
27
+
28
+ ### Upgrade
29
+
30
+ ```bash
31
+ npm install @kudzujs/core@^0.8.36
32
+ ```
33
+
34
+ ## 0.8.35 - Stable module identity
35
+
36
+ Kudzu 0.8.35 completes P0.8 by giving cross-module declarations and authored compiler sites stable identities that do not depend on transformed TypeScript AST objects or readable names.
37
+
38
+ ### Changed in 0.8.35
39
+
40
+ - ProjectSession summarizes declaration, import, named re-export, and `export *` sites as source-local SiteId records.
41
+ - ModuleSymbol identity combines a project-relative module path with the authored declaration SiteId; readable names remain diagnostic metadata.
42
+ - Default and named exports, local aliases, barrel chains, `export *`, cycles, and ambiguous star exports resolve through one cycle-safe symbol graph.
43
+ - Imported semantic consumers resolve ModuleSymbols first, then locate the authored SiteId in their private normalized AST clone instead of rescanning by declaration name.
44
+ - Component owners, component calls, hooks, keyed lists, effects, states, refs, and IDs expose deterministic source-local SiteIds in compiler analysis records.
45
+ - Repeated symbol resolutions are cached against their source dependencies and invalidate when any dependency source changes.
46
+
47
+ ### Performance
48
+
49
+ - The 100-importer fixture retains 103 parse and 103 summary misses while removing 100 unnecessary intermediate barrel clones, reducing importer-local clones from 200 to 100.
50
+ - Twenty-one alternating `v0.8.34`/candidate samples measured compiler medians of 760.967 ms and 755.595 ms. The paired candidate-minus-baseline median was -2.971 ms, with the candidate faster in 13/21 pairs; this is a small directional improvement, not a material speedup claim.
51
+ - Peak RSS medians were 256.9 MiB and 257.8 MiB. The 12,743-byte source-result increase is deterministic SiteId metadata in build scratch, not deployed JavaScript.
52
+ - A matched whole-build check measured Kudzu at 495.4 ms, Astro at 974.5 ms, Vue SSR at 1,477.3 ms, React SSR at 1,639.5 ms, and Svelte SSR at 2,033.1 ms. Kudzu shipped less JavaScript gzip than React, Vue, and Svelte; Astro's direct imperative implementation remained much smaller.
53
+
54
+ ### Validation
55
+
56
+ - `npm run check`, `npm test`, `npm run test:package`, and all 195 tests pass.
57
+ - Focused coverage verifies default/named exports, aliases, barrel chains, `export *`, ambiguity, cycles, repeated-session IDs, repeated-compilation IDs, resolution-cache invalidation, and clone-local declaration lookup.
58
+ - All five cross-framework device targets pass the same Chrome checks for initial HTML, filtering, empty state, detail fetch, superseded commands, stale-result rejection, and HTTP failures.
59
+ - Existing browser runtime code, route capabilities, source syntax, and deployed JavaScript remain unchanged; the repository build emits 150 pages.
60
+ - P0.9 semantic state operations is next.
61
+
62
+ ### Upgrade
63
+
64
+ ```bash
65
+ npm install @kudzujs/core@^0.8.35
66
+ ```
67
+
3
68
  ## 0.8.34 - Session-local module cache
4
69
 
5
70
  Kudzu 0.8.34 completes P0.7 by making source parsing and supported-export summarization proportional to unique modules within one ProjectSession instead of repeated importer edges.
@@ -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.34` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.7 parsed module and export summary caching is complete; P0.8 stable ModuleSymbol and SiteId is next. The plan 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.36` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.9 semantic state operations is complete; P0.10 ModuleIR reference unification is next. The plan 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.34` 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.36` 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,7 +8,7 @@ This maps the current `0.8.34` architecture, built on the completed `0.8.23` Goa
8
8
  |---|---|---|
9
9
  | CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
10
10
  | Project session | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs), `createProjectSession()` | Owns one absolute root, standard project paths, source records, bound graph operations, and Worker compiler for a build. Omitted roots resolve from call-time CWD. |
11
- | Parsed module cache | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs) | Parses each unchanged source module and summarizes its supported exports once per ProjectSession. Read-only graph/export/style/Worker consumers share the canonical tree; normalization receives a fresh deep clone with independent parent links. |
11
+ | Parsed module cache and symbols | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs) | Parses each unchanged source module once per ProjectSession and records source-local declaration/import/re-export sites. Stable ModuleSymbol records resolve direct, aliased, barrel, and `export *` exports with cycle and ambiguity checks; repeated resolutions are cached against their source dependencies, and normalization consumers locate the resolved SiteId in a fresh clone with independent parent links. |
12
12
  | Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteIR rendering, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
13
13
  | 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. |
14
14
  | 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. |
@@ -17,9 +17,9 @@ This maps the current `0.8.34` architecture, built on the completed `0.8.23` Goa
17
17
  | 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. |
18
18
  | 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. |
19
19
  | 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. |
20
- | 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. |
20
+ | 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, source-local SiteIds, and source provenance; AST identity remains private to its source-local session. |
21
21
  | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, imports, and client roots into ModuleIR and validates every local slot reference. |
22
- | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Supported command handlers specialize to JSON-safe ModuleIR, then emit the existing `__kBehavior` AST without changing route plans. |
22
+ | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
23
23
  | Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
24
24
  | Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback/binding/list AST rewriting and diagnostics before the JSON-safe IR boundary. |
25
25
  | Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
@@ -39,7 +39,7 @@ This maps the current `0.8.34` architecture, built on the completed `0.8.23` Goa
39
39
 
40
40
  ```text
41
41
  src/pages entries + config
42
- -> ProjectSession(root) with project paths, source records, parsed/export caches, graph, and Worker compiler
42
+ -> ProjectSession(root) with project paths, source records, parsed/symbol caches, graph, and Worker compiler
43
43
  -> project discovery and reachable relative graph
44
44
  -> compileSource()
45
45
  -> ordered normalization and parent repair
@@ -71,8 +71,8 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
71
71
  - Transient component rewrite indexes remain source-local AST indexes; handler, binding, derived, keyed, effect, and component ownership now have explicit JSON-safe source results.
72
72
  - `build()` still owns explicit artifact selection and filesystem writes after generator results are produced.
73
73
  - Runtime generators intentionally specialize readable authored sources through exact anchors; every required anchor fails closed, but a future generator format may remove this transitional dependency.
74
- - Source reachability and source compilation remain in one session-bound compiler factory because both consume the same normalization and import graph contracts. Export summaries deliberately preserve the existing narrow forms; stable ModuleSymbol and SiteId identity remains P0.8 work.
75
- - Imported, specialized, and compiler-synthesized trees still use conservative name/scope fallback where the source-local binding index does not own the complete AST; cross-module semantics remain deferred to stable ModuleSymbol and SiteId work.
74
+ - Source reachability and source compilation remain in one session-bound compiler factory because both consume the same normalization and import graph contracts. ModuleSymbol resolution is stable across canonical and cloned trees; unsupported export syntax remains intentionally narrow.
75
+ - Imported declarations resolve by ModuleSymbol and source-local SiteId. Specialized and compiler-synthesized trees still use conservative name/scope fallback where the source-local binding index does not own the complete AST.
76
76
 
77
77
  These are future simplification opportunities, not incomplete Goal A contracts. Goal A changed no source support, browser output semantics, or browser architecture.
78
78
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Active execution plan after `0.8.34`. 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.35`. 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
 
@@ -136,7 +136,8 @@ This is an incremental evolution of the current repository:
136
136
  - [x] P0.5 Staged and collision-safe output is complete in `0.8.32`. Production output completes in one project-local staging tree, public files are compared against generated route/runtime/handler/chunk/Worker/CSS paths while copying, `afterBuild` runs before rollback-safe promotion, and ordinary failures preserve the prior `dist`. Same-root overlap and stale locks fail closed; after stale-lock removal, an interrupted promotion backup recovers on the next admitted build. Focused collision/replacement/dev checks pass with equivalent `v0.8.31` commerce deploy output; `.kudzu` remains compiler scratch for P0.6.
137
137
  - [x] P0.6 Explicit ProjectSession is complete in `0.8.33`. Each build owns an absolute root, project paths, source records, bound graph operations, and Worker compiler. Explicit-root build/dev entry points preserve omitted-root CLI CWD behavior. One imported build function compiles two same-shaped roots with isolated config, HTML, `.kudzu`, source results, and Worker bundles; all 191 tests and package checks pass without browser or source-syntax changes.
138
138
  - [x] P0.7 Parsed module and export summary caching is complete in `0.8.34`. Canonical read-only source trees and narrow export summaries are invalidated together by source text and remain ProjectSession-local; every normalization context receives a deep clone with independent parent links. A 100-importer fixture parses and summarizes 103 unique page/barrel/component/helper modules exactly once and creates 200 importer-local clones. The maintained paired benchmark preserves the complete source-result digest and establishes no material timing or peak-RSS conclusion; all 193 tests and package checks pass without broadening exports, source syntax, or browser output.
139
- - [ ] P0.8 Stable ModuleSymbol and SiteId is next. Cross-module declarations, aliases, re-exports, owners, and call sites must stop depending on transient AST identity outside one pass.
139
+ - [x] P0.8 Stable ModuleSymbol and SiteId is complete in `0.8.35`. ProjectSession records source-local declaration, import, and re-export sites and resolves stable ModuleSymbol records through default/named exports, aliases, barrel chains, `export *`, ambiguity, and cycles. Cross-module compiler consumers locate resolved declarations by SiteId in their private normalized clones, while component calls, hooks, keyed blocks, effects, and ownership records expose deterministic source-local SiteIds. Repeated sessions and compilations preserve IDs, and symbol-only barrel traversal avoids cloning intermediate modules without changing browser artifacts or source syntax.
140
+ - [x] P0.9 Semantic State Operations is complete in `0.8.36`. Direct setters, one immutable state-value alias, one synchronous zero-argument arrow helper, and one synchronous one-parameter function helper lower to identical existing command HandlerIR. Binding identity proves state/setter/helper/parameter ownership; recursion, escape, mutation, and dynamic helper dispatch fail at authored source locations. Existing direct command specialization remains first, unrelated safe handlers retain native ESM, and no command ABI, runtime, JavaScript VM, or general expression IR is added.
140
141
 
141
142
  ### P0: Semantic Correctness And Compiler Foundation
142
143
 
@@ -320,6 +321,8 @@ Every result carries a stable source-local binding slot, debug name, declaration
320
321
 
321
322
  **Done condition:** cross-module semantic consumers use ModuleSymbol records and source-local SiteId values; readable names remain diagnostic metadata.
322
323
 
324
+ **Completed in `0.8.35`:** ProjectSession export summaries now include stable declaration/import/re-export records. ModuleSymbol identity is project-relative module path plus declaration SiteId and excludes readable names; cycle-safe resolution supports direct/default/named/aliased/barrel/`export *` paths and rejects ambiguity. Imported semantic consumers resolve symbols first, then find the authored SiteId in an importer-private normalized clone. Component analysis and ModuleIR ownership records carry deterministic owner, component-call, hook, keyed-list, and effect SiteIds. Focused fixtures verify default/named aliases, barrels, `export *`, cycles, ambiguity, repeated-session determinism, repeated-compilation determinism, and source-dependent resolution-cache invalidation. The 100-importer fixture retains 103 parse and summary misses while removing 100 intermediate barrel clones.
325
+
323
326
  ### PR 9: Semantic State Operations
324
327
 
325
328
  **Objective:** lower semantically equivalent state updates to the existing structured HandlerIR path.
@@ -337,6 +340,8 @@ function increment(value) { setCount(value + 1) }; increment(count)
337
340
 
338
341
  **Done condition:** the equivalent forms produce the same StateWrite semantics, browser result, ownership references, and zero-unused-runtime behavior; recursion, escape, mutation, and dynamic dispatch fail with explicit diagnostics.
339
342
 
343
+ **Completed in `0.8.36`:** The command specializer now performs narrow whole-handler analysis only when the existing direct fast path fails. The four required forms emit the same `add` command, signal slot, owner, build-module behavior call, RouteIR command, and browser command runtime; alias/helper forms emit no native handler module. Focused compiler and command-only fixture coverage proves JSON-safe IR equivalence, source diagnostics, no native behavior artifacts, and unchanged batching/runtime ABI. The maintained 100-importer benchmark retains identical output and cache counts with no material timing or peak-RSS conclusion.
344
+
340
345
  ### PR 10: ModuleIR Reference Unification
341
346
 
342
347
  **Objective:** replace mixed state names, export strings, formatted owner strings, and slots with validated source-local slots and SymbolRef records.
@@ -466,4 +471,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
466
471
 
467
472
  ## Immediate Decision
468
473
 
469
- PR 1 through PR 7 are complete. The next PR is **PR 8: Stable ModuleSymbol And SiteId**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
474
+ PR 1 through PR 9 are complete. The next PR is **PR 10: ModuleIR Reference Unification**. 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.34` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.36` 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
 
@@ -25,6 +25,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
25
25
  | `0.8.32` | Stage and validate production output before rollback-safe promotion, reject same-root overlap, and tighten keyed browser paths. | Build/hook failures preserve the prior output, stale locks fail closed, interrupted backups recover on the next admitted build after lock removal, public/generated collisions fail, and deploy output remains equivalent. |
26
26
  | `0.8.33` | Move root, graph, source records, compiler paths, and Worker ownership into an explicit build-scoped ProjectSession. | Two independent roots compile through one imported build entry without config, source, `.kudzu`, Worker, or output leakage; CLI CWD behavior remains unchanged. |
27
27
  | `0.8.34` | Cache canonical parsed modules and narrow export summaries within one ProjectSession while cloning transformer input. | A 100-importer fixture parses and summarizes 103 unique modules once, creates independent normalization trees, preserves the complete source-result digest, and leaks no cache state across projects. |
28
+ | `0.8.35` | Resolve cross-module declarations through stable ModuleSymbol and source-local SiteId records. | Default/named exports, aliases, barrel chains, `export *`, cycles, ambiguity, repeated compilation, source invalidation, private clones, and measured build behavior remain deterministic. |
29
+ | `0.8.36` | Lower equivalent direct, aliased, and local-helper state updates through existing command HandlerIR. | The four required forms emit identical commands and no native handler module; recursion, escape, mutation, and dynamic dispatch fail at authored source locations. |
28
30
 
29
31
  ## Sequence Rules
30
32
 
@@ -37,7 +39,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
37
39
 
38
40
  ## Generator Versions
39
41
 
40
- Keep `create-kudzu@0.1.101` while its template remains unchanged. Its `@kudzujs/core` range `^0.8.15` already accepts every planned Goal A patch. Publish `0.1.102` only when the template or minimum core range changes; a future `0.9.0` template must use `^0.9.0`.
42
+ `create-kudzu@0.1.102` adds an explicit install step to the generated README and clearer completion output. Its `@kudzujs/core` range `^0.8.15` accepts every current `0.8.x` patch; a future `0.9.0` template must use `^0.9.0`.
41
43
 
42
44
  ## Release Boundary
43
45
 
@@ -8,7 +8,7 @@ export function createComponentAnalysisSession(analysis) {
8
8
  function registerOwner(identity, descriptor = {}) {
9
9
  let owner = owners.get(identity)
10
10
  if (!owner) {
11
- owner = { slot: analysis.owners.length, kind: descriptor.kind ?? "component", name: descriptor.name ?? "anonymous", props: descriptor.props ?? [], states: [], setters: [], refs: [], ids: [], ...(descriptor.source ? { source: descriptor.source } : {}) }
11
+ owner = { slot: analysis.owners.length, kind: descriptor.kind ?? "component", name: descriptor.name ?? "anonymous", props: descriptor.props ?? [], states: [], setters: [], refs: [], ids: [], ...(descriptor.site ? { site: descriptor.site } : {}), ...(descriptor.source ? { source: descriptor.source } : {}) }
12
12
  owners.set(identity, owner)
13
13
  analysis.owners.push(owner)
14
14
  }
@@ -20,7 +20,7 @@ export function createComponentAnalysisSession(analysis) {
20
20
  if (!owner) throw new Error("Component owner must be registered before its state")
21
21
  let state = owner.states.find(entry => entry.name === descriptor.name && entry.owner === descriptor.owner)
22
22
  if (!state) {
23
- state = { slot: owner.states.length, name: descriptor.name, kind: descriptor.kind, ...(descriptor.owner ? { owner: descriptor.owner } : {}), ...(descriptor.source ? { source: descriptor.source } : {}) }
23
+ state = { slot: owner.states.length, name: descriptor.name, kind: descriptor.kind, ...(descriptor.owner ? { owner: descriptor.owner } : {}), ...(descriptor.site ? { site: descriptor.site } : {}), ...(descriptor.source ? { source: descriptor.source } : {}) }
24
24
  owner.states.push(state)
25
25
  }
26
26
  if (descriptor.setter && !owner.setters.some(entry => entry.name === descriptor.setter)) owner.setters.push({ name: descriptor.setter, signal: state.slot, kind: descriptor.kind })
@@ -149,8 +149,9 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
149
149
 
150
150
  function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
151
151
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
152
- const commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
153
- if (!commands.length || commands.some(command => !command)) return undefined
152
+ let commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
153
+ if ((!commands.length || commands.some(command => !command)) && compileEventCommand.handler) commands = compileEventCommand.handler(expression, setters, bindingIndex)
154
+ if (!commands?.length || commands.some(command => !command)) return undefined
154
155
  const original = ts.getOriginalNode(expression)
155
156
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
156
157
  const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
@@ -1,32 +1,203 @@
1
1
  import ts from "typescript"
2
+ import { sourceNodeError } from "../ast-helpers.mjs"
2
3
 
3
4
  export function createCommandSpecializer({ isPrimitiveLiteral }) {
4
- return function specializeCommand(expression, setters) {
5
+ const specialize = (expression, setters) => {
5
6
  if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
7
  return { operation: "log", state: expression.arguments[1].text, value: expression.arguments[0].text }
7
8
  }
8
-
9
9
  if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
10
  const state = setters.get(expression.expression.text)
11
11
  if (!state) return undefined
12
-
13
12
  const value = expression.arguments[0]
14
- if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state && ts.isNumericLiteral(value.right)) {
15
- if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
16
- const operand = signedNumber(value.right, value.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
17
- return operand ? { operation: "add", state, ...operand } : undefined
18
- }
19
- if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
- if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
- const operand = signedNumber(value.body.right, value.body.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
22
- return operand ? { operation: "add", state, ...operand } : undefined
23
- }
13
+ if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state) return addCommand(state, value)
14
+ if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text) return addCommand(state, value.body)
24
15
  if (isPrimitiveLiteral(value)) {
25
16
  const literal = primitiveValue(value)
26
17
  return literal ? { operation: "set", state, ...literal } : undefined
27
18
  }
28
- return undefined
29
19
  }
20
+ specialize.handler = (handler, setters, bindingIndex) => specializeHandler(handler, setters, bindingIndex, specialize)
21
+ return specialize
22
+ }
23
+
24
+ function specializeHandler(handler, setters, bindingIndex, specialize) {
25
+ const statements = ts.isBlock(handler.body) ? handler.body.statements.filter(statement => !ts.isEmptyStatement(statement)) : []
26
+ if (statements.length === 2) {
27
+ const command = aliasCommand(statements, handler, setters, bindingIndex) ?? helperCommand(statements, handler, setters, bindingIndex, specialize)
28
+ if (command) return [command]
29
+ }
30
+ rejectUnsafe(statements, handler, setters, bindingIndex)
31
+ }
32
+
33
+ function aliasCommand([first, second], boundary, setters, bindingIndex) {
34
+ const declaration = oneDeclaration(first)
35
+ if (!declaration || !(first.declarationList.flags & ts.NodeFlags.Const) || !declaration.initializer || !ts.isExpressionStatement(second)) return undefined
36
+ const value = stateAdd(declaration.initializer, boundary, setters, bindingIndex)
37
+ const call = second.expression
38
+ if (!value || !directCall(call) || call.arguments.length !== 1 || !ts.isIdentifier(call.arguments[0])) return undefined
39
+ const state = setters.get(call.expression.text)
40
+ if (state !== value.state || !captured(call.expression, boundary, call.expression.text, bindingIndex) || !declaredBy(call.arguments[0], declaration.name, boundary, bindingIndex) || refs(boundary, declaration.name, boundary, bindingIndex).length !== 1) return undefined
41
+ return { operation: "add", state, ...value.operand }
42
+ }
43
+
44
+ function helperCommand([first, second], boundary, setters, bindingIndex, specialize) {
45
+ if (!ts.isExpressionStatement(second) || !directCall(second.expression)) return undefined
46
+ const call = second.expression
47
+ let name
48
+ let command
49
+ const declaration = oneDeclaration(first)
50
+ if (declaration && first.declarationList.flags & ts.NodeFlags.Const && declaration.initializer && ts.isArrowFunction(declaration.initializer) && !declaration.initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !declaration.initializer.parameters.length && !ts.isBlock(declaration.initializer.body) && !call.arguments.length) {
51
+ name = declaration.name
52
+ command = safeDirect(declaration.initializer.body, boundary, setters, bindingIndex, specialize)
53
+ } else if (ts.isFunctionDeclaration(first) && first.name && first.body && !first.asteriskToken && !first.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) && first.parameters.length === 1 && first.body.statements.length === 1 && ts.isExpressionStatement(first.body.statements[0]) && call.arguments.length === 1 && ts.isIdentifier(call.arguments[0])) {
54
+ const parameter = first.parameters[0]
55
+ if (!ts.isIdentifier(parameter.name) || parameter.initializer || parameter.dotDotDotToken) return undefined
56
+ name = first.name
57
+ command = parameterCommand(first.body.statements[0].expression, parameter.name, first, call.arguments[0], boundary, setters, bindingIndex)
58
+ }
59
+ if (!name || !command || call.expression.text !== name.text || !declaredBy(call.expression, name, boundary, bindingIndex) || refs(boundary, name, boundary, bindingIndex).length !== 1) return undefined
60
+ return command
61
+ }
62
+
63
+ function safeDirect(expression, boundary, setters, bindingIndex, specialize) {
64
+ const command = specialize(expression, setters)
65
+ if (!command || command.operation === "log") return undefined
66
+ if (!captured(expression.expression, boundary, expression.expression.text, bindingIndex)) return undefined
67
+ const value = expression.arguments[0]
68
+ return !ts.isBinaryExpression(value) || captured(value.left, boundary, command.state, bindingIndex) ? command : undefined
69
+ }
70
+
71
+ function parameterCommand(expression, parameter, helper, argument, boundary, setters, bindingIndex) {
72
+ if (!directCall(expression) || expression.arguments.length !== 1 || !ts.isBinaryExpression(expression.arguments[0])) return undefined
73
+ const state = setters.get(expression.expression.text)
74
+ const value = expression.arguments[0]
75
+ if (!state || !captured(expression.expression, boundary, expression.expression.text, bindingIndex) || !ts.isIdentifier(value.left) || !declaredBy(value.left, parameter, helper, bindingIndex) || !captured(argument, boundary, state, bindingIndex)) return undefined
76
+ const operand = addOperand(value)
77
+ return operand ? { operation: "add", state, ...operand } : undefined
78
+ }
79
+
80
+ function rejectUnsafe(statements, boundary, setters, bindingIndex) {
81
+ if (!bindingIndex) return
82
+ for (const statement of statements) {
83
+ const declaration = oneDeclaration(statement)
84
+ if (declaration?.initializer && ts.isObjectLiteralExpression(declaration.initializer) && containsSetter(declaration.initializer, boundary, setters, bindingIndex)) {
85
+ const use = refs(boundary, declaration.name, boundary, bindingIndex).find(dynamic)
86
+ if (use) fail(use, "Semantic state helpers do not support dynamic dispatch")
87
+ }
88
+ if (declaration?.initializer && stateAdd(declaration.initializer, boundary, setters, bindingIndex)) {
89
+ const use = setterAliasUse(boundary, declaration.name, setters, bindingIndex)
90
+ if (!use) continue
91
+ const uses = refs(boundary, declaration.name, boundary, bindingIndex)
92
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const) || uses.some(mutated)) fail(declaration.name, "Semantic state aliases must remain immutable")
93
+ fail(uses.find(reference => reference !== use) ?? use, "Semantic state aliases must be passed directly to one setter and cannot escape")
94
+ }
95
+ const helper = helperDeclaration(statement, boundary, setters, bindingIndex)
96
+ if (!helper) continue
97
+ const uses = refs(boundary, helper.name, boundary, bindingIndex)
98
+ if (uses.some(reference => inside(reference, helper.body))) fail(helper.name, "Semantic state helpers cannot be recursive")
99
+ if (helper.mutable || uses.some(mutated)) fail(helper.name, "Semantic state helpers must remain immutable")
100
+ if (uses.some(dynamic)) fail(uses.find(dynamic), "Semantic state helpers do not support dynamic dispatch")
101
+ if (uses.length !== 1 || !directIdentifierCall(uses[0])) fail(uses.find(reference => !directIdentifierCall(reference)) ?? helper.name, "Semantic state helpers must be called exactly once and cannot escape")
102
+ fail(helper.body, "Semantic state helpers must contain one synchronous state operation")
103
+ }
104
+ }
105
+
106
+ function helperDeclaration(statement, boundary, setters, bindingIndex) {
107
+ const declaration = oneDeclaration(statement)
108
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) && containsSetter(declaration.initializer, boundary, setters, bindingIndex)) return { name: declaration.name, body: declaration.initializer, mutable: !(statement.declarationList.flags & ts.NodeFlags.Const) }
109
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body && containsSetter(statement.body, boundary, setters, bindingIndex)) return { name: statement.name, body: statement.body, mutable: false }
110
+ }
111
+
112
+ function stateAdd(expression, boundary, setters, bindingIndex) {
113
+ if (!ts.isBinaryExpression(expression) || !ts.isIdentifier(expression.left) || ![...setters.values()].includes(expression.left.text) || !captured(expression.left, boundary, expression.left.text, bindingIndex)) return undefined
114
+ const operand = addOperand(expression)
115
+ return operand ? { state: expression.left.text, operand } : undefined
116
+ }
117
+
118
+ function addCommand(state, expression) {
119
+ const operand = addOperand(expression)
120
+ return operand ? { operation: "add", state, ...operand } : undefined
121
+ }
122
+
123
+ function addOperand(expression) {
124
+ if (!ts.isBinaryExpression(expression) || !ts.isNumericLiteral(expression.right) || ![ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken].includes(expression.operatorToken.kind)) return undefined
125
+ return signedNumber(expression.right, expression.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
126
+ }
127
+
128
+ function oneDeclaration(statement) {
129
+ if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1) return undefined
130
+ const declaration = statement.declarationList.declarations[0]
131
+ return ts.isIdentifier(declaration.name) ? declaration : undefined
132
+ }
133
+
134
+ function directCall(node) {
135
+ return ts.isCallExpression(node) && !node.questionDotToken && ts.isIdentifier(node.expression)
136
+ }
137
+
138
+ function captured(identifier, boundary, name, bindingIndex) {
139
+ if (!ts.isIdentifier(identifier)) return false
140
+ const resolution = bindingIndex?.resolveReference(identifier, boundary)
141
+ return !resolution ? identifier.text === name : resolution.debugName === name && ["capture", "unresolved"].includes(resolution.kind)
142
+ }
143
+
144
+ function declaredBy(identifier, declaration, boundary, bindingIndex) {
145
+ const resolution = bindingIndex?.resolveReference(identifier, boundary)
146
+ return bindingIndex ? resolution?.declaration === declaration : identifier.text === declaration.text
147
+ }
148
+
149
+ function refs(root, declaration, boundary, bindingIndex) {
150
+ const found = []
151
+ const visit = node => {
152
+ if (ts.isIdentifier(node) && declaredBy(node, declaration, boundary, bindingIndex)) found.push(node)
153
+ ts.forEachChild(node, visit)
154
+ }
155
+ visit(root)
156
+ return found
157
+ }
158
+
159
+ function containsSetter(root, boundary, setters, bindingIndex) {
160
+ let found = false
161
+ const visit = node => {
162
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && captured(node.expression, boundary, node.expression.text, bindingIndex)) found = true
163
+ if (!found) ts.forEachChild(node, visit)
164
+ }
165
+ visit(root)
166
+ return found
167
+ }
168
+
169
+ function setterAliasUse(root, declaration, setters, bindingIndex) {
170
+ let found
171
+ const visit = node => {
172
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && ts.isIdentifier(node.arguments[0]) && declaredBy(node.arguments[0], declaration, root, bindingIndex)) found = node.arguments[0]
173
+ if (!found) ts.forEachChild(node, visit)
174
+ }
175
+ visit(root)
176
+ return found
177
+ }
178
+
179
+ function directIdentifierCall(identifier) {
180
+ return ts.isCallExpression(identifier.parent) && identifier.parent.expression === identifier && !identifier.parent.questionDotToken
181
+ }
182
+
183
+ function dynamic(identifier) {
184
+ if (ts.isCallExpression(identifier.parent) && identifier.parent.expression === identifier) return Boolean(identifier.parent.questionDotToken)
185
+ for (let current = identifier.parent; current && !ts.isStatement(current); current = current.parent) if (ts.isCallExpression(current)) return true
186
+ return false
187
+ }
188
+
189
+ function mutated(identifier) {
190
+ const parent = identifier.parent
191
+ return ts.isPrefixUnaryExpression(parent) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(parent.operator) || ts.isPostfixUnaryExpression(parent) || ts.isBinaryExpression(parent) && parent.left === identifier && parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
192
+ }
193
+
194
+ function inside(node, root) {
195
+ for (let current = node; current; current = current.parent) if (current === root) return true
196
+ return false
197
+ }
198
+
199
+ function fail(node, message) {
200
+ throw sourceNodeError(node, node.getSourceFile(), message)
30
201
  }
31
202
 
32
203
  function primitiveValue(node) {
@@ -1,5 +1,6 @@
1
- import { join, resolve } from "node:path"
1
+ import { join, relative, resolve, sep } from "node:path"
2
2
  import ts from "typescript"
3
+ import { sourceNodeError } from "./ast-helpers.mjs"
3
4
  import { assetPath } from "./path-helpers.mjs"
4
5
  import { createSourceGraph } from "./source-graph.mjs"
5
6
  import { createWorkerCompiler } from "./worker-compiler.mjs"
@@ -8,7 +9,7 @@ export function createProjectSession(projectRoot = process.cwd(), { counters, so
8
9
  const root = resolve(projectRoot)
9
10
  const sourceDirectory = join(root, "src")
10
11
  const graph = createSourceGraph(root)
11
- const modules = createModuleCache(sourceIndex, graph.parseSourceFile, counters)
12
+ const modules = createModuleCache(root, sourceIndex, graph, counters)
12
13
  return {
13
14
  root,
14
15
  sourceDirectory,
@@ -23,8 +24,19 @@ export function createProjectSession(projectRoot = process.cwd(), { counters, so
23
24
  }
24
25
  }
25
26
 
26
- function createModuleCache(sourceIndex, parseSourceFile, counters) {
27
+ function createModuleCache(root, sourceIndex, graph, counters) {
27
28
  const records = new Map()
29
+ let resolvedExports = new WeakMap()
30
+ const moduleName = file => relative(root, file).replaceAll(sep, "/")
31
+ const siteId = (node, role = "site") => {
32
+ const original = ts.getOriginalNode(node)
33
+ return `${role}:${original.getStart(original.getSourceFile())}:${original.end}`
34
+ }
35
+ const moduleSymbol = (file, node, name) => {
36
+ const site = siteId(node, "declaration")
37
+ const module = moduleName(file)
38
+ return { id: `${module}#${site}`, module, site, name }
39
+ }
28
40
  const increment = name => {
29
41
  if (counters) counters[name] = (counters[name] ?? 0) + 1
30
42
  }
@@ -32,12 +44,13 @@ function createModuleCache(sourceIndex, parseSourceFile, counters) {
32
44
  if (typeof source !== "string") throw new Error(`Source text is unavailable for ${file}`)
33
45
  const cached = records.get(file)
34
46
  if (cached?.source === source) return cached
35
- const sourceFile = parseSourceFile(file, source)
47
+ const sourceFile = graph.parseSourceFile(file, source)
36
48
  increment("parsedModules")
37
- const exports = summarizeExports(sourceFile)
49
+ const summary = summarizeModule(file, sourceFile, moduleSymbol, siteId)
38
50
  increment("exportSummaries")
39
- const record = { source, sourceFile, exports }
51
+ const record = { source, sourceFile, ...summary }
40
52
  records.set(file, record)
53
+ resolvedExports = new WeakMap()
41
54
  return record
42
55
  }
43
56
  const clone = (file, factory, context) => {
@@ -52,39 +65,121 @@ function createModuleCache(sourceIndex, parseSourceFile, counters) {
52
65
  increment("clonedModules")
53
66
  return sourceFile
54
67
  }
55
- return { clone, read }
68
+ const resolveExport = (file, exportName, sourceFiles) => {
69
+ let cached = resolvedExports.get(sourceFiles)
70
+ const cacheKey = `${file}:${exportName}`
71
+ const cachedResolution = cached?.get(cacheKey)
72
+ if (cachedResolution?.dependencies.every(([target, source]) => sourceIndex.get(target) === source)) return cachedResolution.symbol
73
+ const dependencies = new Set()
74
+ const resolveEntry = (currentFile, name, trail) => {
75
+ const key = `${currentFile}:${name}`
76
+ if (trail.includes(key)) return { cycle: [...trail, key] }
77
+ const record = read(currentFile)
78
+ dependencies.add(currentFile)
79
+ const entry = record.exports.get(name)
80
+ const nextTrail = [...trail, key]
81
+ if (entry?.kind === "local") {
82
+ const symbol = entry.symbol ?? record.declarations.get(entry.local)
83
+ return symbol ? { symbol } : {}
84
+ }
85
+ if (entry?.kind === "reexport") {
86
+ if (!entry.specifier.startsWith(".")) throw sourceNodeError(entry.node, record.sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
87
+ return resolveEntry(graph.resolveSourceImport(currentFile, entry.specifier, sourceFiles), entry.imported, nextTrail)
88
+ }
89
+ if (name === "default") return {}
90
+ const matches = new Map()
91
+ let cycle
92
+ for (const star of record.exportStars) {
93
+ if (!star.specifier.startsWith(".")) continue
94
+ const result = resolveEntry(graph.resolveSourceImport(currentFile, star.specifier, sourceFiles), name, nextTrail)
95
+ if (result.symbol) matches.set(result.symbol.id, result.symbol)
96
+ else cycle ??= result.cycle
97
+ }
98
+ if (matches.size > 1) throw new Error(`${moduleName(currentFile)} has an ambiguous export named ${JSON.stringify(name)}`)
99
+ return matches.size ? { symbol: matches.values().next().value } : cycle ? { cycle } : {}
100
+ }
101
+ const result = resolveEntry(file, exportName, [])
102
+ if (result.cycle) throw new Error(`Imported keyed list component re-export cycle: ${result.cycle.map(entry => moduleName(entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
103
+ cached = resolvedExports.get(sourceFiles) ?? new Map()
104
+ cached.set(cacheKey, { symbol: result.symbol, dependencies: [...dependencies].map(target => [target, sourceIndex.get(target)]) })
105
+ resolvedExports.set(sourceFiles, cached)
106
+ return result.symbol
107
+ }
108
+ const declaration = (symbol, sourceFile) => {
109
+ const file = resolve(root, symbol.module)
110
+ const record = read(file)
111
+ if (!sourceFile || sourceFile === record.sourceFile) return record.declarationSites.get(symbol.site)
112
+ for (const statement of sourceFile.statements) {
113
+ if (ts.isFunctionDeclaration(statement) && siteId(statement, "declaration") === symbol.site) return statement
114
+ if (ts.isVariableStatement(statement)) for (const entry of statement.declarationList.declarations) {
115
+ if (siteId(entry, "declaration") === symbol.site) return entry
116
+ }
117
+ }
118
+ }
119
+ return { clone, declaration, read, resolveExport, siteId, symbol: moduleSymbol }
56
120
  }
57
121
 
58
- function summarizeExports(sourceFile) {
122
+ function summarizeModule(file, sourceFile, moduleSymbol, siteId) {
59
123
  const exports = new Map()
124
+ const imports = new Map()
125
+ const declarations = new Map()
126
+ const declarationSites = new Map()
127
+ const exportStars = []
60
128
  const add = (name, value) => {
61
129
  if (!exports.has(name)) exports.set(name, value)
62
130
  }
131
+ const declare = (name, node) => {
132
+ const symbol = moduleSymbol(file, node, name)
133
+ if (!declarations.has(name)) declarations.set(name, symbol)
134
+ declarationSites.set(symbol.site, node)
135
+ return symbol
136
+ }
63
137
  for (const statement of sourceFile.statements) {
138
+ if (ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier)) {
139
+ const specifier = statement.moduleSpecifier.text
140
+ const clause = statement.importClause
141
+ if (clause?.name) imports.set(clause.name.text, { kind: "default", imported: "default", specifier, site: siteId(clause.name, "import") })
142
+ if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) imports.set(clause.namedBindings.name.text, { kind: "namespace", imported: "*", specifier, site: siteId(clause.namedBindings, "import") })
143
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) {
144
+ if (!element.isTypeOnly) imports.set(element.name.text, { kind: "named", imported: (element.propertyName ?? element.name).text, specifier, site: siteId(element, "import") })
145
+ }
146
+ continue
147
+ }
64
148
  if (ts.isFunctionDeclaration(statement)) {
65
149
  const exported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
66
150
  const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
67
- if (isDefault) add("default", { kind: "function", local: statement.name?.text })
68
- if (exported && statement.name) add(statement.name.text, { kind: "function", local: statement.name.text })
151
+ const name = statement.name?.text ?? (isDefault ? "default" : undefined)
152
+ const symbol = name ? declare(name, statement) : undefined
153
+ if (isDefault && symbol) add("default", { kind: "local", symbol })
154
+ if (exported && !isDefault && statement.name && symbol) add(statement.name.text, { kind: "local", local: statement.name.text, symbol })
69
155
  continue
70
156
  }
71
- if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
72
- for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) add(declaration.name.text, { kind: "local", local: declaration.name.text })
157
+ if (ts.isVariableStatement(statement)) {
158
+ const exported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
159
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) {
160
+ const symbol = declare(declaration.name.text, declaration)
161
+ if (exported) add(declaration.name.text, { kind: "local", local: declaration.name.text, symbol })
162
+ }
73
163
  continue
74
164
  }
75
165
  if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
76
166
  add("default", { kind: "local", local: statement.expression.text })
77
167
  continue
78
168
  }
79
- if (!ts.isExportDeclaration(statement) || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue
169
+ if (!ts.isExportDeclaration(statement)) continue
170
+ if (!statement.exportClause && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
171
+ exportStars.push({ specifier: statement.moduleSpecifier.text, site: siteId(statement, "reexport") })
172
+ continue
173
+ }
174
+ if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue
80
175
  for (const element of statement.exportClause.elements) {
81
176
  if (element.isTypeOnly) continue
82
177
  const name = element.name.text
83
178
  const imported = (element.propertyName ?? element.name).text
84
179
  add(name, statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
85
- ? { kind: "reexport", imported, specifier: statement.moduleSpecifier.text, node: statement }
180
+ ? { kind: "reexport", imported, specifier: statement.moduleSpecifier.text, site: siteId(element, "reexport"), node: statement }
86
181
  : { kind: "local", local: imported })
87
182
  }
88
183
  }
89
- return exports
184
+ return { declarations, declarationSites, exports, exportStars, imports }
90
185
  }
@@ -324,6 +324,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
324
324
  const original = ts.getOriginalNode(node)
325
325
  return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
326
326
  }
327
+ const analysisSite = (node, role) => {
328
+ const original = ts.getOriginalNode(node)
329
+ return original.pos >= 0 && original.end >= 0 ? modules.siteId(original, role) : undefined
330
+ }
327
331
  const analyzedProps = owner => {
328
332
  if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
329
333
  return owner.parameters[0].name.elements.map(element => ({
@@ -334,14 +338,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
334
338
  }))
335
339
  }
336
340
  const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
337
- const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
341
+ const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), site: analysisSite(owner, "owner"), source: analysisSource(owner) })
338
342
  const registerState = (owner, state, setter, kind, node, externalOwner) => {
339
343
  const ownerRecord = ensureOwner(owner)
340
344
  const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
341
345
  const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
342
346
  stateOwners.set(state, stateOwner)
343
347
  stateOwnersByFunction.set(owner, stateOwners)
344
- return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
348
+ return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), site: analysisSite(node, "hook"), source: analysisSource(node) })
345
349
  }
346
350
  const stateOwnersForNode = node => {
347
351
  for (let current = node.parent; current; current = current.parent) {
@@ -643,12 +647,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
643
647
  if (!nullInitializer && owner.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) throw sourceNodeError(node.initializer, sourceFile, "Mutable value useRef() is unsupported except for an effect-owned useRef(0) animation-frame handle; otherwise keep resource-private mutable values inside the owning effect")
644
648
  if (nullInitializer) {
645
649
  ensureOwner(owner)
646
- componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
650
+ componentAnalysis.registerRef(owner, { name: node.name.text, site: analysisSite(node, "hook"), source: analysisSource(node) })
647
651
  }
648
652
  }
649
653
  if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
650
654
  ensureOwner(owner)
651
- componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
655
+ componentAnalysis.registerId(owner, { name: node.name.text, site: analysisSite(node, "hook"), source: analysisSource(node) })
652
656
  }
653
657
  }
654
658
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
@@ -834,6 +838,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
834
838
  result.analysis = componentAnalysis.registerSpecialization({
835
839
  kind: label,
836
840
  ...(owner ? { owner: ensureOwner(owner).slot } : {}),
841
+ ...(analysisSite(call, "component-call") ? { site: analysisSite(call, "component-call") } : {}),
837
842
  ...(analysisSource(call) ? { source: analysisSource(call) } : {}),
838
843
  props: result.props.map(prop => {
839
844
  const expression = result.propExpressions.get(prop.name)
@@ -841,11 +846,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
841
846
  return { ...prop, ...(signals.length ? { signals } : {}) }
842
847
  }),
843
848
  states: [
844
- ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
845
- ...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
849
+ ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
850
+ ...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
846
851
  ],
847
- refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
848
- ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
852
+ refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
853
+ ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
849
854
  })
850
855
  for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
851
856
  for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
@@ -1427,6 +1432,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1427
1432
  const rowRefs = (listParts.rowRefs ?? []).map(ref => ({ name: ref.name, owner: ref.analysisOwner, ...(analysisSource(ref.source) ? { source: analysisSource(ref.source) } : {}) }))
1428
1433
  const specializations = [...new Set([...(listParts.specializations ?? []), ...rowStates.map(state => state.owner), ...rowRefs.map(ref => ref.owner)].filter(value => value !== undefined).map(value => typeof value === "string" ? Number(value.slice(value.lastIndexOf(":") + 1)) : value))]
1429
1434
  const block = descriptors.registerKeyedBlock({
1435
+ ...(analysisSite(node, "keyed-list") ? { site: analysisSite(node, "keyed-list") } : {}),
1430
1436
  ...(analysisSource(node) ? { source: analysisSource(node) } : {}),
1431
1437
  ...(parent ? { parent: parent.slot } : {}),
1432
1438
  children: [],
@@ -1607,9 +1613,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1607
1613
  ownership: {
1608
1614
  kind: activeKeyedBlock ? "keyed" : "component",
1609
1615
  ...(activeKeyedBlock ? { keyedBlock: activeKeyedBlock.block.slot } : {}),
1610
- ...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
1616
+ ...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSite(lexicalOwner, "owner") ? { site: analysisSite(lexicalOwner, "owner") } : {}), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
1611
1617
  },
1612
1618
  workers,
1619
+ ...(analysisSite(effectSource, "hook") ? { site: analysisSite(effectSource, "hook") } : {}),
1613
1620
  ...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
1614
1621
  })
1615
1622
  const dependencyExpressions = effect.dependencies.map(dependency => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependency.name])
@@ -2719,25 +2726,12 @@ function normalizeImportedStaticCollections(sourceFile, collections, factory, co
2719
2726
  return ts.visitNode(sourceFile, visitor)
2720
2727
  }
2721
2728
 
2722
- function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
2723
- const key = `${file}:${exportName}`
2724
- if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
2725
- const sourceFile = getSource(file)
2726
- const nextTrail = [...trail, key]
2727
- const entry = modules.read(file).exports.get(exportName)
2728
- if (entry?.kind === "reexport") {
2729
- if (!entry.specifier.startsWith(".")) throw sourceNodeError(entry.node, modules.read(file).sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
2730
- const target = resolveSourceImport(file, entry.specifier, sourceFiles)
2731
- return resolveComponentExport(target, entry.imported, getSource, sourceFiles, nextTrail)
2732
- }
2733
- if (entry?.kind === "function" && !entry.local) {
2734
- const component = sourceFile.statements.find(statement => ts.isFunctionDeclaration(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword))
2735
- if (component) return component
2736
- }
2737
- if (entry?.local) {
2738
- const component = localComponentDeclaration(sourceFile, entry.local)
2739
- if (component) return component
2740
- }
2729
+ function resolveComponentExport(file, exportName, getSource, sourceFiles) {
2730
+ const symbol = modules.resolveExport(file, exportName, sourceFiles)
2731
+ const target = symbol && resolve(root, symbol.module)
2732
+ const declaration = symbol && modules.declaration(symbol, getSource(target))
2733
+ if (declaration && ts.isFunctionDeclaration(declaration)) return declaration
2734
+ if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
2741
2735
  throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
2742
2736
  }
2743
2737
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.34",
3
+ "version": "0.8.36",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",