Skip to content

feat(plugin-vite): enable main process hot restart via exported API - #4346

Draft
erickzhao wants to merge 7 commits into
nextfrom
feat/vite-hot-restart
Draft

feat(plugin-vite): enable main process hot restart via exported API#4346
erickzhao wants to merge 7 commits into
nextfrom
feat/vite-hot-restart

Conversation

@erickzhao

@erickzhao erickzhao commented Aug 18, 2026

Copy link
Copy Markdown
Member

Continues @bglgwyng's work in #4210, which could not be pushed to directly since it lives on a fork. Their commits are preserved here; the fixes from a review pass are added on top.

Closes #4210 (superseded).

What this adds

plugin-vite gains an opt-in hotRestart option. With it enabled, rebuilding the main process bundle during electron-forge start restarts the running app automatically, rather than requiring rs in the terminal.

new VitePlugin({
  build: [/* ... */],
  renderer: [/* ... */],
  hotRestart: true,
})

The mechanism is a small internal API in core-utils: start() installs a restart handler, and the Vite plugin's closeBundle hook requests a restart through it. In dev, vite.build() runs in-process, so the two share a module singleton. It has no effect when packaging, where builds run in a subprocess that cannot reach the app.

Fixes on top of #4210

Correctness:

  • Only ignore an app exit when that child was restarted. The CLI checked the first child it ever spawned, whose restarted flag stays true forever, so every exit after the first restart was swallowed along with its exit code.
  • Queue a restart requested while one is already in flight. lastSpawned is briefly null between kill and respawn, so a rebuild landing in that window was reported as "nothing to restart" and dropped.
  • Don't let a late close discard a replacement child that has already been installed.
  • Don't restart on a failed build. Rollup passes the build error to closeBundle before rethrowing it, so the app was restarted onto a stale bundle and silently ran the previous build's code.
  • Report a failed relaunch instead of rejecting unobserved, which would take down the Forge process.
  • Initialize ElectronProcess.restarted, which is declared non-optional but was left undefined until the first restart.
  • Catch a throwing restart handler. Callers are bundler hooks, so a throw surfaced a restart failure to the user as a build failure.

Design:

  • Replaced the module-level EventEmitter with a single handler slot plus a disposer. Two handlers would race to kill and respawn the same child process.
  • Exposed the restart API under a @electron-forge/core-utils/restart subpath so plugin-vite and the packaging subprocess don't pull in the whole barrel (@electron/rebuild, find-up, semver, …), and kept it out of the public entrypoint since it's internal.
  • Gave the two plugin instances distinct names (:hot-restart / :hot-reload) rather than sharing one.
  • The warning for a rebuild that fails to reach the app lives in the Vite plugin, not in requestAppRestart. Only the plugin can distinguish a first build — where the app isn't spawned yet and a no-op is correct — from a rebuild.

Testing

19 new tests covering the restart handler slot, the plugin's closeBundle behavior, and the restart lifecycle in start(). Full fast project passes (429 tests); yarn build and yarn lint are clean.

Notes for review

  • hotRestart defaults to false and is not enabled in the vite / vite-typescript templates. Worth deciding whether it should be.
  • The restart handler is a process-wide singleton, so two overlapping start() calls contend for it and only the most recent app stays restartable. Threading the capability through the postStart hook instead would be cleaner, but that's a larger redesign than this branch should carry.
  • Merged next to resolve conflicts. next had since replaced chalk with node:util's styleText, so this branch's new output was migrated to match, and the rs handler's inline restart logic on next was dropped in favour of the extracted restartRunningApp().

🤖 Generated with Claude Code

bglgwyng and others added 5 commits April 9, 2026 14:39
Add restartApp()/onAppRestart() to @electron-forge/core-utils as an
explicit API for triggering Electron app restarts. The Vite plugin
calls restartApp() in its closeBundle hook when the main process
bundle is rebuilt. The start API registers the actual restart logic
via onAppRestart(). Also backport the duplicate restart guard
(!lastSpawned.restarted) to the stdin handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stdin 'rs' handler now calls restartApp() instead of duplicating
the kill→respawn logic, so all restart requests flow through the
single onAppRestart callback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent unnecessary terminal cursor manipulation when the Electron app
has not been spawned yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `hotRestart` option to VitePluginConfig (default: false).
The main process restart is now only enabled when explicitly configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follow-up fixes on top of #4210, from a review pass over the exported
restart API.

Correctness:

- Only ignore an app exit when *that* child was restarted. The CLI checked
  the first child it ever spawned, whose `restarted` flag stays true
  forever, so every exit after the first restart was swallowed along with
  its exit code.
- Queue a restart requested while one is already in flight. `lastSpawned`
  is briefly null between kill and respawn, so a rebuild landing in that
  window was reported as "nothing to restart" and dropped.
- Don't let a late `close` discard a replacement child that has already
  been installed.
- Don't restart on a failed build. Rollup passes the build error to
  `closeBundle` before rethrowing it, so the app was restarted onto a
  stale bundle and silently ran the previous build's code.
- Report a failed relaunch instead of rejecting unobserved, which would
  take down the Forge process.
- Initialize `ElectronProcess.restarted`, which is declared non-optional
  but was left undefined until the first restart.
- Catch a throwing restart handler. Callers are bundler hooks, so a throw
  surfaced a restart failure to the user as a build failure.

Design:

- Replace the module-level EventEmitter with a single handler slot plus a
  disposer. Two handlers would race to kill and respawn the same child.
- Expose the restart API under a `@electron-forge/core-utils/restart`
  subpath so plugin-vite and the packaging subprocess don't pull in the
  whole barrel, and keep it out of the public entrypoint.
- Give the two plugin instances distinct names rather than sharing one.
- Warn from the Vite plugin, not from `requestAppRestart`, since only the
  plugin can distinguish a first build (app not yet spawned, legitimately
  a no-op) from a rebuild that failed to reach the app.

Also documents `hotRestart` in the plugin README and adds coverage for
the restart slot, the plugin's `closeBundle` behavior, and the restart
lifecycle in `start()`.

Co-Authored-By: bgl gwyng <bgl@gwyng.com>
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
@github-actions github-actions Bot added the next label Aug 18, 2026
erickzhao and others added 2 commits August 18, 2026 11:13
Resolves two conflicts:

- `packages/api/core/src/api/start.ts`: `next` still restarted the app
  inline in the `rs` stdin handler, which this branch extracted into
  `restartRunningApp()` behind the exported restart API. Kept the
  extracted version and dropped the inline block.
- `vitest.config.mts`: `next` added the `**/.claude/**` test exclude
  independently, so this branch's version of that change is redundant.
  Took `next`'s.

Also migrates this branch's new output off `chalk`, which `next` replaced
with `node:util`'s `styleText` repo-wide.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
The `lint:markdown-js` check parses fenced JS blocks, and the bare
`config: { ... }` fragment isn't valid JavaScript. Show the full
`forge.config.js` shape instead, matching the example above it.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants