Skip to content

Commit 510642b

Browse files
authored
fix: ignore SPA redirect in dev mode to allow Vite to take over (#514)
* fix: ignore SPA redirect in dev mode to allow Vite to serve JS modules Filter out the SPA redirect pattern (from '/*' to '/index.html' with status 200) in createRewriter when ignoreSPARedirect is true. This prevents the redirect from interfering with local dev servers like Vite, while still allowing it to work in production. RedirectsHandler now always passes ignoreSPARedirect: true to createRewriter, ensuring the SPA redirect is ignored in dev mode. Added e2e test to verify JS modules load correctly and execute when SPA redirect is configured in netlify.toml. Fixes #325 * fix: use normalized redirect.origin in SPA redirect filter After normalization, redirects use 'origin' instead of 'from', so we need to check redirect.origin for the SPA pattern match. * test: improve SPA redirect test to avoid Windows path issues Removed direct JS file check that was causing path resolution issues on Windows with Vite 5. The test now verifies the fix by checking that JS executes and updates the page, which is a better test of the actual user-facing behavior. * test: remove redundant step
1 parent cff2b66 commit 510642b

File tree

3 files changed

+74
-1
lines changed

3 files changed

+74
-1
lines changed

packages/redirects/src/lib/rewriter.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const createRewriter = async function ({
2323
configPath,
2424
configRedirects,
2525
geoCountry,
26+
ignoreSPARedirect = false,
2627
jwtRoleClaim,
2728
jwtSecret,
2829
projectDir,
@@ -31,6 +32,7 @@ export const createRewriter = async function ({
3132
configPath?: string | undefined
3233
configRedirects: Redirect[]
3334
geoCountry?: string | undefined
35+
ignoreSPARedirect?: boolean
3436
jwtRoleClaim: string
3537
jwtSecret: string
3638
projectDir: string
@@ -40,7 +42,21 @@ export const createRewriter = async function ({
4042
const redirectsFiles = [
4143
...new Set([path.resolve(publicDir ?? '', REDIRECTS_FILE_NAME), path.resolve(projectDir, REDIRECTS_FILE_NAME)]),
4244
]
43-
const redirects = await parseRedirects({ configRedirects, redirectsFiles, configPath })
45+
let redirects = await parseRedirects({ configRedirects, redirectsFiles, configPath })
46+
47+
// Hacky solution: Filter out the SPA redirect pattern when requested.
48+
// This prevents the redirect from interfering with local dev servers like Vite,
49+
// while still allowing it to work in production.
50+
// See: https://github.com/netlify/primitives/issues/325
51+
if (ignoreSPARedirect) {
52+
redirects = redirects.filter((redirect) => {
53+
// Filter out redirects that match the SPA pattern: from "/*" to "/index.html" with status 200
54+
// See https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps,
55+
const isSPARedirect = redirect.origin === '/*' && redirect.to === '/index.html' && redirect.status === 200
56+
57+
return !isSPARedirect
58+
})
59+
}
4460

4561
const getMatcher = async (): Promise<RedirectMatcher> => {
4662
if (matcher) return matcher

packages/redirects/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class RedirectsHandler {
5656
configPath,
5757
configRedirects,
5858
geoCountry,
59+
ignoreSPARedirect: true,
5960
jwtRoleClaim,
6061
jwtSecret,
6162
projectDir,

packages/vite-plugin/src/main.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,62 @@ defined on your team and site and much more. Run npx netlify init to get started
705705
await server.close()
706706
await fixture.destroy()
707707
})
708+
709+
test('Ignores SPA redirect in dev mode', async () => {
710+
const fixture = new Fixture()
711+
.withFile(
712+
'netlify.toml',
713+
`[[redirects]]
714+
from = "/*"
715+
to = "/index.html"
716+
status = 200`,
717+
)
718+
.withFile(
719+
'vite.config.js',
720+
`import { defineConfig } from 'vite';
721+
import netlify from '@netlify/vite-plugin';
722+
723+
export default defineConfig({
724+
plugins: [
725+
netlify({
726+
middleware: true,
727+
})
728+
]
729+
});`,
730+
)
731+
.withFile(
732+
'index.html',
733+
`<!DOCTYPE html>
734+
<html>
735+
<head><title>SPA App</title></head>
736+
<body>
737+
<div id="app"></div>
738+
<script type="module" src="/src/main.js"></script>
739+
</body>
740+
</html>`,
741+
)
742+
.withFile('src/main.js', `document.getElementById('app').textContent = 'Hello from SPA'`)
743+
const directory = await fixture.create()
744+
await fixture
745+
.withPackages({
746+
vite: viteVersion,
747+
'@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(),
748+
})
749+
.create()
750+
751+
const { server, url } = await startTestServer({
752+
root: directory,
753+
})
754+
755+
// Any route should render the root index.html (Vite handles it) and JS should execute (which
756+
// verifies the SPA redirect isn't interfering with loading the .js module).
757+
await page.goto(`${url}/some-route`)
758+
await page.waitForSelector('#app')
759+
expect(await page.textContent('#app')).toBe('Hello from SPA')
760+
761+
await server.close()
762+
await fixture.destroy()
763+
})
708764
})
709765

710766
describe('With @vitejs/plugin-react', () => {

0 commit comments

Comments
 (0)