[next] support maxDuration in Next.js deployments (#10069)

Follow up PR to
8703c55f9f
which reads the newly created function config manifest and patches in
the options for resource creation.

---------

Co-authored-by: Steven <steven@ceriously.com>
This commit is contained in:
Florentin / 珞辰
2023-07-14 15:27:06 +02:00
committed by GitHub
parent a91bde5287
commit cae60155f3
11 changed files with 150 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
'@vercel/next': minor
---
Support maxDuration in Next.js deployments

View File

@@ -90,6 +90,7 @@ import {
validateEntrypoint,
getOperationType,
isApiPage,
getFunctionsConfigManifest,
} from './utils';
export const version = 2;
@@ -272,7 +273,7 @@ export const build: BuildV2 = async ({
});
let hasLegacyRoutes = false;
const hasFunctionsConfig = !!config.functions;
const hasFunctionsConfig = Boolean(config.functions);
if (await pathExists(dotNextStatic)) {
console.warn('WARNING: You should not upload the `.next` directory.');
@@ -487,7 +488,12 @@ export const build: BuildV2 = async ({
? await getRequiredServerFilesManifest(entryPath, outputDirectory)
: false;
isServerMode = !!requiredServerFilesManifest;
isServerMode = Boolean(requiredServerFilesManifest);
const functionsConfigManifest = await getFunctionsConfigManifest(
entryPath,
outputDirectory
);
const routesManifest = await getRoutesManifest(
entryPath,
@@ -1312,6 +1318,7 @@ export const build: BuildV2 = async ({
return serverBuild({
config,
functionsConfigManifest,
nextVersion,
trailingSlash,
appPathRoutesManifest,
@@ -1570,6 +1577,7 @@ export const build: BuildV2 = async ({
const initialPageLambdaGroups = await getPageLambdaGroups({
entryPath,
config,
functionsConfigManifest,
pages: nonApiPages,
prerenderRoutes: new Set(),
pageTraces,
@@ -1586,6 +1594,7 @@ export const build: BuildV2 = async ({
const initialApiLambdaGroups = await getPageLambdaGroups({
entryPath,
config,
functionsConfigManifest,
pages: apiPages,
prerenderRoutes: new Set(),
pageTraces,

View File

@@ -44,6 +44,7 @@ import {
getFilesMapFromReasons,
UnwrapPromise,
getOperationType,
FunctionsConfigManifestV1,
} from './utils';
import {
nodeFileTrace,
@@ -66,6 +67,7 @@ export async function serverBuild({
dynamicPages,
pagesDir,
config = {},
functionsConfigManifest,
privateOutputs,
baseDir,
workPath,
@@ -105,6 +107,7 @@ export async function serverBuild({
dynamicPages: string[];
trailingSlash: boolean;
config: Config;
functionsConfigManifest?: FunctionsConfigManifestV1;
pagesDir: string;
baseDir: string;
canUsePreviewMode: boolean;
@@ -752,6 +755,7 @@ export async function serverBuild({
const pageLambdaGroups = await getPageLambdaGroups({
entryPath: projectDir,
config,
functionsConfigManifest,
pages: nonApiPages,
prerenderRoutes,
pageTraces,
@@ -767,6 +771,7 @@ export async function serverBuild({
const appRouterLambdaGroups = await getPageLambdaGroups({
entryPath: projectDir,
config,
functionsConfigManifest,
pages: appRouterPages,
prerenderRoutes,
pageTraces,
@@ -789,6 +794,7 @@ export async function serverBuild({
const apiLambdaGroups = await getPageLambdaGroups({
entryPath: projectDir,
config,
functionsConfigManifest,
pages: apiPages,
prerenderRoutes,
pageTraces,

View File

@@ -1397,6 +1397,7 @@ const LAMBDA_RESERVED_COMPRESSED_SIZE = 250 * KIB;
export async function getPageLambdaGroups({
entryPath,
config,
functionsConfigManifest,
pages,
prerenderRoutes,
pageTraces,
@@ -1410,6 +1411,7 @@ export async function getPageLambdaGroups({
}: {
entryPath: string;
config: Config;
functionsConfigManifest?: FunctionsConfigManifestV1;
pages: string[];
prerenderRoutes: Set<string>;
pageTraces: {
@@ -1436,16 +1438,26 @@ export async function getPageLambdaGroups({
let opts: { memory?: number; maxDuration?: number } = {};
if (
functionsConfigManifest &&
functionsConfigManifest.functions[routeName]
) {
opts = functionsConfigManifest.functions[routeName];
}
if (config && config.functions) {
const sourceFile = await getSourceFilePathFromPage({
workPath: entryPath,
page,
pageExtensions,
});
opts = await getLambdaOptionsFromFunction({
const vercelConfigOpts = await getLambdaOptionsFromFunction({
sourceFile,
config,
});
opts = { ...vercelConfigOpts, ...opts };
}
let matchingGroup = groups.find(group => {
@@ -2388,6 +2400,16 @@ export {
getSourceFilePathFromPage,
};
export type FunctionsConfigManifestV1 = {
version: 1;
functions: Record<
string,
{
maxDuration?: number;
}
>;
};
type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2;
interface MiddlewareManifestV1 {
@@ -2731,6 +2753,37 @@ export async function getMiddlewareBundle({
};
}
/**
* Attempts to read the functions config manifest from the pre-defined
* location. If the manifest can't be found it will resolve to
* undefined.
*/
export async function getFunctionsConfigManifest(
entryPath: string,
outputDirectory: string
): Promise<FunctionsConfigManifestV1 | undefined> {
const functionConfigManifestPath = path.join(
entryPath,
outputDirectory,
'./server/functions-config-manifest.json'
);
const hasManifest = await fs
.access(functionConfigManifestPath)
.then(() => true)
.catch(() => false);
if (!hasManifest) {
return;
}
const manifest: FunctionsConfigManifestV1 = await fs.readJSON(
functionConfigManifestPath
);
return manifest.version === 1 ? manifest : undefined;
}
/**
* Attempts to read the middleware manifest from the pre-defined
* location. If the manifest can't be found it will resolve to

View File

@@ -0,0 +1,6 @@
export const GET = req => {
console.log(req.url);
return new Response('hello world');
};
export const maxDuration = 7;

View File

@@ -0,0 +1,12 @@
/* eslint-env jest */
const path = require('path');
const { deployAndTest } = require('../../utils');
const ctx = {};
describe(`${__dirname.split(path.sep).pop()}`, () => {
it('should deploy and pass probe checks', async () => {
const info = await deployAndTest(__dirname);
Object.assign(ctx, info);
});
});

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"next": "canary",
"react": "experimental",
"react-dom": "experimental"
},
"ignoreNextjsUpdates": true
}

View File

@@ -0,0 +1,5 @@
export default function handler(req, res) {
return res.json({ hello: 'world' });
}
export const maxDuration = 7;

View File

@@ -0,0 +1,8 @@
{
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
]
}

View File

@@ -122,6 +122,40 @@ if (parseInt(process.versions.node.split('.')[0], 10) >= 16) {
// );
});
it('should build with app-dir with segment options correctly', async () => {
const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir-segment-options')
);
const lambdas = new Set();
for (const key of Object.keys(buildResult.output)) {
if (buildResult.output[key].type === 'Lambda') {
lambdas.add(buildResult.output[key]);
}
}
expect(
buildResult.routes.some(
route =>
route.src?.includes('_next/data') && route.src?.includes('.rsc')
)
).toBeFalsy();
expect(lambdas.size).toBe(2);
expect(buildResult.output['api/hello']).toBeDefined();
expect(buildResult.output['api/hello'].type).toBe('Lambda');
expect(buildResult.output['api/hello'].maxDuration).toBe(7);
expect(buildResult.output['api/hello-again']).toBeDefined();
expect(buildResult.output['api/hello-again'].type).toBe('Lambda');
expect(buildResult.output['api/hello-again'].maxDuration).toBe(7);
expect(
buildResult.output['api/hello-again'].supportsResponseStreaming
).toBe(true);
});
it('should build with app-dir in edge runtime correctly', async () => {
const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir-edge')