Deno desktop error report JSON schema
Error reports are sent as JSON with the following fields: version (1), message (string - the error's message), stack (string - the error's stack, source-mapped where possible), appVersion (string | null - Deno.desktopVersion at time of error), timestamp (ISO 8601 string in UTC), platform ("darwin" | "windows" | "linux"), arch (string - from Deno.build.arch). The Content-Type header is application/json.
Desktop error report HTTP delivery
Reports are sent as a single POST request with no retry. If the server is down, the report is lost. For high-importance reports, consider queuing them locally and resending on the next application launch.
What gets reported by Deno desktop error reporting
Captured: uncaught exceptions in Deno-side code, unhandled rejections in Deno-side code, uncaught exceptions in renderer-side JavaScript (caught via renderer's error event), Rust panics in the Deno runtime, and Rust panics in the rendering backend. Not captured: console.error/console.warn calls (not errors), exceptions handled with try/catch, or errors thrown inside binding handlers (which are caught by the webview and do not surface as uncaught errors).
Binding handler errors and error reporting
Errors thrown inside a binding handler propagate to the webview side and reject the calling promise. They are not reported as uncaught errors because the webview catches them. To report them, log them yourself in the binding handler.
CI matrix example for deno desktop
A typical GitHub Actions matrix for building platform-native installers in parallel includes: { os: macos-14, target: aarch64-apple-darwin }, { os: macos-15-intel, target: x86_64-apple-darwin }, { os: windows-latest, target: x86_64-pc-windows-msvc }, { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }. Cross-compiling from a single host (e.g. only running on ubuntu-latest with --all-targets) works for bundles and Linux .AppImage. Only the macOS .dmg needs a macOS host.
deno desktop CLI commands for building
deno desktop main.ts builds for the host platform. deno desktop --target aarch64-apple-darwin main.ts builds for a specific target. deno desktop --all-targets main.ts builds for every supported target in one go.
macOS output formats
macOS outputs are: MyApp.app/ (default; .app bundle produced directly), MyApp.dmg (drag-to-Applications disk image produced by hdiutil). The .app bundle has a standard layout with Contents/Info.plist, Contents/MacOS/MyApp launcher, Contents/Resources/icon.icns, and Contents/Frameworks/Chromium Embedded Framework.framework/. Self-extracting mode is enabled by default: the embedded virtual filesystem extracts to disk on first run so frameworks like Next.js find their build output relative to CWD.
Windows output formats
Windows outputs are: MyApp/ (default; directory with launcher and support files), MyApp.msi (Windows Installer package for per-machine install). The MyApp/ directory contains: MyApp.bat launcher, denort.dll (Deno runtime + code), *.dll files (rendering backend and CEF libraries), resources.pak and locales/ (CEF support files), AppIcon.ico (optional icon). The .msi installs per-machine under %ProgramFiles%\<AppName>\ and registers an uninstaller. The MSI is authored in pure Rust and cross-compiles from any host.
Linux output formats
Linux outputs are: my-app/ (default; app directory with launcher shell script), my-app.AppImage (single-file portable bundle), my-app.deb (Debian/Ubuntu package), my-app.rpm (Fedora/RHEL package). The app directory contains: my-app launcher script, libdenort.so (Deno runtime + code), *.so files (rendering backend and CEF libraries), resources.pak and locales/ (CEF support files), AppIcon.png (optional icon). AppImage is the most portable Linux format: one file, no install step, runs on any modern distro. deno desktop builds it directly by packing the app directory into a SquashFS image and prepending the AppImage Type-2 runtime, adding required AppRun, .desktop, and icon entries. No external tool like appimagetool is needed, and it works from any build host.
deno desktop compression options
deno desktop --compress main.ts produces a self-extracting, compressed bundle that unpacks on first launch to a per-user data directory, shrinking a webview hello-world from about 66 MB to 19 MB. Codecs can be chosen explicitly: --compress=xz (produces smaller artifact) or --compress=zstd (trades some size for faster first-launch decompression).
deno desktop output path priority and configuration
The output path is determined by priority: 1) The --output CLI flag, 2) The desktop.output field in deno.json (per-platform), 3) The default: the project name with the platform-appropriate extension. CLI example: deno desktop --output ./builds/MyApp-1.4.0.dmg main.ts. deno.json example with per-platform outputs: { "desktop": { "output": { "macos": "./dist/macos/MyApp.app", "windows": "./dist/windows/MyApp", "linux": "./dist/linux/my-app.AppImage" } } }
deno desktop JavaScript engine selection
deno desktop accepts --engine v8|quickjs to select the JavaScript engine inside the downloaded runtime library. V8 is the default; the experimental QuickJS build is smaller but does not receive the same security updates.
deno desktop cross-compilation model
There is no Rust toolchain involved in cross-compiling a desktop app. You are not compiling Rust on the host; you are downloading prebuilt artifacts (denort binary and backend archive) for the target and packaging them with your code. Both downloads are SHA-256 verified and cached under <deno_dir>/. This is the same model as deno compile --target.
deno desktop .dmg build requirement
Icon assembly (.icns, .ico) and Linux .AppImage are produced on any host. The macOS .dmg must be built on a macOS host because it shells out to hdiutil. To produce a .dmg from another platform, build it on a macOS CI machine.
macOS code signing in deno desktop
deno desktop code-signs the macOS bundle by default with an ad-hoc signature (-), which gives the app a stable code identity and grants notification permission, but is not enough to distribute without Gatekeeper warnings. To produce a distributable, notarizable bundle, set a real signing identity in deno.json (signing must run on a macOS host, using codesign(1)). Example: { "desktop": { "app": { "identifier": "com.example.myapp" }, "macos": { "codesignIdentity": "Developer ID Application: Acme, Inc. (TEAMID)" } } }. With a real identity, the bundle is signed with Hardened Runtime and a secure timestamp. Notarization is still a separate step using xcrun notarytool submit and stapling the ticket.
Windows executable signing in deno desktop
On Windows, sign the produced executables (the backend .exe and denort.dll in the output directory) externally, for example: signtool sign /f cert.pfx /tr <timestamp> <file>.
deno desktop cross-compilation and distribution
deno desktop is available starting in Deno v2.9.0. It cross-compiles from any host to build for macOS Intel, macOS arm64, Windows x86_64, Linux arm64, and Linux x86_64. Backend binaries (CEF, WebView, etc.) are downloaded as needed. No platform-specific toolchain is required on the host.
deno desktop target triples
Supported target triples for deno desktop: aarch64-apple-darwin (macOS arm64), x86_64-apple-darwin (macOS Intel), x86_64-pc-windows-msvc (Windows x86_64), aarch64-unknown-linux-gnu (Linux arm64), x86_64-unknown-linux-gnu (Linux x86_64).
deno desktop updates via autoUpdate
Once your binary is in users' hands, ship updates via Deno.autoUpdate(): bsdiff patches shipped from your own server, no app store required.
Open connections persist across HMR
With V8's Debugger.setScriptSource hot-swap, open file handles, network connections, child processes, and HTTP listeners are all preserved across module reloads. Timers and intervals keep firing on their original schedule unless explicitly cleared with clearTimeout or clearInterval.
Module-level state persists across HMR
With V8's Debugger.setScriptSource hot-swap, module-level state is preserved across reloads. This includes top-level let bindings, top-level Map objects, and other module-scope variables.
Plain-app HMR implementation
For projects without a detected framework, --hmr watches source files and uses V8's Debugger.setScriptSource to hot-swap modules into the running isolate. When source files are edited and saved, changes apply without restarting the runtime, reloading the webview, or unbinding the listening socket.
Framework HMR behavior
When a framework is detected, `deno desktop --hmr` runs the framework's own dev server instead of its production server. The webview connects to that dev server directly, providing fast refresh, state preservation, and error overlays the same as in a browser tab. The dev server's exact behavior comes from the framework. The framework's dev script does not need to be run separately; `deno desktop --hmr` starts it as part of the desktop runtime.
HMR runtime persistence
In both framework and plain-app HMR modes, the Deno runtime and rendering backend (CEF, WebView, etc.) stay alive across changes. There is no full restart, no webview teardown, no reconnect.
--hmr cooperates with --inspect
The --hmr flag cooperates with --inspect, allowing you to attach a debugger to a running --hmr session and step through newly-swapped code.
Source maps required for HMR stack traces
Source maps are required for accurate line numbers in stack traces after a hot swap. They are emitted by default; do not disable them in bundler config.
--hmr is for development only
The --hmr flag is for development only. Do not ship a binary built with --hmr; the file watcher and inspector overhead are not appropriate for end users.
Browser-side HMR coexists with Deno-side HMR
Browser HMR (such as fast refresh in React or Vue's HMR runtime) runs entirely inside the rendering backend and is separate from Deno-side HMR. Both coexist: a change to a React component file applies browser HMR inside the webview, while a change to a Deno.serve() handler applies Deno-side HMR inside the runtime. Both happen on save.
Debugger.setScriptSource limitations
Debugger.setScriptSource cannot replace top-level statements that have already executed (such as a console.log at module scope), the signature of a class (adding fields or changing constructors), or the set of imports (adding a new import line requires a full reload). When a change is too disruptive, --hmr falls back to a full reload of the affected module. If even a full reload is not safe, it logs a warning suggesting a full restart.
deno desktop --hmr basic syntax
The --hmr flag enables hot module replacement during development. The basic syntax is `deno desktop --hmr .` or `deno desktop --hmr main.ts`. The --hmr mode is selected automatically based on what the project looks like.
HMR modes based on project type
For detected frameworks (Next.js, Astro, Fresh, etc.), --hmr runs the framework's own dev server. For plain Deno.serve() scripts without a detected framework, --hmr watches source files and uses V8's Debugger.setScriptSource to hot-swap modules into the running isolate.
deno desktop --hmr availability
The `deno desktop --hmr` feature is available starting in Deno v2.9.0.
Plain-app HMR example
Example: A main.ts file with `Deno.serve((req) => { return new Response("hello world"); });` can be run with `deno desktop --hmr main.ts`. When you edit main.ts (change the response body, add a route), the change applies on save without runtime restart or webview reload.
When HMR changes take effect
When using Debugger.setScriptSource, replaced functions execute their new bodies the next time they are called. A request handler change takes effect on the next request, a timer callback change on the next firing, and an event listener change on the next event.
deno desktop framework auto-detection
deno desktop automatically detects and supports Next.js, Astro, Fresh, Remix, React Router, Nuxt, SvelteKit, SolidStart, TanStack Start, and Vite SSR projects. It runs the production server in release mode and the dev server with hot reload under --hmr. Most frameworks require no special adapter.
deno desktop cross-compilation
The same machine can build deno desktop applications for macOS, Windows, and Linux. Backends are downloaded as needed, not built locally.
deno desktop purpose
deno desktop turns a Deno project (from a single TypeScript file to a Next.js app) into a self-contained desktop application. The output is a redistributable binary that bundles code, the Deno runtime, and a web rendering engine into one bundle per platform.
deno desktop webview backend
The default WebView backend uses the operating system's own webview for small binaries. The entire npm ecosystem is available through Deno's Node compatibility layer. An optional bundled Chromium (CEF) backend is available for identical rendering across macOS, Windows, and Linux.
deno desktop in-process bindings
deno desktop uses in-process bindings instead of IPC for backend and UI communication. Values are encoded as they cross the call boundary, but there is no cross-process round-trip between Deno code and the webview.
deno desktop auto-update mechanism
deno desktop includes built-in binary-diff auto-update. Ship a single latest.json manifest and bsdiff patches; the runtime polls, applies, and rolls back automatically on failed launches.
deno desktop basic example
A one-file desktop app with deno desktop: create main.ts with Deno.serve(() => new Response("<h1>Hello, desktop</h1>", { headers: { "content-type": "text/html" } })), then run deno desktop main.ts. The compiled binary opens a window with a local HTTP server. Execute the binary directly: ./main on macOS/Linux or .\main.exe on Windows.
deno desktop Deno.serve binding
Deno.serve() automatically binds to the address the webview navigates to in a deno desktop application, so you do not need to pass a port or hostname.
SvelteKit with deno desktop
For SvelteKit projects: run `npm run build`, then `deno desktop .`. deno desktop looks for .deno-deploy/server.ts first (the Deno Deploy adapter's output), falling back to .output/server/index.{ts,mjs} (the Node adapter's output). In dev mode, it runs the Vite dev server. If using a different adapter like @sveltejs/adapter-static, serve the output directory yourself with Deno.serve() instead of relying on detection.
Nuxt with deno desktop
For Nuxt projects: run `npm run build` to produce .output/, then `deno desktop .`. deno desktop uses Nuxt's Nitro output at .output/server/index.{ts,mjs}. In dev mode under --hmr, it runs `nuxi dev`.
Hot reload with deno desktop framework projects
Under --hmr, the framework's own dev server runs and the webview connects to it directly. State preservation, fast refresh, and error overlays all work the same as in a browser.
What deno desktop detection does
When a framework is detected, the CLI performs four steps: (1) generates a synthetic entry point that imports the framework's production server or dev server under --hmr; (2) embeds the build output into the binary's virtual filesystem (.next/, dist/, .output/, _fresh/, build/, etc., depending on the framework); (3) self-extracts the VFS at runtime so framework code finds its build output relative to its working directory; (4) runs the framework server as a Deno.serve() handler with the webview navigating to the bound port.
Build projects before running deno desktop
`deno desktop` does not run framework build commands like `next build` or `astro build` for you. The user must run the framework's build step first to produce the build output that will be embedded.
Next.js with deno desktop
For Next.js projects: run `npx next build` to produce .next/, then `deno desktop .`. In production, deno desktop imports next/dist/cli/next-start.js. In dev mode under --hmr, it imports next/dist/cli/next-dev.js. The .next/ directory is embedded. Both App Router and Pages Router work.
Astro with deno desktop
For Astro projects: run `npm run build` to produce dist/, then `deno desktop .`. For Astro projects with an SSR adapter, deno desktop imports ./dist/server/entry.mjs. For static projects without an adapter, deno desktop serves via Deno's static file server pointed at dist/. Both modes work; SSR has access to the full Astro request lifecycle, static mode is faster to start.
Fresh with deno desktop
For Fresh projects: run `deno task build` to produce _fresh/, then `deno desktop .`. Fresh 2.x imports _fresh/server.js and runs the Vite dev server under --hmr. Fresh 1.x imports ./main.ts directly.
Remix with deno desktop
For Remix projects: run `npm run build`, then `deno desktop .`. In production, deno desktop runs remix-serve against the build/ directory. In dev mode under --hmr, it uses @remix-run/dev CLI.
React Router with deno desktop
React Router framework mode is detected by @react-router/dev in package.json. Both SPA mode (ssr: false) and server-side rendering are supported. React Router's default server entry targets Node.js and uses renderToPipeableStream from react-dom/server. Deno resolves react-dom/server to a Web Streams build that uses renderToReadableStream instead. Users must add a Deno-compatible app/entry.server.tsx file before running `react-router build`. React Router also uses this file to prerender the SPA fallback when ssr: false. After creating the entry file, run `deno task build` then `deno desktop .`.
React Router Deno entry.server.tsx example
import type { EntryContext } from "react-router";
import { ServerRouter } from "react-router";
import { renderToReadableStream } from "react-dom/server";
import { isbot } from "isbot";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
) {
if (request.method.toUpperCase() === "HEAD") {
return new Response(null, {
status: responseStatusCode,
headers: responseHeaders,
});
}
let statusCode = responseStatusCode;
const body = await renderToReadableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
statusCode = 500;
console.error(error);
},
},
);
if (
isbot(request.headers.get("user-agent") || "") || routerContext.isSpaMode
) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: statusCode,
});
}
This example shows a Deno-compatible entry.server.tsx file for React Router that uses renderToReadableStream instead of renderToPipeableStream.
React Router deno desktop deployment
For React Router with deno desktop: production serves static client assets from build/client and, for SSR projects, routes requests through build/server/index.js.
Framework fallback behavior when detection fails
If no framework config is detected, `deno desktop` falls back to treating the path as a script, the same as `deno desktop main.ts`. The user must write a Deno.serve() handler and serve their own UI.
Framework auto-detection by config files
Detection is based on config files and package.json dependencies. The detection order and signals are: Next.js detected by next.config.{js,mjs,ts}; Astro by astro.config.{mjs,ts,js}; Fresh by fresh.gen.ts or _fresh/ directory; Remix by @remix-run/react or @remix-run/dev in package.json; React Router by @react-router/dev in package.json; Nuxt by nuxt.config.{ts,js,mjs}; SvelteKit by svelte.config.{js,ts}; SolidStart by @solidjs/start in package.json; TanStack Start by @tanstack/{react,solid}-start in package.json; Vite by vite.config.* or a vite dependency in package.json. The first match wins.
deno desktop basic usage
The `deno desktop` command accepts a directory path and auto-detects the framework, picks the right entry point, embeds the build output in the binary, and runs the framework's production server with the webview pointed at it. The basic command is `deno desktop .` inside a project directory.
deno desktop requires Deno 2.9.0
The `deno desktop` command is available starting in Deno v2.9.0. Users on earlier versions must update Deno to use it.
Opt out of framework detection with explicit script
There is no flag to force framework detection. To opt out and ship a framework project without using detection, pass an explicit script entry like `deno desktop ./my-server.ts`. In the script file, the user imports and starts the framework themselves. Use this approach when control over startup is needed that the detection cannot express.
Vite with deno desktop
Vite projects are detected by a vite.config.* file or a vite dependency in package.json. Vite sits at the lowest bundler priority in detection, so meta-frameworks built on Vite (Astro, SvelteKit, Nuxt, Remix, React Router, SolidStart, TanStack Start) are matched by their own config or dependency first. For Vite SSR projects (with a server.{ts,js,mjs} entry alongside vite.config.*), the SSR entry runs directly in production, and in dev mode under --hmr, the Vite dev server runs in middleware mode. For SPA or MPA Vite projects (no server entry), deno desktop serves the static `vite build` output in dist/ over HTTP with an index.html fallback for client-side routers. Users must run `vite build` first.