Skip to content

presets(deno): add deno-handler preset for library-mode embedding#4285

Draft
petarbasic wants to merge 1 commit into
nitrojs:v2from
drunomics:feat/deno-handler
Draft

presets(deno): add deno-handler preset for library-mode embedding#4285
petarbasic wants to merge 1 commit into
nitrojs:v2from
drunomics:feat/deno-handler

Conversation

@petarbasic
Copy link
Copy Markdown

@petarbasic petarbasic commented May 26, 2026

Discussion thread: #4286

Motivation

Multi-tenant Deno hosting (one process serving multiple Nitro builds) needs
a way to embed the Nitro handler in a host process instead of binding a port
per build. The existing deno-server preset calls Deno.serve() as a
module-level side effect — importing the built index.mjs immediately starts
a server, making it unusable as a library.

The Cloudflare preset family already has the equivalent split:
cloudflare-module exports { fetch } instead of side-effecting a listen.
This PR adds the same pattern for the Deno family — a deno-handler sibling
to deno-server.

Use case

A multi-tenant dispatcher routing many Nitro builds in a single Deno
process by Host header, in-process (no localhost TCP):

// dispatcher.ts
import { fetch as siteA } from "./site-a/.output/server/index.mjs";
import { fetch as siteB } from "./site-b/.output/server/index.mjs";

const handlers = new Map([["site-a", siteA], ["site-b", siteB]]);

Deno.serve({ port: 3000 }, (req) => {
  const segment = (req.headers.get("host") ?? "").split(".")[0];
  return handlers.get(segment)?.(req)
    ?? new Response("unknown host", { status: 404 });
});

Same primitive Cloudflare Workers solves via isolate.fetch() — no
per-build port binding, no per-request localhost loopback.

Implementation

New preset deno-handler (sibling of deno-server / deno-deploy):

  • Mirrors deno-server's entry shape minus the Deno.serve() call.
  • Exports fetch (named) and default { fetch } — mirrors
    cloudflare-module's convention.
  • Drops startScheduleRunner() (library callers own the scheduling
    lifecycle).
  • Same rollupConfig, exportConditions, websocket adapter,
    addEventListener error trapping as deno-server.
  • No commands.preview / hooks.compiled (embedding host owns
    deno.json).

Verification

test/presets/deno-handler.test.ts boots a minimal Deno.serve wrapper
around the exported fetch and runs the shared testNitro() conformance
suite against it.

43/43 tests pass — identical pass/skip ratios to the deno-server v2
baseline.
Skip-list updates in test/tests.ts mirror deno-server's
(same Deno-runtime limitations: module-conflict resolution, sourcemap,
SQL DB driver, BroadcastChannel unstable).

Also verified end-to-end with two real Nitro builds + a routing dispatcher
(multi-site Host-header routing, no port binding at module import) —
matches the production use case the preset is designed for.

Notes

  • WebSocket upgrade requires the caller to pass info
    (Deno.ServeHandlerInfo) as the second arg to fetch(request, info);
    throws clearly if missing. Non-WS calls only need the Request.
  • Scheduled tasks are deliberately omitted from the library entry —
    the host driver owns scheduling.
  • Happy to discuss the design via issue first if preferred; opening this
    as a concrete starting point with passing conformance tests.

The existing deno-server preset calls Deno.serve() as a module-level
side effect — importing the built index.mjs immediately starts a
server, making it unusable as a library.

This adds a sibling deno-handler preset that exports { fetch } instead
of side-effecting a listen, mirroring the cloudflare-worker →
cloudflare-module split. Enables multi-tenant Deno hosting where a
parent process routes requests to multiple Nitro builds in-process
(no localhost TCP between them).

Conformance: 43/43 tests pass, identical to deno-server v2 baseline.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

@petarbasic is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec52e8d3-8d1a-40a9-9fb0-da1dbf871901

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant