Phase 3: bump Electron 27 to 42 and adapt the changed APIs#988
Merged
Conversation
Electron 42 (Node 24, Chromium 148). Empirically the breaking surface is small
for this app (launched the built app on 42 and read the real failures):
- update-electron-app 2 to 3: v3 dropped the callable default export for a named
updateElectronApp; the old require('update-electron-app')() threw "require(...)
is not a function" at startup. Use the named import.
- File.path was removed in Electron 32: the welcome drag-drop handler now resolves
paths through webUtils.getPathForFile, exposed via the welcomeview preload
(webUtils is not available in the page world).
- @electron/notarize 3 (was electron-notarize 1): ESM, imported dynamically from
the CJS afterSign hook; the v2 tool and appBundleId keys are gone.
- electron-builder 24 to 26, electron-log 4 to 5: no source change needed
(electron-log's root export still carries transports.file; BrowserView is still
present at runtime on 42, so the WebContentsView migration is deferred, not
forced by the bump).
Note: AI-assisted (Claude Code). Manually verified: probed BrowserView and
File.path presence inside Electron 42 directly (BrowserView is still a function,
File.path is gone from the typings); tsc 0 errors, build clean, the app launches
on 42 with no runtime errors, 430 unit tests pass.
Electron raised its macOS floor to 12 at v38, so declare it in the builder mac config. The electron-builder electronFuses hardening is intentionally not here; it is tracked separately. Note: AI-assisted (Claude Code). Manually verified: package.json parses.
BrowserView is deprecated and slated for removal. Migrate the four view classes (themed, lab, titlebar, welcome) to WebContentsView and the session window's composition calls: addBrowserView to contentView.addChildView, removeBrowserView to contentView.removeChildView, getBrowserViews() to contentView.children. setBounds/setBackgroundColor/webContents are unchanged. WebContentsView, unlike BrowserView, does not destroy its webContents when removed from the window, so close it explicitly where a view is disposed rather than reused: LabView.dispose() now closes its webContents (covering both the session-stop and the switch-to-welcome paths), and the welcome view is closed when it is swapped out for the lab view. The reused progress and env-select popup views are removed without closing. _openDevTools now guards on instanceof WebContentsView since contentView.children is typed View[]. Note: AI-assisted (Claude Code). Manually verified on the Electron 42 build: tsc 0 errors, eslint clean, the app launches, the multi-view composition and clean-shutdown e2e pass, the welcome-to-lab switch (python-env e2e) works, and 432 unit tests pass.
…t last CJS Patch/minor bumps (no API change): @playwright/test, @types/*, vitest + coverage, typescript-eslint, webpack, js-yaml, semver, winreg, shx, fast-xml-parser (5.9, XMLParser news parse re-verified), @leeoniya/ufuzzy. Majors that are CJS-safe with stable API for this usage: @lumino/signaling 2 (Signal/ISignal unchanged), ejs 6 (render unchanged), fs-extra 11 + tar 7 + istextorbinary 9 (build/extract APIs unchanged, still expose a require export). which 2 to 7: v7 removed the callback API, so _getExecutableInstances is rewritten to the promise form with nothrow. ESM-only packages: pinned to their last CJS-capable major instead of the ESM latest, since the main process is still CJS (require). yargs to 17.7.2 (18 is ESM-only), meow to 9 (10+ ESM; flags schema unchanged so buildutil.js needs no change), rimraf to 4.4.1 (5+ ESM; added --glob to the two conda-lock scripts since rimraf 4 no longer globs by default). fix-path stays at 3 (4+ ESM). Still deferred: @types/node (pinned to 24 to match Electron 42's Node), prettier 3 (reflows the whole repo, its own PR), webpack-cli 7 (moves with webpack core). Note: AI-assisted (Claude Code). Manually verified: tsc 0, eslint clean, build, 432 unit tests, e2e against the Electron 42 dev build; probed each major's runtime API (tar.x, ejs.render, Signal, which, isBinary, XMLParser.parse), ran buildutil.js under meow 9 and a rimraf 4 removal.
Extract the RSS-to-news-items transform from WelcomeView into a pure parseNewsFeed(xml) so the fast-xml-parser usage (which the project re-verifies on every parser bump) is unit-tested instead of only eyeballed against the live blog feed. Tests cover the title/link mapping, percent-encoded links, the maxNewsToShow cap, the single-item isArray case, and empty/malformed feeds. Note: AI-assisted (Claude Code). Manually verified: 6 parse tests pass, the welcome view still renders in the e2e smoke, tsc 0, eslint clean.
Pull the tar.x call out of installCondaPackEnvironment into extractTarball(tar, dest) so the tar usage (re-verified on every tar bump, now at v7) is unit-tested against a real gzipped tarball roundtrip rather than only run during a full conda env install. Tests assert nested files and contents extract, and that a missing tarball rejects. Note: AI-assisted (Claude Code). Manually verified: 2 extract tests pass against a real tar.c/tar.x roundtrip, tsc 0, eslint clean.
Lockfile-only bump of electron from 42.4.0 to 42.4.1, inside the ^42.4.0 already declared in package.json. Part of landing Electron 42, not a separate dependency-maintenance change: the patch clears the remaining low-severity electron alerts on top of the 27 to 42 jump. The only other movement is electron's own transitive subtree re-resolving (a transitive semver lifts to 7.8.5). Note: AI-assisted (Claude Code). Manually verified: lockfile diff is electron + its transitive closure only (no devDep or unrelated direct-dep movement); frozen install self-consistent; type-check 0, unit 440, build ok; e2e 4 pass / 1 skip was confirmed on electron 42.4.1.
which v7 (pulled in by the Electron 42 dep bump) declares an engines range of ^22.22.2 || ^24.15.0 || >=26.0.0, so the Node 20 install steps fail with an incompatible-engine error before any test runs. Move the three jobs that run yarn install (unit tests and the installer matrix in publish.yml, the version-sync bot in sync_lab_release.yml) to Node 24, which matches Electron 42's bundled Node. typecheck.yml and e2e.yml were already on 24. Note: AI-assisted (Claude Code). Manually verified: which engines range confirmed via its package.json; grepped all workflows so no yarn-install job is left on Node 20; local unit suite (440) runs on Node 24.
The lifecycle comments around the explicit webContents.close() calls were imprecise. One claimed Electron destroys the child webContents for us when the window is gone, which is the opposite of the actual behavior: a WebContentsView's webContents is not destroyed on detach or window close, which is why the close() is there in the first place. Reword both sites to state that and point at electron/electron#42884. No code change. Note: AI-assisted (Claude Code). Manually verified against electron/electron#42884 and the electron 42.4.1 typings (WebContentsView exposes only a readonly webContents, no destroy/close of its own; webContents has close/destroy/isDestroyed); the callers drop the view reference after closing so the renderer is reclaimed. tsc 0 errors, lint clean.
62 tasks
krassowski
reviewed
Jun 25, 2026
krassowski
left a comment
Member
There was a problem hiding this comment.
LGTM, will throw copilot at this too
Contributor
There was a problem hiding this comment.
Pull request overview
Phase 3 of the revival roadmap upgrades the app runtime from Electron 27 → 42 and adapts the main-process / preload code to Electron API changes across that range (notably BrowserView → WebContentsView and File.path removal). It also bumps a broad set of build/runtime dependencies and adds unit tests for newly-extracted pure helpers to reduce upgrade risk.
Changes:
- Migrate view wrappers and window composition from
BrowserViewtoWebContentsView, including devtools handling and lifecycle updates. - Replace renderer
File.pathusage with a preload-bridgedwebUtils.getPathForFile()for drag-and-drop. - Add testable pure helpers (
parseNewsFeed,extractTarball) with new unit tests; bump Node/CI baseline and various dependencies (builder/notarize/update tooling).
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit/preload/welcomeview.test.ts | Updates preload API surface expectation to include getPathForFile. |
| test/unit/newsfeed.test.ts | Adds unit coverage for RSS parsing behavior and edge cases. |
| test/unit/extract-tarball.test.ts | Adds unit coverage for tarball extraction helper. |
| src/main/welcomeview/welcomeview.ts | Switches Welcome view to WebContentsView; updates DnD + news parsing callsite. |
| src/main/welcomeview/preload.ts | Exposes getPathForFile via preload using webUtils. |
| src/main/welcomeview/newsfeed.ts | New pure parseNewsFeed helper extracted for testability. |
| src/main/utils.ts | Extracts extractTarball helper and uses it in env install flow. |
| src/main/titlebarview/titlebarview.ts | Migrates TitleBar view wrapper to WebContentsView. |
| src/main/sessionwindow/sessionwindow.ts | Migrates window composition to contentView.addChildView/removeChildView; updates devtools iteration and welcome/lab swapping lifecycle. |
| src/main/registry.ts | Updates env discovery to which promise API (v7) with nothrow. |
| src/main/labview/labview.ts | Migrates Lab view wrapper to WebContentsView; makes dispose() async and explicitly closes webContents. |
| src/main/dialog/themedview.ts | Migrates ThemedView wrapper to WebContentsView. |
| src/main/app.ts | Updates update-electron-app usage to v3 named export. |
| scripts/notarize.js | Migrates notarization hook to @electron/notarize (ESM via dynamic import). |
| package.json | Bumps Electron and many dependencies; adjusts rimraf scripts; adds macOS minimum version. |
| .github/workflows/sync_lab_release.yml | Updates workflow Node version baseline to 24.x. |
| .github/workflows/publish.yml | Updates workflow Node version baseline to 24.x. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
_disposeSession's remote branch dereferenced this._labView without a guard and only cleared the session, while the local branch already guarded and disposed the view. After a remote non-persistent session is closed, the window switches back to Welcome and nulls _labView; closing the window then runs _disposeSession again and the remote branch threw a TypeError on this._labView.view, so dispose rejected. Fold the two branches so the labView teardown is shared and guarded. LabView.dispose already clears the session for a non-persistent remote session and closes the webContents, so routing remote sessions through it fixes the crash, also closes the webContents the remote path used to leak after the WebContentsView move (electron/electron#42884), and drops the now-duplicated clearSession. Awaited so a non-persistent remote session is cleared before the window goes away. Note: AI-assisted (Claude Code). Manually verified: new regression test reproduces the null deref on the pre-fix code and passes after; full unit suite 441 green; tsc 0 errors; lint clean; traced the five _disposeSession callers and the close-to-Welcome path that leaves _labView null.
Lockfile-only refresh, all within the carets already in package.json: electron 42.4.1 to 42.5.0, @playwright/test 1.61.1, fast-xml-parser 5.9.3, js-yaml 4.3.0, semver 7.8.5, tar 7.5.19, webpack 5.108.1. Left the eslint toolchain (typescript-eslint, globals, eslint) to dependabot since it belongs to the flat-config work, not this bump. Note: AI-assisted (Claude Code). Manually verified: each resolved version moved as intended and the excluded toolchain deps did not; frozen install self-consistent; tsc 0, unit 441 (incl newsfeed and tar isolation tests), lint, build, e2e 4 pass / 1 skip on electron 42.5.0.
krassowski
approved these changes
Jun 28, 2026
This was referenced Jun 28, 2026
notluquis
added a commit
that referenced
this pull request
Jun 28, 2026
The python env select popup inlines uFuzzy as a <script> in its HTML string, so it needs the IIFE build as a file in the packaged app. That file was committed under src/assets and refreshed by hand through an update-ufuzzy script, which drifts: #988 bumped @leeoniya/ufuzzy to 1.0.19 but the vendored copy stayed on the old build. Copy it from node_modules at build time instead, like toolkit.min.js right above it in copyassets. The pinned dependency is the single source of truth, the vendored blob and the update-ufuzzy script both go away, and the drift cannot recur. shx was only used by that script, so it drops out of devDependencies here too. Note: AI-assisted (Claude Code). Manually verified by building: build/app-assets/uFuzzy.iife.min.js comes out byte-identical to the 1.0.19 dist, shx is gone from the lockfile with no remaining references, prettier clean, and the e2e suite passes so the popup loads it at runtime.
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.
Phase 3 of the revival roadmap (#951): moves Electron from 27 to 42 and adapts the code to the APIs that changed across that range. It sits on top of the TypeScript 6 / ESLint 10 pre-bump that already landed (#978), so this is the runtime jump, not the toolchain.
Electron 42 ships Node 24.15 and Chromium 148. Opening as a draft while the e2e follow-ups settle on master.
API changes the upgrade forced
BrowserView is gone, so the embedded views move to WebContentsView.
Five view wrappers change:
labview,titlebarview,welcomeview,dialog/themedview, and the progress / env-select popups composed insessionwindow. The container calls change fromwindow.addBrowserView/removeBrowserView/getBrowserViews()towindow.contentView.addChildView/removeChildView/contentView.children.The part worth a close look is lifecycle, not the rename. A WebContentsView does not destroy its
webContentswhen you remove it from the window, where the old BrowserView did. To avoid leaking a renderer, the code now closes the webContents explicitly in two places:LabView.dispose()(now async) and the welcome-to-lab swap insessionwindow. When the parent window is already destroyed, Electron has torn the child down already, so those paths guard onisDestroyed()first. If a removal path is missing aclose(), the symptom would be a lingering renderer rather than a crash, so it is worth checking eachremoveChildViewhas a matching close where the view is not reused._openDevToolsalso had to change:contentView.childrenis typedView[], and onlyWebContentsViewexposeswebContents, so it now guards withinstanceof WebContentsViewbefore opening devtools.File.path was removed from the renderer (Electron 32).
The welcome view's drag-and-drop read
file.pathto learn what was dropped. That property is gone, so the path now comes fromwebUtils.getPathForFile(file), exposed through the welcome preload asgetPathForFile.webUtilsis only available in the preload, not the page, which is why it is bridged rather than called inline.Dependency moves
Electron 27 to 42 pulls a set of build and runtime deps with it:
electron-builder24 to 26, andelectron-notarize(deprecated) to@electron/notarize3. v3 is ESM-only, soscripts/notarize.jsimports it with a dynamicimport()from the CJS hook, and drops thetoolandappBundleIdkeys that v3 removed (notarytool is the only path now).update-electron-app2 to 3, switched to the namedupdateElectronAppexport.which2 to 7. The callback API was removed in v3, so env discovery inregistry.tsnow awaits the promise form withnothrow: true, which returns null instead of throwing when nothing is found.electron-log4 to 5,@lumino/signaling1 to 2,ejs3 to 6,fast-xml-parser5,tar6 to 7,fs-extra9 to 11, and other smaller bumps. A couple of packages that have gone ESM-only upstream are held at their last CJS release so the CommonJS main process keeps loading them; the full ESM migration is a later phase.rimraf3 to 4 changed its glob handling, so the twoupdate_*_conda_lockscripts now pass--glob.^42.4.0range. I left the rest of the in-range refresh out of this PR on purpose so unrelated devDeps do not move inside an Electron bump; those can ride dependabot against master.A side effect: this branch already carries
tarat 7.x andjs-yamlat 4.2.0, so the open dependabot alerts for those two are resolved here rather than separately.Small refactors to make the risky bits testable
Two pure functions were pulled out so the dep bumps they depend on are unit-tested rather than only exercised by a full app run:
parseNewsFeedinwelcomeview/newsfeed.tsisolates the fast-xml-parser usage (the news feed parsing the project re-verifies on every parser bump).extractTarballinutils.tsisolates thetar.xcall used by the bundled-env install.Where to review and how to check
sessionwindow.tsandlabview.ts, specifically that everyremoveChildViewof a non-reused view is paired with awebContents.close().File.pathtowebUtilspath).whichv7 change).scripts/notarize.js) only runs in a signed mac CI build with certs, so it is not exercised by the checks below.Verification run locally, rebased onto current master
yarn type-check(tsc --noEmit): 0 errorsyarn test:unit: 440 passingyarn lint:check: cleanyarn build: succeedsyarn test:e2e: 4 passing, 1 skipped (the skipped test seeds a real Python env). The e2e launch is what exercises the WebContentsView migration end to end, since the window and view composition only fails at runtime.I launched the app and confirmed the session window still composes the titlebar, welcome, and content views and shuts down without hanging.
Note: AI-assisted (Claude Code). Manually verified: type-check, unit (440), lint, build, e2e (4 pass / 1 skip) on the rebased branch; launched the app to confirm the WebContentsView composition and clean shutdown