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.

Astro 7.0.0-beta.4 Makes Sätteri the Default Markdown Processor, Promotes Advanced Routing, Custom Logger, and Queued Rendering to Stable
frameworks

Astro 7.0.0-beta.4 Makes Sätteri the Default Markdown Processor, Promotes Advanced Routing, Custom Logger, and Queued Rendering to Stable

Astro 7.0.0-beta.4 (June 15, 2026) flips the Sätteri Rust-based Markdown pipeline on by default, removes the experimental flag from advanced routing, the custom logger, and the new rendering engine, deletes the deprecated astro db/login/logout/link/init CLI commands, and folds the dev-server background mode added in alpha.2 into the default CLI.
GLM-5.2 From Z.ai Is a 1M-Context Open-Weights Model Built for Long-Horizon Coding Agents: A Deep Dive
tooling

GLM-5.2 From Z.ai Is a 1M-Context Open-Weights Model Built for Long-Horizon Coding Agents: A Deep Dive

On June 16, 2026, Z.ai released GLM-5.2 under MIT, with a solid 1M-token context, an IndexShare sparse-attention trick that cuts per-token FLOPs by 2.9×, an anti-hack module for coding RL, and benchmark numbers that put it within a few points of Claude Opus 4.8 on Terminal-Bench 2.1, ahead of GPT-5.5 on three long-horizon suites, and at the top of the open-weights leaderboard on every coding benchmark the company chose to publish. This long read walks through the architecture, the IndexShare + MTP story, the slime agentic RL infrastructure, the anti-hack module, the full benchmark table with all the footnotes, and what this means for the rest of the open-weights long-context race.
Fresh 2.3: Zero JS by Default, View Transitions, and WebSocket Support
runtimes

Fresh 2.3: Zero JS by Default, View Transitions, and WebSocket Support

Fresh 2.3 makes the 'zero JavaScript by default' promise a hard reality, adds native View Transitions, introduces built-in WebSocket handlers, CSP nonce injection, and Temporal API support for islands.

Comments

Log in Log in to join the conversation.

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