Repository navigation
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
Merged
Changes from 2 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…
hyungwookchoi 3d00753
ci: apply automated fixes
autofix-ci[bot] b9d38a1
fix(router): use router for script tag rendering
hyungwookchoi 93d0f16
fix(solid-router): prevent null router access in Script component dur…
hyungwookchoi c72b7e8
style(react-router): reorder imports in Asset.tsx
hyungwookchoi cd2bffb
test: remove comments
hyungwookchoi e424427
refactor(router): improve script deduplication with URL normalization…
hyungwookchoi 1480587
fix(router): improve type safety for script attributes
hyungwookchoi 22b268f
fix(router): normalize URL base for script attributes
hyungwookchoi 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,170 @@ | ||
| import { expect, test } from '@playwright/test' | ||
|
|
||
| test.describe('Script Duplication Prevention', () => { | ||
| test('should not create duplicate scripts on SSR route', async ({ page }) => { | ||
| // Navigate directly to scripts route (SSR scenario) | ||
| await page.goto('/scripts') | ||
|
|
||
| await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() | ||
|
|
||
| // Count script tags with src="script.js" | ||
| const scriptCount = await page.evaluate(() => { | ||
| return document.querySelectorAll('script[src="script.js"]').length | ||
| }) | ||
|
|
||
| // Should have exactly one script tag | ||
| expect(scriptCount).toBe(1) | ||
|
|
||
| // Verify the script executed correctly | ||
| expect(await page.evaluate('window.SCRIPT_1')).toBe(true) | ||
| }) | ||
|
|
||
| test('should not create duplicate scripts during client-side navigation', async ({ | ||
| page, | ||
| }) => { | ||
| // Start from home page | ||
| await page.goto('/') | ||
|
|
||
| // Navigate to scripts route (client-side navigation) | ||
| await page.getByRole('link', { name: 'Scripts', exact: true }).click() | ||
| await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() | ||
|
|
||
| // Count script tags after first navigation | ||
| const firstNavCount = await page.evaluate(() => { | ||
| return document.querySelectorAll('script[src="script.js"]').length | ||
| }) | ||
| expect(firstNavCount).toBe(1) | ||
|
|
||
| // Navigate away from scripts route | ||
| await page.getByRole('link', { name: 'Home' }).click() | ||
| await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() | ||
|
|
||
| // Navigate back to scripts route | ||
| await page.getByRole('link', { name: 'Scripts', exact: true }).click() | ||
| await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() | ||
|
|
||
| // Count script tags after second navigation - should still be 1 | ||
| const secondNavCount = await page.evaluate(() => { | ||
| return document.querySelectorAll('script[src="script.js"]').length | ||
| }) | ||
| expect(secondNavCount).toBe(1) | ||
|
|
||
| // Verify the script is still working | ||
| expect(await page.evaluate('window.SCRIPT_1')).toBe(true) | ||
| }) | ||
|
|
||
| test('should not create duplicate scripts with multiple navigation cycles', async ({ | ||
| page, | ||
| }) => { | ||
| // Start from home page | ||
| await page.goto('/') | ||
|
|
||
| // Navigate to scripts route multiple times | ||
| for (let i = 0; i < 3; i++) { | ||
| // Go to scripts | ||
| await page.getByRole('link', { name: 'Scripts', exact: true }).click() | ||
| await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() | ||
|
|
||
| // Go back to home | ||
| await page.getByRole('link', { name: 'Home' }).click() | ||
| await expect(page.getByRole('link', { name: 'Posts' })).toBeVisible() | ||
| } | ||
|
|
||
| // Final navigation to scripts | ||
| await page.getByRole('link', { name: 'Scripts', exact: true }).click() | ||
| await expect(page.getByTestId('scripts-test-heading')).toBeInViewport() | ||
|
|
||
| // Count script tags - should still be exactly 1 | ||
| const finalCount = await page.evaluate(() => { | ||
| return document.querySelectorAll('script[src="script.js"]').length | ||
| }) | ||
| expect(finalCount).toBe(1) | ||
|
|
||
| // Verify the script is still working | ||
| expect(await page.evaluate('window.SCRIPT_1')).toBe(true) | ||
| }) | ||
|
|
||
| test('should not create duplicate inline scripts', async ({ page }) => { | ||
| // Navigate directly to inline scripts route (SSR scenario) | ||
| await page.goto('/inline-scripts') | ||
|
|
||
| await expect( | ||
| page.getByTestId('inline-scripts-test-heading'), | ||
| ).toBeInViewport() | ||
|
|
||
| // Count specific inline scripts | ||
| 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 | ||
| }) | ||
|
|
||
| // Should have exactly one of each inline script | ||
| expect(script1Count).toBe(1) | ||
| expect(script2Count).toBe(1) | ||
|
|
||
| // Verify the scripts executed correctly | ||
| 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, | ||
| }) => { | ||
| // Start from home page | ||
| await page.goto('/') | ||
|
|
||
| // Navigate to inline scripts route (client-side navigation) | ||
| await page.getByRole('link', { name: 'Inline Scripts' }).click() | ||
| await expect( | ||
| page.getByTestId('inline-scripts-test-heading'), | ||
| ).toBeInViewport() | ||
|
|
||
| // Count inline scripts after first navigation | ||
| 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) | ||
|
|
||
| // Navigate away and back | ||
| 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() | ||
|
|
||
| // Count inline scripts after second navigation - should still be 1 | ||
| 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.
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