diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index a03622be..d92d4b12 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -221,6 +221,36 @@ export const REACTIVE_RUNTIME = String.raw` { binding: statement, functionName: match[1] }, ); } + + function warnMissingThemeTokens() { + var referenced = Object.create(null); + function collect(rules) { + Array.prototype.forEach.call(rules || [], function (rule) { + var cssText = rule.cssText || ""; + var match; + var pattern = /var\(\s*(--wire-[A-Za-z0-9_-]+)/g; + while ((match = pattern.exec(cssText))) referenced[match[1]] = true; + try { + if (rule.cssRules) collect(rule.cssRules); + } catch (_) { + // Cross-origin and disabled stylesheets can deny CSSOM access. + } + }); + } + Array.prototype.forEach.call(document.styleSheets || [], function (sheet) { + try { collect(sheet.cssRules); } catch (_) {} + }); + var rendered = window.getComputedStyle(document.documentElement); + Object.keys(referenced).forEach(function (token) { + if (rendered.getPropertyValue(token).trim()) return; + warnOnce( + "WRN-DEV-THEME-TOKEN-MISSING", + "Theme token '" + token + "' is referenced by rendered CSS but is not defined.", + document.documentElement, + { token: token }, + ); + }); + } /*__WRNEXUS_DEV_END__*/ function scheduleUpdateHook(element, callback) { @@ -3438,6 +3468,9 @@ export const REACTIVE_RUNTIME = String.raw` hydratePreferenceControllers(host); hydrateSelectControllers(host); hydratePinInputControllers(host); + /*__WRNEXUS_DEV_START__*/ + warnMissingThemeTokens(); + /*__WRNEXUS_DEV_END__*/ } var navbarOutsideClickBound = false; diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts index c00855d5..81a4ed0d 100644 --- a/packages/csr/test/reactive.test.ts +++ b/packages/csr/test/reactive.test.ts @@ -724,6 +724,32 @@ test("development runtime warns when a component binding names a missing functio expect(String(warnings[0]?.[0])).toContain("missingSave"); }); +test("development runtime warns for referenced theme tokens absent from rendered CSS", () => { + const win = new Window() as unknown as Window & Record; + win.document.head.innerHTML = + ``; + 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); + } finally { + console.warn = originalWarn; + } + expect(warnings).toHaveLength(1); + expect(String(warnings[0]?.[0])).toContain("WRN-DEV-THEME-TOKEN-MISSING"); + expect(String(warnings[0]?.[0])).toContain("--wire-missing-test"); +}); + test("production runtime strips development diagnostics", () => { const production = getReactiveRuntime(); expect(production).not.toContain("WRN-DEV-");