From 860760ecfccbcb44f5abe1e1128957aeaf279042 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 11:48:20 +0200 Subject: [PATCH] docs: add TypeScript migration plan --- docs/ts-migration-plan.md | 549 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 docs/ts-migration-plan.md diff --git a/docs/ts-migration-plan.md b/docs/ts-migration-plan.md new file mode 100644 index 00000000..330a4e69 --- /dev/null +++ b/docs/ts-migration-plan.md @@ -0,0 +1,549 @@ + + +# TypeScript Migration Plan + +> **Scope**: `src/trx-client/trx-frontend/trx-frontend-http/` +> +> **Status**: Proposed + +## 1. Decision + +Migrating the web frontend to TypeScript is worthwhile, provided it is used to +remove implicit contracts and global coupling. Renaming JavaScript files to +`.ts` without changing their boundaries would add tooling without delivering +the main safety benefits. + +The migration must be incremental. Every intermediate commit and pull request +must leave the frontend buildable and usable. + +## 2. Current State + +The frontend currently contains roughly 21,700 lines of first-party +JavaScript. Its largest components include: + +- `app.js`: approximately 8,900 lines; +- `map-core.js`: approximately 3,500 lines; +- `plugins/scheduler.js`: approximately 1,500 lines; +- `plugins/bookmarks.js`: approximately 800 lines; +- `plugins/vchan.js`: approximately 560 lines. + +Important characteristics of the current architecture are: + +- scripts are embedded individually into the Rust binary with `include_str!`; +- the Rust HTTP server exposes an explicit route for each asset; +- plugins are loaded dynamically as classic scripts; +- script order is significant; +- modules communicate through `window.*`, `window.trx`, callbacks, and shared + mutable state; +- the main HTML file contains the plugin loader; +- a Web Worker is loaded through a fixed URL; +- Cargo builds do not require Node.js; +- CI images contain Node.js, but CI currently runs only Rust and REUSE checks; +- Leaflet, Opus decoder, fonts, images, and other vendored assets are local. + +These constraints make a big-bang rewrite unnecessarily risky. + +## 3. Goals + +The migration should: + +1. Create checked contracts between Rust responses and browser code. +2. Replace global callbacks with explicit module interfaces. +3. Split `app.js` by responsibility. +4. Make rig, capability, decoder, scheduler, audio, and spectrum state explicit. +5. Preserve lazy loading for expensive features. +6. Add frontend type checking, linting, and automated tests to CI. +7. Keep ordinary Cargo builds independent of Node.js and network access. +8. Preserve existing browser behavior throughout the migration. + +## 4. Non-Goals + +The migration will not initially: + +- introduce a UI framework; +- redesign the user interface; +- convert vendored JavaScript to TypeScript; +- change REST, SSE, WebSocket, or worker protocols unless required to make an + existing contract unambiguous; +- make `build.rs` install packages or download frontend dependencies; +- convert all of `app.js` in one pull request. + +## 5. Target Layout + +```text +trx-frontend-http/ +├── frontend/ +│ ├── package.json +│ ├── package-lock.json +│ ├── tsconfig.json +│ ├── tsconfig.worker.json +│ ├── build.mjs +│ ├── src/ +│ │ ├── bootstrap.ts +│ │ ├── api/ +│ │ │ ├── client.ts +│ │ │ └── generated.ts +│ │ ├── core/ +│ │ │ ├── dom.ts +│ │ │ ├── events.ts +│ │ │ ├── settings.ts +│ │ │ └── state.ts +│ │ ├── features/ +│ │ │ ├── audio/ +│ │ │ ├── bookmarks/ +│ │ │ ├── map/ +│ │ │ ├── navigation/ +│ │ │ ├── radio/ +│ │ │ ├── recorder/ +│ │ │ ├── scheduler/ +│ │ │ └── spectrum/ +│ │ ├── decoders/ +│ │ ├── workers/ +│ │ └── legacy/ +│ │ └── global-bridge.ts +│ └── tests/ +└── assets/web/ + ├── generated/ + ├── vendor/ + ├── index.html + ├── style.css + └── themes.css +``` + +The exact feature directories may evolve, but dependencies should point from +features toward `core` and `api`, never from `core` back into features. + +## 6. Tooling + +Use TypeScript for type checking and esbuild for bundling. A framework-specific +development server is not needed. + +Recommended compiler baseline: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "useUnknownInCatchVariables": true, + "noEmit": true, + "lib": ["ES2022", "DOM"] + } +} +``` + +Worker code should use a separate configuration with `WebWorker` rather than +`DOM` globals. + +The package scripts should provide at least: + +```text +npm run typecheck +npm run lint +npm test +npm run build +npm run verify-generated +``` + +Pin dependencies in `package-lock.json`. New package and generated files must +carry or inherit valid REUSE licensing information. + +## 7. Cargo and Asset Integration + +Cargo must remain usable on machines without Node.js. Do not invoke `npm` +automatically from `build.rs`. + +The initial integration should work as follows: + +1. TypeScript and JavaScript sources live under `frontend/src`. +2. esbuild writes browser-ready output under `assets/web/generated`. +3. Generated browser output is committed to the repository. +4. Rust embeds generated output, not TypeScript source. +5. CI rebuilds the frontend and fails if committed output is stale. + +During early phases, retain the existing public URLs, including: + +```text +/app.js +/ui-core.js +/map-core.js +/scheduler.js +/decode-history-worker.js +``` + +Keeping these URLs stable avoids coupling the first migration phases to changes +in HTML loading, authentication rules, caching, or Rust routing. + +After modules and code splitting are established, replace the explicit list of +Rust constants and handlers with an embedded generated-asset directory. The +server must still enforce an allowlist, correct MIME types, cache headers, and +path traversal protection. + +## 8. Rust-to-TypeScript Contracts + +Generate TypeScript definitions from Rust wire types instead of maintaining +parallel handwritten representations. + +Initial candidates include: + +- `RigState`, `RigInfo`, and `RigCapabilities`; +- the `/rigs` response; +- decoder registry entries; +- scheduler configuration and status; +- filter and spectrum state; +- recorder responses; +- SSE update payloads; +- decoded-message variants; +- WebSocket control and status messages. + +A generator such as `ts-rs` can produce +`frontend/src/api/generated.ts`. Generated definitions should describe only +wire formats. UI state and view models should remain handwritten. + +CI must regenerate these definitions and fail when the working tree changes. +Serde renames, tagged enums, optional fields, flattened values, and numeric +ranges must be checked explicitly during the initial generator integration. + +TypeScript types do not validate data at runtime. Critical compatibility +boundaries should retain focused runtime checks for missing or malformed data, +particularly when clients and servers may run different versions. + +## 9. Target Module Contracts + +Plugins should eventually implement an explicit interface rather than assigning +callbacks to `window`: + +```ts +interface TrxPlugin { + readonly id: string; + initialize(context: PluginContext): void | Promise; + dispose?(): void; +} + +interface PluginContext { + api: TrxApi; + events: TrxEventBus; + navigation: NavigationService; + notifications: NotificationService; + state: ReadonlyRadioState; +} +``` + +During the transition, `legacy/global-bridge.ts` may expose the minimum globals +required by unmigrated scripts. The bridge must shrink as migration progresses; +new feature code must not add new global callbacks. + +State should be divided by responsibility rather than replaced by one large +typed global: + +```ts +interface RadioState { + activeRigId: string | null; + capabilities: RigCapabilities | null; + connection: ConnectionState; + frequencyHz: number | null; + mode: RigMode | null; + bandwidthHz: number | null; +} + +interface AudioState { + rx: AudioStreamState; + tx: AudioStreamState; + volume: VolumeState; +} + +interface DecoderState { + registry: DecoderDescriptor[]; + status: ReadonlyMap; +} +``` + +Commands should receive a rig ID explicitly. They must not infer their target +from mutable global selection state after an asynchronous operation begins. + +## 10. Migration Phases + +### Phase 0: Baseline and Measurements + +- Record current asset names, sizes, and load order. +- Add a browser startup smoke test. +- Record existing global symbols and plugin callbacks. +- Confirm that generated bundles do not introduce remote runtime dependencies. + +**Exit criterion:** current behavior and asset loading have an automated +baseline. + +### Phase 1: Tooling Scaffold + +- Add the frontend package, lockfile, TypeScript configuration, and esbuild. +- Allow existing JavaScript as build input without type checking it globally. +- Produce fixed-name outputs matching current URLs. +- Add frontend commands and CI checks. +- Document local frontend development commands. + +**Exit criterion:** existing JavaScript passes through the frontend build with +no runtime changes, and CI detects stale generated output. + +### Phase 2: Generated API Types + +- Add Rust-to-TypeScript type generation. +- Generate the first status, rig, capability, and decoder contracts. +- Introduce a typed fetch/post client and typed SSE decoding boundary. +- Keep narrow runtime guards at compatibility boundaries. + +**Exit criterion:** new API consumers cannot use untyped response objects. + +### Phase 3: Core Browser Services + +Convert or create: + +- DOM lookup helpers; +- notifications and confirmations; +- settings and per-rig preferences; +- navigation; +- the event bus; +- application state stores; +- plugin registry and loader. + +Convert `ui-core.js` as the first strict TypeScript entry. + +**Exit criterion:** shared UI behavior is TypeScript, tested, and does not add +new globals. + +### Phase 4: Independent Leaf Modules + +Convert lower-coupling code first: + +1. WebGL renderer; +2. decode-history worker; +3. screenshot support; +4. FT2 and FT4; +5. WSPR; +6. CW and other decoder views. + +Workers must be separate build entries. + +**Exit criterion:** each converted module has typed inputs, outputs, and tests. + +### Phase 5: Plugin Loading + +- Move the inline loader out of `index.html`. +- Replace classic-script injection with typed dynamic imports. +- Register plugins through `TrxPlugin`. +- Preserve lazy loading by tab and feature. +- Remove the corresponding global callbacks after each plugin migrates. + +**Exit criterion:** plugin dependencies and loading order are represented by the +module graph rather than implicit script order. + +### Phase 6: Split the Main Application + +Extract `app.js` by responsibility: + +1. authentication and API transport; +2. rig enumeration and switching; +3. radio commands and capabilities; +4. audio streaming; +5. spectrum state and rendering; +6. decode history; +7. recorder; +8. keyboard shortcuts; +9. application bootstrap. + +Do not split by arbitrary line ranges. Each extraction must establish an +explicit interface and remove the corresponding globals. + +**Exit criterion:** the bootstrap file composes services and features but does +not contain their implementations. + +### Phase 7: High-Coupling Features + +Convert the remaining large features after the core contracts are stable: + +- scheduler and satellite scheduler; +- bookmarks; +- virtual channels; +- map and map-backed statistics; +- remaining decoder plugins. + +**Exit criterion:** no first-party classic scripts or untyped plugin callbacks +remain. + +### Phase 8: Asset-Server Consolidation + +- Enable code splitting and hashed chunks. +- Embed the generated asset directory in Rust. +- Serve generated assets through a generic, allowlisted handler. +- Add appropriate immutable caching for hashed output. +- Retain stable handling for HTML and version metadata. +- Remove obsolete fixed-asset constants and handlers. + +**Exit criterion:** Rust no longer needs a source edit for every generated +frontend chunk. + +### Phase 9: Strictness and Cleanup + +- Remove `allowJs`. +- Remove the legacy global bridge. +- Enable all selected strict compiler and lint rules. +- Remove obsolete generated compatibility bundles. +- Update architecture and contributor documentation. + +**Exit criterion:** all first-party frontend source is strict TypeScript and the +browser runtime exposes only intentionally documented globals. + +## 11. Testing Strategy + +Frontend CI should run: + +```sh +npm ci +npm run typecheck +npm run lint +npm test +npm run build +git diff --exit-code -- assets/web/generated frontend/src/api/generated.ts +``` + +Testing should cover four layers: + +1. **Pure unit tests** for bandwidth calculations, formatting, state changes, + capability decisions, and message routing. +2. **DOM component tests** for dialogs, tabs, workspace selection, rig + switching, and collapsible control sections. +3. **Worker tests** for decode-history pruning, batching, and message formats. +4. **Browser smoke tests** for startup, authentication gating, plugin loading, + navigation, rig switching, audio controls, and map initialization. + +The existing dependency-free `ui-core` test can remain during the scaffold +phase. It should be moved to the standard TypeScript test runner once that +runner is established. + +## 12. CI Changes + +Add a dedicated frontend job rather than hiding frontend work inside the Cargo +jobs. The job should: + +- use the Node.js version provided by the runner image; +- run with `npm ci`, never a mutable install; +- cache the npm download cache, not `node_modules`; +- type-check, lint, test, and build; +- verify generated artifacts and Rust-generated types are current; +- run REUSE validation after generated files are produced. + +Rust lint and test jobs should continue to consume committed generated assets. + +## 13. Pull Request Strategy + +Use small, independently reversible pull requests. A recommended sequence is: + +1. tooling and unchanged JavaScript build; +2. generated Rust wire types and typed API client; +3. `ui-core` conversion; +4. worker and WebGL conversion; +5. decoder plugin conversions in small groups; +6. plugin registry and dynamic imports; +7. one `app.js` responsibility per PR; +8. scheduler, bookmarks, and map conversions; +9. generic embedded asset serving; +10. removal of JavaScript compatibility mode. + +Every PR should include: + +- runtime behavior preserved or intentionally documented; +- frontend type checking and tests; +- strict Rust formatting and Clippy; +- generated-artifact drift verification; +- no newly introduced undocumented `window` globals; +- no remote runtime asset dependency. + +## 14. Risks and Mitigations + +### Superficial Conversion + +**Risk:** globals receive declarations but remain coupled and mutable. + +**Mitigation:** require every converted module to expose explicit inputs and +outputs and remove at least the globals it replaces. + +### Cargo Becomes Dependent on Node + +**Risk:** builds fail on systems without Node or network access. + +**Mitigation:** commit deterministic generated output and keep npm outside +`build.rs`. + +### Script-Order Regressions + +**Risk:** switching to modules changes execution timing and scope. + +**Mitigation:** retain current public entries initially, then replace script +loading only after the plugin registry is in place. + +### Unreviewable `app.js` Rewrite + +**Risk:** a large conversion is difficult to review, test, or bisect. + +**Mitigation:** extract one responsibility at a time and keep bootstrap working +after every extraction. + +### Rust and Browser Contracts Drift + +**Risk:** TypeScript compiles against stale wire definitions. + +**Mitigation:** generate types from Rust and enforce a clean working tree after +generation in CI. + +### Generated Asset Noise + +**Risk:** committed bundles make reviews noisy. + +**Mitigation:** separate source and generated commits when useful, include +source maps, and require deterministic output. + +### Bundle or Startup Regression + +**Risk:** bundling loads too much code eagerly. + +**Mitigation:** record the current baseline, preserve feature-level lazy loads, +and track entry/chunk sizes in CI. + +## 15. Initial Proof of Concept + +The first implementation PR should be deliberately limited to: + +1. adding the frontend package and locked toolchain; +2. building existing JavaScript to fixed-name generated output; +3. adding frontend CI and drift checks; +4. generating initial rig/status TypeScript definitions; +5. introducing the typed API client; +6. converting `ui-core.js` to strict TypeScript; +7. preserving every existing asset URL and observable behavior. + +This proof of concept is the decision gate for the remainder of the migration. +If it materially improves contract safety without making Cargo development +unwieldy, continue with the phased plan. If it does not, the repository can +retain the tooling and the converted core without committing to a full rewrite. + +## 16. Completion Criteria + +The migration is complete when: + +- all first-party browser source is strict TypeScript; +- vendor files remain isolated and declared through narrow type adapters; +- Rust wire types generate their browser contracts; +- no feature depends on accidental script ordering; +- no undocumented mutable `window` callbacks remain; +- `app.js` has been replaced by a small typed bootstrap and feature modules; +- frontend type checking, linting, unit tests, component tests, browser smoke + tests, and generated-output checks run in CI; +- Cargo builds remain possible without Node.js or network access; +- production assets remain local, embedded, cacheable, and reproducible.