You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is a comprehensive tracking issue for reviving active maintenance of JupyterLab Desktop. Supersedes #919 (Electron v38, EOL).
Work branch:phase3/electron-42 for Phase 3; Phase 1–2 PRs land directly on master. (The earlier revival/electron-41 branch was retired: it carried obsolete Phase 2 commits already on master plus security work that belongs in the private advisory process, so Phase 3 starts from a clean branch off current master.)
Current state:
Electron 27.0.2 — EOL, 15 majors behind (current: 42.x)
TypeScript 4.2 (current: 6.x); no React in the source, the @types/react devDeps are dead weight to drop
Apple cert and CI secrets expired during the maintenance gap status unconfirmed from the code (no signed release since v4.2.5-1, 2024-08-29) — see To verify
Several draft security advisories pending fix/publication before next release
Previous Electron upgrade attempt: PR #910 (Electron 37, self-closed) — not merged, not rebased onto current branch. Build issues documented in #919. Phase 3 starts from scratch.
Phase 1 — Dependency security fixes
Triaged the Dependabot bumps by where each dep runs (yarn why). Build/dev-only deps don't ship in the asar (yarn 1 omits devDeps), so their runtime risk is null even when the PR is stuck.
C2 (test: main-process unit coverage with CI-enforced thresholds (C2) #967, merged): remaining main-process unit tests (tokens, eventtypes, utils, settings, workspacesettings, appdata, sessionconfig, cli + handlers, EventManager), table-driven where useful, plus v8 coverage thresholds with per-file locks on the covered modules. 306 tests, signed, test-only. 320 tests, signed. CI runs yarn test:coverage, so the per-file locks gate every PR (not just locally). Logic layer locked (eventmanager 100%, sessionconfig 94%, settings 91%, appdata 77%); cli.ts/env.ts stay low by design, their spawn + Python-discovery branches are integration-only and deferred to C3. The tests surfaced 5 real main-process bugs now fixed in the same PR (a delete-guard that fell through to rmSync, sync ipc handlers removed with the wrong API, a session-clear that could hang, two settle-without-return). Copilot review addressed, threads resolved.
C2b (build: husky + lint-staged pre-commit hook (C2b) #968, closed): Husky + lint-staged pre-commit got a maintainer -1; closed. A Python pre-commit setup (the Jupyter-org norm) is the likely replacement, revisited later.
Cross-platform CI matrix moved into Phase 3, where the packaged-build e2e makes a [ubuntu, macos, windows] matrix clean (the current dev-build provisioning is Linux-specific)
Supply-chain CI hardening deferred to post-release (gitleaks/trufflehog)
Phase 2b — Security advisories
Several draft security advisories are being addressed through GitHub's private advisory process before the next release.
1 advisory (IPC token leak via third-party content): fix not yet started
1 advisory (nodeIntegration/contextIsolation): closed by the Phase 3 pre-bump PR that makes contextIsolation/nodeIntegration/sandbox explicit on every view and window (no longer blocked on the Electron refactor)
Phase 3 — Electron 41 42 upgrade
Target: Electron 41.x Electron 42.x (41 is EOS ~25 Aug 2026, 42 shipped; the cumulative breaking-change set is the same). Grep-verified against this codebase, so the real surface is small (most generic breaking changes don't apply here).
Pre-bump PRs (land on master first)
The Electron bump can't be a single safe step. The TypeScript stack has to move first (Electron 42 runs on Node 24, which forces @types/node up, which forces TypeScript up), and a few code/CI changes de-risk the packaged build. These go up as separate PRs on master before the bump branch opens. The first two are ordered; the rest are independent. All of the items below are built and verified locally; PRs open once the set is complete.
Type-check gate — a dedicated tsc --noEmit workflow on the current TypeScript 4.2, baseline zero errors. The net for the next PR. (publish.yml only runs tsc inside the slow installer job, so a fast gate is net-new.)
Type + lint modernization — TypeScript 4.2 → 6.0, @types/node 14 → 24, @typescript-eslint 5 → 8, ESLint 8 → 10 (flat config, .eslintrc.js → eslint.config.mjs, tseslint.config()), removed the dead @types/react/@types/react-dom/eslint-plugin-react. Forced here because @types/node@24 needs TS ≥ 5.3 and @typescript-eslint@8 supports TS <6.1. Verified outcomes: moduleResolution: nodenext (not the deprecated node), strict: false to preserve the 4.2 level (strict-null migration deferred), skipLibCheck for the transient @types/node@24 vs electron@27 clash, preloads moved to import electron = require('electron') (native ipcRenderer types, runtime stays a sandbox require), and fast-xml-parser 5.7 → 5.8 which fixed an upstream .d.cts typo and removed any need for ignoreDeprecations. Green on tsc/eslint/prettier/build/430 unit/e2e; news feed re-verified against the live feed.
P3-A — contextIsolation/nodeIntegration/sandbox explicit at the seven construction sites via a shared hardenedWebPreferences helper. Closes the nodeIntegration/contextIsolation advisory. sandbox:true verified non-breaking (the 12 preloads use only contextBridge/ipcRenderer) by unit + e2e in real runtime.
P3-B — a JLAB_DESKTOP_E2E_MODE=1 opt-out for app.relaunch() + requestSingleInstanceLock so packaged-build Playwright can launch the real artifact. Unit-tested with a mutation check; the existing e2e still pass (default behavior preserved).
P3-C — scripts/stub-env-tar.js producing a minimal env_installer/jlab_server.tar.gz so electron-builder --dir runs in CI without conda-lock, with a unit test on the tarball. The bundled env is unused in e2e (Python comes from JLAB_TEST_PYTHON_PATH).
Code changes (what the app actually uses)
BrowserView → WebContentsView (deprecated v30, removed v42) — 12 call sites in sessionwindow.ts (addBrowserView→contentView.addChildView, removeBrowserView→removeChildView, getBrowserViews()→.children) plus the BrowserView type in 4 view classes. setBounds/setBackgroundColor/webContents unchanged. Verified safe: all 4 views already call setBackgroundColor in their constructors, so the transparent-by-default change needs no new calls. Real leak introduced: WCV does NOT auto-destroy webContents on removeChildView (BrowserView did) — needs explicit webContents.close() at the three disposal sites (sessionwindow.ts:265, :1181, :1191); the reused progress/popup views must NOT be closed. Review hazard: SessionWindow has its own contentView getter that now reads next to win.contentView. Highest-risk migration, isolate in its own commit. Risk: HIGH.
File.path → webUtils.getPathForFile() (removed v32) — welcomeview.ts:532 drag-drop. Trap: the handler lives in a data:text/html<script> (page/renderer world), where webUtils is unavailable, so it cannot be swapped in place. Fix is two-part: add getPathForFile to the welcomeview preload's contextBridge surface (webUtils.getPathForFile(file)), and have the page script call window.electronAPI.getPathForFile(file). Risk: MEDIUM (the renderer/preload boundary, no unit coverage for data: page scripts).
contextBridge whole-ipcRenderer ban (v29) — all 12 preloads already wrap each call (verified test: characterize the 12 preload contextBridge contracts #971); confirm still compliant after the bump. (draft: compliance assumed from current code, not tested on E42)
clipboard out of the renderer (v40) — copyToClipboard already routes via IPC (preload), not navigator.clipboard; confirm. (draft: assumed compliant, not tested on E42)
N/A here (grep-confirmed, no usage in src/):webRequest urls:[]→['<all_urls>'] (v35) · session.setPreloads→registerPreloadScript (v35) · registerFileProtocol/baseURLForDataURL (v33) · crash events→render-process-gone (v29) · WebContents nav→navigationHistory (v32) · setAutoResize removed (already manual setBounds).
Config / build
Electron-side dependency bumps, landed together on the Phase 3 branch (the type stack already moved to the pre-bump PR above): electron ^27 → ^42, electron-builder ^24.9 → ^26.x, electron-notarize ^1.2 → @electron/notarize ^3 (v3 is ESM-only → dynamic import() from the CJS notarize script; drop the removed tool and appBundleId keys), electron-log ^4.4 → ^5.4 (split import electron-log/main + log.initialize(); the rest of the API this app uses is unchanged), update-electron-app ^2 → ^3 (named export, still CJS-compatible).
electron-builder v26 electronFuses block (cookie encryption, asar integrity, only-load-app-from-asar, no NODE_OPTIONS, no inspect args, runAsNode off). Net-new (there is no afterPack today, only afterSign: notarize); builder v26 flips fuses after pack, before sign, so no resetAdHocDarwinSignature is needed. Caveat:runAsNode: false + the no-NODE_OPTIONS/no-inspect fuses overlap the macOS entitlements track (the allow-dyld-environment-variables etc. in build/entitlements.plist). Do NOT tighten entitlements in the fuses commit; coordinate with that separate work so they don't collide. Validate a packaged build launches per-OS before enabling on CI (fuses can make an app fail Gatekeeper). Risk: MEDIUM-HIGH.
Linux Wayland default (v38, --ozone-platform=auto) — keep auto or pin --ozone-platform=x11.
No native node modules in the tree (the bundled Python is conda, not a node addon), so the Node-ABI / C++20 / @electron/rebuild concern is N/A here; only electron-builder rebuilds its own runtime.
Verify only: macOS notifications now require code-signing (v42) · PDFs share the host WebContents via OOPIF (v41) · GTK4 default on GNOME (v36) · the electron npm package no longer downloads its binary in postinstall (v42) → CI install/cache impact · setTrafficLightPosition→setWindowButtonPosition and ipcRenderer.sendTo removed (v28) if ever used.
e2e hardening (Phase 3, golden standard — diagnosed 2026-05-28)
The Phase 2 e2e (#976) launches the DEV build (electron.launch({ args: ['.'] })) on Linux only, with a system-unzip step because electron 27's postinstall extract-zip drops the binary on the runner. Two upgrades belong here, where Electron/packaging change anyway:
Provisioning: at Electron 42 the npm electron postinstall no longer downloads the binary, so the current node install.js+unzip step stops working. The golden standard is to stop launching node_modules/electron entirely: package with electron-builder --dir and launch the real artifact via electron-playwright-helpers findLatestBuild() + parseElectronApp() (electron.launch({ executablePath, ... })). Verified locally: parseElectronApp resolves the electron-builder dist/<platform> layout and the packaged app launches + exposes CDP. The launch-stall BLOCKER (the packaged app's requestSingleInstanceLock + app.relaunch() make the monitored process exit) is resolved by P3-B (the JLAB_DESKTOP_E2E_MODE opt-out) + P3-C (the stub jlab_server.tar.gz for the extraResources copy).
Cross-OS macOS leg: e2e now runs a [ubuntu, macos] matrix (fail-fast: false), the Linux-only Electron-unzip workaround + xvfb-run guarded to ubuntu, plain launch on macOS. Built locally (the macOS leg verified by reading, runs in CI). Windows leg follows with the packaged-build switch.
Jupyter server isolation: the e2e helper now redirects HOME + JUPYTER_CONFIG_DIR + JUPYTER_DATA_DIR + JUPYTER_RUNTIME_DIR to per-launch temp dirs (pytest-jupyter jp_environ parity), so the spawned server never touches the runner's real ~/.jupyter/~/Library/Jupyter. Verified empirically: jupyter wrote jpserver-*.json into the temp and zero new files into the real runtime dir. Note: on macOS Electron's app.getPath('home') ignores $HOME (NSHomeDirectory), so HOME isolates only the spawned python server, not Electron's own logs; --user-data-dir covers Electron's userData. Added a unit test on server.ts asserting JUPYTER_CONFIG_DIR = userData.
Packaged-build provisioning (the remaining Phase 3 e2e): at Electron 42 the npm electron postinstall no longer downloads the binary, so package with electron-builder --dir and launch via parseElectronApp() + findLatestBuild(). The launch-stall blocker is unblocked by P3-B (e2e-mode) + P3-C (stub tar). This pulls the Windows leg in too.
Testing (2026 golden standard): stay on Playwright _electron (the Electron docs pick no winner between it, WebdriverIO and a custom driver; move to @wdio/electron-service only if main-process API mocking becomes a wall). Do not e2e the JupyterLab view through Electron — it is a web app on an HTTP URL, so test it with a plain Playwright browser against the running server; reserve full-app e2e for the Electron glue (the right windows/views appear, IPC, lifecycle).
Testing gaps to add during Phase 3 (draft — spike first): today's e2e is first-run with no Python env, so welcome actions are disabled and the labview/dialogs are never reached. Add, in a Playwright fixture (alongside the per-test --user-data-dir):
Seed-a-Python-env mechanism + welcome-actions-enable: DONE in Phase 2 (test: Playwright e2e suite (first-run smoke + env-backed welcome) #976) — seeds app-data.jsonpythonPath (the real seam; NOT JUPYTERLAB_DESKTOP_PYTHON_PATH, which the app doesn't read), CI provisions a venv with jupyterlab.
e2e glue: "New notebook" boots a server and the labview reaches its URL — DONE in Phase 2 (test: Playwright e2e suite (first-run smoke + env-backed welcome) #976). The session reads its env from settings.json (pythonPath) + app-data.json (userSetPythonEnvs registers it); both are seeded. waitForWindowByUrl asserts the local server URL without driving the JupyterLab DOM.
JupyterLab content smoke uses plain @playwright/test (already a devDep, zero new deps) against the embedded Jupyter server URL. Initial scope: assert .jp-FileBrowser + .jp-Launcher render in the labview page after waitForWindowByUrl; deeper coverage (open a notebook, run a cell) deferred unless needed. The org standard is @jupyterlab/galata, but it's rejected here on dep weight: it pulls ~20 packages (half of @jupyterlab/* + vega/vega-lite/@stdlib) and exists to test JupyterLab's own UI in depth — we only need "the labview loads", not JupyterLab-internals coverage.
Unit stays on vitest (ahead of the org, which is jest-29-except-lumino) — intentional for the Node-side Electron logic; alignment that matters is the Galata/visual layer above.
Spike first (prove the stack assumption before building): confirm Playwright/CDP attaches to the labview WebContentsView and waitForWindowByUrl resolves on the launched app. (draft)
Commit + revert hygiene for Phase 3
Each substantive change is its own atomic, build-green commit (revertible on its own if a PR is ever rebase-merged instead of squashed). Phase 3 is split into separate PRs (the pre-bump PRs plus the bump) so revert granularity on master is per concern. CI-debug iterations (diagnostics, install-tooling dead-ends) get folded into the commit whose fix they justify rather than kept as standalone commits, since they are not atomic units and only add noise to the squashed body. Per-commit AI disclosure stays.
Phase 4 — Native cleanup, drop deps
The TypeScript/ESLint modernization moved up to Phase 3 (the Electron bump forces it). What's left is replacing deps with native Node 24 / Electron 42 builtins, area by area, so each PR stays reviewable. A dependency audit (usage grepped against native equivalents) sorted these:
Build-script-only, native fs, low risk (these never ship in the asar):
meow → util.parseArgs (buildutil.js only; not yargs, which stays in the production CLI)
shx → native fs in the one cp script
node-watch → fs.watch (copyassets.js dev watch)
webpack + webpack-cli → esbuild for the preload bundling (the only thing webpack does here; ~25 MB of devDeps gone)
Runtime, worth it but needs care, one area at a time:
ejs → template literals. Nine dialogs, and some templates do have logic (ternaries, injected conditionals), so this is per-dialog and never eval.
fix-path → internalize. It wraps shell-path → shell-env, which spawns the user's login shell to capture the real PATH (pyenv, conda, homebrew, nvm). A hardcoded path list would break Python-env discovery, so the replacement has to keep the shell spawn.
Opportunistic dep bump:
tar 6 → 7 (v7 ships its own types, so @types/tar goes; tar.x still exists, the one extraction call survives)
prettier 2.1 (2020) → 3.x as a dedicated format-only PR, kept off the bump diff
Keep (no reasonable native equivalent): @lumino/signaling, fast-xml-parser, js-yaml, semver, which, winreg, yargs, istextorbinary, @leeoniya/ufuzzy, @jupyter-notebook/web-components, read-package-tree (deprecated but the replacement is fragile).
Remaining Dependabot PRs (webpack folded into the esbuild move, js-yaml, CI actions)
To verify / external blockers (can't confirm from the code; need Foundation/org or a tagged release run)
Apple Developer cert renewal (NumFocus → Linux Foundation migration) — without updated CSC_LINK/APPLEID/APPLE_TEAM_ID secrets, no signed macOS releases
GitHub Actions secrets renewal (expired during maintenance gap)
Snap Store credential refresh (SNAPCRAFT_STORE_CREDENTIALS)
Whether the macOS signing secrets are actually still expired — PR installer builds are unsigned (CSC_IDENTITY_AUTO_DISCOVERY=false off-release), so green PR CI does not test signing; only a tagged release run does. Last signed release: v4.2.5-1 (2024-08-29).
This is a comprehensive tracking issue for reviving active maintenance of JupyterLab Desktop. Supersedes #919 (Electron v38, EOL).
Work branch:
phase3/electron-42for Phase 3; Phase 1–2 PRs land directly on master. (The earlierrevival/electron-41branch was retired: it carried obsolete Phase 2 commits already on master plus security work that belongs in the private advisory process, so Phase 3 starts from a clean branch off current master.)Current state:
@types/reactdevDeps are dead weight to dropexpired during the maintenance gapstatus unconfirmed from the code (no signed release since v4.2.5-1, 2024-08-29) — see To verifymacOS Tahoe perf regression on current Electron (Electron-based apps cause a huge system-wide lag on macOS 26 electron/electron#48311)moved to To verify (can't confirm applicability from the code)Previous Electron upgrade attempt: PR #910 (Electron 37, self-closed) — not merged, not rebased onto current branch. Build issues documented in #919. Phase 3 starts from scratch.
Phase 1 — Dependency security fixes
Triaged the Dependabot bumps by where each dep runs (
yarn why). Build/dev-only deps don't ship in the asar (yarn 1 omits devDeps), so their runtime risk is null even when the PR is stuck.fast-xml-parser4.2.5 → 5.7.0 (XML entity fix) — Bump fast-xml-parser from 4.2.5 to 5.7.0 #849 mergedserialize-javascript→ 6.0.2 (XSS), build-only viaterser-webpack-plugin; never reaches the build output on masterwebpack5.76 → 5.94 (Bump webpack from 5.76.0 to 5.94.0 #863, rebased, open/MERGEABLE) — direct devDepresolutionsjs-yaml4.1.0 → 4.1.1 (Bump js-yaml from 4.1.0 to 4.1.1 #934, runtime path inapp.ts) — rebased, awaiting reviewPhase 2 — Test infrastructure
Stacked PRs against master, each atomic:
C1 (feat: add test infrastructure (Vitest + Playwright) #961, merged): Vitest + Playwright + 21 env tests + 1 e2e smoke + a
publish.ymltest job (least-privilegecontents: read). Test-only.C2 (test: main-process unit coverage with CI-enforced thresholds (C2) #967, merged): remaining main-process unit tests (tokens, eventtypes, utils, settings, workspacesettings, appdata, sessionconfig, cli + handlers, EventManager), table-driven where useful, plus v8 coverage thresholds with per-file locks on the covered modules.
306 tests, signed, test-only.320 tests, signed. CI runsyarn test:coverage, so the per-file locks gate every PR (not just locally). Logic layer locked (eventmanager 100%, sessionconfig 94%, settings 91%, appdata 77%); cli.ts/env.ts stay low by design, their spawn + Python-discovery branches are integration-only and deferred to C3. The tests surfaced 5 real main-process bugs now fixed in the same PR (a delete-guard that fell through to rmSync, sync ipc handlers removed with the wrong API, a session-clear that could hang, two settle-without-return). Copilot review addressed, threads resolved.C2b (build: husky + lint-staged pre-commit hook (C2b) #968, closed): Husky + lint-staged pre-commit got a maintainer -1; closed. A Python
pre-commitsetup (the Jupyter-org norm) is the likely replacement, revisited later.C3 (the test/build safety net before the upgrade): shipped as three independent PRs off repo branches, each CI-green, plus one closed: preload contextBridge characterization across all 12 files (test: characterize the 12 preload contextBridge contracts #971, merged); cli.ts/env.ts spawn + Python-discovery coverage (test: cover cli.ts and env.ts process-spawn branches #972, merged); e2e expansion + fixtures + CI wiring under xvfb (test: Playwright e2e suite (first-run smoke + env-backed welcome) #976, merged). The
tsc --noEmitgate (ci: add a tsc --noEmit type-check workflow #973) was closed unmerged and folds into Phase 3 as a pre-bump PR (the type-check is most useful sitting in front of the Electron + TS bumps).preload bundle budgetdropped (ci: add a preload bundle size budget check #974): a desktop preload bundle has no transfer cost and real bundle breakage is already caught by the installer build.gitleaks secret scandeferred to post-release. CI standardizes on Node 24 (the version Electron bundles), not 26 (Node 26 broke Electron's install tooling). SHA-pinned actions,contents: readdefault and--frozen-lockfilealready landed in fix: resolve CodeQL high-severity findings and harden CI #966. Note: Disable yarn scripts #963 (enableScripts: false) was Yarn Berry only, no-op on Yarn 1, so it is closed; that hardening rides on the Yarn 1 -> Berry migration in Phase 4.Vitest unit tests — feat: add test infrastructure (Vitest + Playwright) #961, expanded in C2 (320 tests)
Playwright +
_electronE2E — smoke feat: add test infrastructure (Vitest + Playwright) #961; expanded + wired into CI (test: Playwright e2e suite (first-run smoke + env-backed welcome) #976)Smoke for the Upgrade Electron ecosystem (37/EB 26) + deps, CI, React types #910/Upgrade to Electron v38 #919 blockers: launch, DOM load, clean exit
Coverage thresholds on the logic layer, enforced in CI via
test:coverage— C2Husky + lint-staged — C2b (build: husky + lint-staged pre-commit hook (C2b) #968)closed, maintainer -1; pivot to Pythonpre-commit(the Jupyter-org norm) or droptsc --noEmittype-check workflow — folded into Phase 3 as a pre-bump PR (ci: add a tsc --noEmit type-check workflow #973 closed unmerged)Preload contextBridge characterization, 12 files — test: characterize the 12 preload contextBridge contracts #971
cli.ts/env.ts spawn + discovery coverage — test: cover cli.ts and env.ts process-spawn branches #972
e2e suite wired into CI: first-run smoke (launch, welcome renders, multi-view composition guarding the BrowserView→WebContentsView migration, clean shutdown) + harness — test: Playwright e2e suite (first-run smoke + env-backed welcome) #976
e2e: welcome local-server actions enable when a Python env is seeded into
app-data.json(env-backed, CI provisions a venv with jupyterlab) — test: Playwright e2e suite (first-run smoke + env-backed welcome) #976e2e: "New notebook" boots a real Jupyter server and the labview reaches its URL (waitForWindowByUrl), validated in CI under xvfb — test: Playwright e2e suite (first-run smoke + env-backed welcome) #976
Cross-platform CI matrixmoved into Phase 3, where the packaged-build e2e makes a[ubuntu, macos, windows]matrix clean (the current dev-build provisioning is Linux-specific)Supply-chain CI hardeningdeferred to post-release (gitleaks/trufflehog)Phase 2b — Security advisories
Several draft security advisories are being addressed through GitHub's private advisory process before the next release.
contextIsolation/nodeIntegration/sandboxexplicit on every view and window (no longer blocked on the Electron refactor)Phase 3 — Electron
4142 upgradeTarget:
Electron 41.xElectron 42.x (41 is EOS ~25 Aug 2026, 42 shipped; the cumulative breaking-change set is the same). Grep-verified against this codebase, so the real surface is small (most generic breaking changes don't apply here).Pre-bump PRs (land on master first)
The Electron bump can't be a single safe step. The TypeScript stack has to move first (Electron 42 runs on Node 24, which forces
@types/nodeup, which forces TypeScript up), and a few code/CI changes de-risk the packaged build. These go up as separate PRs on master before the bump branch opens. The first two are ordered; the rest are independent. All of the items below are built and verified locally; PRs open once the set is complete.tsc --noEmitworkflow on the current TypeScript 4.2, baseline zero errors. The net for the next PR. (publish.ymlonly runstscinside the slow installer job, so a fast gate is net-new.)@types/node14 → 24,@typescript-eslint5 → 8, ESLint 8 → 10 (flat config,.eslintrc.js→eslint.config.mjs,tseslint.config()), removed the dead@types/react/@types/react-dom/eslint-plugin-react. Forced here because@types/node@24needs TS ≥ 5.3 and@typescript-eslint@8supports TS<6.1. Verified outcomes:moduleResolution: nodenext(not the deprecatednode),strict: falseto preserve the 4.2 level (strict-null migration deferred),skipLibCheckfor the transient@types/node@24vs electron@27 clash, preloads moved toimport electron = require('electron')(native ipcRenderer types, runtime stays a sandbox require), andfast-xml-parser5.7 → 5.8 which fixed an upstream.d.ctstypo and removed any need forignoreDeprecations. Green on tsc/eslint/prettier/build/430 unit/e2e; news feed re-verified against the live feed.contextIsolation/nodeIntegration/sandboxexplicit at the seven construction sites via a sharedhardenedWebPreferenceshelper. Closes the nodeIntegration/contextIsolation advisory. sandbox:true verified non-breaking (the 12 preloads use only contextBridge/ipcRenderer) by unit + e2e in real runtime.JLAB_DESKTOP_E2E_MODE=1opt-out forapp.relaunch()+requestSingleInstanceLockso packaged-build Playwright can launch the real artifact. Unit-tested with a mutation check; the existing e2e still pass (default behavior preserved).scripts/stub-env-tar.jsproducing a minimalenv_installer/jlab_server.tar.gzsoelectron-builder --dirruns in CI without conda-lock, with a unit test on the tarball. The bundled env is unused in e2e (Python comes fromJLAB_TEST_PYTHON_PATH).Code changes (what the app actually uses)
BrowserView→WebContentsView(deprecated v30, removed v42) — 12 call sites insessionwindow.ts(addBrowserView→contentView.addChildView,removeBrowserView→removeChildView,getBrowserViews()→.children) plus theBrowserViewtype in 4 view classes.setBounds/setBackgroundColor/webContentsunchanged. Verified safe: all 4 views already callsetBackgroundColorin their constructors, so the transparent-by-default change needs no new calls. Real leak introduced: WCV does NOT auto-destroywebContentsonremoveChildView(BrowserView did) — needs explicitwebContents.close()at the three disposal sites (sessionwindow.ts:265,:1181,:1191); the reused progress/popup views must NOT be closed. Review hazard:SessionWindowhas its owncontentViewgetter that now reads next towin.contentView. Highest-risk migration, isolate in its own commit. Risk: HIGH.File.path→webUtils.getPathForFile()(removed v32) —welcomeview.ts:532drag-drop. Trap: the handler lives in adata:text/html<script>(page/renderer world), wherewebUtilsis unavailable, so it cannot be swapped in place. Fix is two-part: addgetPathForFileto the welcomeview preload's contextBridge surface (webUtils.getPathForFile(file)), and have the page script callwindow.electronAPI.getPathForFile(file). Risk: MEDIUM (the renderer/preload boundary, no unit coverage fordata:page scripts).ipcRendererban (v29) — all 12 preloads already wrap each call (verified test: characterize the 12 preload contextBridge contracts #971); confirm still compliant after the bump. (draft: compliance assumed from current code, not tested on E42)copyToClipboardalready routes via IPC (preload), notnavigator.clipboard; confirm. (draft: assumed compliant, not tested on E42)N/A here (grep-confirmed, no usage in
src/):webRequest·urls:[]→['<all_urls>'](v35)·session.setPreloads→registerPreloadScript(v35)·registerFileProtocol/baseURLForDataURL(v33)crash events→·render-process-gone(v29)WebContents nav→·navigationHistory(v32)(already manualsetAutoResizeremovedsetBounds).Config / build
electron ^27 → ^42,electron-builder ^24.9 → ^26.x,electron-notarize ^1.2 → @electron/notarize ^3(v3 is ESM-only → dynamicimport()from the CJS notarize script; drop the removedtoolandappBundleIdkeys),electron-log ^4.4 → ^5.4(split importelectron-log/main+log.initialize(); the rest of the API this app uses is unchanged),update-electron-app ^2 → ^3(named export, still CJS-compatible).electron-builderv26electronFusesblock (cookie encryption, asar integrity, only-load-app-from-asar, noNODE_OPTIONS, no inspect args,runAsNodeoff). Net-new (there is no afterPack today, onlyafterSign: notarize); builder v26 flips fuses after pack, before sign, so noresetAdHocDarwinSignatureis needed. Caveat:runAsNode: false+ the no-NODE_OPTIONS/no-inspect fuses overlap the macOS entitlements track (theallow-dyld-environment-variablesetc. inbuild/entitlements.plist). Do NOT tighten entitlements in the fuses commit; coordinate with that separate work so they don't collide. Validate a packaged build launches per-OS before enabling on CI (fuses can make an app fail Gatekeeper). Risk: MEDIUM-HIGH.mac.minimumSystemVersion: "12.0.0"(macOS floor: v33→11, v38→12).--ozone-platform=auto) — keep auto or pin--ozone-platform=x11.@electron/rebuildconcern is N/A here; only electron-builder rebuilds its own runtime.Verify only: macOS notifications now require code-signing (v42) · PDFs share the host WebContents via OOPIF (v41) · GTK4 default on GNOME (v36) · the
electronnpm package no longer downloads its binary in postinstall (v42) → CI install/cache impact ·setTrafficLightPosition→setWindowButtonPositionandipcRenderer.sendToremoved (v28) if ever used.e2e hardening (Phase 3, golden standard — diagnosed 2026-05-28)
The Phase 2 e2e (#976) launches the DEV build (
electron.launch({ args: ['.'] })) on Linux only, with a system-unzipstep because electron 27's postinstallextract-zipdrops the binary on the runner. Two upgrades belong here, where Electron/packaging change anyway:electronpostinstall no longer downloads the binary, so the currentnode install.js+unzipstep stops working. The golden standard is to stop launchingnode_modules/electronentirely: package withelectron-builder --dirand launch the real artifact via electron-playwright-helpersfindLatestBuild()+parseElectronApp()(electron.launch({ executablePath, ... })). Verified locally: parseElectronApp resolves the electron-builderdist/<platform>layout and the packaged app launches + exposes CDP. The launch-stall BLOCKER (the packaged app'srequestSingleInstanceLock+app.relaunch()make the monitored process exit) is resolved by P3-B (theJLAB_DESKTOP_E2E_MODEopt-out) + P3-C (the stubjlab_server.tar.gzfor theextraResourcescopy).[ubuntu, macos]matrix (fail-fast: false), the Linux-only Electron-unzip workaround +xvfb-runguarded to ubuntu, plain launch on macOS. Built locally (the macOS leg verified by reading, runs in CI). Windows leg follows with the packaged-build switch.HOME+JUPYTER_CONFIG_DIR+JUPYTER_DATA_DIR+JUPYTER_RUNTIME_DIRto per-launch temp dirs (pytest-jupyterjp_environparity), so the spawned server never touches the runner's real~/.jupyter/~/Library/Jupyter. Verified empirically: jupyter wrotejpserver-*.jsoninto the temp and zero new files into the real runtime dir. Note: on macOS Electron'sapp.getPath('home')ignores$HOME(NSHomeDirectory), soHOMEisolates only the spawned python server, not Electron's own logs;--user-data-dircovers Electron's userData. Added a unit test onserver.tsassertingJUPYTER_CONFIG_DIR = userData.electronpostinstall no longer downloads the binary, so package withelectron-builder --dirand launch viaparseElectronApp()+findLatestBuild(). The launch-stall blocker is unblocked by P3-B (e2e-mode) + P3-C (stub tar). This pulls the Windows leg in too.Testing (2026 golden standard): stay on Playwright
_electron(the Electron docs pick no winner between it, WebdriverIO and a custom driver; move to@wdio/electron-serviceonly if main-process API mocking becomes a wall). Do not e2e the JupyterLab view through Electron — it is a web app on an HTTP URL, so test it with a plain Playwright browser against the running server; reserve full-app e2e for the Electron glue (the right windows/views appear, IPC, lifecycle).Testing gaps to add during Phase 3 (draft — spike first): today's e2e is first-run with no Python env, so welcome actions are disabled and the labview/dialogs are never reached. Add, in a Playwright fixture (alongside the per-test
--user-data-dir):app-data.jsonpythonPath(the real seam; NOTJUPYTERLAB_DESKTOP_PYTHON_PATH, which the app doesn't read), CI provisions a venv with jupyterlab.@playwright/test(already a devDep, zero new deps) against the embedded Jupyter server URL. Initial scope: assert.jp-FileBrowser+.jp-Launcherrender in the labview page afterwaitForWindowByUrl; deeper coverage (open a notebook, run a cell) deferred unless needed. The org standard is@jupyterlab/galata, but it's rejected here on dep weight: it pulls ~20 packages (half of@jupyterlab/*+ vega/vega-lite/@stdlib) and exists to test JupyterLab's own UI in depth — we only need "the labview loads", not JupyterLab-internals coverage.please update snapshotsCI bot that regenerates baselines in CI (portjupyter/notebook'splaywright-update.yml, gated to OWNER/COLLABORATOR/MEMBER). Generate baselines in CI, never locally. This is the org's bulletproof pattern; supersedes Fix Playwright tests to properly launch JupyterLab Desktop and capture distinct UI screenshots #909's hand-committed PNGs (no CI-regen bot) — close Fix Playwright tests to properly launch JupyterLab Desktop and capture distinct UI screenshots #909.waitForWindowByUrlresolves on the launched app. (draft)Commit + revert hygiene for Phase 3
Each substantive change is its own atomic, build-green commit (revertible on its own if a PR is ever rebase-merged instead of squashed). Phase 3 is split into separate PRs (the pre-bump PRs plus the bump) so revert granularity on master is per concern. CI-debug iterations (diagnostics, install-tooling dead-ends) get folded into the commit whose fix they justify rather than kept as standalone commits, since they are not atomic units and only add noise to the squashed body. Per-commit AI disclosure stays.
Phase 4 — Native cleanup, drop deps
The TypeScript/ESLint modernization moved up to Phase 3 (the Electron bump forces it). What's left is replacing deps with native Node 24 / Electron 42 builtins, area by area, so each PR stays reviewable. A dependency audit (usage grepped against native equivalents) sorted these:
Build-script-only, native
fs, low risk (these never ship in the asar):fs-extra→fs.cpSync/fs.rmSync/fs.mkdirSync({ recursive: true })(3 build scripts)rimraf→fs.rmSync({ recursive: true, force: true })(only in npm scripts)meow→util.parseArgs(buildutil.js only; not yargs, which stays in the production CLI)shx→ nativefsin the onecpscriptnode-watch→fs.watch(copyassets.js dev watch)webpack+webpack-cli→ esbuild for the preload bundling (the only thing webpack does here; ~25 MB of devDeps gone)Runtime, worth it but needs care, one area at a time:
ejs→ template literals. Nine dialogs, and some templates do have logic (ternaries, injected conditionals), so this is per-dialog and nevereval.fix-path→ internalize. It wrapsshell-path→shell-env, which spawns the user's login shell to capture the real PATH (pyenv, conda, homebrew, nvm). A hardcoded path list would break Python-env discovery, so the replacement has to keep the shell spawn.Opportunistic dep bump:
tar6 → 7 (v7 ships its own types, so@types/targoes;tar.xstill exists, the one extraction call survives)prettier2.1 (2020) → 3.x as a dedicated format-only PR, kept off the bump diffKeep (no reasonable native equivalent):
@lumino/signaling,fast-xml-parser,js-yaml,semver,which,winreg,yargs,istextorbinary,@leeoniya/ufuzzy,@jupyter-notebook/web-components,read-package-tree(deprecated but the replacement is fragile).Phase 5 — JupyterLab version catch-up
To verify / external blockers (can't confirm from the code; need Foundation/org or a tagged release run)
CSC_LINK/APPLEID/APPLE_TEAM_IDsecrets, no signed macOS releasesSNAPCRAFT_STORE_CREDENTIALS)CSC_IDENTITY_AUTO_DISCOVERY=falseoff-release), so green PR CI does not test signing; only a tagged release run does. Last signed release: v4.2.5-1 (2024-08-29).I'll handle all the engineering. Phases 1–5 are unblocked on my end. The governance items need someone from the Foundation or org side.
@krassowski @ijstokes — need someone to approve and merge PRs once CI is green. Even one review every few weeks unblocks this.
@afshin — any movement on the Apple cert from the Foundation side? That's the hard blocker for signed macOS releases.