feat(plugin-vite): enable main process hot restart via exported API - #4346
Draft
erickzhao wants to merge 7 commits into
Draft
feat(plugin-vite): enable main process hot restart via exported API#4346erickzhao wants to merge 7 commits into
erickzhao wants to merge 7 commits into
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-vitegains an opt-inhotRestartoption. With it enabled, rebuilding the main process bundle duringelectron-forge startrestarts the running app automatically, rather than requiringrsin the terminal.The mechanism is a small internal API in
core-utils:start()installs a restart handler, and the Vite plugin'scloseBundlehook 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:
restartedflag stays true forever, so every exit after the first restart was swallowed along with its exit code.lastSpawnedis briefly null between kill and respawn, so a rebuild landing in that window was reported as "nothing to restart" and dropped.closediscard a replacement child that has already been installed.closeBundlebefore rethrowing it, so the app was restarted onto a stale bundle and silently ran the previous build's code.ElectronProcess.restarted, which is declared non-optional but was left undefined until the first restart.Design:
EventEmitterwith a single handler slot plus a disposer. Two handlers would race to kill and respawn the same child process.@electron-forge/core-utils/restartsubpath soplugin-viteand 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.:hot-restart/:hot-reload) rather than sharing one.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
closeBundlebehavior, and the restart lifecycle instart(). Fullfastproject passes (429 tests);yarn buildandyarn lintare clean.Notes for review
hotRestartdefaults tofalseand is not enabled in thevite/vite-typescripttemplates. Worth deciding whether it should be.start()calls contend for it and only the most recent app stays restartable. Threading the capability through thepostStarthook instead would be cleaner, but that's a larger redesign than this branch should carry.nextto resolve conflicts.nexthad since replacedchalkwithnode:util'sstyleText, so this branch's new output was migrated to match, and thershandler's inline restart logic onnextwas dropped in favour of the extractedrestartRunningApp().🤖 Generated with Claude Code