Skip to content

feat: deno desktop subcommand#33441

Merged
crowlKats merged 116 commits into
denoland:mainfrom
crowlKats:desktop-framework-hmr
Jun 16, 2026
Merged

feat: deno desktop subcommand#33441
crowlKats merged 116 commits into
denoland:mainfrom
crowlKats:desktop-framework-hmr

Conversation

@crowlKats

@crowlKats crowlKats commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary

Adds the deno desktop subcommand for building desktop apps from Deno projects.

Core:

  • deno desktop <entry> compiles a project into a self-contained desktop app, built on WEF (prebuilt backends downloaded from github.com/denoland/wef/releases, pinned via Cargo.lock, SHA256-verified, cached under <deno_dir>/wef/<version>/). WEF_DEV_DIR points at a local wef checkout for development.
  • Backends: cef (default, bundled Chromium), webview (OS webview), raw (winit, no engine).
  • Deno.serve() in the entry auto-binds to the port the webview navigates to (via DENO_SERVE_ADDRESS).

Framework auto-detection (cli/tools/framework.rs): Next.js, Astro, Fresh, Remix, Nuxt, SvelteKit, SolidStart, TanStack Start, Vite SSR. Prod server runs by default; dev server runs under --hmr.

HMR (--hmr): framework projects use the framework's dev server; non-framework apps use file-watch + Debugger.setScriptSource hot-swap with the runtime and CEF staying alive.

Deno.BrowserWindow API (cli/tsc/dts/lib.deno.desktop.d.ts): window lifecycle (show/hide/focus/close/reload), size/position, always-on-top, navigation, bind/unbind RPC to webview JS via bindings.<name>(), executeJs, app/context menus, native window handle, keyboard/mouse/wheel/resize/focus events.

Runtime integration: prompt()/alert()/confirm() become native popups; uncaught errors show a native alert and optionally POST to desktop.errorReporting.url.

Auto-updater: Deno.desktopVersion + Deno.autoUpdate({ url, interval, onUpdateReady, onRollback }). Polls <url>/latest.json, applies bsdiff patches (qbsdiff) to the dylib, stages for next launch, rolls back on failed launch.

Unified DevTools (--inspect / --inspect-brk / --inspect-wait): single DevTools session showing both the Deno runtime V8 and the CEF renderer V8 as attached targets — one Console dropdown (Renderer / Deno), one Sources panel with both threads. Implemented as a CDP multiplexer in cli/tools/desktop_devtools.rs. --inspect-brk pauses both isolates (Deno via its own mechanism, CEF via injected Debugger.enable + Debugger.pause before navigation).

Dock / Tray: Deno.dock (macOS) and Deno.Tray (cross-platform) for status-area icons with tooltips, dark-mode icons, and context menus.

Distribution / cross-compile: --target and --all-targets download prebuilt denort + WEF backends for the target triple. Outputs:

  • macOS: .app bundle (framework under Contents/Frameworks/), .dmg via hdiutil
  • Windows: .exe + DLLs directory
  • Linux: app directory, .AppImage via appimagetool

OS-specific limitations

Several features are currently wired up for a subset of platforms. They degrade gracefully (the rest of the app still works) but are worth calling out:

  • Auto-updater is unix-only. apply_pending_update, get_dylib_path, and AutoUpdateState are all #[cfg(unix)]. On non-unix (Windows) update_rolled_back is hardcoded to false and auto_update_state is None, so the staged-update / patch-apply / rollback path is a no-op there. Bringing it to Windows means implementing the dylib-swap + pending-update application for that platform (likely behind a small platform module so the #[cfg] branches in run_desktop/laufey::main! collapse).
  • NAPI native-addon support is unix-only. promote_dylib_symbols_to_global (re-dlopens the runtime dylib with RTLD_GLOBAL so NAPI symbols are visible to addons like next-swc) is #[cfg(unix)]. On Windows, frameworks that pull native addons may fail to load those addons.
  • 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. The non-macOS path is a plain tokio spawn with kill_on_drop.
  • Linux is X11-only (no native Wayland). The generated launcher forces --ozone-platform=x11 + GDK_BACKEND=x11, because the LAUFEY mouse/focus/resize event monitor uses XI2 on X11. On Wayland sessions it runs through XWayland; native Wayland is unsupported.
  • Deno.dock is macOS-only (no-op / unavailable elsewhere).
  • Icon sets are not supported in --hmr mode (any platform). IconConfig::Set errors with "icon sets are not supported in --hmr mode yet"; only a single icon path works under --hmr.

Not yet implemented

  • Notarization / stapling. Codesigning is implemented (macOS, via macos.codesignIdentity in deno.json, with an ad-hoc signature fallback for unsigned builds), but there's no notarytool submission / stapler step yet.
  • Windows MSI, Linux .deb / .rpm installers
  • Clipboard, secureStorage
  • Test coverage (unit + integration)

Closes #3234

