`vite build` never exits in SPA mode with React Router 8
Reproduction
https://github.com/nvignola/rr8-spa-build-hang
git clone https://github.com/nvignola/rr8-spa-build-hang
cd rr8-spa-build-hang
npm install
npx vite build
The repository is untouched create-react-router output with one line changed: ssr: true → ssr: false in react-router.config.ts.
To build it from scratch instead:
npx create-react-router@latest repro --yes --install --no-git-init
cd repro
# set `ssr: false` in react-router.config.ts
npx vite build
System Info
System:
OS: macOS 26.6.1
CPU: (18) arm64 Apple M5 Max
Memory: 118.88 MB / 36.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.14.1
npm: 11.11.0
Browsers:
Brave Browser: 150.1.92.140
Chrome: 151.0.7922.170
Firefox Developer Edition: 153.0
Safari: 26.6
npmPackages:
@react-router/dev: ^8 => 8.3.0
@react-router/node: ^8 => 8.3.0
@react-router/serve: ^8 => 8.3.0
react-router: ^8 => 8.3.0
vite: ^8.0.3 => 8.2.2
Used Package Manager
npm
Expected Behavior
vite build writes the output and exits, as it does with ssr: true and as it did in React Router 7.18.2.
Actual Behavior
The build finishes in well under a second, prints its last line, and the process never exits:
✓ built in 28ms
SPA Mode: Generated build/client/index.html
Everything in build/ is complete and correct: client/index.html, client/assets/, and server/. Only the exit is missing.
Scope
| Setup | vite build |
|---|---|
8.3.0, ssr: false |
never exits |
8.0.0, ssr: false |
never exits |
8.3.0, ssr: true |
exits 0 |
7.18.2, ssr: false |
exits 0 |
Unaffected by the TypeScript version and by other Vite plugins.
Why this may not have been reported before
react-router build hides it. The CLI exits explicitly once the command resolves, so open handles never matter:
https://github.com/remix-run/react-router/blob/react-router%408.3.0/packages/react-router-dev/cli/index.ts#L6
The project template ships "build": "react-router build", so only the hang is met only by calling vite build directly.
That is also why I could not use the integration/bug-report-test.ts template. The integration harness builds fixture apps by spawning react-router build: https://github.com/remix-run/react-router/blob/react-router%408.3.0/integration/helpers/vite.ts#L303
Where it looks like it comes from
SPA mode prerenders index.html behind a Vite preview server:
which is closed in a finally:
} finally {
await previewServer.close();
}
Node's server.close() stops new connections but does not destroy idle keep-alive sockets, so the handle can outlive the call. The hang begins on the line right after the prerender message prints, which fits.
Source: remix-run/react-router