From 504d06500308144d97287077b1234f0d177dcc44 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sun, 9 Aug 2026 11:25:48 +0530 Subject: [PATCH] feat(csr): add stripped dev output diagnostics --- packages/csr/src/index.ts | 12 ++++++++-- packages/csr/src/reactive-runtime.ts | 30 +++++++++++++++++++++++++ packages/csr/test/reactive.test.ts | 33 ++++++++++++++++++++++++++++ packages/dev-server/src/assets.ts | 2 +- 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/csr/src/index.ts b/packages/csr/src/index.ts index f6d7ad13..965aad54 100644 --- a/packages/csr/src/index.ts +++ b/packages/csr/src/index.ts @@ -18,8 +18,16 @@ export { REALTIME_RUNTIME } from "./realtime-runtime.ts"; export { ACTION_RUNTIME } from "./action-runtime.ts"; /** The reactive runtime served at `/__wrnexus/reactive.js` (plain browser JS). */ -export function getReactiveRuntime(): string { - return REACTIVE_RUNTIME; +export function getReactiveRuntime(development = false): string { + if (development) { + return REACTIVE_RUNTIME + .replace(/\/\*__WRNEXUS_DEV_START__\*\//g, "") + .replace(/\/\*__WRNEXUS_DEV_END__\*\//g, ""); + } + return REACTIVE_RUNTIME.replace( + /\/\*__WRNEXUS_DEV_START__\*\/[\s\S]*?\/\*__WRNEXUS_DEV_END__\*\//g, + "", + ); } /** The client-side navigation runtime served at `/__wrnexus/nav.js`. */ diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index fb301ad8..7380da29 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -191,6 +191,27 @@ export const REACTIVE_RUNTIME = String.raw` } } + /*__WRNEXUS_DEV_START__*/ + var developmentWarnings = Object.create(null); + function warnOnce(code, message, element, detail) { + var key = code + "\\n" + message; + if (developmentWarnings[key]) return; + developmentWarnings[key] = true; + var payload = { + code: code, + message: message, + hydrationId: element && element.getAttribute ? element.getAttribute("data-wrn-hydration") : null, + detail: detail || null, + }; + console.warn("[" + code + "] " + message, detail || ""); + try { + window.dispatchEvent(new CustomEvent("wrnexus:diagnostic", { detail: payload })); + } catch (_) { + // CustomEvent can be unavailable in minimal DOM test environments. + } + } + /*__WRNEXUS_DEV_END__*/ + function scheduleUpdateHook(element, callback) { pendingUpdateHooks.set(element, callback); if (updateHooksScheduled) return; @@ -3953,6 +3974,15 @@ export const REACTIVE_RUNTIME = String.raw` ? Promise.all(values) : values[values.length - 1]; } + /*__WRNEXUS_DEV_START__*/ + warnOnce( + "WRN-DEV-OUTPUT-UNHANDLED", + "Output '" + name + "' has no parent binding. Registered bindings: " + + (registry ? Object.keys(registry).join(", ") || "none" : "none") + ".", + root, + { output: name, registeredBindings: registry ? Object.keys(registry) : [] }, + ); + /*__WRNEXUS_DEV_END__*/ return dispatchComponentEvent(root, name, payload); } diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts index 336545d1..6e3126fa 100644 --- a/packages/csr/test/reactive.test.ts +++ b/packages/csr/test/reactive.test.ts @@ -1,6 +1,7 @@ import { test, expect, beforeEach } from "bun:test"; import { Window } from "happy-dom"; import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; +import { getReactiveRuntime } from "../src/index.ts"; import { mountHtml } from "@wrnexus/test"; // Fresh DOM per test, with the runtime's globals bound. @@ -663,6 +664,38 @@ test("a camelCase output reaches a parent binding despite attribute lowercasing" expect(win.document.querySelector("#out")?.textContent).toBe("yes"); }); +test("development runtime warns once for an output with no parent binding", () => { + const win = new Window() as unknown as Window & Record; + win.document.body.innerHTML = + `
`; + (globalThis as Record).window = win; + (globalThis as Record).document = win.document; + (globalThis as Record).location = win.location; + (globalThis as Record).NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter; + (globalThis as Record).MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver; + (globalThis as Record).CustomEvent = (win as unknown as { CustomEvent: unknown }).CustomEvent; + const warnings: unknown[][] = []; + const originalWarn = console.warn; + console.warn = (...args: unknown[]) => warnings.push(args); + try { + (0, eval)(getReactiveRuntime(true)); + (win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(win.document); + (win.document.querySelector("button") as unknown as HTMLElement).click(); + (win.document.querySelector("button") as unknown as HTMLElement).click(); + } finally { + console.warn = originalWarn; + } + expect(warnings).toHaveLength(1); + expect(String(warnings[0]?.[0])).toContain("WRN-DEV-OUTPUT-UNHANDLED"); +}); + +test("production runtime strips development diagnostics", () => { + const production = getReactiveRuntime(); + expect(production).not.toContain("WRN-DEV-"); + expect(production).not.toContain("warnOnce"); + expect(REACTIVE_RUNTIME).toContain("__WRNEXUS_DEV_START__"); +}); + // --- browser globals + regex literals in client expressions ---------------- // Client functions and inline handlers are interpreted by the runtime's own // eval-free expression engine (so a strict CSP needs no unsafe-eval). Anything diff --git a/packages/dev-server/src/assets.ts b/packages/dev-server/src/assets.ts index 115268fb..0b23a33a 100644 --- a/packages/dev-server/src/assets.ts +++ b/packages/dev-server/src/assets.ts @@ -93,7 +93,7 @@ export function createDevAssetServer( if (pathname.startsWith("/__wrnexus/client/")) { return serveWrnBrowserArtifact(pathname) ?? new Response("Not Found", { status: 404 }); } - if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime()); + if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime(true)); if (pathname === "/__wrnexus/nav.js") return jsResponse(getNavRuntime()); if (pathname === "/__wrnexus/realtime.js") return jsResponse(getRealtimeRuntime()); if (pathname === "/__wrnexus/actions.js") return jsResponse(getActionRuntime());