Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/2.deploy/20.providers/vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,67 @@ export default defineNitroConfig({

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.

## Queues

:read-more{title="Vercel Queues" to="https://vercel.com/docs/queues"}

Nitro integrates with [Vercel Queues](https://vercel.com/docs/queues) to process messages asynchronously. Define your queue topics in the Nitro config and handle incoming messages with the `vercel:queue` runtime hook.

```ts [nitro.config.ts]
export default defineNitroConfig({
vercel: {
queues: {
triggers: [
{ topic: "orders" },
{ topic: "notifications" },
],
},
},
});
```

### Handling messages

Use the `vercel:queue` hook in a [Nitro plugin](/guide/plugins) to process incoming queue messages:

```ts [server/plugins/queues.ts]
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook("vercel:queue", ({ message, metadata }) => {
console.log(`[${metadata.topicName}] Message ${metadata.messageId}:`, message);
});
});
```

### Running tasks from queue messages

You can use queue messages to trigger [Nitro tasks](/tasks):

```ts [server/plugins/queues.ts]
import { runTask } from "nitro/task";

export default defineNitroPlugin((nitro) => {
nitro.hooks.hook("vercel:queue", async ({ message, metadata }) => {
if (metadata.topicName === "orders") {
await runTask("orders:fulfill", { payload: message });
}
});
});
```

### Sending messages

Use the `@vercel/queue` package directly to send messages to a topic:

```ts [server/routes/api/orders.post.ts]
import { send } from "@vercel/queue";

export default defineEventHandler(async (event) => {
const order = await event.req.json();
const { messageId } = await send("orders", order);
return { messageId };
});
```

## Custom build output configuration

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.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"@types/semver": "^7.7.1",
"@types/xml2js": "^0.4.14",
"@typescript/native-preview": "latest",
"@vercel/queue": "^0.1.4",
"@vitest/coverage-v8": "^4.1.0",
"automd": "^0.4.3",
"c12": "^4.0.0-beta.3",
Expand Down
42 changes: 31 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions src/presets/vercel/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineNitroPreset } from "../_utils/preset.ts";
import type { Nitro } from "nitro/types";
import { presetsDir } from "nitro/meta";
import { join } from "pathe";
import { importDep } from "../../utils/dep.ts";
import {
deprecateSWR,
generateFunctionFiles,
Expand Down Expand Up @@ -65,6 +66,35 @@ const vercel = defineNitroPreset(
handler: join(presetsDir, "vercel/runtime/cron-handler"),
});
}

// Queue consumer handler
const queues = nitro.options.vercel?.queues;
if (queues?.triggers?.length) {
await importDep({
id: "@vercel/queue",
dir: nitro.options.rootDir,
reason: "Vercel Queues",
});

const handlerRoute = queues.handlerRoute || "/_vercel/queues/consumer";

nitro.options.handlers.push({
route: handlerRoute,
lazy: true,
handler: join(presetsDir, "vercel/runtime/queue-handler"),
});

nitro.options.vercel!.routeFunctionConfig = {
...nitro.options.vercel!.routeFunctionConfig,
[handlerRoute]: {
...nitro.options.vercel!.routeFunctionConfig?.[handlerRoute],
experimentalTriggers: queues.triggers.map((t) => ({
type: "queue/v2beta" as const,
topic: t.topic,
})),
},
};
}
},
"rollup:before": (nitro: Nitro) => {
deprecateSWR(nitro);
Expand Down
11 changes: 11 additions & 0 deletions src/presets/vercel/runtime/queue-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { handleCallback } from "@vercel/queue";
import { defineHandler } from "nitro";
import { useNitroHooks } from "nitro/app";

const handler = handleCallback(async (message, metadata) => {
await useNitroHooks().callHook("vercel:queue", { message, metadata });
});

export default defineHandler((event) => {
return handler(event.req as Request);
});
47 changes: 47 additions & 0 deletions src/presets/vercel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,44 @@ export interface VercelOptions {
*/
cronHandlerRoute?: string;

/**
* Vercel Queues configuration.
*
* Messages are delivered via the `vercel:queue` runtime hook.
*
* @example
* ```ts
* // nitro.config.ts
* export default defineNitroConfig({
* vercel: {
* queues: {
* triggers: [{ topic: "orders" }],
* },
* },
* });
* ```
*
* ```ts
* // server/plugins/queues.ts
* export default defineNitroPlugin((nitro) => {
* nitro.hooks.hook("vercel:queue", ({ message, metadata }) => {
* console.log(`Received message on ${metadata.topicName}:`, message);
* });
* });
* ```
*
* @see https://vercel.com/docs/queues
*/
queues?: {
/**
* Route path for the queue consumer handler.
* @default "/_vercel/queues/consumer"
*/
handlerRoute?: string;
/** Queue topic triggers to subscribe to. */
triggers: Array<{ topic: string }>;
};

/**
* Per-route function configuration overrides.
*
Expand Down Expand Up @@ -206,3 +244,12 @@ export type PrerenderFunctionConfig = {
*/
exposeErrBody?: boolean;
};

declare module "nitro/types" {
export interface NitroRuntimeHooks {
"vercel:queue": (_: {
message: unknown;
metadata: import("@vercel/queue").MessageMetadata;
}) => void;
}
}
41 changes: 41 additions & 0 deletions test/presets/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,47 @@ describe("nitro:preset:vercel:route-function-config", async () => {
});
});

describe("nitro:preset:vercel:queues", async () => {
const ctx = await setupTest("vercel", {
outDirSuffix: "-queues",
config: {
preset: "vercel",
vercel: {
queues: {
triggers: [{ topic: "orders" }, { topic: "notifications" }],
},
},
},
});

it("should create queue consumer function directory with experimentalTriggers", async () => {
const funcDir = resolve(ctx.outDir, "functions/_vercel/queues/consumer.func");
const stat = await fsp.lstat(funcDir);
expect(stat.isDirectory()).toBe(true);
expect(stat.isSymbolicLink()).toBe(false);

const config = await fsp
.readFile(resolve(funcDir, ".vc-config.json"), "utf8")
.then((r) => JSON.parse(r));
expect(config.experimentalTriggers).toEqual([
{ type: "queue/v2beta", topic: "orders" },
{ type: "queue/v2beta", topic: "notifications" },
]);
expect(config.handler).toBe("index.mjs");
});

it("should add queue consumer route in config.json", async () => {
const config = await fsp
.readFile(resolve(ctx.outDir, "config.json"), "utf8")
.then((r) => JSON.parse(r));
const routes = config.routes as { src: string; dest: string }[];
const queueRoute = routes.find(
(r) => r.dest === "/_vercel/queues/consumer" && r.src === "/_vercel/queues/consumer"
);
expect(queueRoute).toBeDefined();
});
});

describe.skip("nitro:preset:vercel:bun-verceljson", async () => {
const vercelJsonPath = join(fixtureDir, "vercel.json");

Expand Down
Loading