# TypeScript Migration Plan > **Scope**: `src/trx-client/trx-frontend/trx-frontend-http/` > > **Status**: Complete (2026-08-01) ## Implementation result The migration was completed on `feat/typescript-frontend-migration`. The baseline and phased sections below are retained as the decision record; their descriptions of JavaScript files and classic loading refer to the pre-migration state. Completion evidence: - all first-party browser sources are strict `.ts` files, checked by separate DOM and Web Worker TypeScript projects with no JavaScript compatibility mode or suppression directives; - `bootstrap.ts` is the single first-party HTML entry and esbuild represents startup order, lazy feature imports, shared hashed chunks, and the worker in its module graph; - obsolete source and generated compatibility JavaScript was removed; - Rust generates rig, status, capability, decoder, and flattened frontend metadata contracts into `api/generated.ts`; runtime guards validate HTTP, SSE, WebSocket, and worker boundaries; - the generic embedded-asset handler serves a build-generated allowlist with constrained MIME types, compression, ETags, immutable caching, and no file system lookup; - intentional browser host namespaces and transitional lazy-feature properties are documented in `docs/frontend-architecture.md`; - CI uses locked npm dependencies, caches npm downloads rather than `node_modules`, runs strict type checking and linting, unit/DOM/worker tests, Chromium startup coverage, generated-output drift checks, and REUSE after generation; - Cargo continues to consume committed generated assets without invoking Node or requiring network access. The final local gate ran `npm ci`, type checking, linting, 30 frontend tests, the Chromium smoke flow (startup, auth gate, audio controls, rig switching, map initialization, and navigation), generated-contract and bundle verification, workspace formatting, Clippy with warnings denied, all-target builds, workspace tests, and REUSE 3.3 validation. ## 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. Baseline 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.