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
}
diff --git a/packages/solid-router/src/Asset.tsx b/packages/solid-router/src/Asset.tsx
index 62e1b8468e9..d7bc725debd 100644
--- a/packages/solid-router/src/Asset.tsx
+++ b/packages/solid-router/src/Asset.tsx
@@ -1,5 +1,6 @@
import { Meta, Style, Title } from '@solidjs/meta'
import { onCleanup, onMount } from 'solid-js'
+import { useRouter } from './useRouter'
import type { RouterManagedTag } from '@tanstack/router-core'
import type { JSX } from 'solid-js'
@@ -36,8 +37,26 @@ function Script({
attrs?: ScriptAttrs
children?: string
}): JSX.Element | null {
+ const router = useRouter()
+
onMount(() => {
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)) {
@@ -56,7 +75,30 @@ function Script({
script.parentNode.removeChild(script)
}
})
- } else if (typeof children === 'string') {
+ }
+
+ 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
@@ -81,6 +123,10 @@ function Script({
}
})
+ if (router && !router.isServer) {
+ return null
+ }
+
if (attrs?.src && typeof attrs.src === 'string') {
return
}