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 ``. 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', () => { 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')