diff --git a/packages/redirects/src/lib/rewriter.ts b/packages/redirects/src/lib/rewriter.ts index 57d94049..42f0d1c7 100644 --- a/packages/redirects/src/lib/rewriter.ts +++ b/packages/redirects/src/lib/rewriter.ts @@ -23,6 +23,7 @@ export const createRewriter = async function ({ configPath, configRedirects, geoCountry, + ignoreSPARedirect = false, jwtRoleClaim, jwtSecret, projectDir, @@ -31,6 +32,7 @@ export const createRewriter = async function ({ configPath?: string | undefined configRedirects: Redirect[] geoCountry?: string | undefined + ignoreSPARedirect?: boolean jwtRoleClaim: string jwtSecret: string projectDir: string @@ -40,7 +42,21 @@ export const createRewriter = async function ({ const redirectsFiles = [ ...new Set([path.resolve(publicDir ?? '', REDIRECTS_FILE_NAME), path.resolve(projectDir, REDIRECTS_FILE_NAME)]), ] - const redirects = await parseRedirects({ configRedirects, redirectsFiles, configPath }) + let redirects = await parseRedirects({ configRedirects, redirectsFiles, configPath }) + + // Hacky solution: Filter out the SPA redirect pattern when requested. + // This prevents the redirect from interfering with local dev servers like Vite, + // while still allowing it to work in production. + // See: https://github.com/netlify/primitives/issues/325 + if (ignoreSPARedirect) { + redirects = redirects.filter((redirect) => { + // Filter out redirects that match the SPA pattern: from "/*" to "/index.html" with status 200 + // See https://docs.netlify.com/manage/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps, + const isSPARedirect = redirect.origin === '/*' && redirect.to === '/index.html' && redirect.status === 200 + + return !isSPARedirect + }) + } const getMatcher = async (): Promise => { if (matcher) return matcher diff --git a/packages/redirects/src/main.ts b/packages/redirects/src/main.ts index 280e7b45..709cf21f 100644 --- a/packages/redirects/src/main.ts +++ b/packages/redirects/src/main.ts @@ -56,6 +56,7 @@ export class RedirectsHandler { configPath, configRedirects, geoCountry, + ignoreSPARedirect: true, jwtRoleClaim, jwtSecret, projectDir, diff --git a/packages/vite-plugin/src/main.test.ts b/packages/vite-plugin/src/main.test.ts index e5fe53f5..346cc3b6 100644 --- a/packages/vite-plugin/src/main.test.ts +++ b/packages/vite-plugin/src/main.test.ts @@ -705,6 +705,62 @@ defined on your team and site and much more. Run npx netlify init to get started await server.close() await fixture.destroy() }) + + test('Ignores SPA redirect in dev mode', async () => { + const fixture = new Fixture() + .withFile( + 'netlify.toml', + `[[redirects]] + from = "/*" + to = "/index.html" + status = 200`, + ) + .withFile( + 'vite.config.js', + `import { defineConfig } from 'vite'; + import netlify from '@netlify/vite-plugin'; + + export default defineConfig({ + plugins: [ + netlify({ + middleware: true, + }) + ] + });`, + ) + .withFile( + 'index.html', + ` + + SPA App + +
+ + + `, + ) + .withFile('src/main.js', `document.getElementById('app').textContent = 'Hello from SPA'`) + const directory = await fixture.create() + await fixture + .withPackages({ + vite: viteVersion, + '@netlify/vite-plugin': pathToFileURL(path.resolve(directory, PLUGIN_PATH)).toString(), + }) + .create() + + const { server, url } = await startTestServer({ + root: directory, + }) + + // Any route should render the root index.html (Vite handles it) and JS should execute (which + // verifies the SPA redirect isn't interfering with loading the .js module). + await page.goto(`${url}/some-route`) + await page.waitForSelector('#app') + expect(await page.textContent('#app')).toBe('Hello from SPA') + + await server.close() + await fixture.destroy() + }) }) describe('With @vitejs/plugin-react', () => {