@kudzujs/core 0.9.1 → 0.9.3
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 +19 -1
- package/README.md +5 -2
- package/RELEASES.md +69 -0
- package/docs/next-architecture/1.0-large-application-compatibility-audit.md +370 -0
- package/docs/next-architecture/README.md +12 -9
- package/docs/next-architecture/goal-d-routing-compatibility-decisions.md +1 -0
- package/framework/README.md +10 -4
- package/framework/build.mjs +11 -2
- package/framework/compiler/param-codegen.mjs +4 -9
- package/framework/compiler/react-migration-pass.mjs +65 -3
- package/framework/compiler/router-pass.mjs +99 -11
- package/framework/compiler/source-compiler.mjs +21 -7
- package/framework/core.mjs +14 -1
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -2,10 +2,28 @@
|
|
|
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
|
|
5
|
+
The completed `0.9.0` execution sequence is maintained in [`docs/next-architecture/0.9-semantic-compression.md`](./docs/next-architecture/0.9-semantic-compression.md), with session-level implementation records in [`docs/next-architecture/0.9-implementation-plan.md`](./docs/next-architecture/0.9-implementation-plan.md). The next authorized milestone is the 1.0 AI delivery protocol recorded there. The completed compiler foundation and longer-term sequence remain in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md); 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
|
|
|
9
|
+
The post-0.9 local discovery audit of Memos, Apache Answer, and Actual Budget is recorded in [`docs/next-architecture/1.0-large-application-compatibility-audit.md`](./docs/next-architecture/1.0-large-application-compatibility-audit.md). Its passing reduced builds are not whole-application migrations. Each package-component, browser-data, router/layout, provider, and localization shape requires a separate executable evidence packet under the authorized 1.0 protocol before implementation.
|
|
10
|
+
|
|
11
|
+
The first Apache Answer reduction is complete: one direct top-level `const match = useMatch("/exact-path")` over a named or aliased React Router import folds case-insensitively against the build-known application route. Reused relative components produce route-specific static HTML with zero browser bytes. Layout use, runtime-parameter pages, params, wildcards, query/hash patterns, trailing slashes, dynamic values, indirect calls, and pattern objects remain diagnosed.
|
|
12
|
+
|
|
13
|
+
The next Apache Answer reductions preserve its direct numeric page fallback and string order fallback over `useSearchParams()`. Both reuse nullable query signals and existing primitive binding ESM; the string form accepts only a static element of a named relative JSON-safe immutable array. Dynamic indexes, arbitrary wrappers, and general query expressions remain diagnosed.
|
|
14
|
+
|
|
15
|
+
Apache Answer layouts may retain named React Bootstrap `Row` and `Col` components when they use children, static `className`, and numeric literal `Col` spans from 1 through 12 for `xs` through `xxl`. Compiler normalization emits native Bootstrap grid classes with no browser JavaScript and covers all 63 observed static-breakpoint Col sites; Bootstrap stylesheet ownership remains with the application. Dynamic/object/boolean grid props and broader React Bootstrap components remain separate evidence packets.
|
|
16
|
+
|
|
17
|
+
Apache Answer's `useTranslation()` cannot be folded safely because its language resources come from 46 YAML files, browser storage, and server responses. Kudzu emits a source-located diagnostic directing build-known locales to `getStaticPaths()` and props or browser-only selection to an owned effect; it does not substitute English or ship i18next.
|
|
18
|
+
|
|
19
|
+
Apache Answer's SWR/axios/qs question reads now have an executable migration path through one native fetch effect with application-owned loading/error/data state. Derived `page` and `order` query dependencies restart the effect, stale responses lose setter authority, recommendation endpoints and HTTP recovery remain explicit, nested keyed tags retain list ownership, and a static sibling remains JavaScript-free. No cache, Provider, retry scheduler, SWR, axios, or query runtime is added.
|
|
20
|
+
|
|
21
|
+
Apache Answer authentication now has a reduced layout-owned session path: native login form submission persists a token, a mount effect restores and validates it, shared header/settings consumers read one package-neutral state record, and a 401 clears storage/state before native replacement navigation. The public sibling remains JavaScript-free. Full browser acceptance and the broader server-fed settings stores remain subsequent gates.
|
|
22
|
+
|
|
23
|
+
Apache Answer's first route-shell gate replaces `RouterProvider` and nested `Outlet` ownership with seven directly addressable Kudzu file routes, one ordinary shared shell, native anchors, and runtime question/tag parameters. Questions, tags, admin dashboard, and unwrapped legal output build without a browser router; the two bracket routes emit only pathname capability and rewrite metadata.
|
|
24
|
+
|
|
25
|
+
Apache Answer authoring/admin intake now builds native question creation and keyed administration without form, editor, query, or state package runtimes. Constraint-backed title/markdown fields, controlled preview, markdown file import, local draft mutation, keyed admin rows, and deletion compile through existing handlers/lists while a legal sibling remains JavaScript-free. Rich CodeMirror ownership, server validation, upload progress, and destructive confirmation remain later acceptance gates.
|
|
26
|
+
|
|
9
27
|
## North Star
|
|
10
28
|
|
|
11
29
|
An AI should be able to migrate ordinary React-shaped TypeScript and TSX to CDN-deployable static HTML, CSS, and only the route-specific ESM capabilities actually used, with minimal source restructuring.
|
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.9.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.9.
|
|
17
|
+
**Latest release: 0.9.3 - Large-app migration slices.** Apache Answer-derived fixtures add exact route matching, composed query reads, native React Bootstrap layout, owned browser data, authentication build intake, route-shell, and authoring/admin migration evidence without React or package runtimes. Read the [release notes](./RELEASES.md#093---large-app-migration-slices), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.9.3), 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)
|
|
@@ -103,7 +103,10 @@ ordinary React-shaped TSX
|
|
|
103
103
|
- Native document navigation is the default; static routes do not load a client runtime.
|
|
104
104
|
- A named or aliased React Router `Link` with a static root-relative `to` erases to a base-aware native anchor; no router package or runtime is emitted.
|
|
105
105
|
- A direct named or aliased React Router `useParams()` call on a `runtimeParams` bracket route reuses Kudzu's route-specific pathname reader.
|
|
106
|
-
-
|
|
106
|
+
- A direct top-level React Router `useMatch("/exact-path")` route binding folds case-insensitively from the build-known application route without browser JavaScript.
|
|
107
|
+
- React Router `useSearchParams()` supports direct static `get("name")` locals, the exact numeric pagination fallback `Number(params.get("page")) || 1`, a static imported-array string fallback, and inline setter updaters, lowering reads and history writes to one route-specific query capability.
|
|
108
|
+
- React Bootstrap `Row` and `Col` with children, static classes, and numeric literal `Col` breakpoints erase to native Bootstrap grid markup; applications retain ownership of Bootstrap CSS.
|
|
109
|
+
- Browser-only query data uses owned effects with application loading/error/data state; URL-derived dependencies, stale-response isolation, keyed results, and recovery require no query package runtime.
|
|
107
110
|
- Only TypeScript modules reachable from pages are compiled. Imported immutable direct maps can fold to static HTML, while direct fields from relative structured calculations reevaluate through route binding ESM.
|
|
108
111
|
- Package imports used directly inside JSX event callbacks are removed from build modules and retained only in bundled route handler ESM.
|
|
109
112
|
- A named or aliased React Router `useNavigate()` top-level binding lowers direct nested-callback calls with safe static root-relative destinations to native `location.assign()` or `location.replace()` navigation.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.9.3 - Large-app migration slices
|
|
4
|
+
|
|
5
|
+
Kudzu 0.9.3 adds evidence-backed compiler and source-migration paths derived
|
|
6
|
+
from Apache Answer while preserving static-first output and fail-closed package
|
|
7
|
+
boundaries.
|
|
8
|
+
|
|
9
|
+
### Changed in 0.9.3
|
|
10
|
+
|
|
11
|
+
- Exact static React Router `useMatch()` folds from the build-known route with
|
|
12
|
+
zero browser JavaScript.
|
|
13
|
+
- React Router query reads support numeric pagination and static imported-string
|
|
14
|
+
fallbacks, including reactive effect dependencies and read-only `popstate`.
|
|
15
|
+
- React Bootstrap `Row` and `Col` lower to native grid markup, including numeric
|
|
16
|
+
literal breakpoint spans from 1 through 12.
|
|
17
|
+
- Owned native fetch effects compose URL dependencies, stale-response isolation,
|
|
18
|
+
loading/error/data state, and nested keyed results without SWR or axios.
|
|
19
|
+
- Reduced shared-authentication build intake and route-shell, authoring, and
|
|
20
|
+
admin fixtures prove package-neutral paths with static sibling exclusion.
|
|
21
|
+
- Runtime-resource `react-i18next` receives an actionable locale-ownership
|
|
22
|
+
diagnostic rather than silently folding English or shipping i18next.
|
|
23
|
+
|
|
24
|
+
### Validation
|
|
25
|
+
|
|
26
|
+
- Apache Answer-derived positive and negative fixtures cover router, query,
|
|
27
|
+
layout, data, authentication, route-shell, authoring, and admin boundaries.
|
|
28
|
+
- Static layout/legal/public siblings remain JavaScript-free.
|
|
29
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass on the release
|
|
30
|
+
worktree.
|
|
31
|
+
|
|
32
|
+
### Limits
|
|
33
|
+
|
|
34
|
+
This release does not claim a complete Apache Answer migration. Runtime i18n,
|
|
35
|
+
rich CodeMirror ownership, upload progress, broader React Bootstrap compounds,
|
|
36
|
+
plugins, AI screens, and all-route acceptance remain separate evidence gates.
|
|
37
|
+
|
|
38
|
+
### Upgrade
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install @kudzujs/core@^0.9.3
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 0.9.2 - Legacy CSS safety
|
|
45
|
+
|
|
46
|
+
Kudzu 0.9.2 prevents pre-0.8.53 applications from silently producing an
|
|
47
|
+
unstyled deploy after upgrading to route-aware CSS ownership.
|
|
48
|
+
|
|
49
|
+
### Changed in 0.9.2
|
|
50
|
+
|
|
51
|
+
- When no route imports CSS and `kudzu.config styles` is empty, source CSS is
|
|
52
|
+
linked globally in deterministic path order using the legacy behavior.
|
|
53
|
+
- The build prints an actionable warning directing applications to explicit
|
|
54
|
+
source imports or global configuration.
|
|
55
|
+
- Projects with any explicit CSS ownership retain unchanged route-aware output;
|
|
56
|
+
the fallback adds no browser JavaScript or explicit-path build work.
|
|
57
|
+
|
|
58
|
+
### Validation
|
|
59
|
+
|
|
60
|
+
- A two-route legacy fixture emits both unimported styles on both routes in path
|
|
61
|
+
order and keeps both routes JavaScript-free.
|
|
62
|
+
- Configured global styles and explicit source-style tests retain their existing
|
|
63
|
+
output.
|
|
64
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with 254 tests.
|
|
65
|
+
|
|
66
|
+
### Upgrade
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm install @kudzujs/core@^0.9.2
|
|
70
|
+
```
|
|
71
|
+
|
|
3
72
|
## 0.9.1 - Positional primitive lists
|
|
4
73
|
|
|
5
74
|
Kudzu 0.9.1 allows JSON-safe primitive values in lists keyed by their authored
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Kudzu 1.0 Large-Application Compatibility Audit
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
This 2026-08-21 local discovery audit measures source compatibility after
|
|
6
|
+
`0.9.0`; it does not authorize a new runtime, replace the 1.0 AI delivery
|
|
7
|
+
protocol, or claim whole-application migration success. The probes used Kudzu
|
|
8
|
+
`0.9.2` at `f69722032a3790dfb13151e17e1ab0f60c8280d3` on Node 24.14.0.
|
|
9
|
+
Original repositories and Kudzu remained unchanged.
|
|
10
|
+
|
|
11
|
+
The result is a compatibility gap: Kudzu builds reduced public slices from all
|
|
12
|
+
three applications, but meaningful source retention is too low for a React-like
|
|
13
|
+
large-application migration claim.
|
|
14
|
+
|
|
15
|
+
## Pinned Applications
|
|
16
|
+
|
|
17
|
+
| Application | Revision | License | Frontend scope | Original build |
|
|
18
|
+
|---|---|---|---:|---|
|
|
19
|
+
| [Memos](https://github.com/usememos/memos) | `e5ed6e7ec60e141d6e354f3ab59906c2c47dccac` | MIT | 393 TS/TSX modules, 43,099 LOC, 19 leaf routes | Pass: 5,089 modules, 292 files |
|
|
20
|
+
| [Apache Answer](https://github.com/apache/answer) | `3b9f1370612e690a0b7f230f05e688930db4c6d3` | Apache-2.0 | 377 TS/TSX modules, 48,091 LOC, 88 explicit paths | Pass: 681 files |
|
|
21
|
+
| [Actual Budget](https://github.com/actualbudget/actual) | `87e33e49eb4d666c33e0c0f172faf340d1b7aa0a` | MIT | sparse desktop client: 810 TS/TSX modules, 163,344 LOC, 80 route declarations | Not established: acquisition lacks workspace tooling and core packages |
|
|
22
|
+
|
|
23
|
+
The Actual Budget result is diagnostic only. Its sparse checkout contains the
|
|
24
|
+
selected desktop components but not the component library, core backend, Yarn
|
|
25
|
+
release, database, worker coordinator, or spreadsheet implementation.
|
|
26
|
+
|
|
27
|
+
The migrated adapters, copied source, detailed diagnostic logs, and line-diff
|
|
28
|
+
inputs remain local under `/tmp/opencode/kudzu-large-apps`; they are not durable
|
|
29
|
+
tracked evidence. The pinned upstream commits and original build commands are
|
|
30
|
+
reproducible, but retention and migrated-output values remain discovery results
|
|
31
|
+
until an external fixture repository pins the adapters and acceptance harness.
|
|
32
|
+
|
|
33
|
+
## Migration Results
|
|
34
|
+
|
|
35
|
+
### Memos
|
|
36
|
+
|
|
37
|
+
The probe selected `/about`, `/explore`, and `/auth` from the real route and
|
|
38
|
+
component closures. The reduced Kudzu app builds three pages and one interactive
|
|
39
|
+
page. `/about` and `/explore` emit zero JavaScript; `/auth` emits 12,374 raw /
|
|
40
|
+
5,956 gzip bytes of route JavaScript.
|
|
41
|
+
|
|
42
|
+
| Measure | Result |
|
|
43
|
+
|---|---:|
|
|
44
|
+
| Direct corresponding source retained | 163 / 640 lines (25.5%) |
|
|
45
|
+
| Retention including omitted feed/auth closure | 163 / 996 lines (16.4%) |
|
|
46
|
+
| Final migrated TS/TSX | 12 files, 408 LOC |
|
|
47
|
+
| Deploy | 14 files |
|
|
48
|
+
|
|
49
|
+
The passing output is not behavior-equivalent. Live instance data, Connect RPC,
|
|
50
|
+
TanStack Query, pagination, privacy filtering, markdown, reactions, SSE refresh,
|
|
51
|
+
authentication, token storage, SSO discovery, and post-auth navigation are
|
|
52
|
+
absent. The public feed is a build-known snapshot.
|
|
53
|
+
|
|
54
|
+
### Apache Answer
|
|
55
|
+
|
|
56
|
+
The probe selected questions list/detail and tags list/detail. It preserves all
|
|
57
|
+
six selected component roles, but replaces the browser router, React Bootstrap,
|
|
58
|
+
i18next, SWR/axios, Zustand-backed globals, guards, and live API data. Four Kudzu
|
|
59
|
+
pages build; the questions list is zero JavaScript and the tags list uses the
|
|
60
|
+
existing keyed-filter capabilities.
|
|
61
|
+
|
|
62
|
+
| Measure | Result |
|
|
63
|
+
|---|---:|
|
|
64
|
+
| Exact selected source retained | 22 / 1,103 lines (2.0%) |
|
|
65
|
+
| Final migrated TS/TSX | 8 files, 257 LOC |
|
|
66
|
+
| Deploy | 17 files, 48,389 raw bytes |
|
|
67
|
+
| JavaScript | 33,384 raw / 13,641 gzip bytes |
|
|
68
|
+
|
|
69
|
+
The result has no Answer API, auth, mutations, loading/error state, pagination,
|
|
70
|
+
editor, cache invalidation, or route guards. Runtime detail paths render one
|
|
71
|
+
representative snapshot rather than the requested entity.
|
|
72
|
+
|
|
73
|
+
### Actual Budget
|
|
74
|
+
|
|
75
|
+
The probe selected the grouped report-table workflow and retained five imported
|
|
76
|
+
component boundaries. It produces one complete static report page with 0 bytes
|
|
77
|
+
of JavaScript.
|
|
78
|
+
|
|
79
|
+
| Measure | Result |
|
|
80
|
+
|---|---:|
|
|
81
|
+
| Exact selected source retained | 22 / 906 lines (2.4%) |
|
|
82
|
+
| Final authored probe | 268 LOC |
|
|
83
|
+
| Output | 2,810 B HTML, 1,990 B CSS, 0 B JavaScript |
|
|
84
|
+
|
|
85
|
+
This is an exported report, not Actual Budget. Database opening, query execution,
|
|
86
|
+
spreadsheet bindings, Redux and query state, synchronization, undo, preferences,
|
|
87
|
+
privacy formatting, Worker/SharedWorker ownership, synchronized scrolling, and
|
|
88
|
+
transaction drill-down are absent.
|
|
89
|
+
|
|
90
|
+
## Repeated Blockers
|
|
91
|
+
|
|
92
|
+
| Blocker | Memos | Answer | Actual | Decision |
|
|
93
|
+
|---|---|---|---|---|
|
|
94
|
+
| Package components used during render | Lucide, Base UI | React Bootstrap | Actual component library, React Aria | Record separate package shapes; this broad grouping does not prove one shared semantic concept. |
|
|
95
|
+
| Browser data/query ownership | TanStack Query, Connect RPC, SSE | SWR, axios | TanStack Query, worker-backed database | Define build-time snapshot and owned-effect migration contracts before considering an adapter. Do not add a query cache runtime. |
|
|
96
|
+
| Router/layout semantics | RouterProvider, guards, lazy routes | nested guarded routes, outlets, `useMatch` | React Router screens and drill-down state | Keep native/file routing as the default; inventory nested layout and guard source shapes separately. |
|
|
97
|
+
| Provider/global state | auth, instance, view/filter contexts | Zustand site/session/UI stores | Redux, preferences, non-serializable state | Continue package-neutral action/state reductions only from executable behavior fixtures. |
|
|
98
|
+
| Localization and formatting | i18next | i18next | i18next, locale/currency/privacy preferences | Prefer build-known locale routes; browser-switchable locale ownership needs independent evidence. |
|
|
99
|
+
| Dynamic presentation | markdown/editor/media/map | editor, Markdown, package widgets | grids, charts, spreadsheets | Treat imperative leaves and package adapters independently; no generic component runtime. |
|
|
100
|
+
|
|
101
|
+
These broad blocker families recur, but they do not establish one shared missing
|
|
102
|
+
semantic concept. Each package, query/data, router/layout, provider, localization,
|
|
103
|
+
or imperative-leaf shape still needs an independent executable reduction before
|
|
104
|
+
architecture review. Fixture count does not authorize a semantic primitive. The
|
|
105
|
+
first implementation must stop at native behavior, existing semantics,
|
|
106
|
+
normalization, or a bounded internal adapter before considering kernel growth.
|
|
107
|
+
|
|
108
|
+
## Compatibility Conclusion
|
|
109
|
+
|
|
110
|
+
Kudzu `0.9.2` supports large output and selected ordinary TSX, but not low-change
|
|
111
|
+
migration of these large React applications. The reported retention denominators
|
|
112
|
+
are not directly comparable: Memos includes an omitted feed/auth closure, Answer
|
|
113
|
+
counts six selected modules, and Actual counts five retained component files from
|
|
114
|
+
an incomplete sparse checkout. Every method still reports substantial rewriting.
|
|
115
|
+
|
|
116
|
+
Current viable use is narrower:
|
|
117
|
+
|
|
118
|
+
- static public pages and build-known read-only exports;
|
|
119
|
+
- bounded forms and keyed interactions that already match Kudzu ownership;
|
|
120
|
+
- separately accepted imperative leaves owned by effects;
|
|
121
|
+
- native navigation to an existing application for unsupported workflows.
|
|
122
|
+
|
|
123
|
+
Do not describe Memos, Answer, or Actual Budget as migrated applications. The
|
|
124
|
+
passing probes are reductions that identify compatibility work.
|
|
125
|
+
|
|
126
|
+
## Authorized Handoff
|
|
127
|
+
|
|
128
|
+
The authoritative next milestone remains the equal-condition 1.0 AI delivery
|
|
129
|
+
protocol in `0.9-implementation-plan.md`; this audit is candidate-selection input
|
|
130
|
+
only. If that protocol selects an Apache Answer task, its first unsupported source
|
|
131
|
+
shape is `useMatch` in the questions route. The implementation process must:
|
|
132
|
+
|
|
133
|
+
1. preserve that exact failing source and reduce only the first unsupported shape;
|
|
134
|
+
2. identify existing IR producers, consumers, ownership, nearby rejected forms,
|
|
135
|
+
and expected browser-byte impact before editing the compiler;
|
|
136
|
+
3. run behavior and accessibility acceptance rather than build checks alone;
|
|
137
|
+
4. report source retention as a diagnostic with an explicit denominator, not an
|
|
138
|
+
unapproved pass threshold;
|
|
139
|
+
5. keep package-component, query/data, router/layout, localization, and provider
|
|
140
|
+
work as separate evidence packets.
|
|
141
|
+
|
|
142
|
+
Combining these blockers into one compatibility runtime is not approved.
|
|
143
|
+
|
|
144
|
+
## First Reduction Completion
|
|
145
|
+
|
|
146
|
+
The user selected the Apache Answer routing probe under the authorized protocol.
|
|
147
|
+
The first unsupported source shape, direct `const isIndexPage = useMatch("/")`,
|
|
148
|
+
now lowers through the existing router normalization pass to one compiler-internal
|
|
149
|
+
build-time route comparison. The same relative `Questions` component renders the
|
|
150
|
+
Answer title at `/` and the Questions title at `/questions`.
|
|
151
|
+
|
|
152
|
+
| Measure | Result |
|
|
153
|
+
|---|---:|
|
|
154
|
+
| Upstream-derived positive fixtures | 1 |
|
|
155
|
+
| Nearby negative fixtures | 2 |
|
|
156
|
+
| Semantic primitives | 0 |
|
|
157
|
+
| ModuleIR/RouteIR records | 0 |
|
|
158
|
+
| Compiler passes | 0 new |
|
|
159
|
+
| Browser runtime concepts | 0 |
|
|
160
|
+
| Browser JavaScript | 0 B on both routes |
|
|
161
|
+
|
|
162
|
+
The producer is `compiler/router-pass.mjs`; the only consumer is a build-time
|
|
163
|
+
helper in `core.mjs` reading the invocation's application route. No SPA router,
|
|
164
|
+
path matcher, route signal, state, effect, handler, or browser module is added.
|
|
165
|
+
Layout and runtime-parameter use fail before output; dynamic patterns and
|
|
166
|
+
parameterized/wildcard/query/hash/trailing-slash forms fail with source
|
|
167
|
+
diagnostics. Matching follows React Router's default case-insensitive behavior.
|
|
168
|
+
Broader nested layouts, guards, outlets, and dynamic matching remain separate
|
|
169
|
+
evidence packets.
|
|
170
|
+
|
|
171
|
+
## Second Reduction Completion
|
|
172
|
+
|
|
173
|
+
The next repeated Apache Answer blocker, direct
|
|
174
|
+
`const curPage = Number(urlSearchParams.get("page")) || 1`, now separates the
|
|
175
|
+
nullable query signal into a compiler-owned local and reuses the existing pure
|
|
176
|
+
primitive binding evaluator. Query removal through `popstate` restores the
|
|
177
|
+
finite authored fallback.
|
|
178
|
+
|
|
179
|
+
| Measure | Result |
|
|
180
|
+
|---|---:|
|
|
181
|
+
| Upstream-derived positive fixtures | 1 |
|
|
182
|
+
| Nearby negative fixtures | 1 |
|
|
183
|
+
| Semantic primitives | 0 |
|
|
184
|
+
| ModuleIR/RouteIR records | 0 |
|
|
185
|
+
| Existing core files changed | 3 |
|
|
186
|
+
| Core LOC delta | +39 |
|
|
187
|
+
| Compiler passes | 0 new |
|
|
188
|
+
| Browser runtime concepts | 0 |
|
|
189
|
+
| Raw browser ESM | 11,211 B |
|
|
190
|
+
| Build benchmark delta | Not measured; route-local compatibility slice |
|
|
191
|
+
|
|
192
|
+
The producer remains `compiler/router-pass.mjs`; generic reactive-local
|
|
193
|
+
substitution and binding consumers handle the result. `param-codegen.mjs` now
|
|
194
|
+
applies its existing `popstate` synchronization to read-only query routes as
|
|
195
|
+
well as writable routes. `Math.max`, `parseInt`, dynamic names, aliases, and
|
|
196
|
+
other composed query expressions remain unsupported pending independent source
|
|
197
|
+
evidence.
|
|
198
|
+
|
|
199
|
+
## Third Reduction Completion
|
|
200
|
+
|
|
201
|
+
Apache Answer's adjacent
|
|
202
|
+
`(urlSearchParams.get("order") || QUESTION_ORDER_KEYS[0]) as QuestionOrderBy`
|
|
203
|
+
now follows the same compiler-owned query signal path. The fallback is accepted
|
|
204
|
+
only when it is a static numeric element of a named relative JSON-safe immutable
|
|
205
|
+
array. The selected string is folded before binding/effect analysis, so the
|
|
206
|
+
browser handler retains neither the array nor its source module.
|
|
207
|
+
|
|
208
|
+
| Measure | Result |
|
|
209
|
+
|---|---:|
|
|
210
|
+
| Upstream-derived positive fixtures | 1 |
|
|
211
|
+
| Nearby negative fixtures | 1 |
|
|
212
|
+
| Semantic primitives | 0 |
|
|
213
|
+
| ModuleIR/RouteIR records | 0 |
|
|
214
|
+
| Existing core files changed | 2 |
|
|
215
|
+
| Core LOC delta | +7 |
|
|
216
|
+
| Compiler passes | 0 new |
|
|
217
|
+
| Browser runtime concepts | 0 |
|
|
218
|
+
| Raw browser ESM | 11,210 B |
|
|
219
|
+
| Build benchmark delta | Not measured; route-local compatibility slice |
|
|
220
|
+
|
|
221
|
+
Static output uses `newest`, `?order=active` updates both the attribute and text,
|
|
222
|
+
and query removal through `popstate` restores `newest`. Dynamic indexes, local
|
|
223
|
+
fallback arrays, package imports, calls, and arbitrary expressions remain
|
|
224
|
+
unsupported.
|
|
225
|
+
|
|
226
|
+
## Fourth Reduction Completion
|
|
227
|
+
|
|
228
|
+
Apache Answer's Questions page uses React Bootstrap `Row` and `Col` only as
|
|
229
|
+
declarative layout wrappers with static classes and children. The existing React
|
|
230
|
+
migration normalization pass now erases those named package imports and emits
|
|
231
|
+
native `div.row` and `div.col` markup. Bootstrap CSS remains an ordinary
|
|
232
|
+
application stylesheet dependency rather than a Kudzu runtime.
|
|
233
|
+
|
|
234
|
+
| Measure | Result |
|
|
235
|
+
|---|---:|
|
|
236
|
+
| Upstream-derived positive fixtures | 1 |
|
|
237
|
+
| Nearby negative fixtures | 1 |
|
|
238
|
+
| Semantic primitives | 0 |
|
|
239
|
+
| ModuleIR/RouteIR records | 0 |
|
|
240
|
+
| Existing core files changed | 1 |
|
|
241
|
+
| Core LOC delta | +48 |
|
|
242
|
+
| Compiler passes | 0 new |
|
|
243
|
+
| Browser runtime concepts | 0 |
|
|
244
|
+
| Browser JavaScript | 0 B |
|
|
245
|
+
| Build benchmark delta | Not measured; static normalization slice |
|
|
246
|
+
|
|
247
|
+
The generated document preserves the Questions heading, complementary sidebar,
|
|
248
|
+
children order, and Bootstrap class contract without scripts or behavior
|
|
249
|
+
markers. Breakpoint/grid props such as `md={6}`, dynamic classes, spreads,
|
|
250
|
+
subpath/default/namespace imports, and other React Bootstrap components remain
|
|
251
|
+
unsupported.
|
|
252
|
+
|
|
253
|
+
## Runtime Translation Boundary
|
|
254
|
+
|
|
255
|
+
Apache Answer's next Questions-page failure is `react-i18next`
|
|
256
|
+
`useTranslation()`. Its values are not build-known: initialization uses the
|
|
257
|
+
repository's English YAML fallback, while storage and server responses can add
|
|
258
|
+
or replace the active resource bundle. Folding English would lose behavior and
|
|
259
|
+
shipping i18next would add a forbidden package runtime.
|
|
260
|
+
|
|
261
|
+
Kudzu now reports that boundary at the hook reference and directs migration to
|
|
262
|
+
the existing static-locale treatment: `getStaticPaths()` plus locale props when
|
|
263
|
+
resources are build-known, or an owned browser effect when selection only exists
|
|
264
|
+
in the browser. This diagnostic adds no semantic primitive, pass, runtime
|
|
265
|
+
concept, browser byte, or successful migration claim.
|
|
266
|
+
|
|
267
|
+
The following Questions-page store reads are not the same reduced Zustand shape:
|
|
268
|
+
`loggedUserInfoStore`, `siteInfoStore`, and `loginSettingStore` own multiple data
|
|
269
|
+
fields, server-fed replacement actions, and storage side effects. Their values
|
|
270
|
+
are not build-known and cannot be preserved by folding initial literals. They
|
|
271
|
+
remain an application data-ownership migration packet; this audit does not
|
|
272
|
+
authorize a Zustand runtime or broaden `SharedStateIR` around one application's
|
|
273
|
+
store graph.
|
|
274
|
+
|
|
275
|
+
## Fifth Reduction Completion
|
|
276
|
+
|
|
277
|
+
Apache Answer's `useQuestionList()` and `useQuestionRecommendList()` wrappers
|
|
278
|
+
compose SWR, axios, and `qs` around browser-only data. The executable reduction
|
|
279
|
+
keeps the Questions page's URL-derived page/order semantics but moves ownership
|
|
280
|
+
to one native fetch effect with application-owned loading, error, and keyed data
|
|
281
|
+
state. The existing dependency replacement contract prevents a delayed old
|
|
282
|
+
response from overwriting a newer query.
|
|
283
|
+
|
|
284
|
+
| Measure | Result |
|
|
285
|
+
|---|---:|
|
|
286
|
+
| Upstream-derived positive fixtures | 1 |
|
|
287
|
+
| Static exclusion fixtures | 1 |
|
|
288
|
+
| Semantic primitives | 0 |
|
|
289
|
+
| ModuleIR/RouteIR records | 0 new |
|
|
290
|
+
| Existing core files changed | 2 |
|
|
291
|
+
| Core LOC delta | +6 |
|
|
292
|
+
| Compiler passes | 0 new |
|
|
293
|
+
| Browser runtime concepts | 0 new |
|
|
294
|
+
| Raw interactive browser ESM | 32,005 B |
|
|
295
|
+
| Static sibling JavaScript | 0 B |
|
|
296
|
+
| Build benchmark delta | Not measured; browser-data compatibility slice |
|
|
297
|
+
|
|
298
|
+
Chrome acceptance starts from `?page=2&order=active`, replaces it with the
|
|
299
|
+
recommendation endpoint, rejects the delayed stale response, renders nested
|
|
300
|
+
keyed tags, reports HTTP 500 through `role="alert"`, and recovers on the next
|
|
301
|
+
query. SWR, axios, `qs`, caches, Providers, retries, deduplication, and background
|
|
302
|
+
refetch remain absent. The only compiler adjustment folds the already validated
|
|
303
|
+
imported immutable string fallback before generic effect dependency analysis.
|
|
304
|
+
|
|
305
|
+
## Sixth Reduction Completion
|
|
306
|
+
|
|
307
|
+
The full Answer source contains 94 direct `Col` elements; 63 use only static
|
|
308
|
+
numeric spans across `xs`, `sm`, `md`, `lg`, `xl`, and `xxl`. The existing React
|
|
309
|
+
Bootstrap normalization now maps integer literals 1 through 12 to native
|
|
310
|
+
Bootstrap classes, omits bare `col` when spans exist, and preserves static
|
|
311
|
+
authored classes. All 98 observed breakpoint props fit this shape.
|
|
312
|
+
|
|
313
|
+
| Measure | Result |
|
|
314
|
+
|---|---:|
|
|
315
|
+
| Upstream-derived positive fixtures | 1 |
|
|
316
|
+
| Nearby negative fixtures | 1 |
|
|
317
|
+
| Observed Col sites covered | 63 / 94 |
|
|
318
|
+
| Semantic primitives | 0 |
|
|
319
|
+
| ModuleIR/RouteIR records | 0 |
|
|
320
|
+
| Existing core files changed | 1 |
|
|
321
|
+
| Core LOC delta | +14 |
|
|
322
|
+
| Compiler passes | 0 new |
|
|
323
|
+
| Browser runtime concepts | 0 |
|
|
324
|
+
| Browser JavaScript | 0 B |
|
|
325
|
+
| Build benchmark delta | Not measured; static normalization slice |
|
|
326
|
+
|
|
327
|
+
Chrome is unnecessary for this static mapping: emitted HTML exactly carries
|
|
328
|
+
`col-md-6`, multi-breakpoint login classes, and the five-span tag grid without
|
|
329
|
+
scripts or behavior markers. Dynamic, boolean, object, offset/order, style,
|
|
330
|
+
spread, and polymorphic forms remain separate evidence packets.
|
|
331
|
+
|
|
332
|
+
## Seventh Reduction Intake
|
|
333
|
+
|
|
334
|
+
The first authentication fixture now builds one layout-owned session record from
|
|
335
|
+
the Answer login/current-user/401 shapes. Native `FormData`, token storage,
|
|
336
|
+
current-user fetch, shared header/settings reads, clear, and replacement
|
|
337
|
+
navigation compile without Zustand, axios, React Router, or React in output; a
|
|
338
|
+
public sibling remains static. One compiler correction prevents shared-state
|
|
339
|
+
identifiers such as `session.token` from being misclassified as imported
|
|
340
|
+
calculation results during effect dependency analysis. This adds no IR kind,
|
|
341
|
+
pass, or runtime concept. Browser login/reload/401 acceptance remains required
|
|
342
|
+
before this intake can be marked a completed authentication reduction.
|
|
343
|
+
|
|
344
|
+
## Route Shell Intake
|
|
345
|
+
|
|
346
|
+
The first route-shell fixture builds seven representative Answer paths: home,
|
|
347
|
+
questions, runtime question detail, tags, runtime tag detail, admin dashboard,
|
|
348
|
+
and legal. `Layout`/`SideNavLayout` become one ordinary composed shell, links are
|
|
349
|
+
native anchors, and bracket pages reuse the existing pathname reader and rewrite
|
|
350
|
+
metadata. No RouterProvider, Outlet, SPA router, semantic primitive, pass, or new
|
|
351
|
+
runtime concept is added; the legal sibling remains zero-JavaScript. Expanding
|
|
352
|
+
the same manifest/composition treatment across all 88 upstream paths remains the
|
|
353
|
+
next route migration gate.
|
|
354
|
+
|
|
355
|
+
Chrome acceptance directly enters `/questions/42/example-question`, verifies
|
|
356
|
+
both runtime pathname fields and the composed primary/section navigation, then
|
|
357
|
+
performs native document navigation to `/tags/kudzu` and verifies the tag field
|
|
358
|
+
in the destination document. No hydration or browser router participates.
|
|
359
|
+
|
|
360
|
+
## Authoring And Admin Intake
|
|
361
|
+
|
|
362
|
+
A backend-independent Answer authoring/admin fixture now builds three routes.
|
|
363
|
+
Question creation uses native constraints, `FormData`, a controlled markdown
|
|
364
|
+
textarea, file text import, preview, and browser-owned persistence. The admin
|
|
365
|
+
route restores that data into a keyed table and deletes rows through existing
|
|
366
|
+
list and handler semantics. React Hook Form, CodeMirror, axios, Zustand, and
|
|
367
|
+
React are absent from deploy handlers, and the legal sibling remains static.
|
|
368
|
+
This intake adds no compiler or runtime concept; rich editor lifecycle, server
|
|
369
|
+
validation, upload progress, dialogs, and complete admin browser journeys remain
|
|
370
|
+
separate gates.
|
|
@@ -13,6 +13,8 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
|
|
|
13
13
|
|
|
14
14
|
The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) and [`0.9-implementation-plan.md`](./0.9-implementation-plan.md). It freezes the `0.8.62` baseline, then closes evidence-backed work on Derived, shared state/actions, resource ownership, component ownership, pass reduction, and final cross-framework proof. [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md) remains the completed foundation and longer-term plan. Model-driven delivery is separately gated for 1.0. ResourceIR remains unapproved; range ownership, virtualization, optimistic transactions, public adapters, and generic runtimes still require independent evidence and architecture review.
|
|
15
15
|
|
|
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
|
+
|
|
16
18
|
## Required Invariants
|
|
17
19
|
|
|
18
20
|
- Every route remains a complete static HTML document.
|
|
@@ -29,19 +31,20 @@ The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](.
|
|
|
29
31
|
2. [`0.9-implementation-plan.md`](./0.9-implementation-plan.md): detailed session packets, benchmark plan, reports, and release procedure.
|
|
30
32
|
3. [`0.9-baseline.md`](./0.9-baseline.md): dated architecture, pass, IR, runtime-concept, LOC, and test baseline.
|
|
31
33
|
4. [`0.9-benchmark-contracts.md`](./0.9-benchmark-contracts.md): frozen matched-content, stateful, commerce, scale, resource, and anti-gaming contracts.
|
|
32
|
-
5. [`
|
|
33
|
-
6. [`
|
|
34
|
-
7. [`
|
|
35
|
-
8. [`
|
|
36
|
-
9. [`
|
|
37
|
-
10. [`
|
|
38
|
-
11. [`goal-
|
|
39
|
-
12. [`goal-
|
|
34
|
+
5. [`1.0-large-application-compatibility-audit.md`](./1.0-large-application-compatibility-audit.md): local large-application migration probes, retention caveats, blockers, and authorized-protocol handoff.
|
|
35
|
+
6. [`compiler-current-architecture.md`](./compiler-current-architecture.md): exact current responsibilities and data flow.
|
|
36
|
+
7. [`performance-gates.md`](./performance-gates.md): output, build, and browser gates.
|
|
37
|
+
8. [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md): completed foundation, longer-term direction, and completion evidence.
|
|
38
|
+
9. [`goal-a-compiler-foundation.md`](./goal-a-compiler-foundation.md): completed extraction and generator foundation.
|
|
39
|
+
10. [`versioning.md`](./versioning.md): completed patch sequence and future sequencing rules.
|
|
40
|
+
11. [`goal-b-optimization-benchmarks.md`](./goal-b-optimization-benchmarks.md): active measurements, retained optimizations, and continuation rules.
|
|
41
|
+
12. [`goal-c-state-resource-research.md`](./goal-c-state-resource-research.md): research boundary only.
|
|
42
|
+
13. [`goal-d-routing-compatibility-decisions.md`](./goal-d-routing-compatibility-decisions.md): routing and islands decisions.
|
|
40
43
|
|
|
41
44
|
## Resume Checklist
|
|
42
45
|
|
|
43
46
|
- [ ] Read [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md), [`0.9-semantic-compression.md`](./0.9-semantic-compression.md), and [`0.9-implementation-plan.md`](./0.9-implementation-plan.md) before planning migration work.
|
|
44
|
-
- [ ] Follow the
|
|
47
|
+
- [ ] Follow the authorized 1.0 AI delivery protocol; treat post-0.9 compatibility audits as candidate-selection input, not replacement work queues.
|
|
45
48
|
- [ ] Confirm `package.json` still reports the actual current version; never infer release status from this plan.
|
|
46
49
|
- [ ] Inspect the worktree and preserve unrelated or uncommitted changes.
|
|
47
50
|
- [ ] Start later architecture work only by an explicit decision from the completed Goal A baseline.
|
|
@@ -12,6 +12,7 @@ Supported React Router-shaped migration remains narrow:
|
|
|
12
12
|
|
|
13
13
|
- static root-relative `Link` lowers to a base-prefixed native `<a>`;
|
|
14
14
|
- bracket runtime routes may lower direct `useParams()` to the pathname reader;
|
|
15
|
+
- route-owned exact static root-relative `useMatch()` bindings fold case-insensitively from the build-known application route;
|
|
15
16
|
- top-level direct `useSearchParams()` reads/writes lower to URL signals and native history;
|
|
16
17
|
- direct nested `useNavigate()` calls lower to native `location.assign()` or `location.replace()`;
|
|
17
18
|
- no React Router package executes in build output or the browser.
|
package/framework/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Native platform work remains ordinary source. A direct async handler or directly
|
|
|
10
10
|
|
|
11
11
|
Imperative canvas migrations use the same effect ownership rather than a component or canvas runtime. One `null`-initialized canvas DOM ref may feed an inline effect whose local variables persist across a recursive animation-frame callback, an `IntersectionObserver`, and native canvas/window listeners; the returned cleanup must cancel the latest frame, disconnect the observer, and remove every listener. Bare `IntersectionObserver` and `performance` identifiers remain browser globals in emitted effect ESM. Component-level mutable value refs and callbacks shared across effects or JSX handlers remain unsupported; move resource-private state and listeners into the owning effect.
|
|
12
12
|
|
|
13
|
-
Localized static migrations do not need a package router. `getStaticPaths()` emits each build-known locale, a small relative Link replacement prefixes native hrefs from the locale prop, and an optional `/` mount effect may choose a stored locale or inspect `navigator.languages` before calling `location.replace()` with the original query and hash. This preserves automatic browser-language entry and locale-prefixed navigation; request-time `Accept-Language` negotiation still requires host or edge configuration. Build-known MDX becomes static `dangerouslySetInnerHTML`, while copy controls, tabs, and other supported interactive components remain ordinary state and handler capabilities beside that HTML.
|
|
13
|
+
Localized static migrations do not need a package router. `getStaticPaths()` emits each build-known locale, a small relative Link replacement prefixes native hrefs from the locale prop, and an optional `/` mount effect may choose a stored locale or inspect `navigator.languages` before calling `location.replace()` with the original query and hash. Runtime-resource `react-i18next` `useTranslation()` calls receive a source diagnostic directing this migration instead of silently selecting one language or retaining i18next. This preserves automatic browser-language entry and locale-prefixed navigation; request-time `Accept-Language` negotiation still requires host or edge configuration. Build-known MDX becomes static `dangerouslySetInnerHTML`, while copy controls, tabs, and other supported interactive components remain ordinary state and handler capabilities beside that HTML.
|
|
14
14
|
|
|
15
15
|
A top-level component local shaped exactly as `const supported = "literal" in navigator` may control one direct `supported && <Branch />` JSX site. Kudzu emits the branch as false static fallback, evaluates the browser capability once through an existing mount effect, and uses existing state-conditional ownership to mount its DOM and handlers only when supported. The value cannot escape or feed aliases, composed expressions, ternaries, dynamic properties, `canShare()`, or other browser globals.
|
|
16
16
|
|
|
@@ -20,10 +20,16 @@ A named or aliased `Link` import from `react-router-dom` may render directly wit
|
|
|
20
20
|
|
|
21
21
|
A named or aliased React Router `useParams` import may also be called directly without runtime arguments on a bracket route exporting `runtimeParams = true`. The compiler redirects that local binding to `@kudzujs/core`, preserving one optional TypeScript type argument, and reuses the existing route-specific pathname matcher. Indirect calls and other router hooks remain unsupported. Build-known `getStaticPaths()` routes use page props instead because no browser pathname capability is needed.
|
|
22
22
|
|
|
23
|
-
A named or aliased React Router `
|
|
23
|
+
A named or aliased React Router `useMatch` import may directly initialize one top-level `const` from an exact static root-relative string such as `useMatch("/")` in route scope. The compiler compares it case-insensitively with the build-known application route and returns the conventional match object or `null` during rendering, then erases the router import. Reused relative components receive the route of each page invocation. Layout use, runtime-parameter pages, params, wildcards, query/hash patterns, trailing slashes, dynamic patterns, indirect calls, and pattern objects are rejected. No route matcher ships to the browser.
|
|
24
|
+
|
|
25
|
+
A named or aliased React Router `useSearchParams` import may initialize one top-level `const [params]` or `const [params, setParams]` binding. Each top-level `const value = params.get("literal")` lowers to one cached nullable `useSearchParam()` signal. The exact top-level pagination form `const page = Number(params.get("literal")) || finiteNumber` and string form `const order = params.get("literal") || importedImmutableArray[staticIndex]` separate that raw signal into a compiler-owned local and reuse the existing primitive binding evaluator. The string fallback must be a static numeric element of a named relative JSON-safe immutable array. New Kudzu source may call the literal reader directly; binding nullable query values to hidden input `value` and `disabled` attributes initializes native GET form carry without a generic mount effect. Direct setter calls inside nested browser callbacks accept one synchronous inline updater over native `URLSearchParams`; no options pushes history and exactly `{ replace: true }` replaces it. The route parameter asset recommits changed query signals and follows `popstate`, including read-only routes. Missing keys remain `null`, direct text renders blank, and nullable attributes are removed. Dynamic names or indexes, direct-value setters, other methods on the outer params object, aliases, indirect reads, broader wrappers, and layout ownership are rejected. Routes without query reads or writes do not emit this branch or a parameter asset.
|
|
24
26
|
|
|
25
27
|
A named or aliased React Router `useNavigate` import may initialize one top-level `const` identifier. A direct call from a nested browser callback with one safe static root-relative string lowers to `location.assign()` after applying `base`; exactly `{ replace: true }` lowers to `location.replace()`. This deliberately performs native document navigation even when enhanced navigation is configured. Dynamic or relative destinations, render-time calls, aliases passed as values, and options such as `state`, `relative`, or `preventScrollReset` are rejected. Routes without an actual navigation handler emit no browser JavaScript.
|
|
26
28
|
|
|
29
|
+
Named or aliased React Bootstrap `Row` and `Col` imports may be used as direct JSX elements with children and at most one static `className`. The React migration normalization pass erases those package imports and emits native `<div>` elements. `Row` emits `row`; a `Col` without spans emits `col`, while numeric literal `xs`, `sm`, `md`, `lg`, `xl`, and `xxl` values from 1 through 12 emit Bootstrap span classes in deterministic largest-to-smallest breakpoint order. The application must keep Bootstrap CSS reachable. Boolean, object, dynamic, offset, and order grid props, dynamic classes, spreads, default/subpath/namespace imports, non-JSX references, and other React Bootstrap components are rejected or remain unsupported. This shape emits no browser JavaScript.
|
|
30
|
+
|
|
31
|
+
Browser-only package-query migrations use one inline dependency effect with native `fetch()` and application-owned loading, error, and data state. Apache Answer-shaped page/order search signals may derive both effect dependencies and endpoint values; dependency replacement prevents a superseded response from writing state, while existing keyed-list ownership handles nested result rows. SWR, axios, `qs`, query clients, caches, Providers, retries, and background schedulers are not retained. Build-known reads should remain async page/component work instead.
|
|
32
|
+
|
|
27
33
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
28
34
|
|
|
29
35
|
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Reactive text and attributes may reference recursively chained top-level immutable locals derived through supported pure primitive expressions from direct state; the compiler substitutes those expressions into the existing binding evaluator and subscribes every source state. Fixed-locale `new Intl.NumberFormat("literal").format(Math.round(expression))` display chains reuse that binding ESM, while dynamic locales and options remain unsupported. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
@@ -47,7 +53,7 @@ Reduced Zustand migration stores lower through package-neutral `SharedStateIR` a
|
|
|
47
53
|
- `compiler/handler-lowering.mjs`: source-local native/effect callback, reducer, Context, package-neutral shared-action, reactive-binding, and list-evaluator AST lowering before IR finalization.
|
|
48
54
|
- `compiler/handler-codegen.mjs`: AST-free ordered import rendering and finalized handler/binding export-source assembly.
|
|
49
55
|
- `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
|
|
50
|
-
- `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
|
|
56
|
+
- `compiler/router-pass.mjs`: React Router import validation and native Link, build-time exact match, pathname parameter, search parameter, and imperative navigation lowering.
|
|
51
57
|
- `compiler/route-capability-planner.mjs`: RouteIR v1 validation and pure CapabilityIR v1 projection into runtime and artifact requirements.
|
|
52
58
|
- `compiler/runtime-family-planner.mjs`: deterministic standalone capability deduplication and enhanced-navigation group unioning at the ESM singleton boundary.
|
|
53
59
|
- `compiler/route-artifact-report.mjs`: deterministic per-route capability signatures and runtime requirements plus handler, Worker, stylesheet, and transitive esbuild chunk closure reporting.
|
|
@@ -80,7 +86,7 @@ Static routes receive no browser runtime. Command routes receive `runtime.js`; d
|
|
|
80
86
|
|
|
81
87
|
Exact relative `.worker.ts` constructors in inline effects are validated and bundled in a separate content-hashed ESM graph under `dist/assets/workers/`. Those files are referenced only by rendered effect handlers and never become document capability scripts, preloads, or window imports; unreachable source effects do not emit their Worker roots. Worker graphs allow relative TypeScript ESM runtime imports only and reject JSX, package runtime imports, import-equals declarations, dynamic imports, `require()`, and paths outside `src`. Ordinary runtime imports or re-exports of `.worker.ts` and Worker construction in imported keyed-row effects are rejected.
|
|
82
88
|
|
|
83
|
-
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Relative source CSS imports follow each page's reachable TypeScript graph and link only from those routes; `kudzu.config` styles remain explicitly global. Enhanced navigation loads incoming managed styles before route replacement, reuses shared links, and removes outgoing links. All styles are emitted before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
89
|
+
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Relative source CSS imports follow each page's reachable TypeScript graph and link only from those routes; `kudzu.config` styles remain explicitly global. For pre-0.8.53 projects with source CSS but no explicit style ownership at all, Kudzu preserves the former global path-ordered output and prints a migration warning instead of silently emitting an unstyled deploy. Enhanced navigation loads incoming managed styles before route replacement, reuses shared links, and removes outgoing links. All styles are emitted before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
84
90
|
|
|
85
91
|
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases use the existing generic `setAttribute` path. Reactive conditionals and flat intrinsic keyed lists store inert branch or row markup on SVG markers and parse it in the actual parent namespace only when replacement nodes are needed; existing condition/list ownership then handles insertion, identity, updates, and removal. Focus, keyboard, and click handlers on keyed SVG points may update parent state for an external accessible HTML tooltip, and retained handlers read the latest item after list updates. Builds without structural SVG compile out that fragment path, and static SVG adds no JavaScript. Keyed-item conditions, nested SVG lists, MathML structures, and namespaced attributes remain unsupported.
|
|
86
92
|
|
package/framework/build.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
|
|
|
45
45
|
try {
|
|
46
46
|
await recoverOutput(outputDirectory, backupOutput)
|
|
47
47
|
await rm(stagedOutput, { recursive: true, force: true })
|
|
48
|
-
const { result, pageCount, behaviorCount, cache } = await buildInto(project, stagedOutput, { changedFiles, minify, retainCache })
|
|
48
|
+
const { result, pageCount, behaviorCount, cache } = await buildInto(project, stagedOutput, { changedFiles, minify, quiet, retainCache })
|
|
49
49
|
await promoteOutput(stagedOutput, outputDirectory, backupOutput)
|
|
50
50
|
project.buildCache = retainCache ? cache : undefined
|
|
51
51
|
if (!quiet) console.log(`Built ${pageCount} page(s), ${behaviorCount} interactive page(s) into dist/`)
|
|
@@ -63,7 +63,7 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
async function buildInto(project, outputDirectory, { changedFiles, minify, retainCache }) {
|
|
66
|
+
async function buildInto(project, outputDirectory, { changedFiles, minify, quiet, retainCache }) {
|
|
67
67
|
const { root, sourceDirectory, pagesDirectory, workDirectory } = project
|
|
68
68
|
const previous = retainCache ? project.buildCache : undefined
|
|
69
69
|
project.buildGeneration = (project.buildGeneration ?? 0) + 1
|
|
@@ -108,6 +108,14 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, retai
|
|
|
108
108
|
for (const file of sourceFiles) sourceFileSet.add(file)
|
|
109
109
|
const staticFiles = await safeStaticFiles(projectFiles)
|
|
110
110
|
const stylesByPage = new Map(pageFiles.map(file => [file, orderSourceStyles([file], sourceFiles, sourceIndex, staticFiles).filter(style => !configuredStyleSources.has(style))]))
|
|
111
|
+
const explicitSourceStyles = new Set([...stylesByPage.values()].flat())
|
|
112
|
+
const legacyStyles = configuredStyles.sources.length || configuredStyles.urls.length || explicitSourceStyles.size
|
|
113
|
+
? []
|
|
114
|
+
: [...staticFiles].filter(file => file.toLowerCase().endsWith(".css")).sort()
|
|
115
|
+
if (legacyStyles.length) {
|
|
116
|
+
for (const file of pageFiles) stylesByPage.set(file, legacyStyles)
|
|
117
|
+
if (!quiet) console.warn("Kudzu: no route imports CSS and kudzu.config styles is empty; applying the legacy global CSS fallback. Import CSS from reachable source or declare config.styles for route-aware output.")
|
|
118
|
+
}
|
|
111
119
|
const cssFiles = [...new Set([...stylesByPage.values()].flat())]
|
|
112
120
|
const importedAssets = new Set()
|
|
113
121
|
const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base, project)
|
|
@@ -238,6 +246,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify, retai
|
|
|
238
246
|
styles: styleUrls.length ? styleUrls : false,
|
|
239
247
|
managedStyles: navigable ? sourceStyleUrls : [],
|
|
240
248
|
base,
|
|
249
|
+
applicationRoute,
|
|
241
250
|
runtimeAsset: runtimePlaceholder,
|
|
242
251
|
bindingAsset: bindingPlaceholder,
|
|
243
252
|
listAsset: listPlaceholder,
|
|
@@ -39,7 +39,7 @@ function decodeSegment(raw, param) {
|
|
|
39
39
|
return value
|
|
40
40
|
}
|
|
41
41
|
` : ""
|
|
42
|
-
const searchInitializer =
|
|
42
|
+
const searchInitializer = searchParams.length ? `function initializeSearch(search) {
|
|
43
43
|
const query = new URLSearchParams(search)
|
|
44
44
|
for (const param of ${inlineJson(searchParams)}) {
|
|
45
45
|
const value = query.get(param.name)
|
|
@@ -48,13 +48,7 @@ for (const param of ${inlineJson(searchParams)}) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
` : ""
|
|
51
|
-
const query = searchParams.length ?
|
|
52
|
-
for (const param of ${inlineJson(searchParams)}) {
|
|
53
|
-
const value = query.get(param.name)
|
|
54
|
-
browserState.set(param.id, value)
|
|
55
|
-
commitDom(param.id, value)
|
|
56
|
-
}
|
|
57
|
-
` : ""
|
|
51
|
+
const query = searchParams.length ? "initializeSearch(search)\n" : ""
|
|
58
52
|
const writer = searchParamsWritable ? `
|
|
59
53
|
function setSearchParams(update, replace) {
|
|
60
54
|
const next = update(new URLSearchParams(location.search))
|
|
@@ -66,7 +60,8 @@ function setSearchParams(update, replace) {
|
|
|
66
60
|
}
|
|
67
61
|
${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
|
|
68
62
|
addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
|
|
63
|
+
const reader = !navigable && !searchParamsWritable && searchParams.length ? '\naddEventListener("popstate", () => initializeSearch(location.search))' : ""
|
|
69
64
|
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}
|
|
70
|
-
${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
|
|
65
|
+
${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}${reader}`
|
|
71
66
|
}
|
|
72
67
|
}
|
|
@@ -3,6 +3,68 @@ import { bindingNames, importDeclarationNames, isFunctionLike, isLocalConst, isR
|
|
|
3
3
|
import { analyzeCollectionPipeline, isArrayFromCall } from "./collection-analysis.mjs"
|
|
4
4
|
|
|
5
5
|
export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
6
|
+
function normalizeReactBootstrapLayout(sourceFile, factory, context) {
|
|
7
|
+
const layouts = new Map()
|
|
8
|
+
for (const statement of sourceFile.statements) {
|
|
9
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react-bootstrap") continue
|
|
10
|
+
const clause = statement.importClause
|
|
11
|
+
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
|
+
for (const entry of clause.namedBindings.elements) {
|
|
13
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
14
|
+
if (!entry.isTypeOnly && ["Row", "Col"].includes(imported)) layouts.set(entry.name.text, imported.toLowerCase())
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!layouts.size) return sourceFile
|
|
18
|
+
|
|
19
|
+
const layout = tag => ts.isIdentifier(tag) && layouts.has(tag.text) && !isShadowedIdentifier(tag, sourceFile) ? layouts.get(tag.text) : undefined
|
|
20
|
+
const attributes = node => {
|
|
21
|
+
let authored
|
|
22
|
+
const spans = new Map()
|
|
23
|
+
const kind = layout(node.parent.tagName)
|
|
24
|
+
const breakpoints = ["xxl", "xl", "lg", "md", "sm", "xs"]
|
|
25
|
+
for (const property of node.properties) {
|
|
26
|
+
if (ts.isJsxSpreadAttribute(property)) throw sourceNodeError(property, sourceFile, "React Bootstrap Row and Col do not support spread props during native layout lowering")
|
|
27
|
+
const name = property.name.text
|
|
28
|
+
const initializer = property.initializer
|
|
29
|
+
const value = ts.isStringLiteral(initializer) ? initializer : initializer && ts.isJsxExpression(initializer) ? unwrapExpression(initializer.expression) : undefined
|
|
30
|
+
if (name === "className") {
|
|
31
|
+
if (authored !== undefined) throw sourceNodeError(property, sourceFile, "React Bootstrap Row and Col accept className only once")
|
|
32
|
+
if (!value || !ts.isStringLiteral(value) && !ts.isNoSubstitutionTemplateLiteral(value)) throw sourceNodeError(property, sourceFile, "React Bootstrap Row and Col className must be a static string")
|
|
33
|
+
authored = value.text
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (kind === "col" && breakpoints.includes(name)) {
|
|
37
|
+
const span = value && ts.isNumericLiteral(value) ? Number(value.text) : NaN
|
|
38
|
+
if (!Number.isInteger(span) || span < 1 || span > 12) throw sourceNodeError(property, sourceFile, `React Bootstrap Col breakpoint ${JSON.stringify(name)} must be an integer literal from 1 through 12`)
|
|
39
|
+
spans.set(name, span)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
throw sourceNodeError(property, sourceFile, `React Bootstrap Row and Col only support a static className or numeric Col breakpoint prop; found ${JSON.stringify(name)}`)
|
|
43
|
+
}
|
|
44
|
+
const generated = kind === "row" ? ["row"] : spans.size ? breakpoints.filter(name => spans.has(name)).map(name => name === "xs" ? `col-${spans.get(name)}` : `col-${name}-${spans.get(name)}`) : ["col"]
|
|
45
|
+
return factory.createJsxAttributes([factory.createJsxAttribute(factory.createIdentifier("className"), factory.createStringLiteral([...generated, authored].filter(Boolean).join(" ")))])
|
|
46
|
+
}
|
|
47
|
+
const visitor = node => {
|
|
48
|
+
if (ts.isJsxElement(node) && layout(node.openingElement.tagName)) {
|
|
49
|
+
const opening = factory.updateJsxOpeningElement(node.openingElement, factory.createIdentifier("div"), node.openingElement.typeArguments, attributes(node.openingElement.attributes))
|
|
50
|
+
const closing = factory.updateJsxClosingElement(node.closingElement, factory.createIdentifier("div"))
|
|
51
|
+
return factory.updateJsxElement(node, opening, ts.visitNodes(node.children, visitor), closing)
|
|
52
|
+
}
|
|
53
|
+
if (ts.isJsxSelfClosingElement(node) && layout(node.tagName)) return factory.updateJsxSelfClosingElement(node, factory.createIdentifier("div"), node.typeArguments, attributes(node.attributes))
|
|
54
|
+
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 === "react-bootstrap") {
|
|
56
|
+
const clause = node.importClause
|
|
57
|
+
const bindings = clause?.namedBindings
|
|
58
|
+
if (!clause || !bindings || !ts.isNamedImports(bindings)) return node
|
|
59
|
+
const elements = bindings.elements.filter(entry => entry.isTypeOnly || !["Row", "Col"].includes((entry.propertyName ?? entry.name).text))
|
|
60
|
+
if (!elements.length) return undefined
|
|
61
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, undefined, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
|
|
62
|
+
}
|
|
63
|
+
return ts.visitEachChild(node, visitor, context)
|
|
64
|
+
}
|
|
65
|
+
return ts.visitNode(sourceFile, visitor)
|
|
66
|
+
}
|
|
67
|
+
|
|
6
68
|
function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
|
|
7
69
|
const supported = new Set(["createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
|
|
8
70
|
const erased = new Set(["createRef", "forwardRef", "memo", "useCallback", "useMemo"])
|
|
@@ -19,7 +81,7 @@ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
|
19
81
|
else if (!entry.isTypeOnly && /^use[A-Z]/.test(imported)) throw sourceNodeError(entry, sourceFile, `React ${imported} is not supported by Kudzu migration input`)
|
|
20
82
|
}
|
|
21
83
|
}
|
|
22
|
-
if (!aliases.size && !reactObjects.size) return sourceFile
|
|
84
|
+
if (!aliases.size && !reactObjects.size) return normalizeReactBootstrapLayout(sourceFile, factory, context)
|
|
23
85
|
|
|
24
86
|
const migrationCallName = call => {
|
|
25
87
|
if (ts.isIdentifier(call.expression) && aliases.has(call.expression.text) && !isShadowedIdentifier(call.expression, sourceFile)) return aliases.get(call.expression.text)
|
|
@@ -193,7 +255,7 @@ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
|
193
255
|
}
|
|
194
256
|
let normalized = ts.visitNode(sourceFile, visitor)
|
|
195
257
|
const missing = [...required].filter(name => !imported.has(name)).sort()
|
|
196
|
-
if (!missing.length) return normalized
|
|
258
|
+
if (!missing.length) return normalizeReactBootstrapLayout(normalized, factory, context)
|
|
197
259
|
for (const name of missing) {
|
|
198
260
|
const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== "react")
|
|
199
261
|
if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
|
|
@@ -203,7 +265,7 @@ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
|
|
|
203
265
|
const lastImport = statements.findLastIndex(statement => ts.isImportDeclaration(statement))
|
|
204
266
|
statements.splice(lastImport + 1, 0, declaration)
|
|
205
267
|
normalized = factory.updateSourceFile(normalized, statements)
|
|
206
|
-
return normalized
|
|
268
|
+
return normalizeReactBootstrapLayout(normalized, factory, context)
|
|
207
269
|
}
|
|
208
270
|
|
|
209
271
|
function lowerReactForwardRef(call, sourceFile, factory) {
|
|
@@ -2,9 +2,10 @@ import ts from "typescript"
|
|
|
2
2
|
import { bindingNames, functionVarDeclaresName, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedIdentifier, loopDeclaresName, nearestFunction, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
3
|
|
|
4
4
|
export function createRouterPass({ withBase }) {
|
|
5
|
-
return function normalizeReactRouterSyntax(sourceFile, factory, context, base) {
|
|
5
|
+
return function normalizeReactRouterSyntax(sourceFile, factory, context, base, importedCollections = new Map()) {
|
|
6
6
|
const links = new Set()
|
|
7
7
|
const params = new Set()
|
|
8
|
+
const matchHooks = new Set()
|
|
8
9
|
const searchHooks = new Set()
|
|
9
10
|
const navigateHooks = new Set()
|
|
10
11
|
for (const statement of sourceFile.statements) {
|
|
@@ -13,22 +14,62 @@ export function createRouterPass({ withBase }) {
|
|
|
13
14
|
const clause = statement.importClause
|
|
14
15
|
if (clause?.isTypeOnly) continue
|
|
15
16
|
if (!clause) throw sourceNodeError(statement, sourceFile, "Side-effect React Router imports are not supported")
|
|
16
|
-
if (clause.name) throw sourceNodeError(clause.name, sourceFile, "React Router default imports are not supported; use named Link, useParams, useSearchParams, or useNavigate imports")
|
|
17
|
+
if (clause.name) throw sourceNodeError(clause.name, sourceFile, "React Router default imports are not supported; use named Link, useParams, useMatch, useSearchParams, or useNavigate imports")
|
|
17
18
|
const bindings = clause.namedBindings
|
|
18
|
-
if (!bindings || ts.isNamespaceImport(bindings)) throw sourceNodeError(bindings ?? statement, sourceFile, "React Router namespace imports are not supported; use named Link, useParams, useSearchParams, or useNavigate imports")
|
|
19
|
+
if (!bindings || ts.isNamespaceImport(bindings)) throw sourceNodeError(bindings ?? statement, sourceFile, "React Router namespace imports are not supported; use named Link, useParams, useMatch, useSearchParams, or useNavigate imports")
|
|
19
20
|
for (const entry of bindings.elements) {
|
|
20
21
|
if (entry.isTypeOnly) continue
|
|
21
22
|
const imported = (entry.propertyName ?? entry.name).text
|
|
22
23
|
if (imported === "NavLink") throw sourceNodeError(entry, sourceFile, "React Router NavLink active-route semantics cannot be erased to a native anchor")
|
|
23
24
|
if (imported === "Link") links.add(entry.name.text)
|
|
24
25
|
else if (imported === "useParams") params.add(entry.name.text)
|
|
26
|
+
else if (imported === "useMatch") matchHooks.add(entry.name.text)
|
|
25
27
|
else if (imported === "useSearchParams") searchHooks.add(entry.name.text)
|
|
26
28
|
else if (imported === "useNavigate") navigateHooks.add(entry.name.text)
|
|
27
|
-
else throw sourceNodeError(entry, sourceFile, `React Router ${imported} is not supported; only named Link, useParams, useSearchParams, and useNavigate imports can be lowered`)
|
|
29
|
+
else throw sourceNodeError(entry, sourceFile, `React Router ${imported} is not supported; only named Link, useParams, useMatch, useSearchParams, and useNavigate imports can be lowered`)
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
|
-
if (!links.size && !params.size && !searchHooks.size && !navigateHooks.size) return sourceFile
|
|
33
|
+
if (!links.size && !params.size && !matchHooks.size && !searchHooks.size && !navigateHooks.size) return sourceFile
|
|
34
|
+
|
|
35
|
+
let matchHelper = "__kUseRouteMatch"
|
|
36
|
+
while (sourceFile.text.includes(matchHelper)) matchHelper += "_"
|
|
37
|
+
const matchCalls = new Map()
|
|
38
|
+
const topLevelFunction = owner => owner.parent === sourceFile || ts.isExportAssignment(owner.parent) && owner.parent.parent === sourceFile || (ts.isArrowFunction(owner) || ts.isFunctionExpression(owner)) && ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
|
|
39
|
+
const componentFunction = owner => {
|
|
40
|
+
if (!topLevelFunction(owner)) return false
|
|
41
|
+
if (ts.isExportAssignment(owner.parent)) return true
|
|
42
|
+
if (ts.isFunctionDeclaration(owner) && owner.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) return true
|
|
43
|
+
const name = ts.isFunctionDeclaration(owner) ? owner.name?.text : ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : undefined
|
|
44
|
+
if (!name || !/^[A-Z]/.test(name)) return false
|
|
45
|
+
let eventHandlerUse = false
|
|
46
|
+
const inspect = node => {
|
|
47
|
+
if (eventHandlerUse) return
|
|
48
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) {
|
|
49
|
+
for (let current = node.parent; current && current !== sourceFile; current = current.parent) {
|
|
50
|
+
if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) eventHandlerUse = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
ts.forEachChild(node, inspect)
|
|
54
|
+
}
|
|
55
|
+
inspect(sourceFile)
|
|
56
|
+
return !eventHandlerUse
|
|
57
|
+
}
|
|
58
|
+
const collectMatchHooks = node => {
|
|
59
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && matchHooks.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
|
|
60
|
+
const declaration = node.parent
|
|
61
|
+
const statement = declaration?.parent?.parent
|
|
62
|
+
const owner = nearestFunction(node)
|
|
63
|
+
if (node.questionDotToken || node.typeArguments?.length || node.arguments.length !== 1 || !ts.isStringLiteral(node.arguments[0]) || !ts.isVariableDeclaration(declaration) || declaration.initializer !== node || !ts.isIdentifier(declaration.name) || !isLocalConst(declaration) || !owner || !componentFunction(owner) || statement?.parent !== owner.body) {
|
|
64
|
+
throw sourceNodeError(node, sourceFile, 'React Router useMatch must directly initialize one top-level const from one static root-relative pattern such as useMatch("/")')
|
|
65
|
+
}
|
|
66
|
+
const pattern = node.arguments[0].text
|
|
67
|
+
if (!pattern.startsWith("/") || pattern.startsWith("//") || pattern !== "/" && pattern.endsWith("/") || /[:*?#\\\0]/.test(pattern)) throw sourceNodeError(node.arguments[0], sourceFile, 'React Router useMatch only supports an exact static root-relative pattern without params, wildcards, query, hash, or a trailing slash')
|
|
68
|
+
matchCalls.set(node, pattern)
|
|
69
|
+
}
|
|
70
|
+
ts.forEachChild(node, collectMatchHooks)
|
|
71
|
+
}
|
|
72
|
+
collectMatchHooks(sourceFile)
|
|
32
73
|
|
|
33
74
|
let searchHelper = "__kUseSearchParam"
|
|
34
75
|
while (sourceFile.text.includes(searchHelper)) searchHelper += "_"
|
|
@@ -36,8 +77,18 @@ export function createRouterPass({ withBase }) {
|
|
|
36
77
|
while (sourceFile.text.includes(searchWriterHelper)) searchWriterHelper += "_"
|
|
37
78
|
const searchDeclarations = new Set()
|
|
38
79
|
const searchReads = new Map()
|
|
80
|
+
const searchFallbackValues = new Map()
|
|
81
|
+
const composedSearchDeclarations = new Map()
|
|
82
|
+
const composedSearchCalls = new Map()
|
|
39
83
|
const searchWrites = new Map()
|
|
40
84
|
const searchObjects = []
|
|
85
|
+
let composedSearchIndex = 0
|
|
86
|
+
const composedSearchName = () => {
|
|
87
|
+
let name
|
|
88
|
+
do name = `__kRouterSearchParam${composedSearchIndex++ || ""}`
|
|
89
|
+
while (sourceFile.text.includes(name) || [...composedSearchCalls.values()].includes(name))
|
|
90
|
+
return name
|
|
91
|
+
}
|
|
41
92
|
const collectSearchHooks = node => {
|
|
42
93
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && searchHooks.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
|
|
43
94
|
const declaration = node.parent
|
|
@@ -71,15 +122,38 @@ export function createRouterPass({ withBase }) {
|
|
|
71
122
|
if (entry.setter && ts.isCallExpression(node.parent) && node.parent.arguments.includes(node) && ts.isIdentifier(node.parent.expression) && node.parent.expression.text === entry.setter) return
|
|
72
123
|
const property = node.parent
|
|
73
124
|
const call = property?.parent
|
|
74
|
-
const
|
|
125
|
+
const directDeclaration = call?.parent
|
|
126
|
+
const numberCall = directDeclaration && ts.isCallExpression(directDeclaration) && ts.isIdentifier(directDeclaration.expression) && directDeclaration.expression.text === "Number" && !isShadowedIdentifier(directDeclaration.expression, sourceFile) && directDeclaration.arguments.length === 1 && directDeclaration.arguments[0] === call && !directDeclaration.questionDotToken && !directDeclaration.typeArguments?.length ? directDeclaration : undefined
|
|
127
|
+
const binary = numberCall?.parent ?? directDeclaration
|
|
128
|
+
const numericRight = binary?.right
|
|
129
|
+
const numericValue = numericRight && ts.isNumericLiteral(numericRight) ? Number(numericRight.text) : numericRight && ts.isPrefixUnaryExpression(numericRight) && [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken].includes(numericRight.operator) && ts.isNumericLiteral(numericRight.operand) ? Number(numericRight.getText(sourceFile)) : NaN
|
|
130
|
+
const numericFallback = numberCall && ts.isBinaryExpression(binary) && binary.left === numberCall && binary.operatorToken.kind === ts.SyntaxKind.BarBarToken && Number.isFinite(numericValue) ? binary : undefined
|
|
131
|
+
const importedValue = !numberCall && ts.isBinaryExpression(binary) && binary.left === call && binary.operatorToken.kind === ts.SyntaxKind.BarBarToken ? binary.right : undefined
|
|
132
|
+
const importedCollection = importedValue && ts.isElementAccessExpression(importedValue) && !importedValue.questionDotToken && ts.isIdentifier(importedValue.expression) && ts.isNumericLiteral(importedValue.argumentExpression) ? importedCollections.get(importedValue.expression.text) : undefined
|
|
133
|
+
const importedIndex = importedValue && ts.isElementAccessExpression(importedValue) && ts.isNumericLiteral(importedValue.argumentExpression) ? Number(importedValue.argumentExpression.text) : undefined
|
|
134
|
+
const importedElement = importedCollection && Number.isSafeInteger(importedIndex) ? importedCollection.elements[importedIndex] : undefined
|
|
135
|
+
const importedString = importedElement && unwrapExpression(importedElement)
|
|
136
|
+
const importedFallback = importedString && (ts.isStringLiteral(importedString) || ts.isNoSubstitutionTemplateLiteral(importedString)) ? binary : undefined
|
|
137
|
+
const fallback = numericFallback ?? importedFallback
|
|
138
|
+
let composedInitializer = fallback
|
|
139
|
+
while (composedInitializer?.parent && (ts.isParenthesizedExpression(composedInitializer.parent) || ts.isAsExpression(composedInitializer.parent) || ts.isTypeAssertionExpression(composedInitializer.parent) || ts.isSatisfiesExpression(composedInitializer.parent) || ts.isNonNullExpression(composedInitializer.parent)) && composedInitializer.parent.expression === composedInitializer) composedInitializer = composedInitializer.parent
|
|
140
|
+
const composedDeclaration = composedInitializer?.parent
|
|
141
|
+
const declaration = directDeclaration && ts.isVariableDeclaration(directDeclaration) ? directDeclaration : composedDeclaration && ts.isVariableDeclaration(composedDeclaration) ? composedDeclaration : undefined
|
|
75
142
|
const statement = declaration?.parent?.parent
|
|
76
143
|
if (!ts.isPropertyAccessExpression(property) || property.expression !== node || property.name.text !== "get" || !ts.isCallExpression(call) || call.expression !== property || call.questionDotToken || call.typeArguments?.length || call.arguments.length !== 1 || !ts.isStringLiteral(call.arguments[0])) {
|
|
77
144
|
throw sourceNodeError(node, sourceFile, 'React Router search parameters only support direct get("static-name") reads')
|
|
78
145
|
}
|
|
79
|
-
if (!
|
|
80
|
-
throw sourceNodeError(call, sourceFile,
|
|
146
|
+
if (!declaration || declaration.initializer !== call && declaration.initializer !== composedInitializer || !ts.isIdentifier(declaration.name) || !isLocalConst(declaration) || statement?.parent !== entry.owner.body) {
|
|
147
|
+
throw sourceNodeError(call, sourceFile, 'React Router search parameter get() must directly initialize one top-level const, appear as Number(params.get("name")) || finiteNumber, or use params.get("name") || importedArray[staticIndex]')
|
|
81
148
|
}
|
|
149
|
+
if (fallback && declaration.parent.declarations.length !== 1) throw sourceNodeError(declaration, sourceFile, "React Router composed search parameter reads require their own top-level const statement")
|
|
82
150
|
searchReads.set(call, call.arguments[0])
|
|
151
|
+
if (importedValue && importedFallback) searchFallbackValues.set(importedValue, importedString.text)
|
|
152
|
+
if (fallback) {
|
|
153
|
+
const name = composedSearchName()
|
|
154
|
+
composedSearchDeclarations.set(declaration, { call, name })
|
|
155
|
+
composedSearchCalls.set(call, name)
|
|
156
|
+
}
|
|
83
157
|
return
|
|
84
158
|
}
|
|
85
159
|
ts.forEachChild(node, collectReads)
|
|
@@ -185,6 +259,17 @@ export function createRouterPass({ withBase }) {
|
|
|
185
259
|
}
|
|
186
260
|
const importedLink = tag => ts.isIdentifier(tag) && links.has(tag.text) && !isShadowedIdentifier(tag, sourceFile)
|
|
187
261
|
const visitor = node => {
|
|
262
|
+
if (searchFallbackValues.has(node)) return factory.createStringLiteral(searchFallbackValues.get(node))
|
|
263
|
+
if (ts.isVariableStatement(node) && node.declarationList.declarations.length === 1) {
|
|
264
|
+
const declaration = node.declarationList.declarations[0]
|
|
265
|
+
const composed = composedSearchDeclarations.get(declaration)
|
|
266
|
+
if (composed) {
|
|
267
|
+
const raw = factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
|
|
268
|
+
factory.createVariableDeclaration(composed.name, undefined, undefined, factory.createCallExpression(factory.createIdentifier(searchHelper), undefined, [searchReads.get(composed.call)]))
|
|
269
|
+
], ts.NodeFlags.Const))
|
|
270
|
+
return [raw, ts.visitEachChild(node, visitor, context)]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
188
273
|
if (ts.isVariableStatement(node) && node.declarationList.declarations.some(declaration => searchDeclarations.has(declaration) || navigateDeclarations.has(declaration))) {
|
|
189
274
|
const declarations = node.declarationList.declarations.flatMap(declaration => {
|
|
190
275
|
if (navigateDeclarations.has(declaration)) return []
|
|
@@ -196,7 +281,7 @@ export function createRouterPass({ withBase }) {
|
|
|
196
281
|
if (!declarations.length) return undefined
|
|
197
282
|
return factory.updateVariableStatement(node, node.modifiers, factory.updateVariableDeclarationList(node.declarationList, declarations))
|
|
198
283
|
}
|
|
199
|
-
if (ts.isCallExpression(node) && searchReads.has(node)) return factory.createCallExpression(factory.createIdentifier(searchHelper), undefined, [searchReads.get(node)])
|
|
284
|
+
if (ts.isCallExpression(node) && searchReads.has(node)) return composedSearchCalls.has(node) ? factory.createIdentifier(composedSearchCalls.get(node)) : factory.createCallExpression(factory.createIdentifier(searchHelper), undefined, [searchReads.get(node)])
|
|
200
285
|
if (ts.isCallExpression(node) && searchWrites.has(node)) {
|
|
201
286
|
const { updater, replace } = searchWrites.get(node)
|
|
202
287
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("globalThis"), "__kSetSearchParams"), undefined, [ts.visitNode(updater, visitor), replace ? factory.createTrue() : factory.createFalse()])
|
|
@@ -205,6 +290,7 @@ export function createRouterPass({ withBase }) {
|
|
|
205
290
|
const { method, destination } = navigateCalls.get(node)
|
|
206
291
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createPropertyAccessExpression(factory.createIdentifier("globalThis"), "location"), method), undefined, [factory.createStringLiteral(destination)])
|
|
207
292
|
}
|
|
293
|
+
if (ts.isCallExpression(node) && matchCalls.has(node)) return factory.createCallExpression(factory.createIdentifier(matchHelper), undefined, [factory.createStringLiteral(matchCalls.get(node))])
|
|
208
294
|
if (ts.isJsxElement(node) && importedLink(node.openingElement.tagName)) {
|
|
209
295
|
const opening = factory.updateJsxOpeningElement(node.openingElement, factory.createIdentifier("a"), node.openingElement.typeArguments, attributes(node.openingElement.attributes))
|
|
210
296
|
const closing = factory.updateJsxClosingElement(node.closingElement, factory.createIdentifier("a"))
|
|
@@ -217,6 +303,7 @@ export function createRouterPass({ withBase }) {
|
|
|
217
303
|
}
|
|
218
304
|
if (ts.isIdentifier(node) && links.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router Link imports may only be used as direct JSX elements")
|
|
219
305
|
if (ts.isIdentifier(node) && params.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router useParams imports may only be called directly")
|
|
306
|
+
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")
|
|
220
307
|
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")
|
|
221
308
|
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")
|
|
222
309
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react-router-dom") {
|
|
@@ -224,16 +311,17 @@ export function createRouterPass({ withBase }) {
|
|
|
224
311
|
if (!clause || clause.isTypeOnly) return node
|
|
225
312
|
const bindings = clause.namedBindings
|
|
226
313
|
if (!bindings || !ts.isNamedImports(bindings)) return node
|
|
227
|
-
const elements = bindings.elements.filter(entry => entry.isTypeOnly || !["Link", "useParams", "useSearchParams", "useNavigate"].includes((entry.propertyName ?? entry.name).text))
|
|
314
|
+
const elements = bindings.elements.filter(entry => entry.isTypeOnly || !["Link", "useParams", "useMatch", "useSearchParams", "useNavigate"].includes((entry.propertyName ?? entry.name).text))
|
|
228
315
|
if (!elements.length) return undefined
|
|
229
316
|
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, undefined, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
|
|
230
317
|
}
|
|
231
318
|
return ts.visitEachChild(node, visitor, context)
|
|
232
319
|
}
|
|
233
320
|
const normalized = ts.visitNode(sourceFile, visitor)
|
|
234
|
-
if (!params.size && !searchHooks.size) return normalized
|
|
321
|
+
if (!params.size && !matchHooks.size && !searchHooks.size) return normalized
|
|
235
322
|
const imports = [
|
|
236
323
|
...[...params].map(name => factory.createImportSpecifier(false, name === "useParams" ? undefined : factory.createIdentifier("useParams"), factory.createIdentifier(name))),
|
|
324
|
+
...(matchHooks.size ? [factory.createImportSpecifier(false, matchHelper === "__kUseRouteMatch" ? undefined : factory.createIdentifier("__kUseRouteMatch"), factory.createIdentifier(matchHelper))] : []),
|
|
237
325
|
...(searchReads.size ? [factory.createImportSpecifier(false, factory.createIdentifier("useSearchParam"), factory.createIdentifier(searchHelper))] : []),
|
|
238
326
|
...(searchObjects.some(entry => entry.setter) ? [factory.createImportSpecifier(false, factory.createIdentifier("useSearchParamsWriter"), factory.createIdentifier(searchWriterHelper))] : [])
|
|
239
327
|
]
|
|
@@ -315,12 +315,12 @@ function normalizeParameterizedDebounceHooks(sourceFile, factory, context) {
|
|
|
315
315
|
return ts.visitNode(sourceFile, visitor)
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
|
|
318
|
+
function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedFallbackCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
|
|
319
319
|
const factory = context.factory
|
|
320
320
|
let customHookTimerStates = new Set()
|
|
321
321
|
sourceFile = applyNormalizationPasses(sourceFile, [
|
|
322
322
|
...(importedStaticCollections ? [source => normalizeImportedStaticCollections(source, importedStaticCollections, factory, context)] : []),
|
|
323
|
-
source => normalizeReactRouterSyntax(source, factory, context, base),
|
|
323
|
+
source => normalizeReactRouterSyntax(source, factory, context, base, importedFallbackCollections),
|
|
324
324
|
source => normalizeClsxSyntax(source, factory, context),
|
|
325
325
|
source => normalizeMediaQueryExternalStores(source, factory, context),
|
|
326
326
|
source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
|
|
@@ -354,7 +354,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
354
354
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
355
355
|
const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex, true)
|
|
356
356
|
const importedCollections = new Set(importedStaticCollections.keys())
|
|
357
|
-
const
|
|
357
|
+
const importedFallbackCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
|
|
358
|
+
const normalized = normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedFallbackCollections, importedStaticCollections, sourceFiles, sourceIndex })
|
|
358
359
|
sourceFile = normalized.sourceFile
|
|
359
360
|
const { customHookTimerStates } = normalized
|
|
360
361
|
const bindingIndex = createBindingIndex(sourceFile)
|
|
@@ -398,9 +399,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
398
399
|
})
|
|
399
400
|
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
400
401
|
const packageBindings = packageImportBindings(sourceFile)
|
|
401
|
-
for (const [name] of packageBindings) {
|
|
402
|
+
for (const [name, binding] of packageBindings) {
|
|
402
403
|
const references = referenceIdentifiers(sourceFile, name)
|
|
403
404
|
const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile) && !insideOwnedEffectCallback(reference, sourceFile))
|
|
405
|
+
if (invalid && binding.target === "react-i18next" && 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")
|
|
404
406
|
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`)
|
|
405
407
|
}
|
|
406
408
|
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"))
|
|
@@ -661,6 +663,15 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
661
663
|
const collect = node => {
|
|
662
664
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
663
665
|
const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
|
|
666
|
+
if (ts.isIdentifier(node.name) && /^__kRouterSearchParam\d*$/.test(node.name.text) && /^__kUseSearchParam_*$/.test(callName)) {
|
|
667
|
+
const owner = nearestFunction(node)
|
|
668
|
+
if (!owner || !isLocalConst(node) || node.parent?.parent?.parent !== owner.body) throw sourceNodeError(node, sourceFile, "Compiler-owned search parameter signals must be top-level component const declarations")
|
|
669
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
670
|
+
const setter = `__kSet${node.name.text}`
|
|
671
|
+
setters.set(setter, node.name.text)
|
|
672
|
+
registerState(owner, node.name.text, setter, "search-param", node)
|
|
673
|
+
settersByFunction.set(owner, setters)
|
|
674
|
+
}
|
|
664
675
|
const customHookImport = callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !resolvedSharedState(importBindings.get(callName))
|
|
665
676
|
const debounce = customHookImport && ts.isIdentifier(node.name) ? resolveParameterizedDebounceHook(importBindings.get(callName)) : undefined
|
|
666
677
|
if (debounce) {
|
|
@@ -1110,6 +1121,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1110
1121
|
const declarations = jsxLocalDeclarations.get(owner)
|
|
1111
1122
|
if (!declarations) return expression
|
|
1112
1123
|
const substitutions = new Map()
|
|
1124
|
+
const stateNames = new Set(setters.values())
|
|
1113
1125
|
const resolving = []
|
|
1114
1126
|
const resolve = (name, reference) => {
|
|
1115
1127
|
if (substitutions.has(name)) return
|
|
@@ -1122,7 +1134,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1122
1134
|
resolving.push(name)
|
|
1123
1135
|
const initializer = entries[0].initializer
|
|
1124
1136
|
const visit = node => {
|
|
1125
|
-
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text)) resolve(node.text, node)
|
|
1137
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text) && !stateNames.has(node.text)) resolve(node.text, node)
|
|
1126
1138
|
ts.forEachChild(node, visit)
|
|
1127
1139
|
}
|
|
1128
1140
|
visit(initializer)
|
|
@@ -1130,7 +1142,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1130
1142
|
resolving.pop()
|
|
1131
1143
|
}
|
|
1132
1144
|
const visit = node => {
|
|
1133
|
-
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text)) resolve(node.text, node)
|
|
1145
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text) && !stateNames.has(node.text)) resolve(node.text, node)
|
|
1134
1146
|
ts.forEachChild(node, visit)
|
|
1135
1147
|
}
|
|
1136
1148
|
visit(expression)
|
|
@@ -1632,7 +1644,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1632
1644
|
const entries = jsxLocalDeclarations.get(nearestFunction(node))?.get(value.expression.text)
|
|
1633
1645
|
if (!entries?.length) return undefined
|
|
1634
1646
|
const initializer = entries.length === 1 ? unwrapExpression(entries[0].initializer) : undefined
|
|
1635
|
-
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
|
|
1647
|
+
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text) || resolvedSharedState(importBindings.get(initializer.expression.text))) return undefined
|
|
1636
1648
|
if (entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value.expression, `Calculated collection result "${value.expression.text}" must be one top-level immutable local`)
|
|
1637
1649
|
validateImportedCalculation(initializer, value.name.text)
|
|
1638
1650
|
const expanded = resolveReactiveJsxExpression(value, nearestFunction(node), setters)
|
|
@@ -1974,11 +1986,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1974
1986
|
if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
|
|
1975
1987
|
if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
|
|
1976
1988
|
const setters = settersForNode(node, settersByFunction)
|
|
1989
|
+
const effectStateNames = new Set(setters.values())
|
|
1977
1990
|
const resolveCalculation = dependency => {
|
|
1978
1991
|
if (specializedEffect) return undefined
|
|
1979
1992
|
const value = unwrapExpression(dependency)
|
|
1980
1993
|
const result = ts.isIdentifier(value) ? value : ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value) ? unwrapExpression(value.expression) : undefined
|
|
1981
1994
|
if (!result || !ts.isIdentifier(result)) return undefined
|
|
1995
|
+
if (effectStateNames.has(result.text)) return undefined
|
|
1982
1996
|
const entries = jsxLocalDeclarations.get(effectOwner)?.get(result.text)
|
|
1983
1997
|
const initializer = entries?.length === 1 && entries[0].node.parent?.parent?.parent === effectOwner?.body ? unwrapExpression(entries[0].initializer) : undefined
|
|
1984
1998
|
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
|
package/framework/core.mjs
CHANGED
|
@@ -123,6 +123,19 @@ export function useParams() {
|
|
|
123
123
|
return renderContext.params
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export function __kUseRouteMatch(pattern) {
|
|
127
|
+
if (!renderContext) throw new Error("React Router useMatch can only run while rendering a Kudzu component")
|
|
128
|
+
if (renderContext.renderScope === "layout") throw new Error("React Router useMatch is only supported in route scope")
|
|
129
|
+
if (renderContext.runtimeParamNames?.length) throw new Error("React Router useMatch requires a build-known route and cannot run on a runtimeParams bracket page")
|
|
130
|
+
const pathname = renderContext.applicationRoute
|
|
131
|
+
let decodedPathname = pathname
|
|
132
|
+
try {
|
|
133
|
+
decodedPathname = pathname.split("/").map(segment => decodeURIComponent(segment).replaceAll("/", "%2F")).join("/")
|
|
134
|
+
} catch {}
|
|
135
|
+
const matches = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i").test(decodedPathname)
|
|
136
|
+
return matches ? { params: {}, pathname: decodedPathname, pathnameBase: decodedPathname, pattern: { path: pattern, caseSensitive: false, end: true } } : null
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
export function useSearchParam(name) {
|
|
127
140
|
if (!renderContext) throw new Error("useSearchParam() can only run while rendering a Kudzu component")
|
|
128
141
|
if (renderContext?.renderScope === "layout") throw new Error("useSearchParam() is only supported in route scope")
|
|
@@ -471,7 +484,7 @@ function serializeCapture(name, value, seen) {
|
|
|
471
484
|
}
|
|
472
485
|
|
|
473
486
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
474
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], captureCache: new WeakMap(), sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
487
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", applicationRoute: metadata.applicationRoute, counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], captureCache: new WeakMap(), sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
475
488
|
|
|
476
489
|
try {
|
|
477
490
|
const page = { [routeScopeMarker]: true, component, props }
|