Skip to content

Commit

Permalink
Add missing matcher support
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk committed Nov 9, 2022
1 parent b9c7408 commit 0381cf0
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 140 deletions.
1 change: 1 addition & 0 deletions packages/next/build/analysis/get-page-static-info.ts
Expand Up @@ -25,6 +25,7 @@ export interface MiddlewareMatcher {
regexp: string
locale?: false
has?: RouteHas[]
missing?: RouteHas[]
}

export interface PageStaticInfo {
Expand Down
Expand Up @@ -157,8 +157,13 @@ export function getUtils({
)
let params = matcher(parsedUrl.pathname)

if (rewrite.has && params) {
const hasParams = matchHas(req, rewrite.has, parsedUrl.query)
if ((rewrite.has || rewrite.missing) && params) {
const hasParams = matchHas(
req,
parsedUrl.query,
rewrite.has,
rewrite.missing
)

if (hasParams) {
Object.assign(params, hasParams)
Expand Down
107 changes: 71 additions & 36 deletions packages/next/lib/load-custom-routes.ts
Expand Up @@ -24,6 +24,7 @@ export type Rewrite = {
basePath?: false
locale?: false
has?: RouteHas[]
missing?: RouteHas[]
}

export type Header = {
Expand All @@ -32,6 +33,7 @@ export type Header = {
locale?: false
headers: Array<{ key: string; value: string }>
has?: RouteHas[]
missing?: RouteHas[]
}

// internal type used for validation (not user facing)
Expand All @@ -41,6 +43,7 @@ export type Redirect = {
basePath?: false
locale?: false
has?: RouteHas[]
missing?: RouteHas[]
} & (
| {
statusCode?: never
Expand All @@ -56,6 +59,7 @@ export type Middleware = {
source: string
locale?: false
has?: RouteHas[]
missing?: RouteHas[]
}

const allowedHasTypes = new Set(['header', 'cookie', 'query', 'host'])
Expand Down Expand Up @@ -132,8 +136,9 @@ export function checkCustomRoutes(
let numInvalidRoutes = 0
let hadInvalidStatus = false
let hadInvalidHas = false
let hadInvalidMissing = false

const allowedKeys = new Set<string>(['source', 'locale', 'has'])
const allowedKeys = new Set<string>(['source', 'locale', 'has', 'missing'])

if (type === 'rewrite') {
allowedKeys.add('basePath')
Expand Down Expand Up @@ -198,48 +203,65 @@ export function checkCustomRoutes(
invalidParts.push('`locale` must be undefined or false')
}

if (typeof route.has !== 'undefined' && !Array.isArray(route.has)) {
invalidParts.push('`has` must be undefined or valid has object')
hadInvalidHas = true
} else if (route.has) {
const invalidHasItems = []
const checkInvalidHasMissing = (
items: any,
fieldName: 'has' | 'missing'
) => {
let hadInvalidItem = false

for (const hasItem of route.has) {
let invalidHasParts = []
if (typeof items !== 'undefined' && !Array.isArray(items)) {
invalidParts.push(
`\`${fieldName}\` must be undefined or valid has object`
)
hadInvalidItem = true
} else if (items) {
const invalidHasItems = []

if (!allowedHasTypes.has(hasItem.type)) {
invalidHasParts.push(`invalid type "${hasItem.type}"`)
}
if (typeof hasItem.key !== 'string' && hasItem.type !== 'host') {
invalidHasParts.push(`invalid key "${hasItem.key}"`)
}
if (
typeof hasItem.value !== 'undefined' &&
typeof hasItem.value !== 'string'
) {
invalidHasParts.push(`invalid value "${hasItem.value}"`)
}
if (typeof hasItem.value === 'undefined' && hasItem.type === 'host') {
invalidHasParts.push(`value is required for "host" type`)
}
for (const hasItem of items) {
let invalidHasParts = []

if (invalidHasParts.length > 0) {
invalidHasItems.push(
`${invalidHasParts.join(', ')} for ${JSON.stringify(hasItem)}`
)
if (!allowedHasTypes.has(hasItem.type)) {
invalidHasParts.push(`invalid type "${hasItem.type}"`)
}
if (typeof hasItem.key !== 'string' && hasItem.type !== 'host') {
invalidHasParts.push(`invalid key "${hasItem.key}"`)
}
if (
typeof hasItem.value !== 'undefined' &&
typeof hasItem.value !== 'string'
) {
invalidHasParts.push(`invalid value "${hasItem.value}"`)
}
if (typeof hasItem.value === 'undefined' && hasItem.type === 'host') {
invalidHasParts.push(`value is required for "host" type`)
}

if (invalidHasParts.length > 0) {
invalidHasItems.push(
`${invalidHasParts.join(', ')} for ${JSON.stringify(hasItem)}`
)
}
}
}

if (invalidHasItems.length > 0) {
hadInvalidHas = true
const itemStr = `item${invalidHasItems.length === 1 ? '' : 's'}`
if (invalidHasItems.length > 0) {
hadInvalidItem = true
const itemStr = `item${invalidHasItems.length === 1 ? '' : 's'}`

console.error(
`Invalid \`has\` ${itemStr}:\n` + invalidHasItems.join('\n')
)
console.error()
invalidParts.push(`invalid \`has\` ${itemStr} found`)
console.error(
`Invalid \`${fieldName}\` ${itemStr}:\n` +
invalidHasItems.join('\n')
)
console.error()
invalidParts.push(`invalid \`${fieldName}\` ${itemStr} found`)
}
}
return hadInvalidItem
}
if (checkInvalidHasMissing(route.has, 'has')) {
hadInvalidHas = true
}
if (checkInvalidHasMissing(route.missing, 'missing')) {
hadInvalidMissing = true
}

if (!route.source) {
Expand Down Expand Up @@ -421,6 +443,19 @@ export function checkCustomRoutes(
)}`
)
}
if (hadInvalidMissing) {
console.error(
`\nValid \`missing\` object shape is ${JSON.stringify(
{
type: [...allowedHasTypes].join(', '),
key: 'the key to check for',
value: 'undefined or a value string to match against',
},
null,
2
)}`
)
}
console.error()
console.error(
`Error: Invalid ${type}${numInvalidRoutes === 1 ? '' : 's'} found`
Expand Down
10 changes: 8 additions & 2 deletions packages/next/server/router.ts
Expand Up @@ -30,6 +30,7 @@ type RouteResult = {
export type Route = {
match: RouteMatch
has?: RouteHas[]
missing?: RouteHas[]
type: string
check?: boolean
statusCode?: number
Expand Down Expand Up @@ -416,8 +417,13 @@ export default class Router {
})

let params = route.match(matchPathname)
if (route.has && params) {
const hasParams = matchHas(req, route.has, parsedUrlUpdated.query)
if ((route.has || route.missing) && params) {
const hasParams = matchHas(
req,
parsedUrlUpdated.query,
route.has,
route.missing
)
if (hasParams) {
Object.assign(params, hasParams)
} else {
Expand Down
Expand Up @@ -25,8 +25,8 @@ export function getMiddlewareRouteMatcher(
continue
}

if (matcher.has) {
const hasParams = matchHas(req, matcher.has, query)
if (matcher.has || matcher.missing) {
const hasParams = matchHas(req, query, matcher.has, matcher.missing)
if (!hasParams) {
continue
}
Expand Down
13 changes: 9 additions & 4 deletions packages/next/shared/lib/router/utils/prepare-destination.ts
Expand Up @@ -42,12 +42,13 @@ function unescapeSegments(str: string) {

export function matchHas(
req: BaseNextRequest | IncomingMessage,
has: RouteHas[],
query: Params
query: Params,
has: RouteHas[] = [],
missing: RouteHas[] = []
): false | Params {
const params: Params = {}

const allMatch = has.every((hasItem) => {
const hasMatch = (hasItem: RouteHas) => {
let value: undefined | string
let key = hasItem.key

Expand Down Expand Up @@ -100,7 +101,11 @@ export function matchHas(
}
}
return false
})
}

const allMatch =
has.every((item) => hasMatch(item)) &&
!missing.some((item) => hasMatch(item))

if (allMatch) {
return params
Expand Down
5 changes: 3 additions & 2 deletions packages/next/shared/lib/router/utils/resolve-rewrites.ts
Expand Up @@ -44,7 +44,7 @@ export default function resolveRewrites(

let params = matcher(parsedAs.pathname)

if (rewrite.has && params) {
if ((rewrite.has || rewrite.missing) && params) {
const hasParams = matchHas(
{
headers: {
Expand All @@ -58,8 +58,9 @@ export default function resolveRewrites(
return acc
}, {}),
} as any,
parsedAs.query,
rewrite.has,
parsedAs.query
rewrite.missing
)

if (hasParams) {
Expand Down
20 changes: 20 additions & 0 deletions test/e2e/middleware-custom-matchers/app/middleware.js
Expand Up @@ -57,5 +57,25 @@ export const config = {
},
],
},
{
source: '/missing-match-1',
missing: [
{
type: 'header',
key: 'hello',
value: '(.*)',
},
],
},
{
source: '/missing-match-2',
missing: [
{
type: 'query',
key: 'test',
value: 'value',
},
],
},
],
}
22 changes: 22 additions & 0 deletions test/e2e/middleware-custom-matchers/test/index.test.ts
Expand Up @@ -21,6 +21,28 @@ describe('Middleware custom matchers', () => {
afterAll(() => next.destroy())

const runTests = () => {
it('should match missing header correctly', async () => {
const res = await fetchViaHTTP(next.url, '/missing-match-1')
expect(res.headers.get('x-from-middleware')).toBeDefined()

const res2 = await fetchViaHTTP(next.url, '/missing-match-1', undefined, {
headers: {
hello: 'world',
},
})
expect(res2.headers.get('x-from-middleware')).toBeFalsy()
})

it('should match missing query correctly', async () => {
const res = await fetchViaHTTP(next.url, '/missing-match-2')
expect(res.headers.get('x-from-middleware')).toBeDefined()

const res2 = await fetchViaHTTP(next.url, '/missing-match-2', {
test: 'value',
})
expect(res2.headers.get('x-from-middleware')).toBeFalsy()
})

it('should match source path', async () => {
const res = await fetchViaHTTP(next.url, '/source-match')
expect(res.status).toBe(200)
Expand Down
33 changes: 32 additions & 1 deletion test/integration/custom-routes/next.config.js
@@ -1,5 +1,4 @@
module.exports = {
// target: 'serverless',
async rewrites() {
// no-rewrites comment
return {
Expand Down Expand Up @@ -205,6 +204,38 @@ module.exports = {
],
destination: '/blog-catchall/:post',
},
{
source: '/missing-rewrite-1',
missing: [
{
type: 'header',
key: 'x-my-header',
value: '(?<myHeader>.*)',
},
],
destination: '/with-params',
},
{
source: '/missing-rewrite-2',
missing: [
{
type: 'query',
key: 'my-query',
},
],
destination: '/with-params',
},
{
source: '/missing-rewrite-3',
missing: [
{
type: 'cookie',
key: 'loggedIn',
value: '(?<loggedIn>true)',
},
],
destination: '/with-params?authorized=1',
},
{
source: '/blog/about',
destination: '/hello',
Expand Down

0 comments on commit 0381cf0

Please sign in to comment.