diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index f207e0384a..069b97375c 100644 --- a/src/presets/_types.gen.ts +++ b/src/presets/_types.gen.ts @@ -20,6 +20,6 @@ export interface PresetOptions { export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel"] as const; -export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "deno-server-legacy" | "digital-ocean" | "edgio" | "firebase" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; +export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-dev" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-handler" | "deno-server" | "deno-server-legacy" | "digital-ocean" | "edgio" | "firebase" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; -export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "deno-server-legacy" | "denoServerLegacy" | "deno_server_legacy" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "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" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); +export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-dev" | "cloudflareDev" | "cloudflare_dev" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-handler" | "denoHandler" | "deno_handler" | "deno-server" | "denoServer" | "deno_server" | "deno-server-legacy" | "denoServerLegacy" | "deno_server_legacy" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "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" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); diff --git a/src/presets/deno/preset.ts b/src/presets/deno/preset.ts index 9979c4fd62..091ec5cff8 100644 --- a/src/presets/deno/preset.ts +++ b/src/presets/deno/preset.ts @@ -72,4 +72,23 @@ const denoServer = defineNitroPreset( } ); -export default [denoServerLegacy, denoDeploy, denoServer] as const; +const denoHandler = defineNitroPreset( + { + extends: "node-server", + entry: "./runtime/deno-handler", + exportConditions: ["deno"], + rollupConfig: { + external: (id) => id.startsWith("https://"), + output: { + hoistTransitiveImports: false, + }, + }, + }, + { + name: "deno-handler" as const, + compatibilityDate: "2025-05-20", + url: import.meta.url, + } +); + +export default [denoServerLegacy, denoDeploy, denoServer, denoHandler] as const; diff --git a/src/presets/deno/runtime/deno-handler.ts b/src/presets/deno/runtime/deno-handler.ts new file mode 100644 index 0000000000..eea9f97566 --- /dev/null +++ b/src/presets/deno/runtime/deno-handler.ts @@ -0,0 +1,58 @@ +import "#nitro-internal-pollyfills"; +import "./_deno-env-polyfill"; +import { useNitroApp } from "nitropack/runtime"; + +import type { Deno as _Deno } from "@deno/types"; +import wsAdapter from "crossws/adapters/deno"; + +// TODO: Declare conflict with crossws +declare global { + const Deno: typeof import("@deno/types").Deno; +} + +const nitroApp = useNitroApp(); + +// Websocket support +const ws = import.meta._websocket + ? wsAdapter(nitroApp.h3App.websocket) + : undefined; + +export async function fetch( + request: Request, + info?: _Deno.ServeHandlerInfo +): Promise { + // https://crossws.unjs.io/adapters/deno + if ( + import.meta._websocket && + request.headers.get("upgrade") === "websocket" + ) { + if (!info) { + throw new Error( + "deno-handler: websocket upgrade requires the second `info` arg from Deno.serve" + ); + } + return ws!.handleUpgrade(request, info); + } + + const url = new URL(request.url); + + // https://deno.land/api?s=Body + let body; + if (request.body) { + body = await request.arrayBuffer(); + } + + return nitroApp.localFetch(url.pathname + url.search, { + host: url.hostname, + protocol: url.protocol, + headers: request.headers, + method: request.method, + redirect: request.redirect, + body, + }); +} + +// Library mode: caller invokes `fetch(request, info?)` directly. No port is +// bound; the embedding process owns the listen lifecycle (e.g. a multi-site +// dispatcher routing several Nitro builds in the same Deno runtime). +export default { fetch }; diff --git a/test/presets/deno-handler.test.ts b/test/presets/deno-handler.test.ts new file mode 100644 index 0000000000..9bb583fd45 --- /dev/null +++ b/test/presets/deno-handler.test.ts @@ -0,0 +1,40 @@ +import { execa, execaCommandSync } from "execa"; +import { getRandomPort, waitForPort } from "get-port-please"; +import { promises as fsp } from "node:fs"; +import { resolve } from "pathe"; +import { describe } from "vitest"; +import { setupTest, testNitro } from "../tests.ts"; + +const hasDeno = + execaCommandSync("deno --version", { stdio: "ignore", reject: false }) + .exitCode === 0; + +describe.runIf(hasDeno)("nitro:preset:deno-handler", async () => { + const ctx = await setupTest("deno-handler"); + testNitro(ctx, async () => { + const port = await getRandomPort(); + // The preset emits a handler-only build; spin up a minimal Deno wrapper + // that imports the exported `fetch` and serves it. Library users would + // do the same in their own dispatcher or test harness. + const wrapperPath = resolve(ctx.outDir, "server/wrapper.mjs"); + await fsp.writeFile( + wrapperPath, + `import { fetch } from "./index.mjs";\nDeno.serve({ port: ${port}, hostname: "127.0.0.1" }, fetch);\n` + ); + execa("deno", ["run", "-A", wrapperPath], { + cwd: ctx.outDir, + stdio: "ignore", + }); + ctx.server = { + url: `http://127.0.0.1:${port}`, + close: () => { + // p.kill() + }, + } as any; + await waitForPort(port, { delay: 1000, retries: 20, host: "127.0.0.1" }); + return async ({ url, ...opts }) => { + const res = await ctx.fetch(url, opts); + return res; + }; + }); +}); diff --git a/test/tests.ts b/test/tests.ts index 77e0019eb7..72e2058cfc 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -495,7 +495,7 @@ export function testNitro( }); }); - it.skipIf(ctx.preset === "deno-server")( + it.skipIf(["deno-server", "deno-handler"].includes(ctx.preset))( "resolve module version conflicts", async () => { const { data } = await callHandler({ url: "/modules" }); @@ -690,6 +690,7 @@ export function testNitro( "nitro-dev", "vercel", (nodeMajorVersion || 0) < 18 && "deno-server", + (nodeMajorVersion || 0) < 18 && "deno-handler", (nodeMajorVersion || 0) < 18 && "bun", ].filter(Boolean); if (notSplittingPresets.includes(ctx.preset)) { @@ -724,6 +725,7 @@ export function testNitro( // TODO: Investigate ctx.preset === "bun" || ctx.preset === "deno-server" || + ctx.preset === "deno-handler" || ctx.preset === "nitro-dev" )("sourcemap works", async () => { const { data } = await callHandler({ url: "/error-stack" }); @@ -806,6 +808,7 @@ export function testNitro( [ "bun", "deno-server", + "deno-handler", "deno-deploy", "netlify", "netlify-legacy", @@ -853,7 +856,10 @@ export function testNitro( ) { continue; } - if (ctx.preset === "deno-server" && key === "globals:BroadcastChannel") { + if ( + ["deno-server", "deno-handler"].includes(ctx.preset) && + key === "globals:BroadcastChannel" + ) { continue; // unstable API } expect(data[key], key).toBe(true);