Skip to content

fix(desktop): run Vite-based HMR dev servers inside the desktop runtime - #36488

Merged
crowlKats merged 1 commit into
denoland:mainfrom
crowlKats:fix/desktop-hmr-in-runtime-dev-server
Aug 8, 2026
Merged

crowlKats merged 1 commit into
denoland:mainfrom
crowlKats:fix/desktop-hmr-in-runtime-dev-server

Conversation

@crowlKats

Copy link
Copy Markdown
Member

Fixes #35899

Problem

With deno desktop --hmr on a framework project, the dev server was spawned as a plain deno task dev subprocess. Server-side code (SvelteKit endpoints, Vite SSR handlers) therefore had no access to the desktop APIs (Deno.BrowserWindow / Deno.Tray / Deno.Dock) — inconsistent with the compiled app, where the server entrypoint runs inside the desktop runtime. Verified with a SvelteKit endpoint probing the globals: {"hasBrowserWindow":false} under --hmr, true in the built app.

Fix

For frameworks whose dev command is plain vite dev (Vite, SvelteKit), --hmr now compiles a generated dev entrypoint that boots the project's own Vite dev server through Vite's JS API (createServer()) inside the desktop runtime, bound to the DENO_SERVE_ADDRESS port the webview already polls and navigates to (DENO_DESKTOP_FRAMEWORK_DEV=1 signals rt_desktop to restore CWD to the source dir and leave hot reloading to the framework).

Side benefit: the external-process path — parsing the dev server's stdout for the Local: URL with a 15s timeout (#35910's failure mode) — is no longer used for these frameworks. The generated shim is compiled with --no-check semantics (nothing user-authored in it) and allow-all permissions, matching the previously-unsandboxed deno task dev subprocess.

Wrapper dev CLIs that aren't plain Vite (nuxi dev, vinxi dev, react-router dev, Fresh) keep the existing external-process behavior for now.

Latent crash this surfaced (fixed too)

Booting a real dev server in-runtime made the runtime parse a much bigger module graph, which exposed that laufey invokes laufey_runtime_start on its RuntimeLoader C++ thread — which gets the platform-default secondary-thread stack, only 512KB on macOS. Synchronous swc parsing of SvelteKit's graph overflows it and kills the process with SIGBUS (KERN_PROTECTION_FAILURE in the stack guard region, ~220 swc parser frames). This can plausibly also kill production desktop apps with big server bundles.

The runtime (and headless forked workers) now run on a dedicated 8MB-stack thread, and the tokio runtime is built via tokio_util::create_basic_runtime() so blocking-pool threads get the established stack sizes as well.

Verification (macOS, debug build + rebuilt denort_desktop dylib)

  • SvelteKit (adapter-node, sv create scaffold) with an /api endpoint probing globals: {"hasDeno":true,"hasBrowserWindow":true,"hasTray":true,"hasDock":true} under --hmr; page HTML serves; no SIGBUS.
  • Plain Vite React SPA with a configureServer middleware probe: same result, and editing src/App.jsx triggers [vite] hmr update /src/App.jsx (file watching from the restored CWD works).
  • cargo test -p deno --lib framework:: — 71 passed.
  • tools/format.js clean; tools/lint.js --rs clean (JS lint fails only on untracked local scratch files unrelated to this PR).

With `deno desktop --hmr` on a framework project, the dev server was
spawned as a plain `deno task dev` subprocess, so server-side code
(SvelteKit endpoints, Vite SSR) had no access to the desktop APIs
(Deno.BrowserWindow / Deno.Tray / Deno.Dock) — inconsistent with the
compiled app, where the server runs inside the desktop runtime.

For frameworks whose dev command is plain `vite dev` (Vite, SvelteKit),
compile a generated dev entrypoint that boots the project's own Vite dev
server via createServer() inside the desktop runtime, bound to the
DENO_SERVE_ADDRESS port the webview already polls and navigates to. The
external-process path (and its dev-server stdout URL scraping with the
15s timeout) is no longer used for these frameworks; wrapper dev CLIs
(nuxi/vinxi/react-router/Fresh) keep the existing behavior.

