From 521114d8109080bf2166df216d7a94be11481e8a Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Fri, 1 Dec 2023 10:24:55 +0100 Subject: [PATCH] feat: allow wildcard redirects --- docs/content/1.guide/3.routing.md | 2 ++ docs/content/3.config.md | 1 + src/options.ts | 4 ++++ src/presets/netlify.ts | 2 +- src/presets/vercel.ts | 6 ++++-- src/runtime/route-rules.ts | 18 +++++++++++++----- test/fixture/nitro.config.ts | 1 + test/presets/netlify.test.ts | 1 + test/presets/vercel.test.ts | 11 +++++++++-- test/tests.ts | 6 ++++++ 10 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docs/content/1.guide/3.routing.md b/docs/content/1.guide/3.routing.md index 3e605031aa..7765d3b875 100644 --- a/docs/content/1.guide/3.routing.md +++ b/docs/content/1.guide/3.routing.md @@ -167,6 +167,7 @@ export default defineNitroConfig({ '/assets/**': { headers: { 'cache-control': 's-maxage=0' } }, '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, '/old-page': { redirect: '/new-page' }, + '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, } @@ -182,6 +183,7 @@ export default defineNuxtConfig({ '/assets/**': { headers: { 'cache-control': 's-maxage=0' } }, '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, '/old-page': { redirect: '/new-page' }, + '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, } diff --git a/docs/content/3.config.md b/docs/content/3.config.md index 3c36d36bdf..509eadac2f 100644 --- a/docs/content/3.config.md +++ b/docs/content/3.config.md @@ -332,6 +332,7 @@ routeRules: { '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } }, '/old-page': { redirect: '/new-page' }, // uses status code 307 (Temporary Redirect) '/old-page2': { redirect: { to:'/new-page2', statusCode: 301 } }, + '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, } diff --git a/src/options.ts b/src/options.ts index 4ccc3cc09e..3d4b4c6c7a 100644 --- a/src/options.ts +++ b/src/options.ts @@ -454,6 +454,10 @@ export function normalizeRouteRules( ? { to: routeConfig.redirect } : routeConfig.redirect), }; + if (path.endsWith("/**")) { + // Internal flag + (routeRules.redirect as any)._redirectStripBase = path.slice(0, -3); + } } // Proxy if (routeConfig.proxy) { diff --git a/src/presets/netlify.ts b/src/presets/netlify.ts index 59a17461e0..03af1a93f4 100644 --- a/src/presets/netlify.ts +++ b/src/presets/netlify.ts @@ -148,7 +148,7 @@ async function writeRedirects(nitro: Nitro) { let code = routeRules.redirect.statusCode; code = { 307: 302, 308: 301 }[code] || code; contents = - `${key.replace("/**", "/*")}\t${routeRules.redirect.to}\t${code}\n` + + `${key.replace("/**", "/*")}\t${routeRules.redirect.to.replace("/**", "/:splat")}\t${code}\n` + contents; } diff --git a/src/presets/vercel.ts b/src/presets/vercel.ts index 9c91b88edd..db498f3567 100644 --- a/src/presets/vercel.ts +++ b/src/presets/vercel.ts @@ -178,12 +178,14 @@ function generateBuildConfig(nitro: Nitro) { .filter(([_, routeRules]) => routeRules.redirect || routeRules.headers) .map(([path, routeRules]) => { let route = { - src: path.replace("/**", "/.*"), + src: path.replace("/**", "/(.*)"), }; if (routeRules.redirect) { route = defu(route, { status: routeRules.redirect.statusCode, - headers: { Location: routeRules.redirect.to }, + headers: { + Location: routeRules.redirect.to.replace("/**", "/$1"), + }, }); } if (routeRules.headers) { diff --git a/src/runtime/route-rules.ts b/src/runtime/route-rules.ts index dd236033bd..74ffb5af79 100644 --- a/src/runtime/route-rules.ts +++ b/src/runtime/route-rules.ts @@ -28,11 +28,19 @@ export function createRouteRulesHandler(ctx: { } // Apply redirect options if (routeRules.redirect) { - return sendRedirect( - event, - routeRules.redirect.to, - routeRules.redirect.statusCode - ); + let target = routeRules.redirect.to; + if (target.endsWith("/**")) { + let targetPath = event.path; + const strpBase = (routeRules.redirect as any)._redirectStripBase; + if (strpBase) { + targetPath = withoutBase(targetPath, strpBase); + } + target = joinURL(target.slice(0, -3), targetPath); + } else if (event.path.includes("?")) { + const query = getQuery(event.path); + target = withQuery(target, query); + } + return sendRedirect(event, target, routeRules.redirect.statusCode); } // Apply proxy options if (routeRules.proxy) { diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index a639e247de..d792d7f2f4 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -70,6 +70,7 @@ export default defineNitroConfig({ "/rules/redirect/obj": { redirect: { to: "https://nitro.unjs.io/", statusCode: 308 }, }, + "/rules/redirect/wildcard/**": { redirect: "https://nitro.unjs.io/**" }, "/rules/nested/**": { redirect: "/base", headers: { "x-test": "test" } }, "/rules/nested/override": { redirect: { to: "/other" } }, "/rules/_/noncached/cached": { swr: true }, diff --git a/test/presets/netlify.test.ts b/test/presets/netlify.test.ts index c1d582131d..d2a0a50e60 100644 --- a/test/presets/netlify.test.ts +++ b/test/presets/netlify.test.ts @@ -51,6 +51,7 @@ describe("nitro:preset:netlify", async () => { /* eslint-disable no-tabs */ expect(redirects).toMatchInlineSnapshot(` "/rules/nested/override /other 302 + /rules/redirect/wildcard/* https://nitro.unjs.io/:splat 302 /rules/redirect/obj https://nitro.unjs.io/ 301 /rules/nested/* /base 302 /rules/redirect /base 302 diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index 8a78cb8f99..b71f32e2a8 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -40,6 +40,13 @@ describe("nitro:preset:vercel", async () => { "src": "/rules/redirect/obj", "status": 308, }, + { + "headers": { + "Location": "https://nitro.unjs.io/$1", + }, + "src": "/rules/redirect/wildcard/(.*)", + "status": 307, + }, { "headers": { "Location": "/other", @@ -74,14 +81,14 @@ describe("nitro:preset:vercel", async () => { "Location": "/base", "x-test": "test", }, - "src": "/rules/nested/.*", + "src": "/rules/nested/(.*)", "status": 307, }, { "headers": { "cache-control": "public, max-age=3600, immutable", }, - "src": "/build/.*", + "src": "/build/(.*)", }, { "continue": true, diff --git a/test/tests.ts b/test/tests.ts index c0dd3dbb42..d7f6ba6947 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -248,6 +248,12 @@ export function testNitro( const obj = await callHandler({ url: "/rules/redirect/obj" }); expect(obj.status).toBe(308); expect(obj.headers.location).toBe("https://nitro.unjs.io/"); + + const wildcard = await callHandler({ + url: "/rules/redirect/wildcard/nuxt", + }); + expect(wildcard.status).toBe(307); + expect(wildcard.headers.location).toBe("https://nitro.unjs.io/nuxt"); }); it("binary response", async () => {