diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index 7380da29..a03622be 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -210,6 +210,17 @@ export const REACTIVE_RUNTIME = String.raw` // CustomEvent can be unavailable in minimal DOM test environments. } } + + function warnMissingBindingFunction(statement, resolve, element) { + var match = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(statement || ""); + if (!match || typeof resolve(match[1]) === "function") return; + warnOnce( + "WRN-DEV-BINDING-MISSING", + "Component binding calls '" + match[1] + "', but that function does not exist in the parent scope.", + element, + { binding: statement, functionName: match[1] }, + ); + } /*__WRNEXUS_DEV_END__*/ function scheduleUpdateHook(element, callback) { @@ -2514,6 +2525,9 @@ export const REACTIVE_RUNTIME = String.raw` locals.event = undefined; locals.$event = undefined; try { + /*__WRNEXUS_DEV_START__*/ + warnMissingBindingFunction(outStmt, function (name) { return peekScope(name); }, componentRoot); + /*__WRNEXUS_DEV_END__*/ return runStmt(outStmt, locals); } catch (error) { console.error( diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts index 6e3126fa..c00855d5 100644 --- a/packages/csr/test/reactive.test.ts +++ b/packages/csr/test/reactive.test.ts @@ -689,6 +689,41 @@ test("development runtime warns once for an output with no parent binding", () = expect(String(warnings[0]?.[0])).toContain("WRN-DEV-OUTPUT-UNHANDLED"); }); +test("development runtime warns when a component binding names a missing function", () => { + 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; + const originalError = console.error; + console.warn = (...args: unknown[]) => warnings.push(args); + console.error = () => {}; + try { + (0, eval)(getReactiveRuntime(true)); + (win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(win.document); + const target = win.document.querySelector("[data-wrn-events]") as unknown as { + __wrnexusOutputHandlers?: Record unknown>>; + }; + target.__wrnexusOutputHandlers?.save?.forEach((handler) => handler({ id: 1 })); + target.__wrnexusOutputHandlers?.save?.forEach((handler) => handler({ id: 2 })); + } finally { + console.warn = originalWarn; + console.error = originalError; + } + expect(warnings).toHaveLength(1); + expect(String(warnings[0]?.[0])).toContain("WRN-DEV-BINDING-MISSING"); + expect(String(warnings[0]?.[0])).toContain("missingSave"); +}); + test("production runtime strips development diagnostics", () => { const production = getReactiveRuntime(); expect(production).not.toContain("WRN-DEV-");