--- title: "Controller Tooling & HMR" description: "How RadiantController registration behaves by default, when to use explicit replacement, and where the current Ecopages JSX integration should move upstream." group: Tools order: 1 --- # Controller Tooling & HMR `RadiantController` registration is intentionally stable by default. That stability matters because controller identifiers are global and controller runtimes may already have live instances attached to DOM nodes by the time another module evaluates. ## Default Contract The default registry contract is: - `@controller(...)` uses first-registration-wins semantics - `registerController(...)` keeps the original class for an identifier - a later registration does not mutate already-connected instances That is the safe runtime contract for application code. It prevents accidental double imports, repeated side-effect evaluation, or duplicate bundle edges from silently swapping controller behavior underneath a live page. ## Explicit Mutation Path When tooling really does need mutation, the registry exposes separate opt-in APIs through `@ecopages/radiant/controller-registry`. Use these APIs deliberately: - `replaceController(identifier, ControllerClass)` replaces the registered class and reconnects matching live hosts - `enableControllerReplacementForHmr()` switches decorator-driven registration into replacement mode for the current runtime Example: ```ts import { enableControllerReplacementForHmr } from '@ecopages/radiant/controller-registry'; if (import.meta.hot) { enableControllerReplacementForHmr(); } ``` That helper is intentionally named for tooling. It is not meant as normal application setup. ## Current Docs-App Integration In this docs app, the HMR hook currently lives in the shared browser shell: - [apps/docs/src/layouts/base-layout/base-layout.script.ts](/Users/andeeplus/github/radiant/apps/docs/src/layouts/base-layout/base-layout.script.ts) That script runs on every docs page and enables controller replacement only when `import.meta.hot` exists. This is the current best integration point inside the repo because it is: - global for the docs browser runtime - cheap to reason about - isolated from production builds - independent from individual controller example modules ## Shared Runtime Integration The long-term integration point is the Ecopages JSX browser-runtime bootstrap, not each application shell. Today that bootstrap only installs the Radiant hydrator. In practice, that means the docs app still owns the HMR-specific opt-in in its shared browser shell. The current bootstrap output is: ```ts import { installRadiantHydrator } from '@ecopages/radiant/client/hydrator'; installRadiantHydrator(); ``` That makes it the natural future integration point for controller HMR too. ## Planned Plugin Port The clean upstream direction is: 1. Extend the Ecopages JSX runtime bootstrap so it can emit additional Radiant browser setup, not only hydrator installation. 2. Import `enableControllerReplacementForHmr()` in that same bootstrap source. 3. Guard it behind the browser HMR runtime so production output keeps the default stable registration contract. At a high level, the upstream bootstrap would become: ```ts import { installRadiantHydrator } from '@ecopages/radiant/client/hydrator'; import { enableControllerReplacementForHmr } from '@ecopages/radiant/controller-registry'; installRadiantHydrator(); if (import.meta.hot) { enableControllerReplacementForHmr(); } ``` ## Design Debt To Keep Visible There is one real piece of design debt here: - the global browser bootstrap currently belongs to the JSX plugin, while controller registration belongs to the Radiant runtime That split is reasonable, but it means controller HMR currently needs an app-level bridge until the plugin grows a better browser bootstrap extension point. So the current state is intentionally transitional: - app shell today - plugin bootstrap later That is better than making `@controller(...)` mutating by default. ## Recommendation Use this rule: - application runtime: keep default registration semantics - tooling and HMR: opt into explicit replacement - framework integration: move the opt-in to the highest shared browser bootstrap you control For this repo today, that highest shared bootstrap is the base layout script. For Ecopages JSX generally, the right eventual destination is the plugin runtime bundle service. ## See Also - [RadiantController](/docs/components/radiant-controller) - [@controller](/docs/decorators/controller)