From f97c514e1058f491c35ae6a35cbcac5029e58ccf Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 9 Mar 2026 17:13:14 +0000 Subject: [PATCH 1/3] add option to set integrity when using useScript --- .../src/useScript/useScript.test.ts | 21 +++++++++++++++++++ .../usehooks-ts/src/useScript/useScript.ts | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/packages/usehooks-ts/src/useScript/useScript.test.ts b/packages/usehooks-ts/src/useScript/useScript.test.ts index 65076546..c7b90748 100644 --- a/packages/usehooks-ts/src/useScript/useScript.test.ts +++ b/packages/usehooks-ts/src/useScript/useScript.test.ts @@ -80,4 +80,25 @@ describe('useScript', () => { expect(document.querySelector(`script[id="${id}"]`)).not.toBeNull() expect(document.querySelector(`script[src="${src}"]`)?.id).toBe(id) }) + + it('should set `integrity` when given', () => { + const src = 'https://example.com/unique-script-integrity.js' + const integrity = 'integrity-hash' + + const { result } = renderHook(() => useScript(src, { integrity })) + + act(() => { + document + .querySelector(`script[src="${src}"]`) + ?.dispatchEvent(new Event('load')) + }) + + expect(result.current).toBe('ready') + + // Assert the attribute set by setAttribute (impl uses setAttribute for integrity) + const script = document.querySelector(`script[src="${src}"]`) + expect(script).not.toBeNull() + expect((script as HTMLScriptElement)?.integrity).toBe(integrity) + expect((script as HTMLScriptElement)?.crossOrigin).toBe('anonymous') + }) }) diff --git a/packages/usehooks-ts/src/useScript/useScript.ts b/packages/usehooks-ts/src/useScript/useScript.ts index 4772402c..eb90bbcb 100644 --- a/packages/usehooks-ts/src/useScript/useScript.ts +++ b/packages/usehooks-ts/src/useScript/useScript.ts @@ -11,6 +11,8 @@ type UseScriptOptions = { removeOnUnmount?: boolean /** Script's `id` (optional). */ id?: string + /** Script's `integrity` property (optional). */ + integrity?: string } // Cached script statuses @@ -92,6 +94,10 @@ export function useScript( if (options?.id) { scriptNode.id = options.id } + if (options?.integrity) { + scriptNode.crossOrigin = 'anonymous' + scriptNode.integrity = options.integrity + } scriptNode.setAttribute('data-status', 'loading') document.body.appendChild(scriptNode) From 97907d3fea6ebacac0ea969ced9dd8ab288cd904 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 10 Mar 2026 13:56:13 +0000 Subject: [PATCH 2/3] add option to set crossOrigin when using useScript --- .../src/useScript/useScript.test.ts | 67 ++++++++++++++++--- .../usehooks-ts/src/useScript/useScript.ts | 12 +++- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/packages/usehooks-ts/src/useScript/useScript.test.ts b/packages/usehooks-ts/src/useScript/useScript.test.ts index c7b90748..e2d25bb9 100644 --- a/packages/usehooks-ts/src/useScript/useScript.test.ts +++ b/packages/usehooks-ts/src/useScript/useScript.test.ts @@ -4,7 +4,7 @@ import { useScript } from './useScript' describe('useScript', () => { it('should handle script loading error', () => { - const src = 'https://example.com/myscript.js' + const src = 'https://example.com/test-error.js' const { result } = renderHook(() => useScript(src)) @@ -21,7 +21,7 @@ describe('useScript', () => { }) it('should remove script on unmount', () => { - const src = '/' + const src = 'https://example.com/test-remove-on-unmount.js' // First load the script const { result } = renderHook(() => @@ -63,7 +63,7 @@ describe('useScript', () => { }) it('should have a `id` attribute when given', () => { - const src = '/' + const src = 'https://example.com/test-id.js' const id = 'my-script' const { result } = renderHook(() => useScript(src, { id })) @@ -81,11 +81,11 @@ describe('useScript', () => { expect(document.querySelector(`script[src="${src}"]`)?.id).toBe(id) }) - it('should set `integrity` when given', () => { - const src = 'https://example.com/unique-script-integrity.js' - const integrity = 'integrity-hash' + it('should have a `crossOrigin` attribute when given', () => { + const src = 'https://example.com/test-crossorigin.js' + const crossOrigin = 'use-credentials' - const { result } = renderHook(() => useScript(src, { integrity })) + const { result } = renderHook(() => useScript(src, { crossOrigin })) act(() => { document @@ -95,10 +95,55 @@ describe('useScript', () => { expect(result.current).toBe('ready') - // Assert the attribute set by setAttribute (impl uses setAttribute for integrity) - const script = document.querySelector(`script[src="${src}"]`) + const script = document.querySelector( + `script[src="${src}"]`, + )! expect(script).not.toBeNull() - expect((script as HTMLScriptElement)?.integrity).toBe(integrity) - expect((script as HTMLScriptElement)?.crossOrigin).toBe('anonymous') + expect(script.crossOrigin).toBe(crossOrigin) + }) + + it.each([ + { + scenario: + "defaults crossOrigin to 'anonymous' when integrity is set and crossOrigin is not supplied", + integrity: 'integrity-hash-1', + crossOrigin: undefined, + expectedCrossOrigin: 'anonymous', + }, + { + scenario: + "uses supplied crossOrigin 'anonymous' when both integrity and crossOrigin are set", + integrity: 'integrity-hash-2', + crossOrigin: 'anonymous', + expectedCrossOrigin: 'anonymous', + }, + { + scenario: + "uses supplied crossOrigin 'use-credentials' when both integrity and crossOrigin are set", + integrity: 'integrity-hash-3', + crossOrigin: 'use-credentials', + expectedCrossOrigin: 'use-credentials', + }, + ])('$scenario', ({ integrity, crossOrigin, expectedCrossOrigin }) => { + const src = `https://example.com/file-${integrity}.js` + + const { result } = renderHook(() => + useScript(src, { integrity, crossOrigin }), + ) + + act(() => { + document + .querySelector(`script[src="${src}"]`) + ?.dispatchEvent(new Event('load')) + }) + + expect(result.current).toBe('ready') + + const script = document.querySelector( + `script[src="${src}"]`, + )! + expect(script).not.toBeNull() + expect(script.integrity).toBe(integrity) + expect(script.crossOrigin).toBe(expectedCrossOrigin) }) }) diff --git a/packages/usehooks-ts/src/useScript/useScript.ts b/packages/usehooks-ts/src/useScript/useScript.ts index eb90bbcb..3282670f 100644 --- a/packages/usehooks-ts/src/useScript/useScript.ts +++ b/packages/usehooks-ts/src/useScript/useScript.ts @@ -11,8 +11,14 @@ type UseScriptOptions = { removeOnUnmount?: boolean /** Script's `id` (optional). */ id?: string - /** Script's `integrity` property (optional). */ + /** + * Script's [`integrity` property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/integrity) (optional). + * + * _Note:_ As a side effect of this being set the `crossOrigin` property will be set to `'anonymous' unless explicitly configured differently. + */ integrity?: string + /** Script's [`crossOrigin` property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/crossOrigin) (optional). */ + crossOrigin?: string } // Cached script statuses @@ -95,9 +101,11 @@ export function useScript( scriptNode.id = options.id } if (options?.integrity) { - scriptNode.crossOrigin = 'anonymous' scriptNode.integrity = options.integrity } + if (options?.crossOrigin || options?.integrity) { + scriptNode.crossOrigin = options?.crossOrigin ?? 'anonymous' + } scriptNode.setAttribute('data-status', 'loading') document.body.appendChild(scriptNode) From 8192265780c6f956cc3d512a686b282e20c40929 Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 10 Mar 2026 14:02:55 +0000 Subject: [PATCH 3/3] appease linter --- packages/usehooks-ts/src/useScript/useScript.test.ts | 10 +++++----- packages/usehooks-ts/src/useScript/useScript.ts | 11 +++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/usehooks-ts/src/useScript/useScript.test.ts b/packages/usehooks-ts/src/useScript/useScript.test.ts index e2d25bb9..b142d8eb 100644 --- a/packages/usehooks-ts/src/useScript/useScript.test.ts +++ b/packages/usehooks-ts/src/useScript/useScript.test.ts @@ -97,9 +97,9 @@ describe('useScript', () => { const script = document.querySelector( `script[src="${src}"]`, - )! + ) expect(script).not.toBeNull() - expect(script.crossOrigin).toBe(crossOrigin) + expect(script?.crossOrigin).toBe(crossOrigin) }) it.each([ @@ -141,9 +141,9 @@ describe('useScript', () => { const script = document.querySelector( `script[src="${src}"]`, - )! + ) expect(script).not.toBeNull() - expect(script.integrity).toBe(integrity) - expect(script.crossOrigin).toBe(expectedCrossOrigin) + expect(script?.integrity).toBe(integrity) + expect(script?.crossOrigin).toBe(expectedCrossOrigin) }) }) diff --git a/packages/usehooks-ts/src/useScript/useScript.ts b/packages/usehooks-ts/src/useScript/useScript.ts index 3282670f..70e20f4f 100644 --- a/packages/usehooks-ts/src/useScript/useScript.ts +++ b/packages/usehooks-ts/src/useScript/useScript.ts @@ -103,7 +103,7 @@ export function useScript( if (options?.integrity) { scriptNode.integrity = options.integrity } - if (options?.crossOrigin || options?.integrity) { + if (!!options?.crossOrigin || !!options?.integrity) { scriptNode.crossOrigin = options?.crossOrigin ?? 'anonymous' } scriptNode.setAttribute('data-status', 'loading') @@ -150,7 +150,14 @@ export function useScript( cachedScriptStatuses.delete(src) } } - }, [src, options?.shouldPreventLoad, options?.removeOnUnmount, options?.id]) + }, [ + src, + options?.shouldPreventLoad, + options?.removeOnUnmount, + options?.id, + options?.integrity, + options?.crossOrigin, + ]) return status }