Skip to content

Commit 661c576

Browse files
notluquiskrassowskiCopilot
authored
Phase 3: bump Electron 27 to 42 and adapt the changed APIs (#988)
* feat: bump Electron 27 to 42 and adapt the changed APIs 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. * build: set mac minimumSystemVersion to 12.0.0 for Electron 42 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. * refactor: migrate BrowserView to WebContentsView 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. * deps: bump outdated dependencies, adapting which v7 and pinning ESM at 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. * test: cover the news-feed parse and isolate the fast-xml-parser usage 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. * test: cover the bundled-env tarball extract and isolate the tar usage 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. * deps: refresh electron to 42.4.1 patch within range 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. * ci: run yarn jobs on Node 24 so which v7 installs 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. * refactor: tighten WebContentsView teardown comments 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. * Respect max number set to 0 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: dispose the labView for remote sessions and guard re-dispose _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. * deps: bump electron to 42.5.0 and refresh in-range deps 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. --------- Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent a8cf84b commit 661c576

19 files changed

Lines changed: 1800 additions & 1153 deletions

File tree

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
2424
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
2525
with:
26-
node-version: '20.x'
26+
node-version: '24.x'
2727
cache: 'yarn'
2828
- run: yarn install --frozen-lockfile
2929
- run: yarn test:coverage
@@ -59,7 +59,7 @@ jobs:
5959
- name: Install node
6060
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
6161
with:
62-
node-version: '20.x'
62+
node-version: '24.x'
6363
cache: 'yarn'
6464

6565
- name: Install dependencies

.github/workflows/sync_lab_release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
- name: Install Node
9494
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
9595
with:
96-
node-version: '20.x'
96+
node-version: '24.x'
9797

9898
- name: Install npm dependencies
9999
run: |

package.json

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"dist:osx-arm64-dev": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --macos --arm64 --publish never",
3131
"dist:win-64": "yarn build && electron-builder --win --publish never",
3232
"dist:win-arm64": "yarn build && yarn electron-builder --arm64 --publish never",
33-
"update_workflow_conda_lock": "cd workflow_env && rimraf *.lock && conda-lock --kind explicit -f publish_env.yaml && cd -",
34-
"update_conda_lock": "cd env_installer && rimraf *.lock && conda-lock --kind explicit -f jlab_server.yaml && cd -",
33+
"update_workflow_conda_lock": "cd workflow_env && rimraf --glob *.lock && conda-lock --kind explicit -f publish_env.yaml && cd -",
34+
"update_conda_lock": "cd env_installer && rimraf --glob *.lock && conda-lock --kind explicit -f jlab_server.yaml && cd -",
3535
"clean_env_installer": "rimraf ./env_installer/jlab_server.tar.gz && rimraf ./env_installer/jlab_server",
3636
"create_env_installer": "yarn clean_env_installer && conda-lock install --no-validate-platform --prefix ./env_installer/jlab_server ./env_installer/conda-${ENV_PLATFORM}.lock && yarn copy_extras_to_bundled_env && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz",
3737
"create_env_installer:linux-64": "ENV_PLATFORM=linux-64 yarn create_env_installer",
@@ -157,6 +157,7 @@
157157
"darkModeSupport": true,
158158
"hardenedRuntime": true,
159159
"gatekeeperAssess": false,
160+
"minimumSystemVersion": "12.0.0",
160161
"artifactName": "${productName}-${arch}.${ext}",
161162
"extraResources": [
162163
{
@@ -182,54 +183,54 @@
182183
"repository": "https://github.com/jupyterlab/jupyterlab-desktop",
183184
"license": "BSD-3-Clause",
184185
"devDependencies": {
186+
"@electron/notarize": "^3.1.1",
185187
"@eslint/js": "^10.0.1",
186188
"@jupyter-notebook/web-components": "0.9.1",
187-
"@leeoniya/ufuzzy": "1.0.14",
188-
"@playwright/test": "^1.59.1",
189-
"@types/ejs": "^3.1.0",
190-
"@types/js-yaml": "^4.0.3",
189+
"@leeoniya/ufuzzy": "1.0.19",
190+
"@playwright/test": "^1.61.0",
191+
"@types/ejs": "^3.1.5",
192+
"@types/js-yaml": "^4.0.9",
191193
"@types/node": "^24",
192-
"@types/semver": "^7.3.4",
193-
"@types/tar": "^6.1.5",
194-
"@types/yargs": "^17.0.18",
195-
"@vitest/coverage-v8": "^4.1.5",
196-
"electron": "^27.0.2",
197-
"electron-builder": "^24.9.1",
198-
"electron-notarize": "^1.2.2",
194+
"@types/semver": "^7.7.1",
195+
"@types/tar": "^6.1.13",
196+
"@types/yargs": "^17.0.35",
197+
"@vitest/coverage-v8": "^4.1.9",
198+
"electron": "^42.4.0",
199+
"electron-builder": "^26.15.3",
199200
"electron-playwright-helpers": "^2.1.0",
200201
"eslint": "^10.5.0",
201202
"eslint-config-prettier": "^10.1.8",
202-
"fs-extra": "~9.1.0",
203+
"fs-extra": "^11.3.5",
203204
"globals": "^17.6.0",
204-
"istextorbinary": "^6.0.0",
205-
"meow": "^6.0.1",
205+
"istextorbinary": "^9.5.0",
206+
"meow": "^9.0.0",
206207
"node-watch": "^0.7.4",
207208
"prettier": "~2.1.1",
208209
"read-package-tree": "^5.1.6",
209-
"rimraf": "~3.0.0",
210-
"shx": "^0.3.4",
210+
"rimraf": "^4.4.1",
211+
"shx": "^0.4.0",
211212
"typescript": "^6.0.0",
212-
"typescript-eslint": "^8.61.0",
213-
"vitest": "^4.1.5",
214-
"webpack": "^5.76.0",
213+
"typescript-eslint": "^8.61.1",
214+
"vitest": "^4.1.9",
215+
"webpack": "^5.107.2",
215216
"webpack-cli": "^4.5.0"
216217
},
217218
"resolutions": {
218219
"serialize-javascript": "^6.0.2",
219220
"micromatch": "^4.0.8"
220221
},
221222
"dependencies": {
222-
"@lumino/signaling": "^1.10.0",
223-
"ejs": "^3.1.10",
224-
"electron-log": "^4.4.8",
225-
"fast-xml-parser": "^5.8.0",
223+
"@lumino/signaling": "^2.1.5",
224+
"ejs": "^6.0.1",
225+
"electron-log": "^5.4.4",
226+
"fast-xml-parser": "^5.9.0",
226227
"fix-path": "^3.0.0",
227-
"js-yaml": "^4.1.0",
228-
"semver": "^7.5.4",
229-
"tar": "^6.2.1",
230-
"update-electron-app": "^2.0.1",
231-
"which": "^2.0.2",
232-
"winreg": "^1.2.4",
233-
"yargs": "^17.6.2"
228+
"js-yaml": "^4.2.0",
229+
"semver": "^7.8.4",
230+
"tar": "^7.5.16",
231+
"update-electron-app": "^3.2.0",
232+
"which": "^7.0.0",
233+
"winreg": "^1.2.5",
234+
"yargs": "^17.7.2"
234235
}
235236
}

scripts/notarize.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
/* Based on https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ */
22

3-
const { notarize } = require('electron-notarize');
4-
53
exports.default = async function notarizing(context) {
64
const { electronPlatformName, appOutDir } = context;
75
if (
@@ -11,14 +9,17 @@ exports.default = async function notarizing(context) {
119
return;
1210
}
1311

12+
// @electron/notarize v3 is ESM; import it dynamically from this CJS hook.
13+
// notarytool is the only tool now, so the v2 `tool` and `appBundleId` keys
14+
// are gone.
15+
const { notarize } = await import('@electron/notarize');
16+
1417
const appName = context.packager.appInfo.productFilename;
1518

1619
return await notarize({
17-
appBundleId: 'org.jupyter.jupyterlab-desktop',
1820
appPath: `${appOutDir}/${appName}.app`,
1921
appleId: process.env.APPLEID,
2022
appleIdPassword: process.env.APPLEIDPASS,
21-
tool: 'notarytool',
2223
teamId: process.env.APPLE_TEAM_ID
2324
});
2425
};

src/main/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4+
import { updateElectronApp } from 'update-electron-app';
45
import {
56
app,
67
autoUpdater,
@@ -567,7 +568,7 @@ export class JupyterApplication implements IApplication, IDisposable {
567568
log.error(message);
568569
});
569570

570-
require('update-electron-app')();
571+
updateElectronApp();
571572
}
572573

573574
private _validateRemoteServerUrl(url: string): Promise<IJupyterServerInfo> {

src/main/dialog/themedview.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4-
import { BrowserView } from 'electron';
4+
import { WebContentsView } from 'electron';
55
import * as fs from 'fs';
66
import * as path from 'path';
77
import { DarkThemeBGColor, LightThemeBGColor } from '../utils';
88

99
export class ThemedView {
1010
constructor(options: ThemedView.IOptions) {
1111
this._isDarkTheme = options.isDarkTheme;
12-
this._view = new BrowserView({
12+
this._view = new WebContentsView({
1313
webPreferences: {
1414
preload: options.preload || path.join(__dirname, './preload.js')
1515
}
@@ -19,7 +19,7 @@ export class ThemedView {
1919
);
2020
}
2121

22-
get view(): BrowserView {
22+
get view(): WebContentsView {
2323
return this._view;
2424
}
2525

@@ -119,7 +119,7 @@ export class ThemedView {
119119
}
120120

121121
private _isDarkTheme: boolean;
122-
private _view: BrowserView;
122+
private _view: WebContentsView;
123123
}
124124

125125
export namespace ThemedView {

src/main/labview/labview.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import {
5-
BrowserView,
65
clipboard,
76
dialog,
87
Menu,
9-
MenuItemConstructorOptions
8+
MenuItemConstructorOptions,
9+
WebContentsView
1010
} from 'electron';
1111
import log from 'electron-log';
1212
import * as path from 'path';
@@ -56,7 +56,7 @@ export class LabView implements IDisposable {
5656
partition = `partition-${Date.now()}`;
5757
}
5858
}
59-
this._view = new BrowserView({
59+
this._view = new WebContentsView({
6060
webPreferences: {
6161
preload: path.join(__dirname, './preload.js'),
6262
partition
@@ -126,7 +126,7 @@ export class LabView implements IDisposable {
126126
}
127127
}
128128

129-
public get view(): BrowserView {
129+
public get view(): WebContentsView {
130130
return this._view;
131131
}
132132

@@ -305,21 +305,22 @@ export class LabView implements IDisposable {
305305
});
306306
}
307307

308-
dispose(): Promise<void> {
308+
async dispose(): Promise<void> {
309309
this._evm.dispose();
310310

311311
// if local or remote with no data persistence, clear session data
312312
if (
313313
this._sessionConfig.isRemote &&
314-
!this._sessionConfig.persistSessionData
314+
!this._sessionConfig.persistSessionData &&
315+
!this._parent.window.isDestroyed()
315316
) {
316-
if (!this._parent.window.isDestroyed()) {
317-
return clearSession(this._view.webContents.session);
318-
} else {
319-
return Promise.resolve();
320-
}
321-
} else {
322-
return Promise.resolve();
317+
await clearSession(this._view.webContents.session);
318+
}
319+
320+
// A WebContentsView's webContents is not destroyed on detach or window
321+
// close (electron/electron#42884); close it so the dropped view frees its renderer.
322+
if (!this._view.webContents.isDestroyed()) {
323+
this._view.webContents.close();
323324
}
324325
}
325326

@@ -547,7 +548,7 @@ export class LabView implements IDisposable {
547548
});
548549
}
549550

550-
private _view: BrowserView;
551+
private _view: WebContentsView;
551552
private _parent: SessionWindow;
552553
private _sessionConfig: SessionConfig;
553554
private _jlabBaseUrl: string;

src/main/registry.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -992,31 +992,21 @@ export class Registry implements IRegistry, IDisposable {
992992
}
993993
}
994994

995-
private _getExecutableInstances(
995+
private async _getExecutableInstances(
996996
executableName: string,
997997
path: string
998998
): Promise<string[]> {
999-
return new Promise<string[]>((resolve, reject) => {
1000-
which(
1001-
executableName,
1002-
{ all: true, path: path },
1003-
(err: any, result: string | string[]) => {
1004-
if (err) {
1005-
if (err.code === 'ENOENT') {
1006-
resolve([]);
1007-
} else {
1008-
reject(err);
1009-
}
1010-
} else {
1011-
if (typeof result === 'string') {
1012-
resolve([result]);
1013-
} else {
1014-
resolve(result);
1015-
}
1016-
}
1017-
}
1018-
);
999+
// which v7 is promise-only (the callback API was removed in v3); nothrow
1000+
// returns null instead of throwing when the executable is not found.
1001+
const result = await which(executableName, {
1002+
all: true,
1003+
path,
1004+
nothrow: true
10191005
});
1006+
if (!result) {
1007+
return [];
1008+
}
1009+
return typeof result === 'string' ? [result] : result;
10201010
}
10211011

10221012
private _runPythonModuleCommand(

0 commit comments

Comments
 (0)