Skip to content

Commit adc5d5c

Browse files
authored
fix(react-router, solid-router): make head function scripts load properly (#4323)
1 parent 6f15922 commit adc5d5c

File tree

6 files changed

+190
-30
lines changed

6 files changed

+190
-30
lines changed

e2e/react-start/basic/tests/navigation.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ test('Navigating nested layouts', async ({ page }) => {
3232
await expect(page.locator('body')).toContainText("I'm layout B!")
3333
})
3434

35+
test('client side navigating to a route with scripts', async ({ page }) => {
36+
await page.goto('/')
37+
await page.getByRole('link', { name: 'Scripts' }).click()
38+
expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
39+
expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined)
40+
})
41+
3542
test('directly going to a route with scripts', async ({ page }) => {
3643
await page.goto('/scripts')
3744
expect(await page.evaluate('window.SCRIPT_1')).toBe(true)

e2e/solid-start/basic/src/routes/scripts.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createFileRoute } from '@tanstack/solid-router'
2+
23
const isProd = import.meta.env.PROD
34

45
export const Route = createFileRoute('/scripts')({

e2e/solid-start/basic/tests/navigation.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ test('Navigating nested layouts', async ({ page }) => {
3232
await expect(page.locator('body')).toContainText("I'm layout B!")
3333
})
3434

35+
test('client side navigating to a route with scripts', async ({ page }) => {
36+
await page.goto('/')
37+
await page.getByRole('link', { name: 'Scripts' }).click()
38+
expect(await page.evaluate('window.SCRIPT_1')).toBe(true)
39+
expect(await page.evaluate('window.SCRIPT_2')).toBe(undefined)
40+
})
41+
3542
test('directly going to a route with scripts', async ({ page }) => {
3643
await page.goto('/scripts')
3744
expect(await page.evaluate('window.SCRIPT_1')).toBe(true)

packages/react-router/src/Asset.tsx

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
import * as React from 'react'
12
import type { RouterManagedTag } from '@tanstack/router-core'
23

3-
export function Asset({ tag, attrs, children }: RouterManagedTag): any {
4+
interface ScriptAttrs {
5+
[key: string]: string | boolean | undefined
6+
src?: string
7+
suppressHydrationWarning?: boolean
8+
}
9+
10+
export function Asset({
11+
tag,
12+
attrs,
13+
children,
14+
}: RouterManagedTag): React.ReactElement | null {
415
switch (tag) {
516
case 'title':
617
return (
@@ -16,25 +27,93 @@ export function Asset({ tag, attrs, children }: RouterManagedTag): any {
1627
return (
1728
<style
1829
{...attrs}
19-
dangerouslySetInnerHTML={{ __html: children as any }}
30+
dangerouslySetInnerHTML={{ __html: children as string }}
2031
/>
2132
)
2233
case 'script':
23-
if ((attrs as any) && (attrs as any).src) {
24-
return <script {...attrs} suppressHydrationWarning />
25-
}
26-
if (typeof children === 'string')
27-
return (
28-
<script
29-
{...attrs}
30-
dangerouslySetInnerHTML={{
31-
__html: children,
32-
}}
33-
suppressHydrationWarning
34-
/>
35-
)
36-
return null
34+
return <Script attrs={attrs}>{children}</Script>
3735
default:
3836
return null
3937
}
4038
}
39+
40+
function Script({
41+
attrs,
42+
children,
43+
}: {
44+
attrs?: ScriptAttrs
45+
children?: string
46+
}) {
47+
React.useEffect(() => {
48+
if (attrs?.src) {
49+
const script = document.createElement('script')
50+
51+
for (const [key, value] of Object.entries(attrs)) {
52+
if (
53+
key !== 'suppressHydrationWarning' &&
54+
value !== undefined &&
55+
value !== false
56+
) {
57+
script.setAttribute(
58+
key,
59+
typeof value === 'boolean' ? '' : String(value),
60+
)
61+
}
62+
}
63+
64+
document.head.appendChild(script)
65+
66+
return () => {
67+
if (script.parentNode) {
68+
script.parentNode.removeChild(script)
69+
}
70+
}
71+
}
72+
73+
if (typeof children === 'string') {
74+
const script = document.createElement('script')
75+
script.textContent = children
76+
77+
if (attrs) {
78+
for (const [key, value] of Object.entries(attrs)) {
79+
if (
80+
key !== 'suppressHydrationWarning' &&
81+
value !== undefined &&
82+
value !== false
83+
) {
84+
script.setAttribute(
85+
key,
86+
typeof value === 'boolean' ? '' : String(value),
87+
)
88+
}
89+
}
90+
}
91+
92+
document.head.appendChild(script)
93+
94+
return () => {
95+
if (script.parentNode) {
96+
script.parentNode.removeChild(script)
97+
}
98+
}
99+
}
100+
101+
return undefined
102+
}, [attrs, children])
103+
104+
if (attrs?.src && typeof attrs.src === 'string') {
105+
return <script {...attrs} suppressHydrationWarning />
106+
}
107+
108+
if (typeof children === 'string') {
109+
return (
110+
<script
111+
{...attrs}
112+
dangerouslySetInnerHTML={{ __html: children }}
113+
suppressHydrationWarning
114+
/>
115+
)
116+
}
117+
118+
return null
119+
}

packages/solid-router/src/Asset.tsx

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Meta, Style, Title } from '@solidjs/meta'
2+
import { onCleanup, onMount } from 'solid-js'
23
import type { RouterManagedTag } from '@tanstack/router-core'
4+
import type { JSX } from 'solid-js'
35

4-
export function Asset({ tag, attrs, children }: RouterManagedTag): any {
6+
export function Asset({
7+
tag,
8+
attrs,
9+
children,
10+
}: RouterManagedTag): JSX.Element | null {
511
switch (tag) {
612
case 'title':
713
return <Title {...attrs}>{children}</Title>
@@ -12,13 +18,76 @@ export function Asset({ tag, attrs, children }: RouterManagedTag): any {
1218
case 'style':
1319
return <Style {...attrs} innerHTML={children} />
1420
case 'script':
15-
if ((attrs as any) && (attrs as any).src) {
16-
return <script {...attrs} />
17-
}
18-
if (typeof children === 'string')
19-
return <script {...attrs} innerHTML={children} />
20-
return null
21+
return <Script attrs={attrs}>{children}</Script>
2122
default:
2223
return null
2324
}
2425
}
26+
27+
interface ScriptAttrs {
28+
[key: string]: string | boolean | undefined
29+
src?: string
30+
}
31+
32+
function Script({
33+
attrs,
34+
children,
35+
}: {
36+
attrs?: ScriptAttrs
37+
children?: string
38+
}): JSX.Element | null {
39+
onMount(() => {
40+
if (attrs?.src) {
41+
const script = document.createElement('script')
42+
43+
for (const [key, value] of Object.entries(attrs)) {
44+
if (value !== undefined && value !== false) {
45+
script.setAttribute(
46+
key,
47+
typeof value === 'boolean' ? '' : String(value),
48+
)
49+
}
50+
}
51+
52+
document.head.appendChild(script)
53+
54+
onCleanup(() => {
55+
if (script.parentNode) {
56+
script.parentNode.removeChild(script)
57+
}
58+
})
59+
} else if (typeof children === 'string') {
60+
const script = document.createElement('script')
61+
script.textContent = children
62+
63+
if (attrs) {
64+
for (const [key, value] of Object.entries(attrs)) {
65+
if (value !== undefined && value !== false) {
66+
script.setAttribute(
67+
key,
68+
typeof value === 'boolean' ? '' : String(value),
69+
)
70+
}
71+
}
72+
}
73+
74+
document.head.appendChild(script)
75+
76+
onCleanup(() => {
77+
if (script.parentNode) {
78+
script.parentNode.removeChild(script)
79+
}
80+
})
81+
}
82+
})
83+
84+
if (attrs?.src && typeof attrs.src === 'string') {
85+
return <script {...attrs} />
86+
}
87+
88+
if (typeof children === 'string') {
89+
return <script {...attrs} innerHTML={children} />
90+
}
91+
92+
return null
93+
}

packages/solid-router/tests/Scripts.test.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,9 @@ describe('ssr scripts', () => {
5252
initialEntries: ['/'],
5353
}),
5454
routeTree: rootRoute.addChildren([indexRoute]),
55+
isServer: true,
5556
})
5657

57-
router.isServer = true
58-
5958
await router.load()
6059

6160
expect(router.state.matches.map((d) => d.headScripts).flat(1)).toEqual([
@@ -87,10 +86,9 @@ describe('ssr scripts', () => {
8786
initialEntries: ['/'],
8887
}),
8988
routeTree: rootRoute.addChildren([indexRoute]),
89+
isServer: true,
9090
})
9191

92-
router.isServer = true
93-
9492
await router.load()
9593

9694
expect(router.state.matches.map((d) => d.scripts).flat(1)).toEqual([
@@ -102,7 +100,7 @@ describe('ssr scripts', () => {
102100
const { container } = render(() => <RouterProvider router={router} />)
103101

104102
expect(container.innerHTML).toEqual(
105-
`<script src="script.js"></script><script src="script3.js"></script>`,
103+
'<script src="script.js"></script><script src="script3.js"></script>',
106104
)
107105
})
108106
})
@@ -179,10 +177,9 @@ describe('ssr HeadContent', () => {
179177
initialEntries: ['/'],
180178
}),
181179
routeTree: rootRoute.addChildren([indexRoute]),
180+
isServer: true,
182181
})
183182

184-
router.isServer = true
185-
186183
await router.load()
187184

188185
expect(router.state.matches.map((d) => d.meta).flat(1)).toEqual([

0 commit comments

Comments
 (0)