mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
### What?
Converts every test under `test/integration/` to an isolated test
running through `nextTestSetup` (under `test/e2e/`, `test/production/`,
`test/development/`, or `test/unit/`), then deletes `test/integration/`
along with the legacy CI orchestration that was specific to it.
- `test/integration/` removed entirely (~327 test suites)
- New isolated suites added across the existing folders:
- `test/e2e/` — 175
- `test/production/` — 130
- `test/development/` — 43
- `test/unit/` — 1
- `.github/workflows/build_and_test.yml` and `run-tests.js` no longer
have any `integration` branches
- `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a
small number of tests that drive their own proxy/static-export server
can keep using `next.browser(...)` instead of importing `next-webdriver`
directly
### Why?
`test/integration/` predated `nextTestSetup` and ran tests directly
against the source checkout via custom helpers (`launchApp`,
`nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`,
…). Each suite hand-rolled its own dev/start/build orchestration,
fixture mutation, and process management.
The isolated test model used by the rest of the repo gives each suite an
isolated working directory containing a packed `next.tgz` install, a
uniform `next.start()` / `next.build()` / `next.fetch()` /
`next.browser()` API, and the same lifecycle for dev, start, and deploy
modes — so a single set of assertions covers all three. Deploy-mode
skips and per-feature gates are expressed declaratively
(`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`)
instead of branching on `process.env`.
Removing `test/integration/` lets us:
- Delete the bespoke orchestration code in the CI workflow and
`run-tests.js`
- Run every converted suite consistently in dev, start, and deploy modes
(where applicable)
- Reproduce every test locally with the same `pnpm
test-{dev,start}-{turbo,webpack}` commands; no separate `integration`
path
- Open the door to running `test/production` against deployments in the
future (the converted suites already declare `skipDeployment` so they
can be flipped on)
### How?
Mechanical conversion per suite, with targeted clean-ups:
1. **Per-suite conversion.** Each
`test/integration/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.test.ts` under the right folder based on what the
original exercised:
- `launchApp` / dev-only assertions → `test/development/`
- `nextBuild` + `nextStart` / start-only assertions → `test/production/`
- Both → `test/e2e/`
- The one pure jsdom render check (`link-without-router`) → `test/unit/`
2. **API mapping.** Custom helpers were replaced by `nextTestSetup`
equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`,
`runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`,
`webdriver(...)` → `next.browser(...)`. Fixture mutations switched from
raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg
`runWithTempContent` callback when the change has a defined scope) and
`next.deleteFile`.
3. **Deploy-mode handling.** Suites that can't run in deploy mode (use
`patchFile` / `next.build()` / depend on local CLI output) declare
`skipDeployment: true` and early-return on the `skipped` boolean. Suites
where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare
`disableAutoSkewProtection: true`.
4. **`next.browser({ baseUrl })`.** A handful of tests
(`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both
`react-virtualized` suites) need to drive a separate server (a
static-export server or an `http-proxy` instance) rather than the
Next.js process. Instead of importing `next-webdriver` directly, those
tests now pass `{ baseUrl: <port|url> }` to `next.browser()`. For the
proxy cases, the proxy was moved into `server.js` inside the fixture and
`http-proxy` declared via the `dependencies` option of `nextTestSetup`,
so the test runs with a fully isolated dependency graph.
5. **CI clean-up.** With `test/integration` gone, the `test
integration*` jobs and `integration-tests-manifest`-related logic in
`.github/workflows/build_and_test.yml` were removed, and `run-tests.js`
no longer has the `integration` test-folder branch.
6. **Validation.** The PR was iterated against multiple full CI runs;
the remaining failures on the latest run are pre-existing flakes
(segment-cache 60s `act` timeouts in turbopack-prod) or transient
infrastructure issues unrelated to the conversion.
3675 lines
116 KiB
TypeScript
3675 lines
116 KiB
TypeScript
import http from 'http'
|
|
import stripAnsi from 'strip-ansi'
|
|
import cheerio from 'cheerio'
|
|
import WebSocket from 'ws'
|
|
import {
|
|
waitForNoRedbox,
|
|
findPort,
|
|
getBrowserBodyText,
|
|
waitFor,
|
|
normalizeRegEx,
|
|
normalizeManifest,
|
|
retry,
|
|
} from 'next-test-utils'
|
|
import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils'
|
|
|
|
describe('Custom routes', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipStart: true,
|
|
disableAutoSkewProtection: true,
|
|
skipDeployment: true,
|
|
})
|
|
if (skipped) return
|
|
|
|
let externalServerPort: number
|
|
let externalServer: http.Server
|
|
let externalServerHits: Set<string>
|
|
let buildId: string
|
|
let buildCliOutput: string = ''
|
|
|
|
beforeEach(() => {
|
|
externalServerHits = new Set()
|
|
})
|
|
|
|
beforeAll(async () => {
|
|
externalServerPort = await findPort()
|
|
externalServer = http.createServer((req, res) => {
|
|
externalServerHits.add(req.url)
|
|
const nextHost = req.headers['x-forwarded-host']
|
|
const externalHost = req.headers['host']
|
|
res.end(`hi ${nextHost} from ${externalHost}`)
|
|
})
|
|
const wsServer = new WebSocket.Server({ noServer: true })
|
|
|
|
externalServer.on('upgrade', (req, socket, head) => {
|
|
externalServerHits.add(req.url)
|
|
wsServer.handleUpgrade(req, socket, head, (client) => {
|
|
client.send('hello world')
|
|
})
|
|
})
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onError = (error: Error) => {
|
|
reject(error)
|
|
}
|
|
externalServer.once('error', onError)
|
|
externalServer.listen(externalServerPort, () => {
|
|
externalServer.off('error', onError)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
await next.patchFile('next.config.js', (content) =>
|
|
content.replace(/__EXTERNAL_PORT__/g, String(externalServerPort))
|
|
)
|
|
|
|
if (!isNextDev) {
|
|
const result = await next.build({ args: ['-d'] })
|
|
buildCliOutput = result.cliOutput || ''
|
|
buildId = (await next.readFile('.next/BUILD_ID')).trim()
|
|
await next.start({ skipBuild: true })
|
|
} else {
|
|
buildId = 'development'
|
|
await next.start()
|
|
}
|
|
})
|
|
|
|
afterAll(() => {
|
|
externalServer.close()
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
path: '/to-ANOTHER',
|
|
content: /could not be found/,
|
|
status: 404,
|
|
},
|
|
{
|
|
path: '/HELLO-world',
|
|
content: /could not be found/,
|
|
status: 404,
|
|
},
|
|
{
|
|
path: '/docs/GITHUB',
|
|
content: /could not be found/,
|
|
status: 404,
|
|
},
|
|
{
|
|
path: '/add-HEADER',
|
|
content: /could not be found/,
|
|
status: 404,
|
|
},
|
|
])(
|
|
'should honor caseSensitiveRoutes config for $path',
|
|
async ({ path, status, content }) => {
|
|
const res = await next.fetch(path, { redirect: 'manual' })
|
|
|
|
if (status) {
|
|
expect(res.status).toBe(status)
|
|
}
|
|
|
|
if (content) {
|
|
expect(await res.text()).toMatch(content)
|
|
}
|
|
}
|
|
)
|
|
|
|
it('should successfully rewrite a WebSocket request', async () => {
|
|
const messages = []
|
|
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
|
let socket = new WebSocket(`ws://localhost:${next.appPort}/to-websocket`)
|
|
socket.on('message', (data) => {
|
|
messages.push(data.toString())
|
|
})
|
|
socket.on('open', () => resolve(socket))
|
|
socket.on('error', (err) => {
|
|
console.error(err)
|
|
socket.close()
|
|
reject()
|
|
})
|
|
})
|
|
|
|
await retry(() => {
|
|
expect(messages.length).toBeGreaterThan(0)
|
|
})
|
|
ws.close()
|
|
expect([...externalServerHits]).toEqual(['/_next/hmr?page=/about'])
|
|
})
|
|
|
|
it('should successfully rewrite a WebSocket request to a page', async () => {
|
|
const messages = []
|
|
try {
|
|
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
|
let socket = new WebSocket(
|
|
`ws://localhost:${next.appPort}/websocket-to-page`
|
|
)
|
|
socket.on('message', (data) => {
|
|
messages.push(data.toString())
|
|
})
|
|
socket.on('open', () => resolve(socket))
|
|
socket.on('error', (err) => {
|
|
console.error(err)
|
|
socket.close()
|
|
reject()
|
|
})
|
|
})
|
|
ws.close()
|
|
} catch (err) {
|
|
messages.push(err)
|
|
}
|
|
expect(next.cliOutput).not.toContain('unhandledRejection')
|
|
})
|
|
|
|
it('should not rewrite for _next/data route when a match is found', async () => {
|
|
const initial = await next.fetch('/overridden/first')
|
|
expect(initial.status).toBe(200)
|
|
expect(await initial.text()).toContain('this page is overridden')
|
|
|
|
const nextData = await next.fetch(
|
|
`/_next/data/${buildId}/overridden/first.json`
|
|
)
|
|
expect(nextData.status).toBe(200)
|
|
expect(await nextData.json()).toEqual({
|
|
pageProps: { params: { slug: 'first' } },
|
|
__N_SSG: true,
|
|
})
|
|
})
|
|
|
|
it('should handle has query encoding correctly', async () => {
|
|
for (const expected of [
|
|
{
|
|
post: 'first',
|
|
slug: ['first'],
|
|
},
|
|
{
|
|
post: 'hello%20world',
|
|
slug: ['hello world'],
|
|
},
|
|
{
|
|
post: 'hello/world',
|
|
slug: ['hello', 'world'],
|
|
},
|
|
{
|
|
post: 'hello%2fworld',
|
|
slug: ['hello', 'world'],
|
|
},
|
|
]) {
|
|
const { post } = expected
|
|
const res = await next.fetch(`/has-rewrite-8?post=${post}`, {
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
const $ = cheerio.load(await res.text())
|
|
expect(JSON.parse($('#props').text())).toEqual({
|
|
params: {
|
|
slug: expected.slug,
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
it('should handle external beforeFiles rewrite correctly', async () => {
|
|
const res = await next.fetch('/overridden')
|
|
const html = await res.text()
|
|
|
|
if (res.status !== 200) {
|
|
console.error('Invalid response', html)
|
|
}
|
|
expect(res.status).toBe(200)
|
|
expect(html).toContain('Example Domain')
|
|
|
|
const browser = await next.browser('/nav')
|
|
await browser.elementByCss('#to-before-files-overridden').click()
|
|
await retry(async () => {
|
|
expect(await browser.eval('document.documentElement.innerHTML')).toMatch(
|
|
/Example Domain/
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should handle beforeFiles rewrite to dynamic route correctly', async () => {
|
|
const res = await next.fetch('/nfl')
|
|
const html = await res.text()
|
|
|
|
if (res.status !== 200) {
|
|
console.error('Invalid response', html)
|
|
}
|
|
expect(res.status).toBe(200)
|
|
expect(html).toContain('/_sport/[slug]')
|
|
|
|
const browser = await next.browser('/nav')
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser.elementByCss('#to-before-files-dynamic').click()
|
|
await retry(async () => {
|
|
expect(await browser.eval('document.documentElement.innerHTML')).toMatch(
|
|
/_sport\/\[slug\]/
|
|
)
|
|
})
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
slug: 'nfl',
|
|
})
|
|
expect(await browser.elementByCss('#pathname').text()).toBe(
|
|
'/_sport/[slug]'
|
|
)
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
})
|
|
|
|
it('should handle beforeFiles rewrite to partly dynamic route correctly', async () => {
|
|
const res = await next.fetch('/nfl')
|
|
const html = await res.text()
|
|
|
|
if (res.status !== 200) {
|
|
console.error('Invalid response', html)
|
|
}
|
|
expect(res.status).toBe(200)
|
|
expect(html).toContain('/_sport/[slug]')
|
|
|
|
const browser = await next.browser('/nav')
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser.elementByCss('#to-before-files-dynamic-again').click()
|
|
await retry(async () => {
|
|
expect(await browser.eval('document.documentElement.innerHTML')).toMatch(
|
|
/_sport\/\[slug\]\/test/
|
|
)
|
|
})
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
slug: 'nfl',
|
|
})
|
|
expect(await browser.elementByCss('#pathname').text()).toBe(
|
|
'/_sport/[slug]/test'
|
|
)
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
})
|
|
|
|
it('should support long URLs for rewrites', async () => {
|
|
const res = await next.fetch(
|
|
'/catchall-rewrite/a9btBxtHQALZ6cxfuj18X6OLGNSkJVzrOXz41HG4QwciZfn7ggRZzPx21dWqGiTBAqFRiWvVNm5ko2lpyso5jtVaXg88dC1jKfqI2qmIcdeyJat8xamrIh2LWnrYRrsBcoKfQU65KHod8DPANuzPS3fkVYWlmov05GQbc82HwR1exOvPVKUKb5gBRWiN0WOh7hN4QyezIuq3dJINAptFQ6m2bNGjYACBRk4MOSHdcQG58oq5Ch7luuqrl9EcbWSa'
|
|
)
|
|
|
|
const html = await res.text()
|
|
expect(res.status).toBe(200)
|
|
expect(html).toContain('/with-params')
|
|
})
|
|
|
|
it('should resolveHref correctly navigating through history', async () => {
|
|
const browser = await next.browser('/')
|
|
await browser.eval('window.beforeNav = 1')
|
|
|
|
expect(await browser.eval('document.documentElement.innerHTML')).toContain(
|
|
'multi-rewrites'
|
|
)
|
|
|
|
await browser.eval('next.router.push("/rewriting-to-auto-export")')
|
|
await browser.waitForElementByCss('#auto-export')
|
|
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
slug: 'hello',
|
|
rewrite: '1',
|
|
})
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
|
|
await browser.eval('next.router.push("/nav")')
|
|
await browser.waitForElementByCss('#nav')
|
|
|
|
expect(await browser.elementByCss('#nav').text()).toBe('Nav')
|
|
|
|
await browser.back()
|
|
await browser.waitForElementByCss('#auto-export')
|
|
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
slug: 'hello',
|
|
rewrite: '1',
|
|
})
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
|
|
if (isNextDev) {
|
|
await waitForNoRedbox(browser)
|
|
}
|
|
})
|
|
|
|
it('should continue in beforeFiles rewrites', async () => {
|
|
const res = await next.fetch('/old-blog/about')
|
|
expect(res.status).toBe(200)
|
|
|
|
const html = await res.text()
|
|
const $ = cheerio.load(html)
|
|
|
|
expect($('#hello').text()).toContain('Hello')
|
|
|
|
const browser = await next.browser('/nav')
|
|
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser
|
|
.elementByCss('#to-old-blog')
|
|
.click()
|
|
.waitForElementByCss('#hello')
|
|
expect(await browser.elementByCss('#hello').text()).toContain('Hello')
|
|
})
|
|
|
|
it('should not hang when proxy rewrite fails', async () => {
|
|
const res = await next.fetch('/to-nowhere', {
|
|
timeout: 5000,
|
|
})
|
|
|
|
expect(res.status).toBe(500)
|
|
})
|
|
|
|
it('should parse params correctly for rewrite to auto-export dynamic page', async () => {
|
|
const browser = await next.browser('/rewriting-to-auto-export')
|
|
await retry(async () => {
|
|
expect(
|
|
await browser.eval(() => document.documentElement.innerHTML)
|
|
).toMatch(/auto-export.*?hello/)
|
|
})
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
rewrite: '1',
|
|
slug: 'hello',
|
|
})
|
|
})
|
|
|
|
it('should provide params correctly for rewrite to auto-export non-dynamic page', async () => {
|
|
const browser = await next.browser(
|
|
'/rewriting-to-another-auto-export/first'
|
|
)
|
|
|
|
expect(await browser.elementByCss('#auto-export-another').text()).toBe(
|
|
'auto-export another'
|
|
)
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
rewrite: '1',
|
|
path: ['first'],
|
|
})
|
|
})
|
|
|
|
it('should handle one-to-one rewrite successfully', async () => {
|
|
const html = await next.render('/first')
|
|
expect(html).toMatch(/hello/)
|
|
})
|
|
|
|
it('should handle chained rewrites successfully', async () => {
|
|
const html = await next.render('/')
|
|
expect(html).toMatch(/multi-rewrites/)
|
|
})
|
|
|
|
it('should handle param like headers properly', async () => {
|
|
const res = await next.fetch('/my-other-header/my-path')
|
|
expect(res.headers.get('x-path')).toBe('my-path')
|
|
expect(res.headers.get('somemy-path')).toBe('hi')
|
|
expect(res.headers.get('x-test')).toBe('some:value*')
|
|
expect(res.headers.get('x-test-2')).toBe('value*')
|
|
expect(res.headers.get('x-test-3')).toBe(':value?')
|
|
expect(res.headers.get('x-test-4')).toBe(':value+')
|
|
expect(res.headers.get('x-test-5')).toBe('something https:')
|
|
expect(res.headers.get('x-test-6')).toBe(':hello(world)')
|
|
expect(res.headers.get('x-test-7')).toBe('hello(world)')
|
|
expect(res.headers.get('x-test-8')).toBe('hello{1,}')
|
|
expect(res.headers.get('x-test-9')).toBe(':hello{1,2}')
|
|
expect(res.headers.get('content-security-policy')).toBe(
|
|
"default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/my-path"
|
|
)
|
|
})
|
|
|
|
it('should not match dynamic route immediately after applying header', async () => {
|
|
const res = await next.fetch('/blog/post-321')
|
|
expect(res.headers.get('x-something')).toBe('applied-everywhere')
|
|
|
|
const $ = cheerio.load(await res.text())
|
|
expect(JSON.parse($('p').text()).path).toBe('blog')
|
|
})
|
|
|
|
it('should handle chained redirects successfully', async () => {
|
|
const res1 = await next.fetch('/redir-chain1', { redirect: 'manual' })
|
|
const res1location = new URL(res1.headers.get('location'), res1.url)
|
|
.pathname
|
|
expect(res1.status).toBe(301)
|
|
expect(res1location).toBe('/redir-chain2')
|
|
|
|
const res2 = await next.fetch(res1location, { redirect: 'manual' })
|
|
const res2location = new URL(res2.headers.get('location'), res2.url)
|
|
.pathname
|
|
expect(res2.status).toBe(302)
|
|
expect(res2location).toBe('/redir-chain3')
|
|
|
|
const res3 = await next.fetch(res2location, { redirect: 'manual' })
|
|
const res3location = new URL(res3.headers.get('location'), res3.url)
|
|
.pathname
|
|
expect(res3.status).toBe(303)
|
|
expect(res3location).toBe('/')
|
|
})
|
|
|
|
it('should not match redirect for /_next', async () => {
|
|
const res = await next.fetch('/_next/has-redirect-5', {
|
|
headers: {
|
|
'x-test-next': 'true',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/another/has-redirect-5', {
|
|
headers: {
|
|
'x-test-next': 'true',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.status).toBe(307)
|
|
})
|
|
|
|
it('should redirect successfully with permanent: false', async () => {
|
|
const res = await next.fetch('/redirect1', { redirect: 'manual' })
|
|
const { pathname } = new URL(res.headers.get('location'), res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/')
|
|
})
|
|
|
|
it('should redirect with params successfully', async () => {
|
|
const res = await next.fetch('/hello/123/another', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname } = new URL(res.headers.get('location'), res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/blog/123')
|
|
})
|
|
|
|
it('should redirect with hash successfully', async () => {
|
|
const res = await next.fetch('/docs/router-status/500', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname, hash, search } = new URL(
|
|
res.headers.get('location'),
|
|
res.url
|
|
)
|
|
expect(res.status).toBe(301)
|
|
expect(pathname).toBe('/docs/v2/network/status-codes')
|
|
expect(hash).toBe('#500')
|
|
expect(search).toEqual('')
|
|
})
|
|
|
|
it('should redirect successfully with provided statusCode', async () => {
|
|
const res = await next.fetch('/redirect2', { redirect: 'manual' })
|
|
const { pathname, search } = new URL(res.headers.get('location'), res.url)
|
|
expect(res.status).toBe(301)
|
|
expect(pathname).toBe('/')
|
|
expect(search).toEqual('')
|
|
})
|
|
|
|
it('should redirect successfully with catchall', async () => {
|
|
const res = await next.fetch('/catchall-redirect/hello/world', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname, search } = new URL(res.headers.get('location'), res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/somewhere')
|
|
expect(search).toEqual('')
|
|
})
|
|
|
|
it('should server static files through a rewrite', async () => {
|
|
const text = await next.render('/hello-world')
|
|
expect(text).toBe('hello world!')
|
|
})
|
|
|
|
it('should rewrite with params successfully', async () => {
|
|
const html = await next.render('/test/hello')
|
|
expect(html).toMatch(/Hello/)
|
|
})
|
|
|
|
it('should not append params when one is used in destination path', async () => {
|
|
const html = await next.render('/test/with-params?a=b')
|
|
const $ = cheerio.load(html)
|
|
expect(JSON.parse($('p').text())).toEqual({ a: 'b' })
|
|
})
|
|
|
|
it('should double redirect successfully', async () => {
|
|
const html = await next.render('/docs/github')
|
|
expect(html).toMatch(/hi there/)
|
|
})
|
|
|
|
it('should allow params in query for rewrite', async () => {
|
|
const html = await next.render('/query-rewrite/hello/world?a=b')
|
|
const $ = cheerio.load(html)
|
|
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
|
|
first: 'hello',
|
|
second: 'world',
|
|
a: 'b',
|
|
section: 'hello',
|
|
name: 'world',
|
|
})
|
|
})
|
|
|
|
it('should have correct params for catchall rewrite', async () => {
|
|
const html = await next.render('/catchall-rewrite/hello/world?a=b')
|
|
const $ = cheerio.load(html)
|
|
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
|
|
a: 'b',
|
|
path: ['hello', 'world'],
|
|
})
|
|
})
|
|
|
|
it('should have correct encoding for params with catchall rewrite', async () => {
|
|
const html = await next.render(
|
|
'/catchall-rewrite/hello%20world%3Fw%3D24%26focalpoint%3Dcenter?a=b'
|
|
)
|
|
const $ = cheerio.load(html)
|
|
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
|
|
a: 'b',
|
|
path: ['hello%20world%3Fw%3D24%26focalpoint%3Dcenter'],
|
|
})
|
|
})
|
|
|
|
it('should have correct query for catchall rewrite', async () => {
|
|
const html = await next.render('/catchall-query/hello/world?a=b')
|
|
const $ = cheerio.load(html)
|
|
expect(JSON.parse($('#__NEXT_DATA__').html()).query).toEqual({
|
|
a: 'b',
|
|
another: 'hello/world',
|
|
path: ['hello', 'world'],
|
|
})
|
|
})
|
|
|
|
it('should have correct header for catchall rewrite', async () => {
|
|
const res = await next.fetch('/catchall-header/hello/world?a=b')
|
|
const headerValue = res.headers.get('x-value')
|
|
expect(headerValue).toBe('hello/world')
|
|
})
|
|
|
|
it('should allow params in query for redirect', async () => {
|
|
const res = await next.fetch('/query-redirect/hello/world?a=b', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname, searchParams } = new URL(
|
|
res.headers.get('location'),
|
|
res.url
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/with-params')
|
|
expect(Object.fromEntries(searchParams)).toEqual({
|
|
first: 'hello',
|
|
second: 'world',
|
|
a: 'b',
|
|
})
|
|
})
|
|
|
|
it('should have correctly encoded params in query for redirect', async () => {
|
|
const res = await next.fetch(
|
|
'/query-redirect/hello%20world%3Fw%3D24%26focalpoint%3Dcenter/world?a=b',
|
|
{ redirect: 'manual' }
|
|
)
|
|
const { pathname, searchParams } = new URL(
|
|
res.headers.get('location'),
|
|
res.url
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/with-params')
|
|
expect(Object.fromEntries(searchParams)).toEqual({
|
|
first: 'hello world?w=24&focalpoint=center',
|
|
second: 'world',
|
|
a: 'b',
|
|
})
|
|
})
|
|
|
|
it('should overwrite param values correctly', async () => {
|
|
const html = await next.render('/test-overwrite/first/second')
|
|
expect(html).toMatch(/this-should-be-the-value/)
|
|
expect(html).not.toMatch(/first/)
|
|
expect(html).toMatch(/second/)
|
|
})
|
|
|
|
it('should handle query for rewrite correctly', async () => {
|
|
const html = await next.render(
|
|
'/query-rewrite/first/second?section=overridden&name=overridden&first=overridden&second=overridden&keep=me'
|
|
)
|
|
|
|
const data = JSON.parse(cheerio.load(html)('p').text())
|
|
expect(data).toEqual({
|
|
first: 'first',
|
|
second: 'second',
|
|
section: 'first',
|
|
name: 'second',
|
|
keep: 'me',
|
|
})
|
|
})
|
|
|
|
it('should not allow rewrite to override page file', async () => {
|
|
const html = await next.render('/nav')
|
|
expect(html).toContain('to-hello')
|
|
})
|
|
|
|
it('show allow redirect to override the page', async () => {
|
|
const res = await next.fetch('/redirect-override', { redirect: 'manual' })
|
|
const { pathname } = new URL(res.headers.get('location') || '', res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/thank-you-next')
|
|
})
|
|
|
|
it('should work successfully on the client', async () => {
|
|
const browser = await next.browser('/nav')
|
|
await browser.elementByCss('#to-hello').click()
|
|
await browser.waitForElementByCss('#hello')
|
|
|
|
expect(await browser.eval('window.location.href')).toMatch(/\/first$/)
|
|
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
|
|
|
|
await browser.eval('window.location.href = window.location.href')
|
|
await waitFor(500)
|
|
expect(await browser.eval('window.location.href')).toMatch(/\/first$/)
|
|
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
|
|
|
|
await browser.elementByCss('#to-nav').click()
|
|
await browser.waitForElementByCss('#to-hello-again')
|
|
await browser.elementByCss('#to-hello-again').click()
|
|
await browser.waitForElementByCss('#hello-again')
|
|
|
|
expect(await browser.eval('window.location.href')).toMatch(/\/second$/)
|
|
expect(await getBrowserBodyText(browser)).toMatch(/Hello again/)
|
|
|
|
await browser.eval('window.location.href = window.location.href')
|
|
await waitFor(500)
|
|
expect(await browser.eval('window.location.href')).toMatch(/\/second$/)
|
|
expect(await getBrowserBodyText(browser)).toMatch(/Hello again/)
|
|
})
|
|
|
|
it('should work with rewrite when manually specifying href/as', async () => {
|
|
const browser = await next.browser('/nav')
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser
|
|
.elementByCss('#to-params-manual')
|
|
.click()
|
|
.waitForElementByCss('#query')
|
|
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
const query = JSON.parse(await browser.elementByCss('#query').text())
|
|
expect(query).toEqual({
|
|
something: '1',
|
|
another: 'value',
|
|
})
|
|
})
|
|
|
|
it('should work with rewrite when only specifying href', async () => {
|
|
const browser = await next.browser('/nav')
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser
|
|
.elementByCss('#to-params')
|
|
.click()
|
|
.waitForElementByCss('#query')
|
|
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
const query = JSON.parse(await browser.elementByCss('#query').text())
|
|
expect(query).toEqual({
|
|
something: '1',
|
|
another: 'value',
|
|
})
|
|
})
|
|
|
|
it('should work with rewrite when only specifying href and ends in dynamic route', async () => {
|
|
const browser = await next.browser('/nav')
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser
|
|
.elementByCss('#to-rewritten-dynamic')
|
|
.click()
|
|
.waitForElementByCss('#auto-export')
|
|
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
|
|
const text = await browser.eval(() => document.documentElement.innerHTML)
|
|
expect(text).toContain('auto-export hello')
|
|
})
|
|
|
|
it('should match a page after a rewrite', async () => {
|
|
const html = await next.render('/to-hello')
|
|
expect(html).toContain('Hello')
|
|
})
|
|
|
|
it('should match dynamic route after rewrite', async () => {
|
|
const html = await next.render('/blog/post-1')
|
|
expect(html).toMatch(/post:.*?post-2/)
|
|
})
|
|
|
|
it('should match public file after rewrite', async () => {
|
|
const data = await next.render('/blog/data.json')
|
|
expect(JSON.parse(data)).toEqual({ hello: 'world' })
|
|
})
|
|
|
|
it('should match /_next file after rewrite', async () => {
|
|
await next.render('/hello')
|
|
const data = await next.render(
|
|
`/hidden/_next/static/${buildId}/_buildManifest.js`
|
|
)
|
|
expect(data).toContain('/hello')
|
|
})
|
|
|
|
it('should allow redirecting to external resource', async () => {
|
|
const res = await next.fetch('/to-external', { redirect: 'manual' })
|
|
const location = res.headers.get('location')
|
|
expect(res.status).toBe(307)
|
|
expect(location).toBe('https://google.com/')
|
|
})
|
|
|
|
it('should apply headers for exact match', async () => {
|
|
const res = await next.fetch('/add-header')
|
|
expect(res.headers.get('x-custom-header')).toBe('hello world')
|
|
expect(res.headers.get('x-another-header')).toBe('hello again')
|
|
})
|
|
|
|
it('should apply headers for multi match', async () => {
|
|
const res = await next.fetch('/my-headers/first')
|
|
expect(res.headers.get('x-first-header')).toBe('first')
|
|
expect(res.headers.get('x-second-header')).toBe('second')
|
|
})
|
|
|
|
it('should apply params for header key/values', async () => {
|
|
const res = await next.fetch('/my-other-header/first')
|
|
expect(res.headers.get('x-path')).toBe('first')
|
|
expect(res.headers.get('somefirst')).toBe('hi')
|
|
})
|
|
|
|
it('should support URL for header key/values', async () => {
|
|
const res = await next.fetch('/without-params/url')
|
|
expect(res.headers.get('x-origin')).toBe('https://example.com')
|
|
})
|
|
|
|
it('should apply params header key/values with URL', async () => {
|
|
const res = await next.fetch('/with-params/url/first')
|
|
expect(res.headers.get('x-url')).toBe('https://example.com/first')
|
|
})
|
|
|
|
it('should apply params header key/values with URL that has port', async () => {
|
|
const res = await next.fetch('/with-params/url2/first')
|
|
expect(res.headers.get('x-url')).toBe(
|
|
'https://example.com:8080?hello=first'
|
|
)
|
|
})
|
|
|
|
it('should support named pattern for header key/values', async () => {
|
|
const res = await next.fetch('/named-pattern/hello')
|
|
expect(res.headers.get('x-something')).toBe('value=hello')
|
|
expect(res.headers.get('path-hello')).toBe('end')
|
|
})
|
|
|
|
it('should support proxying to external resource', async () => {
|
|
const res = await next.fetch('/proxy-me/first?keep=me&and=me')
|
|
expect(res.status).toBe(200)
|
|
expect(
|
|
[...externalServerHits].map((u) => {
|
|
const { pathname, searchParams } = new URL(u, res.url)
|
|
return {
|
|
pathname,
|
|
query: Object.fromEntries(searchParams),
|
|
}
|
|
})
|
|
).toEqual([
|
|
{
|
|
pathname: '/first',
|
|
query: {
|
|
keep: 'me',
|
|
and: 'me',
|
|
},
|
|
},
|
|
])
|
|
const nextHost = `localhost:${next.appPort}`
|
|
const externalHost = `localhost:${externalServerPort}`
|
|
expect(await res.text()).toContain(`hi ${nextHost} from ${externalHost}`)
|
|
})
|
|
|
|
it('should support unnamed parameters correctly', async () => {
|
|
const res = await next.fetch('/unnamed/first/final', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname } = new URL(res.headers.get('location') || '', res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/got-unnamed')
|
|
})
|
|
|
|
it('should support named like unnamed parameters correctly', async () => {
|
|
const res = await next.fetch('/named-like-unnamed/first', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname } = new URL(res.headers.get('location') || '', res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/first')
|
|
})
|
|
|
|
it('should add refresh header for 308 redirect', async () => {
|
|
const res = await next.fetch('/redirect4', { redirect: 'manual' })
|
|
expect(res.status).toBe(308)
|
|
expect(res.headers.get('refresh')).toBe(`0;url=/`)
|
|
})
|
|
|
|
it('should have correctly encoded query in location and refresh headers', async () => {
|
|
const res = await next.fetch(
|
|
'/redirect4?%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82',
|
|
{ redirect: 'manual' }
|
|
)
|
|
expect(res.status).toBe(308)
|
|
|
|
expect(res.headers.get('location').split('?')[1]).toBe(
|
|
'%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82'
|
|
)
|
|
expect(res.headers.get('refresh')).toBe(
|
|
'0;url=/?%E3%83%86%E3%82%B9%E3%83%88=%E3%81%82'
|
|
)
|
|
})
|
|
|
|
it('should handle basic api rewrite successfully', async () => {
|
|
const data = await next.render('/api-hello')
|
|
expect(JSON.parse(data)).toEqual({ query: {} })
|
|
})
|
|
|
|
it('should handle api rewrite with un-named param successfully', async () => {
|
|
const data = await next.render('/api-hello-regex/hello/world')
|
|
expect(JSON.parse(data)).toEqual({
|
|
query: { name: 'hello/world', first: 'hello/world' },
|
|
})
|
|
})
|
|
|
|
it('should handle api rewrite with param successfully', async () => {
|
|
const data = await next.render('/api-hello-param/hello')
|
|
expect(JSON.parse(data)).toEqual({
|
|
query: { name: 'hello', hello: 'hello' },
|
|
})
|
|
})
|
|
|
|
it('should handle encoded value in the pathname correctly', async () => {
|
|
const res = await next.fetch(
|
|
'/redirect/me/to-about/' + encodeURI('\\google.com'),
|
|
{ redirect: 'manual' }
|
|
)
|
|
|
|
const { pathname, hostname, searchParams } = new URL(
|
|
res.headers.get('location') || '',
|
|
res.url
|
|
)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe(encodeURI('/\\google.com/about'))
|
|
expect(hostname).not.toBe('google.com')
|
|
expect(Object.fromEntries(searchParams)).toEqual({})
|
|
})
|
|
|
|
it('should handle unnamed parameters with multi-match successfully', async () => {
|
|
const html = await next.render(
|
|
'/unnamed-params/nested/first/second/hello/world'
|
|
)
|
|
const params = JSON.parse(cheerio.load(html)('p').text())
|
|
expect(params).toEqual({ test: 'hello' })
|
|
})
|
|
|
|
it('should handle named regex parameters with multi-match successfully', async () => {
|
|
const res = await next.fetch('/docs/integrations/v2-some/thing', {
|
|
redirect: 'manual',
|
|
})
|
|
const { pathname } = new URL(res.headers.get('location') || '', res.url)
|
|
expect(res.status).toBe(307)
|
|
expect(pathname).toBe('/integrations/-some/thing')
|
|
})
|
|
|
|
it('should redirect with URL in query correctly', async () => {
|
|
const res = await next.fetch('/to-external-with-query', {
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
expect(res.headers.get('location')).toBe(
|
|
'https://authserver.example.com/set-password?returnUrl=https://www.example.com/login'
|
|
)
|
|
})
|
|
|
|
it('should redirect with URL in query correctly non-encoded', async () => {
|
|
const res = await next.fetch('/to-external-with-query', {
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
expect(res.headers.get('location')).toBe(
|
|
'https://authserver.example.com/set-password?returnUrl=https://www.example.com/login'
|
|
)
|
|
})
|
|
|
|
it('should match missing header headers correctly', async () => {
|
|
const res = await next.fetch('/missing-headers-1', {
|
|
headers: {
|
|
'x-my-header': 'hello world!!',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-headers-1', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.headers.get('x-new-header')).toBe('new-value')
|
|
})
|
|
|
|
it('should match missing query headers correctly', async () => {
|
|
const res = await next.fetch('/missing-headers-2?my-query=hellooo')
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-headers-2', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.headers.get('x-new-header')).toBe('new-value')
|
|
})
|
|
|
|
it('should match missing cookie headers correctly', async () => {
|
|
const res = await next.fetch('/missing-headers-3', {
|
|
headers: {
|
|
cookie: 'loggedIn=true',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-headers-3', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.headers.get('x-new-header')).toBe('new-value')
|
|
})
|
|
|
|
it('should match missing header redirect correctly', async () => {
|
|
const res = await next.fetch('/missing-rewrite-1', {
|
|
headers: {
|
|
'x-my-header': 'hello world!!',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-redirect-1', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.status).toBe(307)
|
|
const url = new URL(res2.headers.get('location'), 'http://n')
|
|
expect(url.pathname).toBe('/with-params')
|
|
})
|
|
|
|
it('should match missing query redirect correctly', async () => {
|
|
const res = await next.fetch('/missing-redirect-2?my-query=hellooo')
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-redirect-2', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.status).toBe(307)
|
|
const url = new URL(res2.headers.get('location'), 'http://n')
|
|
expect(url.pathname).toBe('/with-params')
|
|
})
|
|
|
|
it('should match missing cookie redirect correctly', async () => {
|
|
const res = await next.fetch('/missing-redirect-3', {
|
|
headers: {
|
|
cookie: 'loggedIn=true',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-redirect-3', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res2.status).toBe(307)
|
|
const url = new URL(res2.headers.get('location'), 'http://n')
|
|
expect(url.pathname).toBe('/with-params')
|
|
expect(url.search).toBe('?authorized=1')
|
|
})
|
|
|
|
it('should match missing header rewrite correctly', async () => {
|
|
const res = await next.fetch('/missing-rewrite-1', {
|
|
headers: {
|
|
'x-my-header': 'hello world!!',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-rewrite-1')
|
|
const $2 = cheerio.load(await res2.text())
|
|
|
|
expect(res2.status).toBe(200)
|
|
expect(JSON.parse($2('#query').text())).toEqual({})
|
|
})
|
|
|
|
it('should match missing query rewrite correctly', async () => {
|
|
const res = await next.fetch('/missing-rewrite-2?my-query=hellooo')
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-rewrite-2')
|
|
const $2 = cheerio.load(await res2.text())
|
|
expect(res2.status).toBe(200)
|
|
expect(JSON.parse($2('#query').text())).toEqual({})
|
|
})
|
|
|
|
it('should match missing cookie rewrite correctly', async () => {
|
|
const res = await next.fetch('/missing-rewrite-3', {
|
|
headers: {
|
|
cookie: 'loggedIn=true',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(404)
|
|
|
|
const res2 = await next.fetch('/missing-rewrite-3')
|
|
const $2 = cheerio.load(await res2.text())
|
|
expect(JSON.parse($2('#query').text())).toEqual({
|
|
authorized: '1',
|
|
})
|
|
expect(res2.status).toBe(200)
|
|
})
|
|
|
|
it('should match has header rewrite correctly', async () => {
|
|
const res = await next.fetch('/has-rewrite-1', {
|
|
headers: {
|
|
'x-my-header': 'hello world!!',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
myHeader: 'hello world!!',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-rewrite-1')
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should match has query rewrite correctly', async () => {
|
|
const res = await next.fetch('/has-rewrite-2?my-query=hellooo')
|
|
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
'my-query': 'hellooo',
|
|
myquery: 'hellooo',
|
|
value: 'hellooo',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-rewrite-2')
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should match has cookie rewrite correctly', async () => {
|
|
const res = await next.fetch('/has-rewrite-3', {
|
|
headers: {
|
|
cookie: 'loggedIn=true',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
loggedIn: 'true',
|
|
authorized: '1',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-rewrite-3')
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should match has host rewrite correctly', async () => {
|
|
const res1 = await next.fetch('/has-rewrite-4')
|
|
expect(res1.status).toBe(404)
|
|
|
|
const res = await next.fetch('/has-rewrite-4', {
|
|
headers: {
|
|
host: 'example.com',
|
|
},
|
|
})
|
|
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
host: '1',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-rewrite-4')
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should pass has segment for rewrite correctly', async () => {
|
|
const res1 = await next.fetch('/has-rewrite-5')
|
|
expect(res1.status).toBe(404)
|
|
|
|
const res = await next.fetch('/has-rewrite-5?hasParam=with-params')
|
|
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
hasParam: 'with-params',
|
|
})
|
|
})
|
|
|
|
it('should not pass non captured has value for rewrite correctly', async () => {
|
|
const res1 = await next.fetch('/has-rewrite-6')
|
|
expect(res1.status).toBe(404)
|
|
|
|
const res = await next.fetch('/has-rewrite-6', {
|
|
headers: {
|
|
hasParam: 'with-params',
|
|
},
|
|
})
|
|
expect(res.status).toBe(200)
|
|
|
|
const $ = cheerio.load(await res.text())
|
|
expect(JSON.parse($('#query').text())).toEqual({})
|
|
})
|
|
|
|
it('should pass captured has value for rewrite correctly', async () => {
|
|
const res1 = await next.fetch('/has-rewrite-7')
|
|
expect(res1.status).toBe(404)
|
|
|
|
const res = await next.fetch('/has-rewrite-7?hasParam=with-params')
|
|
expect(res.status).toBe(200)
|
|
|
|
const $ = cheerio.load(await res.text())
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
hasParam: 'with-params',
|
|
idk: 'with-params',
|
|
})
|
|
})
|
|
|
|
it('should match has rewrite correctly before files', async () => {
|
|
const res1 = await next.fetch('/hello')
|
|
expect(res1.status).toBe(200)
|
|
const $1 = cheerio.load(await res1.text())
|
|
expect($1('#hello').text()).toBe('Hello')
|
|
|
|
const res = await next.fetch('/hello?overrideMe=1')
|
|
|
|
expect(res.status).toBe(200)
|
|
const $ = cheerio.load(await res.text())
|
|
|
|
expect(JSON.parse($('#query').text())).toEqual({
|
|
overrideMe: '1',
|
|
overridden: '1',
|
|
})
|
|
|
|
const browser = await next.browser('/nav')
|
|
await browser.eval('window.beforeNav = 1')
|
|
await browser.elementByCss('#to-overridden').click()
|
|
await browser.waitForElementByCss('#query')
|
|
|
|
expect(await browser.eval('window.next.router.pathname')).toBe(
|
|
'/with-params'
|
|
)
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
|
overridden: '1',
|
|
overrideMe: '1',
|
|
})
|
|
expect(await browser.eval('window.beforeNav')).toBe(1)
|
|
})
|
|
|
|
it('should match has header redirect correctly', async () => {
|
|
const res = await next.fetch('/has-redirect-1', {
|
|
headers: {
|
|
'x-my-header': 'hello world!!',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
const parsed = new URL(res.headers.get('location'), res.url)
|
|
|
|
expect(parsed.pathname).toBe('/another')
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
myHeader: 'hello world!!',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-redirect-1', { redirect: 'manual' })
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should match has query redirect correctly', async () => {
|
|
const res = await next.fetch('/has-redirect-2?my-query=hellooo', {
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
const parsed = new URL(res.headers.get('location'), res.url)
|
|
|
|
expect(parsed.pathname).toBe('/another')
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
value: 'hellooo',
|
|
'my-query': 'hellooo',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-redirect-2', { redirect: 'manual' })
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should match has cookie redirect correctly', async () => {
|
|
const res = await next.fetch('/has-redirect-3', {
|
|
headers: {
|
|
cookie: 'loggedIn=true',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
const parsed = new URL(res.headers.get('location'), res.url)
|
|
|
|
expect(parsed.pathname).toBe('/another')
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
authorized: '1',
|
|
})
|
|
|
|
const res2 = await next.fetch('/has-redirect-3', { redirect: 'manual' })
|
|
expect(res2.status).toBe(404)
|
|
})
|
|
|
|
it('should match has host redirect correctly', async () => {
|
|
const res1 = await next.fetch('/has-redirect-4', { redirect: 'manual' })
|
|
expect(res1.status).toBe(404)
|
|
|
|
const res = await next.fetch('/has-redirect-4', {
|
|
headers: {
|
|
host: 'example.com',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
const parsed = new URL(res.headers.get('location'), res.url)
|
|
|
|
expect(parsed.pathname).toBe('/another')
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
host: '1',
|
|
})
|
|
})
|
|
|
|
it('should match has host redirect and insert in destination correctly', async () => {
|
|
const res1 = await next.fetch('/has-redirect-6', { redirect: 'manual' })
|
|
expect(res1.status).toBe(404)
|
|
|
|
const res = await next.fetch('/has-redirect-6', {
|
|
headers: {
|
|
host: 'hello-test.example.com',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.status).toBe(307)
|
|
const parsed = new URL(res.headers.get('location'), res.url)
|
|
|
|
expect(parsed.protocol).toBe('https:')
|
|
expect(parsed.hostname).toBe('hello.example.com')
|
|
expect(parsed.pathname).toBe('/some-path/end')
|
|
expect(Object.fromEntries(parsed.searchParams)).toEqual({
|
|
a: 'b',
|
|
})
|
|
})
|
|
|
|
it('should match has query redirect with duplicate query key', async () => {
|
|
const res = await next.fetch('/has-redirect-7?hello=world&hello=another', {
|
|
redirect: 'manual',
|
|
})
|
|
expect(res.status).toBe(307)
|
|
const parsed = new URL(res.headers.get('location'), res.url)
|
|
|
|
expect(parsed.pathname).toBe('/somewhere')
|
|
const query = {}
|
|
for (const key of parsed.searchParams.keys()) {
|
|
const values = parsed.searchParams.getAll(key)
|
|
query[key] = values.length > 1 ? values : values[0]
|
|
}
|
|
expect(query).toEqual({
|
|
hello: ['world', 'another'],
|
|
value: 'another',
|
|
})
|
|
})
|
|
|
|
it('should match has header for header correctly', async () => {
|
|
const res = await next.fetch('/has-header-1', {
|
|
headers: {
|
|
'x-my-header': 'hello world!!',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.headers.get('x-another')).toBe('header')
|
|
|
|
const res2 = await next.fetch('/has-header-1', { redirect: 'manual' })
|
|
expect(res2.headers.get('x-another')).toBe(null)
|
|
})
|
|
|
|
it('should match has query for header correctly', async () => {
|
|
const res = await next.fetch('/has-header-2?my-query=hellooo', {
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.headers.get('x-added')).toBe('value')
|
|
|
|
const res2 = await next.fetch('/has-header-2', { redirect: 'manual' })
|
|
expect(res2.headers.get('x-another')).toBe(null)
|
|
})
|
|
|
|
it('should match has cookie for header correctly', async () => {
|
|
const res = await next.fetch('/has-header-3', {
|
|
headers: {
|
|
cookie: 'loggedIn=true',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.headers.get('x-is-user')).toBe('yuuuup')
|
|
|
|
const res2 = await next.fetch('/has-header-3', { redirect: 'manual' })
|
|
expect(res2.headers.get('x-is-user')).toBe(null)
|
|
})
|
|
|
|
it('should match has host for header correctly', async () => {
|
|
const res = await next.fetch('/has-header-4', {
|
|
headers: {
|
|
host: 'example.com',
|
|
},
|
|
redirect: 'manual',
|
|
})
|
|
|
|
expect(res.headers.get('x-is-host')).toBe('yuuuup')
|
|
|
|
const res2 = await next.fetch('/has-header-4', { redirect: 'manual' })
|
|
expect(res2.headers.get('x-is-host')).toBe(null)
|
|
})
|
|
|
|
if (!isNextDev) {
|
|
it('should output routes-manifest successfully', async () => {
|
|
const manifest = JSON.parse(
|
|
await next.readFile('.next/routes-manifest.json')
|
|
)
|
|
|
|
for (const route of [
|
|
...manifest.dynamicRoutes,
|
|
...manifest.rewrites.beforeFiles,
|
|
...manifest.rewrites.afterFiles,
|
|
...manifest.rewrites.fallback,
|
|
...manifest.redirects,
|
|
...manifest.headers,
|
|
]) {
|
|
route.regex = normalizeRegEx(route.regex)
|
|
}
|
|
for (const route of manifest.dataRoutes) {
|
|
route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex)
|
|
}
|
|
|
|
const normalizedManifest = normalizeManifest(manifest, [
|
|
[buildId, 'BUILD_ID'],
|
|
[`${externalServerPort}`, 'EXTERNAL_SERVER_PORT'],
|
|
])
|
|
|
|
if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
|
|
expect(normalizedManifest).toMatchInlineSnapshot(`
|
|
{
|
|
"appType": "pages",
|
|
"basePath": "",
|
|
"caseSensitive": true,
|
|
"dataRoutes": [
|
|
{
|
|
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/blog\\-catchall\\/(.+?)\\.json$",
|
|
"namedDataRouteRegex": "^/_next/data/BUILD_ID/blog\\-catchall/(?<nxtPslug>.+?)\\.json$",
|
|
"page": "/blog-catchall/[...slug]",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/overridden\\/([^\\/]+?)\\.json$",
|
|
"namedDataRouteRegex": "^/_next/data/BUILD_ID/overridden/(?<nxtPslug>[^/]+?)\\.json$",
|
|
"page": "/overridden/[slug]",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
],
|
|
"dynamicRoutes": [
|
|
{
|
|
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/_sport/[slug]",
|
|
"regex": "^\\/_sport\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)/test(?:/)?$",
|
|
"page": "/_sport/[slug]/test",
|
|
"regex": "^\\/_sport\\/([^\\/]+?)\\/test(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/another/(?<nxtPid>[^/]+?)(?:/)?$",
|
|
"page": "/another/[id]",
|
|
"regex": "^\\/another\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPid": "nxtPid",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/api/dynamic/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/api/dynamic/[slug]",
|
|
"regex": "^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/auto\\-export/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/auto-export/[slug]",
|
|
"regex": "^\\/auto\\-export\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/blog/(?<nxtPpost>[^/]+?)(?:/)?$",
|
|
"page": "/blog/[post]",
|
|
"regex": "^\\/blog\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPpost": "nxtPpost",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/blog\\-catchall/(?<nxtPslug>.+?)(?:/)?$",
|
|
"page": "/blog-catchall/[...slug]",
|
|
"regex": "^\\/blog\\-catchall\\/(.+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/overridden/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/overridden/[slug]",
|
|
"regex": "^\\/overridden\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-new-header",
|
|
"value": "new-value",
|
|
},
|
|
],
|
|
"missing": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-headers-1(?:\\/)?$",
|
|
"source": "/missing-headers-1",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-new-header",
|
|
"value": "new-value",
|
|
},
|
|
],
|
|
"missing": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-headers-2(?:\\/)?$",
|
|
"source": "/missing-headers-2",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-new-header",
|
|
"value": "new-value",
|
|
},
|
|
],
|
|
"missing": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-headers-3(?:\\/)?$",
|
|
"source": "/missing-headers-3",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-custom-header",
|
|
"value": "hello world",
|
|
},
|
|
{
|
|
"key": "x-another-header",
|
|
"value": "hello again",
|
|
},
|
|
],
|
|
"regex": "^\\/add-header(?:\\/)?$",
|
|
"source": "/add-header",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-first-header",
|
|
"value": "first",
|
|
},
|
|
{
|
|
"key": "x-second-header",
|
|
"value": "second",
|
|
},
|
|
],
|
|
"regex": "^\\/my-headers(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/my-headers/(.*)",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-path",
|
|
"value": ":path",
|
|
},
|
|
{
|
|
"key": "some:path",
|
|
"value": "hi",
|
|
},
|
|
{
|
|
"key": "x-test",
|
|
"value": "some:value*",
|
|
},
|
|
{
|
|
"key": "x-test-2",
|
|
"value": "value*",
|
|
},
|
|
{
|
|
"key": "x-test-3",
|
|
"value": ":value?",
|
|
},
|
|
{
|
|
"key": "x-test-4",
|
|
"value": ":value+",
|
|
},
|
|
{
|
|
"key": "x-test-5",
|
|
"value": "something https:",
|
|
},
|
|
{
|
|
"key": "x-test-6",
|
|
"value": ":hello(world)",
|
|
},
|
|
{
|
|
"key": "x-test-7",
|
|
"value": "hello(world)",
|
|
},
|
|
{
|
|
"key": "x-test-8",
|
|
"value": "hello{1,}",
|
|
},
|
|
{
|
|
"key": "x-test-9",
|
|
"value": ":hello{1,2}",
|
|
},
|
|
{
|
|
"key": "content-security-policy",
|
|
"value": "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path",
|
|
},
|
|
],
|
|
"regex": "^\\/my-other-header(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/my-other-header/:path",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-origin",
|
|
"value": "https://example.com",
|
|
},
|
|
],
|
|
"regex": "^\\/without-params\\/url(?:\\/)?$",
|
|
"source": "/without-params/url",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-url",
|
|
"value": "https://example.com/:path*",
|
|
},
|
|
],
|
|
"regex": "^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/with-params/url/:path*",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-url",
|
|
"value": "https://example.com:8080?hello=:path*",
|
|
},
|
|
],
|
|
"regex": "^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/with-params/url2/:path*",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-something",
|
|
"value": "applied-everywhere",
|
|
},
|
|
],
|
|
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/:path*",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-something",
|
|
"value": "value=:path",
|
|
},
|
|
{
|
|
"key": "path-:path",
|
|
"value": "end",
|
|
},
|
|
],
|
|
"regex": "^\\/named-pattern(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/named-pattern/:path(.*)",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-value",
|
|
"value": ":path*",
|
|
},
|
|
],
|
|
"regex": "^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-header/:path*",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-another",
|
|
"value": "header",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-1(?:\\/)?$",
|
|
"source": "/has-header-1",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-added",
|
|
"value": "value",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-2(?:\\/)?$",
|
|
"source": "/has-header-2",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "true",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-is-user",
|
|
"value": "yuuuup",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-3(?:\\/)?$",
|
|
"source": "/has-header-3",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "example.com",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-is-host",
|
|
"value": "yuuuup",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-4(?:\\/)?$",
|
|
"source": "/has-header-4",
|
|
},
|
|
],
|
|
"onMatchHeaders": [],
|
|
"pages404": true,
|
|
"ppr": {
|
|
"chain": {
|
|
"headers": {
|
|
"next-resume": "1",
|
|
},
|
|
},
|
|
},
|
|
"redirects": [
|
|
{
|
|
"destination": "/:path+",
|
|
"internal": true,
|
|
"priority": true,
|
|
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$",
|
|
"source": "/:path+/",
|
|
"statusCode": 308,
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/missing-redirect-1(?:\\/)?$",
|
|
"source": "/missing-redirect-1",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/missing-redirect-2(?:\\/)?$",
|
|
"source": "/missing-redirect-2",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/with-params?authorized=1",
|
|
"missing": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/missing-redirect-3(?:\\/)?$",
|
|
"source": "/missing-redirect-3",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/:lang/about",
|
|
"regex": "^(?!\\/_next)\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/redirect/me/to-about/:lang",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/docs/v2/network/status-codes#:code",
|
|
"regex": "^(?!\\/_next)\\/docs\\/router-status(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/docs/router-status/:code",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/docs/v2/advanced/now-for-github",
|
|
"regex": "^(?!\\/_next)\\/docs\\/github(?:\\/)?$",
|
|
"source": "/docs/github",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/docs/v2/more/:all",
|
|
"regex": "^(?!\\/_next)\\/docs\\/v2\\/advanced(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/docs/v2/advanced/:all(.*)",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/blog/:id",
|
|
"regex": "^(?!\\/_next)\\/hello(?:\\/([^\\/]+?))\\/another(?:\\/)?$",
|
|
"source": "/hello/:id/another",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redirect1(?:\\/)?$",
|
|
"source": "/redirect1",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redirect2(?:\\/)?$",
|
|
"source": "/redirect2",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/another",
|
|
"regex": "^(?!\\/_next)\\/redirect3(?:\\/)?$",
|
|
"source": "/redirect3",
|
|
"statusCode": 302,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redirect4(?:\\/)?$",
|
|
"source": "/redirect4",
|
|
"statusCode": 308,
|
|
},
|
|
{
|
|
"destination": "/redir-chain2",
|
|
"regex": "^(?!\\/_next)\\/redir-chain1(?:\\/)?$",
|
|
"source": "/redir-chain1",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/redir-chain3",
|
|
"regex": "^(?!\\/_next)\\/redir-chain2(?:\\/)?$",
|
|
"source": "/redir-chain2",
|
|
"statusCode": 302,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redir-chain3(?:\\/)?$",
|
|
"source": "/redir-chain3",
|
|
"statusCode": 303,
|
|
},
|
|
{
|
|
"destination": "https://google.com",
|
|
"regex": "^(?!\\/_next)\\/to-external(?:\\/)?$",
|
|
"source": "/to-external",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/with-params?first=:section&second=:name",
|
|
"regex": "^(?!\\/_next)\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/query-redirect/:section/:name",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/got-unnamed",
|
|
"regex": "^(?!\\/_next)\\/unnamed(?:\\/(first|second))(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/unnamed/(first|second)/(.*)",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/:0",
|
|
"regex": "^(?!\\/_next)\\/named-like-unnamed(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/named-like-unnamed/:0",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/thank-you-next",
|
|
"regex": "^(?!\\/_next)\\/redirect-override(?:\\/)?$",
|
|
"source": "/redirect-override",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/:first/:second",
|
|
"regex": "^(?!\\/_next)\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)(?:\\/)?$",
|
|
"source": "/docs/:first(integrations|now-cli)/v2:second(.*)",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/somewhere",
|
|
"regex": "^(?!\\/_next)\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-redirect/:path*",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "https://authserver.example.com/set-password?returnUrl=https%3A%2F%2Fwww.example.com/login",
|
|
"regex": "^(?!\\/_next)\\/to-external-with-query(?:\\/)?$",
|
|
"source": "/to-external-with-query",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "https://authserver.example.com/set-password?returnUrl=https://www.example.com/login",
|
|
"regex": "^(?!\\/_next)\\/to-external-with-query-2(?:\\/)?$",
|
|
"source": "/to-external-with-query-2",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?myHeader=:myHeader",
|
|
"has": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-1(?:\\/)?$",
|
|
"source": "/has-redirect-1",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?value=:myquery",
|
|
"has": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-2(?:\\/)?$",
|
|
"source": "/has-redirect-2",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?authorized=1",
|
|
"has": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "true",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-3(?:\\/)?$",
|
|
"source": "/has-redirect-3",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?host=1",
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "example.com",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-4(?:\\/)?$",
|
|
"source": "/has-redirect-4",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/somewhere",
|
|
"has": [
|
|
{
|
|
"key": "x-test-next",
|
|
"type": "header",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)(?:\\/([^\\/]+?))\\/has-redirect-5(?:\\/)?$",
|
|
"source": "/:path/has-redirect-5",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "https://:subdomain.example.com/some-path/end?a=b",
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "(?<subdomain>.*)-test.example.com",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-6(?:\\/)?$",
|
|
"source": "/has-redirect-6",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/somewhere?value=:hello",
|
|
"has": [
|
|
{
|
|
"key": "hello",
|
|
"type": "query",
|
|
"value": "(?<hello>.*)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-7(?:\\/)?$",
|
|
"source": "/has-redirect-7",
|
|
"statusCode": 307,
|
|
},
|
|
],
|
|
"rewriteHeaders": {
|
|
"pathHeader": "x-nextjs-rewritten-path",
|
|
"queryHeader": "x-nextjs-rewritten-query",
|
|
},
|
|
"rewrites": {
|
|
"afterFiles": [
|
|
{
|
|
"destination": "http://localhost:EXTERNAL_SERVER_PORT/_next/hmr?page=/about",
|
|
"regex": "^\\/to-websocket(?:\\/)?$",
|
|
"source": "/to-websocket",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/websocket-to-page(?:\\/)?$",
|
|
"source": "/websocket-to-page",
|
|
},
|
|
{
|
|
"destination": "http://localhost:12233",
|
|
"regex": "^\\/to-nowhere(?:\\/)?$",
|
|
"source": "/to-nowhere",
|
|
},
|
|
{
|
|
"destination": "/auto-export/hello?rewrite=1",
|
|
"regex": "^\\/rewriting-to-auto-export(?:\\/)?$",
|
|
"source": "/rewriting-to-auto-export",
|
|
},
|
|
{
|
|
"destination": "/auto-export/another?rewrite=1",
|
|
"regex": "^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/rewriting-to-another-auto-export/:path*",
|
|
},
|
|
{
|
|
"destination": "/another/one",
|
|
"regex": "^\\/to-another(?:\\/)?$",
|
|
"source": "/to-another",
|
|
},
|
|
{
|
|
"destination": "/404",
|
|
"regex": "^\\/nav(?:\\/)?$",
|
|
"source": "/nav",
|
|
},
|
|
{
|
|
"destination": "/static/hello.txt",
|
|
"regex": "^\\/hello-world(?:\\/)?$",
|
|
"source": "/hello-world",
|
|
},
|
|
{
|
|
"destination": "/another",
|
|
"regex": "^\\/(?:\\/)?$",
|
|
"source": "/",
|
|
},
|
|
{
|
|
"destination": "/multi-rewrites",
|
|
"regex": "^\\/another(?:\\/)?$",
|
|
"source": "/another",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/first(?:\\/)?$",
|
|
"source": "/first",
|
|
},
|
|
{
|
|
"destination": "/hello-again",
|
|
"regex": "^\\/second(?:\\/)?$",
|
|
"source": "/second",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/to-hello(?:\\/)?$",
|
|
"source": "/to-hello",
|
|
},
|
|
{
|
|
"destination": "/blog/post-2",
|
|
"regex": "^\\/blog\\/post-1(?:\\/)?$",
|
|
"source": "/blog/post-1",
|
|
},
|
|
{
|
|
"destination": "/:path",
|
|
"regex": "^\\/test(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/test/:path",
|
|
},
|
|
{
|
|
"destination": "/params/this-should-be-the-value",
|
|
"regex": "^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/test-overwrite/:something/:another",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^\\/params(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/params/:something",
|
|
},
|
|
{
|
|
"destination": "/with-params?first=:section&second=:name",
|
|
"regex": "^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/query-rewrite/:section/:name",
|
|
},
|
|
{
|
|
"destination": "/_next/:path*",
|
|
"regex": "^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/hidden/_next/:path*",
|
|
},
|
|
{
|
|
"destination": "http://localhost:EXTERNAL_SERVER_PORT/:path*",
|
|
"regex": "^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/proxy-me/:path*",
|
|
},
|
|
{
|
|
"destination": "/api/hello",
|
|
"regex": "^\\/api-hello(?:\\/)?$",
|
|
"source": "/api-hello",
|
|
},
|
|
{
|
|
"destination": "/api/hello?name=:first*",
|
|
"regex": "^\\/api-hello-regex(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/api-hello-regex/:first(.*)",
|
|
},
|
|
{
|
|
"destination": "/api/hello?hello=:name",
|
|
"regex": "^\\/api-hello-param(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/api-hello-param/:name",
|
|
},
|
|
{
|
|
"destination": "/api/dynamic/:name?hello=:name",
|
|
"regex": "^\\/api-dynamic-param(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/api-dynamic-param/:name",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^(?:\\/([^\\/]+?))\\/post-321(?:\\/)?$",
|
|
"source": "/:path/post-321",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/unnamed-params/nested/(.*)/:test/(.*)",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-rewrite/:path*",
|
|
},
|
|
{
|
|
"destination": "/with-params?another=:path*",
|
|
"regex": "^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-query/:path*",
|
|
},
|
|
{
|
|
"destination": "/with-params?myHeader=:myHeader",
|
|
"has": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-1(?:\\/)?$",
|
|
"source": "/has-rewrite-1",
|
|
},
|
|
{
|
|
"destination": "/with-params?value=:myquery",
|
|
"has": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-2(?:\\/)?$",
|
|
"source": "/has-rewrite-2",
|
|
},
|
|
{
|
|
"destination": "/with-params?authorized=1",
|
|
"has": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-3(?:\\/)?$",
|
|
"source": "/has-rewrite-3",
|
|
},
|
|
{
|
|
"destination": "/with-params?host=1",
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "example.com",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-4(?:\\/)?$",
|
|
"source": "/has-rewrite-4",
|
|
},
|
|
{
|
|
"destination": "/:hasParam",
|
|
"has": [
|
|
{
|
|
"key": "hasParam",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-5(?:\\/)?$",
|
|
"source": "/has-rewrite-5",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"has": [
|
|
{
|
|
"key": "hasParam",
|
|
"type": "header",
|
|
"value": "with-params",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-6(?:\\/)?$",
|
|
"source": "/has-rewrite-6",
|
|
},
|
|
{
|
|
"destination": "/with-params?idk=:idk",
|
|
"has": [
|
|
{
|
|
"key": "hasParam",
|
|
"type": "query",
|
|
"value": "(?<idk>with-params|hello)",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-7(?:\\/)?$",
|
|
"source": "/has-rewrite-7",
|
|
},
|
|
{
|
|
"destination": "/blog-catchall/:post",
|
|
"has": [
|
|
{
|
|
"key": "post",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-8(?:\\/)?$",
|
|
"source": "/has-rewrite-8",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-rewrite-1(?:\\/)?$",
|
|
"source": "/missing-rewrite-1",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-rewrite-2(?:\\/)?$",
|
|
"source": "/missing-rewrite-2",
|
|
},
|
|
{
|
|
"destination": "/with-params?authorized=1",
|
|
"missing": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-rewrite-3(?:\\/)?$",
|
|
"source": "/missing-rewrite-3",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/blog\\/about(?:\\/)?$",
|
|
"source": "/blog/about",
|
|
},
|
|
{
|
|
"destination": "/overridden",
|
|
"regex": "^\\/overridden(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/overridden/:path*",
|
|
},
|
|
],
|
|
"beforeFiles": [
|
|
{
|
|
"destination": "/with-params?overridden=1",
|
|
"has": [
|
|
{
|
|
"key": "overrideMe",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/hello(?:\\/)?$",
|
|
"source": "/hello",
|
|
},
|
|
{
|
|
"destination": "/blog/:path*",
|
|
"regex": "^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/old-blog/:path*",
|
|
},
|
|
{
|
|
"destination": "https://example.vercel.sh",
|
|
"regex": "^\\/overridden(?:\\/)?$",
|
|
"source": "/overridden",
|
|
},
|
|
{
|
|
"destination": "/_sport/nfl/:path*",
|
|
"regex": "^\\/nfl(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/nfl/:path*",
|
|
},
|
|
],
|
|
"fallback": [],
|
|
},
|
|
"rsc": {
|
|
"clientParamParsing": true,
|
|
"contentTypeHeader": "text/x-component",
|
|
"didPostponeHeader": "x-nextjs-postponed",
|
|
"dynamicRSCPrerender": true,
|
|
"header": "rsc",
|
|
"prefetchHeader": "next-router-prefetch",
|
|
"prefetchSegmentDirSuffix": ".segments",
|
|
"prefetchSegmentHeader": "next-router-segment-prefetch",
|
|
"prefetchSegmentSuffix": ".segment.rsc",
|
|
"suffix": ".rsc",
|
|
"varyHeader": "rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch",
|
|
},
|
|
"staticRoutes": [
|
|
{
|
|
"namedRegex": "^/api/hello(?:/)?$",
|
|
"page": "/api/hello",
|
|
"regex": "^/api/hello(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/auto\\-export/another(?:/)?$",
|
|
"page": "/auto-export/another",
|
|
"regex": "^/auto\\-export/another(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
|
|
"page": "/docs/v2/more/now-for-github",
|
|
"regex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/hello(?:/)?$",
|
|
"page": "/hello",
|
|
"regex": "^/hello(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/hello\\-again(?:/)?$",
|
|
"page": "/hello-again",
|
|
"regex": "^/hello\\-again(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/multi\\-rewrites(?:/)?$",
|
|
"page": "/multi-rewrites",
|
|
"regex": "^/multi\\-rewrites(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/nav(?:/)?$",
|
|
"page": "/nav",
|
|
"regex": "^/nav(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/overridden(?:/)?$",
|
|
"page": "/overridden",
|
|
"regex": "^/overridden(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/redirect\\-override(?:/)?$",
|
|
"page": "/redirect-override",
|
|
"regex": "^/redirect\\-override(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/with\\-params(?:/)?$",
|
|
"page": "/with-params",
|
|
"regex": "^/with\\-params(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
],
|
|
"version": 3,
|
|
}
|
|
`)
|
|
} else {
|
|
expect(normalizedManifest).toMatchInlineSnapshot(`
|
|
{
|
|
"appType": "pages",
|
|
"basePath": "",
|
|
"caseSensitive": true,
|
|
"dataRoutes": [
|
|
{
|
|
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/blog\\-catchall\\/(.+?)\\.json$",
|
|
"namedDataRouteRegex": "^/_next/data/BUILD_ID/blog\\-catchall/(?<nxtPslug>.+?)\\.json$",
|
|
"page": "/blog-catchall/[...slug]",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"dataRouteRegex": "^\\/_next\\/data\\/BUILD_ID\\/overridden\\/([^\\/]+?)\\.json$",
|
|
"namedDataRouteRegex": "^/_next/data/BUILD_ID/overridden/(?<nxtPslug>[^/]+?)\\.json$",
|
|
"page": "/overridden/[slug]",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
],
|
|
"dynamicRoutes": [
|
|
{
|
|
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/_sport/[slug]",
|
|
"regex": "^\\/_sport\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/_sport/(?<nxtPslug>[^/]+?)/test(?:/)?$",
|
|
"page": "/_sport/[slug]/test",
|
|
"regex": "^\\/_sport\\/([^\\/]+?)\\/test(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/another/(?<nxtPid>[^/]+?)(?:/)?$",
|
|
"page": "/another/[id]",
|
|
"regex": "^\\/another\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPid": "nxtPid",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/api/dynamic/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/api/dynamic/[slug]",
|
|
"regex": "^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/auto\\-export/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/auto-export/[slug]",
|
|
"regex": "^\\/auto\\-export\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/blog/(?<nxtPpost>[^/]+?)(?:/)?$",
|
|
"page": "/blog/[post]",
|
|
"regex": "^\\/blog\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPpost": "nxtPpost",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/blog\\-catchall/(?<nxtPslug>.+?)(?:/)?$",
|
|
"page": "/blog-catchall/[...slug]",
|
|
"regex": "^\\/blog\\-catchall\\/(.+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
{
|
|
"namedRegex": "^/overridden/(?<nxtPslug>[^/]+?)(?:/)?$",
|
|
"page": "/overridden/[slug]",
|
|
"regex": "^\\/overridden\\/([^\\/]+?)(?:\\/)?$",
|
|
"routeKeys": {
|
|
"nxtPslug": "nxtPslug",
|
|
},
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-new-header",
|
|
"value": "new-value",
|
|
},
|
|
],
|
|
"missing": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-headers-1(?:\\/)?$",
|
|
"source": "/missing-headers-1",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-new-header",
|
|
"value": "new-value",
|
|
},
|
|
],
|
|
"missing": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-headers-2(?:\\/)?$",
|
|
"source": "/missing-headers-2",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-new-header",
|
|
"value": "new-value",
|
|
},
|
|
],
|
|
"missing": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-headers-3(?:\\/)?$",
|
|
"source": "/missing-headers-3",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-custom-header",
|
|
"value": "hello world",
|
|
},
|
|
{
|
|
"key": "x-another-header",
|
|
"value": "hello again",
|
|
},
|
|
],
|
|
"regex": "^\\/add-header(?:\\/)?$",
|
|
"source": "/add-header",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-first-header",
|
|
"value": "first",
|
|
},
|
|
{
|
|
"key": "x-second-header",
|
|
"value": "second",
|
|
},
|
|
],
|
|
"regex": "^\\/my-headers(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/my-headers/(.*)",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-path",
|
|
"value": ":path",
|
|
},
|
|
{
|
|
"key": "some:path",
|
|
"value": "hi",
|
|
},
|
|
{
|
|
"key": "x-test",
|
|
"value": "some:value*",
|
|
},
|
|
{
|
|
"key": "x-test-2",
|
|
"value": "value*",
|
|
},
|
|
{
|
|
"key": "x-test-3",
|
|
"value": ":value?",
|
|
},
|
|
{
|
|
"key": "x-test-4",
|
|
"value": ":value+",
|
|
},
|
|
{
|
|
"key": "x-test-5",
|
|
"value": "something https:",
|
|
},
|
|
{
|
|
"key": "x-test-6",
|
|
"value": ":hello(world)",
|
|
},
|
|
{
|
|
"key": "x-test-7",
|
|
"value": "hello(world)",
|
|
},
|
|
{
|
|
"key": "x-test-8",
|
|
"value": "hello{1,}",
|
|
},
|
|
{
|
|
"key": "x-test-9",
|
|
"value": ":hello{1,2}",
|
|
},
|
|
{
|
|
"key": "content-security-policy",
|
|
"value": "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com/:path",
|
|
},
|
|
],
|
|
"regex": "^\\/my-other-header(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/my-other-header/:path",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-origin",
|
|
"value": "https://example.com",
|
|
},
|
|
],
|
|
"regex": "^\\/without-params\\/url(?:\\/)?$",
|
|
"source": "/without-params/url",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-url",
|
|
"value": "https://example.com/:path*",
|
|
},
|
|
],
|
|
"regex": "^\\/with-params\\/url(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/with-params/url/:path*",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-url",
|
|
"value": "https://example.com:8080?hello=:path*",
|
|
},
|
|
],
|
|
"regex": "^\\/with-params\\/url2(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/with-params/url2/:path*",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-something",
|
|
"value": "applied-everywhere",
|
|
},
|
|
],
|
|
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/:path*",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-something",
|
|
"value": "value=:path",
|
|
},
|
|
{
|
|
"key": "path-:path",
|
|
"value": "end",
|
|
},
|
|
],
|
|
"regex": "^\\/named-pattern(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/named-pattern/:path(.*)",
|
|
},
|
|
{
|
|
"headers": [
|
|
{
|
|
"key": "x-value",
|
|
"value": ":path*",
|
|
},
|
|
],
|
|
"regex": "^\\/catchall-header(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-header/:path*",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-another",
|
|
"value": "header",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-1(?:\\/)?$",
|
|
"source": "/has-header-1",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-added",
|
|
"value": "value",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-2(?:\\/)?$",
|
|
"source": "/has-header-2",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "true",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-is-user",
|
|
"value": "yuuuup",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-3(?:\\/)?$",
|
|
"source": "/has-header-3",
|
|
},
|
|
{
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "example.com",
|
|
},
|
|
],
|
|
"headers": [
|
|
{
|
|
"key": "x-is-host",
|
|
"value": "yuuuup",
|
|
},
|
|
],
|
|
"regex": "^\\/has-header-4(?:\\/)?$",
|
|
"source": "/has-header-4",
|
|
},
|
|
],
|
|
"onMatchHeaders": [],
|
|
"pages404": true,
|
|
"redirects": [
|
|
{
|
|
"destination": "/:path+",
|
|
"internal": true,
|
|
"priority": true,
|
|
"regex": "^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$",
|
|
"source": "/:path+/",
|
|
"statusCode": 308,
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/missing-redirect-1(?:\\/)?$",
|
|
"source": "/missing-redirect-1",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/missing-redirect-2(?:\\/)?$",
|
|
"source": "/missing-redirect-2",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/with-params?authorized=1",
|
|
"missing": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/missing-redirect-3(?:\\/)?$",
|
|
"source": "/missing-redirect-3",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/:lang/about",
|
|
"regex": "^(?!\\/_next)\\/redirect\\/me\\/to-about(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/redirect/me/to-about/:lang",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/docs/v2/network/status-codes#:code",
|
|
"regex": "^(?!\\/_next)\\/docs\\/router-status(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/docs/router-status/:code",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/docs/v2/advanced/now-for-github",
|
|
"regex": "^(?!\\/_next)\\/docs\\/github(?:\\/)?$",
|
|
"source": "/docs/github",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/docs/v2/more/:all",
|
|
"regex": "^(?!\\/_next)\\/docs\\/v2\\/advanced(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/docs/v2/advanced/:all(.*)",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/blog/:id",
|
|
"regex": "^(?!\\/_next)\\/hello(?:\\/([^\\/]+?))\\/another(?:\\/)?$",
|
|
"source": "/hello/:id/another",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redirect1(?:\\/)?$",
|
|
"source": "/redirect1",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redirect2(?:\\/)?$",
|
|
"source": "/redirect2",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/another",
|
|
"regex": "^(?!\\/_next)\\/redirect3(?:\\/)?$",
|
|
"source": "/redirect3",
|
|
"statusCode": 302,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redirect4(?:\\/)?$",
|
|
"source": "/redirect4",
|
|
"statusCode": 308,
|
|
},
|
|
{
|
|
"destination": "/redir-chain2",
|
|
"regex": "^(?!\\/_next)\\/redir-chain1(?:\\/)?$",
|
|
"source": "/redir-chain1",
|
|
"statusCode": 301,
|
|
},
|
|
{
|
|
"destination": "/redir-chain3",
|
|
"regex": "^(?!\\/_next)\\/redir-chain2(?:\\/)?$",
|
|
"source": "/redir-chain2",
|
|
"statusCode": 302,
|
|
},
|
|
{
|
|
"destination": "/",
|
|
"regex": "^(?!\\/_next)\\/redir-chain3(?:\\/)?$",
|
|
"source": "/redir-chain3",
|
|
"statusCode": 303,
|
|
},
|
|
{
|
|
"destination": "https://google.com",
|
|
"regex": "^(?!\\/_next)\\/to-external(?:\\/)?$",
|
|
"source": "/to-external",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/with-params?first=:section&second=:name",
|
|
"regex": "^(?!\\/_next)\\/query-redirect(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/query-redirect/:section/:name",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/got-unnamed",
|
|
"regex": "^(?!\\/_next)\\/unnamed(?:\\/(first|second))(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/unnamed/(first|second)/(.*)",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/:0",
|
|
"regex": "^(?!\\/_next)\\/named-like-unnamed(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/named-like-unnamed/:0",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/thank-you-next",
|
|
"regex": "^(?!\\/_next)\\/redirect-override(?:\\/)?$",
|
|
"source": "/redirect-override",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/:first/:second",
|
|
"regex": "^(?!\\/_next)\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)(?:\\/)?$",
|
|
"source": "/docs/:first(integrations|now-cli)/v2:second(.*)",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/somewhere",
|
|
"regex": "^(?!\\/_next)\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-redirect/:path*",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "https://authserver.example.com/set-password?returnUrl=https%3A%2F%2Fwww.example.com/login",
|
|
"regex": "^(?!\\/_next)\\/to-external-with-query(?:\\/)?$",
|
|
"source": "/to-external-with-query",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "https://authserver.example.com/set-password?returnUrl=https://www.example.com/login",
|
|
"regex": "^(?!\\/_next)\\/to-external-with-query-2(?:\\/)?$",
|
|
"source": "/to-external-with-query-2",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?myHeader=:myHeader",
|
|
"has": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-1(?:\\/)?$",
|
|
"source": "/has-redirect-1",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?value=:myquery",
|
|
"has": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-2(?:\\/)?$",
|
|
"source": "/has-redirect-2",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?authorized=1",
|
|
"has": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "true",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-3(?:\\/)?$",
|
|
"source": "/has-redirect-3",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/another?host=1",
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "example.com",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-4(?:\\/)?$",
|
|
"source": "/has-redirect-4",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/somewhere",
|
|
"has": [
|
|
{
|
|
"key": "x-test-next",
|
|
"type": "header",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)(?:\\/([^\\/]+?))\\/has-redirect-5(?:\\/)?$",
|
|
"source": "/:path/has-redirect-5",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "https://:subdomain.example.com/some-path/end?a=b",
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "(?<subdomain>.*)-test.example.com",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-6(?:\\/)?$",
|
|
"source": "/has-redirect-6",
|
|
"statusCode": 307,
|
|
},
|
|
{
|
|
"destination": "/somewhere?value=:hello",
|
|
"has": [
|
|
{
|
|
"key": "hello",
|
|
"type": "query",
|
|
"value": "(?<hello>.*)",
|
|
},
|
|
],
|
|
"regex": "^(?!\\/_next)\\/has-redirect-7(?:\\/)?$",
|
|
"source": "/has-redirect-7",
|
|
"statusCode": 307,
|
|
},
|
|
],
|
|
"rewriteHeaders": {
|
|
"pathHeader": "x-nextjs-rewritten-path",
|
|
"queryHeader": "x-nextjs-rewritten-query",
|
|
},
|
|
"rewrites": {
|
|
"afterFiles": [
|
|
{
|
|
"destination": "http://localhost:EXTERNAL_SERVER_PORT/_next/hmr?page=/about",
|
|
"regex": "^\\/to-websocket(?:\\/)?$",
|
|
"source": "/to-websocket",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/websocket-to-page(?:\\/)?$",
|
|
"source": "/websocket-to-page",
|
|
},
|
|
{
|
|
"destination": "http://localhost:12233",
|
|
"regex": "^\\/to-nowhere(?:\\/)?$",
|
|
"source": "/to-nowhere",
|
|
},
|
|
{
|
|
"destination": "/auto-export/hello?rewrite=1",
|
|
"regex": "^\\/rewriting-to-auto-export(?:\\/)?$",
|
|
"source": "/rewriting-to-auto-export",
|
|
},
|
|
{
|
|
"destination": "/auto-export/another?rewrite=1",
|
|
"regex": "^\\/rewriting-to-another-auto-export(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/rewriting-to-another-auto-export/:path*",
|
|
},
|
|
{
|
|
"destination": "/another/one",
|
|
"regex": "^\\/to-another(?:\\/)?$",
|
|
"source": "/to-another",
|
|
},
|
|
{
|
|
"destination": "/404",
|
|
"regex": "^\\/nav(?:\\/)?$",
|
|
"source": "/nav",
|
|
},
|
|
{
|
|
"destination": "/static/hello.txt",
|
|
"regex": "^\\/hello-world(?:\\/)?$",
|
|
"source": "/hello-world",
|
|
},
|
|
{
|
|
"destination": "/another",
|
|
"regex": "^\\/(?:\\/)?$",
|
|
"source": "/",
|
|
},
|
|
{
|
|
"destination": "/multi-rewrites",
|
|
"regex": "^\\/another(?:\\/)?$",
|
|
"source": "/another",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/first(?:\\/)?$",
|
|
"source": "/first",
|
|
},
|
|
{
|
|
"destination": "/hello-again",
|
|
"regex": "^\\/second(?:\\/)?$",
|
|
"source": "/second",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/to-hello(?:\\/)?$",
|
|
"source": "/to-hello",
|
|
},
|
|
{
|
|
"destination": "/blog/post-2",
|
|
"regex": "^\\/blog\\/post-1(?:\\/)?$",
|
|
"source": "/blog/post-1",
|
|
},
|
|
{
|
|
"destination": "/:path",
|
|
"regex": "^\\/test(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/test/:path",
|
|
},
|
|
{
|
|
"destination": "/params/this-should-be-the-value",
|
|
"regex": "^\\/test-overwrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/test-overwrite/:something/:another",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^\\/params(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/params/:something",
|
|
},
|
|
{
|
|
"destination": "/with-params?first=:section&second=:name",
|
|
"regex": "^\\/query-rewrite(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/query-rewrite/:section/:name",
|
|
},
|
|
{
|
|
"destination": "/_next/:path*",
|
|
"regex": "^\\/hidden\\/_next(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/hidden/_next/:path*",
|
|
},
|
|
{
|
|
"destination": "http://localhost:EXTERNAL_SERVER_PORT/:path*",
|
|
"regex": "^\\/proxy-me(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/proxy-me/:path*",
|
|
},
|
|
{
|
|
"destination": "/api/hello",
|
|
"regex": "^\\/api-hello(?:\\/)?$",
|
|
"source": "/api-hello",
|
|
},
|
|
{
|
|
"destination": "/api/hello?name=:first*",
|
|
"regex": "^\\/api-hello-regex(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/api-hello-regex/:first(.*)",
|
|
},
|
|
{
|
|
"destination": "/api/hello?hello=:name",
|
|
"regex": "^\\/api-hello-param(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/api-hello-param/:name",
|
|
},
|
|
{
|
|
"destination": "/api/dynamic/:name?hello=:name",
|
|
"regex": "^\\/api-dynamic-param(?:\\/([^\\/]+?))(?:\\/)?$",
|
|
"source": "/api-dynamic-param/:name",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^(?:\\/([^\\/]+?))\\/post-321(?:\\/)?$",
|
|
"source": "/:path/post-321",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))(?:\\/)?$",
|
|
"source": "/unnamed-params/nested/(.*)/:test/(.*)",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"regex": "^\\/catchall-rewrite(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-rewrite/:path*",
|
|
},
|
|
{
|
|
"destination": "/with-params?another=:path*",
|
|
"regex": "^\\/catchall-query(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/catchall-query/:path*",
|
|
},
|
|
{
|
|
"destination": "/with-params?myHeader=:myHeader",
|
|
"has": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-1(?:\\/)?$",
|
|
"source": "/has-rewrite-1",
|
|
},
|
|
{
|
|
"destination": "/with-params?value=:myquery",
|
|
"has": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-2(?:\\/)?$",
|
|
"source": "/has-rewrite-2",
|
|
},
|
|
{
|
|
"destination": "/with-params?authorized=1",
|
|
"has": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-3(?:\\/)?$",
|
|
"source": "/has-rewrite-3",
|
|
},
|
|
{
|
|
"destination": "/with-params?host=1",
|
|
"has": [
|
|
{
|
|
"type": "host",
|
|
"value": "example.com",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-4(?:\\/)?$",
|
|
"source": "/has-rewrite-4",
|
|
},
|
|
{
|
|
"destination": "/:hasParam",
|
|
"has": [
|
|
{
|
|
"key": "hasParam",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-5(?:\\/)?$",
|
|
"source": "/has-rewrite-5",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"has": [
|
|
{
|
|
"key": "hasParam",
|
|
"type": "header",
|
|
"value": "with-params",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-6(?:\\/)?$",
|
|
"source": "/has-rewrite-6",
|
|
},
|
|
{
|
|
"destination": "/with-params?idk=:idk",
|
|
"has": [
|
|
{
|
|
"key": "hasParam",
|
|
"type": "query",
|
|
"value": "(?<idk>with-params|hello)",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-7(?:\\/)?$",
|
|
"source": "/has-rewrite-7",
|
|
},
|
|
{
|
|
"destination": "/blog-catchall/:post",
|
|
"has": [
|
|
{
|
|
"key": "post",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/has-rewrite-8(?:\\/)?$",
|
|
"source": "/has-rewrite-8",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "x-my-header",
|
|
"type": "header",
|
|
"value": "(?<myHeader>.*)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-rewrite-1(?:\\/)?$",
|
|
"source": "/missing-rewrite-1",
|
|
},
|
|
{
|
|
"destination": "/with-params",
|
|
"missing": [
|
|
{
|
|
"key": "my-query",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-rewrite-2(?:\\/)?$",
|
|
"source": "/missing-rewrite-2",
|
|
},
|
|
{
|
|
"destination": "/with-params?authorized=1",
|
|
"missing": [
|
|
{
|
|
"key": "loggedIn",
|
|
"type": "cookie",
|
|
"value": "(?<loggedIn>true)",
|
|
},
|
|
],
|
|
"regex": "^\\/missing-rewrite-3(?:\\/)?$",
|
|
"source": "/missing-rewrite-3",
|
|
},
|
|
{
|
|
"destination": "/hello",
|
|
"regex": "^\\/blog\\/about(?:\\/)?$",
|
|
"source": "/blog/about",
|
|
},
|
|
{
|
|
"destination": "/overridden",
|
|
"regex": "^\\/overridden(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/overridden/:path*",
|
|
},
|
|
],
|
|
"beforeFiles": [
|
|
{
|
|
"destination": "/with-params?overridden=1",
|
|
"has": [
|
|
{
|
|
"key": "overrideMe",
|
|
"type": "query",
|
|
},
|
|
],
|
|
"regex": "^\\/hello(?:\\/)?$",
|
|
"source": "/hello",
|
|
},
|
|
{
|
|
"destination": "/blog/:path*",
|
|
"regex": "^\\/old-blog(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/old-blog/:path*",
|
|
},
|
|
{
|
|
"destination": "https://example.vercel.sh",
|
|
"regex": "^\\/overridden(?:\\/)?$",
|
|
"source": "/overridden",
|
|
},
|
|
{
|
|
"destination": "/_sport/nfl/:path*",
|
|
"regex": "^\\/nfl(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?(?:\\/)?$",
|
|
"source": "/nfl/:path*",
|
|
},
|
|
],
|
|
"fallback": [],
|
|
},
|
|
"rsc": {
|
|
"clientParamParsing": false,
|
|
"contentTypeHeader": "text/x-component",
|
|
"didPostponeHeader": "x-nextjs-postponed",
|
|
"dynamicRSCPrerender": false,
|
|
"header": "rsc",
|
|
"prefetchHeader": "next-router-prefetch",
|
|
"prefetchSegmentDirSuffix": ".segments",
|
|
"prefetchSegmentHeader": "next-router-segment-prefetch",
|
|
"prefetchSegmentSuffix": ".segment.rsc",
|
|
"suffix": ".rsc",
|
|
"varyHeader": "rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch",
|
|
},
|
|
"staticRoutes": [
|
|
{
|
|
"namedRegex": "^/api/hello(?:/)?$",
|
|
"page": "/api/hello",
|
|
"regex": "^/api/hello(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/auto\\-export/another(?:/)?$",
|
|
"page": "/auto-export/another",
|
|
"regex": "^/auto\\-export/another(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
|
|
"page": "/docs/v2/more/now-for-github",
|
|
"regex": "^/docs/v2/more/now\\-for\\-github(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/hello(?:/)?$",
|
|
"page": "/hello",
|
|
"regex": "^/hello(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/hello\\-again(?:/)?$",
|
|
"page": "/hello-again",
|
|
"regex": "^/hello\\-again(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/multi\\-rewrites(?:/)?$",
|
|
"page": "/multi-rewrites",
|
|
"regex": "^/multi\\-rewrites(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/nav(?:/)?$",
|
|
"page": "/nav",
|
|
"regex": "^/nav(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/overridden(?:/)?$",
|
|
"page": "/overridden",
|
|
"regex": "^/overridden(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/redirect\\-override(?:/)?$",
|
|
"page": "/redirect-override",
|
|
"regex": "^/redirect\\-override(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
{
|
|
"namedRegex": "^/with\\-params(?:/)?$",
|
|
"page": "/with-params",
|
|
"regex": "^/with\\-params(?:/)?$",
|
|
"routeKeys": {},
|
|
},
|
|
],
|
|
"version": 3,
|
|
}
|
|
`)
|
|
}
|
|
})
|
|
|
|
it('should have redirects/rewrites in build output with debug flag', async () => {
|
|
const manifest = JSON.parse(
|
|
await next.readFile('.next/routes-manifest.json')
|
|
)
|
|
const cleanOutput = stripAnsi(buildCliOutput)
|
|
expect(cleanOutput).toContain('Redirects')
|
|
expect(cleanOutput).toContain('Rewrites')
|
|
expect(cleanOutput).toContain('Headers')
|
|
expect(cleanOutput).toMatch(/source.*?/i)
|
|
expect(cleanOutput).toMatch(/destination.*?/i)
|
|
|
|
for (const route of [
|
|
...manifest.redirects,
|
|
...manifest.rewrites.beforeFiles,
|
|
...manifest.rewrites.afterFiles,
|
|
...manifest.rewrites.fallback,
|
|
]) {
|
|
expect(cleanOutput).toContain(route.source)
|
|
expect(cleanOutput).toContain(route.destination)
|
|
}
|
|
|
|
for (const route of manifest.headers) {
|
|
expect(cleanOutput).toContain(route.source)
|
|
|
|
for (const header of route.headers) {
|
|
expect(cleanOutput).toContain(header.key)
|
|
expect(cleanOutput).toContain(header.value)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should not show warning for custom routes when not next export', async () => {
|
|
expect(next.cliOutput).not.toContain(
|
|
`rewrites, redirects, and headers are not applied when exporting your application detected`
|
|
)
|
|
})
|
|
}
|
|
})
|
|
describe('Custom routes no-op rewrite', () => {
|
|
const { next, isTurbopack, isNextStart, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipStart: true,
|
|
env: {
|
|
ADD_NOOP_REWRITE: 'true',
|
|
},
|
|
skipDeployment: true,
|
|
})
|
|
if (skipped) return
|
|
if (isTurbopack && isNextStart) {
|
|
it('skipped - not supported in turbopack build mode', () => {})
|
|
return
|
|
}
|
|
|
|
let externalServer: http.Server
|
|
|
|
beforeAll(async () => {
|
|
const port = await findPort()
|
|
externalServer = http.createServer((req, res) => {
|
|
res.end('external')
|
|
})
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onError = (error: Error) => {
|
|
reject(error)
|
|
}
|
|
externalServer.once('error', onError)
|
|
externalServer.listen(port, () => {
|
|
externalServer.off('error', onError)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
await next.patchFile('next.config.js', (content) =>
|
|
content.replace(/__EXTERNAL_PORT__/g, String(port))
|
|
)
|
|
|
|
if (!isNextDev) {
|
|
await next.build()
|
|
}
|
|
await next.start()
|
|
})
|
|
|
|
afterAll(() => {
|
|
externalServer.close()
|
|
})
|
|
|
|
it('should not error for no-op rewrite and auto export dynamic route', async () => {
|
|
const browser = await next.browser('/auto-export/my-slug')
|
|
await retry(async () => {
|
|
expect(
|
|
await browser.eval(() => document.documentElement.innerHTML)
|
|
).toMatch(/auto-export.*?my-slug/)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Custom routes solo types', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipStart: true,
|
|
disableAutoSkewProtection: true,
|
|
skipDeployment: true,
|
|
})
|
|
if (skipped) return
|
|
|
|
let externalServer: http.Server
|
|
let externalServerPort: number
|
|
|
|
beforeAll(async () => {
|
|
externalServerPort = await findPort()
|
|
externalServer = http.createServer((req, res) => {
|
|
res.end('external')
|
|
})
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onError = (error: Error) => {
|
|
reject(error)
|
|
}
|
|
externalServer.once('error', onError)
|
|
externalServer.listen(externalServerPort, () => {
|
|
externalServer.off('error', onError)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
await next.patchFile('next.config.js', (content) =>
|
|
content.replace(/__EXTERNAL_PORT__/g, String(externalServerPort))
|
|
)
|
|
})
|
|
|
|
afterAll(() => {
|
|
externalServer.close()
|
|
})
|
|
|
|
it('should work with just headers', async () => {
|
|
const content = await next.readFile('next.config.js')
|
|
await next.patchFile(
|
|
'next.config.js',
|
|
content.replace(/(async (?:redirects|rewrites))/g, '$1s')
|
|
)
|
|
try {
|
|
if (!isNextDev) {
|
|
await next.build()
|
|
}
|
|
await next.start()
|
|
|
|
const res = await next.fetch('/add-header')
|
|
const res2 = await next.fetch('/docs/github', { redirect: 'manual' })
|
|
const res3 = await next.fetch('/hello-world')
|
|
|
|
expect(res.headers.get('x-custom-header')).toBe('hello world')
|
|
expect(res.headers.get('x-another-header')).toBe('hello again')
|
|
|
|
expect(res2.status).toBe(404)
|
|
expect(res3.status).toBe(404)
|
|
} finally {
|
|
await next.stop()
|
|
await next.patchFile('next.config.js', content)
|
|
}
|
|
})
|
|
|
|
it('should work with just rewrites', async () => {
|
|
const content = await next.readFile('next.config.js')
|
|
await next.patchFile(
|
|
'next.config.js',
|
|
content.replace(/(async (?:redirects|headers))/g, '$1s')
|
|
)
|
|
try {
|
|
if (!isNextDev) {
|
|
await next.build()
|
|
}
|
|
await next.start()
|
|
|
|
const res = await next.fetch('/add-header')
|
|
const res2 = await next.fetch('/docs/github', { redirect: 'manual' })
|
|
const res3 = await next.fetch('/hello-world')
|
|
|
|
expect(res.headers.get('x-custom-header')).toBeFalsy()
|
|
expect(res.headers.get('x-another-header')).toBeFalsy()
|
|
|
|
expect(res2.status).toBe(404)
|
|
|
|
expect(res3.status).toBe(200)
|
|
expect(await res3.text()).toContain('hello world!')
|
|
} finally {
|
|
await next.stop()
|
|
await next.patchFile('next.config.js', content)
|
|
}
|
|
})
|
|
|
|
it('should work with just redirects', async () => {
|
|
const content = await next.readFile('next.config.js')
|
|
await next.patchFile(
|
|
'next.config.js',
|
|
content.replace(/(async (?:rewrites|headers))/g, '$1s')
|
|
)
|
|
try {
|
|
if (!isNextDev) {
|
|
await next.build()
|
|
}
|
|
await next.start()
|
|
|
|
const res = await next.fetch('/add-header')
|
|
const res2 = await next.fetch('/docs/github', { redirect: 'manual' })
|
|
const res3 = await next.fetch('/hello world')
|
|
|
|
expect(res.headers.get('x-custom-header')).toBeFalsy()
|
|
expect(res.headers.get('x-another-header')).toBeFalsy()
|
|
|
|
const { pathname } = new URL(res2.headers.get('location'), res.url)
|
|
expect(res2.status).toBe(301)
|
|
expect(pathname).toBe('/docs/v2/advanced/now-for-github')
|
|
|
|
expect(res3.status).toBe(404)
|
|
} finally {
|
|
await next.stop()
|
|
await next.patchFile('next.config.js', content)
|
|
}
|
|
})
|
|
})
|
|
;(isNextStart ? describe : describe.skip)('Custom routes export', () => {
|
|
const { next, isNextDeploy } = nextTestSetup({
|
|
files: __dirname,
|
|
skipStart: true,
|
|
skipDeployment: true,
|
|
})
|
|
if (isNextDeploy) return
|
|
|
|
it('should not show warning for custom routes when not next export', async () => {
|
|
await next.patchFile('next.config.js', (content) =>
|
|
content
|
|
.replace(/__EXTERNAL_PORT__/g, '12345')
|
|
.replace('// REPLACEME', `output: 'export',`)
|
|
)
|
|
const { cliOutput } = await next.build()
|
|
|
|
expect(cliOutput).not.toContain(
|
|
`rewrites, redirects, and headers are not applied when exporting your application detected`
|
|
)
|
|
})
|
|
})
|