diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts index b6fc9282f67..1298d9b2113 100644 --- a/e2e/react-start/basic/src/routeTree.gen.ts +++ b/e2e/react-start/basic/src/routeTree.gen.ts @@ -23,6 +23,7 @@ import { Route as StreamRouteImport } from './routes/stream' import { Route as ScriptsRouteImport } from './routes/scripts' import { Route as PostsRouteImport } from './routes/posts' import { Route as LinksRouteImport } from './routes/links' +import { Route as InlineScriptsRouteImport } from './routes/inline-scripts' import { Route as DeferredRouteImport } from './routes/deferred' import { Route as LayoutRouteImport } from './routes/_layout' import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route' @@ -90,6 +91,11 @@ const LinksRoute = LinksRouteImport.update({ path: '/links', getParentRoute: () => rootRouteImport, } as any) +const InlineScriptsRoute = InlineScriptsRouteImport.update({ + id: '/inline-scripts', + path: '/inline-scripts', + getParentRoute: () => rootRouteImport, +} as any) const DeferredRoute = DeferredRouteImport.update({ id: '/deferred', path: '/deferred', @@ -264,6 +270,7 @@ export interface FileRoutesByFullPath { '/not-found': typeof NotFoundRouteRouteWithChildren '/search-params': typeof SearchParamsRouteRouteWithChildren '/deferred': typeof DeferredRoute + '/inline-scripts': typeof InlineScriptsRoute '/links': typeof LinksRoute '/posts': typeof PostsRouteWithChildren '/scripts': typeof ScriptsRoute @@ -298,6 +305,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/deferred': typeof DeferredRoute + '/inline-scripts': typeof InlineScriptsRoute '/links': typeof LinksRoute '/scripts': typeof ScriptsRoute '/stream': typeof StreamRoute @@ -332,6 +340,7 @@ export interface FileRoutesById { '/search-params': typeof SearchParamsRouteRouteWithChildren '/_layout': typeof LayoutRouteWithChildren '/deferred': typeof DeferredRoute + '/inline-scripts': typeof InlineScriptsRoute '/links': typeof LinksRoute '/posts': typeof PostsRouteWithChildren '/scripts': typeof ScriptsRoute @@ -372,6 +381,7 @@ export interface FileRouteTypes { | '/not-found' | '/search-params' | '/deferred' + | '/inline-scripts' | '/links' | '/posts' | '/scripts' @@ -406,6 +416,7 @@ export interface FileRouteTypes { to: | '/' | '/deferred' + | '/inline-scripts' | '/links' | '/scripts' | '/stream' @@ -439,6 +450,7 @@ export interface FileRouteTypes { | '/search-params' | '/_layout' | '/deferred' + | '/inline-scripts' | '/links' | '/posts' | '/scripts' @@ -479,6 +491,7 @@ export interface RootRouteChildren { SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren LayoutRoute: typeof LayoutRouteWithChildren DeferredRoute: typeof DeferredRoute + InlineScriptsRoute: typeof InlineScriptsRoute LinksRoute: typeof LinksRoute PostsRoute: typeof PostsRouteWithChildren ScriptsRoute: typeof ScriptsRoute @@ -552,6 +565,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DeferredRouteImport parentRoute: typeof rootRouteImport } + '/inline-scripts': { + id: '/inline-scripts' + path: '/inline-scripts' + fullPath: '/inline-scripts' + preLoaderRoute: typeof InlineScriptsRouteImport + parentRoute: typeof rootRouteImport + } '/links': { id: '/links' path: '/links' @@ -829,6 +849,13 @@ declare module '@tanstack/react-start/server' { preLoaderRoute: unknown parentRoute: typeof rootServerRouteImport } + '/inline-scripts': { + id: '/inline-scripts' + path: '/inline-scripts' + fullPath: '/inline-scripts' + preLoaderRoute: unknown + parentRoute: typeof rootServerRouteImport + } '/links': { id: '/links' path: '/links' @@ -1148,6 +1175,23 @@ declare module './routes/deferred' { unknown > } +declare module './routes/inline-scripts' { + const createFileRoute: CreateFileRoute< + '/inline-scripts', + FileRoutesByPath['/inline-scripts']['parentRoute'], + FileRoutesByPath['/inline-scripts']['id'], + FileRoutesByPath['/inline-scripts']['path'], + FileRoutesByPath['/inline-scripts']['fullPath'] + > + + const createServerFileRoute: CreateServerFileRoute< + ServerFileRoutesByPath['/inline-scripts']['parentRoute'], + ServerFileRoutesByPath['/inline-scripts']['id'], + ServerFileRoutesByPath['/inline-scripts']['path'], + ServerFileRoutesByPath['/inline-scripts']['fullPath'], + unknown + > +} declare module './routes/links' { const createFileRoute: CreateFileRoute< '/links', @@ -1858,6 +1902,7 @@ const rootRouteChildren: RootRouteChildren = { SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren, LayoutRoute: LayoutRouteWithChildren, DeferredRoute: DeferredRoute, + InlineScriptsRoute: InlineScriptsRoute, LinksRoute: LinksRoute, PostsRoute: PostsRouteWithChildren, ScriptsRoute: ScriptsRoute, diff --git a/e2e/react-start/basic/src/routes/__root.tsx b/e2e/react-start/basic/src/routes/__root.tsx index 3283cb8a1ae..9c3a7138b0e 100644 --- a/e2e/react-start/basic/src/routes/__root.tsx +++ b/e2e/react-start/basic/src/routes/__root.tsx @@ -141,6 +141,14 @@ function RootDocument({ children }: { children: React.ReactNode }) { > Scripts {' '} + + Inline Scripts + {' '} ({ + scripts: [ + { + children: + 'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");', + }, + { + children: + 'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");', + type: 'text/javascript', + }, + ], + }), + component: InlineScriptsComponent, +}) + +function InlineScriptsComponent() { + return ( +
+

Inline Scripts Test

+

+ This route tests inline script duplication prevention. Two inline + scripts should be loaded. +

+
+ ) +} diff --git a/e2e/react-start/basic/tests/navigation.spec.ts b/e2e/react-start/basic/tests/navigation.spec.ts index 5d21f2adbe1..62433a0134d 100644 --- a/e2e/react-start/basic/tests/navigation.spec.ts +++ b/e2e/react-start/basic/tests/navigation.spec.ts @@ -34,7 +34,7 @@ test('Navigating nested layouts', async ({ page }) => { test('client side navigating to a route with scripts', async ({ page }) => { await page.goto('/') - await page.getByRole('link', { name: 'Scripts' }).click() + await page.getByRole('link', { name: 'Scripts', exact: true }).click() await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() expect(await page.evaluate('window.SCRIPT_1')).toBe(true) expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined) diff --git a/e2e/react-start/basic/tests/script-duplication.spec.ts b/e2e/react-start/basic/tests/script-duplication.spec.ts new file mode 100644 index 00000000000..2ed98a3a73f --- /dev/null +++ b/e2e/react-start/basic/tests/script-duplication.spec.ts @@ -0,0 +1,143 @@ +import { expect, test } from '@playwright/test' + +test.describe('Script Duplication Prevention', () => { + test('should not create duplicate scripts on SSR route', async ({ page }) => { + await page.goto('/scripts') + + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const scriptCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + + expect(scriptCount).toBe(1) + + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + }) + + test('should not create duplicate scripts during client-side navigation', async ({ + page, + }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const firstNavCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + expect(firstNavCount).toBe(1) + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() + + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const secondNavCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + expect(secondNavCount).toBe(1) + + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + }) + + test('should not create duplicate scripts with multiple navigation cycles', async ({ + page, + }) => { + await page.goto('/') + + for (let i = 0; i < 3; i++) { + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() + } + + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const finalCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + expect(finalCount).toBe(1) + + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + }) + + test('should not create duplicate inline scripts', async ({ page }) => { + await page.goto('/inline-scripts') + + await expect( + page.getByTestId('inline-scripts-test-heading'), + ).toBeInViewport() + + const script1Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_1 = true'), + ).length + }) + + const script2Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_2 = "test"'), + ).length + }) + + expect(script1Count).toBe(1) + expect(script2Count).toBe(1) + + expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true) + expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test') + }) + + test('should not create duplicate inline scripts during client-side navigation', async ({ + page, + }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Inline Scripts' }).click() + await expect( + page.getByTestId('inline-scripts-test-heading'), + ).toBeInViewport() + + const firstNavScript1Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_1 = true'), + ).length + }) + expect(firstNavScript1Count).toBe(1) + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() + + await page.getByRole('link', { name: 'Inline Scripts' }).click() + await expect( + page.getByTestId('inline-scripts-test-heading'), + ).toBeInViewport() + + const secondNavScript1Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_1 = true'), + ).length + }) + expect(secondNavScript1Count).toBe(1) + + // Verify the scripts are still working + expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true) + expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test') + }) +}) diff --git a/e2e/solid-start/basic/src/routeTree.gen.ts b/e2e/solid-start/basic/src/routeTree.gen.ts index 307552541dc..cf6db920d4e 100644 --- a/e2e/solid-start/basic/src/routeTree.gen.ts +++ b/e2e/solid-start/basic/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as StreamRouteImport } from './routes/stream' import { Route as ScriptsRouteImport } from './routes/scripts' import { Route as PostsRouteImport } from './routes/posts' import { Route as LinksRouteImport } from './routes/links' +import { Route as InlineScriptsRouteImport } from './routes/inline-scripts' import { Route as DeferredRouteImport } from './routes/deferred' import { Route as LayoutRouteImport } from './routes/_layout' import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route' @@ -74,6 +75,11 @@ const LinksRoute = LinksRouteImport.update({ path: '/links', getParentRoute: () => rootRouteImport, } as any) +const InlineScriptsRoute = InlineScriptsRouteImport.update({ + id: '/inline-scripts', + path: '/inline-scripts', + getParentRoute: () => rootRouteImport, +} as any) const DeferredRoute = DeferredRouteImport.update({ id: '/deferred', path: '/deferred', @@ -234,6 +240,7 @@ export interface FileRoutesByFullPath { '/not-found': typeof NotFoundRouteRouteWithChildren '/search-params': typeof SearchParamsRouteRouteWithChildren '/deferred': typeof DeferredRoute + '/inline-scripts': typeof InlineScriptsRoute '/links': typeof LinksRoute '/posts': typeof PostsRouteWithChildren '/scripts': typeof ScriptsRoute @@ -265,6 +272,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/deferred': typeof DeferredRoute + '/inline-scripts': typeof InlineScriptsRoute '/links': typeof LinksRoute '/scripts': typeof ScriptsRoute '/stream': typeof StreamRoute @@ -297,6 +305,7 @@ export interface FileRoutesById { '/search-params': typeof SearchParamsRouteRouteWithChildren '/_layout': typeof LayoutRouteWithChildren '/deferred': typeof DeferredRoute + '/inline-scripts': typeof InlineScriptsRoute '/links': typeof LinksRoute '/posts': typeof PostsRouteWithChildren '/scripts': typeof ScriptsRoute @@ -333,6 +342,7 @@ export interface FileRouteTypes { | '/not-found' | '/search-params' | '/deferred' + | '/inline-scripts' | '/links' | '/posts' | '/scripts' @@ -364,6 +374,7 @@ export interface FileRouteTypes { to: | '/' | '/deferred' + | '/inline-scripts' | '/links' | '/scripts' | '/stream' @@ -395,6 +406,7 @@ export interface FileRouteTypes { | '/search-params' | '/_layout' | '/deferred' + | '/inline-scripts' | '/links' | '/posts' | '/scripts' @@ -431,6 +443,7 @@ export interface RootRouteChildren { SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren LayoutRoute: typeof LayoutRouteWithChildren DeferredRoute: typeof DeferredRoute + InlineScriptsRoute: typeof InlineScriptsRoute LinksRoute: typeof LinksRoute PostsRoute: typeof PostsRouteWithChildren ScriptsRoute: typeof ScriptsRoute @@ -502,6 +515,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof LinksRouteImport parentRoute: typeof rootRouteImport } + '/inline-scripts': { + id: '/inline-scripts' + path: '/inline-scripts' + fullPath: '/inline-scripts' + preLoaderRoute: typeof InlineScriptsRouteImport + parentRoute: typeof rootRouteImport + } '/deferred': { id: '/deferred' path: '/deferred' @@ -843,6 +863,7 @@ const rootRouteChildren: RootRouteChildren = { SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren, LayoutRoute: LayoutRouteWithChildren, DeferredRoute: DeferredRoute, + InlineScriptsRoute: InlineScriptsRoute, LinksRoute: LinksRoute, PostsRoute: PostsRouteWithChildren, ScriptsRoute: ScriptsRoute, diff --git a/e2e/solid-start/basic/src/routes/__root.tsx b/e2e/solid-start/basic/src/routes/__root.tsx index 6598d8b3164..fe2dc996f04 100644 --- a/e2e/solid-start/basic/src/routes/__root.tsx +++ b/e2e/solid-start/basic/src/routes/__root.tsx @@ -103,6 +103,14 @@ function RootComponent() { > Scripts {' '} + + Inline Scripts + {' '} ({ + scripts: [ + { + children: + 'window.INLINE_SCRIPT_1 = true; console.log("Inline script 1 executed");', + }, + { + children: + 'window.INLINE_SCRIPT_2 = "test"; console.log("Inline script 2 executed");', + type: 'text/javascript', + }, + ], + }), + component: InlineScriptsComponent, +}) + +function InlineScriptsComponent() { + return ( +
+

Inline Scripts Test

+

+ This route tests inline script duplication prevention. Two inline + scripts should be loaded. +

+
+ ) +} diff --git a/e2e/solid-start/basic/tests/navigation.spec.ts b/e2e/solid-start/basic/tests/navigation.spec.ts index 5d21f2adbe1..62433a0134d 100644 --- a/e2e/solid-start/basic/tests/navigation.spec.ts +++ b/e2e/solid-start/basic/tests/navigation.spec.ts @@ -34,7 +34,7 @@ test('Navigating nested layouts', async ({ page }) => { test('client side navigating to a route with scripts', async ({ page }) => { await page.goto('/') - await page.getByRole('link', { name: 'Scripts' }).click() + await page.getByRole('link', { name: 'Scripts', exact: true }).click() await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() expect(await page.evaluate('window.SCRIPT_1')).toBe(true) expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined) diff --git a/e2e/solid-start/basic/tests/script-duplication.spec.ts b/e2e/solid-start/basic/tests/script-duplication.spec.ts new file mode 100644 index 00000000000..b6c9e9e2fd4 --- /dev/null +++ b/e2e/solid-start/basic/tests/script-duplication.spec.ts @@ -0,0 +1,138 @@ +import { expect, test } from '@playwright/test' + +test.describe('Script Duplication Prevention', () => { + test('should not create duplicate scripts on SSR route', async ({ page }) => { + await page.goto('/scripts') + + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const scriptCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + + expect(scriptCount).toBe(1) + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + }) + + test('should not create duplicate scripts during client-side navigation', async ({ + page, + }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const firstNavCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + expect(firstNavCount).toBe(1) + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() + + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const secondNavCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + expect(secondNavCount).toBe(1) + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + }) + + test('should not create duplicate scripts with multiple navigation cycles', async ({ + page, + }) => { + await page.goto('/') + + for (let i = 0; i < 3; i++) { + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() + } + + await page.getByRole('link', { name: 'Scripts', exact: true }).click() + await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() + + const finalCount = await page.evaluate(() => { + return document.querySelectorAll('script[src="script.js"]').length + }) + expect(finalCount).toBe(1) + expect(await page.evaluate('window.SCRIPT_1')).toBe(true) + }) + + test('should not create duplicate inline scripts', async ({ page }) => { + await page.goto('/inline-scripts') + + await expect( + page.getByTestId('inline-scripts-test-heading'), + ).toBeInViewport() + + const script1Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_1 = true'), + ).length + }) + + const script2Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_2 = "test"'), + ).length + }) + + expect(script1Count).toBe(1) + expect(script2Count).toBe(1) + expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true) + expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test') + }) + + test('should not create duplicate inline scripts during client-side navigation', async ({ + page, + }) => { + await page.goto('/') + + await page.getByRole('link', { name: 'Inline Scripts' }).click() + await expect( + page.getByTestId('inline-scripts-test-heading'), + ).toBeInViewport() + + const firstNavScript1Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_1 = true'), + ).length + }) + expect(firstNavScript1Count).toBe(1) + + await page.getByRole('link', { name: 'Home' }).click() + await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() + + await page.getByRole('link', { name: 'Inline Scripts' }).click() + await expect( + page.getByTestId('inline-scripts-test-heading'), + ).toBeInViewport() + + const secondNavScript1Count = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script:not([src])')) + return scripts.filter( + (script) => + script.textContent && + script.textContent.includes('window.INLINE_SCRIPT_1 = true'), + ).length + }) + expect(secondNavScript1Count).toBe(1) + + expect(await page.evaluate('window.INLINE_SCRIPT_1')).toBe(true) + expect(await page.evaluate('window.INLINE_SCRIPT_2')).toBe('test') + }) +}) diff --git a/packages/react-router/src/Asset.tsx b/packages/react-router/src/Asset.tsx index 218663e24f2..266261d2e36 100644 --- a/packages/react-router/src/Asset.tsx +++ b/packages/react-router/src/Asset.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' interface ScriptAttrs { @@ -44,8 +45,26 @@ function Script({ attrs?: ScriptAttrs children?: string }) { + const router = useRouter() + React.useEffect(() => { if (attrs?.src) { + const normSrc = (() => { + try { + const base = document.baseURI || window.location.href + return new URL(attrs.src, base).href + } catch { + return attrs.src + } + })() + const existingScript = Array.from( + document.querySelectorAll('script[src]'), + ).find((el) => (el as HTMLScriptElement).src === normSrc) + + if (existingScript) { + return + } + const script = document.createElement('script') for (const [key, value] of Object.entries(attrs)) { @@ -71,6 +90,27 @@ function Script({ } if (typeof children === 'string') { + const typeAttr = + typeof attrs?.type === 'string' ? attrs.type : 'text/javascript' + const nonceAttr = + typeof attrs?.nonce === 'string' ? attrs.nonce : undefined + const existingScript = Array.from( + document.querySelectorAll('script:not([src])'), + ).find((el) => { + if (!(el instanceof HTMLScriptElement)) return false + const sType = el.getAttribute('type') ?? 'text/javascript' + const sNonce = el.getAttribute('nonce') ?? undefined + return ( + el.textContent === children && + sType === typeAttr && + sNonce === nonceAttr + ) + }) + + if (existingScript) { + return + } + const script = document.createElement('script') script.textContent = children @@ -101,6 +141,10 @@ function Script({ return undefined }, [attrs, children]) + if (!router.isServer) { + return null + } + if (attrs?.src && typeof attrs.src === 'string') { return