Skip to content
Open
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
6 changes: 6 additions & 0 deletions docs/2.deploy/20.providers/vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ export default defineEventHandler(async (event) => {
});
```

### Local development

Queues work in `nitro dev` β€” `send()` delivers messages straight to your `vercel:queue` hook, so you can iterate without deploying. Pull your Vercel environment first with `vercel link` and `vercel env pull` so the SDK can authenticate.

If your hook throws, the message is retried locally. Retries honour `retryAfterSeconds` from each trigger when set.

## 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
4 changes: 2 additions & 2 deletions src/presets/_types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export interface PresetOptions {

export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel","zephyr"] as const;

export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgeone" | "edgeone-pages" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zephyr" | "zerops" | "zerops-static";
export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-pages" | "cloudflare-pages-static" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgeone" | "edgeone-pages" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis-handler" | "iis-node" | "koyeb" | "netlify" | "netlify-edge" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-middleware" | "node-server" | "platform-sh" | "render-com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-dev" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zephyr" | "zerops" | "zerops-static";

export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgeone" | "edgeone-pages" | "edgeonePages" | "edgeone_pages" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zephyr" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {});
export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgeone" | "edgeone-pages" | "edgeonePages" | "edgeone_pages" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "netlify" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-middleware" | "nodeMiddleware" | "node_middleware" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "standard" | "static" | "stormkit" | "vercel" | "vercel-dev" | "vercelDev" | "vercel_dev" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zephyr" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {});
44 changes: 44 additions & 0 deletions src/presets/vercel/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Nitro } from "nitro/types";
import { presetsDir } from "nitro/meta";
import { resolveModulePath } from "exsolve";

/**
* Configure local development emulation for the Vercel preset.
*
* When `vercel.queues.triggers` is configured, propagates the trigger list
* to runtime config and injects a runtime plugin that binds each topic to
* the `vercel:queue` hook through env-runner's queue dev bridge.
*
*/
export async function vercelDevModule(nitro: Nitro) {
if (!nitro.options.dev) {
return;
}

const triggers = nitro.options.vercel?.queues?.triggers;
if (!triggers?.length) {
return;
}

if (nitro.options.devServer.runner !== "vercel") {
throw new Error(
`[vercel:queue] Local queue delivery requires the \`vercel\` dev runner, but \`devServer.runner\` is set to "${nitro.options.devServer.runner}". Remove the \`devServer.runner\` override in your \`nitro.config.ts\` or set it explicitly to \`"vercel"\`.`
);
}

// Propagate triggers to the runtime plugin via runtimeConfig.
nitro.options.runtimeConfig.vercel = {
...nitro.options.runtimeConfig.vercel,
queues: {
triggers: triggers.map((t) => ({ ...t })),
},
};

nitro.options.plugins = nitro.options.plugins || [];
nitro.options.plugins.unshift(
resolveModulePath("./vercel/runtime/queue.dev", {
from: presetsDir,
extensions: [".mjs", ".ts"],
})
);
}
16 changes: 15 additions & 1 deletion src/presets/vercel/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
generateStaticFiles,
resolveVercelRuntime,
} from "./utils.ts";
import { vercelDevModule } from "./dev.ts";

import type { VercelFunctionTrigger } from "./types.ts";

Expand Down Expand Up @@ -144,4 +145,17 @@ const vercelStatic = defineNitroPreset(
}
);

export default [vercel, vercelStatic] as const;
export const vercelDev = defineNitroPreset(
{
extends: "nitro-dev",
devServer: { runner: "vercel" },
modules: [vercelDevModule],
},
{
name: "vercel-dev" as const,
aliases: ["vercel"],
dev: true,
}
);

export default [vercel, vercelStatic, vercelDev] as const;
57 changes: 57 additions & 0 deletions src/presets/vercel/runtime/queue.dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { send } from "@vercel/queue";
import type { MessageMetadata } from "@vercel/queue";
import type { NitroAppPlugin } from "nitro/types";
import { useRuntimeConfig } from "nitro/runtime-config";
import { registerVercelQueueConsumer } from "env-runner/runners/vercel/queue-dev";

Check failure on line 5 in src/presets/vercel/runtime/queue.dev.ts

