Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[next] support middleware-manifest v2 #8319

Merged
merged 15 commits into from Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
168 changes: 120 additions & 48 deletions packages/next/src/utils.ts
Expand Up @@ -16,7 +16,13 @@ import {
EdgeFunction,
} from '@vercel/build-utils';
import { NodeFileTraceReasons } from '@vercel/nft';
import { Header, Rewrite, Route, RouteWithSrc } from '@vercel/routing-utils';
import type {
HasField,
Header,
Rewrite,
Route,
RouteWithSrc,
} from '@vercel/routing-utils';
import { Sema } from 'async-sema';
import crc32 from 'buffer-crc32';
import fs, { lstat, stat } from 'fs-extra';
Expand Down Expand Up @@ -2154,23 +2160,47 @@ export {
getSourceFilePathFromPage,
};

interface MiddlewareManifest {
type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2;

interface MiddlewareManifestV1 {
version: 1;
sortedMiddleware: string[];
middleware: { [page: string]: EdgeFunctionInfo };
functions?: { [page: string]: EdgeFunctionInfo };
middleware: { [page: string]: EdgeFunctionInfoV1 };
functions?: { [page: string]: EdgeFunctionInfoV1 };
}

interface MiddlewareManifestV2 {
version: 2;
sortedMiddleware: string[];
middleware: { [page: string]: EdgeFunctionInfoV2 };
functions?: { [page: string]: EdgeFunctionInfoV2 };
}
nkzawa marked this conversation as resolved.
Show resolved Hide resolved

interface EdgeFunctionInfo {
interface BaseEdgeFunctionInfo {
env: string[];
files: string[];
name: string;
page: string;
regexp: string;
wasm?: { filePath: string; name: string }[];
assets?: { filePath: string; name: string }[];
}

interface EdgeFunctionInfoV1 extends BaseEdgeFunctionInfo {
regexp: string;
}

interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
matchers: EdgeFunctionMatcher[];
}

interface EdgeFunctionMatcher {
regexp: string;
has?: HasField;
// TODO: these are not supported yet
basePath?: false;
locale?: false;
}

export async function getMiddlewareBundle({
entryPath,
outputDirectory,
Expand Down Expand Up @@ -2301,7 +2331,7 @@ export async function getMiddlewareBundle({
}),
});
})(),
routeSrc: getRouteSrc(edgeFunction, routesManifest),
routeMatchers: getRouteMatchers(edgeFunction, routesManifest),
};
} catch (e: any) {
e.message = `Can't build edge function ${key}: ${e.message}`;
Expand All @@ -2325,31 +2355,35 @@ export async function getMiddlewareBundle({
const shortPath = edgeFile.replace(/^pages\//, '');
worker.edgeFunction.name = shortPath;
source.edgeFunctions[shortPath] = worker.edgeFunction;
const route: Route = {
continue: true,
src: worker.routeSrc,
missing: [
{
type: 'header',
key: 'x-prerender-revalidate',
value: prerenderBypassToken,
},
],
};

if (worker.type === 'function') {
route.dest = shortPath;
} else {
route.middlewarePath = shortPath;
if (isCorrectMiddlewareOrder) {
route.override = true;
for (const matcher of worker.routeMatchers) {
const route: Route = {
continue: true,
src: matcher.regexp,
has: matcher.has,
missing: [
{
type: 'header',
key: 'x-prerender-revalidate',
value: prerenderBypassToken,
},
],
};

if (worker.type === 'function') {
route.dest = shortPath;
} else {
route.middlewarePath = shortPath;
if (isCorrectMiddlewareOrder) {
route.override = true;
}
}
}

if (routesManifest.version > 3 && isDynamicRoute(worker.page)) {
source.dynamicRouteMap.set(worker.page, route);
} else {
source.staticRoutes.push(route);
if (routesManifest.version > 3 && isDynamicRoute(worker.page)) {
source.dynamicRouteMap.set(worker.page, route);
} else {
source.staticRoutes.push(route);
}
}
}

Expand All @@ -2371,7 +2405,7 @@ export async function getMiddlewareBundle({
export async function getMiddlewareManifest(
entryPath: string,
outputDirectory: string
): Promise<MiddlewareManifest | undefined> {
): Promise<MiddlewareManifestV2 | undefined> {
const middlewareManifestPath = path.join(
entryPath,
outputDirectory,
Expand All @@ -2387,36 +2421,74 @@ export async function getMiddlewareManifest(
return;
}

return fs.readJSON(middlewareManifestPath);
const manifest = (await fs.readJSON(
middlewareManifestPath
)) as MiddlewareManifest;
return manifest.version === 1
? upgradeMiddlewareManifest(manifest)
nkzawa marked this conversation as resolved.
Show resolved Hide resolved
: manifest;
}

export function upgradeMiddlewareManifest(
v1: MiddlewareManifestV1
): MiddlewareManifestV2 {
function updateInfo(v1Info: EdgeFunctionInfoV1): EdgeFunctionInfoV2 {
const { regexp, ...rest } = v1Info;
return {
...rest,
matchers: [{ regexp }],
};
}

const middleware = Object.fromEntries(
Object.entries(v1.middleware).map(([p, info]) => [p, updateInfo(info)])
);
const functions = v1.functions
? Object.fromEntries(
Object.entries(v1.functions).map(([p, info]) => [p, updateInfo(info)])
)
: undefined;

return {
...v1,
version: 2,
middleware,
functions,
};
}

/**
* For an object containing middleware info and a routes manifest this will
* generate a string with the route that will activate the middleware on
* Vercel Proxy.
*
* @param param0 The middleware info including regexp and page.
* @param param0 The middleware info including matchers and page.
* @param param1 The routes manifest
* @returns A regexp string for the middleware route.
* @returns matchers for the middleware route.
*/
function getRouteSrc(
{ regexp, page }: EdgeFunctionInfo,
function getRouteMatchers(
info: EdgeFunctionInfoV2,
{ basePath = '', i18n }: RoutesManifest
): string {
if (page === '/') {
return regexp.replace(
'_next',
`${basePath?.substring(1) ? `${basePath?.substring(1)}/` : ''}_next`
);
}
): EdgeFunctionMatcher[] {
function getRegexp(regexp: string) {
if (info.page === '/') {
const base = basePath?.substring(1);
return regexp.replace('_next', `${base ? `${base}/` : ''}_next`);
}

const locale = i18n?.locales.length
? `(?:/(${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')}))?`
: '';
const locale = i18n?.locales.length
? `(?:/(${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')}))?`
: '';

return `(?:^${basePath}${locale}${regexp.substring(1)})`;
return `(?:^${basePath}${locale}${regexp.substring(1)})`;
}

return info.matchers.map(matcher => ({
...matcher,
regexp: getRegexp(matcher.regexp),
}));
}

/**
Expand Down
149 changes: 149 additions & 0 deletions packages/next/test/unit/middleware-routes.test.js
@@ -0,0 +1,149 @@
const { getMiddlewareBundle } = require('../../dist/utils');
const { genDir } = require('../utils');

describe('middleware routes', () => {
it('should generate a route for v1 middleware manifest', async () => {
const routes = await getMiddlewareRoutes({
version: 1,
sortedMiddleware: ['/'],
middleware: {
'/': {
env: [],
files: [],
name: 'middleware',
page: '/',
regexp: '^/.*$',
},
},
});
expect(routes).toEqual([
{
continue: true,
middlewarePath: 'middleware',
missing: [
{
key: 'x-prerender-revalidate',
type: 'header',
value: '',
},
],
override: true,
src: '^/.*$',
},
]);
});

it('should generate a route for v2 middleware manifest', async () => {
const routes = await getMiddlewareRoutes({
version: 2,
sortedMiddleware: ['/'],
middleware: {
'/': {
env: [],
files: [],
name: 'middleware',
page: '/',
matchers: [{ regexp: '^/.*$' }],
},
},
});
expect(routes).toEqual([
{
continue: true,
middlewarePath: 'middleware',
missing: [
{
key: 'x-prerender-revalidate',
type: 'header',
value: '',
},
],
override: true,
src: '^/.*$',
},
]);
});

it('should generate multiple routes for v2 middleware manifest', async () => {
const routes = await getMiddlewareRoutes({
version: 2,
sortedMiddleware: ['/'],
middleware: {
'/': {
env: [],
files: [],
name: 'middleware',
page: '/',
matchers: [
{ regexp: '^\\/foo[\\/#\\?]?$' },
{
regexp: '^\\/bar[\\/#\\?]?$',
has: [
{
type: 'header',
key: 'x-rewrite-me',
},
],
},
],
},
},
});
expect(routes).toEqual([
{
continue: true,
middlewarePath: 'middleware',
missing: [
{
key: 'x-prerender-revalidate',
type: 'header',
value: '',
},
],
override: true,
src: '^\\/foo[\\/#\\?]?$',
},
{
continue: true,
has: [
{
type: 'header',
key: 'x-rewrite-me',
},
],
middlewarePath: 'middleware',
missing: [
{
key: 'x-prerender-revalidate',
type: 'header',
value: '',
},
],
override: true,
src: '^\\/bar[\\/#\\?]?$',
},
]);
});
});

async function getMiddlewareRoutes(manifest) {
const dir = await genDir({
'.next/server/middleware-manifest.json': JSON.stringify(manifest),
});
const middleware = await getMiddlewareBundle({
entryPath: dir,
outputDirectory: '.next',
routesManifest: {
version: 4,
dynamicRoutes: [],
pages404: false,
redirects: [],
rewrites: [],
staticRoutes: [],
},
isCorrectMiddlewareOrder: true,
prerenderBypassToken: '',
});
expect(middleware.dynamicRouteMap.size).toBe(0);
return middleware.staticRoutes;
}