@kudzujs/core 0.16.7 → 0.16.9
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 +1 -1
- package/PERFORMANCE.md +25 -0
- package/README.md +1 -1
- package/RELEASES.md +63 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/application-capability-release-plan.md +61 -1
- package/docs/next-architecture/compiler-current-architecture.md +4 -2
- package/docs/next-architecture/large-application-ai-native-roadmap.md +3 -3
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/README.md +1 -1
- package/framework/build.mjs +2 -0
- package/framework/compiler/browser-signal-passes.mjs +2 -1
- package/framework/compiler/compatibility-registry.mjs +141 -0
- package/framework/compiler/descriptor-session.mjs +1 -1
- package/framework/compiler/effect-private-ref-pass.mjs +4 -2
- package/framework/compiler/react-migration-pass.mjs +5 -4
- package/framework/compiler/router-pass.mjs +3 -2
- package/framework/compiler/source-compiler.mjs +6 -5
- package/framework/compiler/source-graph.mjs +10 -3
- package/framework/compiler/zustand-pass.mjs +4 -3
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -51,7 +51,7 @@ Syntax compatibility does not mean reproducing React wholesale. Kudzu accepts th
|
|
|
51
51
|
- stronger JavaScript failure resilience;
|
|
52
52
|
- no remaining material large-route build-scaling weakness.
|
|
53
53
|
|
|
54
|
-
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.
|
|
54
|
+
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.19.1` in the capability release plan below. Benchmark-only feature omission, unmatched accessibility, weighted scores that hide losses, and unrecorded environment differences do not count as proof.
|
|
55
55
|
|
|
56
56
|
### 0.10.0 Through 0.21.x: Application Capability Release Train
|
|
57
57
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, `npm run benchmark:module-cache`, `npm run benchmark:project-navigation`, `npm run benchmark:project-state`, and `npm run benchmark:source-scale` 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
|
+
## 0.16.9 Compatibility Boundary And Inventory
|
|
6
|
+
|
|
7
|
+
Measured 2026-08-31 on Linux x64 with Node 24.14.0 and required Chrome. The
|
|
8
|
+
compatibility inventory is compiler scratch generated from reachable original
|
|
9
|
+
source before normalization; it emits no deploy file or browser capability.
|
|
10
|
+
Existing project-output baselines and package-specific diagnostics pass without
|
|
11
|
+
change. The maintained Worker graph remains 907 raw / 477 gzip B and the window
|
|
12
|
+
graph remains 14,456 raw / 6,160 gzip B. Seven clean builds record a 532.6 ms
|
|
13
|
+
median on this loaded host; timing is not compared.
|
|
14
|
+
|
|
15
|
+
## 0.16.8 Shared Lazy Capability Graph
|
|
16
|
+
|
|
17
|
+
Measured 2026-08-31 on Linux x64 with Node 24.14.0 and Chrome. Two route owners
|
|
18
|
+
share one deferred CodeMirror chunk measuring 250,086 raw / 80,890 aggregate
|
|
19
|
+
gzip B. Their eager graphs are 21,599 raw / 9,466 gzip B and 21,609 raw / 9,468
|
|
20
|
+
gzip B. The static sibling remains complete HTML with zero JavaScript.
|
|
21
|
+
|
|
22
|
+
Required Chrome proves zero deferred requests before interaction, one request on
|
|
23
|
+
the first owner, no navigation-triggered prefetch, no duplicate request for the
|
|
24
|
+
second owner, and exact owner/document cleanup. Native ESM supplies the
|
|
25
|
+
document-lifetime cache; no loader, prefetch registry, cache, or runtime concept
|
|
26
|
+
was added. The maintained Worker benchmark remains 907 raw / 477 gzip B for the
|
|
27
|
+
Worker graph and 14,456 raw / 6,160 gzip B for the window graph. Seven clean
|
|
28
|
+
builds record a 696.6 ms median on this loaded host; timing is not compared.
|
|
29
|
+
|
|
5
30
|
## 0.16.7 Lazy Retained Editor Lifecycle
|
|
6
31
|
|
|
7
32
|
Measured 2026-08-30 on macOS arm64 with Node 24.14.0 and Chrome. The existing
|
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.16.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.16.
|
|
17
|
+
**Latest release: 0.16.9 - Compatibility boundary and inventory.** Reachable authored package sites now receive deterministic compatibility classes and source ranges in a compiler-only report without entering semantic IR or browser output. Read the [release notes](./RELEASES.md#0169---compatibility-boundary-and-inventory), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.16.9), 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,68 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.16.9 - Compatibility Boundary And Inventory
|
|
4
|
+
|
|
5
|
+
Kudzu 0.16.9 records deterministic package compatibility provenance from the
|
|
6
|
+
reachable authored source graph without changing semantic IR or browser output.
|
|
7
|
+
|
|
8
|
+
### Changed in 0.16.9
|
|
9
|
+
|
|
10
|
+
- Centralizes package identities and compatibility classifications used by
|
|
11
|
+
React, React Router, React Bootstrap, Zustand, `clsx`, and browser-signal
|
|
12
|
+
normalization.
|
|
13
|
+
- Writes `.kudzu/kudzu-compatibility.json` from original, pre-normalization
|
|
14
|
+
source with package/API identity and exact line/column ranges.
|
|
15
|
+
- Classifies sites as Native, Compiled, Normalized, Adapter, Owned External UI,
|
|
16
|
+
Partial, or Unsupported with stable rule names and deterministic ordering.
|
|
17
|
+
- Includes only modules reachable from page entries; unused migration source is
|
|
18
|
+
excluded from the report just as it is from compilation.
|
|
19
|
+
- Keeps package origin out of ComponentAnalysis, ModuleIR, RouteIR, codegen,
|
|
20
|
+
runtime modules, and deploy output. No plugin API or browser capability is
|
|
21
|
+
added.
|
|
22
|
+
- Updates `create-kudzu@0.1.134` to generate projects on
|
|
23
|
+
`@kudzujs/core@^0.16.9`.
|
|
24
|
+
|
|
25
|
+
### Upgrade
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install @kudzujs/core@^0.16.9
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 0.16.8 - Shared Lazy Capability Graphs
|
|
32
|
+
|
|
33
|
+
Kudzu 0.16.8 shares one deferred package graph across route owners while
|
|
34
|
+
keeping loading interaction-triggered and ownership validation lexical.
|
|
35
|
+
|
|
36
|
+
### Changed in 0.16.8
|
|
37
|
+
|
|
38
|
+
- Records one deferred CodeMirror chunk as shared by both route owners without
|
|
39
|
+
duplicating its package graph.
|
|
40
|
+
- Keeps the shared chunk out of route HTML, module preload, navigation prefetch,
|
|
41
|
+
and static siblings until an owner activates.
|
|
42
|
+
- Uses the native browser module map for document-scoped deduplication and adds
|
|
43
|
+
no loader, prefetch registry, cache, or browser runtime.
|
|
44
|
+
- Resolves lazy `useEffect` ownership through lexical imports so a local
|
|
45
|
+
same-named function cannot bypass ordinary dynamic-import diagnostics.
|
|
46
|
+
- Preserves exact owner cleanup; native in-flight imports remain non-cancellable
|
|
47
|
+
and late callbacks cannot mount after disposal.
|
|
48
|
+
- Updates `create-kudzu@0.1.133` to generate projects on
|
|
49
|
+
`@kudzujs/core@^0.16.8`.
|
|
50
|
+
|
|
51
|
+
### Output And Browser Evidence
|
|
52
|
+
|
|
53
|
+
- Both owners share one 250,086 raw / 80,890 gzip byte deferred chunk. Their
|
|
54
|
+
eager graphs are 21,599 raw / 9,466 gzip and 21,609 raw / 9,468 gzip bytes.
|
|
55
|
+
- Required Chrome proves zero initial requests, one first-owner request, no
|
|
56
|
+
route-prefetch request, no second-owner duplicate, and exact cleanup.
|
|
57
|
+
- The static sibling emits zero JavaScript. The maintained Worker and window
|
|
58
|
+
graphs remain 907 raw / 477 gzip bytes and 14,456 raw / 6,160 gzip bytes.
|
|
59
|
+
|
|
60
|
+
### Upgrade
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install @kudzujs/core@^0.16.8
|
|
64
|
+
```
|
|
65
|
+
|
|
3
66
|
## 0.16.7 - Lazy Retained Editor Lifecycle
|
|
4
67
|
|
|
5
68
|
Kudzu 0.16.7 applies owner-triggered package loading to the real retained
|
|
@@ -15,7 +15,7 @@ The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](.
|
|
|
15
15
|
|
|
16
16
|
[`1.0-large-application-compatibility-audit.md`](./1.0-large-application-compatibility-audit.md) records the first post-0.9 probes against Memos, Apache Answer, and Actual Budget. The audit finds that reduced slices build but whole-application source retention and behavior parity do not yet pass.
|
|
17
17
|
|
|
18
|
-
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.
|
|
18
|
+
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.19.1`. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
|
|
19
19
|
|
|
20
20
|
## Required Invariants
|
|
21
21
|
|
|
@@ -1570,6 +1570,37 @@ packet ships as the actual `0.16.7` patch to retain the public `0.16.x` line.
|
|
|
1570
1570
|
- **Done condition:** policy minimizes unused transfer while meeting accepted
|
|
1571
1571
|
interaction latency.
|
|
1572
1572
|
|
|
1573
|
+
**Completion evidence:** two enhanced-navigation route owners now use one
|
|
1574
|
+
deferred CodeMirror graph. Existing multi-entry esbuild splitting emits one
|
|
1575
|
+
250,086 raw / 80,890 gzip byte chunk, and the existing artifact report records
|
|
1576
|
+
that exact lazy path as shared by `/` and `/second`. Their eager graphs remain
|
|
1577
|
+
21,599 raw / 9,466 gzip and 21,609 raw / 9,468 gzip bytes. Neither route HTML
|
|
1578
|
+
preloads the deferred chunk, navigation document prefetch does not request it,
|
|
1579
|
+
and the static sibling remains complete HTML with 0 B JavaScript.
|
|
1580
|
+
|
|
1581
|
+
Required Chrome proves zero requests before interaction, one request when the
|
|
1582
|
+
first owner activates, exact disposal, no route-triggered request, and no second
|
|
1583
|
+
request when the second owner activates. The selected policy is therefore no
|
|
1584
|
+
optional package preload: interaction calls native `import()` immediately, the
|
|
1585
|
+
browser module map deduplicates it for the document lifetime, and Kudzu owns no
|
|
1586
|
+
additional cache. Native ESM imports cannot be cancelled after request; owner
|
|
1587
|
+
cleanup invalidates the callback and prevents a late mount rather than claiming
|
|
1588
|
+
network cancellation.
|
|
1589
|
+
|
|
1590
|
+
The released lazy-import gate is also hardened before closing the packet.
|
|
1591
|
+
`ownedLazyPackageImport()` now resolves the callback callee through the existing
|
|
1592
|
+
lexical binding index and accepts only named `useEffect` imports from React or
|
|
1593
|
+
Kudzu. A local function with the same text can no longer bypass the ordinary
|
|
1594
|
+
dynamic-import diagnostic. The packet adds no semantic primitive, IR kind,
|
|
1595
|
+
compiler pass, public API, runtime concept/file, loader, prefetch registry, or
|
|
1596
|
+
cache. Four existing compiler files change by 17 added and eight removed lines.
|
|
1597
|
+
The artifact collector covers ten fixtures and 32 routes. `npm run check`, 288
|
|
1598
|
+
required-Chrome tests, package smoke, and the maintained benchmark pass. Worker
|
|
1599
|
+
and window graphs remain 907 raw / 477 gzip and 14,456 raw / 6,160 gzip bytes;
|
|
1600
|
+
the loaded Linux host records a 696.6 ms seven-build median without a timing
|
|
1601
|
+
comparison. This packet ships as the actual `0.16.8` patch to retain the public
|
|
1602
|
+
`0.16.x` line.
|
|
1603
|
+
|
|
1573
1604
|
## `0.19.x`: React Ecosystem Migration
|
|
1574
1605
|
|
|
1575
1606
|
### `0.19.0`: Compatibility Boundary And Inventory
|
|
@@ -1584,6 +1615,34 @@ packet ships as the actual `0.16.7` patch to retain the public `0.16.x` line.
|
|
|
1584
1615
|
- **Done condition:** existing React Router, Bootstrap, Zustand, and browser
|
|
1585
1616
|
normalizations preserve output and diagnostics.
|
|
1586
1617
|
|
|
1618
|
+
Implementation and release-candidate evidence complete. One internal registry
|
|
1619
|
+
now owns package identities and classifies original reachable import, re-export,
|
|
1620
|
+
and literal dynamic-import sites before normalization. The deterministic
|
|
1621
|
+
`.kudzu/kudzu-compatibility.json` report records package/API identity, stable
|
|
1622
|
+
rule, Native, Compiled, Normalized, Adapter, Owned External UI, Partial, or
|
|
1623
|
+
Unsupported treatment, and exact authored line/column range. Type-only,
|
|
1624
|
+
relative, Node, Kudzu-first-party, and unreachable source stays outside the
|
|
1625
|
+
inventory.
|
|
1626
|
+
|
|
1627
|
+
React Router, React Bootstrap, Zustand, React migration, browser-signal, `clsx`,
|
|
1628
|
+
and i18next recognition reuse the registry's package identities. Focused tests
|
|
1629
|
+
cover all seven classes, aliases, exact ranges, deterministic ordering, and a
|
|
1630
|
+
real build where an unreachable unsupported module is excluded. Existing
|
|
1631
|
+
project output baselines and package diagnostics remain green. Package origin
|
|
1632
|
+
does not enter SourceResult, ComponentAnalysis, ModuleIR, RouteIR, codegen,
|
|
1633
|
+
runtime files, `afterBuild()`, or deploy output.
|
|
1634
|
+
|
|
1635
|
+
The slice adds zero semantic primitives, IR kinds, compiler passes,
|
|
1636
|
+
normalization entries, runtime concepts/files, browser bytes, or public APIs.
|
|
1637
|
+
Production compiler/orchestration changes are 161 added and 13 removed lines,
|
|
1638
|
+
including the 141-line registry, for +148 net compatibility/build lines and zero
|
|
1639
|
+
core semantic LOC. `npm run check`, 290 required-Chrome tests, package smoke,
|
|
1640
|
+
and the maintained benchmark pass. Worker and window graphs remain 907 raw /
|
|
1641
|
+
477 gzip and 14,456 raw / 6,160 gzip bytes; seven clean Linux x64 builds record
|
|
1642
|
+
a 532.6 ms median without a timing comparison. This packet ships as the actual
|
|
1643
|
+
`0.16.9` patch, with `create-kudzu@0.1.134`, to retain the public
|
|
1644
|
+
`0.16.x` line.
|
|
1645
|
+
|
|
1587
1646
|
### `0.19.1`: Apache Answer Authentication Journey
|
|
1588
1647
|
|
|
1589
1648
|
- **Purpose:** promote the existing build-only auth fixture to browser evidence.
|
|
@@ -1868,4 +1927,5 @@ release transaction where possible or document and publish a forward-fix patch.
|
|
|
1868
1927
|
| `0.18.0` | Closed by existing artifact reporting | Preserve exact route/source ownership, bytes, hashes, preload equality, zero-JavaScript controls, and bounded prefetch policy. | No production change; no release consumed |
|
|
1869
1928
|
| `0.18.1` | Released as `0.16.6` | Preserve one guarded literal package edge, native module-map deduplication, exact owner cleanup, deferred artifact ownership, and static exclusion. | Patch release retained the `0.16.x` public version line |
|
|
1870
1929
|
| `0.18.2` | Released as `0.16.7` | Preserve on-demand retained editor mount, bidirectional updates, failure recovery, identity, exact cleanup, cached fresh remount, and static exclusion. | Patch release retained the `0.16.x` public version line |
|
|
1871
|
-
| `0.18.3` |
|
|
1930
|
+
| `0.18.3` | Released as `0.16.8` | Preserve shared deferred ownership, interaction-only loading, native document module-map deduplication, exact owner cleanup, binding-aware import validation, and static exclusion. | Patch release retained the `0.16.x` public version line |
|
|
1931
|
+
| `0.19.0` | Released as `0.16.9` | Preserve the compatibility registry, reachable source inventory, exact ranges, and package-neutral output boundary. | Patch release retained the `0.16.x` public version line |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.16.
|
|
3
|
+
This maps the current `0.16.9` architecture, built on the completed `0.9.0` semantic-compression release and `0.8.23` Goal A compiler foundation. The active application packet is `0.19.1`; file and function names are the stable references, while line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -10,7 +10,8 @@ This maps the current `0.16.7` architecture, built on the completed `0.9.0` sema
|
|
|
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. Production builds use one session; development retains it across rebuilds. Omitted roots resolve from call-time CWD. |
|
|
11
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()`, `buildWithSession()` | Coordinates config, discovery, source compilation, RouteBuildRecord collection, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. A retained session caches source results and pre-family route renders by page graph; successful builds alone replace that cache. |
|
|
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. |
|
|
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. A lazy package edge must resolve lexically to a named React or Kudzu `useEffect` import. |
|
|
14
|
+
| Compatibility inventory | [`framework/compiler/compatibility-registry.mjs`](../../framework/compiler/compatibility-registry.mjs), `createCompatibilityReport()` | Classifies package/API sites in reachable original source as Native, Compiled, Normalized, Adapter, Owned External UI, Partial, or Unsupported and writes deterministic source ranges to `.kudzu/kudzu-compatibility.json`. Package provenance ends here and never enters semantic IR or browser output. |
|
|
14
15
|
| 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. |
|
|
15
16
|
| 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. |
|
|
16
17
|
| Shared AST/scope helpers | [`framework/compiler/ast-helpers.mjs`](../../framework/compiler/ast-helpers.mjs) | Binding, scope, reference, inclusive ancestry, effect-return, and source-location analysis. |
|
|
@@ -43,6 +44,7 @@ This maps the current `0.16.7` architecture, built on the completed `0.9.0` sema
|
|
|
43
44
|
src/pages entries + config
|
|
44
45
|
-> ProjectSession(root) with project paths, source records, parsed/symbol caches, graph, and Worker compiler
|
|
45
46
|
-> project discovery and reachable relative graph
|
|
47
|
+
-> classify original package sites into the scratch compatibility report
|
|
46
48
|
-> in development, intersect changed paths with current and prior per-page graphs
|
|
47
49
|
-> reuse unaffected SourceResult and pre-family route render records
|
|
48
50
|
-> compileSource()
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.
|
|
5
|
+
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.19.1`. This document does not mark any remaining 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
|
-
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.
|
|
7
|
+
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.19.1`; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
|
|
8
8
|
|
|
9
9
|
## Product Outcome
|
|
10
10
|
|
|
@@ -483,4 +483,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
483
483
|
|
|
484
484
|
## Immediate Decision
|
|
485
485
|
|
|
486
|
-
All listed foundation and `0.9` slices are complete. Current work continues at `0.
|
|
486
|
+
All listed foundation and `0.9` slices are complete. Current work continues at `0.19.1` Apache Answer Authentication Journey under the [`application-capability-release-plan.md`](./application-capability-release-plan.md); keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
|
|
@@ -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.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.19.1`; that plan supersedes the provisional tool-first 0.10/0.11/0.12 ordering in the completed 0.9 handoff.
|
|
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
|
|
|
@@ -66,7 +66,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
66
66
|
|
|
67
67
|
## Generator Versions
|
|
68
68
|
|
|
69
|
-
`create-kudzu@0.1.
|
|
69
|
+
`create-kudzu@0.1.134` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.16.9`.
|
|
70
70
|
|
|
71
71
|
## Release Boundary
|
|
72
72
|
|
package/framework/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Reduced Zustand migration stores lower through package-neutral `SharedStateIR` a
|
|
|
78
78
|
|
|
79
79
|
Compiler ownership follows explicit stages. `build.mjs` coordinates project discovery, source compilation, RouteIR rendering, RouteBuildRecord collection, CapabilityIR planning, generator calls, and artifact emission. `compiler/normalization-pipeline.mjs` owns pass order and parent repair. The main transformer analyzes normalized source while `compiler/descriptor-session.mjs` owns one per-source semantic artifact containing HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, and client imports. `core.mjs` emits complete HTML, RouteIR v1, and exact handler references: route-local state `slot` is an internal array reference, `id` remains the browser/DOM identity, and `name` remains readable development metadata. `compiler/route-build-record.mjs` validates structural artifact edges, and `compiler/route-capability-planner.mjs` projects CapabilityIR v1 from those records. Focused codegen modules consume IR/descriptors and return route-specific source without source analysis or filesystem ownership. The completed architecture record lives in `docs/next-architecture`; it adds no runtime or accepted syntax by itself.
|
|
80
80
|
|
|
81
|
-
`.kudzu/kudzu-artifacts.json` records each route's exact CapabilityIR signature, assigned emitted runtime family, handler, Worker, stylesheet, and transitive chunk edges. Equal standalone signatures reuse `dist/assets/runtime/<family>/`; every enhanced-navigation group intentionally shares one union family. The same JSON-safe contract is available to `afterBuild()` as `artifacts`.
|
|
81
|
+
`.kudzu/kudzu-artifacts.json` records each route's exact CapabilityIR signature, assigned emitted runtime family, handler, Worker, stylesheet, and transitive chunk edges. Equal standalone signatures reuse `dist/assets/runtime/<family>/`; every enhanced-navigation group intentionally shares one union family. The same JSON-safe contract is available to `afterBuild()` as `artifacts`. `.kudzu/kudzu-compatibility.json` separately inventories reachable authored package sites before normalization with deterministic compatibility classes and source ranges; that provenance does not enter semantic IR, codegen, runtime files, or deploy output.
|
|
82
82
|
|
|
83
83
|
New syntax support belongs in an existing pass or a focused new pass only when a reduced migration fixture proves it. Passes must preserve source-located diagnostics, avoid module-global analysis state, and expose metadata through return values rather than AST-identity side channels. Build orchestration stays in `build.mjs`; feature-specific graph validation or code generation moves under `compiler/` when it has a stable input/output boundary.
|
|
84
84
|
|
package/framework/build.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
|
4
4
|
import { pathToFileURL } from "node:url"
|
|
5
5
|
import { build as bundle, transform } from "esbuild"
|
|
6
6
|
import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
7
|
+
import { createCompatibilityReport } from "./compiler/compatibility-registry.mjs"
|
|
7
8
|
import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
|
|
8
9
|
import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
|
|
9
10
|
import { createProjectSession } from "./compiler/project-session.mjs"
|
|
@@ -103,6 +104,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, quiet
|
|
|
103
104
|
if (!pageFiles.length) throw new Error("No pages found in src/pages/")
|
|
104
105
|
const pageSources = new Map(pageFiles.map(file => [file, new Set(reachableSourceFiles([file], allSourceFileSet, sourceIndex))]))
|
|
105
106
|
const sourceFiles = [...new Set([...pageSources.values()].flatMap(files => [...files]))].sort()
|
|
107
|
+
await writePrettyJson(join(workDirectory, "kudzu-compatibility.json"), createCompatibilityReport(sourceFiles.map(file => ({ file: relative(root, file).replaceAll(sep, "/"), source: sourceIndex.get(file) }))))
|
|
106
108
|
const sourceFileSet = project.sourceFiles
|
|
107
109
|
sourceFileSet.clear()
|
|
108
110
|
for (const file of sourceFiles) sourceFileSet.add(file)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
2
|
import { bindingNames, containsJsx, functionVarDeclaresName, importDeclarationNames, isNodeWithin, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
|
+
import { compatibilityPackages } from "./compatibility-registry.mjs"
|
|
3
4
|
|
|
4
5
|
export function normalizeMediaQueryExternalStores(sourceFile, factory, context) {
|
|
5
|
-
const imports = sourceFile.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text ===
|
|
6
|
+
const imports = sourceFile.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === compatibilityPackages.react && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings))
|
|
6
7
|
const externalStoreImport = imports.flatMap(statement => statement.importClause.namedBindings.elements.map(entry => ({ entry, statement }))).find(({ entry }) => !entry.isTypeOnly && !entry.propertyName && entry.name.text === "useSyncExternalStore")
|
|
7
8
|
if (!externalStoreImport) return sourceFile
|
|
8
9
|
const returnedExpression = callback => {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import ts from "typescript"
|
|
2
|
+
|
|
3
|
+
export const compatibilityClasses = Object.freeze([
|
|
4
|
+
"Native",
|
|
5
|
+
"Compiled",
|
|
6
|
+
"Normalized",
|
|
7
|
+
"Adapter",
|
|
8
|
+
"Owned External UI",
|
|
9
|
+
"Partial",
|
|
10
|
+
"Unsupported"
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
export const compatibilityPackages = Object.freeze({
|
|
14
|
+
clsx: "clsx",
|
|
15
|
+
react: "react",
|
|
16
|
+
reactBootstrap: "react-bootstrap",
|
|
17
|
+
reactI18next: "react-i18next",
|
|
18
|
+
reactRouter: "react-router-dom",
|
|
19
|
+
zustand: "zustand"
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const compiledReact = new Set(["default", "*", "Fragment", "createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
|
|
23
|
+
const normalizedReact = new Set(["createRef", "forwardRef", "memo", "useCallback", "useMemo", "useSyncExternalStore"])
|
|
24
|
+
const normalizedRouter = new Set(["useMatch", "useParams", "useSearchParams"])
|
|
25
|
+
const ownedExternalUi = new Set(["@codemirror/view", "chart.js", "gsap", "sortablejs", "typed.js"])
|
|
26
|
+
|
|
27
|
+
export function createCompatibilityReport(sources) {
|
|
28
|
+
const sites = sources.flatMap(({ file, source }) => compatibilitySites(file, source)).sort(compareSites)
|
|
29
|
+
const counts = Object.fromEntries(compatibilityClasses.map(name => [name, 0]))
|
|
30
|
+
const packages = new Map()
|
|
31
|
+
for (const site of sites) {
|
|
32
|
+
counts[site.classification]++
|
|
33
|
+
const key = `${site.package}\0${site.classification}`
|
|
34
|
+
packages.set(key, { package: site.package, classification: site.classification, sites: (packages.get(key)?.sites ?? 0) + 1 })
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
summary: counts,
|
|
39
|
+
packages: [...packages.values()].sort((left, right) => compareText(left.package, right.package) || compareText(left.classification, right.classification)),
|
|
40
|
+
sites
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compatibilitySites(file, source) {
|
|
45
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
|
|
46
|
+
const sites = []
|
|
47
|
+
const add = (target, imported, local, kind, node) => {
|
|
48
|
+
if (!isBarePackage(target) || target === "@kudzujs/core" || target.startsWith("@kudzujs/core/")) return
|
|
49
|
+
const rule = classify(target, imported, kind)
|
|
50
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
51
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd())
|
|
52
|
+
sites.push({
|
|
53
|
+
package: target,
|
|
54
|
+
imported,
|
|
55
|
+
...(local && local !== imported ? { local } : {}),
|
|
56
|
+
kind,
|
|
57
|
+
classification: rule.classification,
|
|
58
|
+
rule: rule.id,
|
|
59
|
+
file,
|
|
60
|
+
location: { line: start.line + 1, column: start.character + 1, endLine: end.line + 1, endColumn: end.character + 1 }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const visit = node => {
|
|
64
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
65
|
+
const target = node.moduleSpecifier.text
|
|
66
|
+
const clause = node.importClause
|
|
67
|
+
if (!clause?.isTypeOnly) {
|
|
68
|
+
if (!clause) add(target, null, null, "side-effect-import", node.moduleSpecifier)
|
|
69
|
+
if (clause?.name) add(target, "default", clause.name.text, "import", clause.name)
|
|
70
|
+
const bindings = clause?.namedBindings
|
|
71
|
+
if (bindings && ts.isNamespaceImport(bindings)) add(target, "*", bindings.name.text, "import", bindings.name)
|
|
72
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
|
|
73
|
+
if (!entry.isTypeOnly) add(target, (entry.propertyName ?? entry.name).text, entry.name.text, "import", entry)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
79
|
+
const target = node.moduleSpecifier.text
|
|
80
|
+
const clause = node.exportClause
|
|
81
|
+
if (!node.isTypeOnly) {
|
|
82
|
+
if (!clause || ts.isNamespaceExport(clause)) add(target, "*", null, "re-export", clause ?? node.moduleSpecifier)
|
|
83
|
+
else for (const entry of clause.elements) if (!entry.isTypeOnly) add(target, (entry.propertyName ?? entry.name).text, entry.name.text, "re-export", entry)
|
|
84
|
+
}
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0])) {
|
|
88
|
+
add(node.arguments[0].text, "*", null, "dynamic-import", node.arguments[0])
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
ts.forEachChild(node, visit)
|
|
92
|
+
}
|
|
93
|
+
visit(sourceFile)
|
|
94
|
+
return sites
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function classify(target, imported, kind) {
|
|
98
|
+
if (kind === "re-export") return { id: "package.re-export.unsupported", classification: "Unsupported" }
|
|
99
|
+
if (kind === "side-effect-import") return { id: "package.side-effect.unsupported", classification: "Unsupported" }
|
|
100
|
+
const root = packageRoot(target)
|
|
101
|
+
if (kind === "dynamic-import") return { id: "package.dynamic.partial", classification: "Partial" }
|
|
102
|
+
if (target === compatibilityPackages.react) {
|
|
103
|
+
if (compiledReact.has(imported)) return { id: "react.compiled", classification: "Compiled" }
|
|
104
|
+
if (normalizedReact.has(imported)) return { id: "react.normalized", classification: "Normalized" }
|
|
105
|
+
return { id: "react.partial", classification: "Partial" }
|
|
106
|
+
}
|
|
107
|
+
if (target === compatibilityPackages.reactRouter) {
|
|
108
|
+
if (imported === "Link" || imported === "useNavigate") return { id: "react-router-dom.native", classification: "Native" }
|
|
109
|
+
if (normalizedRouter.has(imported)) return { id: "react-router-dom.normalized", classification: "Normalized" }
|
|
110
|
+
return { id: "react-router-dom.partial", classification: "Partial" }
|
|
111
|
+
}
|
|
112
|
+
if (target === compatibilityPackages.reactBootstrap) return ["Row", "Col"].includes(imported)
|
|
113
|
+
? { id: "react-bootstrap.normalized", classification: "Normalized" }
|
|
114
|
+
: { id: "react-bootstrap.partial", classification: "Partial" }
|
|
115
|
+
if (target === compatibilityPackages.zustand) return imported === "create"
|
|
116
|
+
? { id: "zustand.adapter", classification: "Adapter" }
|
|
117
|
+
: { id: "zustand.partial", classification: "Partial" }
|
|
118
|
+
if (target === compatibilityPackages.clsx) return ["default", "clsx"].includes(imported)
|
|
119
|
+
? { id: "clsx.normalized", classification: "Normalized" }
|
|
120
|
+
: { id: "clsx.partial", classification: "Partial" }
|
|
121
|
+
if (target === compatibilityPackages.reactI18next) return { id: "react-i18next.unsupported", classification: "Unsupported" }
|
|
122
|
+
if (ownedExternalUi.has(root)) return { id: "owned-external-ui", classification: "Owned External UI" }
|
|
123
|
+
return { id: "package.contextual", classification: "Partial" }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function packageRoot(target) {
|
|
127
|
+
const parts = target.split("/")
|
|
128
|
+
return target.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isBarePackage(target) {
|
|
132
|
+
return !target.startsWith(".") && !target.startsWith("/") && !target.startsWith("node:") && !/^[a-z][a-z+.-]*:/i.test(target)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function compareSites(left, right) {
|
|
136
|
+
return compareText(left.file, right.file) || left.location.line - right.location.line || left.location.column - right.location.column || compareText(left.package, right.package) || compareText(String(left.imported), String(right.imported))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function compareText(left, right) {
|
|
140
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
141
|
+
}
|
|
@@ -172,7 +172,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
172
172
|
const dynamicImports = []
|
|
173
173
|
const visitDynamicImports = node => {
|
|
174
174
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
175
|
-
if (role !== "effect" || !ownedLazyPackageImport(node)) throw new Error("Dynamic package imports require one literal import() directly inside an inline owned effect")
|
|
175
|
+
if (role !== "effect" || !ownedLazyPackageImport(node, bindingIndex)) throw new Error("Dynamic package imports require one literal import() directly inside an inline owned effect")
|
|
176
176
|
dynamicImports.push({ kind: "dynamic", local: node.arguments[0].text, target: node.arguments[0].text, package: true })
|
|
177
177
|
}
|
|
178
178
|
ts.forEachChild(node, visitDynamicImports)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
|
+
import { createBindingIndex } from "./analysis/binding-index.mjs"
|
|
2
3
|
import { effectReturns, importDeclarationNames, isNodeWithin, isShadowedIdentifier, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
4
|
import { ownedLazyPackageImport } from "./source-graph.mjs"
|
|
4
5
|
|
|
5
6
|
export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
7
|
+
const bindingIndex = createBindingIndex(sourceFile)
|
|
6
8
|
const frameCall = (node, name) => ts.isCallExpression(node) && (
|
|
7
9
|
ts.isIdentifier(node.expression) && node.expression.text === name ||
|
|
8
10
|
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
|
|
@@ -24,14 +26,14 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
|
24
26
|
return isNegated === negated && currentAccess(condition, name)
|
|
25
27
|
}
|
|
26
28
|
const containsLazyImport = node => {
|
|
27
|
-
if (ownedLazyPackageImport(node)) return true
|
|
29
|
+
if (ownedLazyPackageImport(node, bindingIndex)) return true
|
|
28
30
|
let found = false
|
|
29
31
|
ts.forEachChild(node, child => { if (!found) found = containsLazyImport(child) })
|
|
30
32
|
return found
|
|
31
33
|
}
|
|
32
34
|
const directLazyImportCallback = (callback, effectCallback) => {
|
|
33
35
|
const call = callback?.parent
|
|
34
|
-
return (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isCallExpression(call) && call.arguments[0] === callback && ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "then" && ownedLazyPackageImport(unwrapExpression(call.expression.expression)) && isNodeWithin(callback, effectCallback)
|
|
36
|
+
return (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isCallExpression(call) && call.arguments[0] === callback && ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "then" && ownedLazyPackageImport(unwrapExpression(call.expression.expression), bindingIndex) && isNodeWithin(callback, effectCallback)
|
|
35
37
|
}
|
|
36
38
|
const hasUseRefImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useRef"))
|
|
37
39
|
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
2
|
import { bindingNames, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, loopDeclaresName, nearestFunction, nearestFunctionLike, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
3
|
import { analyzeCollectionPipeline, isArrayFromCall } from "./collection-analysis.mjs"
|
|
4
|
+
import { compatibilityPackages } from "./compatibility-registry.mjs"
|
|
4
5
|
|
|
5
6
|
export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
6
7
|
function normalizeReactBootstrapLayout(sourceFile, factory, context) {
|
|
7
8
|
const layouts = new Map()
|
|
8
9
|
for (const statement of sourceFile.statements) {
|
|
9
|
-
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !==
|
|
10
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== compatibilityPackages.reactBootstrap) continue
|
|
10
11
|
const clause = statement.importClause
|
|
11
12
|
if (!clause || clause.name || !clause.namedBindings || ts.isNamespaceImport(clause.namedBindings)) throw sourceNodeError(statement, sourceFile, "React Bootstrap layout migration requires named Row or Col imports")
|
|
12
13
|
for (const entry of clause.namedBindings.elements) {
|
|
@@ -52,7 +53,7 @@ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
|
52
53
|
}
|
|
53
54
|
if (ts.isJsxSelfClosingElement(node) && layout(node.tagName)) return factory.updateJsxSelfClosingElement(node, factory.createIdentifier("div"), node.typeArguments, attributes(node.attributes))
|
|
54
55
|
if (ts.isIdentifier(node) && layouts.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Bootstrap Row and Col imports may only be used as direct JSX elements")
|
|
55
|
-
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text ===
|
|
56
|
+
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === compatibilityPackages.reactBootstrap) {
|
|
56
57
|
const clause = node.importClause
|
|
57
58
|
const bindings = clause?.namedBindings
|
|
58
59
|
if (!clause || !bindings || !ts.isNamedImports(bindings)) return node
|
|
@@ -71,7 +72,7 @@ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
|
71
72
|
const aliases = new Map()
|
|
72
73
|
const reactObjects = new Set()
|
|
73
74
|
for (const statement of sourceFile.statements) {
|
|
74
|
-
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !==
|
|
75
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== compatibilityPackages.react) continue
|
|
75
76
|
if (statement.importClause?.name) reactObjects.add(statement.importClause.name.text)
|
|
76
77
|
const bindings = statement.importClause?.namedBindings
|
|
77
78
|
if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
|
|
@@ -257,7 +258,7 @@ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
|
257
258
|
const missing = [...required].filter(name => !imported.has(name)).sort()
|
|
258
259
|
if (!missing.length) return normalizeReactBootstrapLayout(normalized, factory, context)
|
|
259
260
|
for (const name of missing) {
|
|
260
|
-
const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !==
|
|
261
|
+
const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== compatibilityPackages.react)
|
|
261
262
|
if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
|
|
262
263
|
}
|
|
263
264
|
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("react"))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
2
|
import { bindingNames, functionVarDeclaresName, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedIdentifier, loopDeclaresName, nearestFunction, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
|
+
import { compatibilityPackages } from "./compatibility-registry.mjs"
|
|
3
4
|
|
|
4
5
|
export function createRouterPass({ withBase }) {
|
|
5
6
|
return function normalizeReactRouterSyntax(sourceFile, factory, context, base, importedCollections = new Map()) {
|
|
@@ -9,7 +10,7 @@ export function createRouterPass({ withBase }) {
|
|
|
9
10
|
const searchHooks = new Set()
|
|
10
11
|
const navigateHooks = new Set()
|
|
11
12
|
for (const statement of sourceFile.statements) {
|
|
12
|
-
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text ===
|
|
13
|
+
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === compatibilityPackages.reactRouter) {
|
|
13
14
|
if (ts.isExportDeclaration(statement)) throw sourceNodeError(statement, sourceFile, "React Router exports are not supported; import Link directly where it renders")
|
|
14
15
|
const clause = statement.importClause
|
|
15
16
|
if (clause?.isTypeOnly) continue
|
|
@@ -306,7 +307,7 @@ export function createRouterPass({ withBase }) {
|
|
|
306
307
|
if (ts.isIdentifier(node) && matchHooks.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useMatch imports may only initialize the supported top-level const binding")
|
|
307
308
|
if (ts.isIdentifier(node) && searchHooks.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useSearchParams imports may only initialize the supported top-level tuple binding")
|
|
308
309
|
if (ts.isIdentifier(node) && navigateHooks.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useNavigate imports may only initialize the supported top-level navigate binding")
|
|
309
|
-
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text ===
|
|
310
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === compatibilityPackages.reactRouter) {
|
|
310
311
|
const clause = node.importClause
|
|
311
312
|
if (!clause || clause.isTypeOnly) return node
|
|
312
313
|
const bindings = clause.namedBindings
|
|
@@ -8,6 +8,7 @@ import { normalizeEffectPrivateRefs } from "./effect-private-ref-pass.mjs"
|
|
|
8
8
|
import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
9
9
|
import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./browser-signal-passes.mjs"
|
|
10
10
|
import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./collection-analysis.mjs"
|
|
11
|
+
import { compatibilityPackages } from "./compatibility-registry.mjs"
|
|
11
12
|
import { normalizeCustomHookTimerRefs } from "./custom-hook-timer-pass.mjs"
|
|
12
13
|
import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./descriptor-session.mjs"
|
|
13
14
|
import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./effect-analysis.mjs"
|
|
@@ -170,7 +171,7 @@ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
|
|
|
170
171
|
function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
171
172
|
const names = new Set()
|
|
172
173
|
for (const statement of sourceFile.statements) {
|
|
173
|
-
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !==
|
|
174
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== compatibilityPackages.clsx) continue
|
|
174
175
|
if (statement.importClause?.name) names.add(statement.importClause.name.text)
|
|
175
176
|
const bindings = statement.importClause?.namedBindings
|
|
176
177
|
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "clsx") names.add(entry.name.text)
|
|
@@ -197,7 +198,7 @@ function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
|
197
198
|
const visitor = node => {
|
|
198
199
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) return combine(node.arguments.map(lower))
|
|
199
200
|
if (ts.isIdentifier(node) && names.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "clsx imports may only be called directly")
|
|
200
|
-
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text ===
|
|
201
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === compatibilityPackages.clsx) {
|
|
201
202
|
const clause = node.importClause
|
|
202
203
|
if (!clause || clause.isTypeOnly) return node
|
|
203
204
|
let bindings = clause.namedBindings
|
|
@@ -403,7 +404,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
403
404
|
for (const [name, binding] of packageBindings) {
|
|
404
405
|
const references = referenceIdentifiers(sourceFile, name)
|
|
405
406
|
const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile) && !insideOwnedEffectCallback(reference, sourceFile))
|
|
406
|
-
if (invalid && binding.target ===
|
|
407
|
+
if (invalid && binding.target === compatibilityPackages.reactI18next && binding.imported === "useTranslation") throw sourceNodeError(invalid, sourceFile, "React i18next useTranslation() depends on runtime locale resources; migrate build-known locales through getStaticPaths() and props, or browser-only locale reads through an owned effect")
|
|
407
408
|
if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers or owned effect setup/cleanup callbacks`)
|
|
408
409
|
}
|
|
409
410
|
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
@@ -2032,7 +2033,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2032
2033
|
if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
|
|
2033
2034
|
const lazyPackageImports = []
|
|
2034
2035
|
const collectLazyPackageImports = current => {
|
|
2035
|
-
if (ownedLazyPackageImport(current)) lazyPackageImports.push(current)
|
|
2036
|
+
if (ownedLazyPackageImport(current, bindingIndex)) lazyPackageImports.push(current)
|
|
2036
2037
|
else ts.forEachChild(current, collectLazyPackageImports)
|
|
2037
2038
|
}
|
|
2038
2039
|
collectLazyPackageImports(callback)
|
|
@@ -2136,7 +2137,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2136
2137
|
]) : factory.createNull()) : []
|
|
2137
2138
|
let hasLazyPackageImport = false
|
|
2138
2139
|
const findLazyPackageImport = current => {
|
|
2139
|
-
if (ownedLazyPackageImport(current)) hasLazyPackageImport = true
|
|
2140
|
+
if (ownedLazyPackageImport(current, bindingIndex)) hasLazyPackageImport = true
|
|
2140
2141
|
else ts.forEachChild(current, findLazyPackageImport)
|
|
2141
2142
|
}
|
|
2142
2143
|
findLazyPackageImport(callback)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, extname, join, relative, resolve } from "node:path"
|
|
2
2
|
import ts from "typescript"
|
|
3
|
+
import { createBindingIndex } from "./analysis/binding-index.mjs"
|
|
3
4
|
import { sourceNodeError } from "./ast-helpers.mjs"
|
|
4
5
|
|
|
5
6
|
export function createSourceGraph(root) {
|
|
@@ -17,11 +18,12 @@ export function createSourceGraph(root) {
|
|
|
17
18
|
|
|
18
19
|
const ordinaryRuntimeDependencies = (file, sourceFile, sourceFiles, isStaticImport) => {
|
|
19
20
|
const dependencies = []
|
|
21
|
+
const bindingIndex = createBindingIndex(sourceFile)
|
|
20
22
|
const rejectDynamicImports = node => {
|
|
21
23
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
22
24
|
const argument = node.arguments[0]
|
|
23
25
|
const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(argument) ? JSON.stringify(argument.text) : argument?.getText(sourceFile) ?? "<missing>"
|
|
24
|
-
if (ownedLazyPackageImport(node)) return
|
|
26
|
+
if (ownedLazyPackageImport(node, bindingIndex)) return
|
|
25
27
|
throw sourceNodeError(node, sourceFile, `Dynamic import ${specifier} is not supported in ordinary source modules`)
|
|
26
28
|
}
|
|
27
29
|
ts.forEachChild(node, rejectDynamicImports)
|
|
@@ -45,14 +47,19 @@ export function createSourceGraph(root) {
|
|
|
45
47
|
return { ordinaryRuntimeDependencies, parseSourceFile, resolveSourceImport, runtimeModuleReference }
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export function ownedLazyPackageImport(node) {
|
|
50
|
+
export function ownedLazyPackageImport(node, bindingIndex) {
|
|
49
51
|
if (!ts.isCallExpression(node) || node.expression.kind !== ts.SyntaxKind.ImportKeyword || node.arguments.length !== 1 || !ts.isStringLiteral(node.arguments[0])) return false
|
|
50
52
|
const target = node.arguments[0].text
|
|
51
53
|
if (!target || target.startsWith(".") || target.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(target)) return false
|
|
52
54
|
for (let current = node.parent; current; current = current.parent) {
|
|
53
55
|
if (!ts.isArrowFunction(current) && !ts.isFunctionExpression(current)) continue
|
|
54
56
|
const call = current.parent
|
|
55
|
-
|
|
57
|
+
if (!ts.isCallExpression(call) || call.arguments[0] !== current || !ts.isIdentifier(call.expression) || current.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return false
|
|
58
|
+
const binding = bindingIndex.resolveReference(call.expression, node.getSourceFile())
|
|
59
|
+
const declaration = binding?.kind === "import" ? binding.declaration : undefined
|
|
60
|
+
const specifier = declaration?.parent
|
|
61
|
+
const importDeclaration = specifier?.parent?.parent?.parent
|
|
62
|
+
return Boolean(specifier && ts.isImportSpecifier(specifier) && (specifier.propertyName ?? specifier.name).text === "useEffect" && importDeclaration && ts.isImportDeclaration(importDeclaration) && ts.isStringLiteral(importDeclaration.moduleSpecifier) && ["react", "@kudzujs/core"].includes(importDeclaration.moduleSpecifier.text))
|
|
56
63
|
}
|
|
57
64
|
return false
|
|
58
65
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { relative, sep } from "node:path"
|
|
2
2
|
import ts from "typescript"
|
|
3
3
|
import { isReferenceIdentifier, isShadowedIdentifier, nearestFunction, sourceNodeError, unwrapExpression } from "./ast-helpers.mjs"
|
|
4
|
+
import { compatibilityPackages } from "./compatibility-registry.mjs"
|
|
4
5
|
|
|
5
6
|
export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory }) {
|
|
6
7
|
function analyzeZustandStores(sourceFile) {
|
|
7
8
|
const createNames = new Set()
|
|
8
9
|
for (const statement of sourceFile.statements) {
|
|
9
|
-
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !==
|
|
10
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== compatibilityPackages.zustand) continue
|
|
10
11
|
const bindings = statement.importClause?.namedBindings
|
|
11
12
|
if (statement.importClause?.name || !bindings || !ts.isNamedImports(bindings)) throw sourceNodeError(statement, sourceFile, "Zustand migration input requires a named create import")
|
|
12
13
|
for (const entry of bindings.elements) {
|
|
@@ -77,7 +78,7 @@ export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNam
|
|
|
77
78
|
function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
|
|
78
79
|
const stores = analyzeZustandStores(sourceFile)
|
|
79
80
|
if (!stores.size) {
|
|
80
|
-
const declaration = sourceFile.statements.find(statement => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text ===
|
|
81
|
+
const declaration = sourceFile.statements.find(statement => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === compatibilityPackages.zustand && !statement.importClause?.isTypeOnly)
|
|
81
82
|
if (declaration) throw sourceNodeError(declaration, sourceFile, "Zustand create must directly initialize an exported const store")
|
|
82
83
|
return sourceFile
|
|
83
84
|
}
|
|
@@ -92,7 +93,7 @@ export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNam
|
|
|
92
93
|
factory.createStringLiteral(store.sourceKind)
|
|
93
94
|
]))
|
|
94
95
|
}
|
|
95
|
-
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text ===
|
|
96
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === compatibilityPackages.zustand) return undefined
|
|
96
97
|
return ts.visitEachChild(node, visitor, context)
|
|
97
98
|
}
|
|
98
99
|
const normalized = ts.visitNode(sourceFile, visitor)
|