mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 12:57:46 +00:00
[next][routing-utils] Add missing matcher support (#8874)
### Related Issues This adds handling to ensure we pass through the `missing` route field correctly for custom routes and middleware matchers. Tests are also added in the complimentary Next.js PR for this, example deployment can be seen here https://vtest314-e2e-tests-jj4-vtest314-next-e2e-tests.vercel.app/ x-ref: [slack thread](https://vercel.slack.com/archives/C03S8ED1DKM/p1667935428788529?thread_ts=1667850697.542269&cid=C03S8ED1DKM) x-ref: https://github.com/vercel/next.js/pull/42660 ### 📋 Checklist <!-- Please keep your PR as a Draft until the checklist is complete --> #### Tests - [ ] The code changed/added as part of this PR has been covered with tests - [ ] All tests pass locally with `yarn test-unit` #### Code Review - [ ] This PR has a concise title and thorough description useful to a reviewer - [ ] Issue from task tracker has a link to this PR
This commit is contained in:
@@ -2278,6 +2278,7 @@ interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
|
||||
interface EdgeFunctionMatcher {
|
||||
regexp: string;
|
||||
has?: HasField;
|
||||
missing?: HasField;
|
||||
}
|
||||
|
||||
export async function getMiddlewareBundle({
|
||||
@@ -2479,6 +2480,7 @@ export async function getMiddlewareBundle({
|
||||
key: 'x-prerender-revalidate',
|
||||
value: prerenderBypassToken,
|
||||
},
|
||||
...(matcher.missing || []),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2609,6 +2611,9 @@ function getRouteMatchers(
|
||||
if (matcher.has) {
|
||||
m.has = normalizeHas(matcher.has);
|
||||
}
|
||||
if (matcher.missing) {
|
||||
m.missing = normalizeHas(matcher.missing);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ export function convertRedirects(
|
||||
return redirects.map(r => {
|
||||
const { src, segments } = sourceToRegex(r.source);
|
||||
const hasSegments = collectHasSegments(r.has);
|
||||
normalizeHasKeys(r.has);
|
||||
normalizeHasKeys(r.missing);
|
||||
|
||||
try {
|
||||
const loc = replaceSegments(segments, hasSegments, r.destination, true);
|
||||
let status: number;
|
||||
@@ -70,6 +73,9 @@ export function convertRedirects(
|
||||
if (r.has) {
|
||||
route.has = r.has;
|
||||
}
|
||||
if (r.missing) {
|
||||
route.missing = r.missing;
|
||||
}
|
||||
return route;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse redirect: ${JSON.stringify(r)}`);
|
||||
@@ -84,6 +90,9 @@ export function convertRewrites(
|
||||
return rewrites.map(r => {
|
||||
const { src, segments } = sourceToRegex(r.source);
|
||||
const hasSegments = collectHasSegments(r.has);
|
||||
normalizeHasKeys(r.has);
|
||||
normalizeHasKeys(r.missing);
|
||||
|
||||
try {
|
||||
const dest = replaceSegments(
|
||||
segments,
|
||||
@@ -97,6 +106,9 @@ export function convertRewrites(
|
||||
if (r.has) {
|
||||
route.has = r.has;
|
||||
}
|
||||
if (r.missing) {
|
||||
route.missing = r.missing;
|
||||
}
|
||||
return route;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse rewrite: ${JSON.stringify(r)}`);
|
||||
@@ -109,6 +121,9 @@ export function convertHeaders(headers: Header[]): Route[] {
|
||||
const obj: { [key: string]: string } = {};
|
||||
const { src, segments } = sourceToRegex(h.source);
|
||||
const hasSegments = collectHasSegments(h.has);
|
||||
normalizeHasKeys(h.has);
|
||||
normalizeHasKeys(h.missing);
|
||||
|
||||
const namedSegments = segments.filter(name => name !== UN_NAMED_SEGMENT);
|
||||
const indexes: { [k: string]: string } = {};
|
||||
|
||||
@@ -140,6 +155,9 @@ export function convertHeaders(headers: Header[]): Route[] {
|
||||
if (h.has) {
|
||||
route.has = h.has;
|
||||
}
|
||||
if (h.missing) {
|
||||
route.missing = h.missing;
|
||||
}
|
||||
return route;
|
||||
});
|
||||
}
|
||||
@@ -193,14 +211,19 @@ export function sourceToRegex(source: string): {
|
||||
|
||||
const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g;
|
||||
|
||||
const normalizeHasKeys = (hasItems: HasField = []) => {
|
||||
for (const hasItem of hasItems) {
|
||||
if ('key' in hasItem && hasItem.type === 'header') {
|
||||
hasItem.key = hasItem.key.toLowerCase();
|
||||
}
|
||||
}
|
||||
return hasItems;
|
||||
};
|
||||
|
||||
export function collectHasSegments(has?: HasField) {
|
||||
const hasSegments = new Set<string>();
|
||||
|
||||
for (const hasItem of has || []) {
|
||||
if ('key' in hasItem && hasItem.type === 'header') {
|
||||
hasItem.key = hasItem.key.toLowerCase();
|
||||
}
|
||||
|
||||
if (!hasItem.value && 'key' in hasItem) {
|
||||
hasSegments.add(hasItem.key);
|
||||
}
|
||||
|
||||
148
packages/routing-utils/test/superstatic.spec.ts
vendored
148
packages/routing-utils/test/superstatic.spec.ts
vendored
@@ -224,6 +224,17 @@ test('convertRedirects', () => {
|
||||
],
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/hello/:first',
|
||||
destination: '/another',
|
||||
missing: [
|
||||
{
|
||||
type: 'host',
|
||||
value: '(?<a>.*)\\.(?<b>.*)',
|
||||
},
|
||||
],
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/hello/:first',
|
||||
destination:
|
||||
@@ -384,6 +395,19 @@ test('convertRedirects', () => {
|
||||
src: '^\\/hello(?:\\/([^\\/]+?))$',
|
||||
status: 307,
|
||||
},
|
||||
{
|
||||
missing: [
|
||||
{
|
||||
type: 'host',
|
||||
value: '(?<a>.*)\\.(?<b>.*)',
|
||||
},
|
||||
],
|
||||
headers: {
|
||||
Location: '/another',
|
||||
},
|
||||
src: '^\\/hello(?:\\/([^\\/]+?))$',
|
||||
status: 307,
|
||||
},
|
||||
{
|
||||
has: [
|
||||
{
|
||||
@@ -453,6 +477,7 @@ test('convertRedirects', () => {
|
||||
['/hello/world', '/hello/again'],
|
||||
['/hello/world'],
|
||||
['/hello/world'],
|
||||
['/hello/world'],
|
||||
];
|
||||
|
||||
const mustNotMatch = [
|
||||
@@ -474,6 +499,7 @@ test('convertRedirects', () => {
|
||||
['/feature/first', '/feature'],
|
||||
['/hello', '/hello/another/one'],
|
||||
['/hellooo'],
|
||||
['/hellooo'],
|
||||
['/helloooo'],
|
||||
];
|
||||
|
||||
@@ -586,6 +612,48 @@ test('convertRewrites', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/hello/:first',
|
||||
destination: '/another',
|
||||
missing: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'x-rewrite',
|
||||
},
|
||||
{
|
||||
type: 'cookie',
|
||||
key: 'loggedIn',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
type: 'host',
|
||||
value: 'vercel.com',
|
||||
},
|
||||
{
|
||||
type: 'host',
|
||||
value: '(?<a>.*)\\.(?<b>.*)',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
key: 'host',
|
||||
value: '(?<c>.*)\\.(?<d>.*)',
|
||||
},
|
||||
{
|
||||
type: 'query',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
key: 'x-pathname',
|
||||
value: '(?<pathname>.*)',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
key: 'X-Pathname',
|
||||
value: '(?<another>hello|world)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/array-query-string/:id/:name',
|
||||
destination: 'https://example.com/?tag=1&tag=2',
|
||||
@@ -740,6 +808,49 @@ test('convertRewrites', () => {
|
||||
],
|
||||
src: '^\\/hello(?:\\/([^\\/]+?))$',
|
||||
},
|
||||
{
|
||||
check: true,
|
||||
dest: '/another?first=$1',
|
||||
missing: [
|
||||
{
|
||||
key: 'x-rewrite',
|
||||
type: 'header',
|
||||
},
|
||||
{
|
||||
key: 'loggedIn',
|
||||
type: 'cookie',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
type: 'host',
|
||||
value: 'vercel.com',
|
||||
},
|
||||
{
|
||||
type: 'host',
|
||||
value: '(?<a>.*)\\.(?<b>.*)',
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
type: 'header',
|
||||
value: '(?<c>.*)\\.(?<d>.*)',
|
||||
},
|
||||
{
|
||||
type: 'query',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
key: 'x-pathname',
|
||||
value: '(?<pathname>.*)',
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
key: 'x-pathname',
|
||||
value: '(?<another>hello|world)',
|
||||
},
|
||||
],
|
||||
src: '^\\/hello(?:\\/([^\\/]+?))$',
|
||||
},
|
||||
{
|
||||
src: '^\\/array-query-string(?:\\/([^\\/]+?))(?:\\/([^\\/]+?))$',
|
||||
dest: 'https://example.com/?tag=1&tag=2&id=$1&name=$2',
|
||||
@@ -776,6 +887,7 @@ test('convertRewrites', () => {
|
||||
['/hello/world', '/hello/again'],
|
||||
['/hello/world'],
|
||||
['/hello/world'],
|
||||
['/hello/world'],
|
||||
['/array-query-string/10/email'],
|
||||
['/en/hello'],
|
||||
];
|
||||
@@ -802,6 +914,7 @@ test('convertRewrites', () => {
|
||||
['/hello', '/hello/another/one'],
|
||||
['/hllooo'],
|
||||
['/hllooo'],
|
||||
['/hllooo'],
|
||||
['/array-query-string/10'],
|
||||
['/en/hello/world', '/en/hello/'],
|
||||
];
|
||||
@@ -919,6 +1032,25 @@ test('convertHeaders', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/hello/:first',
|
||||
missing: [
|
||||
{
|
||||
type: 'host',
|
||||
value: '(?<a>.*)\\.(?<b>.*)',
|
||||
},
|
||||
],
|
||||
headers: [
|
||||
{
|
||||
key: 'x-a',
|
||||
value: 'a',
|
||||
},
|
||||
{
|
||||
key: 'x-b',
|
||||
value: 'b',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/hello/:first',
|
||||
has: [
|
||||
@@ -1057,6 +1189,20 @@ test('convertHeaders', () => {
|
||||
},
|
||||
src: '^\\/hello(?:\\/([^\\/]+?))$',
|
||||
},
|
||||
{
|
||||
continue: true,
|
||||
missing: [
|
||||
{
|
||||
type: 'host',
|
||||
value: '(?<a>.*)\\.(?<b>.*)',
|
||||
},
|
||||
],
|
||||
headers: {
|
||||
'x-a': 'a',
|
||||
'x-b': 'b',
|
||||
},
|
||||
src: '^\\/hello(?:\\/([^\\/]+?))$',
|
||||
},
|
||||
{
|
||||
continue: true,
|
||||
has: [
|
||||
@@ -1133,6 +1279,7 @@ test('convertHeaders', () => {
|
||||
['/like/params/first', '/like/params/second'],
|
||||
['/hello/world'],
|
||||
['/hello/world'],
|
||||
['/hello/world'],
|
||||
['/hello'],
|
||||
];
|
||||
|
||||
@@ -1143,6 +1290,7 @@ test('convertHeaders', () => {
|
||||
['/non-match', '/like/params', '/like/params/'],
|
||||
['/hellooo'],
|
||||
['/hellooo'],
|
||||
['/hellooo'],
|
||||
[],
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user