Skip to content

Phase 3: bump Electron 27 to 42 and adapt the changed APIs#988

Merged
krassowski merged 12 commits into
masterfrom
phase3/electron-42
Jun 28, 2026
Merged

Phase 3: bump Electron 27 to 42 and adapt the changed APIs#988
krassowski merged 12 commits into
masterfrom
phase3/electron-42

Conversation

@notluquis

@notluquis notluquis commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

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 in sessionwindow. The container calls change from window.addBrowserView / removeBrowserView / getBrowserViews() to window.contentView.addChildView / removeChildView / contentView.children.

The part worth a close look is lifecycle, not the rename. A WebContentsView does not destroy its webContents when 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 in sessionwindow. When the parent window is already destroyed, Electron has torn the child down already, so those paths guard on isDestroyed() first. If a removal path is missing a close(), the symptom would be a lingering renderer rather than a crash, so it is worth checking each removeChildView has a matching close where the view is not reused.

_openDevTools also had to change: contentView.children is typed View[], and only WebContentsView exposes webContents, so it now guards with instanceof WebContentsView before opening devtools.

File.path was removed from the renderer (Electron 32).
The welcome view's drag-and-drop read file.path to learn what was dropped. That property is gone, so the path now comes from webUtils.getPathForFile(file), exposed through the welcome preload as getPathForFile. webUtils is 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-builder 24 to 26, and electron-notarize (deprecated) to @electron/notarize 3. v3 is ESM-only, so scripts/notarize.js imports it with a dynamic import() from the CJS hook, and drops the tool and appBundleId keys that v3 removed (notarytool is the only path now).
  • update-electron-app 2 to 3, switched to the named updateElectronApp export.
  • which 2 to 7. The callback API was removed in v3, so env discovery in registry.ts now awaits the promise form with nothrow: true, which returns null instead of throwing when nothing is found.
  • electron-log 4 to 5, @lumino/signaling 1 to 2, ejs 3 to 6, fast-xml-parser 5, tar 6 to 7, fs-extra 9 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.
  • rimraf 3 to 4 changed its glob handling, so the two update_*_conda_lock scripts now pass --glob.
  • The lockfile pins electron at 42.4.1, inside the ^42.4.0 range. 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 tar at 7.x and js-yaml at 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:

  • parseNewsFeed in welcomeview/newsfeed.ts isolates the fast-xml-parser usage (the news feed parsing the project re-verifies on every parser bump).
  • extractTarball in utils.ts isolates the tar.x call used by the bundled-env install.

Where to review and how to check

  • WebContentsView lifecycle: sessionwindow.ts and labview.ts, specifically that every removeChildView of a non-reused view is paired with a webContents.close().
  • Drag-and-drop: drop a folder on the welcome window and confirm it still opens the folder (this is the File.path to webUtils path).
  • Python env discovery: confirm environments are still detected (the which v7 change).
  • News panel on the welcome screen renders (the fast-xml-parser path).
  • Notarization (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 errors
  • yarn test:unit: 440 passing
  • yarn lint:check: clean
  • yarn build: succeeds
  • yarn 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

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.
@notluquis notluquis marked this pull request as ready for review June 23, 2026 07:12
@notluquis notluquis requested a review from krassowski June 25, 2026 05:29
@notluquis notluquis self-assigned this Jun 25, 2026
@notluquis notluquis linked an issue Jun 25, 2026 that may be closed by this pull request
62 tasks

@krassowski krassowski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, will throw copilot at this too

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 BrowserView to WebContentsView, including devtools handling and lifecycle updates.
  • Replace renderer File.path usage with a preload-bridged webUtils.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.

Comment thread src/main/sessionwindow/sessionwindow.ts Outdated
Comment thread src/main/welcomeview/newsfeed.ts
krassowski and others added 2 commits June 25, 2026 11:19
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.
@notluquis notluquis requested a review from krassowski June 28, 2026 18:29
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 krassowski merged commit 661c576 into master Jun 28, 2026
12 checks passed
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.
@notluquis notluquis deleted the phase3/electron-42 branch June 29, 2026 05:54
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.

Revival: Electron 41, test infra, deps, CI

3 participants