feat(csr): add stripped dev output diagnostics

This commit is contained in:
2026-08-09 11:25:48 +05:30
parent f09341fcfa
commit 504d065003
4 changed files with 74 additions and 3 deletions
+10 -2
View File
@@ -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`. */
+30
View File
@@ -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);
}
+33
View File
@@ -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<string, unknown>;
win.document.body.innerHTML =
`<div data-scope="" data-wrn-events="complete"><button data-on-click="output.complete({})">go</button></div>`;
(globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location;
(globalThis as Record<string, unknown>).NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter;
(globalThis as Record<string, unknown>).MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver;
(globalThis as Record<string, unknown>).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
+1 -1
View File
@@ -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());