mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +00:00
[build-utils][cli][client][node][next][static-config]: forward crons from vercel.json to config.json (#9454)
This PR changes the way cron jobs are being created in the build output API. This is my first time contributing here. If you see something unusual, let me know. ✅ Good for review Our goal is to: - Allow creating cron jobs via the `crons` property of `vercel.json` for end users - Allow framework authors to create cron jobs on Vercel via the `crons` property of the Build Output API configuration --- As you can see, we removed the previous implementation where cron jobs could be configured at the function code level (export const cron = ""), on top of vercel.json `functions` property. Here's why: - All frameworks would have to implement the configure at the function code level - Not all frameworks can easily map a path to a specific function (example: SvelteKit) and would have to bail on bundling functions inside the same lambda - Configuring a path + scheduler provides a better mapping of what cron jobs are as of today: API routes on a schedule and not functions on a schedule - Dynamic routes Cron Jobs will be supported: /api/crons/sync-slack-team/230 - Query parameters will be supported support: /api/crons/sync-slack-team/230?secret=32k13l2k13lk21 (= securing cron jobs v0) - 100% frameworks compatibility from day one Next.js and other frameworks may choose to implement their own cron jobs feature that will then need to be configured through the `crons` property of `config.json` (build output API). cc @timneutkens @Rich-Harris Internal thread: https://vercel.slack.com/archives/C04DWF5HB6K/p1676366892714349
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { Cron, Files, FunctionFramework } from './types';
|
import type { Files, FunctionFramework } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Edge Functions output
|
* An Edge Functions output
|
||||||
@@ -41,9 +41,6 @@ export class EdgeFunction {
|
|||||||
/** The regions where the edge function will be executed on */
|
/** The regions where the edge function will be executed on */
|
||||||
regions?: string | string[];
|
regions?: string | string[];
|
||||||
|
|
||||||
/** Cronjob definition for the edge function */
|
|
||||||
cron?: Cron;
|
|
||||||
|
|
||||||
/** The framework */
|
/** The framework */
|
||||||
framework?: FunctionFramework;
|
framework?: FunctionFramework;
|
||||||
|
|
||||||
@@ -56,7 +53,6 @@ export class EdgeFunction {
|
|||||||
this.envVarsInUse = params.envVarsInUse;
|
this.envVarsInUse = params.envVarsInUse;
|
||||||
this.assets = params.assets;
|
this.assets = params.assets;
|
||||||
this.regions = params.regions;
|
this.regions = params.regions;
|
||||||
this.cron = params.cron;
|
|
||||||
this.framework = params.framework;
|
this.framework = params.framework;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import minimatch from 'minimatch';
|
|||||||
import { readlink } from 'fs-extra';
|
import { readlink } from 'fs-extra';
|
||||||
import { isSymbolicLink, isDirectory } from './fs/download';
|
import { isSymbolicLink, isDirectory } from './fs/download';
|
||||||
import streamToBuffer from './fs/stream-to-buffer';
|
import streamToBuffer from './fs/stream-to-buffer';
|
||||||
import type { Files, Config, Cron, FunctionFramework } from './types';
|
import type { Files, Config, FunctionFramework } from './types';
|
||||||
|
|
||||||
interface Environment {
|
interface Environment {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
@@ -25,7 +25,6 @@ export interface LambdaOptionsBase {
|
|||||||
supportsWrapper?: boolean;
|
supportsWrapper?: boolean;
|
||||||
experimentalResponseStreaming?: boolean;
|
experimentalResponseStreaming?: boolean;
|
||||||
operationType?: string;
|
operationType?: string;
|
||||||
cron?: Cron;
|
|
||||||
framework?: FunctionFramework;
|
framework?: FunctionFramework;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +63,6 @@ export class Lambda {
|
|||||||
environment: Environment;
|
environment: Environment;
|
||||||
allowQuery?: string[];
|
allowQuery?: string[];
|
||||||
regions?: string[];
|
regions?: string[];
|
||||||
cron?: Cron;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `await lambda.createZip()` instead.
|
* @deprecated Use `await lambda.createZip()` instead.
|
||||||
*/
|
*/
|
||||||
@@ -83,7 +81,6 @@ export class Lambda {
|
|||||||
environment = {},
|
environment = {},
|
||||||
allowQuery,
|
allowQuery,
|
||||||
regions,
|
regions,
|
||||||
cron,
|
|
||||||
supportsMultiPayloads,
|
supportsMultiPayloads,
|
||||||
supportsWrapper,
|
supportsWrapper,
|
||||||
experimentalResponseStreaming,
|
experimentalResponseStreaming,
|
||||||
@@ -138,10 +135,6 @@ export class Lambda {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cron !== undefined) {
|
|
||||||
assert(typeof cron === 'string', '"cron" is not a string');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (framework !== undefined) {
|
if (framework !== undefined) {
|
||||||
assert(typeof framework === 'object', '"framework" is not an object');
|
assert(typeof framework === 'object', '"framework" is not an object');
|
||||||
assert(
|
assert(
|
||||||
@@ -166,7 +159,6 @@ export class Lambda {
|
|||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.allowQuery = allowQuery;
|
this.allowQuery = allowQuery;
|
||||||
this.regions = regions;
|
this.regions = regions;
|
||||||
this.cron = cron;
|
|
||||||
this.zipBuffer = 'zipBuffer' in opts ? opts.zipBuffer : undefined;
|
this.zipBuffer = 'zipBuffer' in opts ? opts.zipBuffer : undefined;
|
||||||
this.supportsMultiPayloads = supportsMultiPayloads;
|
this.supportsMultiPayloads = supportsMultiPayloads;
|
||||||
this.supportsWrapper = supportsWrapper;
|
this.supportsWrapper = supportsWrapper;
|
||||||
@@ -246,7 +238,7 @@ export async function getLambdaOptionsFromFunction({
|
|||||||
sourceFile,
|
sourceFile,
|
||||||
config,
|
config,
|
||||||
}: GetLambdaOptionsFromFunctionOptions): Promise<
|
}: GetLambdaOptionsFromFunctionOptions): Promise<
|
||||||
Pick<LambdaOptions, 'memory' | 'maxDuration' | 'cron'>
|
Pick<LambdaOptions, 'memory' | 'maxDuration'>
|
||||||
> {
|
> {
|
||||||
if (config?.functions) {
|
if (config?.functions) {
|
||||||
for (const [pattern, fn] of Object.entries(config.functions)) {
|
for (const [pattern, fn] of Object.entries(config.functions)) {
|
||||||
@@ -254,7 +246,6 @@ export async function getLambdaOptionsFromFunction({
|
|||||||
return {
|
return {
|
||||||
memory: fn.memory,
|
memory: fn.memory,
|
||||||
maxDuration: fn.maxDuration,
|
maxDuration: fn.maxDuration,
|
||||||
cron: fn.cron,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,6 @@ export const functionsSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
maxLength: 256,
|
maxLength: 256,
|
||||||
},
|
},
|
||||||
cron: {
|
|
||||||
type: 'string',
|
|
||||||
minLength: 9,
|
|
||||||
maxLength: 256,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ export interface BuilderFunctions {
|
|||||||
runtime?: string;
|
runtime?: string;
|
||||||
includeFiles?: string;
|
includeFiles?: string;
|
||||||
excludeFiles?: string;
|
excludeFiles?: string;
|
||||||
cron?: Cron;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +410,11 @@ export interface BuildResultBuildOutput {
|
|||||||
buildOutputPath: string;
|
buildOutputPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Cron = string;
|
export interface Cron {
|
||||||
|
path: string;
|
||||||
|
schedule: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** The framework which created the function */
|
/** The framework which created the function */
|
||||||
export interface FunctionFramework {
|
export interface FunctionFramework {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
BuildResultV2Typical,
|
BuildResultV2Typical,
|
||||||
BuildResultV3,
|
BuildResultV3,
|
||||||
NowBuildError,
|
NowBuildError,
|
||||||
|
Cron,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
import {
|
import {
|
||||||
detectBuilders,
|
detectBuilders,
|
||||||
@@ -88,6 +89,7 @@ interface BuildOutputConfig {
|
|||||||
framework?: {
|
framework?: {
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
crons?: Cron[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -623,6 +625,7 @@ async function doBuild(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mergedImages = mergeImages(localConfig.images, buildResults.values());
|
const mergedImages = mergeImages(localConfig.images, buildResults.values());
|
||||||
|
const mergedCrons = mergeCrons(localConfig.crons, buildResults.values());
|
||||||
const mergedWildcard = mergeWildcard(buildResults.values());
|
const mergedWildcard = mergeWildcard(buildResults.values());
|
||||||
const mergedOverrides: Record<string, PathOverride> =
|
const mergedOverrides: Record<string, PathOverride> =
|
||||||
overrides.length > 0 ? Object.assign({}, ...overrides) : undefined;
|
overrides.length > 0 ? Object.assign({}, ...overrides) : undefined;
|
||||||
@@ -638,6 +641,7 @@ async function doBuild(
|
|||||||
wildcard: mergedWildcard,
|
wildcard: mergedWildcard,
|
||||||
overrides: mergedOverrides,
|
overrides: mergedOverrides,
|
||||||
framework,
|
framework,
|
||||||
|
crons: mergedCrons,
|
||||||
};
|
};
|
||||||
await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
|
await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
|
||||||
|
|
||||||
@@ -746,6 +750,18 @@ function mergeImages(
|
|||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeCrons(
|
||||||
|
crons: BuildOutputConfig['crons'],
|
||||||
|
buildResults: Iterable<BuildResult | BuildOutputConfig>
|
||||||
|
): BuildOutputConfig['crons'] {
|
||||||
|
for (const result of buildResults) {
|
||||||
|
if ('crons' in result && result.crons) {
|
||||||
|
crons = Object.assign({}, crons, result.crons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crons;
|
||||||
|
}
|
||||||
|
|
||||||
function mergeWildcard(
|
function mergeWildcard(
|
||||||
buildResults: Iterable<BuildResult | BuildOutputConfig>
|
buildResults: Iterable<BuildResult | BuildOutputConfig>
|
||||||
): BuildResultV2Typical['wildcard'] {
|
): BuildResultV2Typical['wildcard'] {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Builder,
|
Builder,
|
||||||
BuildResultV2,
|
BuildResultV2,
|
||||||
BuildResultV3,
|
BuildResultV3,
|
||||||
Cron,
|
|
||||||
File,
|
File,
|
||||||
FileFsRef,
|
FileFsRef,
|
||||||
BuilderV2,
|
BuilderV2,
|
||||||
@@ -41,7 +40,6 @@ export const OUTPUT_DIR = join(VERCEL_DIR, 'output');
|
|||||||
* An entry in the "functions" object in `vercel.json`.
|
* An entry in the "functions" object in `vercel.json`.
|
||||||
*/
|
*/
|
||||||
interface FunctionConfiguration {
|
interface FunctionConfiguration {
|
||||||
cron?: Cron;
|
|
||||||
memory?: number;
|
memory?: number;
|
||||||
maxDuration?: number;
|
maxDuration?: number;
|
||||||
}
|
}
|
||||||
@@ -372,14 +370,12 @@ async function writeLambda(
|
|||||||
throw new Error('Malformed `Lambda` - no "files" present');
|
throw new Error('Malformed `Lambda` - no "files" present');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cron = functionConfiguration?.cron ?? lambda.cron;
|
|
||||||
const memory = functionConfiguration?.memory ?? lambda.memory;
|
const memory = functionConfiguration?.memory ?? lambda.memory;
|
||||||
const maxDuration = functionConfiguration?.maxDuration ?? lambda.maxDuration;
|
const maxDuration = functionConfiguration?.maxDuration ?? lambda.maxDuration;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
...lambda,
|
...lambda,
|
||||||
handler: normalizePath(lambda.handler),
|
handler: normalizePath(lambda.handler),
|
||||||
cron,
|
|
||||||
memory,
|
memory,
|
||||||
maxDuration,
|
maxDuration,
|
||||||
type: undefined,
|
type: undefined,
|
||||||
|
|||||||
@@ -93,6 +93,29 @@ const imagesSchema = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cronsSchema = {
|
||||||
|
type: 'array',
|
||||||
|
minItems: 0,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['path', 'schedule'],
|
||||||
|
properties: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 512,
|
||||||
|
pattern: '^/.*',
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
type: 'string',
|
||||||
|
minLength: 9,
|
||||||
|
maxLength: 256,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const vercelConfigSchema = {
|
const vercelConfigSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
// These are not all possibilities because `vc dev`
|
// These are not all possibilities because `vc dev`
|
||||||
@@ -108,6 +131,7 @@ const vercelConfigSchema = {
|
|||||||
trailingSlash: trailingSlashSchema,
|
trailingSlash: trailingSlashSchema,
|
||||||
functions: functionsSchema,
|
functions: functionsSchema,
|
||||||
images: imagesSchema,
|
images: imagesSchema,
|
||||||
|
crons: cronsSchema,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function (req, res) {
|
export default function (req, res) {
|
||||||
res.json('hello from the edge');
|
res.send('Hello from cron job!');
|
||||||
}
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export const config = {
|
|
||||||
runtime: 'edge',
|
|
||||||
cron: '* * * * *',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function edge(request, event) {
|
|
||||||
const requestBody = await request.text();
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
headerContentType: request.headers.get('content-type'),
|
|
||||||
url: request.url,
|
|
||||||
method: request.method,
|
|
||||||
body: requestBody,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function (req, res) {
|
|
||||||
res.end('serverless says hello');
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function (req, res) {
|
|
||||||
res.json({ memory: parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE) });
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
cron: '* * * * *',
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"functions": {
|
"crons": [
|
||||||
"api/overwrite/serverless.js": {
|
{
|
||||||
"cron": "0 10-20 * * *"
|
"path": "/api/cron-job",
|
||||||
},
|
"schedule": "0 0 * * *"
|
||||||
"api/overwrite/edge.js": {
|
|
||||||
"cron": "10 * * * *"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
packages/cli/test/integration.js
vendored
2
packages/cli/test/integration.js
vendored
@@ -2635,7 +2635,7 @@ test('next unsupported functions config shows warning link', async t => {
|
|||||||
t.is(output.exitCode, 0, formatOutput(output));
|
t.is(output.exitCode, 0, formatOutput(output));
|
||||||
t.regex(
|
t.regex(
|
||||||
output.stderr,
|
output.stderr,
|
||||||
/Ignoring function property `runtime`\. When using Next\.js, only `memory`, `maxDuration`, and `cron` can be used\./gm,
|
/Ignoring function property `runtime`\. When using Next\.js, only `memory` and `maxDuration` can be used\./gm,
|
||||||
formatOutput(output)
|
formatOutput(output)
|
||||||
);
|
);
|
||||||
t.regex(
|
t.regex(
|
||||||
|
|||||||
@@ -1104,32 +1104,20 @@ describe('build', () => {
|
|||||||
|
|
||||||
it('should include crons property in build output', async () => {
|
it('should include crons property in build output', async () => {
|
||||||
const cwd = fixture('with-cron');
|
const cwd = fixture('with-cron');
|
||||||
const output = join(cwd, '.vercel', 'output', 'functions', 'api');
|
const output = join(cwd, '.vercel', 'output');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.chdir(cwd);
|
process.chdir(cwd);
|
||||||
const exitCode = await build(client);
|
const exitCode = await build(client);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
const edge = await fs.readJSON(
|
const config = await fs.readJSON(join(output, 'config.json'));
|
||||||
join(output, 'edge.func', '.vc-config.json')
|
expect(config).toHaveProperty('crons', [
|
||||||
);
|
{
|
||||||
expect(edge).toHaveProperty('cron', '* * * * *');
|
path: '/api/cron-job',
|
||||||
|
schedule: '0 0 * * *',
|
||||||
const serverless = await fs.readJSON(
|
},
|
||||||
join(output, 'serverless.func', '.vc-config.json')
|
]);
|
||||||
);
|
|
||||||
expect(serverless).toHaveProperty('cron', '* * * * *');
|
|
||||||
|
|
||||||
const overwriteServerless = await fs.readJSON(
|
|
||||||
join(output, 'overwrite', 'serverless.func', '.vc-config.json')
|
|
||||||
);
|
|
||||||
expect(overwriteServerless).toHaveProperty('cron', '0 10-20 * * *');
|
|
||||||
|
|
||||||
const overwriteEdge = await fs.readJSON(
|
|
||||||
join(output, 'overwrite', 'edge.func', '.vc-config.json')
|
|
||||||
);
|
|
||||||
expect(overwriteEdge).toHaveProperty('cron', '10 * * * *');
|
|
||||||
} finally {
|
} finally {
|
||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
delete process.env.__VERCEL_BUILD_RUNNING;
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
||||||
|
|||||||
@@ -285,4 +285,90 @@ describe('validateConfig', () => {
|
|||||||
|
|
||||||
expect(error!.link).toEqual('https://vercel.link/functions-and-builds');
|
expect(error!.link).toEqual('https://vercel.link/functions-and-builds');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should error when crons have missing schedule', () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
// @ts-ignore
|
||||||
|
crons: [{ path: '/api/test.js' }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0]` missing required property `schedule`.'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when crons have missing path', () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
// @ts-ignore
|
||||||
|
crons: [{ schedule: '* * * * *' }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0]` missing required property `path`.'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when path is too long', () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
crons: [{ path: '/' + 'x'.repeat(512), schedule: '* * * * *' }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0].path` should NOT be longer than 512 characters.'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when schedule is too long', () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
crons: [{ path: '/', schedule: '*'.repeat(257) }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0].schedule` should NOT be longer than 256 characters.'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when path is empty', () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
crons: [{ path: '', schedule: '* * * * *' }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0].path` should NOT be shorter than 1 characters.'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when schedule is too short', () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
crons: [{ path: '/', schedule: '* * * * ' }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0].schedule` should NOT be shorter than 9 characters.'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should error when path doesn't start with `/`", () => {
|
||||||
|
const error = validateConfig({
|
||||||
|
crons: [{ path: 'api/cron', schedule: '* * * * *' }],
|
||||||
|
});
|
||||||
|
expect(error!.message).toEqual(
|
||||||
|
'Invalid vercel.json - `crons[0].path` should match pattern "^/.*".'
|
||||||
|
);
|
||||||
|
expect(error!.link).toEqual(
|
||||||
|
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
BuilderFunctions,
|
BuilderFunctions,
|
||||||
Images,
|
Images,
|
||||||
ProjectSettings,
|
ProjectSettings,
|
||||||
|
Cron,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
import type { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
|
import type { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ export interface VercelConfig {
|
|||||||
framework?: string | null;
|
framework?: string | null;
|
||||||
outputDirectory?: string | null;
|
outputDirectory?: string | null;
|
||||||
images?: Images;
|
images?: Images;
|
||||||
|
crons?: Cron[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GitMetadata {
|
export interface GitMetadata {
|
||||||
|
|||||||
@@ -873,7 +873,6 @@ export async function serverBuild({
|
|||||||
runtime: nodeVersion.runtime,
|
runtime: nodeVersion.runtime,
|
||||||
maxDuration: group.maxDuration,
|
maxDuration: group.maxDuration,
|
||||||
isStreaming: group.isStreaming,
|
isStreaming: group.isStreaming,
|
||||||
cron: group.cron,
|
|
||||||
nextVersion,
|
nextVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
NodejsLambda,
|
NodejsLambda,
|
||||||
EdgeFunction,
|
EdgeFunction,
|
||||||
Images,
|
Images,
|
||||||
Cron,
|
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
import { NodeFileTraceReasons } from '@vercel/nft';
|
import { NodeFileTraceReasons } from '@vercel/nft';
|
||||||
import type {
|
import type {
|
||||||
@@ -1311,7 +1310,6 @@ export function addLocaleOrDefault(
|
|||||||
export type LambdaGroup = {
|
export type LambdaGroup = {
|
||||||
pages: string[];
|
pages: string[];
|
||||||
memory?: number;
|
memory?: number;
|
||||||
cron?: Cron;
|
|
||||||
maxDuration?: number;
|
maxDuration?: number;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
isPrerenders?: boolean;
|
isPrerenders?: boolean;
|
||||||
@@ -1364,7 +1362,7 @@ export async function getPageLambdaGroups({
|
|||||||
const routeName = normalizePage(page.replace(/\.js$/, ''));
|
const routeName = normalizePage(page.replace(/\.js$/, ''));
|
||||||
const isPrerenderRoute = prerenderRoutes.has(routeName);
|
const isPrerenderRoute = prerenderRoutes.has(routeName);
|
||||||
|
|
||||||
let opts: { memory?: number; maxDuration?: number; cron?: Cron } = {};
|
let opts: { memory?: number; maxDuration?: number } = {};
|
||||||
|
|
||||||
if (config && config.functions) {
|
if (config && config.functions) {
|
||||||
const sourceFile = await getSourceFilePathFromPage({
|
const sourceFile = await getSourceFilePathFromPage({
|
||||||
@@ -1382,8 +1380,7 @@ export async function getPageLambdaGroups({
|
|||||||
const matches =
|
const matches =
|
||||||
group.maxDuration === opts.maxDuration &&
|
group.maxDuration === opts.maxDuration &&
|
||||||
group.memory === opts.memory &&
|
group.memory === opts.memory &&
|
||||||
group.isPrerenders === isPrerenderRoute &&
|
group.isPrerenders === isPrerenderRoute;
|
||||||
!opts.cron; // Functions with a cronjob must be on their own
|
|
||||||
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
let newTracedFilesSize = group.pseudoLayerBytes;
|
let newTracedFilesSize = group.pseudoLayerBytes;
|
||||||
@@ -2318,7 +2315,6 @@ interface EdgeFunctionMatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMiddlewareBundle({
|
export async function getMiddlewareBundle({
|
||||||
config = {},
|
|
||||||
entryPath,
|
entryPath,
|
||||||
outputDirectory,
|
outputDirectory,
|
||||||
routesManifest,
|
routesManifest,
|
||||||
@@ -2383,21 +2379,6 @@ export async function getMiddlewareBundle({
|
|||||||
edgeFunction.wasm
|
edgeFunction.wasm
|
||||||
);
|
);
|
||||||
|
|
||||||
const edgeFunctionOptions: { cron?: Cron } = {};
|
|
||||||
if (config.functions) {
|
|
||||||
const sourceFile = await getSourceFilePathFromPage({
|
|
||||||
workPath: entryPath,
|
|
||||||
page: `${edgeFunction.page}.js`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const opts = await getLambdaOptionsFromFunction({
|
|
||||||
sourceFile,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
edgeFunctionOptions.cron = opts.cron;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
page: edgeFunction.page,
|
page: edgeFunction.page,
|
||||||
@@ -2442,7 +2423,6 @@ export async function getMiddlewareBundle({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return new EdgeFunction({
|
return new EdgeFunction({
|
||||||
...edgeFunctionOptions,
|
|
||||||
deploymentTarget: 'v8-worker',
|
deploymentTarget: 'v8-worker',
|
||||||
name: edgeFunction.name,
|
name: edgeFunction.name,
|
||||||
files: {
|
files: {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const { build } = require('../../dist');
|
|
||||||
|
|
||||||
function getFixture(name) {
|
|
||||||
return path.join(__dirname, 'fixtures', name);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialCorepackValue = process.env.COREPACK_ENABLE_STRICT;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
process.env.COREPACK_ENABLE_STRICT = '0';
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.env.COREPACK_ENABLE_STRICT = initialCorepackValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should include cron property from config', async () => {
|
|
||||||
const cwd = getFixture('03-with-api-routes');
|
|
||||||
await fs.remove(path.join(cwd, '.next'));
|
|
||||||
|
|
||||||
const result = await build({
|
|
||||||
workPath: cwd,
|
|
||||||
repoRootPath: cwd,
|
|
||||||
entrypoint: 'package.json',
|
|
||||||
config: {
|
|
||||||
functions: {
|
|
||||||
'pages/api/edge.js': {
|
|
||||||
cron: '* * * * *',
|
|
||||||
},
|
|
||||||
'pages/api/serverless.js': {
|
|
||||||
cron: '* * * * *',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
skipDownload: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.output['api/serverless']).toHaveProperty('cron', '* * * * *');
|
|
||||||
expect(result.output['api/edge']).toHaveProperty('cron', '* * * * *');
|
|
||||||
});
|
|
||||||
@@ -424,8 +424,6 @@ export const build: BuildV3 = async ({
|
|||||||
isEdgeFunction = isEdgeRuntime(staticConfig.runtime);
|
isEdgeFunction = isEdgeRuntime(staticConfig.runtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cron = staticConfig?.cron;
|
|
||||||
|
|
||||||
debug('Tracing input files...');
|
debug('Tracing input files...');
|
||||||
const traceTime = Date.now();
|
const traceTime = Date.now();
|
||||||
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
|
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
|
||||||
@@ -475,7 +473,6 @@ export const build: BuildV3 = async ({
|
|||||||
// TODO: remove - these two properties should not be required
|
// TODO: remove - these two properties should not be required
|
||||||
name: outputPath,
|
name: outputPath,
|
||||||
deploymentTarget: 'v8-worker',
|
deploymentTarget: 'v8-worker',
|
||||||
cron,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// "nodejs" runtime is the default
|
// "nodejs" runtime is the default
|
||||||
@@ -494,7 +491,6 @@ export const build: BuildV3 = async ({
|
|||||||
shouldAddSourcemapSupport,
|
shouldAddSourcemapSupport,
|
||||||
awsLambdaHandler,
|
awsLambdaHandler,
|
||||||
experimentalResponseStreaming,
|
experimentalResponseStreaming,
|
||||||
cron,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const BaseFunctionConfigSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
runtime: { type: 'string' },
|
runtime: { type: 'string' },
|
||||||
cron: { type: 'string' },
|
|
||||||
memory: { type: 'number' },
|
memory: { type: 'number' },
|
||||||
maxDuration: { type: 'number' },
|
maxDuration: { type: 'number' },
|
||||||
regions: {
|
regions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user