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

View File

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

View File

@@ -1397,6 +1397,7 @@ const LAMBDA_RESERVED_COMPRESSED_SIZE = 250 * KIB;
export async function getPageLambdaGroups({ export async function getPageLambdaGroups({
entryPath, entryPath,
config, config,
functionsConfigManifest,
pages, pages,
prerenderRoutes, prerenderRoutes,
pageTraces, pageTraces,
@@ -1410,6 +1411,7 @@ export async function getPageLambdaGroups({
}: { }: {
entryPath: string; entryPath: string;
config: Config; config: Config;
functionsConfigManifest?: FunctionsConfigManifestV1;
pages: string[]; pages: string[];
prerenderRoutes: Set<string>; prerenderRoutes: Set<string>;
pageTraces: { pageTraces: {
@@ -1436,16 +1438,26 @@ export async function getPageLambdaGroups({
let opts: { memory?: number; maxDuration?: number } = {}; let opts: { memory?: number; maxDuration?: number } = {};
if (
functionsConfigManifest &&
functionsConfigManifest.functions[routeName]
) {
opts = functionsConfigManifest.functions[routeName];
}
if (config && config.functions) { if (config && config.functions) {
const sourceFile = await getSourceFilePathFromPage({ const sourceFile = await getSourceFilePathFromPage({
workPath: entryPath, workPath: entryPath,
page, page,
pageExtensions, pageExtensions,
}); });
opts = await getLambdaOptionsFromFunction({
const vercelConfigOpts = await getLambdaOptionsFromFunction({
sourceFile, sourceFile,
config, config,
}); });
opts = { ...vercelConfigOpts, ...opts };
} }
let matchingGroup = groups.find(group => { let matchingGroup = groups.find(group => {
@@ -2388,6 +2400,16 @@ export {
getSourceFilePathFromPage, getSourceFilePathFromPage,
}; };
export type FunctionsConfigManifestV1 = {
version: 1;
functions: Record<
string,
{
maxDuration?: number;
}
>;
};
type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2; type MiddlewareManifest = MiddlewareManifestV1 | MiddlewareManifestV2;
interface MiddlewareManifestV1 { 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 * Attempts to read the middleware manifest from the pre-defined
* location. If the manifest can't be found it will resolve to * 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 () => { it('should build with app-dir in edge runtime correctly', async () => {
const { buildResult } = await runBuildLambda( const { buildResult } = await runBuildLambda(
path.join(__dirname, '../fixtures/00-app-dir-edge') path.join(__dirname, '../fixtures/00-app-dir-edge')