Also fixes a latent crash this surfaced: laufey invokes the runtime on
its RuntimeLoader thread, which only gets the platform-default secondary
thread stack (512KB on macOS). Synchronous swc parsing of a real-world
module graph overflows that and kills the process with SIGBUS. The
runtime now runs on a dedicated 8MB-stack thread, and the tokio runtime
is built via tokio_util::create_basic_runtime() so blocking-pool threads
get proper stack sizes too.

Fixes denoland#35899
@crowlKats
crowlKats merged commit 98dc759 into denoland:main Aug 8, 2026
136 checks passed
bartlomieju pushed a commit that referenced this pull request Aug 27, 2026
…me (#36488)

Fixes #35899

## Problem

With `deno desktop --hmr` on a framework project, the dev server was
spawned as a plain `deno task dev` subprocess. Server-side code
(SvelteKit endpoints, Vite SSR handlers) therefore had no access to the
desktop APIs (`Deno.BrowserWindow` / `Deno.Tray` / `Deno.Dock`) —
inconsistent with the compiled app, where the server entrypoint runs
inside the desktop runtime. Verified with a SvelteKit endpoint probing
the globals: `{"hasBrowserWindow":false}` under `--hmr`, `true` in the
built app.

## Fix

For frameworks whose dev command is plain `vite dev` (Vite, SvelteKit),
`--hmr` now compiles a generated dev entrypoint that boots the project's
own Vite dev server through Vite's JS API (`createServer()`) **inside
the desktop runtime**, bound to the `DENO_SERVE_ADDRESS` port the
webview already polls and navigates to (`DENO_DESKTOP_FRAMEWORK_DEV=1`
signals rt_desktop to restore CWD to the source dir and leave hot
reloading to the framework).

Side benefit: the external-process path — parsing the dev server's
stdout for the `Local:` URL with a 15s timeout (#35910's failure mode) —
is no longer used for these frameworks. The generated shim is compiled
with `--no-check` semantics (nothing user-authored in it) and allow-all
permissions, matching the previously-unsandboxed `deno task dev`
subprocess.

Wrapper dev CLIs that aren't plain Vite (`nuxi dev`, `vinxi dev`,
`react-router dev`, Fresh) keep the existing external-process behavior
for now.

## Latent crash this surfaced (fixed too)

Booting a real dev server in-runtime made the runtime parse a much
bigger module graph, which exposed that laufey invokes
`laufey_runtime_start` on its `RuntimeLoader` C++ thread — which gets
the platform-default *secondary*-thread stack, only **512KB on macOS**.
Synchronous swc parsing of SvelteKit's graph overflows it and kills the
process with SIGBUS (`KERN_PROTECTION_FAILURE` in the stack guard
region, ~220 swc parser frames). This can plausibly also kill
*production* desktop apps with big server bundles.

The runtime (and headless forked workers) now run on a dedicated
8MB-stack thread, and the tokio runtime is built via
`tokio_util::create_basic_runtime()` so blocking-pool threads get the
established stack sizes as well.

## Verification (macOS, debug build + rebuilt `denort_desktop` dylib)

- SvelteKit (adapter-node, `sv create` scaffold) with an `/api` endpoint
probing globals:
`{"hasDeno":true,"hasBrowserWindow":true,"hasTray":true,"hasDock":true}`
under `--hmr`; page HTML serves; no SIGBUS.
- Plain Vite React SPA with a `configureServer` middleware probe: same
result, and editing `src/App.jsx` triggers `[vite] hmr update
/src/App.jsx` (file watching from the restored CWD works).
- `cargo test -p deno --lib framework::` — 71 passed.
- `tools/format.js` clean; `tools/lint.js --rs` clean (JS lint fails
only on untracked local scratch files unrelated to this PR).
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.

Bug: deno desktop --hmr . runs the sveltekit server in a non deno desktop runtime, making deno desktop apis unavailable

2 participants