View workflow job for this annotation

GitHub Actions / tests-checks (ubuntu-latest)

Cannot find module 'env-runner/runners/vercel/queue-dev' or its corresponding type declarations.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate any queue-dev implementation in repo:"
fd -i 'queue-dev.ts|queue-dev.mjs|queue-dev.js'

echo
echo "2) Find all usages of registerVercelQueueConsumer:"
rg -n --type=ts -C3 '\bregisterVercelQueueConsumer\b'

echo
echo "3) Find all imports from env-runner in TS files:"
rg -n --type=ts -C2 'from "env-runner[^"]*"'

echo
echo "4) Check alias/path mapping for env-runner:"
fd -i 'tsconfig*.json' --exec sh -c 'echo "--- $1"; jq ".compilerOptions.paths // {}" "$1"' sh {}

Repository: nitrojs/nitro

Length of output: 30841


🏁 Script executed:

# Check if env-runner is listed as a dependency
fd -i 'package.json' -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ==="; cat "{}" | grep -A 50 -B 5 "env-runner" || true'

Repository: nitrojs/nitro

Length of output: 228


🌐 Web query:

env-runner npm package runners/vercel/queue-dev

πŸ’‘ Result:

The string β€œenv-runner npm package runners/vercel/queue-dev” doesn’t resolve cleanly to a specific, documented npm package path or file name in the sources I found. What I could confirm: 1) The npm package β€œenv-runner” (unjs/env-runner) exists and provides β€œrunners” including a Vercel runner at the import path env-runner/runners/vercel (not β€œqueue-dev”). It also documents that it can run across multiple environments/runtimes and exposes a Vercel runner that simulates a Vercel deployment environment with Vercel-like header injection. [1][2] 2) Vercel-specific behavior for env-runner is implemented via β€œVercelEnvRunner”. Its changes include injection of Vercel-related request headers such as x-vercel-deployment-url and x-vercel-id, and it sets process.env values like VERCEL, VERCEL_ENV, VERCEL_REGION, and NOW_REGION in the worker context. [3][4] 3) Vercel Queues are a separate Vercel product (queues durable event streaming). Vercel’s own docs describe authentication and API behavior for Queues, but those docs don’t mention an env-runner β€œqueue-dev” runner. [5][6] If you meant something else by β€œqueue-dev” (for example, a specific file/directory name inside env-runner like runners/vercel/queue-dev, or a tag/example used for local queue development), tell me one of the following and I can re-search precisely: - the full npm package name (including scope/version) you intend, or - the exact GitHub file path you saw (repo + path), or - a snippet of the import/require statement you’re using. Citations: - env-runner README shows available runners and explicitly includes VercelEnvRunner at env-runner/runners/vercel and describes Vercel context/header injection. [1] - env-runner repository overview similarly lists Vercel runner and its purpose. [2] - env-runner commit implementing Vercel env var + header injection (VercelEnvRunner). [3] - env-runner commit adding VERCEL_DEPLOYMENT_ID and testing it in VercelEnvRunner. [4] - Vercel Queues docs (product/API) don’t reference env-runner β€œqueue-dev”. [5][6]

Citations:


🌐 Web query:

registerVercelQueueConsumer env-runner function

πŸ’‘ Result:

