Deno Lands `deno desktop` Subcommand: WEF-Backed Self-Contained Desktop Apps with Deno.BrowserWindow, Unified DevTools, and Cross-Compile to macOS, Windows, and Linux

Deno Lands `deno desktop` Subcommand: WEF-Backed Self-Contained Desktop Apps with Deno.BrowserWindow, Unified DevTools, and Cross-Compile to macOS, Windows, and Linux

lschvn

Deno merged deno desktop on June 16, 2026 (PR #33441, originally filed in April 2026 as issue #3234), adding a new subcommand that compiles a Deno project into a self-contained desktop application. The same morning also landed three smaller PRs: deno link / deno unlink for the links array in deno.json, deno test --shard to split a test run across machines, and a fetch request_builder_hook for cloud deployments. The headline is the desktop subcommand: it gives Deno a first-class path to ship cross-platform desktop apps with a single deno desktop <entry> invocation, and the implementation is the biggest single addition to the Deno CLI since Deno 2.7 stabilized the Temporal API and Windows-on-Arm builds.

What deno desktop actually does

deno desktop <entry> compiles a Deno project into a self-contained desktop app, built on WEF (WebView Engine Foundation). The CLI downloads prebuilt WEF backends from github.com/denoland/wef/releases, pins the version via the project's Cargo.lock, SHA-256-verifies them, and caches them under <deno_dir>/wef/<version>/. For development against a local WEF checkout, WEF_DEV_DIR points the CLI at a local directory instead of downloading.

There are three backends, selectable per project:

  • cef, the default, a bundled Chromium (CEF is the same engine family Electron's renderer is built on).
  • webview, the host operating system's webview (Edge WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux).
  • raw, a winit window with no embedded engine, for projects that want to draw into the window themselves.

Deno.serve() in the entry auto-binds to the port the webview navigates to, via a new DENO_SERVE_ADDRESS environment variable that the desktop runtime injects. A framework project can keep its normal port config and let the desktop runtime do the binding.

Framework auto-detection and HMR

The CLI auto-detects framework projects in cli/tools/framework.rs, with first-class support for Next.js, Astro, Fresh, Remix, Nuxt, SvelteKit, SolidStart, TanStack Start, and Vite SSR. A detected framework project gets the framework's production server by default; under --hmr, the framework's dev server runs instead (Next's dev server for Next, Nuxt's dev server for Nuxt, Vite's dev server for Vite SSR, etc.).

Non-framework projects still get hot-reload under --hmr, but through a different mechanism. The CLI uses a file-watcher plus a CDP Debugger.setScriptSource hot-swap to push edits into the running V8 isolate while the Deno runtime and the CEF process stay alive. The practical effect is the same: edit a file, see the change, the window does not reload. The asymmetry is that framework projects get the framework's own HMR pipeline, which is usually more granular (component-scoped updates, fast-refresh boundaries, framework-level state preservation), while non-framework projects get V8-script-level hot-swap. The trade-off is documented in the PR: icon sets are not supported in --hmr mode yet, only a single icon path works in that mode.

Deno.BrowserWindow and the native API surface

The new Deno.BrowserWindow API (declared in cli/tsc/dts/lib.deno.desktop.d.ts) is what user code calls to drive a window. The surface covers window lifecycle (show / hide / focus / close / reload), size and position, alwaysOnTop, navigation, app and context menus, native window handles, and keyboard, mouse, wheel, resize, and focus events. Two native-only modules slot in alongside: Deno.dock (macOS-only) for dock-tile integration, and Deno.Tray (cross-platform) for status-area icons with tooltips, dark-mode variants, and context menus.

A bind / unbind RPC channel exposes the webview's JS to the Deno side as bindings.<name>(), and the reverse direction is executeJs from Deno into the webview. The runtime integration is the part that has the most leverage on existing JS code: prompt(), alert(), and confirm() in the webview become native popups, and uncaught errors show a native alert with an optional POST to desktop.errorReporting.url for crash reporting. The auto-updater exposes Deno.desktopVersion and a Deno.autoUpdate({ url, interval, onUpdateReady, onRollback }) that polls <url>/latest.json, applies bsdiff patches to the dylib (via qbsdiff), stages the result for the next launch, and rolls back on a failed launch.

One DevTools session, two V8 isolates

The --inspect and --inspect-brk and --inspect-wait flags go through a new CDP multiplexer in cli/tools/desktop_devtools.rs that exposes the Deno runtime V8 and the CEF renderer V8 as two attached targets in a single DevTools session. The user-facing effect is a single Console dropdown (Renderer / Deno) and a single Sources panel with both threads. The multiplexer speaks Chrome DevTools Protocol to the inspector client and multiplexes it onto two backends, one CDP connection to the runtime's V8 and one to the renderer's V8.

--inspect-brk pauses both isolates at startup (Deno via its own mechanism, CEF via an injected Debugger.enable + Debugger.pause before navigation). For most desktop debugging, that is the workflow: pause both, set a breakpoint in the renderer, set a breakpoint in the runtime, unpause, drive the app, and step between threads in the same Sources panel. The CEF target is the same DevTools target Chromium uses, including the same Performance, Memory, and Network panels; the Deno target is the standard Deno V8 target with deno_core introspection.

Cross-compile and distribution

The --target and --all-targets flags download prebuilt denort binaries and WEF backends for the target triple. The outputs are:

  • macOS: a .app bundle (framework under Contents/Frameworks/), with .dmg produced via hdiutil.
  • Windows: a .exe plus a DLLs directory.
  • Linux: an app directory and a .AppImage produced via appimagetool.

Codesigning is implemented on macOS via the macos.codesignIdentity key in deno.json (with an ad-hoc signature fallback for unsigned builds), but notarization and stapling via notarytool are not wired up yet. Windows MSI installers, Linux .deb / .rpm installers, clipboard, and secureStorage are all still TODO in the PR description.

OS-specific limits worth knowing up front

The PR description is unusually explicit about the gaps. Six limitations are called out as "degrade gracefully" but worth knowing:

  • Auto-update is unix-only. apply_pending_update, get_dylib_path, and AutoUpdateState are #[cfg(unix)], so on Windows the staged-update, bsdiff, and rollback path is a no-op.
  • NAPI native-addon support is unix-only. promote_dylib_symbols_to_global (the re-dlopen with RTLD_GLOBAL that makes NAPI symbols visible to addons like next-swc) is #[cfg(unix)]; on Windows, frameworks that pull native addons may fail to load them.
  • Notification permissions are macOS-specific. The LAUFEY backend is launched via disclaim_spawn (posix_spawn with TCC responsibility disclaimed) so it becomes its own permission principal; without it, UNUserNotificationCenter.requestAuthorization fails.
  • Linux is X11-only. The generated launcher forces --ozone-platform=x11 and GDK_BACKEND=x11 because the LAUFEY mouse, focus, and resize event monitor uses XI2 on X11. On Wayland sessions it runs through XWayland; native Wayland is unsupported.
  • Deno.dock is macOS-only.
  • Icon sets are not supported in --hmr mode (any platform).

The motivation for shipping with these gaps is the same as every "ship the first cut, file the limits" release: the desktop subcommand is the first time Deno has shipped a non-server runtime, and getting the WEF integration, the Deno.BrowserWindow API, the framework auto-detect, the unified DevTools, the auto-updater, and the cross-compile working on at least one platform (macOS) is the prerequisite for everything else. The cross-platform gaps are real, but they are gaps in the first cut, not gaps in the architecture.

The other three PRs from the same morning

The same Deno main branch saw three other features land between 06:59 and 07:15 UTC, before deno desktop at 10:41 UTC. They are smaller in scope but they are all primary-source-verified on the same release and they round out the morning's "Deno adds a bunch of plumbing" story.

deno link and deno unlink give first-class CLI access to the links array in deno.json, which has been the user-facing way to use a local JSR package in place of a registry version, but until now had no CLI to manipulate. deno link <path> validates the path, appends the relative path to links (creating the array if missing, preserving formatting via the existing CST machinery), and runs install. deno unlink <path-or-name> removes an entry by literal path, resolved path, or by the linked package's JSR name read from the target's deno.json. When no argument matches, unlink exits non-zero, so a typo is detectable from scripts. As a discoverability fix, deno add ./local-dir and deno install ./local-dir now bail with a "Did you mean deno link ./local-dir?" hint when the spec resolves to a linkable directory.

deno test --shard=<i>/<n> brings Vitest / Jest / Playwright-style CI sharding to deno test. The discovered test files are sorted for a stable order, then split into <n> consecutive, balanced groups, and only the files for <i> are run. Every file lands in exactly one shard, group sizes differ by at most one, and the selection happens before any --shuffle, so a given shard runs the same files on every machine regardless of the shuffle seed. The PR is explicit about the trade-off: the shard is selected at run time, after the module graph for the full suite has been built and type-checked, so every machine still pays the graph-build and type-check cost for the whole suite. Moving the shard pre-filter ahead of type-checking, so each machine only loads and checks its own subset, is a natural follow-up.

The fetch request_builder_hook reads the X_DENO_FETCH_TOKEN and CDN_LOOP environment variables once and, when present, injects them as x-deno-fetch-token and cdn-loop headers on every outbound fetch() (both the main worker and web workers). The cdn-loop header lets upstream infrastructure detect request loops. To keep the fetch token trustworthy, any user-supplied x-deno-fetch-token header is scrubbed before the runtime value is applied, so user code cannot spoof it. When neither env var is set, no extra headers are added. This is the runtime side of the same cloud-deployment plumbing that Deno's deploy product uses to identify which deployment made an outbound call.

What is next

The four-PR cluster is a strong signal of where Deno is going: a main line that treats desktop as a first-class target, adds CI-sharding parity with Vitest / Jest / Playwright, finishes the npm-link-style local-package workflow, and exposes the runtime-level fetch headers that cloud deployments need. The next Deno release (likely 2.8.4, given the Deno 2.8 release notes shipped on June 1) is the first chance these features get into a tagged build. The deno desktop PR was the last of the four to land, and it is the one with the most surface area to validate against, so the first tagged build that ships all four is also the one to file feedback against.

Frequently Asked Questions

Related articles

More coverage with overlapping topics and tags.

Node.js 26.4.0 'Current' Ships node:vfs Subsystem (Matteo Collina), ESM Loader Package Maps (Maël Nison), TLS Certificate Compression, TCP_KEEPINTVL/TCP_KEEPCNT, and argon2 Stable
runtimes

Node.js 26.4.0 'Current' Ships node:vfs Subsystem (Matteo Collina), ESM Loader Package Maps (Maël Nison), TLS Certificate Compression, TCP_KEEPINTVL/TCP_KEEPCNT, and argon2 Stable

Node.js 26.4.0 (Current), published 2026-06-24 by @aduh95, lands eight SEMVER-MINOR changes: a minimal node:vfs subsystem that mounts user-supplied virtual filesystems (PR #63115, Matteo Collina) plus a follow-up that dispatches node:fs/promises to mounted VFS instances (PR #63537), package maps for ESM loaders that route bare specifiers through the loader hooks (PR #62239, Maël Nison), TLS certificateCompression that wires RFC 8879 zlib and zstd compression through the OpenSSL build config (PR #62217, Tim Perry), TCP_KEEPINTVL and TCP_KEEPCNT support in net.Socket.setKeepAlive (PR #63825, Guy Bedford), caller-supplied buffers in fs.readFile / fs.readFileSync (PR #63634, Matteo Collina), closeIdleConnections that now also drops pre-request sockets (PR #63470, semimikoh), net.BlockList advanced to Release Candidate stability (PR #63050), and crypto argon2 + KEM encap/decap marked stable (PR #63924, Filip Skokan). The release also adds WebCrypto cSHAKE (PR #63988), QUIC listEndpoints (PR #63536) and X509Certificate handles (PR #63191), dgram connectSync / bindSync (PRs #63838 + #63932, Guy Bedford), early-TCP net.BoundSocket (PR #63951), an experimental fast FFI call path for AArch64 and x86_64 (PRs #63068 + #63941, Paolo Insogna), npm 11.17.0 (PR #63857), sqlite 3.53.2, and libffi 3.6.0.
Astro 7.0.0 Stable Ships Vite 8, Makes the Rust Compiler Default, Adds a Background Dev Server for AI Coding Agents, and Promotes Route Caching, Advanced Routing, and Sätteri to First-Class
frameworks

Astro 7.0.0 Stable Ships Vite 8, Makes the Rust Compiler Default, Adds a Background Dev Server for AI Coding Agents, and Promotes Route Caching, Advanced Routing, and Sätteri to First-Class

Astro 7.0.0, published on 2026-06-22, lands the 7.0 stable branch after ten weeks of beta. The headline changes: a Vite 8 upgrade, the Rust-based Astro compiler as the default (the Go compiler is removed), the Sätteri Markdown pipeline as the default (remark/rehype moves out of the default install), advanced routing promoted from experimental (with src/fetch.ts as the new default entrypoint), route caching promoted from experimental (top-level cache and routeRules, with cacheNetlify() and cacheVercel() providers landing in the same release), a background dev server mode designed for AI coding agents (astro dev --background, .astro/dev.json lockfile, astro dev stop|status|logs), compressHTML: 'jsx' as the new default, the @astrojs/db package removed, and every official integration bumped a major version (vue 7, react 6, svelte 9, preact 6, solid-js 7, vercel 11, netlify 8, node 11). create-astro@5.1.0, shipped in the same release wave, now writes an AGENTS.md file into every new project with a CLAUDE.md symlink pointing at it.
Bun Integrates the React Compiler Directly Into Its Bundler, Roughly 20x Faster Than the Babel Plugin
runtimes

Bun Integrates the React Compiler Directly Into Its Bundler, Roughly 20x Faster Than the Babel Plugin

PR #32504, merged into oven-sh/bun on June 20, 2026, turns the upstream React Compiler Rust port into a built-in `bun build` transform behind `--react-compiler` and `Bun.build({ reactCompiler: true })`. Bun ports the upstream `facebook/react` `compiler/crates/` workspace directly into a single `src/react_compiler/` crate (~62k LOC) instead of going through Babel, SWC, or Oxc, and on a large React codebase (around 860 components, 1400 memo slots) the compiler pass runs in 465 ms versus 9.15 s for the Babel plugin. The feature is experimental, off by default, and ships with `reactCompilerOutputMode` (client or ssr) and a `scripts/sync-react-compiler.sh` re-sync helper.

Comments

Log in Log in to join the conversation.

No comments yet. Be the first to share your thoughts.