crowlKats and others added 3 commits June 11, 2026 05:15
Picks up the webview bindings fix (littledivy/laufey#11): macOS/Windows
binding calls now resolve with the real return value instead of null.

Bumps the crate dep + Cargo.lock, and refreshes cli/laufey_sums.lock with
the v0.3.2 release digests (verified against GitHub's per-asset SHA-256).
@kucukkanat

Copy link
Copy Markdown

@crowlKats after your talk today can't wait for this to be merged! super excited!
(Took me a while to dig and find the branch, I feel like a github stalker rn 😔)

@AlbertShown

Copy link
Copy Markdown

after your talk today

@kucukkanat Can you give me a link to the talk show?

@littledivy
littledivy marked this pull request as draft June 13, 2026 10:42
@littledivy
littledivy marked this pull request as ready for review June 13, 2026 10:42
@littledivy littledivy closed this Jun 13, 2026
@littledivy littledivy reopened this Jun 13, 2026
@littledivy littledivy closed this Jun 13, 2026
@littledivy littledivy reopened this Jun 13, 2026
@crowlbot
crowlbot force-pushed the desktop-framework-hmr branch from 935b5c2 to 15b9efb Compare June 16, 2026 08:53
denort is the `deno compile` base binary. libsui rewrites its ELF when
embedding user code, and that rewrite drops `.relr.dyn` relative relocations
when the symbol table has been stripped. With the v8 149.4.0 bump (more
relative relocations) this became fatal: the compiled binary's C++ static-init
guards get an un-relocated mutex pointer and deadlock at startup
(`libc++abi: __cxa_guard_acquire failed to acquire mutex`), which made
compile_watch_test fail only on release linux-x86_64.

Strip only debug info from denort (keep .symtab) so the relocations survive
the libsui rewrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@crowlbot
crowlbot force-pushed the desktop-framework-hmr branch from 15b9efb to 90d62f4 Compare June 16, 2026 09:13
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@crowlKats
crowlKats merged commit 8398162 into denoland:main Jun 16, 2026
267 of 270 checks passed
littledivy added a commit that referenced this pull request Jun 21, 2026
## Summary

Adds `.msi` as a `deno desktop` output format, alongside the existing
`.dmg`/`.AppImage` and the `.deb`/`.rpm` formats added in #35296. As
with those, the format is chosen by the `--output` extension (or the
per-platform `output` key in the `desktop` block of `deno.json` — whose
Windows description already documented `MyApp.msi`); the driver strips
the extension, builds the intermediate Windows app dir, then wraps it in
an MSI.

The MSI database is authored entirely in pure Rust via the
[`msi`](https://crates.io/crates/msi) crate, with the file payload
stored in an embedded MSZIP cabinet
([`cab`](https://crates.io/crates/cab) crate), so it **cross-compiles
from any host** — only the *target* must be Windows (gated like
`.deb`/`.rpm` are gated on a Linux target). The app installs per-machine
under `ProgramFiles64Folder\<AppName>\`, mirroring the staged app-dir
tree exactly (nested directories like CEF's `locales/` are preserved);
uninstall removes it.

```
%ProgramFiles%\<AppName>\
  <AppName>.bat          (launcher)
  laufey.exe             (CEF backend)
  libcef.dll, ...        (CEF support files)
  denort.dll             (compiled runtime + user code)
  ...                    (nested dirs preserved)
```

### Notable choices
- **Deterministic GUIDs** — `ProductCode`, `UpgradeCode`, the package
code, and per-component GUIDs are derived via UUIDv5 from the app
identity (`--identifier`), so identical inputs produce an identical
installer (matching the `source_date(0)` / `mtime: 0` reproducibility of
the `.rpm`/`.deb`/AppImage paths) and the `UpgradeCode` is stable across
versions.
- **Directory/Component model** — one `Component` per install directory
(a file's directory is carried by its component), `KeyPath` = the first
file in that directory, components flagged 64-bit
(`msidbComponentAttributes64bit`). A single `Feature` ties them
together.
- **Embedded cabinet** — one MSZIP folder holding every file named by
its MSI `File` key, referenced by `Media.Cabinet = #appcab`. Summary
stream marks the source compressed with long file names (`x64;1033` /
`Arm64;1033` template).
- **No new config fields** — metadata is derived from the staged app-dir
name, `--identifier`, and the default `1.0.0` version that matches the
macOS bundle.

### Metadata
Derived from existing config (app-dir name → `ProductName`,
`--identifier` → GUID seed, default `1.0.0` version). Help text for
`--output` now lists `MyApp.msi`.

## Why
This rounds out the desktop installer story for Windows — `.msi` was
explicitly left out of the original desktop PR (#33441). `.msi` is what
most Windows users/IT deployments install through.

## Tests
Pure-Rust unit tests (cross-platform): triple→arch mapping,
deterministic+valid GUID derivation, 8.3 short-name minting, and a full
`create_windows_msi` round-trip that parses the produced MSI back and
asserts the `Property`/`Directory`/`File`/`Component`/`Media` tables,
the 1..N file sequence, the two-component layout for a nested dir, and
that a file's bytes round-trip out of the embedded cabinet. All pass;
clippy clean.

As with `.deb`/`.rpm`, the matching-OS integration check (install /
launch / upgrade / uninstall on a real Windows box) can't run in the
cross-platform unit suite. Major-upgrade automation (an `Upgrade` table
+ `RemoveExistingProducts`) is intentionally left as a follow-up.

## New dependencies
`msi = "0.10.0"` and `cab = "0.6.0"` (both pure Rust). Also enables the
`v5` feature on the existing `uuid` dep.

Closes #35377
Ref mdsteele/rust-msi#59
Closes denoland/divybot#582

---------

Co-authored-by: divybot <[email protected]>
Co-authored-by: Divy Srivastava <[email protected]>
@Vasdranna

Copy link
Copy Markdown

This is beyond awesome, thank you so much <3

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.

10 participants