/** * Worker-side bridge for isolated server runtimes (Nitro v3 worker, Cloudflare workerd). * * Injected into the `@tanstack/devtools-event-client` module ONLY in non-client * (server) environments during dev. At module-eval time it gives the isolated * runtime a real `globalThis.__TANSTACK_EVENT_TARGET__` (so the unchanged * `EventClient` uses it instead of a throwaway target) and bridges that target to * the Vite dev process over the framework plugin's existing HMR HotChannel. * * Guards: * - `import.meta.hot` falsy (production / no HMR) -> tree-shaken / no-op. * - global target already set (in-process RunnableDevEnvironment, where * ServerEventBus lives) -> no-op, so existing behavior is unchanged. * * The bridge replicates ServerEventBus's in-process responsibilities so the * EventClient protocol is identical across the wire (see design doc). */ export function generateRuntimeBridgeCode(): string { return ` ;(function __tsdRuntimeBridge() { if (typeof import.meta === 'undefined' || !import.meta.hot) return; if (!globalThis.__TANSTACK_EVENT_TARGET__) { var target = new EventTarget(); globalThis.__TANSTACK_EVENT_TARGET__ = target; // Complete EventClient's connect handshake locally so queued events flush. target.addEventListener('tanstack-connect', function () { target.dispatchEvent(new CustomEvent('tanstack-connect-success')); }); // Worker -> Vite dev server. target.addEventListener('tanstack-dispatch-event', function (e) { import.meta.hot.send('tsd:to-server', e.detail); }); // Vite dev server -> worker listeners. import.meta.hot.on('tsd:to-client', function (event) { target.dispatchEvent(new CustomEvent(event.type, { detail: event })); target.dispatchEvent(new CustomEvent('tanstack-devtools-global', { detail: event })); }); } })(); ` } function isEventClientModule(id: string, code: string): boolean { const isEventClientPath = id.includes('devtools-event-client') || id.includes('event-bus-client') // Only the module that actually defines the class — avoids re-export shims // and unrelated files inside the package. return isEventClientPath && code.includes('EventClient') } export function injectRuntimeBridge( code: string, id: string, environmentName: string | undefined, ): string | undefined { // Only isolated server environments need the bridge. The client environment // has `window`; the in-process RunnableDevEnvironment is handled by the // runtime global guard inside the injected code. if (!environmentName || environmentName === 'client') return undefined if (!isEventClientModule(id, code)) return undefined return `${code}\n${generateRuntimeBridgeCode()}` } interface BridgeHotChannel { on?: (event: string, cb: (data: any) => void) => void off?: (event: string, cb: (data: any) => void) => void send?: (event: string, data: any) => void } interface BridgeServerLike { environments: Record } export function wireRuntimeBridgeChannels( server: BridgeServerLike, getTarget: () => EventTarget | null | undefined, ): () => void { const teardowns: Array<() => void> = [] for (const [name, env] of Object.entries(server.environments)) { if (name === 'client') continue const hot = env?.hot if ( !hot || typeof hot.on !== 'function' || typeof hot.send !== 'function' ) { continue } // Worker -> ServerEventBus (broadcasts to browser + in-process listeners). const onToServer = (event: any) => { getTarget()?.dispatchEvent( new CustomEvent('tanstack-dispatch-event', { detail: event }), ) } hot.on('tsd:to-server', onToServer) teardowns.push(() => hot.off?.('tsd:to-server', onToServer)) // ServerEventBus output -> worker listeners. const target = getTarget() const forward = (e: Event) => hot.send!('tsd:to-client', (e as CustomEvent).detail) target?.addEventListener('tanstack-devtools-global', forward) teardowns.push(() => target?.removeEventListener('tanstack-devtools-global', forward), ) } return () => teardowns.forEach((off) => off()) }