Skip to content

Commit f663e76

Browse files
authored
presets(vercel): integrate with scheduled tasks (#4030)
1 parent 7857f44 commit f663e76

7 files changed

Lines changed: 112 additions & 1 deletion

File tree

docs/1.docs/50.tasks.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export default defineNitroConfig({
7070
### Platform support
7171

7272
- `dev`, `node-server`, `bun` and `deno-server` presets are supported with [croner](https://croner.56k.guru/) engine.
73-
- `cloudflare_module` preset have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Make sure to configure wrangler to use exactly same patterns you define in `scheduledTasks` to be matched.
73+
- `cloudflare_module` preset has native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Make sure to configure wrangler to use the same patterns you define in `scheduledTasks` to be matched.
74+
- `vercel` preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs). Nitro automatically generates the cron job configuration at build time — no manual `vercel.json` setup required.
7475
- More presets (with native primitives support) are planned to be supported!
7576

7677
## Programmatically run tasks

docs/2.deploy/20.providers/vercel.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,34 @@ When the proxy rule uses any of the following `ProxyOptions`, Nitro keeps it as
9191
Response headers defined on the route rule via the `headers` option are still applied to CDN-level rewrites. Only request-level `ProxyOptions.headers` (sent to the upstream) require a runtime proxy.
9292
::
9393

94+
## Scheduled tasks (Cron Jobs)
95+
96+
:read-more{title="Vercel Cron Jobs" to="https://vercel.com/docs/cron-jobs"}
97+
98+
Nitro automatically converts your [`scheduledTasks`](/docs/tasks#scheduled-tasks) configuration into [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs) at build time. Define your schedules in your Nitro config and deploy - no manual `vercel.json` cron configuration required.
99+
100+
```ts [nitro.config.ts]
101+
import { defineNitroConfig } from "nitro/config";
102+
103+
export default defineNitroConfig({
104+
experimental: {
105+
tasks: true
106+
},
107+
scheduledTasks: {
108+
// Run `cms:update` every hour
109+
'0 * * * *': ['cms:update'],
110+
// Run `db:cleanup` every day at midnight
111+
'0 0 * * *': ['db:cleanup']
112+
}
113+
})
114+
```
115+
116+
### Secure cron job endpoints
117+
118+
:read-more{title="Securing cron jobs" to="https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs"}
119+
120+
To prevent unauthorized access to the cron handler, set a `CRON_SECRET` environment variable in your Vercel project settings. When `CRON_SECRET` is set, Nitro validates the `Authorization` header on every cron invocation.
121+
94122
## Custom build output configuration
95123

96124
You can provide additional [build output configuration](https://vercel.com/docs/build-output-api/v3) using `vercel.config` key inside `nitro.config`. It will be merged with built-in auto-generated config.

src/presets/vercel/preset.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { defineNitroPreset } from "../_utils/preset.ts";
22
import type { Nitro } from "nitro/types";
3+
import { presetsDir } from "nitro/meta";
4+
import { join } from "pathe";
35
import {
46
deprecateSWR,
57
generateFunctionFiles,
@@ -19,6 +21,7 @@ const vercel = defineNitroPreset(
1921
},
2022
vercel: {
2123
skewProtection: !!process.env.VERCEL_SKEW_PROTECTION_ENABLED,
24+
cronHandlerRoute: "/_vercel/cron",
2225
},
2326
output: {
2427
dir: "{{ rootDir }}/.vercel/output",
@@ -50,6 +53,18 @@ const vercel = defineNitroPreset(
5053
}
5154
logger.info(`Using \`${serverFormat}\` entry format.`);
5255
nitro.options.entry = nitro.options.entry.replace("{format}", serverFormat);
56+
57+
// Cron tasks handler
58+
if (
59+
nitro.options.experimental.tasks &&
60+
Object.keys(nitro.options.scheduledTasks || {}).length > 0
61+
) {
62+
nitro.options.handlers.push({
63+
route: nitro.options.vercel!.cronHandlerRoute || "/_vercel/cron",
64+
lazy: true,
65+
handler: join(presetsDir, "vercel/runtime/cron-handler"),
66+
});
67+
}
5368
},
5469
"rollup:before": (nitro: Nitro) => {
5570
deprecateSWR(nitro);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { timingSafeEqual } from "node:crypto";
2+
import { defineHandler, HTTPError } from "nitro/h3";
3+
import { runCronTasks } from "#nitro/runtime/task";
4+
5+
export default defineHandler(async (event) => {
6+
// Validate CRON_SECRET if set - https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs
7+
const cronSecret = process.env.CRON_SECRET;
8+
if (cronSecret) {
9+
const authHeader = event.req.headers.get("authorization") || "";
10+
const expected = `Bearer ${cronSecret}`;
11+
const a = Buffer.from(authHeader);
12+
const b = Buffer.from(expected);
13+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
14+
throw new HTTPError("Unauthorized", { status: 401 });
15+
}
16+
}
17+
18+
const cron = event.req.headers.get("x-vercel-cron-schedule");
19+
if (!cron) {
20+
throw new HTTPError("Missing x-vercel-cron-schedule header", { status: 400 });
21+
}
22+
23+
await runCronTasks(cron, {
24+
context: {},
25+
payload: {
26+
scheduledTime: Date.now(),
27+
},
28+
});
29+
30+
return { success: true };
31+
});

src/presets/vercel/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ export interface VercelOptions {
132132
* Possible values are: `web` (default) and `node`.
133133
*/
134134
entryFormat?: "web" | "node";
135+
136+
/**
137+
* The route path for the Vercel cron handler endpoint.
138+
*
139+
* When `experimental.tasks` and `scheduledTasks` are configured,
140+
* Nitro registers a cron handler at this path that Vercel invokes
141+
* on each scheduled cron trigger.
142+
*
143+
* @default "/_vercel/cron"
144+
* @see https://vercel.com/docs/cron-jobs
145+
*/
146+
cronHandlerRoute?: string;
135147
}
136148

137149
/**

src/presets/vercel/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,19 @@ function generateBuildConfig(nitro: Nitro, o11Routes?: ObservabilityRoute[]) {
221221
],
222222
} as VercelBuildConfigV3);
223223

224+
// Cron jobs from scheduledTasks
225+
if (
226+
nitro.options.experimental.tasks &&
227+
Object.keys(nitro.options.scheduledTasks || {}).length > 0
228+
) {
229+
const cronPath = nitro.options.vercel!.cronHandlerRoute || "/_vercel/cron";
230+
const cronEntries = Object.keys(nitro.options.scheduledTasks).map((schedule) => ({
231+
path: cronPath,
232+
schedule,
233+
}));
234+
config.crons = [...cronEntries, ...(config.crons || [])];
235+
}
236+
224237
// Early return if we are building a static site
225238
if (nitro.options.static) {
226239
return config;

test/presets/vercel.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ describe("nitro:preset:vercel:web", async () => {
2929
.then((r) => JSON.parse(r));
3030
expect(config).toMatchInlineSnapshot(`
3131
{
32+
"crons": [
33+
{
34+
"path": "/_vercel/cron",
35+
"schedule": "* * * * *",
36+
},
37+
],
3238
"overrides": {
3339
"_scalar/index.html": {
3440
"path": "_scalar",
@@ -317,6 +323,10 @@ describe("nitro:preset:vercel:web", async () => {
317323
"dest": "/500",
318324
"src": "/500",
319325
},
326+
{
327+
"dest": "/_vercel/cron",
328+
"src": "/_vercel/cron",
329+
},
320330
{
321331
"dest": "/_swagger",
322332
"src": "/_swagger",
@@ -403,6 +413,7 @@ describe("nitro:preset:vercel:web", async () => {
403413
"functions/_openapi.json.func (symlink)",
404414
"functions/_scalar.func (symlink)",
405415
"functions/_swagger.func (symlink)",
416+
"functions/_vercel",
406417
"functions/api/cached.func (symlink)",
407418
"functions/api/db.func (symlink)",
408419
"functions/api/echo.func (symlink)",

0 commit comments

Comments
 (0)