From 85c2c44540cae99ae89f78d6e1fa3510f15a5e03 Mon Sep 17 00:00:00 2001 From: Kamil Bazanow Date: Tue, 19 May 2026 17:52:02 +0200 Subject: [PATCH 1/6] fix(query-devtools): set window.__nonce__ in setupStyleSheet --- packages/query-devtools/src/utils.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/query-devtools/src/utils.tsx b/packages/query-devtools/src/utils.tsx index 9a826cd21e3..d87c368c95e 100644 --- a/packages/query-devtools/src/utils.tsx +++ b/packages/query-devtools/src/utils.tsx @@ -306,6 +306,12 @@ export const deleteNestedDataByPath = ( // Adds a nonce to the style tag if needed export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => { if (!nonce) return + + // Goober reads window.__nonce__ every time it creates or accesses its style + // element (e.nonce = window.__nonce__). Without this, goober overwrites the + // nonce we set on the pre-created element with undefined, clearing it. + ;(window as any).__nonce__ = nonce + const styleExists = document.querySelector('#_goober') || target?.querySelector('#_goober') From 7ede9c799c10db17bdc391de09024bd57a76dc68 Mon Sep 17 00:00:00 2001 From: Kamil Bazanow Date: Tue, 19 May 2026 17:53:08 +0200 Subject: [PATCH 2/6] test(query-devtools): add tests for window.__nonce__ in setupStyleSheet --- packages/query-devtools/src/__tests__/utils.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 07dff9ece4d..e71829a79ce 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -982,6 +982,7 @@ describe('Utils tests', () => { describe('setupStyleSheet', () => { afterEach(() => { document.head.querySelector('#_goober')?.remove() + delete (window as any).__nonce__ }) it('should not insert any style tag when "nonce" is missing', () => { @@ -1042,6 +1043,18 @@ describe('Utils tests', () => { expect(styleTags).toHaveLength(1) expect(styleTags[0]?.getAttribute('nonce')).toBe('first-nonce') }) + + it('should set window.__nonce__ so goober preserves the nonce on its style element', () => { + setupStyleSheet('test-nonce') + + expect((window as any).__nonce__).toBe('test-nonce') + }) + + it('should not set window.__nonce__ when nonce is missing', () => { + setupStyleSheet() + + expect((window as any).__nonce__).toBeUndefined() + }) }) describe('sortFns', () => { From 793a5c78e8e2d207f81304c248c52b49bbeaa47a Mon Sep 17 00:00:00 2001 From: Kamil Bazanow Date: Tue, 19 May 2026 18:28:29 +0200 Subject: [PATCH 3/6] chore: add changeset --- .changeset/mighty-banks-mate.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/mighty-banks-mate.md diff --git a/.changeset/mighty-banks-mate.md b/.changeset/mighty-banks-mate.md new file mode 100644 index 00000000000..3d36984f5f0 --- /dev/null +++ b/.changeset/mighty-banks-mate.md @@ -0,0 +1,7 @@ +--- +'@tanstack/query-devtools': patch +--- + +`setupStyleSheet` now sets `window.__nonce__` when a `styleNonce` is provided. + +The devtools use [goober](https://goober.js.org/) for CSS-in-JS, which reads `window.__nonce__` every time it creates or accesses its style element. Without this, goober overwrote the nonce with `undefined`, causing CSP violations even when `styleNonce` was correctly passed to ``. From 57d270056d70f85945a963f478eb288d39314cad Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sat, 27 Jun 2026 22:06:26 +0200 Subject: [PATCH 4/6] fix: merge conflicts --- .../src/__tests__/utils.test.ts | 13 ++++++++++++ packages/query-devtools/src/utils.tsx | 21 +++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 5d6f33f8040..496594d63cd 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -1044,6 +1044,19 @@ describe('Utils tests', () => { expect(styleTags[0]?.getAttribute('nonce')).toBe('first-nonce') }) + it('should install the style tag into the "ShadowRoot" target even when "document.head" already has one', () => { + const host = document.createElement('div') + const shadow = host.attachShadow({ mode: 'open' }) + + setupStyleSheet('host-nonce') + setupStyleSheet('shadow-nonce', shadow) + + expect(shadow.querySelector('#_goober')).not.toBeNull() + expect(shadow.querySelector('#_goober')?.getAttribute('nonce')).toBe( + 'shadow-nonce', + ) + }) + it('should set window.__nonce__ so goober preserves the nonce on its style element', () => { setupStyleSheet('test-nonce') diff --git a/packages/query-devtools/src/utils.tsx b/packages/query-devtools/src/utils.tsx index 4d45910a04b..77247607af8 100644 --- a/packages/query-devtools/src/utils.tsx +++ b/packages/query-devtools/src/utils.tsx @@ -307,25 +307,18 @@ export const deleteNestedDataByPath = ( // Sets up the goober stylesheet // Adds a nonce to the style tag if needed export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => { - if (!nonce) return - - // Goober reads window.__nonce__ every time it creates or accesses its style - // element (el.nonce = window.__nonce__). Without this, goober overwrites the - // nonce we set on the pre-created element with undefined, clearing it. + if (!nonce) + return // Goober reads window.__nonce__ every time it creates or accesses its style + // element (el.nonce = window.__nonce__). Without this, goober overwrites the + // nonce we set on the pre-created element with undefined, clearing it. ;(window as any).__nonce__ = nonce - const styleExists = - document.querySelector('#_goober') || target?.querySelector('#_goober') - - if (styleExists) return + const root = target ?? document.head + if (root.querySelector('#_goober')) return const styleTag = document.createElement('style') const textNode = document.createTextNode('') styleTag.appendChild(textNode) styleTag.id = '_goober' styleTag.setAttribute('nonce', nonce) - if (target) { - target.appendChild(styleTag) - } else { - document.head.appendChild(styleTag) - } + root.appendChild(styleTag) } From d1484b8ec4f010a0a87e0fc4b23e6066181936d1 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sat, 27 Jun 2026 22:10:18 +0200 Subject: [PATCH 5/6] formatting --- packages/query-devtools/src/utils.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/query-devtools/src/utils.tsx b/packages/query-devtools/src/utils.tsx index 77247607af8..99020e951fc 100644 --- a/packages/query-devtools/src/utils.tsx +++ b/packages/query-devtools/src/utils.tsx @@ -307,10 +307,11 @@ export const deleteNestedDataByPath = ( // Sets up the goober stylesheet // Adds a nonce to the style tag if needed export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => { - if (!nonce) - return // Goober reads window.__nonce__ every time it creates or accesses its style - // element (el.nonce = window.__nonce__). Without this, goober overwrites the - // nonce we set on the pre-created element with undefined, clearing it. + if (!nonce) return + + // Goober reads window.__nonce__ every time it creates or accesses its style + // element (el.nonce = window.__nonce__). Without this, goober overwrites the + // nonce we set on the pre-created element with undefined, clearing it. ;(window as any).__nonce__ = nonce const root = target ?? document.head From 3b05fba15fe464f8857871c120df54127099ab56 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:12:05 +0000 Subject: [PATCH 6/6] ci: apply automated fixes --- packages/query-devtools/src/utils.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/query-devtools/src/utils.tsx b/packages/query-devtools/src/utils.tsx index 99020e951fc..77247607af8 100644 --- a/packages/query-devtools/src/utils.tsx +++ b/packages/query-devtools/src/utils.tsx @@ -307,11 +307,10 @@ export const deleteNestedDataByPath = ( // Sets up the goober stylesheet // Adds a nonce to the style tag if needed export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => { - if (!nonce) return - - // Goober reads window.__nonce__ every time it creates or accesses its style - // element (el.nonce = window.__nonce__). Without this, goober overwrites the - // nonce we set on the pre-created element with undefined, clearing it. + if (!nonce) + return // Goober reads window.__nonce__ every time it creates or accesses its style + // element (el.nonce = window.__nonce__). Without this, goober overwrites the + // nonce we set on the pre-created element with undefined, clearing it. ;(window as any).__nonce__ = nonce const root = target ?? document.head