-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(router): prevent script tag duplication in SSR and client-side navigation #5095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
schiller-manuel
merged 9 commits into
TanStack:main
from
jamongsalguclub:issue-fix-4585
Sep 18, 2025
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
027676c
fix(router): prevent script tag duplication in SSR and client-side na…
jamongsalguclub 3d00753
ci: apply automated fixes
autofix-ci[bot] b9d38a1
fix(router): use router for script tag rendering
jamongsalguclub 93d0f16
fix(solid-router): prevent null router access in Script component dur…
jamongsalguclub c72b7e8
style(react-router): reorder imports in Asset.tsx
jamongsalguclub cd2bffb
test: remove comments
jamongsalguclub e424427
refactor(router): improve script deduplication with URL normalization…
jamongsalguclub 1480587
fix(router): improve type safety for script attributes
jamongsalguclub 22b268f
fix(router): normalize URL base for script attributes
jamongsalguclub File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
export const Route = createFileRoute({ | ||
head: () => ({ | ||
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 ( | ||
<div className="p-2"> | ||
<h3 data-testid="inline-scripts-test-heading">Inline Scripts Test</h3> | ||
<p> | ||
This route tests inline script duplication prevention. Two inline | ||
scripts should be loaded. | ||
</p> | ||
</div> | ||
) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
}) | ||
}) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix createFileRoute usage: missing path argument.
React Start routes typically call
createFileRoute('/inline-scripts')
. The current call with an object will fail type-checking or at runtime.📝 Committable suggestion
🤖 Prompt for AI Agents