There is no documented function named registerVercelQueueConsumer in either the Vercel Queues SDK/docs or the unjs/env-runner project docs. What you likely want (Vercel Queues β€œpush-mode” consumer registration): 1) Configure your consumer endpoint trigger in vercel.json using a queue/v2beta experimentalTriggers entry for your route. Vercel invokes that route automatically in push mode. [1][2][3] Example: { "functions": { "app/api/queues/process-order/route.ts": { "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "orders" }] } } } 2) Export your handler using @vercel/queue’s handleCallback (or handleNodeCallback for certain routers). Messages are acknowledged when your handler completes and retried if the handler throws. [4][3][5] Example: import { handleCallback } from '@vercel/queue'; export const POST = handleCallback(async (message, metadata) => { await processOrder(message); }); How unjs/env-runner fits in: - env-runner is a generic runtime β€œenvironment runner” (it can simulate Vercel via VercelEnvRunner), but its docs don’t mention any Vercel Queue consumer registration helper like registerVercelQueueConsumer. [6][7] - So, in practice, you would still register a Vercel Queues consumer the Vercel way (vercel.json trigger + handleCallback handler), while env-runner may help simulate the Vercel runtime environment during local testing. [6][7][5] If you can share where you saw β€œregisterVercelQueueConsumer” (repo/package/link or the relevant file), I can pinpoint whether it’s from a different library or an internal wrapper and map it to the equivalent Vercel Queues APIs.

Citations:


🏁 Script executed:

# Also check the file to understand what this function is supposed to do
cat -n src/presets/vercel/runtime/queue.dev.ts | head -60

Repository: nitrojs/nitro

Length of output: 2055


Critical: unresolved env-runner/runners/vercel/queue-dev import path

This import does not exist in the env-runner package. The package provides env-runner/runners/vercel but not a queue-dev subpath, and registerVercelQueueConsumer is not an exported function. Verify the correct import path and function name.

Additionally:

  • Line 36: replace console.error with consola.error (required for src/presets/**)
  • Lines 50–52: empty catch block silently swallows teardown errors; add explicit error handling or logging
🧰 Tools
πŸͺ› GitHub Check: tests-checks (ubuntu-latest)

[failure] 5-5:
Cannot find module 'env-runner/runners/vercel/queue-dev' or its corresponding type declarations.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/presets/vercel/runtime/queue.dev.ts` at line 5, The import path and
symbol are wrong: replace the import of registerVercelQueueConsumer from
"env-runner/runners/vercel/queue-dev" with the correct export from
"env-runner/runners/vercel" (verify the exact exported function name in that
package and update the import to match), update any references to
registerVercelQueueConsumer if the exported name differs, change the
console.error call at the place currently using console.error to consola.error,
and inside the teardown catch block (the empty catch around teardown logic) add
explicit error handling/logging (e.g., log the error via consola.error with
context) so errors are not silently swallowed.


interface DevTrigger {
topic: string;
retryAfterSeconds?: number;
initialDelaySeconds?: number;
}

const queueDevPlugin: NitroAppPlugin = (nitroApp) => {
const triggers =
(useRuntimeConfig() as { vercel?: { queues?: { triggers?: DevTrigger[] } } }).vercel?.queues
?.triggers || [];

if (triggers.length === 0) {
return;
}

const unregisters: Array<() => void> = [];

for (const trigger of triggers) {
const unregister = registerVercelQueueConsumer({
topic: trigger.topic,
retryAfterSeconds: trigger.retryAfterSeconds,
handler: async (message: unknown, metadata: unknown) => {
try {
await nitroApp.hooks.callHook("vercel:queue", {
message,
metadata: metadata as MessageMetadata,
send,
});
} catch (error) {
console.error("[vercel:queue]", error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion | 🟠 Major | ⚑ Quick win

Use Nitro/consola logger instead of console.error

Switch this to nitroApp.logger (or consola) to keep logging consistent with preset/dev conventions.

Suggested diff
-          console.error("[vercel:queue]", error);
+          nitroApp.logger.error("[vercel:queue]", error);

As per coding guidelines src/{build,dev,cli,presets}/**/*.{ts,js,mjs}: Use consola for logging in build/dev code, or nitro.logger when available.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error("[vercel:queue]", error);
nitroApp.logger.error("[vercel:queue]", error);
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/presets/vercel/runtime/queue.dev.ts` at line 36, Replace the direct
console.error call in queue.dev.ts with the preset/dev logger: use
nitroApp.logger.error(...) if a nitroApp instance is in scope, otherwise import
and use consola.error(...); specifically change the
console.error("[vercel:queue]", error) invocation to call the appropriate
logger, and add the required import or ensure nitroApp is available in the
surrounding function where the error is handled.

nitroApp.captureError?.(error as Error, {
tags: ["vercel:queue"],
});
// Rethrow so @vercel/queue schedules a local retry.
throw error;
}
},
});
unregisters.push(unregister);
}

nitroApp.hooks.hook("close", () => {
for (const unregister of unregisters) {
try {
unregister();
} catch {}
}
});
};

export default queueDevPlugin;
Loading