Compare commits

...

198 Commits

Author SHA1 Message Date
Samuel Attard
8bc4bc55f1 Revert "ci: split macos-x64 tests into 3 shards (#50968)"
This reverts commit a57dbb55cc.
2026-04-13 01:00:44 -07:00
Samuel Attard
9ef4ca2a7b test: fix remaining linux-x64 unhandled rejections
- extensions.spec.ts: the two 'does not take precedence over Electron
  webRequest' tests use an async IIFE inside new Promise(). onBeforeRequest/
  onBeforeSendHeaders fires on the navigation itself, so the outer promise
  resolves (and the load is cancelled/aborted by teardown) before the IIFE's
  awaited loadURL settles; the IIFE then rejects unhandled. .catch() the IIFE.
- api-service-worker-main.spec.ts: loadWorkerScript() wraps wc.loadURL and all
  24 call sites are fire-and-forget; wrap the helper's return in
  dangerouslyIgnoreWebContentsLoadResult. Also .catch() the once(ipcMain,'ping',
  {signal}).then() chain so abortController.abort() in finally doesn't surface
  an AbortError.
- serial/chromium-focus-and-clipboard.spec.ts: executeJavaScript returned the
  WindowProxy from window.open(), which can't be structured-cloned over IPC;
  append '; null' so the result is serializable.
2026-04-13 01:00:15 -07:00
Samuel Attard
2a6960541e fix: don't call process.exit from uncaughtException after vitest loads
vitest's fork worker patches process.exit to throw. Our worker-entry
uncaughtException handler called process.exit(1), which then threw inside
the handler, so Node exited with code 7 ('exception handler threw') and the
original error was hidden. Scope the handler to bootstrap only and remove it
once workers/forks.js is loaded.

Also fix the crash diagnostic: WorkerRequest carries files at
message.context.files (FileSpecification[]), not message.files.
2026-04-13 01:00:15 -07:00
Samuel Attard
e90456a9d6 build: distribute spec/serial/* across shards alongside parallel files
split-tests now buckets spec/*.spec.ts and spec/serial/*.spec.ts as two
independent sets (each sorted by test count, round-robined) so every shard
gets a proportional slice of both. run.js already routes spec/serial/* to
the --no-file-parallelism phase, so within a shard serial files still run
one-at-a-time. This also fixes the serial phase never running on CI — it
was skipped because split-tests only emitted parallel positionals.
2026-04-13 01:00:15 -07:00
Samuel Attard
8c92be4037 test: fix window leaks in api-subframe, api-view, api-web-frame, api-web-utils
- api-subframe: 'should not crash when changing subframe src' never closed w
- api-view: 'can be added as a child of another View' declared a local const w
  shadowing the describe-level one the afterEach closes
- api-web-frame / api-web-utils: defer(() => w.close()) and afterAll win.close()
  don't await 'closed', so windows were still in getAllWindows() when the
  global leak check fired; use closeWindow() which awaits
2026-04-13 01:00:14 -07:00
Samuel Attard
76900132a9 build: log which files a worker was running when it exits unexpectedly
ElectronPoolWorker now tracks the last 'files' payload sent to the child and
prints it (with pid, code, signal) if the child exits non-zero outside stop().
2026-04-13 01:00:14 -07:00
Samuel Attard
3100799343 refactor: move no-windows-leaked assertion to a global afterAll
closeWindow's assertNotWindows path tried to assert 'this was the only
window' one tick after closing, which only worked if the next-tick fired
after the outer afterEach(closeAllWindows) — a race that mostly held under
mocha and doesn't under vitest.

Replace it with assertNoWindowsLeaked() registered as a setupFiles-level
afterAll — it runs once per file after every test-file afterAll, so suites
that share a window across tests (useRemoteContext, the api-context-bridge
reuse, etc.) aren't flagged on every test. Verified ordering with probe
files: a describe-level afterAll(closeWindow) runs before the global check,
and a file that never cleans up is flagged.

closeWindow is now a plain awaited close; the assertNotWindows option and
its 7 call-site opt-outs are removed. useRemoteContext's afterAll now awaits
'closed' so the shared window is actually gone before the leak check fires.
2026-04-13 01:00:14 -07:00
Samuel Attard
d9c2df194f build: use 2 test shards for linux-x64-asan
ASAN instrumented binaries are slower; keep asan at 2 shards while regular
Linux runs a single shard.
2026-04-13 01:00:13 -07:00
Samuel Attard
9a6e0a0251 test: consume {type:'ready'} and send shutdown in api-app setProxy utility test
api-net's shared-utility-process commit changed api-net-spec.js to post a
ready handshake and wait for an explicit shutdown instead of exiting after
each message. This test forks the same fixture and was still assuming the
old one-shot protocol, so it read 'ready' as the result (data.ok undefined).
2026-04-13 00:59:57 -07:00
Samuel Attard
06f54c3ef1 build: reduce Linux test shards from 3 to 1
vitest's file-level parallelism makes per-shard wall-clock short enough that
the fixed per-job setup cost dominates; one 8-core job beats three.
2026-04-13 00:59:56 -07:00
Samuel Attard
2e175b96a0 fix: guard useRemoteContext afterAll close() with isDestroyed()
If the remote-context window was already torn down (closeAllWindows, crash,
or a prior test's cleanup), w.close() throws 'Object has been destroyed'.
2026-04-13 00:59:30 -07:00
Samuel Attard
b337bdd789 test: fix vacuous expect(async fn).to.not.throw() in BrowserView destroy test
The assertion passed an async function to .to.not.throw(), which only checks
sync throws, so it was always green and the unawaited body leaked the
'data:text/html,hello there' loadURL rejection as unhandled. Invoke the IIFE
and await expect(p).to.not.be.rejected() instead.
2026-04-13 00:59:30 -07:00
Samuel Attard
3e0125da61 build: run macos-arm64 tests on macos-15-xlarge; maxWorkers 3 for arm64
macos-15 (4 vCPU / 7 GB) was starving — several workers missed the 90s
startup window. xlarge gives the same headroom x64 gets.
2026-04-13 00:59:30 -07:00
Samuel Attard
e363fd2136 test: move TAB-focus and darwin notification-event tests to spec/serial/
The 'focus handling' describe in chromium.spec.ts (w.focus() + sendInputEvent
TAB cycling) and the three darwin 'emits show and close events' Notification
tests both depend on OS-singleton state (window focus, Notification Center).
2026-04-13 00:59:30 -07:00
Samuel Attard
aed406000e lint: exclude spec/fixtures/** from spec-only rules
Fixture JS files are standalone Electron apps; they can't import spec-helpers
so no-unawaited-load (and the other spec rules) are noise there.
2026-04-13 00:59:29 -07:00
Samuel Attard
28fb9f0965 chore: wrap unawaited loadURL/loadFile in dangerouslyIgnoreWebContentsLoadResult 2026-04-13 00:59:29 -07:00
Samuel Attard
8c887520fd build: cap vitest maxWorkers on macOS CI (arm64: 2, x64: 6)
macos-arm64 tests run on the 4-vCPU / 7 GB macos-15 runner; three concurrent
Electron main processes (cpus-1 default) plus their GPU/network-service
children exhausted it and several workers missed the 90s start window.
macos-x64 uses macos-15-large (12-vCPU / 30 GB); cap at 6 for headroom.
2026-04-13 00:59:29 -07:00
Samuel Attard
5f7a2f99e1 fix: await closeWindow's assert-no-windows check; add no-unawaited-load lint rule
closeWindow's assertNotWindows path scheduled a detached setTimeout that
asserted BaseWindow.getAllWindows().length === 0; by the time the timer
fired the next test's beforeEach had created windows, so the assertion
rejected unhandled. Now it's an inline awaited check so the failure is
attributed to the right test.

no-unawaited-load flags loadURL()/loadFile() calls whose promise isn't
awaited, returned, assigned, .then/.catch'd, passed as an argument, or
explicitly voided. 393 warnings across spec/ — set to 'warn' for now.
2026-04-13 00:59:28 -07:00
Samuel Attard
032cb1d26c chore: faster darwin update tests 2026-04-13 00:59:28 -07:00
Samuel Attard
ce203ae0fc test: isolate crash-case child Electrons with per-test --user-data-dir; fix express/psList CJS imports
crash.spec spawns child Electrons that defaulted to the shared userData
profile; under parallel workers that contended with other test-spawned
Electrons (autoupdater etc.). Each child now gets a mkdtemp --user-data-dir
that afterEach removes (even on timeout, since the kill+rmSync runs in the
hook).

api-autoupdater-darwin: 'import * as express' under vite SSR gives a
namespace object, so express() fails with '__vite_ssr_import_N__ is not a
function'. Use 'import = require()' for express and psList which are
called directly as functions.
2026-04-13 00:59:28 -07:00
Samuel Attard
56d213bc96 test: use query (encoded) + userData dir for unload/close fixture output path
search: passes the path raw and os.tmpdir() may not be the right tmp inside
the CI container; query: {out} URL-encodes properly and userData is the
per-worker mkdtemp we control.
2026-04-13 00:59:28 -07:00
Samuel Attard
b6494c70b1 test: move BrowserView setBackgroundColor screen-capture tests to spec/serial
These capture the primary display and assert center-pixel colour, which
breaks when a parallel worker has a window visible.
2026-04-13 00:59:27 -07:00
Samuel Attard
a23a4b8f5c test: skip blur-during-destroy test in api-web-contents-view
The 'does not crash when closed via window.close()' test (added in #47933)
listens for 'blur' during WebContents destruction; on macOS that Emit runs
a microtask checkpoint after weak_factory_ is already torn down, so any
re-entrant Destroy() -> GetWeakPtr() DCHECKs at base/memory/weak_ptr.h:298.
Skipped with a TODO until the native lifecycle is fixed.

Also rewrites the 'accepts existing webContents object' count assertion to
compare id sets so still-closing contents from prior tests don't skew it.
2026-04-13 00:59:27 -07:00
Samuel Attard
dd93cd1fa9 test: fix linux shard failures (in-app-purchase, browser-view, web-contents-view, modules/esm)
- api-in-app-purchase: wrap in ifdescribe(darwin) instead of early-return,
  which left an empty suite that vitest treats as an error
- api-browser-view: custom afterEach nulls w before defer() callbacks run;
  call runCleanupFunctions() first so defer(() => w.removeBrowserView(...))
  sees a live window
- api-web-contents-view: afterEach used contents.close() without awaiting
  destruction, so the next test's getAllWebContents() count included
  still-closing leftovers; use cleanupWebContents() which awaits 'destroyed'
- modules/esm: run the ESM-vs-CJS key comparison in a child Electron (outside
  vite's alias) via a .mjs fixture, sort both lists, and filter Node's
  CJS-interop 'default'/'module.exports' keys before comparing
2026-04-13 00:59:27 -07:00
Samuel Attard
dc8cd08dc7 perf: reuse a single BrowserWindow per sandbox mode in api-context-bridge
makeBindingWindow now lazily creates one window per describe and swaps
preloads via session.registerPreloadScript/unregisterPreloadScript instead
of constructing a fresh BrowserWindow per test. ~21s -> ~9s for 144 tests.
2026-04-13 00:59:26 -07:00
Samuel Attard
6bbc801fc2 lint: add /** @remote no-locals */ mode and tag callWithBindings
no-locals is stricter than plain @remote: closures may not reference
ANY identifier declared in the file (imports from anywhere, or file-scope
const/let/var/function/class). JS/DOM globals are fine. Applied to
callWithBindings in api-context-bridge.spec.ts, whose closures are
executeJavaScript'd in a page world with no __rt shim and no require.
2026-04-13 00:59:26 -07:00
Samuel Attard
4a506e7e25 fix: route contextBridge through remote-tools for makeBindingWindow closures
makeBindingWindow stringifies its bindingCreator closure into a preload
script; under vite the captured 'contextBridge' import becomes
__vite_ssr_import_N__.contextBridge. Tag makeBindingWindow /** @remote */,
run rewriteForRemoteEval on the closure, and declare 'const __rt = renderer_1'
in the preload so __rt.contextBridge resolves.

remote-tools.ts re-exports contextBridge/ipcRenderer/webFrame from
electron/renderer — undefined at runtime in the main process, but typed
correctly for closures that only ever execute in a preload.

callWithBindings closures only reference their 'root' param, so no change
needed there. 144/144 pass.
2026-04-13 00:59:26 -07:00
Samuel Attard
59b3e45d0c perf: share a single utility process across itUtility tests in api-net
Lazy-fork once, await a 'ready' handshake, then post each test body to the
same long-lived child. The fixture no longer exits after each message; it
posts {ok:false} on failure and keeps running, and exits cleanly on a
{type:'shutdown'} message sent from afterAll.
2026-04-13 00:59:26 -07:00
Samuel Attard
2276abbd96 feat: support /** @remote */ tagging in the remote-tools-imports lint rule
Functions declared with a /** @remote */ JSDoc (or const/for-of bindings
so tagged) are treated like itremote/remotely: any closure passed to them
is checked for imports that don't come from ./lib/remote-tools.

api-net.spec.ts's itUtility stringifies its closure and posts it to a
utility-process fixture, so it's the same __vite_ssr_import_ problem.
- itUtility now runs rewriteForRemoteEval(fn) before posting
- the fixture declares a local __rt spreading net-helpers + electron/main
- the for-of loop that aliases itUtility as 'test' is tagged @remote
- remote-tools.ts re-exports net, session, http, defer, and the net-helpers
- api-net.spec.ts imports those via remote-tools (closure bodies unchanged)

api-net: 252 passed / 2 todo.
2026-04-13 00:59:25 -07:00
Samuel Attard
5b10d20d38 chore: update yarn.lock after dropping sinon from spec deps 2026-04-13 00:59:25 -07:00
Samuel Attard
88ec0f4bef test: await two previously-unawaited waitUntil calls
Both passed without actually waiting for the condition; now that waitUntil
takes ctx.signal they at least stopped on abort, but the assertions were
still never gating the test.
2026-04-13 00:59:25 -07:00
Samuel Attard
cb60689173 test: replace sinon with vi.mock in version-bump and drop sinon dep
Same ESM-vs-CJS stub issue as release-notes: sinon.stub on
require('node:child_process') doesn't intercept the ESM import that
version-bumper.ts uses under vite. vi.mock('node:child_process', {spy:true})
does. sinon is no longer used anywhere in spec/.
2026-04-13 00:59:24 -07:00
Samuel Attard
34c05cd1a5 refactor: thread ctx.signal through waitUntil so polling stops on test abort 2026-04-13 00:59:24 -07:00
Samuel Attard
4929a267e6 test: replace sinon stub with vi.mock for spawnSync in release-notes
Under vite, notes.ts imports spawnSync via ESM; sinon.stub on the CJS
require() object doesn't intercept that binding. vi.mock('node:child_process',
{spy: true}) replaces the module in vite's graph so vi.mocked(cp.spawnSync)
.mockImplementation() applies to the import notes.ts sees.
2026-04-13 00:59:24 -07:00
Samuel Attard
7d31ad9297 test: write unload/close fixture output to per-test tmp paths
close.html and unload.html previously wrote to __dirname + '/close'|'/unload'
inside spec/fixtures/api/ itself. Pass the output path via ?out= query param
and use a pid+timestamp tmp file so parallel workers and retries never race.
2026-04-13 00:59:24 -07:00
Samuel Attard
8cebedc7b6 test: fix nested it() calls that should have been describe()/ifdescribe()
16 inner it() calls in api-browser-window.spec.ts were wrapped in an outer
it() or ifit() where the author meant describe()/ifdescribe(). Under mocha
these inner tests were registered as siblings at collection time; under
vitest they throw. Also removes two unused chai imports in api-safe-storage
left over from moving chai.use() to setup.ts.
2026-04-13 00:59:23 -07:00
Samuel Attard
69c9ff51eb lint: add no-nested-tests rule for it()/ifit() nested in test bodies
Mocha silently tolerated it() inside another it()/ifit() body; vitest does
not. The rule tracks test-call nesting depth and flags any inner definition.
2026-04-13 00:59:23 -07:00
Samuel Attard
64f3c1c199 chore: route itremote/remotely closure imports through remote-tools
Swaps expect/setTimeout/BrowserWindow imports in the 5 files flagged by
remote-tools-imports/no-foreign-imports-in-remote-closure so their
stringified closures resolve via the __rt shim. Import-line changes only.
2026-04-13 00:59:23 -07:00
Samuel Attard
0d321ccae0 lint: add rule banning foreign imports in itremote/remotely closures
remote-tools-imports/no-foreign-imports-in-remote-closure flags any
identifier inside an itremote()/remotely() closure that is bound to an
import from somewhere other than spec/lib/remote-tools. Those bindings
get rewritten to __vite_ssr_import_N__ by vite's SSR transform and fail
when the closure is stringified and eval'd in a remote context.

Skips type-only imports and type-annotation positions. Currently flags
50 sites across 5 files (node, webview, chromium, fuses, api-native-image).
2026-04-13 00:59:22 -07:00
Samuel Attard
36e5f6a2a5 feat: add spec/lib/remote-tools for imports used in itremote/remotely closures
vite's SSR transform rewrites import bindings to __vite_ssr_import_N__,
which breaks closures that are stringified and eval'd in a remote context.

remote-tools.ts re-exports the common modules (path, fs, url, expect, ...)
so spec files can 'import { path, expect } from ./lib/remote-tools' and
closure bodies stay unchanged. runRemote/remotely regex-replace every
__vite_ssr_import_N__ with __rt (a renderer-side object whose keys mirror
the remote-tools export names), so __rt.path.join(...) etc. resolve with
no property-name ambiguity.

asar.spec.ts: 170 failing itremote tests -> 0, import-line changes only.
2026-04-13 00:59:22 -07:00
Samuel Attard
65f2dee205 build: use 8-core Linux test runners for parallel vitest workers 2026-04-13 00:59:22 -07:00
Samuel Attard
c2304737f1 fix: correct defer() ordering and make runCleanupFunctions drain-safe
vitest's onTestFinished runs *after* afterEach (contrary to the earlier
assumption), so defer() callbacks saw windows already destroyed by
afterEach(closeAllWindows). And runCleanupFunctions didn't clear its
queue on throw, so one bad defer cascaded into every subsequent test.

- runCleanupFunctions now splices the queue before iterating and wraps
  each cleanup in try/catch, aggregating errors.
- closeAllWindows runs runCleanupFunctions() first, so the 190+ existing
  afterEach(closeAllWindows) call sites get mocha's ordering (defers run,
  then windows close) without modification. setup.ts's onTestFinished
  remains as a fallback for tests that defer() without closeAllWindows.
- cleanupWebContents guards against already-destroyed contents.
2026-04-13 00:59:21 -07:00
Samuel Attard
8e51023155 test: await closeWindow(c) in 'should attach child window to parent'
The unawaited async call let the child window linger into the next test's
afterEach assertNotWindows check.
2026-04-13 00:59:21 -07:00
Samuel Attard
7cdd1314d2 fix: drop unused assertNotWindows param from closeAllWindows
vitest passes a context object as the first arg to hook callbacks, so
afterEach(closeAllWindows) effectively called closeAllWindows(ctx) - a
truthy value that triggered the unused assert-no-windows-remain path,
producing unhandled rejections. afterAll(closeAllWindows) failed outright
with a FixtureParseError because vitest parses the param list.

No caller ever passed the argument explicitly, so the param is removed.
2026-04-13 00:59:21 -07:00
Samuel Attard
984f5c5023 test: isolate OS-singleton tests into spec/serial/ for post-parallel execution 2026-04-13 00:59:21 -07:00
Samuel Attard
abadff1643 test: deflake 'creates unique session id for each target'
Await each CDP step in sequence and use Runtime.evaluate on the attached
session after Debugger.enable is acknowledged, so Debugger.scriptParsed is
guaranteed to fire regardless of page-load timing. 0/10 flakes (was ~3/5).
2026-04-13 00:59:20 -07:00
Samuel Attard
6beb0f3de3 fix: use inline require() in stringified utility-process closure
The closure at spec/api-app.spec.ts:2282 is stringified and eval'd inside
a utility-process fixture. Under vite's SSR transform the captured import
bindings become __vite_ssr_import_N__ refs that don't exist in the fixture.
Inline require() calls survive toString() intact.
2026-04-13 00:59:20 -07:00
Samuel Attard
2e826cdaa4 fix: align runner app name, userData path, and decouple net-helpers from vitest
- spec/_vitest_runner/package.json: name/productName match spec/package.json
  so app.name assertions hold
- worker-entry: userData is <mkdtemp>/<app.name>/ so getPath('userData')
  still includes the app name
- net-helpers: import defer from defer-helpers and own listen() directly,
  so the utility-process fixture (which ts-node-requires net-helpers) no
  longer pulls in spec-helpers -> vitest
2026-04-13 00:59:20 -07:00
Samuel Attard
dc02d7907b fix: use regex alias for electron/* and split defer helpers from spec-helpers
vite's resolve.alias in object form matched the bare 'electron' specifier
but not 'electron/main' etc. The array-with-regex form matches all four.

Also extract defer/runCleanupFunctions into spec/lib/defer-helpers.ts so
setup.ts doesn't transitively import 'electron/main' (setupFiles go
through a slightly different SSR load path than test files).
2026-04-13 00:59:19 -07:00
Samuel Attard
3bb67099de chore: remove remaining mocha references
- refresh-page fixture served mocha.js as a 'large JS file' over a custom
  protocol; point it at chai.js instead since mocha is no longer a dep
- drop descriptive 'mocha' mentions from comments
2026-04-13 00:59:19 -07:00
Samuel Attard
6fea5e8691 build: switch spec runner from mocha to vitest 2026-04-13 00:59:19 -07:00
Samuel Attard
7a5dc9e725 chore: remove ELECTRON_FORCE_TEST_SUITE_EXIT mechanism
This was a win-arm64 workaround for the mocha runner: process.exit() could
hang in the single Electron process, so the suite SIGTERMed itself after
printing the failure count to stdout for script/spec-runner.js to scrape.

Under vitest, the process that decides and reports the exit code is the
plain-Node vitest CLI; Electron instances are pool workers that the pool
tears down. The workaround no longer applies.
2026-04-13 00:59:19 -07:00
Samuel Attard
2067a6b975 chore: port CI-only behaviour and defer() ordering from spec/index.js
- allowOnly: !CI matches mocha's forbidOnly
- retry: 3 on CI matches mocha's CI retries
- runCleanupFunctions via ctx.onTestFinished so defer()-ed cleanups run
  before test-file afterEach hooks, matching the per-suite hook ordering
  in spec/index.js
2026-04-13 00:59:18 -07:00
Samuel Attard
3dbd48441e chore: replace ifit/ifdescribe addOnly shim with vitest runIf 2026-04-13 00:59:18 -07:00
Samuel Attard
6eee70a432 chore: migrate webview-spec.ts to vitest 2026-04-13 00:59:18 -07:00
Samuel Attard
07cdd3543b chore: migrate visibility-state-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
e7d867952c chore: migrate version-bump-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
e4474b232e chore: migrate types-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
d39cbc204b chore: migrate spellchecker-spec.ts to vitest 2026-04-13 00:59:17 -07:00
Samuel Attard
b1f3befe21 chore: migrate security-warnings-spec.ts to vitest 2026-04-13 00:59:16 -07:00
Samuel Attard
6d99359799 chore: migrate release-notes-spec.ts to vitest 2026-04-13 00:59:16 -07:00
Samuel Attard
b5c9d0bdff chore: migrate process-binding-spec.ts to vitest 2026-04-13 00:59:16 -07:00
Samuel Attard
46464ace53 chore: migrate parse-features-string-spec.ts to vitest 2026-04-13 00:59:15 -07:00
Samuel Attard
46ce6bdc94 chore: migrate node-spec.ts to vitest 2026-04-13 00:59:15 -07:00
Samuel Attard
295deb50e0 chore: migrate modules-spec.ts to vitest 2026-04-13 00:59:15 -07:00
Samuel Attard
2e8b3fb5bf chore: migrate mas-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
4304cd88b6 chore: migrate logging-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
b93c6dd11a chore: migrate guest-window-manager-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
6c9447f2e6 chore: migrate fuses-spec.ts to vitest 2026-04-13 00:59:14 -07:00
Samuel Attard
1127cd3b6c chore: migrate extensions-spec.ts to vitest 2026-04-13 00:59:13 -07:00
Samuel Attard
0608bc93a0 chore: migrate esm-spec.ts to vitest 2026-04-13 00:59:13 -07:00
Samuel Attard
0a1e6119a6 chore: migrate drag-region-spec.ts to vitest 2026-04-13 00:59:13 -07:00
Samuel Attard
d69cee4a05 chore: migrate deprecate-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
9c6df31551 chore: migrate crash-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
178456f69e chore: migrate cpp-heap-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
8eac3fc601 chore: migrate chromium-spec.ts to vitest 2026-04-13 00:59:12 -07:00
Samuel Attard
971d4369eb chore: migrate autofill-spec.ts to vitest 2026-04-13 00:59:11 -07:00
Samuel Attard
8dc0354881 chore: migrate asar-spec.ts to vitest 2026-04-13 00:59:11 -07:00
Samuel Attard
4d6349eeca chore: migrate asar-integrity-spec.ts to vitest 2026-04-13 00:59:11 -07:00
Samuel Attard
24fc081b2f chore: migrate api-web-utils-spec.ts to vitest 2026-04-13 00:59:10 -07:00
Samuel Attard
1e0e9a41f3 chore: migrate api-web-request-spec.ts to vitest 2026-04-13 00:59:10 -07:00
Samuel Attard
d2a5c72b5f chore: migrate api-web-frame-spec.ts to vitest 2026-04-13 00:59:10 -07:00
Samuel Attard
2e7aaacbbe chore: migrate api-web-frame-main-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
0419a43b84 chore: migrate api-web-contents-view-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
0faef330c1 chore: migrate api-web-contents-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
25a4d53dcc chore: migrate api-view-spec.ts to vitest 2026-04-13 00:59:09 -07:00
Samuel Attard
644bc84146 chore: migrate api-utility-process-spec.ts to vitest 2026-04-13 00:59:08 -07:00
Samuel Attard
040fa8cb6a chore: migrate api-tray-spec.ts to vitest 2026-04-13 00:59:08 -07:00
Samuel Attard
6fb8c72b96 chore: migrate api-touch-bar-spec.ts to vitest 2026-04-13 00:59:08 -07:00
Samuel Attard
8881184167 chore: migrate api-system-preferences-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
c34e07fae5 chore: migrate api-subframe-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
8cdb30f0f0 chore: migrate api-shell-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
b58390fe1f chore: migrate api-shared-texture-spec.ts to vitest 2026-04-13 00:59:07 -07:00
Samuel Attard
86589f707f chore: migrate api-session-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
62a6b8118e chore: migrate api-service-workers-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
7e9d88bd74 chore: migrate api-service-worker-main-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
562330457e chore: migrate api-screen-spec.ts to vitest 2026-04-13 00:59:06 -07:00
Samuel Attard
913304c5bb chore: migrate api-safe-storage-spec.ts to vitest 2026-04-13 00:59:05 -07:00
Samuel Attard
85643d44e5 chore: migrate api-protocol-spec.ts to vitest 2026-04-13 00:59:05 -07:00
Samuel Attard
442cc4e005 chore: migrate api-process-spec.ts to vitest 2026-04-13 00:59:05 -07:00
Samuel Attard
6c8102697a chore: migrate api-power-save-blocker-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
b9b6285b5d chore: migrate api-power-monitor-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
1d2e635ab2 chore: migrate api-notification-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
b474e10633 chore: migrate api-notification-dbus-spec.ts to vitest 2026-04-13 00:59:04 -07:00
Samuel Attard
3a030f81c2 chore: migrate api-net-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
f8d0d2d599 chore: migrate api-net-session-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
220171a697 chore: migrate api-net-log-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
ad5dd349f4 chore: migrate api-net-custom-protocols-spec.ts to vitest 2026-04-13 00:59:03 -07:00
Samuel Attard
8799634083 chore: migrate api-native-theme-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
29ffaae7a1 chore: migrate api-native-image-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
1932404af2 chore: migrate api-menu-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
2c23564a7a chore: migrate api-menu-item-spec.ts to vitest 2026-04-13 00:59:02 -07:00
Samuel Attard
ad1b1d4b0b chore: migrate api-media-handler-spec.ts to vitest 2026-04-13 00:59:01 -07:00
Samuel Attard
e6c6e38b24 chore: migrate api-ipc-spec.ts to vitest 2026-04-13 00:59:01 -07:00
Samuel Attard
d8345f4054 chore: migrate api-ipc-renderer-spec.ts to vitest 2026-04-13 00:59:01 -07:00
Samuel Attard
f6e164dadf chore: migrate api-ipc-main-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
68883ece6f chore: migrate api-in-app-purchase-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
c9625bea13 chore: migrate api-image-view-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
c28e50d10f chore: migrate api-global-shortcut-spec.ts to vitest 2026-04-13 00:59:00 -07:00
Samuel Attard
f945aa5e39 chore: migrate api-dialog-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
046631a8fe chore: migrate api-desktop-capturer-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
f124d7f261 chore: migrate api-debugger-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
5d6ebd6e4a chore: migrate api-crash-reporter-spec.ts to vitest 2026-04-13 00:58:59 -07:00
Samuel Attard
827ad50c53 chore: migrate api-corner-smoothing-spec.ts to vitest 2026-04-13 00:58:58 -07:00
Samuel Attard
c46433d1cd chore: migrate api-context-bridge-spec.ts to vitest 2026-04-13 00:58:58 -07:00
Samuel Attard
515adf3399 chore: migrate api-content-tracing-spec.ts to vitest 2026-04-13 00:58:58 -07:00
Samuel Attard
8d7d778ab7 chore: migrate api-clipboard-spec.ts to vitest 2026-04-13 00:58:57 -07:00
Samuel Attard
93d9fe675d chore: migrate api-browser-window-spec.ts to vitest 2026-04-13 00:58:57 -07:00
Samuel Attard
d64680ff0d chore: migrate api-browser-view-spec.ts to vitest 2026-04-13 00:58:57 -07:00
Samuel Attard
689d8ddc11 chore: migrate api-autoupdater-msix-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
22b79ba2f9 chore: migrate api-autoupdater-darwin-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
02541f8606 chore: migrate api-auto-updater-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
034ac4fc04 chore: migrate api-app-spec.ts to vitest 2026-04-13 00:58:56 -07:00
Samuel Attard
d8fe4fd091 chore: prepare spec helpers and vitest setup for migration 2026-04-13 00:58:55 -07:00
Samuel Attard
7a446b0f84 build: add vitest runner infrastructure for Electron main-process tests
Adds spec/_vitest_runner/, a custom vitest pool that spawns each worker
as a full Electron main process (not a node-mode fork) so tests can use
real Electron APIs. Each worker gets a pool-owned mkdtemp userData dir
that is cleaned up on worker stop. File-level parallelism is enabled;
intra-file concurrency stays off to match current mocha behaviour.

Run with: yarn test:vitest
2026-04-13 00:58:55 -07:00
Kunal Dubey
04d9de6f73 fix: avoid window drag during corner resize in MAS build (#50637)
* fix: avoid window drag during corner resize in MAS build

* chore: update chromium patch offsets
2026-04-13 09:54:27 +02:00
Charles Kerr
b8dbe21b38 chore: do not patch fake_desktop_media_list.cc (#50953)
chore: do not patch files we do not use

do not patch fake_desktop_media_list.cc, .h
2026-04-13 09:27:32 +02:00
Samuel Attard
a57dbb55cc ci: split macos-x64 tests into 3 shards (#50968) 2026-04-13 09:26:19 +02:00
David Sanders
860a544534 ci: capture fatal errors in clang problem matcher (#50984) 2026-04-13 09:25:43 +02:00
David Sanders
e31cd64fe5 ci: ignore canceled jobs in audit (#50981)
* ci: ignore canceled jobs in audit

* chore: add another variation
2026-04-13 09:25:26 +02:00
Samuel Attard
10eb512d1d test: run oxfmt on simpleFullScreen test (#50990)
chore: fix oxfmt formatting in api-browser-window-spec

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 19:58:02 -07:00
Calvin
052efc9727 chore: add AI tool policy to CONTRIBUTING.md & update PR template (#50451)
* chore: update PR template and add AI tool policy to CONTRIBUTING.md

* sentencesmithing
2026-04-12 19:10:42 -05:00
Shelley Vohr
a007bafaf1 fix: simpleFullScreen exits when web content calls requestFullscreen (#50874)
fix: simpleFullScreen exits when web content calls requestFullscreen

SetHtmlApiFullscreen only checked IsFullscreen() to detect that the
window was already fullscreen, missing the simple-fullscreen case on
macOS. When web content triggered requestFullscreen the code fell
through to SetFullScreen(true) which toggled simple fullscreen off.

Include IsSimpleFullScreen() in the guard so the HTML-API fullscreen
state is updated without touching the window's fullscreen mode.
2026-04-12 19:06:07 -05:00
Robo
ea757689b3 refactor: rm chore_add_electron_objects_to_wrappablepointertag.patch (#50957) 2026-04-12 19:02:40 -05:00
Samuel Attard
2c94aac330 build: add oxfmt for JS/TS formatting and import sorting (#50692)
* build: add oxfmt for code formatting and import sorting

Adds oxfmt as a devDependency alongside oxlint and wires it into the
lint pipeline. The .oxfmtrc.json config matches Electron's current JS
style (single quotes, semicolons, 2-space indent, trailing commas off,
printWidth 100) and configures sortImports with custom groups that
mirror the import/order pathGroups previously enforced by ESLint:
@electron/internal, @electron/*, and {electron,electron/**} each get
their own ordered group ahead of external modules.

- `yarn lint:fmt` runs `oxfmt --check` over JS/TS sources and is
  chained into `yarn lint` so CI enforces it automatically.
- `yarn format` runs `oxfmt --write` for local fix-up.
- lint-staged invokes `oxfmt --write` on staged .js/.ts/.mjs/.cjs
  files before oxlint, so formatting is applied at commit time.

The next commit applies the formatter to the existing codebase so the
check actually passes.

* chore: apply oxfmt formatting to JS and TS sources

Runs `yarn format` across lib/, spec/, script/, build/, default_app/,
and npm/ to bring the codebase in line with the .oxfmtrc.json settings
added in the previous commit. This is a pure formatting pass: import
statements are sorted into the groups defined by the config, method
chains longer than printWidth are broken, single-quoted strings
containing apostrophes are switched to double quotes, and a handful of
single-statement `if` bodies are re-wrapped and get braces added by
`oxlint --fix` to satisfy the `curly: multi-line` rule.

No behavior changes.
2026-04-12 02:03:04 -07:00
Shelley Vohr
dcb844c201 chore: add Claude Code skill for Node.js upgrades (#50910)
Adds a new skill mirroring the Chromium upgrade skill, adapted for
Node.js rolls. Covers patch conflict resolution, build fix workflow,
commit guidelines, and documents high-churn patches and major version
upgrade patterns (V8 bridge patch deletions, BoringSSL complexity).
2026-04-11 20:11:49 -07:00
Charles Kerr
9bc55a255c chore: remove some unnecessary diffs in refactor_expose_file_system_access_blocklist.patch (#50909)
chore: remove some unnecessary diffs in refactor_expose_file_system_access_blocklist.patch
2026-04-11 16:48:47 -05:00
Samuel Attard
12b74eac26 fix: respect iframe sandbox flags for external protocol navigation (#50901) 2026-04-11 16:16:23 -04:00
Charles Kerr
6e7938af1d refactor: migrate api::Extensions to cppgc (#50932)
* refactor: migrate api::Extensions to cppgc

* chore: update patch indices
2026-04-12 01:59:54 +09:00
Charles Kerr
5fded05add fix: dangling raw_ptr api::Protocol::protocol_registry_ (#50829) 2026-04-11 08:53:53 -05:00
Charles Kerr
1879998865 refactor: migrate api::ServiceWorkerContext to cppgc (#50931)
refactor: migrate api::ServiceWorkerContext to cppgc
2026-04-11 18:56:31 +09:00
Samuel Attard
b1b02d9123 fix: restrict window.open features to allowlisted BrowserWindow options (#50902) 2026-04-11 03:43:55 -04:00
Samuel Attard
3f140e1b4b fix: clamp autofill popup bounds to the requesting frame viewport (#50903) 2026-04-11 03:43:43 -04:00
Samuel Attard
3ff923990d fix: validate OSR frame geometry against shared-memory mapping size (#50904) 2026-04-11 03:43:24 -04:00
Samuel Attard
b4e14a9004 fix: use ShowItemInFolder for devtools showItemInFolder embedder message (#50905) 2026-04-11 03:43:14 -04:00
Samuel Attard
61bb03ca75 fix: use audit token instead of PID for parent code-signature check (#50907) 2026-04-11 03:42:52 -04:00
Samuel Attard
1a2029c3a2 fix: apply IsSafeRedirectTarget to net module redirects (#50869) 2026-04-10 19:19:51 -07:00
Samuel Attard
bfa5c93332 refactor: attach translator holder via v8::Function data slot (#50867) 2026-04-10 19:19:34 -07:00
Samuel Attard
f36def6601 fix: scope extension tab-ID resolution to the calling BrowserContext (#50906) 2026-04-10 19:16:21 -07:00
David Sanders
97347c4223 chore: clean up clang-tidy warnings (#50862)
* chore: use emplace and use it correctly

* chore: redundant cast to the same type [google-readability-casting]

* chore: do not create objects with +new [google-objc-avoid-nsobject-new]

* chore: default arguments on virtual or override methods are prohibited [google-default-arguments]

* chore: warning: C-style casts are discouraged; use static_cast [google-readability-casting]

CFLocaleGetValue already returns CFTypeRef so that redundant static_cast was removed

* chore: refactor block to avoid use after move warning from clang-tidy

Looks like clang-tidy couldn't tell these were two mutually exclusive
branches so there was no actual issue, but refactoring is cleaner
anyway since it makes it more DRY.

* chore: C-style casts are discouraged; use static_cast [google-readability-casting]

No cast needed here, everything is already the correct type

* chore: C-style casts are discouraged; use static_cast/const_cast/reinterpret_cast [google-readability-casting]

* chore: use '= default' to define a trivial destructor [modernize-use-equals-default]

* chore: use range-based for loop instead [modernize-loop-convert]

* chore: redundant void argument list [modernize-redundant-void-arg]

* chore: address code review feedback

* chore: use auto

Co-authored-by: Charles Kerr <charles@charleskerr.com>

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-10 16:37:57 -07:00
Alexey
8d8847d478 feat: capture JS stack trace on renderer OOM (#50043)
* feat: capture JS stack trace on renderer OOM

When a renderer process approaches its V8 heap limit, capture the
JavaScript stack trace and write it to both a Crashpad crash key
("js-oom-stack") and stderr.

The stack trace is captured via RequestInterrupt rather than directly
inside the NearHeapLimitCallback because CurrentStackTrace is unsafe
to call during OOM — V8 FATALs on optimized (TurboFan) frames that
have had their deoptimization data garbage-collected. RequestInterrupt
defers the capture to the next V8 safe point, where all frames are
guaranteed to have deopt data available. This matches Node.js's
approach of never capturing JS stacks inside the heap limit callback.

The callback is registered once per isolate via an atomic guard in
RendererClientBase::DidCreateScriptContext, preventing the CHECK
failure V8 raises on duplicate AddNearHeapLimitCallback registrations
(which would otherwise occur on page navigations or multiple contexts).

Refs: #46078
Made-with: Cursor

* Update shell/renderer/oom_stack_trace.cc

Co-authored-by: Niklas Wenzel <dev@nikwen.de>

* Update shell/renderer/oom_stack_trace.cc

Co-authored-by: Niklas Wenzel <dev@nikwen.de>

* test: add crash reporter test for OOM JS stack trace

Add a test that verifies the `electron.v8-oom.stack` crash key contains
the JS stack trace (including function names) when a renderer process
runs out of memory. Also deduplicate the heap info formatting in
oom_stack_trace.cc.

Refs: #46078
Made-with: Cursor

* fix: lint formatting in oom_stack_trace.cc

Made-with: Cursor

* fix: use proper logger API instead of cstdio

* fix: check heap headroom before capturing OOM stack trace

deepak1556: "Should there be check for available heap size [for]
CurrentStackTrace and formatting"

CurrentStackTrace allocates StackTraceInfo + StackFrameInfo on the V8
heap. If the 20 MB bump is partially consumed by the time the interrupt
fires, these allocations trigger a secondary OOM. Guard with a 2 MB
headroom check.

Made-with: Cursor

* fix: handle V8 cage limit when bumping heap for OOM stack capture

deepak1556: "Does this bumping work when we are at the cage limit of
4GB"

V8's pointer compression cage caps the heap at ~4 GB. When
current_heap_limit is already near the ceiling, our 20 MB bump gets
clamped to zero and the interrupt never fires. Detect this and record
heap info as the final crash key instead of waiting for a stack trace
that won't arrive.

Made-with: Cursor

* feat: add V8 heap statistics as OOM crash keys

deepak1556: "V8 seems to capture heap stats as crash keys but it gets
missed today due to the OOM callback override... wonder if we can
include that to get some more heuristics in the dump."

Record heap used/total/limit/available, per-space stats for old_space
and large_object_space, native/detached context counts, and utilization
percentage as crash keys. Also add heap stats in the V8OOMErrorCallback
in node_bindings.cc for the final OOM crash report.

Made-with: Cursor

* feat: support worker thread isolates for OOM stack trace

deepak1556: "You need a separate registration for worker threads via
WorkerScriptReadyForEvaluationOnWorkerThread but that also means the
process global g_registered_isolate would break."

Chromium has one V8 isolate per thread (main + one per web worker), so
thread_local is equivalent to per-isolate storage. Replace the global
atomic + mutex/set with a constinit thread_local OomState* that holds
the isolate pointer and per-isolate is_in_oom flag. The void* data
parameter on AddNearHeapLimitCallback delivers OomState* directly into
callbacks, so the hot path needs no TLS lookup.

Add WorkerScriptReadyForEvaluationOnWorkerThread and
WillDestroyWorkerContextOnWorkerThread overrides to RendererClientBase
so both ElectronRendererClient and ElectronSandboxedRendererClient get
worker OOM registration. Update ElectronRendererClient to call the base
class in both worker lifecycle methods.

Add a web worker OOM test that spawns a dedicated Worker with a memory
leak and verifies the stack trace captures the worker function name.

Made-with: Cursor

* fix: register OOM callback for all script contexts

When context isolation is enabled, ShouldNotifyClient skips
DidCreateScriptContext for the main world, but user JS still runs there
and can OOM. Register in DidInstallConditionalFeatures which fires for
every script context. The TLS dedup guard prevents double-registration
on the same isolate.

Made-with: Cursor

* fix: guard against division by zero and cage size changes in OOM handler

Add a zero-guard on heap_size_limit before computing utilization
percentage — maximizes robustness in an OOM code path.

Add static_assert on kPtrComprCageReservationSize to catch any
upstream V8 change to the cage size at compile time.

Made-with: Cursor

* fix: address review feedback on OOM stack trace PR

- Remove redundant RegisterOomStackTraceCallback from
  electron_render_frame_observer.cc; DidCreateScriptContext is sufficient
  since main world and isolated world share the same isolate
- Replace thread_local OomState* with base::ThreadLocalOwnedPointer
  wrapped in base::NoDestructor per Chromium style for non-trivially
  destructible types
- Change heap-headroom and cage-limit logs from ERROR to INFO since
  users cannot act on these diagnostics
- Add comment explaining why base class is called last in
  WillDestroyWorkerContextOnWorkerThread (OOM deregistration ordering)

Made-with: Cursor

* fix: skip OOM stack trace registration for worklet contexts

Worklets can share a thread and isolate via WorkletThreadHolder's
per-process singleton pattern. With per-thread OOM state, the first
worklet to be destroyed would prematurely remove the callback for
any remaining worklets on the same thread. Skip worklets entirely
to avoid this; can be revisited with ref-counting if needed.

Made-with: Cursor

* fix: prevent dangling raw_ptr<v8::Isolate> in OOM state

The OomState held a raw_ptr<v8::Isolate> that outlived the isolate on
the main thread: gin::IsolateHolder destroyed the isolate during
shutdown, but the OomState (stored in thread-local storage) was only
released later in JavascriptEnvironment::~JavascriptEnvironment. This
triggers a dangling pointer check when building with
enable_dangling_raw_ptr_checks.

Register OomState as a gin::PerIsolateData::DisposeObserver so it
clears the raw_ptr and removes the NearHeapLimitCallback before the
isolate is destroyed, regardless of destructor ordering.

Suggested-by: Deepak Mohan
Made-with: Cursor

* test: verify OOM crash keys end-to-end via crash reporter

Replace stderr-based OOM tests with end-to-end crash dump validation.
Instead of parsing log output, start a crash reporter server, trigger
renderer OOM, and verify the uploaded crash dump contains the expected
`electron.v8-oom.*` annotations — the same code path production crash
reports take.

Consolidate all OOM test scenarios (basic heap leak, JSON.stringify,
web worker) into a single `describe('OOM crash keys')` block inside
api-crash-reporter-spec using the existing crash fixture app with new
renderer-oom-json and renderer-oom-worker crash types.

The web worker test verifies that OOM crash keys are present but does
not assert on the JS function name: the 20 MB heap bump may be
exhausted before V8 reaches a safe point to fire the stack-capture
interrupt, leaving the crash key at "(stack pending)". Increasing the
bump or switching to a synchronous capture strategy would fix this but
is left for a follow-up.

Remove the standalone oom-stack-trace-spec.ts and its fixture app.

Made-with: Cursor

---------

Co-authored-by: Niklas Wenzel <dev@nikwen.de>
2026-04-10 15:13:16 -04:00
Niklas Wenzel
b9825ba835 fix: preference initialization with app.setPath('sessionData') (#50891)
fix: preference initialization with app.setPath('sessionData')
2026-04-11 03:22:09 +09:00
Robo
9be03cbe54 fix: enable blink gc plugin (#50465)
chore: address blink gc plugin errors

Key fixes:
- Replace `base::WeakPtrFactory` with `gin::WeakCellFactory` in
  MenuMac, MenuViews, and NetLog, since weak pointers to cppgc-managed
  objects must go through weak cells
- Replace `v8::Global<v8::Value>` with `cppgc::Persistent<Menu>` for
  the menu reference in BaseWindow
- Stop using `gin_helper::Handle<T>` with cppgc types; use raw `T*`
  and add a `static_assert` to prevent future misuse
- Add proper `Trace()` overrides for Menu, MenuMac, MenuViews, and
  NetLog to ensure cppgc members are visited during garbage collection
- Replace `SelfKeepAlive` prevent-GC mechanism in Menu with a
  `cppgc::Persistent` prevent-GC captured in `BindSelfToClosure`
- Introduce `GC_PLUGIN_IGNORE` macro to suppress
  known-safe violations: mojo::Remote fields, ObjC bridging pointers,
  and intentional persistent self-references
- Mark `ArgumentHolder` as `CPPGC_STACK_ALLOCATED()` in both Electron's
  and gin's function_template.h to silence raw-pointer-to-GC-type
  warnings
2026-04-11 03:21:39 +09:00
Michaela Laurencin
861ef95598 fix: remove decorateURL from default_app (#50852)
remove decorateURL from default_app

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-10 13:18:26 -05:00
Jan Hannemann
20ed34a2fd feat: add id, groupId, and groupTitle support for Windows notifications (#50328)
* feat: allow to set id and groupId

* feat: use Id's without hash but check length

* feat: adds visual grouping via groupTitle

* test: tests added for id, groupId and groupTitle

* fix: unused vars on Mac and Linux

* fix: remove redundant parameter

* fix: add doc links for id and group

* fix: throw if groupId is missing

* fix: test
2026-04-10 11:01:57 -07:00
David Sanders
82aa603698 build: don't use //third_party/depot_tools in gn build scripts (#50858) 2026-04-10 13:43:25 -04:00
Charles Kerr
b4f725a763 test: guard permission handlers in File System API tests (#50890)
fix: guard permission handlers in File System API tests

Manual port of #50865 for `main` due to code shear.

1. Chromium can fire unrelated permission checks (e.g. 'background-sync')
on the default session. Copy a safeguard `permission === 'fileSystem'` from
"calls twice when trying to query a read/write file handle permissions".

2. add afterEach cleanup, reset setPermissionCheckHandler(null).
2026-04-10 18:43:04 +02:00
Robo
05f1cb553d fix: shutdown crash when unregistering power notification on windows (#50878)
* fix: crash on shutdown when unregistering power notification

Refs https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregistersuspendresumenotification
the handle should be the return value of registeration api
not the window handle.

* chore: remove redundant selfkeepalive

Followup to 7c0cb61b3c

* chore: fix lint

* chore: address review feedback
2026-04-11 00:47:05 +09:00
Robo
c0f187f90d feat: capture Node.js trace categories via Perfetto (#50591)
* feat: capture Node.js trace categories via Perfetto

* fix: crash in ELECTRON_RUN_AS_NODE

* chore: cleanup macro usage

* chore: update patches
2026-04-10 08:12:46 -04:00
Robo
5f820b7f69 test: add cppgc backed menu leak regression test (#50879)
* spec: add menu leak regression test

* spec: reduce menu count to remove CI flakiness
2026-04-10 20:48:28 +09:00
Calvin
8b7e7de208 fix: return numeric blksize and blocks from asar fs.stat (#50825)
fix: return numeric `blksize` and `blocks` from asar `fs.stat`

Previously, `fs.stat` on files inside `.asar` archives returned
`undefined` for `blksize` and `blocks`, violating the Node.js API
contract where these fields must be `number | bigint`.

Use `4096` for `blksize` (matching the convention used by `memfs` and
the proposed `node:vfs` module in nodejs/node#61478) and compute
`blocks` as `ceil(size / 512)` (standard 512-byte block units).

Fixes #42686

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 10:19:09 +02:00
Shelley Vohr
b59f573097 fix: pass root_gen_dir from GN to generate_node_headers.py (#50847)
fix: pass root_gen_dir from GN to generate_node_headers.py

PR #50828 replaced a local get_out_dir() (defaulting to 'Testing') with
the shared one from script/lib/util.py (defaulting to 'Default').
Neither default is correct because the actual output directory depends
on the active build config. Pass $root_gen_dir from the GN action so
the script always uses the correct path.
2026-04-10 09:41:45 +02:00
Charles Kerr
b417696d6b refactor: migrate electron::api::Protocol to cppgc (#50857)
refactor: migrate api::Protocol to cppgc
2026-04-10 15:58:33 +09:00
Mitchell Cohen
4203d7688f fix: external resize hit targets for frameless windows on Windows (#50706) 2026-04-09 18:13:13 -05:00
Zeenat Lawal
62e637275a fix: move Electron help menu links to default app only (#50629)
* fix: remove Electron links from default help menu

* fix: remove help menu entirely from default menu

* fix: move Electron help menu links to default app

* docs: update default menu items list in menu.md
2026-04-09 12:14:22 -07:00
Shelley Vohr
28c0eb29df fix: webContents.print() ignoring mediaSize when silent (#50808)
fix: webContents.print() ignoring mediaSize when silent

PR #49523 moved the default media size fallback into OnGetDeviceNameToUse,
but the new code unconditionally writes kSettingMediaSize — clobbering
any mediaSize the caller had already set in WebContents::Print() from
options.mediaSize / pageSize. As a result, silent prints with an
explicit pageSize (e.g. "Letter") fell back to A4 with tiny content.

Only populate the default/printer media size when the caller hasn't
already supplied one, preserving the precedence:
  1. user-supplied mediaSize / pageSize
  2. printer default (when usePrinterDefaultPageSize is true)
  3. A4 fallback
2026-04-09 12:16:40 -05:00
Charles Kerr
8a730e2aec fix: remove dangling raw_ptr api::WebContents::zoom_controller_ (#50812)
fix: remove dangling raw_ptr api::WebContents::zoom_controller_
2026-04-09 12:16:17 -05:00
Shelley Vohr
044be7ce40 fix: avoid crash in window.print() when prefilling native print dialog (#50843)
fix: avoid crash in window.print() when prefilling native print dialog

When UpdatePrinterSettings() fails (e.g. the printer rejects the
requested resolution), OnError() nullifies print_info_ via
ReleaseContext(). The return value was not checked, so
AskUserForSettings() passed nil to [NSPrintPanel runModalWithPrintInfo:],
crashing in PJCSessionHasApplicationSetPrinter with a null PMPrintSession.

Check the return value and fall back to UseDefaultSettings() on failure
so the dialog opens with defaults instead of crashing.
2026-04-09 13:14:36 -04:00
Shelley Vohr
7245c6a3f0 ci: re-check signed commits on every PR synchronize (#50811)
The needs-signed-commits label was previously added by the lightweight
synchronize workflow but only removed by a job in build.yml gated on
`gha-done`, which requires every macOS/Linux/Windows build to finish
green. That made label removal both slow (waits on the full pipeline)
and fragile (any unrelated build failure leaves the label pinned even
after commits are properly signed).

Drop the `if` guard on the synchronize job so it re-evaluates signing
on every push, and add a removal step that runs on success when the
label is present. Force-pushing signed commits now clears the label as
soon as the check completes, with no dependency on the build pipeline.
2026-04-09 11:02:01 -04:00
Charles Kerr
b484b0bde9 fix: fix inset and stop using gfx::ToFlooredRectDeprecated() (#50809)
fix: fix inset and stop using ToFlooredRectDeprecated()
2026-04-09 09:55:11 -05:00
Charles Kerr
6c8a910232 refactor: remove unnecessary raw_ptr SavePageHandler::web_contents_ (#50810)
refactor: remove unnecessary field raw_ptr<content::WebContents> SavePageHandler::web_contents_
2026-04-09 09:54:44 -05:00
Noah Gregory
cc3d4f5f58 fix: PDF support when site isolation trials disabled (#50689)
* fix: use proper OOPIF PDF check in `StreamsPrivateAPI`

* fix: add `ShouldEnableSubframeZoom` override to `ElectronBrowserClient` for upstream parity

* fix: add `MaybeOverrideLocalURLCrossOriginEmbedderPolicy` override to `ElectronBrowserClient` for upstream parity

* fix: add `DoesSiteRequireDedicatedProcess` override to `ElectronBrowserClient` for upstream parity

* style: move `DoesSiteRequireDedicatedProcess` to correct override section
2026-04-09 15:35:26 +02:00
Shelley Vohr
b711ce7b04 chore: remove window enlargement revert patch (#50612)
* chore: remove window enlargement revert patch

Chromium removed the `window_enlargement_` system from
DesktopWindowTreeHostWin (1771dbae), which was a workaround for an AMD
driver bug from 2013 (crbug.com/286609) where translucent HWNDs smaller
than 64x64 caused graphical glitches. Chromium confirmed this is no
longer needed and shipped the removal.

This removes the revert patch and all Electron-side code that depended
on the `kEnableTransparentHwndEnlargement` feature flag, including the
`GetExpandedWindowSize` helper and max size constraint expansion in
`NativeWindow::GetContentMaximumSize`.

* test: remove obsolete <64x64 transparent window test

The test was added in 2018 (#12904) to verify the AMD driver
workaround that artificially enlarged translucent HWNDs smaller than
64x64 (crbug.com/286609). The workaround set the real HWND to 64x64
and subtracted a stored window_enlargement_ from every client/window
bounds query, so getContentSize() reported the originally-requested
size even though the actual HWND was larger.

With both the Chromium window_enlargement_ system and Electron's
GetExpandedWindowSize gone, setContentSize on a transparent
thickFrame window calls SetWindowPos directly. WS_THICKFRAME windows
are subject to DefWindowProc's MINMAXINFO.ptMinTrackSize clamp on
programmatic resizes (Chromium's OnGetMinMaxInfo ends with
SetMsgHandled(FALSE), so DefWindowProc overwrites the zeroed
min-track with system defaults), which on Windows Server 2025
floors at 32x39 — hence the failing [32, 39] vs [30, 30].

The removed feature_list.cc comment explicitly flagged this test as
the blocker for retiring kEnableTransparentHwndEnlargement, so
delete it alongside the workaround it was validating.
2026-04-09 15:34:10 +02:00
Alexey
adf9a6e303 fix: restore std::deque for dynamic crash key storage (#50795)
#47171 migrated `std::deque` to `base::circular_deque` in
`shell/common/crash_keys.cc`. However, `CrashKeyString` wraps a
`crashpad::Annotation` that holds self-referential pointers and
registers itself in a process-global linked list. `circular_deque`
relocates elements on growth (via `VectorBuffer::MoveConstructRange`),
leaving those pointers dangling — causing missing crash keys or a hung
crashpad handler (especially on macOS). The `base/containers/README.md`
warns: "Since `base::deque` does not have stable iterators and it will
move the objects it contains, it may not be appropriate for all uses."

Reverts to `std::deque`, whose block-based layout never relocates
existing elements. Adds a regression test that registers 50 dynamic
crash keys and verifies they all survive a renderer crash.

Notes: Fixed crash keys being lost and the crash reporter hanging on
macOS when many dynamic crash keys were registered.

Made-with: Cursor
2026-04-09 10:50:32 +02:00
Calvin
6744293e96 fix: account for extraSize in aspect ratio min/max clamping on macOS (#50794)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 10:50:17 +02:00
dependabot[bot]
0d3342debf build(deps-dev): bump @xmldom/xmldom from 0.8.11 to 0.8.12 in the npm_and_yarn group across 1 directory (#50824)
build(deps-dev): bump @xmldom/xmldom

Bumps the npm_and_yarn group with 1 update in the / directory: [@xmldom/xmldom](https://github.com/xmldom/xmldom).


Updates `@xmldom/xmldom` from 0.8.11 to 0.8.12
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.8.11...0.8.12)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-version: 0.8.12
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 09:54:13 +02:00
Calvin
157cdac4b9 test: use shared get_out_dir() in generate_node_headers.py (#50828)
The local get_out_dir() defaulted to 'Testing' instead of 'Default',
causing e test to fail when using a non-Testing build config. Replace
it with the canonical version from script/lib/util.py.
2026-04-09 09:52:14 +02:00
Shelley Vohr
4dfada86ce fix: menu items not cleaned up after rebuild (#50806)
Menu was holding a SelfKeepAlive to itself from construction, so any
Menu that was never opened (e.g. an application menu replaced before
being shown) stayed pinned in cppgc forever. Repeated calls to
Menu.setApplicationMenu leaked every prior Menu along with its model
and items.

Restore the original Pin/Unpin lifecycle: start keep_alive_ empty and
only assign `this` in OnMenuWillShow. OnMenuWillClose already clears
it.
2026-04-09 11:56:39 +09:00
Kanishk Ranjan
df81a1d4ac test: add desktopCapturer icon validation (#50261)
* chore: testing of desktopCapturer can run on arm

* fix: DesktopMediaListCaptureThread crash

Fixed a crash when Windows calls ::CoCreateInstance() in the
DesktopMediaListCaptureThread before COM is initialized.

* test: added test for desktopCapturer fetchWindowIcons

* chore: updating Chromium patch hash

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-08 14:56:27 -04:00
Shelley Vohr
c3e3958668 fix: devtools re-attaches on open when previously detached (#50807)
PR #50646 added a dock state allowlist in SetDockState() that collapsed any
non-matching value to "right". WebContents::OpenDevTools passes an empty
string when no `mode` option is given, which is the sentinel LoadCompleted()
uses to restore `currentDockState` from prefs. The allowlist clobbered that
sentinel to "right", so previously-undocked devtools would flash detached
and then snap back to the right dock.

Preserve the empty string through SetDockState() so the pref-restore path
runs; still reject any non-empty invalid value to keep the JS-injection
guard from #50646 intact.
2026-04-08 13:36:47 -04:00
electron-roller[bot]
afd5fb4a60 chore: bump chromium to 148.0.7778.0 (main) (#50769)
* chore: bump chromium in DEPS to 148.0.7776.0

* chore: bump chromium in DEPS to 148.0.7778.0

* fix(patch): buffered_data_source_host_impl include added upstream

Ref: https://chromium-review.googlesource.com/c/chromium/src/+/7712714

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* fix(patch): ASan process info callback added upstream

Ref: https://chromium-review.googlesource.com/c/chromium/src/+/7724018

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* fix(patch): ServiceProcessHost per-instance observer migration

Ref: https://chromium-review.googlesource.com/c/chromium/src/+/7700794

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* fix(patch): FSA BlockPath factory method refactor

Upstream refactored BlockPath initialization to use factory methods
(CreateRelative, CreateAbsolute, CreateSuffix) and a switch statement.
Updated the exposed code in the header to match.

Ref: https://chromium-review.googlesource.com/c/chromium/src/+/7665590

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* fix(patch): service process tracker per-instance observer refactor

Ref: https://chromium-review.googlesource.com/c/chromium/src/+/7700794

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* chore: update patches (trivial only)

* 7723958: Rename blink::WebString::FromUTF16() to FromUtf16()

Ref: https://chromium-review.googlesource.com/c/chromium/src/+/7723958

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* fixup! fix(patch): ASan process info callback added upstream

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <samuelmaddock@electronjs.org>
Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
2026-04-08 13:34:24 -04:00
Charles Kerr
8679522922 chore: iwyu commonly-included headers in shell/ (#50778)
* chore: iwyu in shell/browser/api/electron_api_web_contents.h

* chore: iwyu in shell/browser/browser.h

* chore: iwyu in shell/browser/javascript_environment.h

* chore: iwyu in shell/common/gin_hhelper/function_template.h

* chore: do not include node_includes.h if we are not using it

* chore: fix transitive include
2026-04-08 09:33:42 -05:00
David Sanders
0828de3ccd ci: include .obj checksums when calculating object change rate (#50772) 2026-04-08 14:57:40 +02:00
Michaela Laurencin
6b5a4ff66c ci: allow ai-pr label without comment (#50792) 2026-04-08 13:09:23 +02:00
Charles Kerr
ca28023d4d chore: remove unused enum classes (#50782)
chore: remove unused FileSystemAccessPermissionContext::Access enum class

chore: remove unused FileSystemAccessPermissionContext::RequestType enum class

declared in 344aba08 but never used
2026-04-08 09:41:38 +02:00
Samuel Attard
e60441ad60 build: update build-tools to latest (#50786) 2026-04-08 09:31:12 +02:00
Charles Kerr
a189425373 fix: dangling raw_ptr api::Session::browser_context_ (#50784)
* fix: dangling raw_ptr api::Session::browser_context_

* fix: address code review feedback
2026-04-08 15:04:29 +09:00
Charles Kerr
7eccea1315 refactor: remove use of deprecated class base::MemoryPressureListener (#50763) 2026-04-07 20:11:02 -05:00
558 changed files with 25592 additions and 18252 deletions

View File

@@ -0,0 +1,205 @@
---
name: electron-node-upgrade
description: Guide for performing Node.js version upgrades in the Electron project. Use when working on the roller/node/main branch to fix patch conflicts during `e sync --3`. Covers the patch application workflow, conflict resolution, analyzing upstream Node.js changes, and proper commit formatting for patch fixes.
---
# Electron Node.js Upgrade: Phase One
## Summary
Run `e sync --3` repeatedly, fixing patch conflicts as they arise, until it succeeds. Then export patches and commit changes atomically.
## Success Criteria
Phase One is complete when:
- `e sync --3` exits with code 0 (no patch failures)
- All changes are committed per the commit guidelines
Do not stop until these criteria are met.
**CRITICAL** Do not delete or skip patches unless 100% certain the patch is no longer needed. For major version upgrades, patches that shim deprecated V8 APIs or backport upstream changes are often deletable because the new Node.js version already incorporates them — but verify before removing. Complicated conflicts or hard to resolve issues should be presented to the user after you have exhausted all other options. Do not delete the patch just because you can't solve it.
**CRITICAL** Never use `git am --skip` and then manually recreate a patch by making a new commit. This destroys the original patch's authorship, commit message, and position in the series. If `git am --continue` reports "No changes", investigate why — the changes were likely absorbed by a prior conflict resolution's 3-way merge. Present this situation to the user rather than skipping and recreating.
## Context
The `roller/node/main` branch is created by automation to update Electron's Node.js dependency version in `DEPS`. No work has been done to handle breaking changes between the old and new versions.
There are two types of Node.js version updates:
- **Bumps** (patch/minor): Automated by `electron-roller[bot]` with commit title `chore: bump node to v{version}`. Trivial patch index updates are handled automatically by `patchup[bot]`. These often land cleanly, but may require manual patch fixes.
- **Major upgrades** (e.g., v22 → v24): Manual, large PRs with commit title `chore: upgrade Node.js to v{X}.{Y}.{Z}`. These typically involve deleting obsolete patches, adapting many others, and updating `@types/node` in `package.json`.
**Key directories:**
- Current directory: Electron repo (always run `e` commands here)
- `../third_party/electron_node`: Node.js repo (where patches apply)
- `patches/node/`: Patch files for Node.js
- `docs/development/patches.md`: Patch system documentation
## Pre-flight Checks
Run these once at the start of each upgrade session:
1. **Clear rerere cache** (if enabled): `git rerere clear` in both the electron and `../third_party/electron_node` repos. Stale recorded resolutions from a prior attempt can silently apply wrong merges.
2. **Ensure pre-commit hooks are installed**: Check that `.git/hooks/pre-commit` exists. If not, run `yarn husky` to install it. The hook runs `lint-staged` which handles clang-format for C++ files.
## Workflow
1. Run `e sync --3` (the `--3` flag enables 3-way merge, always required)
2. If succeeds → skip to step 5
3. If patch fails:
- Identify target repo and patch from error output
- Analyze failure (see references/patch-analysis.md)
- Fix conflict in `../third_party/electron_node` working directory
- Run `git am --continue` in `../third_party/electron_node`
- Repeat until all patches for that repo apply
- IMPORTANT: Once `git am --continue` succeeds you MUST run `e patches node` to export fixes
- Return to step 1
4. When `e sync --3` succeeds, run `e patches all`
5. **Read `references/phase-one-commit-guidelines.md` NOW**, then commit changes following those instructions exactly.
## Commands Reference
| Command | Purpose |
|---------|---------|
| `e sync --3` | Clone deps and apply patches with 3-way merge |
| `git am --continue` | Continue after resolving conflict (run in node repo) |
| `e patches node` | Export commits from node repo to patch files |
| `e patches all` | Export all patches from all targets |
| `e patches node --commit-updates` | Export patches and auto-commit trivial changes |
| `e patches --list-targets` | List targets and config paths |
## Patch System Mental Model
```
patches/node/*.patch → [e sync --3] → ../third_party/electron_node commits
← [e patches] ←
```
## When to Edit Patches
| Situation | Action |
|-----------|--------|
| During active `git am` conflict | Fix in node repo, then `git am --continue` |
| Modifying patch outside conflict | Edit `.patch` file directly |
| Creating new patch (rare, avoid) | Commit in node repo, then `e patches node` |
Fix existing patches 99% of the time rather than creating new ones.
## Patch Fixing Rules
1. **Preserve authorship**: Keep original author in TODO comments (from patch `From:` field)
2. **Never change TODO assignees**: `TODO(name)` must retain original name
3. **Update descriptions**: If upstream changed APIs or macros, update patch commit message to reflect current state
4. **Never skip-and-recreate a patch**: If `git am --continue` says "No changes — did you forget to use 'git add'?", do NOT run `git am --skip` and create a replacement commit. The patch's changes were already absorbed by a prior 3-way merge resolution. This means an earlier conflict resolution pulled in too many changes. Present the situation to the user for guidance — the correct fix may require re-doing an earlier resolution more carefully to keep each patch's changes separate.
# Electron Node.js Upgrade: Phase Two
## Summary
Run `e build -k 999 -- --quiet` repeatedly, fixing build issues as they arise, until it succeeds. Then run `e start --version` to validate Electron launches and commit changes atomically.
Run Phase Two immediately after Phase One is complete.
## Success Criteria
Phase Two is complete when:
- `e build -k 999 -- --quiet` exits with code 0 (no build failures)
- `e start --version` has been run to check Electron launches
- All changes are committed per the commit guidelines
Do not stop until these criteria are met. Do not delete code or features, never comment out code in order to take short cut. Make all existing code, logic and intention work.
## Context
The `roller/node/main` branch is created by automation to update Electron's Node.js dependency version in `DEPS`. No work has been done to handle breaking changes between the old and new versions. Node.js APIs (especially internal V8 integration, OpenSSL/BoringSSL compatibility, and build system files) frequently change between versions. In every case the code in Electron must be updated to account for the change in Node.js, strongly avoid making changes to the code in Node.js to fix Electron's build.
**Key directories:**
- Current directory: Electron repo (always run `e` commands here)
- `../third_party/electron_node`: Node.js repo (do not touch this code to fix build issues, just read it to obtain context)
## Workflow
1. Run `e build -k 999 -- --quiet` (the `--quiet` flag suppresses per-target status lines, showing only errors and the final result)
2. If succeeds → skip to step 6
3. If build fails:
- Identify underlying file in "electron" from the compilation error message
- Analyze failure
- Fix build issue by adapting Electron's code for the change in Node.js
- Run `e build -t {target_that_failed}.o` to build just the failed target we were specifically fixing
- You can identify the target_that_failed from the failure line in the build log. E.g. `FAILED: 2e506007-8d5d-4f38-bdd1-b5cd77999a77 "./obj/electron/shell/browser/api/electron_api_utility_process.o" CXX obj/electron/shell/browser/api/electron_api_utility_process.o` the target name is `obj/electron/shell/browser/api/electron_api_utility_process.o`
- **Read `references/phase-two-commit-guidelines.md` NOW**, then commit changes following those instructions exactly.
- Return to step 1
4. **CRITICAL**: After ANY commit (especially patch commits), immediately run `git status` in the electron repo
- Look for other modified `.patch` files that only have index/hunk header changes
- These are dependent patches affected by your fix
- Commit them immediately with: `git commit -am "chore: update patches (trivial only)"`
5. Return to step 1
6. When `e build` succeeds, run `e start --version`
7. Check if you have any pending changes in the Node.js repo by running `git status` in `../third_party/electron_node`
- If you have changes follow the instructions below in "A. Patch Fixes" to correctly commit those modifications into the appropriate patch file
## Commands Reference
| Command | Purpose |
|---------|---------|
| `e build -k 999 -- --quiet` | Build Electron, continue on errors, suppress status lines |
| `e build -t {target}.o` | Build just one specific target to verify a fix |
| `e start --version` | Validate Electron launches after successful build |
## Two Types of Build Fixes
### A. Patch Fixes (for files in patched Node.js files)
When the error is in a file that Electron patches (check with `grep -l "filename" patches/node/*.patch`):
1. Edit the file in the Node.js source tree (`../third_party/electron_node/...`)
2. Create a fixup commit targeting the original patch commit:
```bash
cd ../third_party/electron_node
git add <modified-file>
git commit --fixup=<original-patch-commit-hash>
GIT_SEQUENCE_EDITOR=: git rebase --autosquash --autostash -i <commit>^
```
3. Export the updated patch: `e patches node`
4. Commit the updated patch file following `references/phase-one-commit-guidelines.md`.
To find the original patch commit to fixup: `git log --oneline | grep -i "keyword from patch name"`
The base commit for rebase is the Node.js commit before patches were applied. Find it by checking the `refs/patches/upstream-head` ref.
### B. Electron Code Fixes (for files in shell/, electron/, etc.)
When the error is in Electron's own source code:
1. Edit files directly in the electron repo
2. Commit directly (no patch export needed)
# Critical: Read Before Committing
- Before ANY Phase One commits: Read `references/phase-one-commit-guidelines.md`
- Before ANY Phase Two commits: Read `references/phase-two-commit-guidelines.md`
# High-Churn Patches
These patches consistently require the most work during Node.js upgrades:
- **`fix_handle_boringssl_and_openssl_incompatibilities.patch`** — Electron uses BoringSSL (via Chromium) while Node.js expects OpenSSL. This patch is large and complex, and upstream OpenSSL API changes frequently break it.
- **`fix_crypto_tests_to_run_with_bssl.patch`** — Companion to the above; adapts Node.js crypto tests for BoringSSL. Can grow significantly during major upgrades.
- **`support_v8_sandboxed_pointers.patch`** — V8 sandbox pointer support requires careful adaptation when V8 APIs change.
- **`build_add_gn_build_files.patch`** — The GN build file patch is large and touches many build targets. Upstream build system changes frequently conflict.
# Major Version Upgrades
Major Node.js version transitions (e.g., v22 → v24) are significantly more involved than patch bumps:
1. **Expect patch deletions.** Electron uses Chromium's V8, which is often ahead of the V8 version bundled in Node.js. Many patches exist to bridge this gap — shimming newer V8 APIs that Chromium's V8 has but Node.js' older V8 doesn't. When Node.js bumps to a newer major version, its V8 catches up to Chromium's, and those bridge patches can be deleted. In the v22 → v24 upgrade, 17 patches were deleted for this reason.
2. **Update `@types/node`** in `package.json` to match the new major version.
3. **Post-upgrade regressions are expected.** Even after the upgrade lands, follow-up fix PRs for edge cases (ESM path handling, certificate loading, platform-specific issues) are normal.
# Skill Directory Structure
This skill has additional reference files in `references/`:
- patch-analysis.md - How to analyze patch failures
- phase-one-commit-guidelines.md - Commit format for Phase One
- phase-two-commit-guidelines.md - Commit format for Phase Two
Read these when referenced in the workflow steps.

View File

@@ -0,0 +1,112 @@
# Analyzing Patch Failures
## Investigation Steps
1. **Read the patch file** at `patches/node/{patch_name}.patch`
2. **Examine current state** of the file in the Node.js repo at mentioned line numbers
3. **Check recent upstream changes:**
```bash
cd ../third_party/electron_node
git log --oneline -10 -- {file}
```
4. **Find Node.js PR** in commit messages:
```
PR-URL: https://github.com/nodejs/node/pull/{PR_NUMBER}
```
## Critical: Resolve by Intent, Not by Mechanical Merge
When resolving a patch conflict, do NOT blindly preserve the patch's old code. Instead:
1. **Understand the upstream commit's full scope** — not just the conflicting hunk.
Run `git show <commit> --stat` and read diffs for all affected files.
Upstream may have removed structs, members, or methods that the patch
references in other hunks or files.
2. **Re-read the patch commit message** to understand its *intent* — what
behavior does it need to preserve or add?
3. **Implement the intent against the new upstream code.** If the patch's
purpose is "add BoringSSL compatibility", add only the compatibility
layer — don't also restore old code that upstream separately removed.
### Lesson: Upstream Removals Break Patch References
- **Trigger:** Patch conflict involves an upstream refactor (not just context drift)
- **Strategy:** After identifying the upstream commit, check its full diff for
removed types, members, and methods. If the patch's old code references
something removed, the resolution must use the new upstream mechanism.
### Lesson: Separate Patch Purpose from Patch Implementation
- **Trigger:** Conflict between "upstream simplified code" vs "patch has older code"
- **Strategy:** Identify the *minimal* change the patch needs. If the patch
wraps code in a conditional, only add the conditional — don't restore old
code that was inside the conditional but was separately cleaned up upstream.
### Lesson: Finish the Adaptation at Conflict Time
- **Trigger:** A patch conflict involves an upstream API removal or replacement
- **Strategy:** When resolving the conflict, fully adapt the patch to use the
new API in the same commit. Don't remove the old code and leave behind stale
references that will "be fixed in Phase Two." Each patch fix commit should be
a complete resolution.
## Common Failure Patterns
| Pattern | Cause | Solution |
|---------|-------|----------|
| Context lines don't match | Surrounding code changed | Update context in patch |
| File not found | File renamed/moved | Update patch target path |
| Function not found | Refactored upstream | Find new function name |
| OpenSSL → BoringSSL mismatch | Crypto API change | Update to BoringSSL-compatible API |
| GYP/GN build change | Build system refactor | Adapt build patch to new structure |
| Deleted code | Feature removed | Verify patch still needed |
| V8 API bridge patch conflicts | Node.js caught up to Chromium's V8 | Patch may be deletable — verify the API is now in Node.js' V8 natively |
## Using Git Blame
To find the commit that changed specific lines:
```bash
cd ../third_party/electron_node
git blame -L {start},{end} -- {file}
git log -1 {commit_sha} # Look for PR-URL: line
```
## Verifying Patch Necessity
Before deleting a patch, verify:
1. The patched functionality was intentionally removed upstream
2. Electron doesn't need the patch for other reasons
3. No other code depends on the patched behavior
**V8 bridge patches:** Electron uses Chromium's V8, which is often ahead of the V8 bundled in Node.js. Many patches exist to bridge this version gap — adapting Node.js code to work with newer V8 APIs that Chromium's V8 exposes. During major Node.js upgrades, Node.js' V8 catches up to Chromium's, and these bridge patches often become unnecessary. Check whether the API the patch shims is now available natively in the new Node.js version's V8.
When in doubt, keep the patch and adapt it.
## Phase Two: Build-Time Patch Issues
Sometimes patches that applied successfully in Phase One cause build errors in Phase Two. This can happen when:
1. **Incomplete types**: A patch disables a header include, but new upstream code uses the type
2. **Missing members**: A patch modifies a class, but upstream added new code referencing the original
### Finding Which Patch Affects a File
```bash
grep -l "filename.cc" patches/node/*.patch
```
### Matching Existing Patch Patterns
When fixing build errors in patched files, examine the existing patch to understand its style:
- Does it use `#if 0` / `#endif` guards?
- Does it use `#if BUILDFLAG(...)` conditionals?
- Does it use `#ifndef` / `#ifdef` guards for BoringSSL vs OpenSSL?
- What's the pattern for disabled functionality?
Apply fixes consistent with the existing patch style.

View File

@@ -0,0 +1,111 @@
# Phase One Commit Guidelines
Only follow these instructions if there are uncommitted changes to `patches/` after Phase One succeeds.
Ignore other instructions about making commit messages, our guidelines are CRITICALLY IMPORTANT and must be followed.
## Each Commit Must Be Complete
When resolving a patch conflict, fully adapt the patch to the new upstream code in the same commit. If the upstream change removes an API the patch uses, update the patch to use the replacement API now — don't leave stale references knowing they'll need fixing later. The goal is that each commit represents a finished resolution, not a partial one that defers known work to a future phase.
## Commit Message Style
**Titles** follow the 60/80-character guideline: simple changes fit within 60 characters, otherwise the limit is 80 characters.
Always include a `Co-Authored-By` trailer identifying the AI model that assisted (e.g., `Co-Authored-By: <AI model attribution>`).
### Patch conflict fixes
Use `fix(patch):` prefix. The title should name the upstream change, not your response to it:
```
fix(patch): {topic headline}
Ref: {Node.js commit or issue link}
Co-Authored-By: <AI model attribution>
```
Only add a description body if it provides clarity beyond the title. For straightforward context drift or simple API renames, the title + Ref is sufficient.
Examples:
- `fix(patch): stop using v8::PropertyCallbackInfo<T>::This()`
- `fix(patch): BoringSSL and OpenSSL incompatibilities`
- `fix(patch): refactor module_wrap.cc FixedArray::Get params`
### Upstreamed patch removal
When patches are no longer needed (applied cleanly with "already applied" or confirmed upstreamed), group ALL removals into a single commit:
```
chore: remove upstreamed patch
```
or (if multiple):
```
chore: remove upstreamed patches
```
Most Node.js patches in Electron are Electron-authored (no upstream `PR-URL:`). If the patch originated from an upstream Node.js PR, no extra `Ref:` is needed. Otherwise, add a `Ref:` pointing to the relevant Node.js issue or commit if one exists.
### Trivial patch updates
After all fix commits, stage remaining trivial changes (index, line numbers, context only):
```bash
git add patches
git commit -m "chore: update patches (trivial only)"
```
**Conflict resolution can produce trivial results.** A `git am` conflict doesn't always mean the patch content changed — context drift alone can cause a conflict. After resolving and exporting, inspect the patch diff: if only index hashes, line numbers, and context lines changed (not the patch's own `+`/`-` lines), it's trivial and belongs here, not in a `fix(patch):` commit.
## Atomic Commits
Each patch conflict fix gets its own commit with its own Ref.
IMPORTANT: Try really hard to find the PR or commit reference per the instructions below. Each change you made should in theory have been in response to a change made in Node.js that you identified or can identify. Try for a while to identify and include the ref in the commit message. Do not give up easily.
## Finding Commit/Issue References
Use `git log` or `git blame` on Node.js source files in `../third_party/electron_node`. Look for:
```
PR-URL: https://github.com/nodejs/node/pull/XXXXX
```
or issue references in the patch itself:
```
Refs: https://github.com/nodejs/node/issues/XXXXX
```
Note: Most Node.js patches in Electron are Electron-authored and won't have upstream references. In that case, check `git log` in the Node.js repo to find which upstream commit caused the conflict.
If no reference found after searching: `Ref: Unable to locate reference`
## Example Commits
### Patch conflict fix (simple — title is sufficient)
```
fix(patch): stop using v8::PropertyCallbackInfo<T>::This()
Ref: https://github.com/nodejs/node/issues/60616
Co-Authored-By: <AI model attribution>
```
### Patch conflict fix (complex — description adds value)
```
fix(patch): BoringSSL and OpenSSL incompatibilities
Upstream updated OpenSSL APIs that diverge from BoringSSL. Adapted
the compatibility shims in crypto patches to use the BoringSSL
equivalents.
Ref: Unable to locate reference
Co-Authored-By: <AI model attribution>
```

View File

@@ -0,0 +1,96 @@
# Phase Two Commit Guidelines
Only follow these instructions if there are uncommitted changes in the Electron repo after any fixes are made during Phase Two that result a target that was failing, successfully building.
Ignore other instructions about making commit messages, our guidelines are CRITICALLY IMPORTANT and must be followed.
## Commit Message Style
**Titles** follow the 60/80-character guideline: simple changes fit within 60 characters, otherwise the limit is 80 characters. Exception: upstream Node.js PR titles are used verbatim even if longer.
Always include a `Co-Authored-By` trailer identifying the AI model that assisted (e.g., `Co-Authored-By: <AI model attribution>`).
## Two Commit Types
### For Electron Source Changes (shell/, electron/, etc.)
When the upstream Node.js commit has a `PR-URL:`:
```
node#{PR-Number}: {upstream PR's original title}
Ref: {Node.js PR link}
Co-Authored-By: <AI model attribution>
```
When there is no `PR-URL:` but there is an issue reference or commit:
```
fix: {description of the adaptation}
Ref: {Node.js issue or commit link}
Co-Authored-By: <AI model attribution>
```
Use the **upstream commit's original title** when available — do not paraphrase or rewrite it. To find it: check the commit message in `../third_party/electron_node` for `PR-URL:` or `Refs:` lines.
Only add a description body if it provides clarity beyond what the title already says (e.g., when Electron's adaptation is non-obvious). For simple renames, method additions, or straightforward API updates, the title + Ref link is sufficient.
Each change should have its own commit and its own Ref. Logically group into commits that make sense rather than one giant commit. You may include multiple "Ref" links if required.
IMPORTANT: Try really hard to find a reference. Each change you made should in theory have been in response to a change in Node.js. Check `git log` and `git blame` in the Node.js repo. Do not give up easily.
### For Patch Updates (patches/node/*.patch)
Use the same fixup workflow as Phase One and follow `references/phase-one-commit-guidelines.md` for the commit message format (`fix(patch):` prefix, topic style).
## Dependent Patch Header Updates
After any patch modification, check for other affected patches:
```bash
git status
# If other .patch files show as modified with only index, line number, and context changes:
git add patches/
git commit -m "chore: update patches (trivial only)"
```
## Finding References
Use `git log` or `git blame` on Node.js source files in `../third_party/electron_node`. Look for:
```
PR-URL: https://github.com/nodejs/node/pull/XXXXX
Refs: https://github.com/nodejs/node/issues/XXXXX
```
Note: Many Node.js patches in Electron are Electron-authored and won't have upstream `PR-URL:` lines. Check the patch's own commit message for `Refs:` lines, or use `git log` in the Node.js repo to find which upstream commit caused the build break.
If no reference found after searching: `Ref: Unable to locate reference`
## Example Commits
### Electron Source Fix (with upstream PR)
```
node#61898: src: stop using v8::PropertyCallbackInfo<T>::This()
Ref: https://github.com/nodejs/node/pull/61898
Co-Authored-By: <AI model attribution>
```
### Electron Source Fix (with issue reference, no PR)
```
fix: adapt to v8::PropertyCallbackInfo<T>::This() removal
Updated NodeBindings to use HolderV2() after upstream Node.js
stopped using the deprecated This() API.
Ref: https://github.com/nodejs/node/issues/60616
Co-Authored-By: <AI model attribution>
```

View File

@@ -6,14 +6,17 @@ the requirements below.
Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md
NOTE: PRS submitted without this template will be automatically closed.
Using a coding agent / AI? Read the policy: https://github.com/electron/governance/blob/main/policy/ai.md
NOTE: PRs submitted that do not follow this template will be automatically closed.
-->
#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->
- [ ] PR description included
- [ ] I have built and tested this PR
- [ ] I have built and tested this change
- [ ] I have filled out the PR description
- [ ] [I have reviewed and verified the changes](https://github.com/electron/governance/blob/main/policy/ai.md)
- [ ] `npm test` passes
- [ ] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [ ] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)

View File

@@ -15,7 +15,7 @@ runs:
git config --global core.preloadindex true
git config --global core.longpaths true
fi
export BUILD_TOOLS_SHA=a0cc95a1884a631559bcca0c948465b725d9295a
export BUILD_TOOLS_SHA=1b7bd25dae4a780bb3170fff56c9327b53aaf7eb
npm i -g @electron/build-tools
# Update depot_tools to ensure python
e d update_depot_tools
@@ -29,4 +29,4 @@ runs:
else
echo "$HOME/.electron_build_tools/third_party/depot_tools" >> $GITHUB_PATH
echo "$HOME/.electron_build_tools/third_party/depot_tools/python-bin" >> $GITHUB_PATH
fi
fi

View File

@@ -81,7 +81,7 @@ When working on the `roller/chromium/main` branch for Chromium upgrades, use `e
- JS/TS files: kebab-case (`file-name.ts`)
- C++ files: snake_case with `electron_api_` prefix (`electron_api_safe_storage.cc`)
- Test files: `api-{module-name}-spec.ts` in `spec/`
- Test files: `api-{module-name}.spec.ts` in `spec/`
- Source file lists are maintained in `filenames.gni` (with platform-specific sections)
### JavaScript/TypeScript

View File

@@ -5,7 +5,7 @@
"fromPath": "src/out/Default/args.gn",
"pattern": [
{
"regexp": "^(.+)[(:](\\d+)[:,](\\d+)\\)?:\\s+(warning|error):\\s+(.*)$",
"regexp": "^(.+)[(:](\\d+)[:,](\\d+)\\)?:\\s+(warning|fatal error|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,

View File

@@ -87,6 +87,8 @@ jobs:
!message.startsWith("The hosted runner lost communication with the server") &&
!message.startsWith("Dependabot encountered an error performing the update") &&
!message.startsWith("The action 'Run Electron Tests' has timed out") &&
!message.startsWith("The operation was canceled") &&
!message.startsWith("Canceling since") &&
!/Unable to make request/.test(message) &&
!/The requested URL returned error/.test(message),
)

View File

@@ -269,7 +269,7 @@ jobs:
needs: checkout-macos
with:
build-runs-on: macos-15-xlarge
test-runs-on: macos-15
test-runs-on: macos-15-xlarge
target-platform: macos
target-arch: arm64
is-release: false
@@ -290,7 +290,7 @@ jobs:
with:
build-runs-on: electron-arc-centralus-linux-amd64-32core
clang-tidy-runs-on: electron-arc-centralus-linux-amd64-8core
test-runs-on: electron-arc-centralus-linux-amd64-4core
test-runs-on: electron-arc-centralus-linux-amd64-8core
build-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
clang-tidy-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
@@ -312,7 +312,7 @@ jobs:
if: ${{ needs.setup.outputs.src == 'true' }}
with:
build-runs-on: electron-arc-centralus-linux-amd64-32core
test-runs-on: electron-arc-centralus-linux-amd64-4core
test-runs-on: electron-arc-centralus-linux-amd64-8core
build-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
test-container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root --privileged --init"}'
target-platform: linux
@@ -442,34 +442,7 @@ jobs:
contents: read
needs: [docs-only, macos-x64, macos-arm64, linux-x64, linux-x64-asan, linux-arm, linux-arm64, windows-x64, windows-x86, windows-arm64]
if: always() && github.repository == 'electron/electron' && !contains(needs.*.result, 'failure')
steps:
steps:
- name: GitHub Actions Jobs Done
run: |
echo "All GitHub Actions Jobs are done"
check-signed-commits:
name: Check signed commits in green PR
needs: gha-done
if: ${{ contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
runs-on: ubuntu-slim
permissions:
contents: read
pull-requests: write
steps:
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
with:
comment: |
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
for all incoming PRs. To get your PR merged, please sign those commits
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
(`git push --force-with-lease`)
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
- name: Remove needs-signed-commits label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh pr edit $PR_URL --remove-label needs-signed-commits

View File

@@ -57,8 +57,6 @@ jobs:
- name: Run Electron Tests in QEMU 64k Container
shell: bash
env:
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
ELECTRON_DISABLE_SECURITY_WARNINGS: 1
DISPLAY: ':99.0'
run: |

View File

@@ -66,7 +66,7 @@ jobs:
fail-fast: false
matrix:
build-type: ${{ inputs.target-platform == 'macos' && fromJSON('["darwin","mas"]') || (inputs.target-platform == 'win' && fromJSON('["win"]') || fromJSON('["linux"]')) }}
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.target-platform == 'linux', fromJSON('[1, 2, 3]'), fromJSON('[1, 2]')) }}
shard: ${{ case(inputs.display-server == 'wayland', fromJSON('[1]'), inputs.is-asan == true, fromJSON('[1, 2]'), inputs.target-platform == 'linux', fromJSON('[1]'), fromJSON('[1, 2]')) }}
env:
BUILD_TYPE: ${{ matrix.build-type }}
TARGET_ARCH: ${{ inputs.target-arch }}
@@ -218,16 +218,15 @@ jobs:
shell: bash
timeout-minutes: 60
env:
MOCHA_REPORTER: mocha-multi-reporters
MOCHA_MULTI_REPORTERS: mocha-junit-reporter, tap
ELECTRON_DISABLE_SECURITY_WARNINGS: 1
DISPLAY: ':99.0'
NPM_CONFIG_MSVS_VERSION: '2022'
run: |
cd src/electron
export ELECTRON_TEST_RESULTS_DIR=`pwd`/junit
export ELECTRON_EXTRA_ARGS="--trace-uncaught --enable-logging"
JUNIT_ARGS="--reporter=default --reporter=junit --outputFile.junit=junit/test-results-main.xml"
# Get which tests are on this shard
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.target-platform == 'linux', 3, 2) }})
tests_files=$(node script/split-tests ${{ matrix.shard }} ${{ case(inputs.display-server == 'wayland', 1, inputs.is-asan == true, 2, inputs.target-platform == 'linux', 1, 2) }})
if [ "${{ inputs.display-server }}" = "wayland" ]; then
allowlist_file=script/wayland-test-allowlist.txt
filtered_tests=""
@@ -251,11 +250,8 @@ jobs:
if [ "${{ inputs.target-arch }}" = "x86" ]; then
export npm_config_arch="ia32"
fi
if [ "${{ inputs.target-arch }}" = "arm64" ]; then
export ELECTRON_FORCE_TEST_SUITE_EXIT="true"
fi
fi
node script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
node script/yarn.js test $JUNIT_ARGS $tests_files
else
chown :builduser .. && chmod g+w ..
chown -R :builduser . && chmod -R g+w .
@@ -269,21 +265,19 @@ jobs:
export NSS_DISABLE_ARENA_FREE_LIST=1
export NSS_DISABLE_UNLOAD=1
export LLVM_SYMBOLIZER_PATH=$PWD/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer
export MOCHA_TIMEOUT=180000
echo "Piping output to ASAN_SYMBOLIZE ($ASAN_SYMBOLIZE)"
cd electron
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --trace-uncaught --enable-logging --files $tests_files | $ASAN_SYMBOLIZE
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --testTimeout=180000 $JUNIT_ARGS $tests_files | $ASAN_SYMBOLIZE
else
if [ "${{ inputs.target-arch }}" = "arm" ]; then
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
else
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --skipYarnInstall $JUNIT_ARGS $tests_files
else
if [ "${{ inputs.display-server }}" = "wayland" ]; then
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
runuser -u builduser -- script/actions/run-tests-wayland.sh script/yarn.js test $JUNIT_ARGS $tests_files
else
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test --runners=main --enableRerun=3 --trace-uncaught --enable-logging --files $tests_files
runuser -u builduser -- xvfb-run script/actions/run-tests.sh script/yarn.js test $JUNIT_ARGS $tests_files
fi
fi
fi
fi
- name: Take screenshot on timeout or cancellation

View File

@@ -50,7 +50,7 @@ jobs:
field-value: ✅ Reviewed
pull-request-labeled-ai-pr:
name: ai-pr label added
if: github.event.label.name == 'ai-pr'
if: github.event.label.name == 'ai-pr' && github.event.pull_request.state != 'closed'
runs-on: ubuntu-latest
permissions: {}
steps:

View File

@@ -13,7 +13,6 @@ permissions: {}
jobs:
check-signed-commits:
name: Check signed commits in PR
if: ${{ !contains(github.event.pull_request.labels.*.name, 'needs-signed-commits')}}
runs-on: ubuntu-slim
permissions:
contents: read
@@ -23,9 +22,9 @@ jobs:
uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1
with:
comment: |
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
for all incoming PRs. To get your PR merged, please sign those commits
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
⚠️ This PR contains unsigned commits. This repository enforces [commit signatures](https://docs.github.com/en/authentication/managing-commit-signature-verification)
for all incoming PRs. To get your PR merged, please sign those commits
(`git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}`) and force push them to this branch
(`git push --force-with-lease`)
For more information on signing commits, see GitHub's documentation on [Telling Git about your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key).
@@ -37,3 +36,11 @@ jobs:
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh pr edit $PR_URL --add-label needs-signed-commits
- name: Remove needs-signed-commits label
if: ${{ success() && contains(github.event.pull_request.labels.*.name, 'needs-signed-commits') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh pr edit $PR_URL --remove-label needs-signed-commits

47
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,47 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"sortImports": {
"newlinesBetween": true,
"groups": [
"electron-internal",
"electron-scoped",
"electron",
"external",
"builtin",
["sibling", "parent"],
"index",
"type",
"unknown"
],
"customGroups": [
{
"groupName": "electron-internal",
"elementNamePattern": ["@electron/internal", "@electron/internal/**"]
},
{
"groupName": "electron-scoped",
"elementNamePattern": ["@electron/**"]
},
{
"groupName": "electron",
"elementNamePattern": ["electron", "electron/**"]
}
]
},
"ignorePatterns": [
"node_modules",
"out",
"ts-gen",
"spec/node_modules",
"spec/fixtures/native-addon",
".github/workflows/node_modules",
"docs/fiddles",
"shell/browser/resources/win/resource.h",
"shell/common/node_includes.h",
"spec/fixtures/pages/jquery-3.6.0.min.js"
]
}

View File

@@ -11,6 +11,18 @@
{
"name": "no-only-tests",
"specifier": "./script/lint-plugins/no-only-tests.mjs"
},
{
"name": "remote-tools-imports",
"specifier": "./script/lint-plugins/remote-tools-imports.mjs"
},
{
"name": "no-nested-tests",
"specifier": "./script/lint-plugins/no-nested-tests.mjs"
},
{
"name": "no-unawaited-load",
"specifier": "./script/lint-plugins/no-unawaited-load.mjs"
}
],
"categories": {
@@ -315,7 +327,18 @@
{
"files": ["spec/**/*.ts", "spec/**/*.js", "spec/**/*.mjs"],
"rules": {
"no-only-tests/no-only-tests": "error"
"no-only-tests/no-only-tests": "error",
"remote-tools-imports/no-foreign-imports-in-remote-closure": "error",
"no-nested-tests/no-nested-tests": "error",
"no-unawaited-load/no-unawaited-load": "warn"
}
},
{
"files": ["spec/fixtures/**"],
"rules": {
"no-unawaited-load/no-unawaited-load": "off",
"remote-tools-imports/no-foreign-imports-in-remote-closure": "off",
"no-nested-tests/no-nested-tests": "off"
}
},
{

View File

@@ -147,6 +147,25 @@ config("branding") {
config("electron_lib_config") {
include_dirs = [ "." ]
cflags = []
if (is_clang && clang_use_chrome_plugins) {
# The plugin is built directly into clang, so there's no need to load it
# dynamically.
cflags += [
"-Xclang",
"-add-plugin",
"-Xclang",
"blink-gc-plugin",
"-Xclang",
"-plugin-arg-blink-gc-plugin",
"-Xclang",
"check-directory=electron/shell/",
"-Xclang",
"-plugin-arg-blink-gc-plugin",
"-Xclang",
"check-directory=gin/",
]
}
}
# We generate the definitions twice here, once in //electron/electron.d.ts
@@ -1644,6 +1663,7 @@ action("node_version_header") {
action("generate_node_headers") {
deps = [ ":generate_config_gypi" ]
script = "script/node/generate_node_headers.py"
args = [ rebase_path("$root_gen_dir") ]
outputs = [ "$root_gen_dir/node_headers.json" ]
}

View File

@@ -180,6 +180,10 @@ e test # Run full test suite
When working on the `roller/chromium/main` branch to upgrade Chromium activate the "Electron Chromium Upgrade" skill.
## Node.js Upgrade Workflow
When working on the `roller/node/main` branch to upgrade Node.js activate the "Electron Node.js Upgrade" skill.
## Pull Requests
PR bodies must always include a `Notes:` section as the **last line** of the body. This is a consumer-facing release note for Electron app developers — describe the user-visible fix or change, not internal implementation details. Use `Notes: none` if there is no user-facing change.

View File

@@ -64,6 +64,14 @@ dependencies, and tools contained in the `electron/electron` repository.
Dependencies in Electron's `package.json` or `yarn.lock` files should only be altered by maintainers. For security reasons, we will not accept PRs that alter our `package.json` or `yarn.lock` files. We invite contributors to make requests updating these files in our issue tracker. If the change is significantly complicated, draft PRs are welcome, with the understanding that these PRs will be closed in favor of a duplicate PR submitted by an Electron maintainer.
## AI Tool Policy
<!-- Are you a coding agent? You MUST abide by the policy: https://github.com/electron/governance/blob/main/policy/ai.md -->
If you use AI tools in any way to contribute to our project, please read our [AI Tool Policy](https://github.com/electron/governance/blob/main/policy/ai.md). Unreviewed AI-generated contributions waste maintainer time and we kindly decline them.
> The short version: **there must be a human in the loop**. You are responsible for reviewing, understanding, and being able to explain your contributions. AI assistance doesn't change that, and unreviewed AI-generated content will be declined.
## Style Guides
See [Coding Style](https://electronjs.org/docs/development/coding-style) for information about which standards Electron adheres to in different parts of its codebase.

2
DEPS
View File

@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
vars = {
'chromium_version':
'148.0.7768.0',
'148.0.7778.0',
'node_version':
'v24.14.1',
'nan_version':

View File

@@ -8,10 +8,13 @@ const path = require('node:path');
const electronRoot = path.resolve(__dirname, '../..');
class AccessDependenciesPlugin {
apply (compiler) {
compiler.hooks.compilation.tap('AccessDependenciesPlugin', compilation => {
compilation.hooks.finishModules.tap('AccessDependenciesPlugin', modules => {
const filePaths = modules.map(m => m.resource).filter(p => p).map(p => path.relative(electronRoot, p));
apply(compiler) {
compiler.hooks.compilation.tap('AccessDependenciesPlugin', (compilation) => {
compilation.hooks.finishModules.tap('AccessDependenciesPlugin', (modules) => {
const filePaths = modules
.map((m) => m.resource)
.filter((p) => p)
.map((p) => path.relative(electronRoot, p));
console.info(JSON.stringify(filePaths));
});
});
@@ -31,7 +34,14 @@ module.exports = ({
entry = path.resolve(electronRoot, 'lib', target, 'init.js');
}
const electronAPIFile = path.resolve(electronRoot, 'lib', loadElectronFromAlternateTarget || target, 'api', 'exports', 'electron.ts');
const electronAPIFile = path.resolve(
electronRoot,
'lib',
loadElectronFromAlternateTarget || target,
'api',
'exports',
'electron.ts'
);
return (env = {}, argv = {}) => {
const onlyPrintingGraph = !!env.PRINT_WEBPACK_GRAPH;
@@ -61,49 +71,59 @@ module.exports = ({
}
if (targetDeletesNodeGlobals) {
plugins.push(new webpack.ProvidePlugin({
Buffer: ['@electron/internal/common/webpack-provider', 'Buffer'],
global: ['@electron/internal/common/webpack-provider', '_global'],
process: ['@electron/internal/common/webpack-provider', 'process']
}));
plugins.push(
new webpack.ProvidePlugin({
Buffer: ['@electron/internal/common/webpack-provider', 'Buffer'],
global: ['@electron/internal/common/webpack-provider', '_global'],
process: ['@electron/internal/common/webpack-provider', 'process']
})
);
}
// Webpack 5 no longer polyfills process or Buffer.
if (!alwaysHasNode) {
plugins.push(new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser'
}));
plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser'
})
);
}
plugins.push(new webpack.ProvidePlugin({
Promise: ['@electron/internal/common/webpack-globals-provider', 'Promise']
}));
plugins.push(
new webpack.ProvidePlugin({
Promise: ['@electron/internal/common/webpack-globals-provider', 'Promise']
})
);
plugins.push(new webpack.DefinePlugin(defines));
if (wrapInitWithProfilingTimeout) {
plugins.push(new WrapperPlugin({
header: 'function ___electron_webpack_init__() {',
footer: `
plugins.push(
new WrapperPlugin({
header: 'function ___electron_webpack_init__() {',
footer: `
};
if ((globalThis.process || binding.process).argv.includes("--profile-electron-init")) {
setTimeout(___electron_webpack_init__, 0);
} else {
___electron_webpack_init__();
}`
}));
})
);
}
if (wrapInitWithTryCatch) {
plugins.push(new WrapperPlugin({
header: 'try {',
footer: `
plugins.push(
new WrapperPlugin({
header: 'try {',
footer: `
} catch (err) {
console.error('Electron ${outputFilename} script failed to run');
console.error(err);
}`
}));
})
);
}
return {
@@ -133,23 +153,26 @@ if ((globalThis.process || binding.process).argv.includes("--profile-electron-in
}
},
module: {
rules: [{
test: (moduleName) => !onlyPrintingGraph && ignoredModules.includes(moduleName),
loader: 'null-loader'
}, {
test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: path.resolve(electronRoot, 'tsconfig.electron.json'),
transpileOnly: onlyPrintingGraph,
ignoreDiagnostics: [
// File '{0}' is not under 'rootDir' '{1}'.
6059,
// Private field '{0}' must be declared in an enclosing class.
1111
]
rules: [
{
test: (moduleName) => !onlyPrintingGraph && ignoredModules.includes(moduleName),
loader: 'null-loader'
},
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
configFile: path.resolve(electronRoot, 'tsconfig.electron.json'),
transpileOnly: onlyPrintingGraph,
ignoreDiagnostics: [
// File '{0}' is not under 'rootDir' '{1}'.
6059,
// Private field '{0}' must be declared in an enclosing class.
1111
]
}
}
}]
]
},
node: {
__dirname: false,

View File

@@ -1,5 +1,5 @@
import { shell } from 'electron/common';
import { app, dialog, BrowserWindow, ipcMain } from 'electron/main';
import { app, dialog, BrowserWindow, ipcMain, Menu } from 'electron/main';
import * as path from 'node:path';
import * as url from 'node:url';
@@ -11,23 +11,62 @@ app.on('window-all-closed', () => {
app.quit();
});
function decorateURL (url: string) {
// safely add `?utm_source=default_app
const parsedUrl = new URL(url);
parsedUrl.searchParams.append('utm_source', 'default_app');
return parsedUrl.toString();
}
const isMac = process.platform === 'darwin';
app.whenReady().then(() => {
const helpMenu: Electron.MenuItemConstructorOptions = {
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org');
}
},
{
label: 'Documentation',
click: async () => {
const version = process.versions.electron;
await shell.openExternal(`https://github.com/electron/electron/tree/v${version}/docs#readme`);
}
},
{
label: 'Community Discussions',
click: async () => {
await shell.openExternal('https://discord.gg/electronjs');
}
},
{
label: 'Search Issues',
click: async () => {
await shell.openExternal('https://github.com/electron/electron/issues');
}
}
]
};
const macAppMenu: Electron.MenuItemConstructorOptions = { role: 'appMenu' };
const template: Electron.MenuItemConstructorOptions[] = [
...(isMac ? [macAppMenu] : []),
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
{ role: 'windowMenu' },
helpMenu
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});
// Find the shortest path to the electron binary
const absoluteElectronPath = process.execPath;
const relativeElectronPath = path.relative(process.cwd(), absoluteElectronPath);
const electronPath = absoluteElectronPath.length < relativeElectronPath.length
? absoluteElectronPath
: relativeElectronPath;
const electronPath =
absoluteElectronPath.length < relativeElectronPath.length ? absoluteElectronPath : relativeElectronPath;
const indexPath = path.resolve(app.getAppPath(), 'index.html');
function isTrustedSender (webContents: Electron.WebContents) {
function isTrustedSender(webContents: Electron.WebContents) {
if (webContents !== (mainWindow && mainWindow.webContents)) {
return false;
}
@@ -43,7 +82,7 @@ ipcMain.handle('bootstrap', (event) => {
return isTrustedSender(event.sender) ? electronPath : null;
});
async function createWindow (backgroundColor?: string) {
async function createWindow(backgroundColor?: string) {
await app.whenReady();
const options: Electron.BrowserWindowConstructorOptions = {
@@ -68,8 +107,8 @@ async function createWindow (backgroundColor?: string) {
mainWindow = new BrowserWindow(options);
mainWindow.on('ready-to-show', () => mainWindow!.show());
mainWindow.webContents.setWindowOpenHandler(details => {
shell.openExternal(decorateURL(details.url));
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: 'deny' };
});

View File

@@ -15,7 +15,7 @@ type DefaultAppOptions = {
interactive: boolean;
abi: boolean;
modules: string[];
}
};
// Parse command line options.
const argv = process.argv.slice(1);
@@ -74,7 +74,7 @@ if (option.modules.length > 0) {
(Module as any)._preloadModules(option.modules);
}
async function loadApplicationPackage (packagePath: string) {
async function loadApplicationPackage(packagePath: string) {
// Add a flag indicating app is started from default app.
Object.defineProperty(process, 'defaultApp', {
configurable: false,
@@ -92,9 +92,11 @@ async function loadApplicationPackage (packagePath: string) {
const emitWarning = process.emitWarning;
try {
process.emitWarning = () => {};
packageJson = (await import(url.pathToFileURL(packageJsonPath).toString(), {
with: { type: 'json' }
})).default;
packageJson = (
await import(url.pathToFileURL(packageJsonPath).toString(), {
with: { type: 'json' }
})
).default;
} catch (e) {
showErrorMessage(`Unable to parse ${packageJsonPath}\n\n${(e as Error).message}`);
return;
@@ -143,23 +145,23 @@ async function loadApplicationPackage (packagePath: string) {
}
}
function showErrorMessage (message: string) {
function showErrorMessage(message: string) {
app.focus();
dialog.showErrorBox('Error launching app', message);
process.exit(1);
}
async function loadApplicationByURL (appUrl: string) {
async function loadApplicationByURL(appUrl: string) {
const { loadURL } = await import('./default_app.js');
loadURL(appUrl);
}
async function loadApplicationByFile (appPath: string) {
async function loadApplicationByFile(appPath: string) {
const { loadFile } = await import('./default_app.js');
loadFile(appPath);
}
async function startRepl () {
async function startRepl() {
if (process.platform === 'win32') {
console.error('Electron REPL not currently supported on Windows');
process.exit(1);
@@ -187,7 +189,7 @@ async function startRepl () {
process.exit(0);
});
function defineBuiltin (context: any, name: string, getter: Function) {
function defineBuiltin(context: any, name: string, getter: Function) {
const setReal = (val: any) => {
// Deleting the property before re-assigning it disables the
// getter/setter mechanism.
@@ -225,11 +227,42 @@ async function startRepl () {
// we only trigger custom tab-completion when no common words are
// potentially matches.
const commonWords = [
'async', 'await', 'break', 'case', 'catch', 'const', 'continue',
'debugger', 'default', 'delete', 'do', 'else', 'export', 'false',
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let',
'new', 'null', 'return', 'switch', 'this', 'throw', 'true', 'try',
'typeof', 'var', 'void', 'while', 'with', 'yield'
'async',
'await',
'break',
'case',
'catch',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'export',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'let',
'new',
'null',
'return',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
];
const electronBuiltins = [...Object.keys(electron), 'original-fs', 'electron'];

View File

@@ -2,10 +2,10 @@ const { ipcRenderer, contextBridge } = require('electron/renderer');
const policy = window.trustedTypes.createPolicy('electron-default-app', {
// we trust the SVG contents
createHTML: input => input
createHTML: (input) => input
});
async function getOcticonSvg (name: string) {
async function getOcticonSvg(name: string) {
try {
const response = await fetch(`octicon/${name}.svg`);
const div = document.createElement('div');
@@ -16,7 +16,7 @@ async function getOcticonSvg (name: string) {
}
}
async function loadSVG (element: HTMLSpanElement) {
async function loadSVG(element: HTMLSpanElement) {
for (const cssClass of element.classList) {
if (cssClass.startsWith('octicon-')) {
const icon = await getOcticonSvg(cssClass.substr(8));
@@ -32,9 +32,9 @@ async function loadSVG (element: HTMLSpanElement) {
}
}
async function initialize () {
async function initialize() {
const electronPath = await ipcRenderer.invoke('bootstrap');
function replaceText (selector: string, text: string, link?: string) {
function replaceText(selector: string, text: string, link?: string) {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
if (link) {
@@ -51,7 +51,11 @@ async function initialize () {
replaceText('.electron-version', `Electron v${process.versions.electron}`, 'https://electronjs.org/docs');
replaceText('.chrome-version', `Chromium v${process.versions.chrome}`, 'https://developer.chrome.com/docs/chromium');
replaceText('.node-version', `Node v${process.versions.node}`, `https://nodejs.org/docs/v${process.versions.node}/api`);
replaceText(
'.node-version',
`Node v${process.versions.node}`,
`https://nodejs.org/docs/v${process.versions.node}/api`
);
replaceText('.v8-version', `v8 v${process.versions.v8}`, 'https://v8.dev/docs');
replaceText('.command-example', `${electronPath} path-to-app`);

View File

@@ -46,7 +46,7 @@ this has the additional effect of removing the menu bar from the window.
> [!NOTE]
> The default menu will be created automatically if the app does not set one.
> It contains standard items such as `File`, `Edit`, `View`, `Window` and `Help`.
> It contains standard items such as `File`, `Edit`, `View`, and `Window`.
#### `Menu.getApplicationMenu()`

View File

@@ -79,8 +79,9 @@ app.whenReady().then(() => {
### `new Notification([options])`
* `options` Object (optional)
* `id` string (optional) _macOS_ - A unique identifier for the notification, mapping to `UNNotificationRequest`'s [`identifier`](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/identifier) property. Defaults to a random UUID if not provided or if an empty string is passed. This can be used to remove or update previously delivered notifications.
* `groupId` string (optional) _macOS_ - A string identifier used to visually group notifications together in Notification Center. Maps to `UNNotificationContent`'s [`threadIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/threadidentifier) property.
* `id` string (optional) _macOS_ _Windows_ - A unique identifier for the notification. On macOS, maps to `UNNotificationRequest`'s [`identifier`](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/identifier) property. On Windows, maps to the toast notification's [`Tag`](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotification.tag) property. Defaults to a random UUID if not provided or if an empty string is passed. This can be used to remove or update previously delivered notifications.
* `groupId` string (optional) _macOS_ _Windows_ - A string identifier used to visually group notifications together in Notification Center / Action Center. On macOS, maps to `UNNotificationContent`'s [`threadIdentifier`](https://developer.apple.com/documentation/usernotifications/unnotificationcontent/threadidentifier) property. On Windows, maps to the toast notification's [`Group`](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotification.group) property.
* `groupTitle` string (optional) _Windows_ - A title for the notification group header. When both `groupId` and `groupTitle` are specified, Windows will display a header above the notification that groups related notifications together. Maps to the toast notification's [`header`](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-headers) element.
* `title` string (optional) - A title for the notification, which will be displayed at the top of the notification window when it is shown.
* `subtitle` string (optional) _macOS_ - A subtitle for the notification, which will be displayed below the title.
* `body` string (optional) - The body text of the notification, which will be displayed below the title or subtitle.
@@ -329,13 +330,17 @@ app.whenReady().then(() => {
### Instance Properties
#### `notification.id` _macOS_ _Readonly_
#### `notification.id` _macOS_ _Windows_ _Readonly_
A `string` property representing the unique identifier of the notification. This is set at construction time — either from the `id` option or as a generated UUID if none was provided.
#### `notification.groupId` _macOS_ _Readonly_
#### `notification.groupId` _macOS_ _Windows_ _Readonly_
A `string` property representing the group identifier of the notification. Notifications with the same `groupId` will be visually grouped together in Notification Center.
A `string` property representing the group identifier of the notification. Notifications with the same `groupId` will be visually grouped together in Notification Center (macOS) or Action Center (Windows).
#### `notification.groupTitle` _Windows_ _Readonly_
A `string` property representing the title of the notification group header.
#### `notification.title`

View File

@@ -33,10 +33,14 @@ because it is invoked in the main process.
Returns [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) | null
`features` is a comma-separated key-value list, following the standard format of
the browser. Electron will parse [`BrowserWindowConstructorOptions`](structures/browser-window-options.md) out of this
list where possible, for convenience. For full control and better ergonomics,
consider using `webContents.setWindowOpenHandler` to customize the
BrowserWindow creation.
the browser. For convenience, Electron will parse a subset of presentational
[`BrowserWindowConstructorOptions`](structures/browser-window-options.md) out of
this list (such as `width`, `height`, `x`, `y`, `show`, `frame`, `title`,
`backgroundColor`). Because the renderer is untrusted, options that cause the
main process to access the filesystem or that are otherwise privileged (such as
`icon`) are ignored. For full control and better ergonomics, use
`webContents.setWindowOpenHandler` to customize the BrowserWindow creation from
the main process.
A subset of [`WebPreferences`](structures/web-preferences.md) can be set directly,
unnested, from the features string: `zoomFactor`, `nodeIntegration`, `javascript`,
@@ -56,9 +60,10 @@ window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIn
enabled on the parent window.
* JavaScript will always be disabled in the opened `window` if it is disabled on
the parent window.
* Non-standard features (that are not handled by Chromium or Electron) given in
`features` will be passed to any registered `webContents`'s
`did-create-window` event handler in the `options` argument.
* Features that are not handled by Chromium and not in Electron's allowlist of
presentational `BrowserWindowConstructorOptions` are ignored. The raw
`features` string is still available to the main process via
`setWindowOpenHandler`.
* `frameName` follows the specification of `target` located in the [native documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#parameters).
* When opening `about:blank`, the child window's [`WebPreferences`](structures/web-preferences.md) will be copied
from the parent window, and there is no way to override it because Chromium

View File

@@ -680,6 +680,7 @@ filenames = {
"shell/common/gin_helper/wrappable.cc",
"shell/common/gin_helper/wrappable.h",
"shell/common/gin_helper/wrappable_base.h",
"shell/common/gin_helper/wrappable_pointer_tags.h",
"shell/common/heap_snapshot.cc",
"shell/common/heap_snapshot.h",
"shell/common/key_weak_map.h",
@@ -734,6 +735,8 @@ filenames = {
"shell/renderer/electron_sandboxed_renderer_client.h",
"shell/renderer/electron_smooth_round_rect.cc",
"shell/renderer/electron_smooth_round_rect.h",
"shell/renderer/oom_stack_trace.cc",
"shell/renderer/oom_stack_trace.h",
"shell/renderer/preload_realm_context.cc",
"shell/renderer/preload_realm_context.h",
"shell/renderer/preload_utils.cc",
@@ -781,6 +784,8 @@ filenames = {
"shell/browser/extensions/electron_extension_system_factory.h",
"shell/browser/extensions/electron_extension_system.cc",
"shell/browser/extensions/electron_extension_system.h",
"shell/browser/extensions/electron_extension_tab_util.cc",
"shell/browser/extensions/electron_extension_tab_util.h",
"shell/browser/extensions/electron_extension_web_contents_observer.cc",
"shell/browser/extensions/electron_extension_web_contents_observer.h",
"shell/browser/extensions/electron_extensions_api_client.cc",

View File

@@ -41,7 +41,8 @@ Object.assign(app, {
commandLine: {
hasSwitch: (theSwitch: string) => commandLine.hasSwitch(String(theSwitch)),
getSwitchValue: (theSwitch: string) => commandLine.getSwitchValue(String(theSwitch)),
appendSwitch: (theSwitch: string, value?: string) => commandLine.appendSwitch(String(theSwitch), typeof value === 'undefined' ? value : String(value)),
appendSwitch: (theSwitch: string, value?: string) =>
commandLine.appendSwitch(String(theSwitch), typeof value === 'undefined' ? value : String(value)),
appendArgument: (arg: string) => commandLine.appendArgument(String(arg)),
removeSwitch: (theSwitch: string) => commandLine.removeSwitch(String(theSwitch))
} as Electron.CommandLine
@@ -50,10 +51,10 @@ Object.assign(app, {
// we define this here because it'd be overly complicated to
// do in native land
Object.defineProperty(app, 'applicationMenu', {
get () {
get() {
return Menu.getApplicationMenu();
},
set (menu: Electron.Menu | null) {
set(menu: Electron.Menu | null) {
return Menu.setApplicationMenu(menu);
}
});
@@ -116,17 +117,22 @@ for (const name of events) {
}
app._clientCertRequestPasswordHandler = null;
app.setClientCertRequestPasswordHandler = function (handler: (params: Electron.ClientCertRequestParams) => Promise<string>) {
app.setClientCertRequestPasswordHandler = function (
handler: (params: Electron.ClientCertRequestParams) => Promise<string>
) {
app._clientCertRequestPasswordHandler = handler;
};
app.on('-client-certificate-request-password', async (event: Electron.Event<Electron.ClientCertRequestParams>, callback: (password: string) => void) => {
event.preventDefault();
const { hostname, tokenName, isRetry } = event;
if (!app._clientCertRequestPasswordHandler) {
callback('');
return;
app.on(
'-client-certificate-request-password',
async (event: Electron.Event<Electron.ClientCertRequestParams>, callback: (password: string) => void) => {
event.preventDefault();
const { hostname, tokenName, isRetry } = event;
if (!app._clientCertRequestPasswordHandler) {
callback('');
return;
}
const password = await app._clientCertRequestPasswordHandler({ hostname, tokenName, isRetry });
callback(password);
}
const password = await app._clientCertRequestPasswordHandler({ hostname, tokenName, isRetry });
callback(password);
});
);

View File

@@ -135,7 +135,7 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
allowAnyVersion: boolean = false;
// Private: Validate that the URL points to an MSIX file (following redirects)
private async validateMsixUrl (url: string): Promise<void> {
private async validateMsixUrl(url: string): Promise<void> {
try {
// Make a HEAD request to follow redirects and get the final URL
const response = await net.fetch(url, {
@@ -153,7 +153,9 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
const hasMsixExtension = pathname.endsWith('.msix') || pathname.endsWith('.msixbundle');
if (!hasMsixExtension) {
throw new Error(`Update URL does not point to an MSIX file. Expected .msix or .msixbundle extension, got final URL: ${finalUrl}`);
throw new Error(
`Update URL does not point to an MSIX file. Expected .msix or .msixbundle extension, got final URL: ${finalUrl}`
);
}
} catch (error) {
if (error instanceof TypeError) {
@@ -164,7 +166,7 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
}
// Private: Check if URL is a direct MSIX file (following redirects)
private async isDirectMsixUrl (url: string, emitError: boolean = false): Promise<boolean> {
private async isDirectMsixUrl(url: string, emitError: boolean = false): Promise<boolean> {
try {
await this.validateMsixUrl(url);
return true;
@@ -178,12 +180,12 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
// Supports both versioning (x.y.z) and Windows version format (x.y.z.a)
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if v1 === v2
private compareVersions (v1: string, v2: string): number {
const parts1 = v1.split('.').map(part => {
private compareVersions(v1: string, v2: string): number {
const parts1 = v1.split('.').map((part) => {
const parsed = parseInt(part, 10);
return isNaN(parsed) ? 0 : parsed;
});
const parts2 = v2.split('.').map(part => {
const parts2 = v2.split('.').map((part) => {
const parsed = parseInt(part, 10);
return isNaN(parsed) ? 0 : parsed;
});
@@ -203,9 +205,12 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
// Private: Parse the static releases array format
// This is a static JSON file containing all releases
private parseStaticReleasFile (json: any, currentVersion: string): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
private parseStaticReleasFile(
json: any,
currentVersion: string
): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
if (!Array.isArray(json.releases) || !json.currentRelease || typeof json.currentRelease !== 'string') {
this.emitError(new Error('Invalid releases format. Expected \'releases\' array and \'currentRelease\' string.'));
this.emitError(new Error("Invalid releases format. Expected 'releases' array and 'currentRelease' string."));
return { ok: false, available: false };
}
@@ -234,14 +239,18 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
const releaseEntry = json.releases.find((r: any) => r.version === currentReleaseVersion);
if (!releaseEntry || !releaseEntry.updateTo) {
this.emitError(new Error(`Release entry for version '${currentReleaseVersion}' not found or missing 'updateTo' property.`));
this.emitError(
new Error(`Release entry for version '${currentReleaseVersion}' not found or missing 'updateTo' property.`)
);
return { ok: false, available: false };
}
const updateTo = releaseEntry.updateTo;
if (!updateTo.url) {
this.emitError(new Error(`Invalid release entry. 'updateTo.url' is missing for version ${currentReleaseVersion}.`));
this.emitError(
new Error(`Invalid release entry. 'updateTo.url' is missing for version ${currentReleaseVersion}.`)
);
return { ok: false, available: false };
}
@@ -255,15 +264,22 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
};
}
private parseDynamicReleasFile (json: any): { ok: boolean; available: boolean; url?: string; name?: string; notes?: string; pub_date?: string } {
private parseDynamicReleasFile(json: any): {
ok: boolean;
available: boolean;
url?: string;
name?: string;
notes?: string;
pub_date?: string;
} {
if (!json.url) {
this.emitError(new Error('Invalid releases format. Expected \'url\' string property.'));
this.emitError(new Error("Invalid releases format. Expected 'url' string property."));
return { ok: false, available: false };
}
return { ok: true, available: true, url: json.url, name: json.name, notes: json.notes, pub_date: json.pub_date };
}
private async fetchSquirrelJson (url: string) {
private async fetchSquirrelJson(url: string) {
const headers: Record<string, string> = {
...this.updateHeaders,
Accept: 'application/json' // Always set Accept header, overriding any user-provided Accept
@@ -299,8 +315,8 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
}
}
private async getUpdateInfo (url: string): Promise<UpdateInfo> {
if (url && await this.isDirectMsixUrl(url)) {
private async getUpdateInfo(url: string): Promise<UpdateInfo> {
if (url && (await this.isDirectMsixUrl(url))) {
return { ok: true, available: true, updateUrl: url, releaseDate: new Date() };
} else {
const updateJson = await this.fetchSquirrelJson(url);
@@ -321,7 +337,7 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
const releaseName = updateJson.name ?? '';
releaseDate = releaseDate ?? new Date();
if (!await this.isDirectMsixUrl(updateUrl, true)) {
if (!(await this.isDirectMsixUrl(updateUrl, true))) {
return { ok: false };
} else {
return {
@@ -337,11 +353,11 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
}
}
getFeedURL () {
getFeedURL() {
return this.updateURL ?? '';
}
setFeedURL (options: { url: string; headers?: Record<string, string>; allowAnyVersion?: boolean } | string) {
setFeedURL(options: { url: string; headers?: Record<string, string>; allowAnyVersion?: boolean } | string) {
let updateURL: string;
let headers: Record<string, string> | undefined;
let allowAnyVersion: boolean | undefined;
@@ -351,23 +367,23 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
headers = options.headers;
allowAnyVersion = options.allowAnyVersion;
} else {
throw new TypeError('Expected options object to contain a \'url\' string property in setFeedUrl call');
throw new TypeError("Expected options object to contain a 'url' string property in setFeedUrl call");
}
} else if (typeof options === 'string') {
updateURL = options;
} else {
throw new TypeError('Expected an options object with a \'url\' property to be provided');
throw new TypeError("Expected an options object with a 'url' property to be provided");
}
this.updateURL = updateURL;
this.updateHeaders = headers ?? null;
this.allowAnyVersion = allowAnyVersion ?? false;
}
getPackageInfo (): MSIXPackageInfo {
getPackageInfo(): MSIXPackageInfo {
return msixUpdate.getPackageInfo() as MSIXPackageInfo;
}
async checkForUpdates () {
async checkForUpdates() {
const url = this.updateURL;
if (!url) {
return this.emitError(new Error('Update URL is not set'));
@@ -382,7 +398,11 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
// If appInstallerUri is set, Windows App Installer manages updates automatically
// Prevent updates here to avoid conflicts
if (packageInfo.appInstallerUri) {
return this.emitError(new Error('Auto-updates are managed by Windows App Installer. Updates are not allowed when installed via Application Manifest.'));
return this.emitError(
new Error(
'Auto-updates are managed by Windows App Installer. Updates are not allowed when installed via Application Manifest.'
)
);
}
this.emit('checking-for-update');
@@ -405,18 +425,26 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
forceUpdateFromAnyVersion: this.allowAnyVersion
} as UpdateMsixOptions);
this.emit('update-downloaded', {}, msixUrlInfo.releaseNotes, msixUrlInfo.releaseName, msixUrlInfo.releaseDate, msixUrlInfo.updateUrl, () => {
this.quitAndInstall();
});
this.emit(
'update-downloaded',
{},
msixUrlInfo.releaseNotes,
msixUrlInfo.releaseName,
msixUrlInfo.releaseDate,
msixUrlInfo.updateUrl,
() => {
this.quitAndInstall();
}
);
}
} catch (error) {
this.emitError(error as Error);
}
}
async quitAndInstall () {
async quitAndInstall() {
if (!this.updateAvailable) {
this.emitError(new Error('No update available, can\'t quit and install'));
this.emitError(new Error("No update available, can't quit and install"));
app.quit();
return;
}
@@ -441,7 +469,7 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
// Private: Emit both error object and message, this is to keep compatibility
// with Old APIs.
emitError (error: Error) {
emitError(error: Error) {
this.emit('error', error, error.message);
}
}

View File

@@ -8,40 +8,40 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
updateAvailable: boolean = false;
updateURL: string | null = null;
quitAndInstall () {
quitAndInstall() {
if (!this.updateAvailable) {
return this.emitError(new Error('No update available, can\'t quit and install'));
return this.emitError(new Error("No update available, can't quit and install"));
}
squirrelUpdate.processStart();
app.quit();
}
getFeedURL () {
getFeedURL() {
return this.updateURL ?? '';
}
getPackageInfo () {
getPackageInfo() {
// Squirrel-based Windows apps don't have MSIX package information
return undefined;
}
setFeedURL (options: { url: string } | string) {
setFeedURL(options: { url: string } | string) {
let updateURL: string;
if (typeof options === 'object') {
if (typeof options.url === 'string') {
updateURL = options.url;
} else {
throw new TypeError('Expected options object to contain a \'url\' string property in setFeedUrl call');
throw new TypeError("Expected options object to contain a 'url' string property in setFeedUrl call");
}
} else if (typeof options === 'string') {
updateURL = options;
} else {
throw new TypeError('Expected an options object with a \'url\' property to be provided');
throw new TypeError("Expected an options object with a 'url' property to be provided");
}
this.updateURL = updateURL;
}
async checkForUpdates () {
async checkForUpdates() {
const url = this.updateURL;
if (!url) {
return this.emitError(new Error('Update URL is not set'));
@@ -72,7 +72,7 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
// Private: Emit both error object and message, this is to keep compatibility
// with Old APIs.
emitError (error: Error) {
emitError(error: Error) {
this.emit('error', error, error.message);
}
}

View File

@@ -1,4 +1,5 @@
const { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo } =
process._linkedBinding('electron_browser_msix_updater');
const { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo } = process._linkedBinding(
'electron_browser_msix_updater'
);
export { updateMsix, registerPackage, registerRestartOnUpdate, getPackageInfo };

View File

@@ -34,8 +34,12 @@ const spawnUpdate = async function (args: string[], options: { detached: boolean
let stdout = '';
let stderr = '';
spawnedProcess.stdout.on('data', (data) => { stdout += data; });
spawnedProcess.stderr.on('data', (data) => { stderr += data; });
spawnedProcess.stdout.on('data', (data) => {
stdout += data;
});
spawnedProcess.stderr.on('data', (data) => {
stderr += data;
});
spawnedProcess.on('error', (error) => {
spawnedProcess = undefined;
@@ -59,12 +63,12 @@ const spawnUpdate = async function (args: string[], options: { detached: boolean
};
// Start an instance of the installed app.
export function processStart () {
export function processStart() {
spawnUpdate(['--processStartAndWait', exeName], { detached: true });
}
// Download the releases specified by the URL and write new results to stdout.
export async function checkForUpdate (updateURL: string): Promise<any> {
export async function checkForUpdate(updateURL: string): Promise<any> {
const stdout = await spawnUpdate(['--checkForUpdate', updateURL], { detached: false });
try {
// Last line of output is the JSON details about the releases
@@ -76,12 +80,12 @@ export async function checkForUpdate (updateURL: string): Promise<any> {
}
// Update the application to the latest remote version specified by URL.
export async function update (updateURL: string): Promise<void> {
export async function update(updateURL: string): Promise<void> {
await spawnUpdate(['--update', updateURL], { detached: false });
}
// Is the Update.exe installed with the current application?
export function supported () {
export function supported() {
try {
fs.accessSync(updateExe, fs.constants.R_OK);
return true;

View File

@@ -25,88 +25,156 @@ BaseWindow.prototype.setTouchBar = function (touchBar) {
// Properties
Object.defineProperty(BaseWindow.prototype, 'autoHideMenuBar', {
get: function () { return this.isMenuBarAutoHide(); },
set: function (autoHide) { this.setAutoHideMenuBar(autoHide); }
get: function () {
return this.isMenuBarAutoHide();
},
set: function (autoHide) {
this.setAutoHideMenuBar(autoHide);
}
});
Object.defineProperty(BaseWindow.prototype, 'visibleOnAllWorkspaces', {
get: function () { return this.isVisibleOnAllWorkspaces(); },
set: function (visible) { this.setVisibleOnAllWorkspaces(visible); }
get: function () {
return this.isVisibleOnAllWorkspaces();
},
set: function (visible) {
this.setVisibleOnAllWorkspaces(visible);
}
});
Object.defineProperty(BaseWindow.prototype, 'fullScreen', {
get: function () { return this.isFullScreen(); },
set: function (full) { this.setFullScreen(full); }
get: function () {
return this.isFullScreen();
},
set: function (full) {
this.setFullScreen(full);
}
});
Object.defineProperty(BaseWindow.prototype, 'simpleFullScreen', {
get: function () { return this.isSimpleFullScreen(); },
set: function (simple) { this.setSimpleFullScreen(simple); }
get: function () {
return this.isSimpleFullScreen();
},
set: function (simple) {
this.setSimpleFullScreen(simple);
}
});
Object.defineProperty(BaseWindow.prototype, 'focusable', {
get: function () { return this.isFocusable(); },
set: function (focusable) { this.setFocusable(focusable); }
get: function () {
return this.isFocusable();
},
set: function (focusable) {
this.setFocusable(focusable);
}
});
Object.defineProperty(BaseWindow.prototype, 'kiosk', {
get: function () { return this.isKiosk(); },
set: function (kiosk) { this.setKiosk(kiosk); }
get: function () {
return this.isKiosk();
},
set: function (kiosk) {
this.setKiosk(kiosk);
}
});
Object.defineProperty(BaseWindow.prototype, 'documentEdited', {
get: function () { return this.isDocumentEdited(); },
set: function (edited) { this.setDocumentEdited(edited); }
get: function () {
return this.isDocumentEdited();
},
set: function (edited) {
this.setDocumentEdited(edited);
}
});
Object.defineProperty(BaseWindow.prototype, 'shadow', {
get: function () { return this.hasShadow(); },
set: function (shadow) { this.setHasShadow(shadow); }
get: function () {
return this.hasShadow();
},
set: function (shadow) {
this.setHasShadow(shadow);
}
});
Object.defineProperty(BaseWindow.prototype, 'representedFilename', {
get: function () { return this.getRepresentedFilename(); },
set: function (filename) { this.setRepresentedFilename(filename); }
get: function () {
return this.getRepresentedFilename();
},
set: function (filename) {
this.setRepresentedFilename(filename);
}
});
Object.defineProperty(BaseWindow.prototype, 'minimizable', {
get: function () { return this.isMinimizable(); },
set: function (min) { this.setMinimizable(min); }
get: function () {
return this.isMinimizable();
},
set: function (min) {
this.setMinimizable(min);
}
});
Object.defineProperty(BaseWindow.prototype, 'title', {
get: function () { return this.getTitle(); },
set: function (title) { this.setTitle(title); }
get: function () {
return this.getTitle();
},
set: function (title) {
this.setTitle(title);
}
});
Object.defineProperty(BaseWindow.prototype, 'maximizable', {
get: function () { return this.isMaximizable(); },
set: function (max) { this.setMaximizable(max); }
get: function () {
return this.isMaximizable();
},
set: function (max) {
this.setMaximizable(max);
}
});
Object.defineProperty(BaseWindow.prototype, 'resizable', {
get: function () { return this.isResizable(); },
set: function (res) { this.setResizable(res); }
get: function () {
return this.isResizable();
},
set: function (res) {
this.setResizable(res);
}
});
Object.defineProperty(BaseWindow.prototype, 'menuBarVisible', {
get: function () { return this.isMenuBarVisible(); },
set: function (visible) { this.setMenuBarVisibility(visible); }
get: function () {
return this.isMenuBarVisible();
},
set: function (visible) {
this.setMenuBarVisibility(visible);
}
});
Object.defineProperty(BaseWindow.prototype, 'fullScreenable', {
get: function () { return this.isFullScreenable(); },
set: function (full) { this.setFullScreenable(full); }
get: function () {
return this.isFullScreenable();
},
set: function (full) {
this.setFullScreenable(full);
}
});
Object.defineProperty(BaseWindow.prototype, 'closable', {
get: function () { return this.isClosable(); },
set: function (close) { this.setClosable(close); }
get: function () {
return this.isClosable();
},
set: function (close) {
this.setClosable(close);
}
});
Object.defineProperty(BaseWindow.prototype, 'movable', {
get: function () { return this.isMovable(); },
set: function (move) { this.setMovable(move); }
get: function () {
return this.isMovable();
},
set: function (move) {
this.setMovable(move);
}
});
BaseWindow.getFocusedWindow = () => {

View File

@@ -1,4 +1,11 @@
import { BrowserWindow, AutoResizeOptions, Rectangle, WebContentsView, WebPreferences, WebContents } from 'electron/main';
import {
BrowserWindow,
AutoResizeOptions,
Rectangle,
WebContentsView,
WebPreferences,
WebContents
} from 'electron/main';
const v8Util = process._linkedBinding('electron_common_v8_util');
@@ -10,10 +17,10 @@ export default class BrowserView {
// AutoResize state
#resizeListener: ((...args: any[]) => void) | null = null;
#lastWindowSize: {width: number, height: number} = { width: 0, height: 0 };
#lastWindowSize: { width: number; height: number } = { width: 0, height: 0 };
#autoResizeFlags: AutoResizeOptions = {};
constructor (options: {webPreferences: WebPreferences, webContents?: WebContents} = { webPreferences: {} }) {
constructor(options: { webPreferences: WebPreferences; webContents?: WebContents } = { webPreferences: {} }) {
const { webPreferences = {}, webContents } = options;
if (webContents) {
v8Util.setHiddenValue(webPreferences, 'webContents', webContents);
@@ -25,21 +32,21 @@ export default class BrowserView {
this.#webContentsView.webContents.once('destroyed', this.#destroyListener);
}
get webContents () {
get webContents() {
return this.#webContentsView.webContents;
}
setBounds (bounds: Rectangle) {
setBounds(bounds: Rectangle) {
this.#webContentsView.setBounds(bounds);
this.#autoHorizontalProportion = null;
this.#autoVerticalProportion = null;
}
getBounds () {
getBounds() {
return this.#webContentsView.getBounds();
}
setAutoResize (options: AutoResizeOptions) {
setAutoResize(options: AutoResizeOptions) {
if (options == null || typeof options !== 'object') {
throw new Error('Invalid auto resize options');
}
@@ -55,19 +62,19 @@ export default class BrowserView {
this.#autoVerticalProportion = null;
}
setBackgroundColor (color: string) {
setBackgroundColor(color: string) {
this.#webContentsView.setBackgroundColor(color);
}
// Internal methods
get ownerWindow (): BrowserWindow | null {
get ownerWindow(): BrowserWindow | null {
return this.#ownerWindow;
}
// We can't rely solely on the webContents' owner window because
// a webContents can be closed by the user while the BrowserView
// remains alive and attached to a BrowserWindow.
set ownerWindow (w: BrowserWindow | null) {
set ownerWindow(w: BrowserWindow | null) {
this.#removeResizeListener();
if (this.webContents && !this.webContents.isDestroyed()) {
@@ -77,7 +84,7 @@ export default class BrowserView {
this.#ownerWindow = w;
if (w) {
this.#lastWindowSize = w.getBounds();
w.on('resize', this.#resizeListener = this.#autoResize.bind(this));
w.on('resize', (this.#resizeListener = this.#autoResize.bind(this)));
w.on('closed', () => {
this.#removeResizeListener();
this.#ownerWindow = null;
@@ -86,25 +93,25 @@ export default class BrowserView {
}
}
#onDestroy () {
#onDestroy() {
// Ensure that if #webContentsView's webContents is destroyed,
// the WebContentsView is removed from the view hierarchy.
this.#ownerWindow?.contentView.removeChildView(this.webContentsView);
}
#removeResizeListener () {
#removeResizeListener() {
if (this.#ownerWindow && this.#resizeListener) {
this.#ownerWindow.off('resize', this.#resizeListener);
this.#resizeListener = null;
}
}
#autoHorizontalProportion: {width: number, left: number} | null = null;
#autoVerticalProportion: {height: number, top: number} | null = null;
#autoResize () {
#autoHorizontalProportion: { width: number; left: number } | null = null;
#autoVerticalProportion: { height: number; top: number } | null = null;
#autoResize() {
if (!this.ownerWindow) {
throw new Error('Electron bug: #autoResize called without owner window');
};
}
if (this.#autoResizeFlags.horizontal && this.#autoHorizontalProportion == null) {
const viewBounds = this.#webContentsView.getBounds();
@@ -158,7 +165,7 @@ export default class BrowserView {
};
}
get webContentsView () {
get webContentsView() {
return this.#webContentsView;
}
}

View File

@@ -40,10 +40,14 @@ BrowserWindow.prototype._init = function (this: BWT) {
let unresponsiveEvent: NodeJS.Timeout | null = null;
const emitUnresponsiveEvent = () => {
unresponsiveEvent = null;
if (!this.isDestroyed() && this.isEnabled()) { this.emit('unresponsive'); }
if (!this.isDestroyed() && this.isEnabled()) {
this.emit('unresponsive');
}
};
this.webContents.on('unresponsive', () => {
if (!unresponsiveEvent) { unresponsiveEvent = setTimeout(emitUnresponsiveEvent, 50); }
if (!unresponsiveEvent) {
unresponsiveEvent = setTimeout(emitUnresponsiveEvent, 50);
}
});
this.webContents.on('responsive', () => {
if (unresponsiveEvent) {
@@ -83,16 +87,16 @@ BrowserWindow.prototype._init = function (this: BWT) {
this._browserViews = [];
this.on('closed', () => {
this._browserViews.forEach(b => b.webContents?.close({ waitForBeforeUnload: true }));
this._browserViews.forEach((b) => b.webContents?.close({ waitForBeforeUnload: true }));
});
// Notify the creation of the window.
app.emit('browser-window-created', { preventDefault () {} }, this);
app.emit('browser-window-created', { preventDefault() {} }, this);
Object.defineProperty(this, 'devToolsWebContents', {
enumerable: true,
configurable: false,
get () {
get() {
return this.webContents.devToolsWebContents;
}
});
@@ -104,7 +108,7 @@ const isBrowserWindow = (win: any) => {
BrowserWindow.fromId = (id: number) => {
const win = BaseWindow.fromId(id);
return isBrowserWindow(win) ? win as any as BWT : null;
return isBrowserWindow(win) ? (win as any as BWT) : null;
};
BrowserWindow.getAllWindows = () => {
@@ -214,10 +218,12 @@ BrowserWindow.prototype.addBrowserView = function (browserView: BrowserView) {
};
BrowserWindow.prototype.setBrowserView = function (browserView: BrowserView) {
this._browserViews.forEach(bv => {
this._browserViews.forEach((bv) => {
this.removeBrowserView(bv);
});
if (browserView) { this.addBrowserView(browserView); }
if (browserView) {
this.addBrowserView(browserView);
}
};
BrowserWindow.prototype.removeBrowserView = function (browserView: BrowserView) {

View File

@@ -5,7 +5,7 @@ import { app } from 'electron/main';
const binding = process._linkedBinding('electron_browser_crash_reporter');
class CrashReporter implements Electron.CrashReporter {
start (options: Electron.CrashReporterStartOptions) {
start(options: Electron.CrashReporterStartOptions) {
const {
productName = app.name,
companyName,
@@ -21,7 +21,9 @@ class CrashReporter implements Electron.CrashReporter {
if (uploadToServer && !submitURL) throw new Error('submitURL must be specified when uploadToServer is true');
if (!compress && uploadToServer) {
deprecate.log('Sending uncompressed crash reports is deprecated and will be removed in a future version of Electron. Set { compress: true } to opt-in to the new behavior. Crash reports will be uploaded gzipped, which most crash reporting servers support.');
deprecate.log(
'Sending uncompressed crash reports is deprecated and will be removed in a future version of Electron. Set { compress: true } to opt-in to the new behavior. Crash reports will be uploaded gzipped, which most crash reporting servers support.'
);
}
const appVersion = app.getVersion();
@@ -34,26 +36,33 @@ class CrashReporter implements Electron.CrashReporter {
...globalExtra
};
binding.start(submitURL, uploadToServer,
ignoreSystemCrashHandler, rateLimit, compress, globalExtraAmended, extra, false);
binding.start(
submitURL,
uploadToServer,
ignoreSystemCrashHandler,
rateLimit,
compress,
globalExtraAmended,
extra,
false
);
}
getLastCrashReport () {
const reports = this.getUploadedReports()
.sort((a, b) => {
const ats = (a && a.date) ? new Date(a.date).getTime() : 0;
const bts = (b && b.date) ? new Date(b.date).getTime() : 0;
return bts - ats;
});
getLastCrashReport() {
const reports = this.getUploadedReports().sort((a, b) => {
const ats = a && a.date ? new Date(a.date).getTime() : 0;
const bts = b && b.date ? new Date(b.date).getTime() : 0;
return bts - ats;
});
return (reports.length > 0) ? reports[0] : null;
return reports.length > 0 ? reports[0] : null;
}
getUploadedReports (): Electron.CrashReport[] {
getUploadedReports(): Electron.CrashReport[] {
return binding.getUploadedReports();
}
getUploadToServer () {
getUploadToServer() {
if (process.type === 'browser') {
return binding.getUploadToServer();
} else {
@@ -61,7 +70,7 @@ class CrashReporter implements Electron.CrashReporter {
}
}
setUploadToServer (uploadToServer: boolean) {
setUploadToServer(uploadToServer: boolean) {
if (process.type === 'browser') {
return binding.setUploadToServer(uploadToServer);
} else {
@@ -69,15 +78,15 @@ class CrashReporter implements Electron.CrashReporter {
}
}
addExtraParameter (key: string, value: string) {
addExtraParameter(key: string, value: string) {
binding.addExtraParameter(key, value);
}
removeExtraParameter (key: string) {
removeExtraParameter(key: string) {
binding.removeExtraParameter(key);
}
getParameters () {
getParameters() {
return binding.getParameters();
}
}

View File

@@ -1,8 +1,11 @@
import { BrowserWindow } from 'electron/main';
const { createDesktopCapturer, isDisplayMediaSystemPickerAvailable } = process._linkedBinding('electron_browser_desktop_capturer');
const { createDesktopCapturer, isDisplayMediaSystemPickerAvailable } = process._linkedBinding(
'electron_browser_desktop_capturer'
);
const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) => JSON.stringify(a) === JSON.stringify(b);
const deepEqual = (a: ElectronInternal.GetSourcesOptions, b: ElectronInternal.GetSourcesOptions) =>
JSON.stringify(a) === JSON.stringify(b);
let currentlyRunning: {
options: ElectronInternal.GetSourcesOptions;
@@ -10,13 +13,13 @@ let currentlyRunning: {
}[] = [];
// |options.types| can't be empty and must be an array
function isValid (options: Electron.SourcesOptions) {
function isValid(options: Electron.SourcesOptions) {
return Array.isArray(options?.types);
}
export { isDisplayMediaSystemPickerAvailable };
export async function getSources (args: Electron.SourcesOptions) {
export async function getSources(args: Electron.SourcesOptions) {
if (!isValid(args)) throw new Error('Invalid options');
const resizableValues = new Map();
@@ -64,11 +67,11 @@ export async function getSources (args: Electron.SourcesOptions) {
if (resizableValues.has(win.id)) {
win.resizable = resizableValues.get(win.id);
}
};
}
}
}
// Remove from currentlyRunning once we resolve or reject
currentlyRunning = currentlyRunning.filter(running => running.options !== options);
currentlyRunning = currentlyRunning.filter((running) => running.options !== options);
};
capturer._onerror = (error: string) => {

View File

@@ -1,5 +1,13 @@
import { app, BaseWindow } from 'electron/main';
import type { OpenDialogOptions, OpenDialogReturnValue, MessageBoxOptions, SaveDialogOptions, SaveDialogReturnValue, MessageBoxReturnValue, CertificateTrustDialogOptions } from 'electron/main';
import type {
OpenDialogOptions,
OpenDialogReturnValue,
MessageBoxOptions,
SaveDialogOptions,
SaveDialogReturnValue,
MessageBoxReturnValue,
CertificateTrustDialogOptions
} from 'electron/main';
const dialogBinding = process._linkedBinding('electron_browser_dialog');
@@ -60,7 +68,9 @@ const checkAppInitialized = function () {
const setupOpenDialogProperties = (properties: (keyof typeof OpenFileDialogProperties)[]): number => {
let dialogProperties = 0;
for (const property of properties) {
if (Object.hasOwn(OpenFileDialogProperties, property)) { dialogProperties |= OpenFileDialogProperties[property]; }
if (Object.hasOwn(OpenFileDialogProperties, property)) {
dialogProperties |= OpenFileDialogProperties[property];
}
}
return dialogProperties;
};
@@ -68,7 +78,9 @@ const setupOpenDialogProperties = (properties: (keyof typeof OpenFileDialogPrope
const setupSaveDialogProperties = (properties: (keyof typeof SaveFileDialogProperties)[]): number => {
let dialogProperties = 0;
for (const property of properties) {
if (Object.hasOwn(SaveFileDialogProperties, property)) { dialogProperties |= SaveFileDialogProperties[property]; }
if (Object.hasOwn(SaveFileDialogProperties, property)) {
dialogProperties |= SaveFileDialogProperties[property];
}
}
return dialogProperties;
};
@@ -150,7 +162,7 @@ const openDialog = (sync: boolean, window: BaseWindow | null, options?: OpenDial
properties: setupOpenDialogProperties(properties)
};
return (sync) ? dialogBinding.showOpenDialogSync(settings) : dialogBinding.showOpenDialog(settings);
return sync ? dialogBinding.showOpenDialogSync(settings) : dialogBinding.showOpenDialog(settings);
};
const messageBox = (sync: boolean, window: BaseWindow | null, options?: MessageBoxOptions) => {
@@ -194,7 +206,7 @@ const messageBox = (sync: boolean, window: BaseWindow | null, options?: MessageB
// Choose a default button to get selected when dialog is cancelled.
if (cancelId == null) {
// If the defaultId is set to 0, ensure the cancel button is a different index (1)
cancelId = (defaultId === 0 && buttons.length > 1) ? 1 : 0;
cancelId = defaultId === 0 && buttons.length > 1 ? 1 : 0;
for (const [i, button] of buttons.entries()) {
const text = button.toLowerCase();
if (text === 'cancel' || text === 'no') {
@@ -210,7 +222,9 @@ const messageBox = (sync: boolean, window: BaseWindow | null, options?: MessageB
// Generate an ID used for closing the message box.
id = getNextId();
// Close the message box when signal is aborted.
if (signal.aborted) { return Promise.resolve({ cancelId, checkboxChecked }); }
if (signal.aborted) {
return Promise.resolve({ cancelId, checkboxChecked });
}
signal.addEventListener('abort', () => dialogBinding._closeMessageBox(id));
}
@@ -240,59 +254,80 @@ const messageBox = (sync: boolean, window: BaseWindow | null, options?: MessageB
export function showOpenDialog(window: BaseWindow, options: OpenDialogOptions): OpenDialogReturnValue;
export function showOpenDialog(options: OpenDialogOptions): OpenDialogReturnValue;
export function showOpenDialog (windowOrOptions: BaseWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showOpenDialog(
windowOrOptions: BaseWindow | OpenDialogOptions,
maybeOptions?: OpenDialogOptions
): OpenDialogReturnValue {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
return openDialog(false, window, options);
}
export function showOpenDialogSync(window: BaseWindow, options: OpenDialogOptions): OpenDialogReturnValue;
export function showOpenDialogSync(options: OpenDialogOptions): OpenDialogReturnValue;
export function showOpenDialogSync (windowOrOptions: BaseWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showOpenDialogSync(
windowOrOptions: BaseWindow | OpenDialogOptions,
maybeOptions?: OpenDialogOptions
): OpenDialogReturnValue {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
return openDialog(true, window, options);
}
export function showSaveDialog(window: BaseWindow, options: SaveDialogOptions): SaveDialogReturnValue;
export function showSaveDialog(options: SaveDialogOptions): SaveDialogReturnValue;
export function showSaveDialog (windowOrOptions: BaseWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showSaveDialog(
windowOrOptions: BaseWindow | SaveDialogOptions,
maybeOptions?: SaveDialogOptions
): SaveDialogReturnValue {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
return saveDialog(false, window, options);
}
export function showSaveDialogSync(window: BaseWindow, options: SaveDialogOptions): SaveDialogReturnValue;
export function showSaveDialogSync(options: SaveDialogOptions): SaveDialogReturnValue;
export function showSaveDialogSync (windowOrOptions: BaseWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showSaveDialogSync(
windowOrOptions: BaseWindow | SaveDialogOptions,
maybeOptions?: SaveDialogOptions
): SaveDialogReturnValue {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
return saveDialog(true, window, options);
}
export function showMessageBox(window: BaseWindow, options: MessageBoxOptions): MessageBoxReturnValue;
export function showMessageBox(options: MessageBoxOptions): MessageBoxReturnValue;
export function showMessageBox (windowOrOptions: BaseWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showMessageBox(
windowOrOptions: BaseWindow | MessageBoxOptions,
maybeOptions?: MessageBoxOptions
): MessageBoxReturnValue {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
return messageBox(false, window, options);
}
export function showMessageBoxSync(window: BaseWindow, options: MessageBoxOptions): MessageBoxReturnValue;
export function showMessageBoxSync(options: MessageBoxOptions): MessageBoxReturnValue;
export function showMessageBoxSync (windowOrOptions: BaseWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showMessageBoxSync(
windowOrOptions: BaseWindow | MessageBoxOptions,
maybeOptions?: MessageBoxOptions
): MessageBoxReturnValue {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
return messageBox(true, window, options);
}
export function showErrorBox (...args: any[]) {
export function showErrorBox(...args: any[]) {
return dialogBinding.showErrorBox(...args);
}
export function showCertificateTrustDialog (windowOrOptions: BaseWindow | CertificateTrustDialogOptions, maybeOptions?: CertificateTrustDialogOptions) {
const window = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions);
const options = (windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions);
export function showCertificateTrustDialog(
windowOrOptions: BaseWindow | CertificateTrustDialogOptions,
maybeOptions?: CertificateTrustDialogOptions
) {
const window = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? null : windowOrOptions;
const options = windowOrOptions && !(windowOrOptions instanceof BaseWindow) ? windowOrOptions : maybeOptions;
if (options == null || typeof options !== 'object') {
throw new TypeError('options must be an object');

View File

@@ -4,8 +4,12 @@ let _inAppPurchase;
if (process.platform === 'darwin') {
const { inAppPurchase } = process._linkedBinding('electron_browser_in_app_purchase');
const _purchase = inAppPurchase.purchaseProduct as (productID: string, quantity?: number, username?: string) => Promise<boolean>;
inAppPurchase.purchaseProduct = (productID: string, opts?: number | { quantity?: number, username?: string }) => {
const _purchase = inAppPurchase.purchaseProduct as (
productID: string,
quantity?: number,
username?: string
) => Promise<boolean>;
inAppPurchase.purchaseProduct = (productID: string, opts?: number | { quantity?: number; username?: string }) => {
const quantity = typeof opts === 'object' ? opts.quantity : opts;
const username = typeof opts === 'object' ? opts.username : undefined;
return _purchase.apply(inAppPurchase, [productID, quantity, username]);

View File

@@ -1,20 +1,66 @@
import { app, BaseWindow, BrowserWindow, session, webContents, WebContents, MenuItemConstructorOptions } from 'electron/main';
import {
app,
BaseWindow,
BrowserWindow,
session,
webContents,
WebContents,
MenuItemConstructorOptions
} from 'electron/main';
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const isLinux = process.platform === 'linux';
type RoleId = 'about' | 'close' | 'copy' | 'cut' | 'delete' | 'forcereload' | 'front' | 'help' | 'hide' | 'hideothers' | 'minimize' |
'paste' | 'pasteandmatchstyle' | 'quit' | 'redo' | 'reload' | 'resetzoom' | 'selectall' | 'services' | 'recentdocuments' | 'clearrecentdocuments' |
'showsubstitutions' | 'togglesmartquotes' | 'togglesmartdashes' | 'toggletextreplacement' | 'startspeaking' | 'stopspeaking' |
'toggledevtools' | 'togglefullscreen' | 'undo' | 'unhide' | 'window' | 'zoom' | 'zoomin' | 'zoomout' | 'togglespellchecker' |
'appmenu' | 'filemenu' | 'editmenu' | 'viewmenu' | 'windowmenu' | 'sharemenu'
type RoleId =
| 'about'
| 'close'
| 'copy'
| 'cut'
| 'delete'
| 'forcereload'
| 'front'
| 'help'
| 'hide'
| 'hideothers'
| 'minimize'
| 'paste'
| 'pasteandmatchstyle'
| 'quit'
| 'redo'
| 'reload'
| 'resetzoom'
| 'selectall'
| 'services'
| 'recentdocuments'
| 'clearrecentdocuments'
| 'showsubstitutions'
| 'togglesmartquotes'
| 'togglesmartdashes'
| 'toggletextreplacement'
| 'startspeaking'
| 'stopspeaking'
| 'toggledevtools'
| 'togglefullscreen'
| 'undo'
| 'unhide'
| 'window'
| 'zoom'
| 'zoomin'
| 'zoomout'
| 'togglespellchecker'
| 'appmenu'
| 'filemenu'
| 'editmenu'
| 'viewmenu'
| 'windowmenu'
| 'sharemenu';
interface Role {
label: string;
accelerator?: string;
checked?: boolean;
windowMethod?: ((window: BaseWindow) => void);
webContentsMethod?: ((webContents: WebContents) => void);
windowMethod?: (window: BaseWindow) => void;
webContentsMethod?: (webContents: WebContents) => void;
appMethod?: () => void;
registerAccelerator?: boolean;
nonNativeMacOSRole?: boolean;
@@ -23,7 +69,7 @@ interface Role {
export const roleList: Record<RoleId, Role> = {
about: {
get label () {
get label() {
return isLinux ? 'About' : `About ${app.name}`;
},
...((isWindows || isLinux) && { appMethod: () => app.showAboutPanel() })
@@ -31,23 +77,23 @@ export const roleList: Record<RoleId, Role> = {
close: {
label: isMac ? 'Close Window' : 'Close',
accelerator: 'CommandOrControl+W',
windowMethod: w => w.close()
windowMethod: (w) => w.close()
},
copy: {
label: 'Copy',
accelerator: 'CommandOrControl+C',
webContentsMethod: wc => wc.copy(),
webContentsMethod: (wc) => wc.copy(),
registerAccelerator: false
},
cut: {
label: 'Cut',
accelerator: 'CommandOrControl+X',
webContentsMethod: wc => wc.cut(),
webContentsMethod: (wc) => wc.cut(),
registerAccelerator: false
},
delete: {
label: 'Delete',
webContentsMethod: wc => wc.delete()
webContentsMethod: (wc) => wc.delete()
},
forcereload: {
label: 'Force Reload',
@@ -66,7 +112,7 @@ export const roleList: Record<RoleId, Role> = {
label: 'Help'
},
hide: {
get label () {
get label() {
return `Hide ${app.name}`;
},
accelerator: 'Command+H'
@@ -78,28 +124,31 @@ export const roleList: Record<RoleId, Role> = {
minimize: {
label: 'Minimize',
accelerator: 'CommandOrControl+M',
windowMethod: w => {
windowMethod: (w) => {
if (w.minimizable) w.minimize();
}
},
paste: {
label: 'Paste',
accelerator: 'CommandOrControl+V',
webContentsMethod: wc => wc.paste(),
webContentsMethod: (wc) => wc.paste(),
registerAccelerator: false
},
pasteandmatchstyle: {
label: 'Paste and Match Style',
accelerator: isMac ? 'Cmd+Option+Shift+V' : 'Shift+CommandOrControl+V',
webContentsMethod: wc => wc.pasteAndMatchStyle(),
webContentsMethod: (wc) => wc.pasteAndMatchStyle(),
registerAccelerator: false
},
quit: {
get label () {
get label() {
switch (process.platform) {
case 'darwin': return `Quit ${app.name}`;
case 'win32': return 'Exit';
default: return 'Quit';
case 'darwin':
return `Quit ${app.name}`;
case 'win32':
return 'Exit';
default:
return 'Quit';
}
},
accelerator: isWindows ? undefined : 'CommandOrControl+Q',
@@ -108,7 +157,7 @@ export const roleList: Record<RoleId, Role> = {
redo: {
label: 'Redo',
accelerator: isWindows ? 'Control+Y' : 'Shift+CommandOrControl+Z',
webContentsMethod: wc => wc.redo()
webContentsMethod: (wc) => wc.redo()
},
reload: {
label: 'Reload',
@@ -131,7 +180,7 @@ export const roleList: Record<RoleId, Role> = {
selectall: {
label: 'Select All',
accelerator: 'CommandOrControl+A',
webContentsMethod: wc => wc.selectAll()
webContentsMethod: (wc) => wc.selectAll()
},
services: {
label: 'Services'
@@ -164,7 +213,7 @@ export const roleList: Record<RoleId, Role> = {
label: 'Toggle Developer Tools',
accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I',
nonNativeMacOSRole: true,
webContentsMethod: wc => {
webContentsMethod: (wc) => {
const bw = wc.getOwnerBrowserWindow();
if (bw) bw.webContents.toggleDevTools();
}
@@ -179,7 +228,7 @@ export const roleList: Record<RoleId, Role> = {
undo: {
label: 'Undo',
accelerator: 'CommandOrControl+Z',
webContentsMethod: wc => wc.undo()
webContentsMethod: (wc) => wc.undo()
},
unhide: {
label: 'Show All'
@@ -208,7 +257,7 @@ export const roleList: Record<RoleId, Role> = {
},
togglespellchecker: {
label: 'Check Spelling While Typing',
get checked () {
get checked() {
const wc = webContents.getFocusedWebContents();
const ses = wc ? wc.session : session.defaultSession;
return ses.spellCheckerEnabled;
@@ -221,7 +270,7 @@ export const roleList: Record<RoleId, Role> = {
},
// App submenu should be used for Mac only
appmenu: {
get label () {
get label() {
return app.name;
},
submenu: [
@@ -239,9 +288,7 @@ export const roleList: Record<RoleId, Role> = {
// File submenu
filemenu: {
label: 'File',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
submenu: [isMac ? { role: 'close' } : { role: 'quit' }]
},
// Edit submenu
editmenu: {
@@ -254,34 +301,27 @@ export const roleList: Record<RoleId, Role> = {
{ role: 'copy' },
{ role: 'paste' },
...(isMac
? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Substitutions',
submenu: [
{ role: 'showSubstitutions' },
{ type: 'separator' },
{ role: 'toggleSmartQuotes' },
{ role: 'toggleSmartDashes' },
{ role: 'toggleTextReplacement' }
]
},
{
label: 'Speech',
submenu: [
{ role: 'startSpeaking' },
{ role: 'stopSpeaking' }
]
}
] as MenuItemConstructorOptions[]
: [
{ role: 'delete' },
{ type: 'separator' },
{ role: 'selectAll' }
] as MenuItemConstructorOptions[])
? ([
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Substitutions',
submenu: [
{ role: 'showSubstitutions' },
{ type: 'separator' },
{ role: 'toggleSmartQuotes' },
{ role: 'toggleSmartDashes' },
{ role: 'toggleTextReplacement' }
]
},
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }]
}
] as MenuItemConstructorOptions[])
: ([{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }] as MenuItemConstructorOptions[]))
]
},
// View submenu
@@ -306,13 +346,8 @@ export const roleList: Record<RoleId, Role> = {
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac
? [
{ type: 'separator' },
{ role: 'front' }
] as MenuItemConstructorOptions[]
: [
{ role: 'close' }
] as MenuItemConstructorOptions[])
? ([{ type: 'separator' }, { role: 'front' }] as MenuItemConstructorOptions[])
: ([{ role: 'close' }] as MenuItemConstructorOptions[]))
]
},
// Share submenu
@@ -334,34 +369,34 @@ const canExecuteRole = (role: keyof typeof roleList) => {
return roleList[role].nonNativeMacOSRole;
};
export function getDefaultType (role: RoleId) {
export function getDefaultType(role: RoleId) {
if (shouldOverrideCheckStatus(role)) return 'checkbox';
return 'normal';
}
export function getDefaultLabel (role: RoleId) {
export function getDefaultLabel(role: RoleId) {
return hasRole(role) ? roleList[role].label : '';
}
export function getCheckStatus (role: RoleId) {
export function getCheckStatus(role: RoleId) {
if (hasRole(role)) return roleList[role].checked;
}
export function shouldOverrideCheckStatus (role: RoleId) {
export function shouldOverrideCheckStatus(role: RoleId) {
return hasRole(role) && Object.hasOwn(roleList[role], 'checked');
}
export function getDefaultAccelerator (role: RoleId) {
export function getDefaultAccelerator(role: RoleId) {
if (hasRole(role)) return roleList[role].accelerator;
return undefined;
}
export function shouldRegisterAccelerator (role: RoleId) {
export function shouldRegisterAccelerator(role: RoleId) {
const hasRoleRegister = hasRole(role) && roleList[role].registerAccelerator !== undefined;
return hasRoleRegister ? roleList[role].registerAccelerator : true;
}
export function getDefaultSubmenu (role: RoleId) {
export function getDefaultSubmenu(role: RoleId) {
if (!hasRole(role)) return;
let { submenu } = roleList[role];
@@ -374,7 +409,7 @@ export function getDefaultSubmenu (role: RoleId) {
return submenu;
}
export function execute (role: RoleId, focusedWindow: BaseWindow, focusedWebContents: WebContents) {
export function execute(role: RoleId, focusedWindow: BaseWindow, focusedWebContents: WebContents) {
if (!canExecuteRole(role)) return false;
const { appMethod, webContentsMethod, windowMethod } = roleList[role];

View File

@@ -56,8 +56,7 @@ const MenuItem = function (this: any, options: any) {
const click = options.click;
this.click = (event: KeyboardEvent, focusedWindow: BaseWindow, focusedWebContents: WebContents) => {
// Manually flip the checked flags when clicked.
if (!roles.shouldOverrideCheckStatus(this.role) &&
(this.type === 'checkbox' || this.type === 'radio')) {
if (!roles.shouldOverrideCheckStatus(this.role) && (this.type === 'checkbox' || this.type === 'radio')) {
this.checked = !this.checked;
}

View File

@@ -1,13 +1,16 @@
function splitArray<T> (arr: T[], predicate: (x: T) => boolean) {
const result = arr.reduce((multi, item) => {
const current = multi[multi.length - 1];
if (predicate(item)) {
if (current.length > 0) multi.push([]);
} else {
current.push(item);
}
return multi;
}, [[]] as T[][]);
function splitArray<T>(arr: T[], predicate: (x: T) => boolean) {
const result = arr.reduce(
(multi, item) => {
const current = multi[multi.length - 1];
if (predicate(item)) {
if (current.length > 0) multi.push([]);
} else {
current.push(item);
}
return multi;
},
[[]] as T[][]
);
if (result[result.length - 1].length === 0) {
return result.slice(0, result.length - 1);
@@ -15,7 +18,7 @@ function splitArray<T> (arr: T[], predicate: (x: T) => boolean) {
return result;
}
function joinArrays (arrays: any[][], joinIDs: any[]) {
function joinArrays(arrays: any[][], joinIDs: any[]) {
return arrays.reduce((joined, arr, i) => {
if (i > 0 && arr.length) {
if (joinIDs.length > 0) {
@@ -29,26 +32,23 @@ function joinArrays (arrays: any[][], joinIDs: any[]) {
}, []);
}
function pushOntoMultiMap<K, V> (map: Map<K, V[]>, key: K, value: V) {
function pushOntoMultiMap<K, V>(map: Map<K, V[]>, key: K, value: V) {
if (!map.has(key)) {
map.set(key, []);
}
map.get(key)!.push(value);
}
function indexOfGroupContainingID<T> (groups: {id?: T}[][], id: T, ignoreGroup: {id?: T}[]) {
function indexOfGroupContainingID<T>(groups: { id?: T }[][], id: T, ignoreGroup: { id?: T }[]) {
return groups.findIndex(
candidateGroup =>
candidateGroup !== ignoreGroup &&
candidateGroup.some(
candidateItem => candidateItem.id === id
)
(candidateGroup) =>
candidateGroup !== ignoreGroup && candidateGroup.some((candidateItem) => candidateItem.id === id)
);
}
// Sort nodes topologically using a depth-first approach. Encountered cycles
// are broken.
function sortTopologically<T> (originalOrder: T[], edgesById: Map<T, T[]>) {
function sortTopologically<T>(originalOrder: T[], edgesById: Map<T, T[]>) {
const sorted = [] as T[];
const marked = new Set<T>();
@@ -71,7 +71,7 @@ function sortTopologically<T> (originalOrder: T[], edgesById: Map<T, T[]>) {
return sorted;
}
function attemptToMergeAGroup<T> (groups: {before?: T[], after?: T[], id?: T}[][]) {
function attemptToMergeAGroup<T>(groups: { before?: T[]; after?: T[]; id?: T }[][]) {
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
for (const item of group) {
@@ -90,7 +90,7 @@ function attemptToMergeAGroup<T> (groups: {before?: T[], after?: T[], id?: T}[][
return false;
}
function mergeGroups<T> (groups: {before?: T[], after?: T[], id?: T}[][]) {
function mergeGroups<T>(groups: { before?: T[]; after?: T[]; id?: T }[][]) {
let merged = true;
while (merged) {
merged = attemptToMergeAGroup(groups);
@@ -98,7 +98,7 @@ function mergeGroups<T> (groups: {before?: T[], after?: T[], id?: T}[][]) {
return groups;
}
function sortItemsInGroup<T> (group: {before?: T[], after?: T[], id?: T}[]) {
function sortItemsInGroup<T>(group: { before?: T[]; after?: T[]; id?: T }[]) {
const originalOrder = group.map((node, i) => i);
const edges = new Map();
const idToIndex = new Map(group.map((item, i) => [item.id, i]));
@@ -123,10 +123,14 @@ function sortItemsInGroup<T> (group: {before?: T[], after?: T[], id?: T}[]) {
}
const sortedNodes = sortTopologically(originalOrder, edges);
return sortedNodes.map(i => group[i]);
return sortedNodes.map((i) => group[i]);
}
function findEdgesInGroup<T> (groups: {beforeGroupContaining?: T[], afterGroupContaining?: T[], id?: T}[][], i: number, edges: Map<any, any>) {
function findEdgesInGroup<T>(
groups: { beforeGroupContaining?: T[]; afterGroupContaining?: T[]; id?: T }[][],
i: number,
edges: Map<any, any>
) {
const group = groups[i];
for (const item of group) {
if (item.beforeGroupContaining) {
@@ -150,7 +154,7 @@ function findEdgesInGroup<T> (groups: {beforeGroupContaining?: T[], afterGroupCo
}
}
function sortGroups<T> (groups: {id?: T}[][]) {
function sortGroups<T>(groups: { id?: T }[][]) {
const originalOrder = groups.map((item, i) => i);
const edges = new Map();
@@ -159,13 +163,15 @@ function sortGroups<T> (groups: {id?: T}[][]) {
}
const sortedGroupIndexes = sortTopologically(originalOrder, edges);
return sortedGroupIndexes.map(i => groups[i]);
return sortedGroupIndexes.map((i) => groups[i]);
}
export function sortMenuItems (menuItems: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[]) {
export function sortMenuItems(menuItems: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[]) {
const isSeparator = (i: Electron.MenuItemConstructorOptions | Electron.MenuItem) => {
const opts = i as Electron.MenuItemConstructorOptions;
return i.type === 'separator' && !opts.before && !opts.after && !opts.beforeGroupContaining && !opts.afterGroupContaining;
return (
i.type === 'separator' && !opts.before && !opts.after && !opts.beforeGroupContaining && !opts.afterGroupContaining
);
};
const separators = menuItems.filter(isSeparator);

View File

@@ -92,7 +92,7 @@ Menu.prototype._executeCommand = function (event, id) {
Menu.prototype._menuWillShow = function () {
// Ensure radio groups have at least one menu item selected
for (const id of Object.keys(this.groupsMap)) {
const found = this.groupsMap[id].find(item => item.checked) || null;
const found = this.groupsMap[id].find((item) => item.checked) || null;
if (!found) checked.set(this.groupsMap[id][0], true);
}
};
@@ -141,7 +141,7 @@ Menu.prototype.closePopup = function (window) {
Menu.prototype.getMenuItemById = function (id) {
const items = this.items;
let found = items.find(item => item.id === id) || null;
let found = items.find((item) => item.id === id) || null;
for (let i = 0; !found && i < items.length; i++) {
const { submenu } = items[i];
if (submenu) {
@@ -213,7 +213,7 @@ Menu.setApplicationMenu = function (menu: MenuType) {
bindings.setApplicationMenu(menu);
} else {
const windows = BaseWindow.getAllWindows();
windows.map(w => w.setMenu(menu));
windows.map((w) => w.setMenu(menu));
}
};
@@ -244,14 +244,16 @@ Menu.buildFromTemplate = function (template) {
/* Helper Functions */
// validate the template against having the wrong attribute
function areValidTemplateItems (template: (MenuItemConstructorOptions | MenuItem)[]) {
return template.every(item =>
item != null &&
typeof item === 'object' &&
(Object.hasOwn(item, 'label') || Object.hasOwn(item, 'role') || item.type === 'separator'));
function areValidTemplateItems(template: (MenuItemConstructorOptions | MenuItem)[]) {
return template.every(
(item) =>
item != null &&
typeof item === 'object' &&
(Object.hasOwn(item, 'label') || Object.hasOwn(item, 'role') || item.type === 'separator')
);
}
function sortTemplate (template: (MenuItemConstructorOptions | MenuItem)[]) {
function sortTemplate(template: (MenuItemConstructorOptions | MenuItem)[]) {
const sorted = sortMenuItems(template);
for (const item of sorted) {
if (Array.isArray(item.submenu)) {
@@ -262,7 +264,7 @@ function sortTemplate (template: (MenuItemConstructorOptions | MenuItem)[]) {
}
// Search between separators to find a radio menu item and return its group id
function generateGroupId (items: (MenuItemConstructorOptions | MenuItem)[], pos: number) {
function generateGroupId(items: (MenuItemConstructorOptions | MenuItem)[], pos: number) {
if (pos > 0) {
for (let idx = pos - 1; idx >= 0; idx--) {
if (items[idx].type === 'radio') return (items[idx] as MenuItem).groupId;
@@ -278,7 +280,7 @@ function generateGroupId (items: (MenuItemConstructorOptions | MenuItem)[], pos:
return groupIdIndex;
}
function removeExtraSeparators (items: (MenuItemConstructorOptions | MenuItem)[]) {
function removeExtraSeparators(items: (MenuItemConstructorOptions | MenuItem)[]) {
// fold adjacent separators together
let ret = items.filter((e, idx, arr) => {
if (e.visible === false) return true;
@@ -294,7 +296,7 @@ function removeExtraSeparators (items: (MenuItemConstructorOptions | MenuItem)[]
return ret;
}
function insertItemByType (this: MenuType, item: MenuItem, pos: number) {
function insertItemByType(this: MenuType, item: MenuItem, pos: number) {
const types = {
normal: () => this.insertItem(pos, item.commandId, item.label),
header: () => this.insertItem(pos, item.commandId, item.label),

View File

@@ -7,7 +7,7 @@ const { createPair } = process._linkedBinding('electron_browser_message_port');
export default class MessageChannelMain extends EventEmitter implements Electron.MessageChannelMain {
port1: MessagePortMain;
port2: MessagePortMain;
constructor () {
constructor() {
super();
const { port1, port2 } = createPair();
this.port1 = new MessagePortMain(port1);

View File

@@ -4,7 +4,11 @@ import { ClientRequestConstructorOptions, ClientRequest, IncomingMessage, Sessio
import { Readable, Writable, isReadable } from 'stream';
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
function createDeferredPromise<T, E extends Error = Error>(): {
promise: Promise<T>;
resolve: (x: T) => void;
reject: (e: E) => void;
} {
let res: (x: T) => void;
let rej: (e: E) => void;
const promise = new Promise<T>((resolve, reject) => {
@@ -15,8 +19,12 @@ function createDeferredPromise<T, E extends Error = Error> (): { promise: Promis
return { promise, resolve: res!, reject: rej! };
}
export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypassCustomProtocolHandlers?: boolean}) | undefined, session: SessionT | undefined,
request: (options: ClientRequestConstructorOptions | string) => ClientRequest) {
export function fetchWithSession(
input: RequestInfo,
init: (RequestInit & { bypassCustomProtocolHandlers?: boolean }) | undefined,
session: SessionT | undefined,
request: (options: ClientRequestConstructorOptions | string) => ClientRequest
) {
const p = createDeferredPromise<Response>();
let req: Request;
try {
@@ -76,16 +84,18 @@ export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypa
// We can't set credentials to same-origin unless there's an origin set.
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
const r = request(allowAnyProtocol({
session,
method: req.method,
url: req.url,
origin,
credentials,
cache: req.cache,
referrerPolicy: req.referrerPolicy,
redirect: req.redirect
}));
const r = request(
allowAnyProtocol({
session,
method: req.method,
url: req.url,
origin,
credentials,
cache: req.cache,
referrerPolicy: req.referrerPolicy,
redirect: req.redirect
})
);
(r as any)._urlLoaderOptions.bypassCustomProtocolHandlers = !!init?.bypassCustomProtocolHandlers;
@@ -105,7 +115,10 @@ export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypa
headers.set(k, Array.isArray(v) ? v.join(', ') : v);
}
const nullBodyStatus = [101, 204, 205, 304];
const body = nullBodyStatus.includes(resp.statusCode) || req.method === 'HEAD' ? null : Readable.toWeb(resp as unknown as Readable) as ReadableStream;
const body =
nullBodyStatus.includes(resp.statusCode) || req.method === 'HEAD'
? null
: (Readable.toWeb(resp as unknown as Readable) as ReadableStream);
const rResp = new Response(body, {
headers,
status: resp.statusCode,
@@ -122,7 +135,9 @@ export function fetchWithSession (input: RequestInfo, init: (RequestInit & {bypa
// pipeTo expects a WritableStream<Uint8Array>. Node.js' Writable.toWeb returns WritableStream<any>,
// which causes a TS structural mismatch.
const writable = Writable.toWeb(r as unknown as Writable) as unknown as WritableStream<Uint8Array>;
if (!req.body?.pipeTo(writable).then(() => r.end())) { r.end(); }
if (!req.body?.pipeTo(writable).then(() => r.end())) {
r.end();
}
return p.promise;
}

View File

@@ -15,7 +15,7 @@ const stopLogging: typeof session.defaultSession.netLog.stopLogging = async () =
export default {
startLogging,
stopLogging,
get currentlyLogging (): boolean {
get currentlyLogging(): boolean {
if (!app.isReady()) return false;
return session.defaultSession.netLog.currentlyLogging;
}

View File

@@ -5,18 +5,21 @@ import type { ClientRequestConstructorOptions } from 'electron/main';
const { isOnline } = process._linkedBinding('electron_common_net');
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
export function request(
options: ClientRequestConstructorOptions | string,
callback?: (message: IncomingMessage) => void
) {
if (!app.isReady()) {
throw new Error('net module can only be used after app is ready');
}
return new ClientRequest(options, callback);
}
export function fetch (input: RequestInfo, init?: RequestInit): Promise<Response> {
export function fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
return session.defaultSession.fetch(input, init);
}
export function resolveHost (host: string, options?: Electron.ResolveHostOptions): Promise<Electron.ResolvedHost> {
export function resolveHost(host: string, options?: Electron.ResolveHostOptions): Promise<Electron.ResolvedHost> {
return session.defaultSession.resolveHost(host, options);
}

View File

@@ -1,12 +1,7 @@
import { EventEmitter } from 'events';
const {
createPowerMonitor,
getSystemIdleState,
getSystemIdleTime,
getCurrentThermalState,
isOnBatteryPower
} = process._linkedBinding('electron_browser_power_monitor');
const { createPowerMonitor, getSystemIdleState, getSystemIdleTime, getCurrentThermalState, isOnBatteryPower } =
process._linkedBinding('electron_browser_power_monitor');
// Hold the native PowerMonitor at module level so it is never garbage-collected
// while this module is alive. The C++ side registers OS-level callbacks (HWND
@@ -15,7 +10,7 @@ const {
let pm: any;
class PowerMonitor extends EventEmitter implements Electron.PowerMonitor {
constructor () {
constructor() {
super();
// Don't start the event source until both a) the app is ready and b)
// there's a listener registered for a powerMonitor event.
@@ -43,23 +38,23 @@ class PowerMonitor extends EventEmitter implements Electron.PowerMonitor {
});
}
getSystemIdleState (idleThreshold: number) {
getSystemIdleState(idleThreshold: number) {
return getSystemIdleState(idleThreshold);
}
getCurrentThermalState () {
getCurrentThermalState() {
return getCurrentThermalState();
}
getSystemIdleTime () {
getSystemIdleTime() {
return getSystemIdleTime();
}
isOnBatteryPower () {
isOnBatteryPower() {
return isOnBatteryPower();
}
get onBatteryPower () {
get onBatteryPower() {
return this.isOnBatteryPower();
}
}

View File

@@ -7,17 +7,18 @@ import { ReadableStream } from 'stream/web';
import type { ReadableStreamDefaultReader } from 'stream/web';
// Global protocol APIs.
const { registerSchemesAsPrivileged, getStandardSchemes, Protocol } = process._linkedBinding('electron_browser_protocol');
const { registerSchemesAsPrivileged, getStandardSchemes, Protocol } =
process._linkedBinding('electron_browser_protocol');
const ERR_FAILED = -2;
const ERR_UNEXPECTED = -9;
const isBuiltInScheme = (scheme: string) => ['http', 'https', 'file'].includes(scheme);
function makeStreamFromPipe (pipe: any): ReadableStream<Uint8Array> {
function makeStreamFromPipe(pipe: any): ReadableStream<Uint8Array> {
const buf = new Uint8Array(1024 * 1024 /* 1 MB */);
return new ReadableStream({
async pull (controller) {
async pull(controller) {
try {
const rv = await pipe.read(buf);
if (rv > 0) {
@@ -32,7 +33,7 @@ function makeStreamFromPipe (pipe: any): ReadableStream<Uint8Array> {
});
}
function makeStreamFromFileInfo ({
function makeStreamFromFileInfo({
filePath,
offset = 0,
length = -1
@@ -42,13 +43,15 @@ function makeStreamFromFileInfo ({
length?: number;
}): ReadableStream<Uint8Array> {
// Node's Readable.toWeb produces a WHATWG ReadableStream whose chunks are Uint8Array.
return Readable.toWeb(createReadStream(filePath, {
start: offset,
end: length >= 0 ? offset + length : undefined
})) as ReadableStream<Uint8Array>;
return Readable.toWeb(
createReadStream(filePath, {
start: offset,
end: length >= 0 ? offset + length : undefined
})
) as ReadableStream<Uint8Array>;
}
function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): RequestInit['body'] {
function convertToRequestBody(uploadData: ProtocolRequest['uploadData']): RequestInit['body'] {
if (!uploadData) return null;
// Optimization: skip creating a stream if the request is just a single buffer.
if (uploadData.length === 1 && (uploadData[0] as any).type === 'rawData') {
@@ -60,7 +63,7 @@ function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): Reque
// Generic <Uint8Array> ensures reader.read() returns value?: Uint8Array consistent with enqueue.
let current: ReadableStreamDefaultReader<Uint8Array> | null = null;
return new ReadableStream<Uint8Array>({
async pull (controller) {
async pull(controller) {
if (current) {
const { done, value } = await current.read();
// (done => value === undefined) as per WHATWG spec
@@ -71,7 +74,9 @@ function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): Reque
controller.enqueue(value);
}
} else {
if (!chunks.length) { return controller.close(); }
if (!chunks.length) {
return controller.close();
}
const chunk = chunks.shift()!;
if (chunk.type === 'rawData') {
controller.enqueue(chunk.bytes as Uint8Array);
@@ -96,7 +101,7 @@ function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): Reque
}) as RequestInit['body'];
}
function validateResponse (res: Response) {
function validateResponse(res: Response) {
if (!res || typeof res !== 'object') return false;
if (res.type === 'error') return true;
@@ -115,7 +120,11 @@ function validateResponse (res: Response) {
return true;
}
Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, handler: (req: Request) => Response | Promise<Response>) {
Protocol.prototype.handle = function (
this: Electron.Protocol,
scheme: string,
handler: (req: Request) => Response | Promise<Response>
) {
const register = isBuiltInScheme(scheme) ? this.interceptProtocol : this.registerProtocol;
const success = register.call(this, scheme, async (preq: ProtocolRequest, cb: any) => {
try {
@@ -155,7 +164,9 @@ Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, h
Protocol.prototype.unhandle = function (this: Electron.Protocol, scheme: string) {
const unregister = isBuiltInScheme(scheme) ? this.uninterceptProtocol : this.unregisterProtocol;
if (!unregister.call(this, scheme)) { throw new Error(`Failed to unhandle protocol: ${scheme}`); }
if (!unregister.call(this, scheme)) {
throw new Error(`Failed to unhandle protocol: ${scheme}`);
}
};
Protocol.prototype.isProtocolHandled = function (this: Electron.Protocol, scheme: string) {

View File

@@ -14,35 +14,38 @@ const createScreenIfNeeded = () => {
// exposes an instance created by createScreen. In order to avoid
// side-effecting and calling createScreen upon import of this module, instead
// we export a proxy which lazily calls createScreen on first access.
export default new Proxy({}, {
get: (target, property: keyof Electron.Screen) => {
createScreenIfNeeded();
const value = _screen[property];
if (typeof value === 'function') {
return value.bind(_screen);
export default new Proxy(
{},
{
get: (target, property: keyof Electron.Screen) => {
createScreenIfNeeded();
const value = _screen[property];
if (typeof value === 'function') {
return value.bind(_screen);
}
return value;
},
set: (target, property: string, value: unknown) => {
createScreenIfNeeded();
return Reflect.set(_screen, property, value);
},
ownKeys: () => {
createScreenIfNeeded();
return Reflect.ownKeys(_screen);
},
has: (target, property: string) => {
createScreenIfNeeded();
return property in _screen;
},
getOwnPropertyDescriptor: (target, property: string) => {
createScreenIfNeeded();
return Reflect.getOwnPropertyDescriptor(_screen, property);
},
getPrototypeOf: () => {
// This is necessary as a result of weirdness with EventEmitterMixin
// and FunctionTemplate - we need to explicitly ensure it's returned
// in the prototype.
return EventEmitter.prototype;
}
return value;
},
set: (target, property: string, value: unknown) => {
createScreenIfNeeded();
return Reflect.set(_screen, property, value);
},
ownKeys: () => {
createScreenIfNeeded();
return Reflect.ownKeys(_screen);
},
has: (target, property: string) => {
createScreenIfNeeded();
return property in _screen;
},
getOwnPropertyDescriptor: (target, property: string) => {
createScreenIfNeeded();
return Reflect.getOwnPropertyDescriptor(_screen, property);
},
getPrototypeOf: () => {
// This is necessary as a result of weirdness with EventEmitterMixin
// and FunctionTemplate - we need to explicitly ensure it's returned
// in the prototype.
return EventEmitter.prototype;
}
});
);

View File

@@ -3,7 +3,7 @@ import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
const { ServiceWorkerMain } = process._linkedBinding('electron_browser_service_worker_main');
Object.defineProperty(ServiceWorkerMain.prototype, 'ipc', {
get () {
get() {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;

View File

@@ -15,7 +15,7 @@ let fakeVideoWindowId = -1;
const kMacOsNativePickerId = -4;
const systemPickerVideoSource = Object.create(null);
Object.defineProperty(systemPickerVideoSource, 'id', {
get () {
get() {
return `window:${kMacOsNativePickerId}:${fakeVideoWindowId--}`;
}
});
@@ -73,13 +73,18 @@ Session.prototype.setPreloads = function (preloads) {
.forEach((script) => {
this.unregisterPreloadScript(script.id);
});
preloads.map(filePath => ({
type: 'frame',
filePath,
_deprecated: true
}) as Electron.PreloadScriptRegistration).forEach(script => {
this.registerPreloadScript(script);
});
preloads
.map(
(filePath) =>
({
type: 'frame',
filePath,
_deprecated: true
}) as Electron.PreloadScriptRegistration
)
.forEach((script) => {
this.registerPreloadScript(script);
});
};
Session.prototype.getAllExtensions = deprecate.moveAPI(
@@ -114,7 +119,7 @@ Session.prototype.removeExtension = deprecate.moveAPI(
export default {
fromPartition,
fromPath,
get defaultSession () {
get defaultSession() {
return fromPartition('');
}
};

View File

@@ -5,16 +5,16 @@ import { EventEmitter } from 'events';
class ShareMenu extends EventEmitter implements Electron.ShareMenu {
private menu: Menu;
constructor (sharingItem: SharingItem) {
constructor(sharingItem: SharingItem) {
super();
this.menu = new (Menu as any)({ sharingItem });
}
popup (options?: PopupOptions) {
popup(options?: PopupOptions) {
this.menu.popup(options);
}
closePopup (browserWindow?: BrowserWindow) {
closePopup(browserWindow?: BrowserWindow) {
this.menu.closePopup(browserWindow);
}
}

View File

@@ -14,36 +14,41 @@ type SharedTextureImportedWrapper = {
texture: Electron.SharedTextureImported;
allReferencesReleased: AllReleasedCallback | undefined;
mainReference: boolean;
rendererFrameReferences: Map<number, { count: number, reference: Electron.WebFrameMain }>;
}
rendererFrameReferences: Map<number, { count: number; reference: Electron.WebFrameMain }>;
};
ipcMain.handle(IPC_MESSAGES.IMPORT_SHARED_TEXTURE_RELEASE_RENDERER_TO_MAIN, (event: Electron.IpcMainInvokeEvent, textureId: string) => {
const frameTreeNodeId = event.frameTreeNodeId ?? event.sender.mainFrame.frameTreeNodeId;
wrapperReleaseFromRenderer(textureId, frameTreeNodeId);
});
ipcMain.handle(
IPC_MESSAGES.IMPORT_SHARED_TEXTURE_RELEASE_RENDERER_TO_MAIN,
(event: Electron.IpcMainInvokeEvent, textureId: string) => {
const frameTreeNodeId = event.frameTreeNodeId ?? event.sender.mainFrame.frameTreeNodeId;
wrapperReleaseFromRenderer(textureId, frameTreeNodeId);
}
);
let checkManagedSharedTexturesInterval: NodeJS.Timeout | null = null;
function scheduleCheckManagedSharedTextures () {
function scheduleCheckManagedSharedTextures() {
if (checkManagedSharedTexturesInterval === null) {
checkManagedSharedTexturesInterval = setInterval(checkManagedSharedTextures, 1000);
}
}
function unscheduleCheckManagedSharedTextures () {
function unscheduleCheckManagedSharedTextures() {
if (checkManagedSharedTexturesInterval !== null) {
clearInterval(checkManagedSharedTexturesInterval);
checkManagedSharedTexturesInterval = null;
}
}
function checkManagedSharedTextures () {
function checkManagedSharedTextures() {
const texturesToRemoveTracking = new Set<string>();
for (const [, wrapper] of managedSharedTextures) {
for (const [frameTreeNodeId, entry] of wrapper.rendererFrameReferences) {
const frame = entry.reference;
if (!frame || frame.isDestroyed()) {
console.error(`The imported shared texture ${wrapper.texture.textureId} is referenced by a destroyed webContent/webFrameMain, this means a imported shared texture in renderer process is not released before the process is exited. Releasing that dangling reference now.`);
console.error(
`The imported shared texture ${wrapper.texture.textureId} is referenced by a destroyed webContent/webFrameMain, this means a imported shared texture in renderer process is not released before the process is exited. Releasing that dangling reference now.`
);
wrapper.rendererFrameReferences.delete(frameTreeNodeId);
}
}
@@ -65,7 +70,7 @@ function checkManagedSharedTextures () {
}
}
function wrapperReleaseFromRenderer (id: string, frameTreeNodeId: number) {
function wrapperReleaseFromRenderer(id: string, frameTreeNodeId: number) {
const wrapper = managedSharedTextures.get(id);
if (!wrapper) {
throw new Error(`Shared texture with id ${id} not found`);
@@ -92,7 +97,7 @@ function wrapperReleaseFromRenderer (id: string, frameTreeNodeId: number) {
}
}
function wrapperReleaseFromMain (id: string) {
function wrapperReleaseFromMain(id: string) {
const wrapper = managedSharedTextures.get(id);
if (!wrapper) {
throw new Error(`Shared texture with id ${id} not found`);
@@ -108,14 +113,18 @@ function wrapperReleaseFromMain (id: string) {
}
}
async function sendSharedTexture (options: Electron.SendSharedTextureOptions, ...args: any[]) {
async function sendSharedTexture(options: Electron.SendSharedTextureOptions, ...args: any[]) {
const imported = options.importedSharedTexture;
const transfer = imported.subtle.startTransferSharedTexture();
let timeoutHandle: NodeJS.Timeout | null = null;
const timeoutPromise = new Promise<never>((resolve, reject) => {
timeoutHandle = setTimeout(() => {
reject(new Error(`transfer shared texture timed out after ${transferTimeout}ms, ensure you have registered receiver at renderer process.`));
reject(
new Error(
`transfer shared texture timed out after ${transferTimeout}ms, ensure you have registered receiver at renderer process.`
)
);
}, transferTimeout);
});
@@ -124,13 +133,14 @@ async function sendSharedTexture (options: Electron.SendSharedTextureOptions, ..
throw new Error('`frame` should be provided');
}
const invokePromise: Promise<Electron.SharedTextureSyncToken> = ipcMainInternalUtils.invokeInWebFrameMain<Electron.SharedTextureSyncToken>(
targetFrame,
IPC_MESSAGES.IMPORT_SHARED_TEXTURE_TRANSFER_MAIN_TO_RENDERER,
transfer,
imported.textureId,
...args
);
const invokePromise: Promise<Electron.SharedTextureSyncToken> =
ipcMainInternalUtils.invokeInWebFrameMain<Electron.SharedTextureSyncToken>(
targetFrame,
IPC_MESSAGES.IMPORT_SHARED_TEXTURE_TRANSFER_MAIN_TO_RENDERER,
transfer,
imported.textureId,
...args
);
try {
const syncToken = await Promise.race([invokePromise, timeoutPromise]);
@@ -159,7 +169,7 @@ async function sendSharedTexture (options: Electron.SendSharedTextureOptions, ..
scheduleCheckManagedSharedTextures();
}
function importSharedTexture (options: Electron.ImportSharedTextureOptions) {
function importSharedTexture(options: Electron.ImportSharedTextureOptions) {
const id = randomUUID();
const imported = sharedTextureNative.importSharedTexture(Object.assign(options.textureInfo, { id }));
const ret: Electron.SharedTextureImported = {

View File

@@ -10,7 +10,10 @@ if ('getEffectiveAppearance' in systemPreferences) {
}
if ('accessibilityDisplayShouldReduceTransparency' in systemPreferences) {
const reduceTransparencyDeprecated = deprecate.warnOnce('systemPreferences.accessibilityDisplayShouldReduceTransparency', 'nativeTheme.prefersReducedTransparency');
const reduceTransparencyDeprecated = deprecate.warnOnce(
'systemPreferences.accessibilityDisplayShouldReduceTransparency',
'nativeTheme.prefersReducedTransparency'
);
const nativeReduceTransparency = systemPreferences.accessibilityDisplayShouldReduceTransparency;
Object.defineProperty(systemPreferences, 'accessibilityDisplayShouldReduceTransparency', {
get: () => {

View File

@@ -12,41 +12,53 @@ const extendConstructHook = (target: any, hook: Function) => {
};
};
const ImmutableProperty = <T extends TouchBarItem<any>>(def: (config: T extends TouchBarItem<infer C> ? C : never, setInternalProp: <K extends keyof T>(k: K, v: T[K]) => void) => any) => (target: T, propertyKey: keyof T) => {
extendConstructHook(target, function (this: T) {
(this as any)[hiddenProperties][propertyKey] = def((this as any)._config, (k, v) => {
(this as any)[hiddenProperties][k] = v;
const ImmutableProperty =
<T extends TouchBarItem<any>>(
def: (
config: T extends TouchBarItem<infer C> ? C : never,
setInternalProp: <K extends keyof T>(k: K, v: T[K]) => void
) => any
) =>
(target: T, propertyKey: keyof T) => {
extendConstructHook(target, function (this: T) {
(this as any)[hiddenProperties][propertyKey] = def((this as any)._config, (k, v) => {
(this as any)[hiddenProperties][k] = v;
});
});
});
Object.defineProperty(target, propertyKey, {
get: function () {
return this[hiddenProperties][propertyKey];
},
set: function () {
throw new Error(`Cannot override property ${name}`);
},
enumerable: true,
configurable: false
});
};
Object.defineProperty(target, propertyKey, {
get: function () {
return this[hiddenProperties][propertyKey];
},
set: function () {
throw new Error(`Cannot override property ${name}`);
},
enumerable: true,
configurable: false
});
};
const LiveProperty = <T extends TouchBarItem<any>>(def: (config: T extends TouchBarItem<infer C> ? C : never) => any, onMutate?: (self: T, newValue: any) => void) => (target: T, propertyKey: keyof T) => {
extendConstructHook(target, function (this: T) {
(this as any)[hiddenProperties][propertyKey] = def((this as any)._config);
if (onMutate) onMutate((this as any), (this as any)[hiddenProperties][propertyKey]);
});
Object.defineProperty(target, propertyKey, {
get: function () {
return this[hiddenProperties][propertyKey];
},
set: function (value) {
if (onMutate) onMutate((this as any), value);
this[hiddenProperties][propertyKey] = value;
this.emit('change', this);
},
enumerable: true
});
};
const LiveProperty =
<T extends TouchBarItem<any>>(
def: (config: T extends TouchBarItem<infer C> ? C : never) => any,
onMutate?: (self: T, newValue: any) => void
) =>
(target: T, propertyKey: keyof T) => {
extendConstructHook(target, function (this: T) {
(this as any)[hiddenProperties][propertyKey] = def((this as any)._config);
if (onMutate) onMutate(this as any, (this as any)[hiddenProperties][propertyKey]);
});
Object.defineProperty(target, propertyKey, {
get: function () {
return this[hiddenProperties][propertyKey];
},
set: function (value) {
if (onMutate) onMutate(this as any, value);
this[hiddenProperties][propertyKey] = value;
this.emit('change', this);
},
enumerable: true
});
};
abstract class TouchBarItem<ConfigType> extends EventEmitter {
@ImmutableProperty(() => `${nextItemID++}`) id!: string;
@@ -57,17 +69,17 @@ abstract class TouchBarItem<ConfigType> extends EventEmitter {
private _parents: { id: string; type: string }[] = [];
private _config!: ConfigType;
constructor (config: ConfigType) {
constructor(config: ConfigType) {
super();
this._config = this._config || config || {} as ConfigType;
this._config = this._config || config || ({} as ConfigType);
(this as any)[hiddenProperties] = {};
const hook = (this as any)._hook;
if (hook) hook.call(this);
delete (this as any)._hook;
}
public _addParent (item: TouchBarItem<any>) {
const existing = this._parents.some(test => test.id === item.id);
public _addParent(item: TouchBarItem<any>) {
const existing = this._parents.some((test) => test.id === item.id);
if (!existing) {
this._parents.push({
id: item.id,
@@ -76,211 +88,246 @@ abstract class TouchBarItem<ConfigType> extends EventEmitter {
}
}
public _removeParent (item: TouchBarItem<any>) {
this._parents = this._parents.filter(test => test.id !== item.id);
public _removeParent(item: TouchBarItem<any>) {
this._parents = this._parents.filter((test) => test.id !== item.id);
}
}
class TouchBarButton extends TouchBarItem<Electron.TouchBarButtonConstructorOptions> implements Electron.TouchBarButton {
class TouchBarButton
extends TouchBarItem<Electron.TouchBarButtonConstructorOptions>
implements Electron.TouchBarButton
{
@ImmutableProperty(() => 'button')
type!: string;
type!: string;
@LiveProperty<TouchBarButton>(config => config.label)
label!: string;
@LiveProperty<TouchBarButton>((config) => config.label)
label!: string;
@LiveProperty<TouchBarButton>(config => config.accessibilityLabel)
accessibilityLabel!: string;
@LiveProperty<TouchBarButton>((config) => config.accessibilityLabel)
accessibilityLabel!: string;
@LiveProperty<TouchBarButton>(config => config.backgroundColor)
backgroundColor!: string;
@LiveProperty<TouchBarButton>((config) => config.backgroundColor)
backgroundColor!: string;
@LiveProperty<TouchBarButton>(config => config.icon)
icon!: Electron.NativeImage;
@LiveProperty<TouchBarButton>((config) => config.icon)
icon!: Electron.NativeImage;
@LiveProperty<TouchBarButton>(config => config.iconPosition)
iconPosition!: Electron.TouchBarButton['iconPosition'];
@LiveProperty<TouchBarButton>((config) => config.iconPosition)
iconPosition!: Electron.TouchBarButton['iconPosition'];
@LiveProperty<TouchBarButton>(config => typeof config.enabled !== 'boolean' ? true : config.enabled)
enabled!: boolean;
@LiveProperty<TouchBarButton>((config) => (typeof config.enabled !== 'boolean' ? true : config.enabled))
enabled!: boolean;
@ImmutableProperty<TouchBarButton>(({ click: onClick }) => typeof onClick === 'function' ? () => onClick() : null)
onInteraction!: Function | null;
@ImmutableProperty<TouchBarButton>(({ click: onClick }) => (typeof onClick === 'function' ? () => onClick() : null))
onInteraction!: Function | null;
}
class TouchBarColorPicker extends TouchBarItem<Electron.TouchBarColorPickerConstructorOptions> implements Electron.TouchBarColorPicker {
class TouchBarColorPicker
extends TouchBarItem<Electron.TouchBarColorPickerConstructorOptions>
implements Electron.TouchBarColorPicker
{
@ImmutableProperty(() => 'colorpicker')
type!: string;
type!: string;
@LiveProperty<TouchBarColorPicker>(config => config.availableColors)
availableColors!: string[];
@LiveProperty<TouchBarColorPicker>((config) => config.availableColors)
availableColors!: string[];
@LiveProperty<TouchBarColorPicker>(config => config.selectedColor)
selectedColor!: string;
@LiveProperty<TouchBarColorPicker>((config) => config.selectedColor)
selectedColor!: string;
@ImmutableProperty<TouchBarColorPicker>(({ change: onChange }, setInternalProp) => typeof onChange === 'function'
? (details: { color: string }) => {
setInternalProp('selectedColor', details.color);
onChange(details.color);
}
: null)
onInteraction!: Function | null;
@ImmutableProperty<TouchBarColorPicker>(({ change: onChange }, setInternalProp) =>
typeof onChange === 'function'
? (details: { color: string }) => {
setInternalProp('selectedColor', details.color);
onChange(details.color);
}
: null
)
onInteraction!: Function | null;
}
class TouchBarGroup extends TouchBarItem<Electron.TouchBarGroupConstructorOptions> implements Electron.TouchBarGroup {
@ImmutableProperty(() => 'group')
type!: string;
type!: string;
@LiveProperty<TouchBarGroup>(config => config.items instanceof TouchBar ? config.items : new TouchBar(config.items), (self, newChild: TouchBar) => {
if (self.child) {
for (const item of self.child.orderedItems) {
item._removeParent(self);
@LiveProperty<TouchBarGroup>(
(config) => (config.items instanceof TouchBar ? config.items : new TouchBar(config.items)),
(self, newChild: TouchBar) => {
if (self.child) {
for (const item of self.child.orderedItems) {
item._removeParent(self);
}
}
for (const item of newChild.orderedItems) {
item._addParent(self);
}
}
for (const item of newChild.orderedItems) {
item._addParent(self);
}
})
child!: TouchBar;
)
child!: TouchBar;
onInteraction = null;
}
class TouchBarLabel extends TouchBarItem<Electron.TouchBarLabelConstructorOptions> implements Electron.TouchBarLabel {
@ImmutableProperty(() => 'label')
type!: string;
type!: string;
@LiveProperty<TouchBarLabel>(config => config.label)
label!: string;
@LiveProperty<TouchBarLabel>((config) => config.label)
label!: string;
@LiveProperty<TouchBarLabel>(config => config.accessibilityLabel)
accessibilityLabel!: string;
@LiveProperty<TouchBarLabel>((config) => config.accessibilityLabel)
accessibilityLabel!: string;
@LiveProperty<TouchBarLabel>(config => config.textColor)
textColor!: string;
@LiveProperty<TouchBarLabel>((config) => config.textColor)
textColor!: string;
onInteraction = null;
}
class TouchBarPopover extends TouchBarItem<Electron.TouchBarPopoverConstructorOptions> implements Electron.TouchBarPopover {
class TouchBarPopover
extends TouchBarItem<Electron.TouchBarPopoverConstructorOptions>
implements Electron.TouchBarPopover
{
@ImmutableProperty(() => 'popover')
type!: string;
type!: string;
@LiveProperty<TouchBarPopover>(config => config.label)
label!: string;
@LiveProperty<TouchBarPopover>((config) => config.label)
label!: string;
@LiveProperty<TouchBarPopover>(config => config.icon)
icon!: Electron.NativeImage;
@LiveProperty<TouchBarPopover>((config) => config.icon)
icon!: Electron.NativeImage;
@LiveProperty<TouchBarPopover>(config => config.showCloseButton)
showCloseButton!: boolean;
@LiveProperty<TouchBarPopover>((config) => config.showCloseButton)
showCloseButton!: boolean;
@LiveProperty<TouchBarPopover>(config => config.items instanceof TouchBar ? config.items : new TouchBar(config.items), (self, newChild: TouchBar) => {
if (self.child) {
for (const item of self.child.orderedItems) {
item._removeParent(self);
}
}
for (const item of newChild.orderedItems) {
item._addParent(self);
}
})
child!: TouchBar;
onInteraction = null;
}
class TouchBarSlider extends TouchBarItem<Electron.TouchBarSliderConstructorOptions> implements Electron.TouchBarSlider {
@ImmutableProperty(() => 'slider')
type!: string;
@LiveProperty<TouchBarSlider>(config => config.label)
label!: string;
@LiveProperty<TouchBarSlider>(config => config.minValue)
minValue!: number;
@LiveProperty<TouchBarSlider>(config => config.maxValue)
maxValue!: number;
@LiveProperty<TouchBarSlider>(config => config.value)
value!: number;
@ImmutableProperty<TouchBarSlider>(({ change: onChange }, setInternalProp) => typeof onChange === 'function'
? (details: { value: number }) => {
setInternalProp('value', details.value);
onChange(details.value);
}
: null)
onInteraction!: Function | null;
}
class TouchBarSpacer extends TouchBarItem<Electron.TouchBarSpacerConstructorOptions> implements Electron.TouchBarSpacer {
@ImmutableProperty(() => 'spacer')
type!: string;
@ImmutableProperty<TouchBarSpacer>(config => config.size)
size!: Electron.TouchBarSpacer['size'];
onInteraction = null;
}
class TouchBarSegmentedControl extends TouchBarItem<Electron.TouchBarSegmentedControlConstructorOptions> implements Electron.TouchBarSegmentedControl {
@ImmutableProperty(() => 'segmented_control')
type!: string;
@LiveProperty<TouchBarSegmentedControl>(config => config.segmentStyle)
segmentStyle!: Electron.TouchBarSegmentedControl['segmentStyle'];
@LiveProperty<TouchBarSegmentedControl>(config => config.segments || [])
segments!: Electron.SegmentedControlSegment[];
@LiveProperty<TouchBarSegmentedControl>(config => config.selectedIndex)
selectedIndex!: number;
@LiveProperty<TouchBarSegmentedControl>(config => config.mode)
mode!: Electron.TouchBarSegmentedControl['mode'];
@ImmutableProperty<TouchBarSegmentedControl>(({ change: onChange }, setInternalProp) => typeof onChange === 'function'
? (details: { selectedIndex: number, isSelected: boolean }) => {
setInternalProp('selectedIndex', details.selectedIndex);
onChange(details.selectedIndex, details.isSelected);
}
: null)
onInteraction!: Function | null;
}
class TouchBarScrubber extends TouchBarItem<Electron.TouchBarScrubberConstructorOptions> implements Electron.TouchBarScrubber {
@ImmutableProperty(() => 'scrubber')
type!: string;
@LiveProperty<TouchBarScrubber>(config => config.items)
items!: Electron.ScrubberItem[];
@LiveProperty<TouchBarScrubber>(config => config.selectedStyle || null)
selectedStyle!: Electron.TouchBarScrubber['selectedStyle'];
@LiveProperty<TouchBarScrubber>(config => config.overlayStyle || null)
overlayStyle!: Electron.TouchBarScrubber['overlayStyle'];
@LiveProperty<TouchBarScrubber>(config => config.showArrowButtons || false)
showArrowButtons!: boolean;
@LiveProperty<TouchBarScrubber>(config => config.mode || 'free')
mode!: Electron.TouchBarScrubber['mode'];
@LiveProperty<TouchBarScrubber>(config => typeof config.continuous === 'undefined' ? true : config.continuous)
continuous!: boolean;
@ImmutableProperty<TouchBarScrubber>(({ select: onSelect, highlight: onHighlight }) => typeof onSelect === 'function' || typeof onHighlight === 'function'
? (details: { type: 'select'; selectedIndex: number } | { type: 'highlight'; highlightedIndex: number }) => {
if (details.type === 'select') {
if (onSelect) onSelect(details.selectedIndex);
} else {
if (onHighlight) onHighlight(details.highlightedIndex);
@LiveProperty<TouchBarPopover>(
(config) => (config.items instanceof TouchBar ? config.items : new TouchBar(config.items)),
(self, newChild: TouchBar) => {
if (self.child) {
for (const item of self.child.orderedItems) {
item._removeParent(self);
}
}
: null)
onInteraction!: Function | null;
for (const item of newChild.orderedItems) {
item._addParent(self);
}
}
)
child!: TouchBar;
onInteraction = null;
}
class TouchBarSlider
extends TouchBarItem<Electron.TouchBarSliderConstructorOptions>
implements Electron.TouchBarSlider
{
@ImmutableProperty(() => 'slider')
type!: string;
@LiveProperty<TouchBarSlider>((config) => config.label)
label!: string;
@LiveProperty<TouchBarSlider>((config) => config.minValue)
minValue!: number;
@LiveProperty<TouchBarSlider>((config) => config.maxValue)
maxValue!: number;
@LiveProperty<TouchBarSlider>((config) => config.value)
value!: number;
@ImmutableProperty<TouchBarSlider>(({ change: onChange }, setInternalProp) =>
typeof onChange === 'function'
? (details: { value: number }) => {
setInternalProp('value', details.value);
onChange(details.value);
}
: null
)
onInteraction!: Function | null;
}
class TouchBarSpacer
extends TouchBarItem<Electron.TouchBarSpacerConstructorOptions>
implements Electron.TouchBarSpacer
{
@ImmutableProperty(() => 'spacer')
type!: string;
@ImmutableProperty<TouchBarSpacer>((config) => config.size)
size!: Electron.TouchBarSpacer['size'];
onInteraction = null;
}
class TouchBarSegmentedControl
extends TouchBarItem<Electron.TouchBarSegmentedControlConstructorOptions>
implements Electron.TouchBarSegmentedControl
{
@ImmutableProperty(() => 'segmented_control')
type!: string;
@LiveProperty<TouchBarSegmentedControl>((config) => config.segmentStyle)
segmentStyle!: Electron.TouchBarSegmentedControl['segmentStyle'];
@LiveProperty<TouchBarSegmentedControl>((config) => config.segments || [])
segments!: Electron.SegmentedControlSegment[];
@LiveProperty<TouchBarSegmentedControl>((config) => config.selectedIndex)
selectedIndex!: number;
@LiveProperty<TouchBarSegmentedControl>((config) => config.mode)
mode!: Electron.TouchBarSegmentedControl['mode'];
@ImmutableProperty<TouchBarSegmentedControl>(({ change: onChange }, setInternalProp) =>
typeof onChange === 'function'
? (details: { selectedIndex: number; isSelected: boolean }) => {
setInternalProp('selectedIndex', details.selectedIndex);
onChange(details.selectedIndex, details.isSelected);
}
: null
)
onInteraction!: Function | null;
}
class TouchBarScrubber
extends TouchBarItem<Electron.TouchBarScrubberConstructorOptions>
implements Electron.TouchBarScrubber
{
@ImmutableProperty(() => 'scrubber')
type!: string;
@LiveProperty<TouchBarScrubber>((config) => config.items)
items!: Electron.ScrubberItem[];
@LiveProperty<TouchBarScrubber>((config) => config.selectedStyle || null)
selectedStyle!: Electron.TouchBarScrubber['selectedStyle'];
@LiveProperty<TouchBarScrubber>((config) => config.overlayStyle || null)
overlayStyle!: Electron.TouchBarScrubber['overlayStyle'];
@LiveProperty<TouchBarScrubber>((config) => config.showArrowButtons || false)
showArrowButtons!: boolean;
@LiveProperty<TouchBarScrubber>((config) => config.mode || 'free')
mode!: Electron.TouchBarScrubber['mode'];
@LiveProperty<TouchBarScrubber>((config) => (typeof config.continuous === 'undefined' ? true : config.continuous))
continuous!: boolean;
@ImmutableProperty<TouchBarScrubber>(({ select: onSelect, highlight: onHighlight }) =>
typeof onSelect === 'function' || typeof onHighlight === 'function'
? (details: { type: 'select'; selectedIndex: number } | { type: 'highlight'; highlightedIndex: number }) => {
if (details.type === 'select') {
if (onSelect) onSelect(details.selectedIndex);
} else {
if (onHighlight) onHighlight(details.highlightedIndex);
}
}
: null
)
onInteraction!: Function | null;
}
class TouchBarOtherItemsProxy extends TouchBarItem<null> implements Electron.TouchBarOtherItemsProxy {
@@ -292,7 +339,7 @@ const escapeItemSymbol = Symbol('escape item');
class TouchBar extends EventEmitter implements Electron.TouchBar {
// Bind a touch bar to a window
static _setOnWindow (touchBar: TouchBar | Electron.TouchBarConstructorOptions['items'], window: Electron.BaseWindow) {
static _setOnWindow(touchBar: TouchBar | Electron.TouchBarConstructorOptions['items'], window: Electron.BaseWindow) {
if (window._touchBar != null) {
window._touchBar._removeFromWindow(window);
}
@@ -312,7 +359,7 @@ class TouchBar extends EventEmitter implements Electron.TouchBar {
private items = new Map<string, TouchBarItem<any>>();
orderedItems: TouchBarItem<any>[] = [];
constructor (options: Electron.TouchBarConstructorOptions) {
constructor(options: Electron.TouchBarConstructorOptions) {
super();
if (options == null) {
@@ -360,7 +407,7 @@ class TouchBar extends EventEmitter implements Electron.TouchBar {
}
// register in separate loop after all items are validated
for (const item of (items as TouchBarItem<any>[])) {
for (const item of items as TouchBarItem<any>[]) {
this.orderedItems.push(item);
registerItem(item);
}
@@ -372,7 +419,7 @@ class TouchBar extends EventEmitter implements Electron.TouchBar {
private [escapeItemSymbol]: TouchBarItem<unknown> | null = null;
set escapeItem (item: TouchBarItem<unknown> | null) {
set escapeItem(item: TouchBarItem<unknown> | null) {
if (item != null && !(item instanceof TouchBarItem)) {
throw new Error('Escape item must be an instance of TouchBarItem');
}
@@ -387,11 +434,11 @@ class TouchBar extends EventEmitter implements Electron.TouchBar {
this.emit('escape-item-change', item);
}
get escapeItem (): TouchBarItem<unknown> | null {
get escapeItem(): TouchBarItem<unknown> | null {
return this[escapeItemSymbol];
}
_addToWindow (window: Electron.BaseWindow) {
_addToWindow(window: Electron.BaseWindow) {
const { id } = window;
// Already added to window
@@ -447,7 +494,7 @@ class TouchBar extends EventEmitter implements Electron.TouchBar {
escapeItemListener(this.escapeItem);
}
_removeFromWindow (window: Electron.BaseWindow) {
_removeFromWindow(window: Electron.BaseWindow) {
const removeListeners = this.windowListeners.get(window.id);
if (removeListeners != null) removeListeners();
}

View File

@@ -10,7 +10,7 @@ class ForkUtilityProcess extends EventEmitter implements Electron.UtilityProcess
#handle: ElectronInternal.UtilityProcessWrapper | null;
#stdout: Duplex | null = null;
#stderr: Duplex | null = null;
constructor (modulePath: string, args?: string[], options?: Electron.ForkOptions) {
constructor(modulePath: string, args?: string[], options?: Electron.ForkOptions) {
super();
if (!modulePath) {
@@ -53,7 +53,7 @@ class ForkUtilityProcess extends EventEmitter implements Electron.UtilityProcess
}
if (typeof options.stdio === 'string') {
const stdio : Array<'pipe' | 'ignore' | 'inherit'> = [];
const stdio: Array<'pipe' | 'ignore' | 'inherit'> = [];
switch (options.stdio) {
case 'inherit':
case 'ignore':
@@ -119,27 +119,27 @@ class ForkUtilityProcess extends EventEmitter implements Electron.UtilityProcess
};
}
get pid () {
get pid() {
return this.#handle?.pid;
}
get stdout () {
get stdout() {
return this.#stdout;
}
get stderr () {
get stderr() {
return this.#stderr;
}
postMessage (message: any, transfer?: MessagePortMain[]) {
postMessage(message: any, transfer?: MessagePortMain[]) {
if (Array.isArray(transfer)) {
transfer = transfer.map((o: any) => o instanceof MessagePortMain ? o._internalPort : o);
transfer = transfer.map((o: any) => (o instanceof MessagePortMain ? o._internalPort : o));
return this.#handle?.postMessage(message, transfer);
}
return this.#handle?.postMessage(message);
}
kill () : boolean {
kill(): boolean {
if (this.#handle === null) {
return false;
}
@@ -147,6 +147,6 @@ class ForkUtilityProcess extends EventEmitter implements Electron.UtilityProcess
}
}
export function fork (modulePath: string, args?: string[], options?: Electron.ForkOptions) {
export function fork(modulePath: string, args?: string[], options?: Electron.ForkOptions) {
return new ForkUtilityProcess(modulePath, args, options);
}

View File

@@ -1,4 +1,8 @@
import { openGuestWindow, makeWebPreferences, parseContentTypeFormat } from '@electron/internal/browser/guest-window-manager';
import {
openGuestWindow,
makeWebPreferences,
parseContentTypeFormat
} from '@electron/internal/browser/guest-window-manager';
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { parseFeatures } from '@electron/internal/browser/parse-features-string';
@@ -110,7 +114,7 @@ const paperFormats: Record<string, ElectronInternal.PageSize> = {
// Practically, this means microns need to be > 352 microns.
// We therefore need to verify this or it will silently fail.
const isValidCustomPageSize = (width: number, height: number) => {
return [width, height].every(x => x > 352);
return [width, height].every((x) => x > 352);
};
// JavaScript implementations of WebContents.
@@ -130,10 +134,10 @@ WebContents.prototype._sendInternal = function (channel, ...args) {
return this.mainFrame._sendInternal(channel, ...args);
};
function getWebFrame (contents: Electron.WebContents, frame: number | [number, number]) {
function getWebFrame(contents: Electron.WebContents, frame: number | [number, number]) {
if (typeof frame === 'number') {
return webFrameMain.fromId(contents.mainFrame.processId, frame);
} else if (Array.isArray(frame) && frame.length === 2 && frame.every(value => typeof value === 'number')) {
} else if (Array.isArray(frame) && frame.length === 2 && frame.every((value) => typeof value === 'number')) {
return webFrameMain.fromId(frame[0], frame[1]);
} else {
throw new Error('Missing required frame argument (must be number or [processId, frameId])');
@@ -148,12 +152,12 @@ WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
};
// Following methods are mapped to webFrame.
const webFrameMethods = [
'insertCSS',
'insertText',
'removeInsertedCSS',
'setVisualZoomLevelLimits'
] as ('insertCSS' | 'insertText' | 'removeInsertedCSS' | 'setVisualZoomLevelLimits')[];
const webFrameMethods = ['insertCSS', 'insertText', 'removeInsertedCSS', 'setVisualZoomLevelLimits'] as (
| 'insertCSS'
| 'insertText'
| 'removeInsertedCSS'
| 'setVisualZoomLevelLimits'
)[];
for (const method of webFrameMethods) {
WebContents.prototype[method] = function (...args: any[]): Promise<any> {
@@ -175,14 +179,27 @@ const waitTillCanExecuteJavaScript = async (webContents: Electron.WebContents) =
// WebContents has been loaded.
WebContents.prototype.executeJavaScript = async function (code, hasUserGesture) {
await waitTillCanExecuteJavaScript(this);
return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScript', String(code), !!hasUserGesture);
return ipcMainUtils.invokeInWebContents(
this,
IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD,
'executeJavaScript',
String(code),
!!hasUserGesture
);
};
WebContents.prototype.executeJavaScriptInIsolatedWorld = async function (worldId, code, hasUserGesture) {
await waitTillCanExecuteJavaScript(this);
return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScriptInIsolatedWorld', worldId, code, !!hasUserGesture);
return ipcMainUtils.invokeInWebContents(
this,
IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD,
'executeJavaScriptInIsolatedWorld',
worldId,
code,
!!hasUserGesture
);
};
function checkType<T> (value: T, type: 'number' | 'boolean' | 'string' | 'object', name: string): T {
function checkType<T>(value: T, type: 'number' | 'boolean' | 'string' | 'object', name: string): T {
// eslint-disable-next-line valid-typeof
if (typeof value !== type) {
throw new TypeError(`${name} must be a ${type}`);
@@ -191,7 +208,7 @@ function checkType<T> (value: T, type: 'number' | 'boolean' | 'string' | 'object
return value;
}
function parsePageSize (pageSize: string | ElectronInternal.PageSize) {
function parsePageSize(pageSize: string | ElectronInternal.PageSize) {
if (typeof pageSize === 'string') {
const format = paperFormats[pageSize.toLowerCase()];
if (!format) {
@@ -218,8 +235,8 @@ WebContents.prototype.printToPDF = async function (options) {
const pageSize = parsePageSize(options.pageSize ?? 'letter');
const { top, bottom, left, right } = margins;
const validHeight = [top, bottom].every(u => u === undefined || u <= pageSize.paperHeight);
const validWidth = [left, right].every(u => u === undefined || u <= pageSize.paperWidth);
const validHeight = [top, bottom].every((u) => u === undefined || u <= pageSize.paperHeight);
const validWidth = [left, right].every((u) => u === undefined || u <= pageSize.paperWidth);
if (!validHeight || !validWidth) {
throw new Error('margins must be less than or equal to pageSize');
@@ -332,19 +349,21 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
}
const { query, search, hash } = options;
return this.loadURL(url.format({
protocol: 'file',
slashes: true,
pathname: path.resolve(app.getAppPath(), filePath),
query,
search,
hash
}));
return this.loadURL(
url.format({
protocol: 'file',
slashes: true,
pathname: path.resolve(app.getAppPath(), filePath),
query,
search,
hash
})
);
};
type LoadError = { errorCode: number, errorDescription: string, url: string };
type LoadError = { errorCode: number; errorDescription: string; url: string };
function _awaitNextLoad (this: Electron.WebContents, navigationUrl: string) {
function _awaitNextLoad(this: Electron.WebContents, navigationUrl: string) {
return new Promise<void>((resolve, reject) => {
const resolveAndCleanup = () => {
removeListeners();
@@ -352,7 +371,9 @@ function _awaitNextLoad (this: Electron.WebContents, navigationUrl: string) {
};
let error: LoadError | undefined;
const rejectAndCleanup = ({ errorCode, errorDescription, url }: LoadError) => {
const err = new Error(`${errorDescription} (${errorCode}) loading '${typeof url === 'string' ? url.substr(0, 2048) : url}'`);
const err = new Error(
`${errorDescription} (${errorCode}) loading '${typeof url === 'string' ? url.substr(0, 2048) : url}'`
);
Object.assign(err, { errno: errorCode, code: errorDescription, url });
removeListeners();
reject(err);
@@ -385,7 +406,13 @@ function _awaitNextLoad (this: Electron.WebContents, navigationUrl: string) {
navigationStarted = true;
}
};
const failListener = (event: Electron.Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
const failListener = (
event: Electron.Event,
errorCode: number,
errorDescription: string,
validatedURL: string,
isMainFrame: boolean
) => {
if (!error && isMainFrame) {
error = { errorCode, errorDescription, url: validatedURL };
}
@@ -427,7 +454,7 @@ function _awaitNextLoad (this: Electron.WebContents, navigationUrl: string) {
this.on('did-stop-loading', stopLoadingListener);
this.on('destroyed', stopLoadingListener);
});
};
}
WebContents.prototype.loadURL = function (url, options) {
const p = _awaitNextLoad.call(this, url);
@@ -445,11 +472,20 @@ WebContents.prototype.saveVideoFrameAs = function (x: number, y: number) {
this.mainFrame.saveVideoFrameAs(x, y);
};
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => Electron.WindowOpenHandlerResponse) {
WebContents.prototype.setWindowOpenHandler = function (
handler: (details: Electron.HandlerDetails) => Electron.WindowOpenHandlerResponse
) {
this._windowOpenHandler = handler;
};
WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event, details: Electron.HandlerDetails): {browserWindowConstructorOptions: BrowserWindowConstructorOptions | null, outlivesOpener: boolean, createWindow?: Electron.CreateWindowFunction} {
WebContents.prototype._callWindowOpenHandler = function (
event: Electron.Event,
details: Electron.HandlerDetails
): {
browserWindowConstructorOptions: BrowserWindowConstructorOptions | null;
outlivesOpener: boolean;
createWindow?: Electron.CreateWindowFunction;
} {
const defaultResponse = {
browserWindowConstructorOptions: null,
outlivesOpener: false,
@@ -478,13 +514,14 @@ WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event,
return defaultResponse;
} else if (response.action === 'allow') {
return {
browserWindowConstructorOptions: typeof response.overrideBrowserWindowOptions === 'object' ? response.overrideBrowserWindowOptions : null,
browserWindowConstructorOptions:
typeof response.overrideBrowserWindowOptions === 'object' ? response.overrideBrowserWindowOptions : null,
outlivesOpener: typeof response.outlivesOpener === 'boolean' ? response.outlivesOpener : false,
createWindow: typeof response.createWindow === 'function' ? response.createWindow : undefined
};
} else {
event.preventDefault();
console.error('The window open handler response must be an object with an \'action\' property of \'allow\' or \'deny\'.');
console.error("The window open handler response must be an object with an 'action' property of 'allow' or 'deny'.");
return defaultResponse;
}
};
@@ -502,13 +539,19 @@ WebContents.prototype.canGoBack = function () {
return this._canGoBack();
};
const canGoForwardDeprecated = deprecate.warnOnce('webContents.canGoForward', 'webContents.navigationHistory.canGoForward');
const canGoForwardDeprecated = deprecate.warnOnce(
'webContents.canGoForward',
'webContents.navigationHistory.canGoForward'
);
WebContents.prototype.canGoForward = function () {
canGoForwardDeprecated();
return this._canGoForward();
};
const canGoToOffsetDeprecated = deprecate.warnOnce('webContents.canGoToOffset', 'webContents.navigationHistory.canGoToOffset');
const canGoToOffsetDeprecated = deprecate.warnOnce(
'webContents.canGoToOffset',
'webContents.navigationHistory.canGoToOffset'
);
WebContents.prototype.canGoToOffset = function (index: number) {
canGoToOffsetDeprecated();
return this._canGoToOffset(index);
@@ -544,13 +587,17 @@ WebContents.prototype.goToOffset = function (index: number) {
return this._goToOffset(index);
};
const consoleMessageDeprecated = deprecate.warnOnceMessage('\'console-message\' arguments are deprecated and will be removed. Please use Event<WebContentsConsoleMessageEventParams> object instead.');
const consoleMessageDeprecated = deprecate.warnOnceMessage(
"'console-message' arguments are deprecated and will be removed. Please use Event<WebContentsConsoleMessageEventParams> object instead."
);
// Add JavaScript wrappers for WebContents class.
WebContents.prototype._init = function () {
const prefs = this.getLastWebPreferences() || {};
if (!prefs.nodeIntegration && prefs.preload != null && prefs.sandbox == null) {
deprecate.log('The default sandbox option for windows without nodeIntegration is changing. Presently, by default, when a window has a preload script, it defaults to being unsandboxed. In Electron 20, this default will be changing, and all windows that have nodeIntegration: false (which is the default) will be sandboxed by default. If your preload script doesn\'t use Node, no action is needed. If your preload script does use Node, either refactor it to move Node usage to the main process, or specify sandbox: false in your WebPreferences.');
deprecate.log(
"The default sandbox option for windows without nodeIntegration is changing. Presently, by default, when a window has a preload script, it defaults to being unsandboxed. In Electron 20, this default will be changing, and all windows that have nodeIntegration: false (which is the default) will be sandboxed by default. If your preload script doesn't use Node, no action is needed. If your preload script does use Node, either refactor it to move Node usage to the main process, or specify sandbox: false in your WebPreferences."
);
}
// Read off the ID at construction time, so that it's accessible even after
// the underlying C++ WebContents is destroyed.
@@ -564,7 +611,9 @@ WebContents.prototype._init = function () {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', {
get () { return ipc; },
get() {
return ipc;
},
enumerable: true
});
@@ -585,13 +634,15 @@ WebContents.prototype._init = function () {
getEntryAtIndex: this._getNavigationEntryAtIndex.bind(this),
removeEntryAtIndex: this._removeNavigationEntryAtIndex.bind(this),
getAllEntries: this._getHistory.bind(this),
restore: ({ index, entries }: { index?: number, entries: NavigationEntry[] }) => {
restore: ({ index, entries }: { index?: number; entries: NavigationEntry[] }) => {
if (index === undefined) {
index = entries.length - 1;
}
if (index < 0 || !entries[index]) {
throw new Error('Invalid index. Index must be a positive integer and within the bounds of the entries length.');
throw new Error(
'Invalid index. Index must be a positive integer and within the bounds of the entries length.'
);
}
const p = _awaitNextLoad.call(this, entries[index].url);
@@ -615,7 +666,9 @@ WebContents.prototype._init = function () {
// Log out a hint to help users better debug renderer crashes.
if (loggingEnabled()) {
console.info(`Renderer process ${details.reason} - see https://www.electronjs.org/docs/tutorial/application-debugging for potential debugging information.`);
console.info(
`Renderer process ${details.reason} - see https://www.electronjs.org/docs/tutorial/application-debugging for potential debugging information.`
);
}
});
@@ -625,7 +678,9 @@ WebContents.prototype._init = function () {
// All other types should ignore the "proceed" signal and unload
// regardless.
if (type === 'window' || type === 'offscreen' || type === 'browserView') {
if (!proceed) { return event.preventDefault(); }
if (!proceed) {
return event.preventDefault();
}
}
});
@@ -710,8 +765,8 @@ WebContents.prototype._init = function () {
const secureOverrideWebPreferences = windowOpenOverriddenOptions
? {
// Allow setting of backgroundColor as a webPreference even though
// it's technically a BrowserWindowConstructorOptions option because
// we need to access it in the renderer at init time.
// it's technically a BrowserWindowConstructorOptions option because
// we need to access it in the renderer at init time.
backgroundColor: windowOpenOverriddenOptions.backgroundColor,
transparent: windowOpenOverriddenOptions.transparent,
...windowOpenOverriddenOptions.webPreferences
@@ -732,38 +787,54 @@ WebContents.prototype._init = function () {
});
// Create a new browser window for "window.open"
this.on('-add-new-contents', (event, webContents, disposition, _userGesture, _left, _top, _width, _height, url, frameName, referrer, rawFeatures, postData) => {
const overriddenOptions = windowOpenOverriddenOptions || undefined;
const outlivesOpener = windowOpenOutlivesOpenerOption;
const windowOpenFunction = createWindow;
createWindow = undefined;
windowOpenOverriddenOptions = null;
// false is the default
windowOpenOutlivesOpenerOption = false;
if ((disposition !== 'foreground-tab' && disposition !== 'new-window' &&
disposition !== 'background-tab')) {
event.preventDefault();
return;
}
openGuestWindow({
embedder: this,
guest: webContents,
overrideBrowserWindowOptions: overriddenOptions,
this.on(
'-add-new-contents',
(
event,
webContents,
disposition,
_userGesture,
_left,
_top,
_width,
_height,
url,
frameName,
referrer,
postData,
windowOpenArgs: {
url,
frameName,
features: rawFeatures
},
outlivesOpener,
createWindow: windowOpenFunction
});
});
rawFeatures,
postData
) => {
const overriddenOptions = windowOpenOverriddenOptions || undefined;
const outlivesOpener = windowOpenOutlivesOpenerOption;
const windowOpenFunction = createWindow;
createWindow = undefined;
windowOpenOverriddenOptions = null;
// false is the default
windowOpenOutlivesOpenerOption = false;
if (disposition !== 'foreground-tab' && disposition !== 'new-window' && disposition !== 'background-tab') {
event.preventDefault();
return;
}
openGuestWindow({
embedder: this,
guest: webContents,
overrideBrowserWindowOptions: overriddenOptions,
disposition,
referrer,
postData,
windowOpenArgs: {
url,
frameName,
features: rawFeatures
},
outlivesOpener,
createWindow: windowOpenFunction
});
}
);
}
this.on('login', (event, ...args) => {
@@ -802,14 +873,17 @@ WebContents.prototype._init = function () {
originCounts.set(origin, (originCounts.get(origin) ?? 0) + 1);
// TODO: translate?
const checkbox = originCounts.get(origin)! > 1 && prefs.safeDialogs ? prefs.safeDialogsMessage || 'Prevent this app from creating additional dialogs' : '';
const checkbox =
originCounts.get(origin)! > 1 && prefs.safeDialogs
? prefs.safeDialogsMessage || 'Prevent this app from creating additional dialogs'
: '';
const parent = this.getOwnerBrowserWindow();
const abortController = new AbortController();
const options: MessageBoxOptions = {
message: info.messageText,
checkboxLabel: checkbox,
signal: abortController.signal,
...(info.dialogType === 'confirm')
...(info.dialogType === 'confirm'
? {
buttons: ['OK', 'Cancel'],
defaultId: 0,
@@ -819,10 +893,11 @@ WebContents.prototype._init = function () {
buttons: ['OK'],
defaultId: -1, // No default button
cancelId: 0
}
})
};
openDialogs.add(abortController);
const promise = parent && !prefs.offscreen ? dialog.showMessageBox(parent, options) : dialog.showMessageBox(options);
const promise =
parent && !prefs.offscreen ? dialog.showMessageBox(parent, options) : dialog.showMessageBox(options);
try {
const result = await promise;
if (abortController.signal.aborted || this.isDestroyed()) return;
@@ -834,13 +909,15 @@ WebContents.prototype._init = function () {
});
this.on('-cancel-dialogs', () => {
for (const controller of openDialogs) { controller.abort(); }
for (const controller of openDialogs) {
controller.abort();
}
openDialogs.clear();
});
// TODO(samuelmaddock): remove deprecated 'console-message' arguments
this.on('-console-message' as any, (event: Electron.Event<Electron.WebContentsConsoleMessageEventParams>) => {
const hasDeprecatedListener = this.listeners('console-message').some(listener => listener.length > 1);
const hasDeprecatedListener = this.listeners('console-message').some((listener) => listener.length > 1);
if (hasDeprecatedListener) {
consoleMessageDeprecated();
}
@@ -854,7 +931,17 @@ WebContents.prototype._init = function () {
}
});
app.emit('web-contents-created', { sender: this, preventDefault () {}, get defaultPrevented () { return false; } }, this);
app.emit(
'web-contents-created',
{
sender: this,
preventDefault() {},
get defaultPrevented() {
return false;
}
},
this
);
// Properties
@@ -890,23 +977,23 @@ WebContents.prototype._init = function () {
};
// Public APIs.
export function create (options = {}): Electron.WebContents {
export function create(options = {}): Electron.WebContents {
return new (WebContents as any)(options);
}
export function fromId (id: number) {
export function fromId(id: number) {
return binding.fromId(id);
}
export function fromFrame (frame: Electron.WebFrameMain) {
export function fromFrame(frame: Electron.WebFrameMain) {
return binding.fromFrame(frame);
}
export function fromDevToolsTargetId (targetId: string) {
export function fromDevToolsTargetId(targetId: string) {
return binding.fromDevToolsTargetId(targetId);
}
export function getFocusedWebContents () {
export function getFocusedWebContents() {
let focused = null;
for (const contents of binding.getAllWebContents()) {
if (!contents.isFocused()) continue;
@@ -917,6 +1004,6 @@ export function getFocusedWebContents () {
}
return focused;
}
export function getAllWebContents () {
export function getAllWebContents() {
return binding.getAllWebContents();
}

View File

@@ -4,7 +4,7 @@ import { MessagePortMain } from '@electron/internal/browser/message-port-main';
const { WebFrameMain, fromId, fromFrameToken } = process._linkedBinding('electron_browser_web_frame_main');
Object.defineProperty(WebFrameMain.prototype, 'ipc', {
get () {
get() {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;
@@ -37,7 +37,7 @@ WebFrameMain.prototype._sendInternal = function (channel, ...args) {
WebFrameMain.prototype.postMessage = function (...args) {
if (Array.isArray(args[2])) {
args[2] = args[2].map(o => o instanceof MessagePortMain ? o._internalPort : o);
args[2] = args[2].map((o) => (o instanceof MessagePortMain ? o._internalPort : o));
}
this._postMessage(...args);
};

View File

@@ -1,5 +1,4 @@
import { shell } from 'electron/common';
import { app, Menu } from 'electron/main';
import { Menu } from 'electron/main';
const isMac = process.platform === 'darwin';
@@ -12,47 +11,13 @@ export const setApplicationMenuWasSet = () => {
export const setDefaultApplicationMenu = () => {
if (applicationMenuWasSet) return;
const helpMenu: Electron.MenuItemConstructorOptions = {
role: 'help',
submenu: app.isPackaged
? []
: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org');
}
},
{
label: 'Documentation',
click: async () => {
const version = process.versions.electron;
await shell.openExternal(`https://github.com/electron/electron/tree/v${version}/docs#readme`);
}
},
{
label: 'Community Discussions',
click: async () => {
await shell.openExternal('https://discord.gg/electronjs');
}
},
{
label: 'Search Issues',
click: async () => {
await shell.openExternal('https://github.com/electron/electron/issues');
}
}
]
};
const macAppMenu: Electron.MenuItemConstructorOptions = { role: 'appMenu' };
const template: Electron.MenuItemConstructorOptions[] = [
...(isMac ? [macAppMenu] : []),
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ role: 'viewMenu' },
{ role: 'windowMenu' },
helpMenu
{ role: 'windowMenu' }
];
const menu = Menu.buildFromTemplate(template);

View File

@@ -8,29 +8,30 @@ import * as fs from 'fs';
const convertToMenuTemplate = function (items: ContextMenuItem[], handler: (id: number) => void) {
return items.map(function (item) {
const transformed: Electron.MenuItemConstructorOptions = item.type === 'subMenu'
? {
type: 'submenu',
label: item.label,
enabled: item.enabled,
submenu: convertToMenuTemplate(item.subItems, handler)
}
: item.type === 'separator'
const transformed: Electron.MenuItemConstructorOptions =
item.type === 'subMenu'
? {
type: 'separator'
type: 'submenu',
label: item.label,
enabled: item.enabled,
submenu: convertToMenuTemplate(item.subItems, handler)
}
: item.type === 'checkbox'
: item.type === 'separator'
? {
type: 'checkbox',
label: item.label,
enabled: item.enabled,
checked: item.checked
type: 'separator'
}
: {
type: 'normal',
label: item.label,
enabled: item.enabled
};
: item.type === 'checkbox'
? {
type: 'checkbox',
label: item.label,
enabled: item.enabled,
checked: item.checked
}
: {
type: 'normal',
label: item.label,
enabled: item.enabled
};
if (item.id != null) {
transformed.click = () => handler(item.id);
@@ -67,18 +68,21 @@ const assertChromeDevTools = function (contents: Electron.WebContents, api: stri
}
};
ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_CONTEXT_MENU, function (event, items: ContextMenuItem[], isEditMenu: boolean) {
return new Promise<number | void>(resolve => {
if (event.type !== 'frame') return;
assertChromeDevTools(event.sender, 'window.InspectorFrontendHost.showContextMenuAtPoint()');
ipcMainInternal.handle(
IPC_MESSAGES.INSPECTOR_CONTEXT_MENU,
function (event, items: ContextMenuItem[], isEditMenu: boolean) {
return new Promise<number | void>((resolve) => {
if (event.type !== 'frame') return;
assertChromeDevTools(event.sender, 'window.InspectorFrontendHost.showContextMenuAtPoint()');
const template = isEditMenu ? getEditMenuItems() : convertToMenuTemplate(items, resolve);
const menu = Menu.buildFromTemplate(template);
const window = event.sender.getOwnerBrowserWindow()!;
const template = isEditMenu ? getEditMenuItems() : convertToMenuTemplate(items, resolve);
const menu = Menu.buildFromTemplate(template);
const window = event.sender.getOwnerBrowserWindow()!;
menu.popup({ window, callback: () => resolve() });
});
});
menu.popup({ window, callback: () => resolve() });
});
}
);
ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_SELECT_FILE, async function (event) {
if (event.type !== 'frame') return [];
@@ -93,17 +97,20 @@ ipcMainInternal.handle(IPC_MESSAGES.INSPECTOR_SELECT_FILE, async function (event
return [path, data];
});
ipcMainUtils.handleSync(IPC_MESSAGES.INSPECTOR_CONFIRM, async function (event, message: string = '', title: string = '') {
if (event.type !== 'frame') return;
assertChromeDevTools(event.sender, 'window.confirm()');
ipcMainUtils.handleSync(
IPC_MESSAGES.INSPECTOR_CONFIRM,
async function (event, message: string = '', title: string = '') {
if (event.type !== 'frame') return;
assertChromeDevTools(event.sender, 'window.confirm()');
const options = {
message: String(message),
title: String(title),
buttons: ['OK', 'Cancel'],
cancelId: 1
};
const window = event.sender.getOwnerBrowserWindow()!;
const { response } = await dialog.showMessageBox(window, options);
return response === 0;
});
const options = {
message: String(message),
title: String(title),
buttons: ['OK', 'Cancel'],
cancelId: 1
};
const window = event.sender.getOwnerBrowserWindow()!;
const { response } = await dialog.showMessageBox(window, options);
return response === 0;
}
);

View File

@@ -3,7 +3,12 @@ import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-util
import { parseWebViewWebPreferences } from '@electron/internal/browser/parse-features-string';
import { webViewEvents } from '@electron/internal/browser/web-view-events';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { syncMethods, asyncMethods, properties, navigationHistorySyncMethods } from '@electron/internal/common/web-view-methods';
import {
syncMethods,
asyncMethods,
properties,
navigationHistorySyncMethods
} from '@electron/internal/common/web-view-methods';
import { webContents } from 'electron/main';
@@ -22,13 +27,11 @@ const supportedWebViewEvents = Object.keys(webViewEvents);
const guestInstances = new Map<number, GuestInstance>();
const embedderElementsMap = new Map<string, number>();
function makeWebPreferences (embedder: Electron.WebContents, params: Record<string, any>) {
function makeWebPreferences(embedder: Electron.WebContents, params: Record<string, any>) {
// parse the 'webpreferences' attribute string, if set
// this uses the same parsing rules as window.open uses for its features
const parsedWebPreferences =
typeof params.webpreferences === 'string'
? parseWebViewWebPreferences(params.webpreferences)
: null;
typeof params.webpreferences === 'string' ? parseWebViewWebPreferences(params.webpreferences) : null;
const webPreferences: Electron.WebPreferences = {
nodeIntegration: params.nodeintegration ?? false,
@@ -68,7 +71,7 @@ function makeWebPreferences (embedder: Electron.WebContents, params: Record<stri
return webPreferences;
}
function makeLoadURLOptions (params: Record<string, any>) {
function makeLoadURLOptions(params: Record<string, any>) {
const opts: Electron.LoadURLOptions = {};
if (params.httpreferrer) {
opts.httpReferrer = params.httpreferrer;
@@ -80,11 +83,16 @@ function makeLoadURLOptions (params: Record<string, any>) {
}
// Create a new guest instance.
const createGuest = function (embedder: Electron.WebContents, embedderFrameToken: string, elementInstanceId: number, params: Record<string, any>) {
const createGuest = function (
embedder: Electron.WebContents,
embedderFrameToken: string,
elementInstanceId: number,
params: Record<string, any>
) {
const webPreferences = makeWebPreferences(embedder, params);
const event = {
sender: embedder,
preventDefault () {
preventDefault() {
this.defaultPrevented = true;
},
defaultPrevented: false
@@ -266,13 +274,18 @@ const isWebViewTagEnabled = function (contents: Electron.WebContents) {
return isWebViewTagEnabledCache.get(contents);
};
const makeSafeHandler = function<Event extends { sender: Electron.WebContents }> (channel: string, handler: (event: Event, ...args: any[]) => any) {
const makeSafeHandler = function <Event extends { sender: Electron.WebContents }>(
channel: string,
handler: (event: Event, ...args: any[]) => any
) {
return (event: Electron.IpcMainInvokeEvent | Electron.IpcMainServiceWorkerInvokeEvent, ...args: any[]) => {
if (event.type !== 'frame') return;
if (isWebViewTagEnabled(event.sender)) {
return handler(event as unknown as Event, ...args);
} else {
console.error(`<webview> IPC message ${channel} sent by WebContents with <webview> disabled (${event.sender.id})`);
console.error(
`<webview> IPC message ${channel} sent by WebContents with <webview> disabled (${event.sender.id})`
);
throw new Error('<webview> disabled');
}
};
@@ -282,13 +295,19 @@ const handleMessage = function (channel: string, handler: (event: Electron.IpcMa
ipcMainInternal.handle(channel, makeSafeHandler(channel, handler));
};
const handleMessageSync = function (channel: string, handler: (event: { sender: Electron.WebContents }, ...args: any[]) => any) {
const handleMessageSync = function (
channel: string,
handler: (event: { sender: Electron.WebContents }, ...args: any[]) => any
) {
ipcMainUtils.handleSync(channel, makeSafeHandler(channel, handler));
};
handleMessage(IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_AND_ATTACH_GUEST, function (event, embedderFrameToken: string, elementInstanceId: number, params) {
return createGuest(event.sender, embedderFrameToken, elementInstanceId, params);
});
handleMessage(
IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_AND_ATTACH_GUEST,
function (event, embedderFrameToken: string, elementInstanceId: number, params) {
return createGuest(event.sender, embedderFrameToken, elementInstanceId, params);
}
);
handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_DETACH_GUEST, function (event, guestInstanceId: number) {
return detachGuest(event.sender, guestInstanceId);
@@ -301,49 +320,61 @@ ipcMainInternal.on(IPC_MESSAGES.GUEST_VIEW_MANAGER_FOCUS_CHANGE, function (event
}
});
handleMessage(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, function (event, guestInstanceId: number, method: string, args: any[]) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!asyncMethods.has(method)) {
throw new Error(`Invalid method: ${method}`);
}
return (guest as any)[method](...args);
});
handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, function (event, guestInstanceId: number, method: string, args: any[]) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!syncMethods.has(method)) {
throw new Error(`Invalid method: ${method}`);
}
// Redirect history methods to updated navigationHistory property on webContents. See issue #42879.
if (navigationHistorySyncMethods.has(method)) {
let navigationMethod = method;
if (method === 'clearHistory') {
navigationMethod = 'clear';
handleMessage(
IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL,
function (event, guestInstanceId: number, method: string, args: any[]) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!asyncMethods.has(method)) {
throw new Error(`Invalid method: ${method}`);
}
return (guest as any).navigationHistory[navigationMethod](...args);
return (guest as any)[method](...args);
}
);
return (guest as any)[method](...args);
});
handleMessageSync(
IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL,
function (event, guestInstanceId: number, method: string, args: any[]) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!syncMethods.has(method)) {
throw new Error(`Invalid method: ${method}`);
}
// Redirect history methods to updated navigationHistory property on webContents. See issue #42879.
if (navigationHistorySyncMethods.has(method)) {
let navigationMethod = method;
if (method === 'clearHistory') {
navigationMethod = 'clear';
}
return (guest as any).navigationHistory[navigationMethod](...args);
}
handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET, function (event, guestInstanceId: number, property: string) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!properties.has(property)) {
throw new Error(`Invalid property: ${property}`);
return (guest as any)[method](...args);
}
);
return (guest as any)[property];
});
handleMessageSync(
IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET,
function (event, guestInstanceId: number, property: string) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!properties.has(property)) {
throw new Error(`Invalid property: ${property}`);
}
handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET, function (event, guestInstanceId: number, property: string, val: any) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!properties.has(property)) {
throw new Error(`Invalid property: ${property}`);
return (guest as any)[property];
}
);
(guest as any)[property] = val;
});
handleMessageSync(
IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET,
function (event, guestInstanceId: number, property: string, val: any) {
const guest = getGuestForWebContents(guestInstanceId, event.sender);
if (!properties.has(property)) {
throw new Error(`Invalid property: ${property}`);
}
(guest as any)[property] = val;
}
);
// Returns WebContents from its guest id hosted in given webContents.
const getGuestForWebContents = function (guestInstanceId: number, contents: Electron.WebContents) {

View File

@@ -10,27 +10,37 @@ import { parseFeatures } from '@electron/internal/browser/parse-features-string'
import { BrowserWindow } from 'electron/main';
import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';
type PostData = LoadURLOptions['postData']
type PostData = LoadURLOptions['postData'];
export type WindowOpenArgs = {
url: string,
frameName: string,
features: string,
}
url: string;
frameName: string;
features: string;
};
/**
* `openGuestWindow` is called to create and setup event handling for the new
* window.
*/
export function openGuestWindow ({ embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener, createWindow }: {
embedder: WebContents,
guest?: WebContents,
referrer: Referrer,
disposition: string,
postData?: PostData,
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
windowOpenArgs: WindowOpenArgs,
outlivesOpener: boolean,
createWindow?: Electron.CreateWindowFunction
export function openGuestWindow({
embedder,
guest,
referrer,
disposition,
postData,
overrideBrowserWindowOptions,
windowOpenArgs,
outlivesOpener,
createWindow
}: {
embedder: WebContents;
guest?: WebContents;
referrer: Referrer;
disposition: string;
postData?: PostData;
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions;
windowOpenArgs: WindowOpenArgs;
outlivesOpener: boolean;
createWindow?: Electron.CreateWindowFunction;
}): void {
const { url, frameName, features } = windowOpenArgs;
const { options: parsedOptions } = parseFeatures(features);
@@ -50,7 +60,9 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
if (guest != null) {
if (webContents !== guest) {
throw new Error('Invalid webContents. Created window should be connected to webContents passed with options object.');
throw new Error(
'Invalid webContents. Created window should be connected to webContents passed with options object.'
);
}
handleWindowLifecycleEvents({ embedder, guest, outlivesOpener });
@@ -79,7 +91,14 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
handleWindowLifecycleEvents({ embedder, guest: window.webContents, outlivesOpener });
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
embedder.emit('did-create-window', window, {
url,
frameName,
options: browserWindowOptions,
disposition,
referrer,
postData
});
}
/**
@@ -88,10 +107,14 @@ export function openGuestWindow ({ embedder, guest, referrer, disposition, postD
* too is the guest destroyed; this is Electron convention and isn't based in
* browser behavior.
*/
const handleWindowLifecycleEvents = function ({ embedder, guest, outlivesOpener }: {
embedder: WebContents,
guest: WebContents,
outlivesOpener: boolean
const handleWindowLifecycleEvents = function ({
embedder,
guest,
outlivesOpener
}: {
embedder: WebContents;
guest: WebContents;
outlivesOpener: boolean;
}) {
const closedByEmbedder = function () {
guest.removeListener('destroyed', closedByUser);
@@ -121,21 +144,25 @@ const securityWebPreferences: { [key: string]: boolean } = {
enableWebSQL: false
};
export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {} }: {
embedder: WebContents,
insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'],
export function makeWebPreferences({
embedder,
secureOverrideWebPreferences = {},
insecureParsedWebPreferences: parsedWebPreferences = {}
}: {
embedder: WebContents;
insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'];
// Note that override preferences are considered elevated, and should only be
// sourced from the main process, as they override security defaults. If you
// have unvetted prefs, use parsedWebPreferences.
secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'],
secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'];
}) {
const parentWebPreferences = embedder.getLastWebPreferences()!;
const securityWebPreferencesFromParent = (Object.keys(securityWebPreferences).reduce((map, key) => {
const securityWebPreferencesFromParent = Object.keys(securityWebPreferences).reduce((map, key) => {
if (securityWebPreferences[key] === parentWebPreferences[key as keyof Electron.WebPreferences]) {
(map as any)[key] = parentWebPreferences[key as keyof Electron.WebPreferences];
}
return map;
}, {} as Electron.WebPreferences));
}, {} as Electron.WebPreferences);
return {
...parsedWebPreferences,
@@ -147,11 +174,13 @@ export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {
};
}
function formatPostDataHeaders (postData: PostData) {
function formatPostDataHeaders(postData: PostData) {
if (!postData) return;
const { contentType, boundary } = parseContentTypeFormat(postData);
if (boundary != null) { return `content-type: ${contentType}; boundary=${boundary}`; }
if (boundary != null) {
return `content-type: ${contentType}; boundary=${boundary}`;
}
return `content-type: ${contentType}`;
}

View File

@@ -29,12 +29,11 @@ process.on('uncaughtException', function (error) {
// We can't import { dialog } at the top of this file as this file is
// responsible for setting up the require hook for the "electron" module
// so we import it inside the handler down here
import('electron')
.then(({ dialog }) => {
const stack = error.stack ? error.stack : `${error.name}: ${error.message}`;
const message = 'Uncaught Exception:\n' + stack;
dialog.showErrorBox('A JavaScript error occurred in the main process', message);
});
import('electron').then(({ dialog }) => {
const stack = error.stack ? error.stack : `${error.name}: ${error.message}`;
const message = 'Uncaught Exception:\n' + stack;
dialog.showErrorBox('A JavaScript error occurred in the main process', message);
});
});
// Emit 'exit' event on quit.
@@ -64,7 +63,10 @@ if (process.platform === 'win32') {
if (fs.existsSync(updateDotExe)) {
const packageDir = path.dirname(path.resolve(updateDotExe));
const packageName = path.basename(packageDir).replaceAll(/\s/g, '');
const exeName = path.basename(process.execPath).replace(/\.exe$/i, '').replaceAll(/\s/g, '');
const exeName = path
.basename(process.execPath)
.replace(/\.exe$/i, '')
.replaceAll(/\s/g, '');
app.setAppUserModelId(`com.squirrel.${packageName}.${exeName}`);
}
@@ -181,7 +183,9 @@ delete process.appCodeLoaded;
if (packagePath) {
// Finally load app's main.js and transfer control to C++.
if ((packageJson.type === 'module' && !mainStartupScript.endsWith('.cjs')) || mainStartupScript.endsWith('.mjs')) {
const { runEntryPointWithESMLoader } = __non_webpack_require__('internal/modules/run_main') as typeof import('@node/lib/internal/modules/run_main');
const { runEntryPointWithESMLoader } = __non_webpack_require__(
'internal/modules/run_main'
) as typeof import('@node/lib/internal/modules/run_main');
const main = (require('url') as typeof url).pathToFileURL(path.join(packagePath, mainStartupScript));
runEntryPointWithESMLoader(async (cascadedLoader: any) => {
try {
@@ -199,6 +203,6 @@ if (packagePath) {
}
} else {
console.error('Failed to locate a valid package to load (app, app.asar or default_app.asar)');
console.error('This normally means you\'ve damaged the Electron package somehow');
console.error("This normally means you've damaged the Electron package somehow");
appCodeLoaded!();
}

View File

@@ -21,10 +21,14 @@ const addReturnValueToEvent = (event: Electron.IpcMainEvent | Electron.IpcMainSe
});
};
const getServiceWorkerFromEvent = (event: Electron.IpcMainServiceWorkerEvent | Electron.IpcMainServiceWorkerInvokeEvent): ServiceWorkerMain | undefined => {
const getServiceWorkerFromEvent = (
event: Electron.IpcMainServiceWorkerEvent | Electron.IpcMainServiceWorkerInvokeEvent
): ServiceWorkerMain | undefined => {
return event.session.serviceWorkers._getWorkerFromVersionIDIfExists(event.versionId);
};
const addServiceWorkerPropertyToEvent = (event: Electron.IpcMainServiceWorkerEvent | Electron.IpcMainServiceWorkerInvokeEvent) => {
const addServiceWorkerPropertyToEvent = (
event: Electron.IpcMainServiceWorkerEvent | Electron.IpcMainServiceWorkerInvokeEvent
) => {
Object.defineProperty(event, 'serviceWorker', {
get: () => event.session.serviceWorkers.getWorkerFromVersionID(event.versionId)
});
@@ -41,7 +45,9 @@ const cachedIpcEmitters: (ElectronInternal.IpcMainInternal | undefined)[] = [
];
// Get list of relevant IPC emitters for dispatch.
const getIpcEmittersForFrameEvent = (event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent): (ElectronInternal.IpcMainInternal | undefined)[] => {
const getIpcEmittersForFrameEvent = (
event: Electron.IpcMainEvent | Electron.IpcMainInvokeEvent
): (ElectronInternal.IpcMainInternal | undefined)[] => {
// Lookup by FrameTreeNode ID to ensure IPCs received after a frame swap are
// always received. This occurs when a RenderFrame sends an IPC while it's
// unloading and its internal state is pending deletion.
@@ -55,97 +61,121 @@ const getIpcEmittersForFrameEvent = (event: Electron.IpcMainEvent | Electron.Ipc
/**
* Listens for IPC dispatch events on `api`.
*/
export function addIpcDispatchListeners (api: NodeJS.EventEmitter) {
api.on('-ipc-message' as any, function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
export function addIpcDispatchListeners(api: NodeJS.EventEmitter) {
api.on(
'-ipc-message' as any,
function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
if (internal) {
ipcMainInternal.emit(channel, event, ...args);
} else if (event.type === 'frame') {
addReplyToEvent(event);
event.sender.emit('ipc-message', event, channel, ...args);
for (const ipcEmitter of getIpcEmittersForFrameEvent(event)) {
ipcEmitter?.emit(channel, event, ...args);
if (internal) {
ipcMainInternal.emit(channel, event, ...args);
} else if (event.type === 'frame') {
addReplyToEvent(event);
event.sender.emit('ipc-message', event, channel, ...args);
for (const ipcEmitter of getIpcEmittersForFrameEvent(event)) {
ipcEmitter?.emit(channel, event, ...args);
}
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, ...args);
}
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, ...args);
}
} as any);
} as any
);
api.on('-ipc-invoke' as any, async function (event: Electron.IpcMainInvokeEvent | Electron.IpcMainServiceWorkerInvokeEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
api.on(
'-ipc-invoke' as any,
async function (
event: Electron.IpcMainInvokeEvent | Electron.IpcMainServiceWorkerInvokeEvent,
channel: string,
args: any[]
) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
const replyWithResult = (result: any) => event._replyChannel.sendReply({ result });
const replyWithError = (error: Error) => {
console.error(`Error occurred in handler for '${channel}':`, error);
event._replyChannel.sendReply({ error: error.toString() });
};
const replyWithResult = (result: any) => event._replyChannel.sendReply({ result });
const replyWithError = (error: Error) => {
console.error(`Error occurred in handler for '${channel}':`, error);
event._replyChannel.sendReply({ error: error.toString() });
};
const targets: (Electron.IpcMainServiceWorker | ElectronInternal.IpcMainInternal | undefined)[] = [];
const targets: (Electron.IpcMainServiceWorker | ElectronInternal.IpcMainInternal | undefined)[] = [];
if (internal) {
targets.push(ipcMainInternal);
} else if (event.type === 'frame') {
targets.push(...getIpcEmittersForFrameEvent(event));
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
const workerIpc = getServiceWorkerFromEvent(event)?.ipc;
targets.push(workerIpc);
}
const target = targets.find(target => (target as any)?._invokeHandlers.has(channel));
if (target) {
const handler = (target as any)._invokeHandlers.get(channel);
try {
replyWithResult(await Promise.resolve(handler(event, ...args)));
} catch (err) {
replyWithError(err as Error);
if (internal) {
targets.push(ipcMainInternal);
} else if (event.type === 'frame') {
targets.push(...getIpcEmittersForFrameEvent(event));
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
const workerIpc = getServiceWorkerFromEvent(event)?.ipc;
targets.push(workerIpc);
}
} else {
replyWithError(new Error(`No handler registered for '${channel}'`));
}
} as any);
api.on('-ipc-message-sync' as any, function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
addReturnValueToEvent(event);
if (internal) {
ipcMainInternal.emit(channel, event, ...args);
} else if (event.type === 'frame') {
addReplyToEvent(event);
const webContents = event.sender;
const ipcEmitters = getIpcEmittersForFrameEvent(event);
if (
webContents.listenerCount('ipc-message-sync') === 0 &&
ipcEmitters.every(emitter => !emitter || emitter.listenerCount(channel) === 0)
) {
console.warn(`WebContents #${webContents.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`);
const target = targets.find((target) => (target as any)?._invokeHandlers.has(channel));
if (target) {
const handler = (target as any)._invokeHandlers.get(channel);
try {
replyWithResult(await Promise.resolve(handler(event, ...args)));
} catch (err) {
replyWithError(err as Error);
}
} else {
replyWithError(new Error(`No handler registered for '${channel}'`));
}
webContents.emit('ipc-message-sync', event, channel, ...args);
for (const ipcEmitter of ipcEmitters) {
ipcEmitter?.emit(channel, event, ...args);
} as any
);
api.on(
'-ipc-message-sync' as any,
function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, args: any[]) {
const internal = v8Util.getHiddenValue<boolean>(event, 'internal');
addReturnValueToEvent(event);
if (internal) {
ipcMainInternal.emit(channel, event, ...args);
} else if (event.type === 'frame') {
addReplyToEvent(event);
const webContents = event.sender;
const ipcEmitters = getIpcEmittersForFrameEvent(event);
if (
webContents.listenerCount('ipc-message-sync') === 0 &&
ipcEmitters.every((emitter) => !emitter || emitter.listenerCount(channel) === 0)
) {
console.warn(
`WebContents #${webContents.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`
);
}
webContents.emit('ipc-message-sync', event, channel, ...args);
for (const ipcEmitter of ipcEmitters) {
ipcEmitter?.emit(channel, event, ...args);
}
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, ...args);
}
} else if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, ...args);
}
} as any);
} as any
);
api.on('-ipc-message-host', function (event: Electron.IpcMainEvent, channel: string, args: any[]) {
event.sender.emit('-ipc-message-host', event, channel, args);
});
api.on('-ipc-ports' as any, function (event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent, channel: string, message: any, ports: any[]) {
event.ports = ports.map(p => new MessagePortMain(p));
if (event.type === 'frame') {
const ipcEmitters = getIpcEmittersForFrameEvent(event);
for (const ipcEmitter of ipcEmitters) {
ipcEmitter?.emit(channel, event, message);
api.on(
'-ipc-ports' as any,
function (
event: Electron.IpcMainEvent | Electron.IpcMainServiceWorkerEvent,
channel: string,
message: any,
ports: any[]
) {
event.ports = ports.map((p) => new MessagePortMain(p));
if (event.type === 'frame') {
const ipcEmitters = getIpcEmittersForFrameEvent(event);
for (const ipcEmitter of ipcEmitters) {
ipcEmitter?.emit(channel, event, message);
}
}
} if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, message);
}
} as any);
if (event.type === 'service-worker') {
addServiceWorkerPropertyToEvent(event);
getServiceWorkerFromEvent(event)?.ipc.emit(channel, event, message);
}
} as any
);
}

View File

@@ -5,7 +5,7 @@ import { EventEmitter } from 'events';
export class IpcMainImpl extends EventEmitter implements Electron.IpcMain {
private _invokeHandlers: Map<string, (e: IpcMainInvokeEvent, ...args: any[]) => void> = new Map();
constructor () {
constructor() {
super();
// Do not throw exception when channel name is "error".
@@ -29,7 +29,7 @@ export class IpcMainImpl extends EventEmitter implements Electron.IpcMain {
});
};
removeHandler (method: string) {
removeHandler(method: string) {
this._invokeHandlers.delete(method);
}
}

View File

@@ -1,8 +1,8 @@
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
type IPCHandler = (event: ElectronInternal.IpcMainInternalEvent, ...args: any[]) => any
type IPCHandler = (event: ElectronInternal.IpcMainInternalEvent, ...args: any[]) => any;
export const handleSync = function <T extends IPCHandler> (channel: string, handler: T) {
export const handleSync = function <T extends IPCHandler>(channel: string, handler: T) {
ipcMainInternal.on(channel, async (event, ...args) => {
try {
event.returnValue = [null, await handler(event, ...args)];
@@ -14,11 +14,11 @@ export const handleSync = function <T extends IPCHandler> (channel: string, hand
let nextId = 0;
export function invokeInWebContents<T> (sender: Electron.WebContents, command: string, ...args: any[]) {
export function invokeInWebContents<T>(sender: Electron.WebContents, command: string, ...args: any[]) {
return new Promise<T>((resolve, reject) => {
const requestId = ++nextId;
const channel = `${command}_RESPONSE_${requestId}`;
ipcMainInternal.on(channel, function handler (event, error: Error, result: any) {
ipcMainInternal.on(channel, function handler(event, error: Error, result: any) {
if (event.type !== 'frame' || event.sender !== sender) {
console.error(`Reply to ${command} sent by unexpected sender`);
return;
@@ -37,12 +37,12 @@ export function invokeInWebContents<T> (sender: Electron.WebContents, command: s
});
}
export function invokeInWebFrameMain<T> (sender: Electron.WebFrameMain, command: string, ...args: any[]) {
export function invokeInWebFrameMain<T>(sender: Electron.WebFrameMain, command: string, ...args: any[]) {
return new Promise<T>((resolve, reject) => {
const requestId = ++nextId;
const channel = `${command}_RESPONSE_${requestId}`;
const frameTreeNodeId = sender.frameTreeNodeId;
ipcMainInternal.on(channel, function handler (event, error: Error, result: any) {
ipcMainInternal.on(channel, function handler(event, error: Error, result: any) {
if (event.type !== 'frame' || event.frameTreeNodeId !== frameTreeNodeId) {
console.error(`Reply to ${command} sent by unexpected sender`);
return;

View File

@@ -2,26 +2,28 @@ import { EventEmitter } from 'events';
export class MessagePortMain extends EventEmitter implements Electron.MessagePortMain {
_internalPort: any;
constructor (internalPort: any) {
constructor(internalPort: any) {
super();
this._internalPort = internalPort;
this._internalPort.emit = (channel: string, event: {ports: any[]}) => {
if (channel === 'message') { event = { ...event, ports: event.ports.map(p => new MessagePortMain(p)) }; }
this._internalPort.emit = (channel: string, event: { ports: any[] }) => {
if (channel === 'message') {
event = { ...event, ports: event.ports.map((p) => new MessagePortMain(p)) };
}
this.emit(channel, event);
};
}
start () {
start() {
return this._internalPort.start();
}
close () {
close() {
return this._internalPort.close();
}
postMessage (...args: any[]) {
postMessage(...args: any[]) {
if (Array.isArray(args[1])) {
args[1] = args[1].map((o: any) => o instanceof MessagePortMain ? o._internalPort : o);
args[1] = args[1].map((o: any) => (o instanceof MessagePortMain ? o._internalPort : o));
}
return this._internalPort.postMessage(...args);
}

View File

@@ -6,14 +6,15 @@ import { BrowserWindowConstructorOptions } from 'electron/main';
type RequiredBrowserWindowConstructorOptions = Required<BrowserWindowConstructorOptions>;
type IntegerBrowserWindowOptionKeys = {
[K in keyof RequiredBrowserWindowConstructorOptions]:
RequiredBrowserWindowConstructorOptions[K] extends number ? K : never
[K in keyof RequiredBrowserWindowConstructorOptions]: RequiredBrowserWindowConstructorOptions[K] extends number
? K
: never;
}[keyof RequiredBrowserWindowConstructorOptions];
// This could be an array of keys, but an object allows us to add a compile-time
// check validating that we haven't added an integer property to
// BrowserWindowConstructorOptions that this module doesn't know about.
const keysOfTypeNumberCompileTimeCheck: { [K in IntegerBrowserWindowOptionKeys] : true } = {
const keysOfTypeNumberCompileTimeCheck: { [K in IntegerBrowserWindowOptionKeys]: true } = {
x: true,
y: true,
width: true,
@@ -31,7 +32,13 @@ const keysOfTypeNumberCompileTimeCheck: { [K in IntegerBrowserWindowOptionKeys]
// to `innerWidth` / `innerHeight`. However, our implementation currently incorrectly maps
// `width` and `height` to `outerWidth` and `outerHeight`, or the size of the window
// with all border and related window chrome.
const keysOfTypeNumber = new Set(['top', 'left', 'innerWidth', 'innerHeight', ...Object.keys(keysOfTypeNumberCompileTimeCheck)]);
const keysOfTypeNumber = new Set([
'top',
'left',
'innerWidth',
'innerHeight',
...Object.keys(keysOfTypeNumberCompileTimeCheck)
]);
/**
* Note that we only allow "0" and "1" boolean conversion when the type is known
@@ -41,7 +48,7 @@ const keysOfTypeNumber = new Set(['top', 'left', 'innerWidth', 'innerHeight', ..
* https://html.spec.whatwg.org/multipage/window-object.html#concept-window-open-features-parse-boolean
*/
type CoercedValue = string | number | boolean;
function coerce (key: string, value: string): CoercedValue {
function coerce(key: string, value: string): CoercedValue {
if (keysOfTypeNumber.has(key)) {
return parseInt(value, 10);
}
@@ -61,27 +68,89 @@ function coerce (key: string, value: string): CoercedValue {
}
}
export function parseCommaSeparatedKeyValue (source: string) {
export function parseCommaSeparatedKeyValue(source: string) {
const parsed = {} as { [key: string]: any };
for (const keyValuePair of source.split(',')) {
const [key, value] = keyValuePair.split('=').map(str => str.trim());
if (key) { parsed[key] = coerce(key, value); }
const [key, value] = keyValuePair.split('=').map((str) => str.trim());
if (key) {
parsed[key] = coerce(key, value);
}
}
return parsed;
}
export function parseWebViewWebPreferences (preferences: string) {
export function parseWebViewWebPreferences(preferences: string) {
return parseCommaSeparatedKeyValue(preferences);
}
const allowedWebPreferences = ['zoomFactor', 'nodeIntegration', 'javascript', 'contextIsolation', 'webviewTag'] as const;
const allowedWebPreferences = [
'zoomFactor',
'nodeIntegration',
'javascript',
'contextIsolation',
'webviewTag'
] as const;
type AllowedWebPreference = (typeof allowedWebPreferences)[number];
// Top-level BrowserWindow options that may be set via the window.open()
// features string. Options not listed here are silently dropped; apps that
// need to pass other options should use setWindowOpenHandler in the main
// process.
const allowedWindowOptions = new Set<string>([
// standard window.open() position/size features
'top',
'left',
'innerWidth',
'innerHeight',
// numeric
'x',
'y',
'width',
'height',
'minWidth',
'minHeight',
'maxWidth',
'maxHeight',
'opacity',
// presentational booleans
'show',
'center',
'useContentSize',
'frame',
'transparent',
'hasShadow',
'movable',
'closable',
'focusable',
'minimizable',
'maximizable',
'fullscreenable',
'alwaysOnTop',
'skipTaskbar',
'modal',
'acceptFirstMouse',
'autoHideMenuBar',
'enableLargerThanScreen',
'paintWhenInitiallyHidden',
'roundedCorners',
'thickFrame',
'disableAutoHideCursor',
'hiddenInMissionControl',
// presentational strings (no filesystem/network side effects)
'title',
'backgroundColor',
'tabbingIdentifier',
'titleBarStyle',
'vibrancy',
'visualEffectState',
'backgroundMaterial'
]);
/**
* Parses a feature string that has the format used in window.open().
*/
export function parseFeatures (features: string) {
export function parseFeatures(features: string) {
const parsed = parseCommaSeparatedKeyValue(features);
const webPreferences: { [K in AllowedWebPreference]?: any } = {};
@@ -100,8 +169,15 @@ export function parseFeatures (features: string) {
if (parsed.left !== undefined) parsed.x = parsed.left;
if (parsed.top !== undefined) parsed.y = parsed.top;
const options: { [key: string]: CoercedValue } = {};
for (const key of Object.keys(parsed)) {
if (allowedWindowOptions.has(key)) {
options[key] = parsed[key];
}
}
return {
options: parsed as Omit<BrowserWindowConstructorOptions, 'webPreferences'>,
options: options as Omit<BrowserWindowConstructorOptions, 'webPreferences'>,
webPreferences
};
}

View File

@@ -54,19 +54,19 @@ const getPreloadScriptsFromEvent = (event: ElectronInternal.IpcMainInternalEvent
let preloadScripts = session.getPreloadScripts();
if (event.type === 'frame') {
preloadScripts = preloadScripts.filter(script => script.type === 'frame');
preloadScripts = preloadScripts.filter((script) => script.type === 'frame');
const webPrefPreload = event.sender._getPreloadScript();
if (webPrefPreload) preloadScripts.push(webPrefPreload);
} else if (event.type === 'service-worker') {
preloadScripts = preloadScripts.filter(script => script.type === 'service-worker');
preloadScripts = preloadScripts.filter((script) => script.type === 'service-worker');
} else {
throw new Error(`getPreloadScriptsFromEvent: event.type is invalid (${(event as any).type})`);
}
// TODO(samuelmaddock): Remove filter after Session.setPreloads is fully
// deprecated. The new API will prevent relative paths from being registered.
return preloadScripts.filter(script => path.isAbsolute(script.filePath));
return preloadScripts.filter((script) => path.isAbsolute(script.filePath));
};
const readPreloadScript = async function (script: Electron.PreloadScript): Promise<ElectronInternal.PreloadScript> {
@@ -103,7 +103,7 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event
ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_NONSANDBOX_LOAD, function (event) {
const preloadScripts = getPreloadScriptsFromEvent(event);
return { preloadPaths: preloadScripts.map(script => script.filePath) };
return { preloadPaths: preloadScripts.map((script) => script.filePath) };
});
ipcMainInternal.on(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, function (event, preloadPath: string, error: Error) {

View File

@@ -17,7 +17,14 @@ export const webViewEvents: Record<string, readonly string[]> = {
'did-start-navigation': ['url', 'isInPlace', 'isMainFrame', 'frameProcessId', 'frameRoutingId'],
'did-redirect-navigation': ['url', 'isInPlace', 'isMainFrame', 'frameProcessId', 'frameRoutingId'],
'did-navigate': ['url', 'httpResponseCode', 'httpStatusText'],
'did-frame-navigate': ['url', 'httpResponseCode', 'httpStatusText', 'isMainFrame', 'frameProcessId', 'frameRoutingId'],
'did-frame-navigate': [
'url',
'httpResponseCode',
'httpStatusText',
'isMainFrame',
'frameProcessId',
'frameRoutingId'
],
'did-navigate-in-page': ['url', 'isMainFrame', 'frameProcessId', 'frameRoutingId'],
'-focus-change': ['focus'],
close: [],

View File

@@ -1,16 +1,9 @@
import type {
ClientRequestConstructorOptions,
UploadProgress
} from 'electron/common';
import type { ClientRequestConstructorOptions, UploadProgress } from 'electron/common';
import { Readable, Writable } from 'stream';
import * as url from 'url';
const {
isValidHeaderName,
isValidHeaderValue,
createURLLoader
} = process._linkedBinding('electron_common_net');
const { isValidHeaderName, isValidHeaderValue, createURLLoader } = process._linkedBinding('electron_common_net');
const kHttpProtocols = new Set(['http:', 'https:']);
@@ -43,20 +36,20 @@ class IncomingMessage extends Readable {
_responseHead: NodeJS.ResponseHead;
_resume: (() => void) | null = null;
constructor (responseHead: NodeJS.ResponseHead) {
constructor(responseHead: NodeJS.ResponseHead) {
super();
this._responseHead = responseHead;
}
get statusCode () {
get statusCode() {
return this._responseHead.statusCode;
}
get statusMessage () {
get statusMessage() {
return this._responseHead.statusMessage;
}
get headers () {
get headers() {
const filteredHeaders: Record<string, string | string[]> = {};
const { headers, rawHeaders } = this._responseHead;
for (const [name, values] of Object.entries(headers)) {
@@ -65,11 +58,13 @@ class IncomingMessage extends Readable {
const cookies = rawHeaders.filter(({ key }) => key.toLowerCase() === 'set-cookie').map(({ value }) => value);
// keep set-cookie as an array per Node.js rules
// see https://nodejs.org/api/http.html#http_message_headers
if (cookies.length) { filteredHeaders['set-cookie'] = cookies; }
if (cookies.length) {
filteredHeaders['set-cookie'] = cookies;
}
return filteredHeaders;
}
get rawHeaders () {
get rawHeaders() {
const rawHeadersArr: string[] = [];
const { rawHeaders } = this._responseHead;
for (const header of rawHeaders) {
@@ -78,34 +73,34 @@ class IncomingMessage extends Readable {
return rawHeadersArr;
}
get httpVersion () {
get httpVersion() {
return `${this.httpVersionMajor}.${this.httpVersionMinor}`;
}
get httpVersionMajor () {
get httpVersionMajor() {
return this._responseHead.httpVersion.major;
}
get httpVersionMinor () {
get httpVersionMinor() {
return this._responseHead.httpVersion.minor;
}
get rawTrailers () {
get rawTrailers() {
throw new Error('HTTP trailers are not supported');
}
get trailers () {
get trailers() {
throw new Error('HTTP trailers are not supported');
}
_storeInternalData (chunk: Buffer | null, resume: (() => void) | null) {
_storeInternalData(chunk: Buffer | null, resume: (() => void) | null) {
// save the network callback for use in _pushInternalData
this._resume = resume;
this._data.push(chunk);
this._pushInternalData();
}
_pushInternalData () {
_pushInternalData() {
while (this._shouldPush && this._data.length > 0) {
const chunk = this._data.shift();
this._shouldPush = this.push(chunk);
@@ -121,7 +116,7 @@ class IncomingMessage extends Readable {
}
}
_read () {
_read() {
this._shouldPush = true;
this._pushInternalData();
}
@@ -130,17 +125,19 @@ class IncomingMessage extends Readable {
/** Writable stream that buffers up everything written to it. */
class SlurpStream extends Writable {
_data: Buffer;
constructor () {
constructor() {
super();
this._data = Buffer.alloc(0);
}
_write (chunk: Buffer, encoding: string, callback: () => void) {
_write(chunk: Buffer, encoding: string, callback: () => void) {
this._data = Buffer.concat([this._data, chunk]);
callback();
}
data () { return this._data; }
data() {
return this._data;
}
}
class ChunkedBodyStream extends Writable {
@@ -149,12 +146,12 @@ class ChunkedBodyStream extends Writable {
_pendingCallback?: (error?: Error) => void;
_clientRequest: ClientRequest;
constructor (clientRequest: ClientRequest) {
constructor(clientRequest: ClientRequest) {
super();
this._clientRequest = clientRequest;
}
_write (chunk: Buffer, encoding: string, callback: () => void) {
_write(chunk: Buffer, encoding: string, callback: () => void) {
if (this._downstream) {
this._downstream.write(chunk).then(callback, callback);
} else {
@@ -168,12 +165,12 @@ class ChunkedBodyStream extends Writable {
}
}
_final (callback: () => void) {
_final(callback: () => void) {
this._downstream!.done();
callback();
}
startReading (pipe: NodeJS.DataPipe) {
startReading(pipe: NodeJS.DataPipe) {
if (this._downstream) {
throw new Error('two startReading calls???');
}
@@ -198,7 +195,7 @@ class ChunkedBodyStream extends Writable {
type RedirectPolicy = 'manual' | 'follow' | 'error';
const kAllowNonHttpProtocols = Symbol('kAllowNonHttpProtocols');
export function allowAnyProtocol (opts: ClientRequestConstructorOptions): ClientRequestConstructorOptions {
export function allowAnyProtocol(opts: ClientRequestConstructorOptions): ClientRequestConstructorOptions {
return {
...opts,
[kAllowNonHttpProtocols]: true
@@ -206,12 +203,12 @@ export function allowAnyProtocol (opts: ClientRequestConstructorOptions): Client
}
type ExtraURLLoaderOptions = {
redirectPolicy: RedirectPolicy;
headers: Record<string, { name: string, value: string | string[] }>;
allowNonHttpProtocols: boolean;
}
redirectPolicy: RedirectPolicy;
headers: Record<string, { name: string; value: string | string[] }>;
allowNonHttpProtocols: boolean;
};
function validateHeader (name: any, value: any): void {
function validateHeader(name: any, value: any): void {
if (typeof name !== 'string') {
throw new TypeError('`name` should be a string in setHeader(name, value)');
}
@@ -226,7 +223,9 @@ function validateHeader (name: any, value: any): void {
}
}
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & ExtraURLLoaderOptions {
function parseOptions(
optionsIn: ClientRequestConstructorOptions | string
): NodeJS.CreateURLLoaderOptions & ExtraURLLoaderOptions {
const options: any = typeof optionsIn === 'string' ? new URL(optionsIn) : { ...optionsIn };
let urlStr: string = options.url || options.href;
@@ -276,7 +275,11 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
throw new TypeError('headers must be an object');
}
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }>, allowNonHttpProtocols: boolean } = {
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & {
redirectPolicy: RedirectPolicy;
headers: Record<string, { name: string; value: string | string[] }>;
allowNonHttpProtocols: boolean;
} = {
method: (options.method || 'GET').toUpperCase(),
url: urlStr,
redirectPolicy,
@@ -303,7 +306,9 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
if (process.type !== 'utility') {
const { Session } = process._linkedBinding('electron_browser_session');
if (options.session) {
if (!(options.session instanceof Session)) { throw new TypeError('`session` should be an instance of the Session class'); }
if (!(options.session instanceof Session)) {
throw new TypeError('`session` should be an instance of the Session class');
}
urlLoaderOptions.session = options.session;
} else if (options.partition) {
if (typeof options.partition === 'string') {
@@ -322,14 +327,16 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
_aborted: boolean = false;
_chunkedEncoding: boolean | undefined;
_body: Writable | undefined;
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { headers: Record<string, { name: string, value: string | string[] }> };
_urlLoaderOptions: NodeJS.CreateURLLoaderOptions & {
headers: Record<string, { name: string; value: string | string[] }>;
};
_redirectPolicy: RedirectPolicy;
_followRedirectCb?: () => void;
_uploadProgress?: { active: boolean, started: boolean, current: number, total: number };
_uploadProgress?: { active: boolean; started: boolean; current: number; total: number };
_urlLoader?: NodeJS.URLLoader;
_response?: IncomingMessage;
constructor (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
constructor(options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
super({ autoDestroy: true });
if (callback) {
@@ -341,16 +348,18 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
if (!urlLoaderOptions.allowNonHttpProtocols && !kHttpProtocols.has(urlObj.protocol)) {
throw new Error('ClientRequest only supports http: and https: protocols');
}
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) {
throw new Error('credentials: same-origin requires origin to be set');
}
this._urlLoaderOptions = urlLoaderOptions;
this._redirectPolicy = redirectPolicy;
}
get chunkedEncoding () {
get chunkedEncoding() {
return this._chunkedEncoding || false;
}
set chunkedEncoding (value: boolean) {
set chunkedEncoding(value: boolean) {
if (this._started) {
throw new Error('chunkedEncoding can only be set before the request is started');
}
@@ -366,9 +375,9 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
}
}
setHeader (name: string, value: string) {
setHeader(name: string, value: string) {
if (this._started || this._firstWrite) {
throw new Error('Can\'t set headers after they are sent');
throw new Error("Can't set headers after they are sent");
}
validateHeader(name, value);
@@ -376,30 +385,30 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
this._urlLoaderOptions.headers[key] = { name, value };
}
getHeader (name: string) {
getHeader(name: string) {
if (name == null) {
throw new Error('`name` is required for getHeader(name)');
}
const key = name.toLowerCase();
const header = this._urlLoaderOptions.headers[key];
return header && header.value as any;
return header && (header.value as any);
}
removeHeader (name: string) {
removeHeader(name: string) {
if (name == null) {
throw new Error('`name` is required for removeHeader(name)');
}
if (this._started || this._firstWrite) {
throw new Error('Can\'t remove headers after they are sent');
throw new Error("Can't remove headers after they are sent");
}
const key = name.toLowerCase();
delete this._urlLoaderOptions.headers[key];
}
_write (chunk: Buffer, encoding: BufferEncoding, callback: () => void) {
_write(chunk: Buffer, encoding: BufferEncoding, callback: () => void) {
this._firstWrite = true;
if (!this._body) {
this._body = new SlurpStream();
@@ -412,7 +421,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
this._body.write(chunk, encoding, callback);
}
_final (callback: () => void) {
_final(callback: () => void) {
if (this._body) {
// TODO: is this the right way to forward to another stream?
this._body.end(callback);
@@ -423,9 +432,9 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
}
}
_startRequest () {
_startRequest() {
this._started = true;
const stringifyValues = (obj: Record<string, { name: string, value: string | string[] }>) => {
const stringifyValues = (obj: Record<string, { name: string; value: string | string[] }>) => {
const ret: Record<string, string> = {};
for (const { name, value } of Object.values(obj)) {
ret[name] = value.toString();
@@ -440,14 +449,16 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.headers) };
this._urlLoader = createURLLoader(opts);
this._urlLoader.on('response-started', (event, finalUrl, responseHead) => {
const response = this._response = new IncomingMessage(responseHead);
const response = (this._response = new IncomingMessage(responseHead));
this.emit('response', response);
});
this._urlLoader.on('data', (event, data, resume) => {
this._response!._storeInternalData(Buffer.from(data), resume);
});
this._urlLoader.on('complete', () => {
if (this._response) { this._response._storeInternalData(null, null); }
if (this._response) {
this._response._storeInternalData(null, null);
}
});
this._urlLoader.on('error', (event, netErrorString) => {
const error = new Error(netErrorString);
@@ -466,10 +477,12 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
this._urlLoader.on('redirect', (event, redirectInfo, headers) => {
const { statusCode, newMethod, newUrl } = redirectInfo;
if (this._redirectPolicy === 'error') {
this._die(new Error('Attempted to redirect, but redirect policy was \'error\''));
this._die(new Error("Attempted to redirect, but redirect policy was 'error'"));
} else if (this._redirectPolicy === 'manual') {
let _followRedirect = false;
this._followRedirectCb = () => { _followRedirect = true; };
this._followRedirectCb = () => {
_followRedirect = true;
};
try {
this.emit('redirect', statusCode, newMethod, newUrl, headers);
} finally {
@@ -505,7 +518,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
});
}
followRedirect () {
followRedirect() {
if (this._followRedirectCb) {
this._followRedirectCb();
} else {
@@ -513,15 +526,17 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
}
}
abort () {
abort() {
if (!this._aborted) {
process.nextTick(() => { this.emit('abort'); });
process.nextTick(() => {
this.emit('abort');
});
}
this._aborted = true;
this._die();
}
_die (err?: Error) {
_die(err?: Error) {
// Node.js assumes that any stream which is ended is no longer capable of emitted events
// which is a faulty assumption for the case of an object that is acting like a stream
// (our urlRequest). If we don't emit here, this causes errors since we *do* expect
@@ -537,7 +552,7 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
}
}
getUploadProgress (): UploadProgress {
getUploadProgress(): UploadProgress {
return this._uploadProgress ? { ...this._uploadProgress } : { active: false, started: false, current: 0, total: 0 };
}
}

View File

@@ -5,7 +5,7 @@ const handleESModule = (loader: ElectronInternal.ModuleLoader) => () => {
};
// Attaches properties to |targetExports|.
export function defineProperties (targetExports: Object, moduleList: ElectronInternal.ModuleEntry[]) {
export function defineProperties(targetExports: Object, moduleList: ElectronInternal.ModuleEntry[]) {
const descriptors: PropertyDescriptorMap = {};
for (const module of moduleList) {
descriptors[module.name] = {

View File

@@ -2,13 +2,15 @@ type DeprecationHandler = (message: string) => void;
let deprecationHandler: DeprecationHandler | null = null;
export function warnOnce (oldName: string, newName?: string) {
return warnOnceMessage(newName
? `'${oldName}' is deprecated and will be removed. Please use '${newName}' instead.`
: `'${oldName}' is deprecated and will be removed.`);
export function warnOnce(oldName: string, newName?: string) {
return warnOnceMessage(
newName
? `'${oldName}' is deprecated and will be removed. Please use '${newName}' instead.`
: `'${oldName}' is deprecated and will be removed.`
);
}
export function warnOnceMessage (msg: string) {
export function warnOnceMessage(msg: string) {
let warned = false;
return () => {
if (!warned && !process.noDeprecation) {
@@ -18,21 +20,21 @@ export function warnOnceMessage (msg: string) {
};
}
export function setHandler (handler: DeprecationHandler | null): void {
export function setHandler(handler: DeprecationHandler | null): void {
deprecationHandler = handler;
}
export function getHandler (): DeprecationHandler | null {
export function getHandler(): DeprecationHandler | null {
return deprecationHandler;
}
export function warn (oldName: string, newName: string): void {
export function warn(oldName: string, newName: string): void {
if (!process.noDeprecation) {
log(`'${oldName}' is deprecated. Use '${newName}' instead.`);
}
}
export function log (message: string): void {
export function log(message: string): void {
if (typeof deprecationHandler === 'function') {
deprecationHandler(message);
} else if (process.throwDeprecation) {
@@ -45,8 +47,10 @@ export function log (message: string): void {
}
// remove a function with no replacement
export function removeFunction<T extends Function> (fn: T, removedName: string): T {
if (!fn) { throw new Error(`'${removedName} function' is invalid or does not exist.`); }
export function removeFunction<T extends Function>(fn: T, removedName: string): T {
if (!fn) {
throw new Error(`'${removedName} function' is invalid or does not exist.`);
}
// wrap the deprecated function to warn user
const warn = warnOnce(`${fn.name} function`);
@@ -57,7 +61,7 @@ export function removeFunction<T extends Function> (fn: T, removedName: string):
}
// change the name of a function
export function renameFunction<T extends Function> (fn: T, newName: string): T {
export function renameFunction<T extends Function>(fn: T, newName: string): T {
const warn = warnOnce(`${fn.name} function`, `${newName} function`);
return function (this: any) {
warn();
@@ -66,7 +70,12 @@ export function renameFunction<T extends Function> (fn: T, newName: string): T {
}
// change the name of an event
export function event (emitter: NodeJS.EventEmitter, oldName: string, newName: string, transformer: (...args: any[]) => any[] | undefined = (...args) => args) {
export function event(
emitter: NodeJS.EventEmitter,
oldName: string,
newName: string,
transformer: (...args: any[]) => any[] | undefined = (...args) => args
) {
const warn = newName.startsWith('-') /* internal event */
? warnOnce(`${oldName} event`)
: warnOnce(`${oldName} event`, `${newName} event`);
@@ -82,7 +91,11 @@ export function event (emitter: NodeJS.EventEmitter, oldName: string, newName: s
}
// remove a property with no replacement
export function removeProperty<T extends Object, K extends (keyof T & string)>(object: T, removedName: K, onlyForValues?: any[]): T {
export function removeProperty<T extends Object, K extends keyof T & string>(
object: T,
removedName: K,
onlyForValues?: any[]
): T {
// if the property's already been removed, warn about it
// eslint-disable-next-line no-proto
const info = Object.getOwnPropertyDescriptor((object as any).__proto__, removedName);
@@ -103,7 +116,7 @@ export function removeProperty<T extends Object, K extends (keyof T & string)>(o
warn();
return info.get!.call(object);
},
set: newVal => {
set: (newVal) => {
if (!onlyForValues || onlyForValues.includes(newVal)) {
warn();
}
@@ -113,12 +126,16 @@ export function removeProperty<T extends Object, K extends (keyof T & string)>(o
}
// change the name of a property
export function renameProperty<T extends Object, K extends (keyof T & string)>(object: T, oldName: string, newName: K): T {
export function renameProperty<T extends Object, K extends keyof T & string>(
object: T,
oldName: string,
newName: K
): T {
const warn = warnOnce(oldName, newName);
// if the new property isn't there yet,
// inject it and warn about it
if ((oldName in object) && !(newName in object)) {
if (oldName in object && !(newName in object)) {
warn();
object[newName] = (object as any)[oldName];
}
@@ -130,14 +147,14 @@ export function renameProperty<T extends Object, K extends (keyof T & string)>(o
warn();
return object[newName];
},
set: value => {
set: (value) => {
warn();
object[newName] = value;
}
});
}
export function moveAPI<T extends Function> (fn: T, oldUsage: string, newUsage: string): T {
export function moveAPI<T extends Function>(fn: T, oldUsage: string, newUsage: string): T {
const warn = warnOnce(oldUsage, newUsage);
return function (this: any) {
warn();

View File

@@ -3,7 +3,7 @@ import * as util from 'util';
import type * as stream from 'stream';
type AnyFn = (...args: any[]) => any
type AnyFn = (...args: any[]) => any;
// setImmediate and process.nextTick makes use of uv_check and uv_prepare to
// run the callbacks, however since we only run uv loop on requests, the
@@ -11,7 +11,7 @@ type AnyFn = (...args: any[]) => any
// which would delay the callbacks for arbitrary long time. So we should
// initiatively activate the uv loop once setImmediate and process.nextTick is
// called.
const wrapWithActivateUvLoop = function <T extends AnyFn> (func: T): T {
const wrapWithActivateUvLoop = function <T extends AnyFn>(func: T): T {
return wrap(func, function (func) {
return function (this: any, ...args: any[]) {
process.activateUvLoop();
@@ -26,7 +26,7 @@ const wrapWithActivateUvLoop = function <T extends AnyFn> (func: T): T {
*
* Refs: https://github.com/Microsoft/TypeScript/issues/1863
*/
function wrap <T extends AnyFn> (func: T, wrapper: (fn: AnyFn) => T) {
function wrap<T extends AnyFn>(func: T, wrapper: (fn: AnyFn) => T) {
const wrapped = wrapper(func);
if ((func as any)[util.promisify.custom]) {
(wrapped as any)[util.promisify.custom] = wrapper((func as any)[util.promisify.custom]);
@@ -55,8 +55,7 @@ timers.setInterval = wrapWithActivateUvLoop(timers.setInterval);
// only in the process that runs node event loop alongside chromium
// event loop. We skip renderer with nodeIntegration here because node globals
// are deleted in these processes, see renderer/init.js for reference.
if (process.type === 'browser' ||
process.type === 'utility') {
if (process.type === 'browser' || process.type === 'utility') {
global.setTimeout = timers.setTimeout;
global.setInterval = timers.setInterval;
}
@@ -69,7 +68,7 @@ if (process.platform === 'win32') {
Object.defineProperty(process, 'stdin', {
configurable: false,
enumerable: true,
get () {
get() {
return stdin;
}
});
@@ -108,7 +107,11 @@ const originalResolveFilename = Module._resolveFilename;
// renderer process regardless of the names, they're superficial for TypeScript
// only.
const electronModuleNames = new Set([
'electron', 'electron/main', 'electron/renderer', 'electron/common', 'electron/utility'
'electron',
'electron/main',
'electron/renderer',
'electron/common',
'electron/utility'
]);
Module._resolveFilename = function (request, parent, isMain, options) {
if (electronModuleNames.has(request)) {

View File

@@ -27,5 +27,5 @@ export const enum IPC_MESSAGES {
INSPECTOR_SELECT_FILE = 'INSPECTOR_SELECT_FILE',
IMPORT_SHARED_TEXTURE_TRANSFER_MAIN_TO_RENDERER = 'IMPORT_SHARED_TEXTURE_TRANSFER_MAIN_TO_RENDERER',
IMPORT_SHARED_TEXTURE_RELEASE_RENDERER_TO_MAIN = 'IMPORT_SHARED_TEXTURE_RELEASE_RENDERER_TO_MAIN',
IMPORT_SHARED_TEXTURE_RELEASE_RENDERER_TO_MAIN = 'IMPORT_SHARED_TEXTURE_RELEASE_RENDERER_TO_MAIN'
}

View File

@@ -14,15 +14,18 @@ class Timeout {
_id: ReturnType<typeof globalThis.setTimeout>;
_clearFn: (id: ReturnType<typeof globalThis.setTimeout>) => void;
constructor (id: ReturnType<typeof globalThis.setTimeout>, clearFn: (id: ReturnType<typeof globalThis.setTimeout>) => void) {
constructor(
id: ReturnType<typeof globalThis.setTimeout>,
clearFn: (id: ReturnType<typeof globalThis.setTimeout>) => void
) {
this._id = id;
this._clearFn = clearFn;
}
unref () {}
ref () {}
unref() {}
ref() {}
close () {
close() {
this._clearFn.call(globalThis, this._id);
}
}
@@ -64,7 +67,7 @@ export const active = function (item: EnrollableItem) {
const msecs = item._idleTimeout;
if (msecs !== undefined && msecs >= 0) {
item._idleTimeoutId = setTimeout(function onTimeout () {
item._idleTimeoutId = setTimeout(function onTimeout() {
if (item._onTimeout) item._onTimeout();
}, msecs);
}
@@ -80,23 +83,23 @@ const clearImmediateFallback = function (id: number) {
delete immediateIds[id];
};
export const setImmediate = typeof globalThis.setImmediate === 'function'
? globalThis.setImmediate
: function (fn: (...args: unknown[]) => void, ...rest: unknown[]) {
const id = nextImmediateId++;
export const setImmediate =
typeof globalThis.setImmediate === 'function'
? globalThis.setImmediate
: function (fn: (...args: unknown[]) => void, ...rest: unknown[]) {
const id = nextImmediateId++;
immediateIds[id] = true;
immediateIds[id] = true;
Promise.resolve().then(function onMicrotask () {
if (immediateIds[id]) {
fn(...rest);
clearImmediateFallback(id);
}
});
Promise.resolve().then(function onMicrotask() {
if (immediateIds[id]) {
fn(...rest);
clearImmediateFallback(id);
}
});
return id;
};
return id;
};
export const clearImmediate = typeof globalThis.clearImmediate === 'function'
? globalThis.clearImmediate
: clearImmediateFallback;
export const clearImmediate =
typeof globalThis.clearImmediate === 'function' ? globalThis.clearImmediate : clearImmediateFallback;

View File

@@ -58,13 +58,7 @@ export const syncMethods = new Set([
...navigationHistorySyncMethods
]);
export const properties = new Set([
'audioMuted',
'userAgent',
'zoomLevel',
'zoomFactor',
'frameRate'
]);
export const properties = new Set(['audioMuted', 'userAgent', 'zoomLevel', 'zoomFactor', 'frameRate']);
export const asyncMethods = new Set([
'capturePage',

View File

@@ -11,8 +11,4 @@ const _global = typeof globalThis !== 'undefined' ? globalThis.global : (self ||
const process = _global.process;
const Buffer = _global.Buffer;
export {
_global,
process,
Buffer
};
export { _global, process, Buffer };

View File

@@ -5,6 +5,7 @@ declare const isolatedApi: WebViewImplHooks;
if (isolatedApi.guestViewInternal) {
// Must setup the WebView element in main world.
const { setupWebView } = require('@electron/internal/renderer/web-view/web-view-element') as typeof webViewElementModule;
const { setupWebView } =
require('@electron/internal/renderer/web-view/web-view-element') as typeof webViewElementModule;
setupWebView(isolatedApi);
}

View File

@@ -12,9 +12,7 @@ const Module = require('module') as NodeJS.ModuleInternal;
const Promise: PromiseConstructor = global.Promise;
const envNoAsar = process.env.ELECTRON_NO_ASAR &&
process.type !== 'browser' &&
process.type !== 'renderer';
const envNoAsar = process.env.ELECTRON_NO_ASAR && process.type !== 'browser' && process.type !== 'renderer';
const isAsarDisabled = () => process.noAsar || envNoAsar;
const internalBinding = process.internalBinding!;
@@ -48,20 +46,15 @@ process._getOrCreateArchive = getOrCreateArchive;
const asarRe = /\.asar/i;
const {
getValidatedPath,
getOptions,
getDirent
} = __non_webpack_require__('internal/fs/utils') as typeof import('@node/lib/internal/fs/utils');
const { getValidatedPath, getOptions, getDirent } = __non_webpack_require__(
'internal/fs/utils'
) as typeof import('@node/lib/internal/fs/utils');
const {
assignFunctionName
} = __non_webpack_require__('internal/util') as typeof import('@node/lib/internal/util');
const { assignFunctionName } = __non_webpack_require__('internal/util') as typeof import('@node/lib/internal/util');
const {
validateBoolean,
validateFunction
} = __non_webpack_require__('internal/validators') as typeof import('@node/lib/internal/validators');
const { validateBoolean, validateFunction } = __non_webpack_require__(
'internal/validators'
) as typeof import('@node/lib/internal/validators');
// In the renderer node internals use the node global URL but we do not set that to be
// the global URL instance. We need to do instanceof checks against the internal URL impl
@@ -94,7 +87,7 @@ const gid = process.getgid?.() ?? 0;
const fakeTime = new Date();
function getDirents (p: string, { 0: names, 1: types }: any[][]): Dirent[] {
function getDirents(p: string, { 0: names, 1: types }: any[][]): Dirent[] {
for (let i = 0; i < names.length; i++) {
let type = types[i];
const info = splitPath(path.join(p, names[i]));
@@ -114,7 +107,7 @@ function getDirents (p: string, { 0: names, 1: types }: any[][]): Dirent[] {
enum AsarFileType {
kFile = (constants as any).UV_DIRENT_FILE,
kDirectory = (constants as any).UV_DIRENT_DIR,
kLink = (constants as any).UV_DIRENT_LINK,
kLink = (constants as any).UV_DIRENT_LINK
}
const fileTypeToMode = new Map<AsarFileType, number>([
@@ -126,7 +119,8 @@ const fileTypeToMode = new Map<AsarFileType, number>([
const asarStatsToFsStats = function (stats: NodeJS.AsarFileStat) {
const { Stats } = require('fs');
const mode = constants.S_IROTH | constants.S_IRGRP | constants.S_IRUSR | constants.S_IWUSR | fileTypeToMode.get(stats.type)!;
const mode =
constants.S_IROTH | constants.S_IRGRP | constants.S_IRUSR | constants.S_IWUSR | fileTypeToMode.get(stats.type)!;
return new Stats(
1, // dev
@@ -135,10 +129,10 @@ const asarStatsToFsStats = function (stats: NodeJS.AsarFileStat) {
uid,
gid,
0, // rdev
undefined, // blksize
4096, // blksize
++nextInode, // ino
stats.size,
undefined, // blocks,
Math.ceil(stats.size / 512), // blocks (512-byte units)
fakeTime.getTime(), // atim_msec
fakeTime.getTime(), // mtim_msec
fakeTime.getTime(), // ctim_msec
@@ -153,9 +147,9 @@ const enum AsarError {
INVALID_ARCHIVE = 'INVALID_ARCHIVE'
}
type AsarErrorObject = Error & { code?: string, errno?: number };
type AsarErrorObject = Error & { code?: string; errno?: number };
const createError = (errorType: AsarError, { asarPath, filePath }: { asarPath?: string, filePath?: string } = {}) => {
const createError = (errorType: AsarError, { asarPath, filePath }: { asarPath?: string; filePath?: string } = {}) => {
let error: AsarErrorObject;
switch (errorType) {
case AsarError.NOT_FOUND:
@@ -182,7 +176,12 @@ const createError = (errorType: AsarError, { asarPath, filePath }: { asarPath?:
return error;
};
const overrideAPISync = function (module: Record<string, any>, name: string, pathArgumentIndex?: number | null, fromAsync: boolean = false) {
const overrideAPISync = function (
module: Record<string, any>,
name: string,
pathArgumentIndex?: number | null,
fromAsync: boolean = false
) {
if (pathArgumentIndex == null) pathArgumentIndex = 0;
const old = module[name];
const func = function (this: any, ...args: any[]) {
@@ -251,7 +250,7 @@ const overrideAPI = function (module: Record<string, any>, name: string, pathArg
};
let crypto: typeof Crypto;
function validateBufferIntegrity (buffer: Buffer, integrity: NodeJS.AsarFileInfo['integrity']) {
function validateBufferIntegrity(buffer: Buffer, integrity: NodeJS.AsarFileInfo['integrity']) {
if (!integrity) return;
// Delay load crypto to improve app boot performance
@@ -317,7 +316,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
if (!archive) {
if (shouldThrowStatError(options)) {
throw createError(AsarError.INVALID_ARCHIVE, { asarPath });
};
}
return null;
}
@@ -325,7 +324,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
if (!stats) {
if (shouldThrowStatError(options)) {
throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
};
}
return null;
}
@@ -453,7 +452,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
fs.promises.realpath = util.promisify(fs.realpath.native);
const { exists: nativeExists } = fs;
fs.exists = function exists (pathArgument: string, callback: any) {
fs.exists = function exists(pathArgument: string, callback: any) {
let pathInfo: ReturnType<typeof splitPath>;
try {
pathInfo = splitPath(pathArgument);
@@ -471,11 +470,11 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return;
}
const pathExists = (archive.stat(filePath) !== false);
const pathExists = archive.stat(filePath) !== false;
nextTick(callback, [pathExists]);
};
fs.exists[util.promisify.custom] = function exists (pathArgument: string) {
fs.exists[util.promisify.custom] = function exists(pathArgument: string) {
const pathInfo = splitPath(pathArgument);
if (!pathInfo.isAsar) return nativeExists[util.promisify.custom](pathArgument);
const { asarPath, filePath } = pathInfo;
@@ -596,7 +595,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
}
};
function fsReadFileAsar (pathArgument: string, options: any, callback: any) {
function fsReadFileAsar(pathArgument: string, options: any, callback: any) {
const pathInfo = splitPath(pathArgument);
if (pathInfo.isAsar) {
const { asarPath, filePath } = pathInfo;
@@ -674,7 +673,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return p(pathArgument, options);
};
function readFileFromArchiveSync (
function readFileFromArchiveSync(
pathInfo: { asarPath: string; filePath: string },
options: any
): ReturnType<typeof readFileSync> {
@@ -686,7 +685,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
const info = archive.getFileInfo(filePath);
if (!info) throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
if (info.size === 0) return (options) ? '' : Buffer.alloc(0);
if (info.size === 0) return options ? '' : Buffer.alloc(0);
if (info.unpacked) {
const realPath = archive.copyFileOut(filePath);
return fs.readFileSync(realPath, options);
@@ -710,7 +709,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
logASARAccess(asarPath, filePath, info.offset);
fs.readSync(fd, buffer, 0, info.size, info.offset);
validateBufferIntegrity(buffer, info.integrity);
return (encoding) ? buffer.toString(encoding) : buffer;
return encoding ? buffer.toString(encoding) : buffer;
}
const { readFileSync } = fs;
@@ -721,12 +720,16 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return readFileFromArchiveSync(pathInfo, options);
};
type ReaddirOptions = { encoding: BufferEncoding | null; withFileTypes?: false, recursive?: false } | undefined | null;
type ReaddirOptions =
| { encoding: BufferEncoding | null; withFileTypes?: false; recursive?: false }
| undefined
| null;
type ReaddirCallback = (err: NodeJS.ErrnoException | null, files?: string[]) => void;
const processReaddirResult = (args: any) => (args.context.withFileTypes ? handleDirents(args) : handleFilePaths(args));
const processReaddirResult = (args: any) =>
args.context.withFileTypes ? handleDirents(args) : handleFilePaths(args);
function handleDirents ({ result, currentPath, context }: { result: any[], currentPath: string, context: any }) {
function handleDirents({ result, currentPath, context }: { result: any[]; currentPath: string; context: any }) {
const length = result[0].length;
for (let i = 0; i < length; i++) {
const resultPath = path.join(currentPath, result[0][i]);
@@ -751,7 +754,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
}
}
function handleFilePaths ({ result, currentPath, context }: { result: string[], currentPath: string, context: any }) {
function handleFilePaths({ result, currentPath, context }: { result: string[]; currentPath: string; context: any }) {
for (let i = 0; i < result.length; i++) {
const resultPath = path.join(currentPath, result[i]);
const relativeResultPath = path.relative(context.basePath, resultPath);
@@ -764,7 +767,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
}
}
function readdirRecursive (basePath: string, options: ReaddirOptions, callback: ReaddirCallback) {
function readdirRecursive(basePath: string, options: ReaddirOptions, callback: ReaddirCallback) {
const context = {
withFileTypes: Boolean(options!.withFileTypes),
encoding: options!.encoding,
@@ -775,7 +778,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
let i = 0;
function read (pathArg: string) {
function read(pathArg: string) {
const req = new binding.FSReqCallback();
req.oncomplete = (err: any, result: string) => {
if (err) {
@@ -824,7 +827,8 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
// native call to readdir withFileTypes i.e. an array of arrays.
if (context.withFileTypes) {
readdirResult = [
[...readdirResult], readdirResult.map((p: string) => {
[...readdirResult],
readdirResult.map((p: string) => {
return internalBinding('fs').internalModuleStat(path.join(pathArg, p));
})
];
@@ -842,12 +846,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
callback(null, context.readdirResults);
}
} else {
binding.readdir(
pathArg,
context.encoding,
context.withFileTypes,
req
);
binding.readdir(pathArg, context.encoding, context.withFileTypes, req);
}
}
@@ -1023,11 +1022,11 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
const stats = archive.stat(filePath);
if (!stats) return -34;
return (stats.type === AsarFileType.kDirectory) ? 1 : 0;
return stats.type === AsarFileType.kDirectory ? 1 : 0;
};
const { kUsePromises } = binding;
async function readdirRecursivePromises (originalPath: string, options: ReaddirOptions) {
async function readdirRecursivePromises(originalPath: string, options: ReaddirOptions) {
const result: any[] = [];
const pathInfo = splitPath(originalPath);
@@ -1046,7 +1045,8 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
initialItem = files;
if (withFileTypes) {
initialItem = [
[...initialItem], initialItem.map((p: string) => {
[...initialItem],
initialItem.map((p: string) => {
return internalBinding('fs').internalModuleStat(path.join(originalPath, p));
})
];
@@ -1079,17 +1079,13 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
if (!files) continue;
readdirResult = [
[...files], files.map((p: string) => {
[...files],
files.map((p: string) => {
return internalBinding('fs').internalModuleStat(path.join(direntPath, p));
})
];
} else {
readdirResult = await binding.readdir(
direntPath,
options!.encoding,
true,
kUsePromises
);
readdirResult = await binding.readdir(direntPath, options!.encoding, true, kUsePromises);
}
queue.push([direntPath, readdirResult]);
}
@@ -1114,12 +1110,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
if (!files) return result;
item = files;
} else {
item = await binding.readdir(
path.toNamespacedPath(direntPath),
options!.encoding,
false,
kUsePromises
);
item = await binding.readdir(path.toNamespacedPath(direntPath), options!.encoding, false, kUsePromises);
}
queue.push([direntPath, item]);
}
@@ -1130,7 +1121,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return result;
}
function readdirSyncRecursive (basePath: string, options: ReaddirOptions) {
function readdirSyncRecursive(basePath: string, options: ReaddirOptions) {
const context = {
withFileTypes: Boolean(options!.withFileTypes),
encoding: options!.encoding,
@@ -1139,7 +1130,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
pathsQueue: [basePath]
};
function read (pathArg: string) {
function read(pathArg: string) {
let readdirResult;
const pathInfo = splitPath(pathArg);
@@ -1154,17 +1145,14 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
// native call to readdir withFileTypes i.e. an array of arrays.
if (context.withFileTypes) {
readdirResult = [
[...readdirResult], readdirResult.map((p: string) => {
[...readdirResult],
readdirResult.map((p: string) => {
return internalBinding('fs').internalModuleStat(path.join(pathArg, p));
})
];
}
} else {
readdirResult = binding.readdir(
path.toNamespacedPath(pathArg),
context.encoding,
context.withFileTypes
);
readdirResult = binding.readdir(path.toNamespacedPath(pathArg), context.encoding, context.withFileTypes);
}
if (readdirResult === undefined) {
@@ -1215,7 +1203,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
};
}
function invokeWithNoAsar (func: Function) {
function invokeWithNoAsar(func: Function) {
return function (this: any) {
const processNoAsarOriginalValue = process.noAsar;
process.noAsar = true;
@@ -1247,7 +1235,10 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
// command as a single path to an archive.
const { exec, execSync } = childProcess;
childProcess.exec = invokeWithNoAsar(exec);
childProcess.exec[util.promisify.custom] = assignFunctionName('exec', invokeWithNoAsar(exec[util.promisify.custom]));
childProcess.exec[util.promisify.custom] = assignFunctionName(
'exec',
invokeWithNoAsar(exec[util.promisify.custom])
);
childProcess.execSync = invokeWithNoAsar(execSync);
overrideAPI(childProcess, 'execFile');

View File

@@ -25,8 +25,11 @@ cp.fork = (modulePath, args?, options?: cp.ForkOptions) => {
args = [];
}
// Fallback to original fork to report arg type errors.
if (typeof modulePath !== 'string' || !Array.isArray(args) ||
(typeof options !== 'object' && typeof options !== 'undefined')) {
if (
typeof modulePath !== 'string' ||
!Array.isArray(args) ||
(typeof options !== 'object' && typeof options !== 'undefined')
) {
return originalFork(modulePath, args, options);
}
// When forking a child script, we setup a special environment to make

View File

@@ -1,22 +1,23 @@
import '@electron/internal/sandboxed_renderer/pre-init';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils';
import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload';
import {
createPreloadProcessObject,
executeSandboxedPreloadScripts
} from '@electron/internal/sandboxed_renderer/preload';
import * as events from 'events';
declare const binding: {
get: (name: string) => any;
process: NodeJS.Process;
createPreloadScript: (src: string) => Function
createPreloadScript: (src: string) => Function;
};
const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const ipcRendererUtils =
require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const {
preloadScripts,
process: processProps
} = ipcRendererUtils.invokeSync<{
const { preloadScripts, process: processProps } = ipcRendererUtils.invokeSync<{
preloadScripts: ElectronInternal.PreloadScript[];
process: NodeJS.Process;
}>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
@@ -44,15 +45,18 @@ Object.assign(process, processProps);
require('@electron/internal/renderer/ipc-native-setup');
executeSandboxedPreloadScripts({
loadedModules,
loadableModules,
process: preloadProcess,
createPreloadScript: binding.createPreloadScript,
exposeGlobals: {
Buffer,
// FIXME(samuelmaddock): workaround webpack bug replacing this with just
// `__webpack_require__.g,` which causes script error
global: globalThis
}
}, preloadScripts);
executeSandboxedPreloadScripts(
{
loadedModules,
loadableModules,
process: preloadProcess,
createPreloadScript: binding.createPreloadScript,
exposeGlobals: {
Buffer,
// FIXME(samuelmaddock): workaround webpack bug replacing this with just
// `__webpack_require__.g,` which causes script error
global: globalThis
}
},
preloadScripts
);

View File

@@ -1,15 +1,15 @@
const binding = process._linkedBinding('electron_renderer_crash_reporter');
export default {
addExtraParameter (key: string, value: string) {
addExtraParameter(key: string, value: string) {
binding.addExtraParameter(key, value);
},
removeExtraParameter (key: string) {
removeExtraParameter(key: string) {
binding.removeExtraParameter(key);
},
getParameters () {
getParameters() {
return binding.getParameters();
}
};

View File

@@ -6,19 +6,19 @@ const ipc = getIPCRenderer();
const internal = false;
class IpcRenderer extends EventEmitter implements Electron.IpcRenderer {
send (channel: string, ...args: any[]) {
send(channel: string, ...args: any[]) {
return ipc.send(internal, channel, args);
}
sendSync (channel: string, ...args: any[]) {
sendSync(channel: string, ...args: any[]) {
return ipc.sendSync(internal, channel, args);
}
sendToHost (channel: string, ...args: any[]) {
sendToHost(channel: string, ...args: any[]) {
return ipc.sendToHost(channel, args);
}
async invoke (channel: string, ...args: any[]) {
async invoke(channel: string, ...args: any[]) {
const { error, result } = await ipc.invoke(internal, channel, args);
if (error) {
throw new Error(`Error invoking remote method '${channel}': ${error}`);
@@ -26,7 +26,7 @@ class IpcRenderer extends EventEmitter implements Electron.IpcRenderer {
return result;
}
postMessage (channel: string, message: any, transferables: any) {
postMessage(channel: string, message: any, transferables: any) {
return ipc.postMessage(channel, message, transferables);
}
}

View File

@@ -14,17 +14,12 @@ Object.defineProperty(WebFramePrototype, 'routingId', {
configurable: true,
get: function (this: Electron.WebFrame) {
routingIdDeprecated();
return ipcRendererUtils.invokeSync<number>(
IPC_MESSAGES.BROWSER_GET_FRAME_ROUTING_ID_SYNC,
this.frameToken
);
return ipcRendererUtils.invokeSync<number>(IPC_MESSAGES.BROWSER_GET_FRAME_ROUTING_ID_SYNC, this.frameToken);
}
});
const findFrameByRoutingIdDeprecated = deprecate.warnOnce('webFrame.findFrameByRoutingId', 'webFrame.findFrameByToken');
WebFramePrototype.findFrameByRoutingId = function (
routingId: number
): Electron.WebFrame | null {
WebFramePrototype.findFrameByRoutingId = function (routingId: number): Electron.WebFrame | null {
findFrameByRoutingIdDeprecated();
const frameToken = ipcRendererUtils.invokeSync<string | undefined>(
IPC_MESSAGES.BROWSER_GET_FRAME_TOKEN_SYNC,

View File

@@ -43,6 +43,7 @@ webFrameInit();
// Warn about security issues
if (process.isMainFrame) {
const { securityWarnings } = require('@electron/internal/renderer/security-warnings') as typeof securityWarningsModule;
const { securityWarnings } =
require('@electron/internal/renderer/security-warnings') as typeof securityWarningsModule;
securityWarnings(nodeIntegration);
}

View File

@@ -12,7 +12,9 @@ const Module = require('module') as NodeJS.ModuleInternal;
const originalModuleLoad = Module._load;
Module._load = function (request: string) {
if (request === 'vm') {
console.warn('The vm module of Node.js is unsupported in Electron\'s renderer process due to incompatibilities with the Blink rendering engine. Crashes are likely and avoiding the module is highly recommended. This module may be removed in a future release.');
console.warn(
"The vm module of Node.js is unsupported in Electron's renderer process due to incompatibilities with the Blink rendering engine. Crashes are likely and avoiding the module is highly recommended. This module may be removed in a future release."
);
}
return originalModuleLoad.apply(this, arguments as any);
};
@@ -33,9 +35,9 @@ Module._load = function (request: string) {
// variables to this wrapper please ensure to update that plugin as well.
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname, process, global, Buffer) { ' +
// By running the code in a new closure, it would be possible for the module
// code to override "process" and "Buffer" with local variables.
'return function (exports, require, module, __filename, __dirname) { ',
// By running the code in a new closure, it would be possible for the module
// code to override "process" and "Buffer" with local variables.
'return function (exports, require, module, __filename, __dirname) { ',
'\n}.call(this, exports, require, module, __filename, __dirname); });'
];
@@ -46,8 +48,10 @@ process.argv.splice(1, 1);
// Import common settings.
require('@electron/internal/common/init');
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const { ipcRendererInternal } =
require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
const ipcRendererUtils =
require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
process.getProcessMemoryInfo = () => {
return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
@@ -65,7 +69,9 @@ require('@electron/internal/renderer/common-init');
if (nodeIntegration) {
// Export node bindings to global.
const { makeRequireFunction } = __non_webpack_require__('internal/modules/helpers') as typeof import('@node/lib/internal/modules/helpers');
const { makeRequireFunction } = __non_webpack_require__(
'internal/modules/helpers'
) as typeof import('@node/lib/internal/modules/helpers');
global.module = new Module('electron/js2c/renderer_init');
global.require = makeRequireFunction(global.module) as NodeRequire;
@@ -136,8 +142,8 @@ const { appCodeLoaded } = process;
delete process.appCodeLoaded;
const { preloadPaths } = ipcRendererUtils.invokeSync<{ preloadPaths: string[] }>(IPC_MESSAGES.BROWSER_NONSANDBOX_LOAD);
const cjsPreloads = preloadPaths.filter(p => path.extname(p) !== '.mjs');
const esmPreloads = preloadPaths.filter(p => path.extname(p) === '.mjs');
const cjsPreloads = preloadPaths.filter((p) => path.extname(p) !== '.mjs');
const esmPreloads = preloadPaths.filter((p) => path.extname(p) === '.mjs');
if (cjsPreloads.length) {
// Load the preload scripts.
for (const preloadScript of cjsPreloads) {
@@ -152,17 +158,21 @@ if (cjsPreloads.length) {
}
}
if (esmPreloads.length) {
const { runEntryPointWithESMLoader } = __non_webpack_require__('internal/modules/run_main') as typeof import('@node/lib/internal/modules/run_main');
const { runEntryPointWithESMLoader } = __non_webpack_require__(
'internal/modules/run_main'
) as typeof import('@node/lib/internal/modules/run_main');
runEntryPointWithESMLoader(async (cascadedLoader: any) => {
// Load the preload scripts.
for (const preloadScript of esmPreloads) {
await cascadedLoader.import(pathToFileURL(preloadScript).toString(), undefined, Object.create(null)).catch((err: Error) => {
console.error(`Unable to load preload script: ${preloadScript}`);
console.error(err);
await cascadedLoader
.import(pathToFileURL(preloadScript).toString(), undefined, Object.create(null))
.catch((err: Error) => {
console.error(`Unable to load preload script: ${preloadScript}`);
console.error(err);
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, preloadScript, err);
});
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, preloadScript, err);
});
}
}).finally(() => appCodeLoaded!());
} else {

View File

@@ -8,13 +8,14 @@ import { webFrame } from 'electron/renderer';
const { contextIsolationEnabled } = internalContextBridge;
/* Corrects for some Inspector adaptations needed in Electron.
* 1) Use menu API to show context menu.
*/
* 1) Use menu API to show context menu.
*/
window.onload = function () {
if (contextIsolationEnabled) {
internalContextBridge.tryOverrideGlobalValueFromIsolatedWorld([
'InspectorFrontendHost', 'showContextMenuAtPoint'
], createMenu);
internalContextBridge.tryOverrideGlobalValueFromIsolatedWorld(
['InspectorFrontendHost', 'showContextMenuAtPoint'],
createMenu
);
} else {
window.InspectorFrontendHost!.showContextMenuAtPoint = createMenu;
}
@@ -26,16 +27,19 @@ window.confirm = function (message?: string, title?: string) {
};
const useEditMenuItems = function (x: number, y: number, items: ContextMenuItem[]) {
return items.length === 0 && document.elementsFromPoint(x, y).some(element => {
return element.nodeName === 'INPUT' ||
element.nodeName === 'TEXTAREA' ||
(element as HTMLElement).isContentEditable;
});
return (
items.length === 0 &&
document.elementsFromPoint(x, y).some((element) => {
return (
element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA' || (element as HTMLElement).isContentEditable
);
})
);
};
const createMenu = function (x: number, y: number, items: ContextMenuItem[]) {
const isEditMenu = useEditMenuItems(x, y, items);
ipcRendererInternal.invoke<number>(IPC_MESSAGES.INSPECTOR_CONTEXT_MENU, items, isEditMenu).then(id => {
ipcRendererInternal.invoke<number>(IPC_MESSAGES.INSPECTOR_CONTEXT_MENU, items, isEditMenu).then((id) => {
if (typeof id === 'number') {
webFrame.executeJavaScript(`window.DevToolsAPI.contextMenuItemSelected(${JSON.stringify(id)})`);
}

View File

@@ -7,7 +7,7 @@ const v8Util = process._linkedBinding('electron_common_v8_util');
// ElectronApiServiceImpl will look for the "ipcNative" hidden object when
// invoking the 'onMessage' callback.
v8Util.setHiddenValue(globalThis, 'ipcNative', {
onMessage (internal: boolean, channel: string, ports: MessagePort[], args: any[]) {
onMessage(internal: boolean, channel: string, ports: MessagePort[], args: any[]) {
const sender = internal ? ipcRendererInternal : ipcRenderer;
sender.emit(channel, { sender, ports }, ...args);
}

View File

@@ -3,7 +3,7 @@ let ipc: NodeJS.IpcRendererImpl | undefined;
/**
* Get IPCRenderer implementation for the current process.
*/
export function getIPCRenderer () {
export function getIPCRenderer() {
if (ipc) return ipc;
const ipcBinding = process._linkedBinding('electron_renderer_ipc');
switch (process.type) {
@@ -14,4 +14,4 @@ export function getIPCRenderer () {
default:
throw new Error(`Cannot create IPCRenderer for '${process.type}' process`);
}
};
}

View File

@@ -1,8 +1,8 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
type IPCHandler = (event: Electron.IpcRendererEvent, ...args: any[]) => any
type IPCHandler = (event: Electron.IpcRendererEvent, ...args: any[]) => any;
export const handle = function <T extends IPCHandler> (channel: string, handler: T) {
export const handle = function <T extends IPCHandler>(channel: string, handler: T) {
ipcRendererInternal.on(channel, async (event, requestId, ...args) => {
const replyChannel = `${channel}_RESPONSE_${requestId}`;
try {
@@ -13,7 +13,7 @@ export const handle = function <T extends IPCHandler> (channel: string, handler:
});
};
export function invokeSync<T> (command: string, ...args: any[]): T {
export function invokeSync<T>(command: string, ...args: any[]): T {
const [error, result] = ipcRendererInternal.sendSync(command, ...args);
if (error) {

View File

@@ -6,21 +6,21 @@ const ipc = getIPCRenderer();
const internal = true;
class IpcRendererInternal extends EventEmitter implements ElectronInternal.IpcRendererInternal {
send (channel: string, ...args: any[]) {
send(channel: string, ...args: any[]) {
return ipc.send(internal, channel, args);
}
sendSync (channel: string, ...args: any[]) {
sendSync(channel: string, ...args: any[]) {
return ipc.sendSync(internal, channel, args);
}
async invoke<T> (channel: string, ...args: any[]) {
async invoke<T>(channel: string, ...args: any[]) {
const { error, result } = await ipc.invoke<T>(internal, channel, args);
if (error) {
throw new Error(`Error invoking remote method '${channel}': ${error}`);
}
return result;
};
}
}
export const ipcRendererInternal = new IpcRendererInternal();

View File

@@ -22,8 +22,7 @@ const shouldLogSecurityWarnings = function (): boolean {
switch (platform) {
case 'darwin':
shouldLog = execPath.endsWith('MacOS/Electron') ||
execPath.includes('Electron.app/Contents/Frameworks/');
shouldLog = execPath.endsWith('MacOS/Electron') || execPath.includes('Electron.app/Contents/Frameworks/');
break;
case 'freebsd':
case 'linux':
@@ -36,13 +35,11 @@ const shouldLogSecurityWarnings = function (): boolean {
shouldLog = false;
}
if ((env && env.ELECTRON_DISABLE_SECURITY_WARNINGS) ||
(window && window.ELECTRON_DISABLE_SECURITY_WARNINGS)) {
if ((env && env.ELECTRON_DISABLE_SECURITY_WARNINGS) || (window && window.ELECTRON_DISABLE_SECURITY_WARNINGS)) {
shouldLog = false;
}
if ((env && env.ELECTRON_ENABLE_SECURITY_WARNINGS) ||
(window && window.ELECTRON_ENABLE_SECURITY_WARNINGS)) {
if ((env && env.ELECTRON_ENABLE_SECURITY_WARNINGS) || (window && window.ELECTRON_ENABLE_SECURITY_WARNINGS)) {
shouldLog = true;
}
@@ -97,10 +94,8 @@ const warnAboutInsecureResources = function () {
return;
}
const isLocal = (url: URL): boolean =>
['localhost', '127.0.0.1', '[::1]', ''].includes(url.hostname);
const isInsecure = (url: URL): boolean =>
['http:', 'ftp:'].includes(url.protocol) && !isLocal(url);
const isLocal = (url: URL): boolean => ['localhost', '127.0.0.1', '[::1]', ''].includes(url.hostname);
const isInsecure = (url: URL): boolean => ['http:', 'ftp:'].includes(url.protocol) && !isLocal(url);
const resources = window.performance
.getEntriesByType('resource')
@@ -117,8 +112,7 @@ const warnAboutInsecureResources = function () {
Consider loading the following resources over HTTPS or FTPS. \n${resources}
\n${moreInformation}`;
console.warn('%cElectron Security Warning (Insecure Resources)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (Insecure Resources)', 'font-weight: bold;', warning);
};
/**
@@ -135,8 +129,11 @@ const warnAboutNodeWithRemoteContent = function (nodeIntegration: boolean) {
and attempted to load remote content from '${window.location}'. This
exposes users of this app to severe security risks.\n${moreInformation}`;
console.warn('%cElectron Security Warning (Node.js Integration with Remote Content)',
'font-weight: bold;', warning);
console.warn(
'%cElectron Security Warning (Node.js Integration with Remote Content)',
'font-weight: bold;',
warning
);
}
};
@@ -157,8 +154,7 @@ const warnAboutDisabledWebSecurity = function (webPreferences?: Electron.WebPref
const warning = `This renderer process has "webSecurity" disabled. This
exposes users of this app to severe security risks.\n${moreInformation}`;
console.warn('%cElectron Security Warning (Disabled webSecurity)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (Disabled webSecurity)', 'font-weight: bold;', warning);
};
/**
@@ -174,8 +170,7 @@ const warnAboutInsecureCSP = function () {
Policy set or a policy with "unsafe-eval" enabled. This exposes users of
this app to unnecessary security risks.\n${moreInformation}`;
console.warn('%cElectron Security Warning (Insecure Content-Security-Policy)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (Insecure Content-Security-Policy)', 'font-weight: bold;', warning);
};
/**
@@ -190,8 +185,7 @@ const warnAboutInsecureContentAllowed = function (webPreferences?: Electron.WebP
enabled. This exposes users of this app to severe security risks.\n
${moreInformation}`;
console.warn('%cElectron Security Warning (allowRunningInsecureContent)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (allowRunningInsecureContent)', 'font-weight: bold;', warning);
};
/**
@@ -200,7 +194,7 @@ const warnAboutInsecureContentAllowed = function (webPreferences?: Electron.WebP
* Logs a warning message about experimental features.
*/
const warnAboutExperimentalFeatures = function (webPreferences?: Electron.WebPreferences) {
if (!webPreferences || (!webPreferences.experimentalFeatures)) {
if (!webPreferences || !webPreferences.experimentalFeatures) {
return;
}
@@ -208,8 +202,7 @@ const warnAboutExperimentalFeatures = function (webPreferences?: Electron.WebPre
This exposes users of this app to some security risk. If you do not need
this feature, you should disable it.\n${moreInformation}`;
console.warn('%cElectron Security Warning (experimentalFeatures)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (experimentalFeatures)', 'font-weight: bold;', warning);
};
/**
@@ -218,9 +211,11 @@ const warnAboutExperimentalFeatures = function (webPreferences?: Electron.WebPre
* Logs a warning message about enableBlinkFeatures
*/
const warnAboutEnableBlinkFeatures = function (webPreferences?: Electron.WebPreferences) {
if (!webPreferences ||
if (
!webPreferences ||
!Object.hasOwn(webPreferences, 'enableBlinkFeatures') ||
(webPreferences.enableBlinkFeatures != null && webPreferences.enableBlinkFeatures.length === 0)) {
(webPreferences.enableBlinkFeatures != null && webPreferences.enableBlinkFeatures.length === 0)
) {
return;
}
@@ -228,8 +223,7 @@ const warnAboutEnableBlinkFeatures = function (webPreferences?: Electron.WebPref
enabled. This exposes users of this app to some security risk. If you do not
need this feature, you should disable it.\n${moreInformation}`;
console.warn('%cElectron Security Warning (enableBlinkFeatures)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (enableBlinkFeatures)', 'font-weight: bold;', warning);
};
/**
@@ -250,8 +244,7 @@ const warnAboutAllowedPopups = function () {
BrowserWindows. If you do not need this feature, you should disable it.\n
${moreInformation}`;
console.warn('%cElectron Security Warning (allowpopups)',
'font-weight: bold;', warning);
console.warn('%cElectron Security Warning (allowpopups)', 'font-weight: bold;', warning);
}
};
@@ -261,9 +254,7 @@ const warnAboutAllowedPopups = function () {
// #13 Disable or limit creation of new windows
// #14 Do not use `openExternal` with untrusted content
const logSecurityWarnings = function (
webPreferences: Electron.WebPreferences | undefined, nodeIntegration: boolean
) {
const logSecurityWarnings = function (webPreferences: Electron.WebPreferences | undefined, nodeIntegration: boolean) {
warnAboutNodeWithRemoteContent(nodeIntegration);
warnAboutDisabledWebSecurity(webPreferences);
warnAboutInsecureResources();
@@ -282,7 +273,7 @@ const getWebPreferences = async function () {
}
};
export function securityWarnings (nodeIntegration: boolean) {
export function securityWarnings(nodeIntegration: boolean) {
const loadHandler = async function () {
if (shouldLogSecurityWarnings()) {
const webPreferences = await getWebPreferences();

View File

@@ -5,18 +5,18 @@ import { webFrame, WebFrame } from 'electron/renderer';
// All keys of WebFrame that extend Function
type WebFrameMethod = {
[K in keyof WebFrame]:
WebFrame[K] extends Function ? K : never
}
[K in keyof WebFrame]: WebFrame[K] extends Function ? K : never;
};
export const webFrameInit = () => {
// Call webFrame method
ipcRendererUtils.handle(IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, (
event, method: keyof WebFrameMethod, ...args: any[]
) => {
// The TypeScript compiler cannot handle the sheer number of
// call signatures here and simply gives up. Incorrect invocations
// will be caught by "keyof WebFrameMethod" though.
return (webFrame[method] as any)(...args);
});
ipcRendererUtils.handle(
IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD,
(event, method: keyof WebFrameMethod, ...args: any[]) => {
// The TypeScript compiler cannot handle the sheer number of
// call signatures here and simply gives up. Incorrect invocations
// will be caught by "keyof WebFrameMethod" though.
return (webFrame[method] as any)(...args);
}
);
};

View File

@@ -5,48 +5,61 @@ import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-inte
const { mainFrame: webFrame } = process._linkedBinding('electron_renderer_web_frame');
export interface GuestViewDelegate {
dispatchEvent (eventName: string, props: Record<string, any>): void;
dispatchEvent(eventName: string, props: Record<string, any>): void;
}
export function registerEvents (viewInstanceId: number, delegate: GuestViewDelegate) {
ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT}-${viewInstanceId}`, function (event, eventName, props) {
delegate.dispatchEvent(eventName, props);
});
export function registerEvents(viewInstanceId: number, delegate: GuestViewDelegate) {
ipcRendererInternal.on(
`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT}-${viewInstanceId}`,
function (event, eventName, props) {
delegate.dispatchEvent(eventName, props);
}
);
}
export function deregisterEvents (viewInstanceId: number) {
export function deregisterEvents(viewInstanceId: number) {
ipcRendererInternal.removeAllListeners(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT}-${viewInstanceId}`);
}
export function createGuest (iframe: HTMLIFrameElement, elementInstanceId: number, params: Record<string, any>): Promise<number> {
export function createGuest(
iframe: HTMLIFrameElement,
elementInstanceId: number,
params: Record<string, any>
): Promise<number> {
if (!(iframe instanceof HTMLIFrameElement)) {
throw new TypeError('Invalid embedder frame');
}
const embedderFrame = webFrame._findFrameByWindow(iframe.contentWindow!);
if (!embedderFrame) { // this error should not happen.
if (!embedderFrame) {
// this error should not happen.
throw new Error('Invalid embedder frame');
}
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_AND_ATTACH_GUEST, embedderFrame.frameToken, elementInstanceId, params);
return ipcRendererInternal.invoke(
IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_AND_ATTACH_GUEST,
embedderFrame.frameToken,
elementInstanceId,
params
);
}
export function detachGuest (guestInstanceId: number) {
export function detachGuest(guestInstanceId: number) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_DETACH_GUEST, guestInstanceId);
}
export function invoke (guestInstanceId: number, method: string, args: any[]) {
export function invoke(guestInstanceId: number, method: string, args: any[]) {
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, guestInstanceId, method, args);
}
export function invokeSync (guestInstanceId: number, method: string, args: any[]) {
export function invokeSync(guestInstanceId: number, method: string, args: any[]) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, guestInstanceId, method, args);
}
export function propertyGet (guestInstanceId: number, name: string) {
export function propertyGet(guestInstanceId: number, name: string) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET, guestInstanceId, name);
}
export function propertySet (guestInstanceId: number, name: string, value: any) {
export function propertySet(guestInstanceId: number, name: string, value: any) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET, guestInstanceId, name, value);
}

View File

@@ -6,7 +6,7 @@ const resolveURL = function (url?: string | null) {
};
interface MutationHandler {
handleMutation (_oldValue: any, _newValue: any): any;
handleMutation(_oldValue: any, _newValue: any): any;
}
// Attribute objects.
@@ -15,7 +15,10 @@ export class WebViewAttribute implements MutationHandler {
public value: any;
public ignoreMutation = false;
constructor (public name: string, public webViewImpl: WebViewImpl) {
constructor(
public name: string,
public webViewImpl: WebViewImpl
) {
this.name = name;
this.value = (webViewImpl.webviewNode as Record<string, any>)[name] || '';
this.webViewImpl = webViewImpl;
@@ -23,24 +26,24 @@ export class WebViewAttribute implements MutationHandler {
}
// Retrieves and returns the attribute's value.
public getValue () {
public getValue() {
return this.webViewImpl.webviewNode.getAttribute(this.name) || this.value;
}
// Sets the attribute's value.
public setValue (value: any) {
public setValue(value: any) {
this.webViewImpl.webviewNode.setAttribute(this.name, value || '');
}
// Changes the attribute's value without triggering its mutation handler.
public setValueIgnoreMutation (value: any) {
public setValueIgnoreMutation(value: any) {
this.ignoreMutation = true;
this.setValue(value);
this.ignoreMutation = false;
}
// Defines this attribute as a property on the webview node.
public defineProperty () {
public defineProperty() {
return Object.defineProperty(this.webViewImpl.webviewNode, this.name, {
get: () => {
return this.getValue();
@@ -58,11 +61,11 @@ export class WebViewAttribute implements MutationHandler {
// An attribute that is treated as a Boolean.
class BooleanAttribute extends WebViewAttribute {
getValue () {
getValue() {
return this.webViewImpl.webviewNode.hasAttribute(this.name);
}
setValue (value: boolean) {
setValue(value: boolean) {
if (value) {
this.webViewImpl.webviewNode.setAttribute(this.name, '');
} else {
@@ -75,7 +78,7 @@ class BooleanAttribute extends WebViewAttribute {
export class PartitionAttribute extends WebViewAttribute {
public validPartitionId = true;
constructor (public webViewImpl: WebViewImpl) {
constructor(public webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.PARTITION, webViewImpl);
}
@@ -99,12 +102,12 @@ export class PartitionAttribute extends WebViewAttribute {
export class SrcAttribute extends WebViewAttribute {
public observer!: MutationObserver;
constructor (public webViewImpl: WebViewImpl) {
constructor(public webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.SRC, webViewImpl);
this.setupMutationObserver();
}
public getValue () {
public getValue() {
if (this.webViewImpl.webviewNode.hasAttribute(this.name)) {
return resolveURL(this.webViewImpl.webviewNode.getAttribute(this.name));
} else {
@@ -112,7 +115,7 @@ export class SrcAttribute extends WebViewAttribute {
}
}
public setValueIgnoreMutation (value: any) {
public setValueIgnoreMutation(value: any) {
super.setValueIgnoreMutation(value);
// takeRecords() is needed to clear queued up src mutations. Without it, it
@@ -140,7 +143,7 @@ export class SrcAttribute extends WebViewAttribute {
// attribute without any changes to its value. This is useful in the case
// where the webview guest has crashed and navigating to the same address
// spawns off a new process.
public setupMutationObserver () {
public setupMutationObserver() {
this.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const { oldValue } = mutation;
@@ -161,8 +164,12 @@ export class SrcAttribute extends WebViewAttribute {
this.observer.observe(this.webViewImpl.webviewNode, params);
}
public parse () {
if (!this.webViewImpl.elementAttached || !(this.webViewImpl.attributes.get(WEB_VIEW_ATTRIBUTES.PARTITION) as PartitionAttribute).validPartitionId || !this.getValue()) {
public parse() {
if (
!this.webViewImpl.elementAttached ||
!(this.webViewImpl.attributes.get(WEB_VIEW_ATTRIBUTES.PARTITION) as PartitionAttribute).validPartitionId ||
!this.getValue()
) {
return;
}
if (this.webViewImpl.guestInstanceId == null) {
@@ -186,34 +193,33 @@ export class SrcAttribute extends WebViewAttribute {
opts.userAgent = useragent;
}
(this.webViewImpl.webviewNode as Electron.WebviewTag).loadURL(this.getValue(), opts)
.catch(err => {
console.error('Unexpected error while loading URL', err);
});
(this.webViewImpl.webviewNode as Electron.WebviewTag).loadURL(this.getValue(), opts).catch((err) => {
console.error('Unexpected error while loading URL', err);
});
}
}
// Attribute specifies HTTP referrer.
class HttpReferrerAttribute extends WebViewAttribute {
constructor (webViewImpl: WebViewImpl) {
constructor(webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.HTTPREFERRER, webViewImpl);
}
}
// Attribute specifies user agent
class UserAgentAttribute extends WebViewAttribute {
constructor (webViewImpl: WebViewImpl) {
constructor(webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.USERAGENT, webViewImpl);
}
}
// Attribute that set preload script.
class PreloadAttribute extends WebViewAttribute {
constructor (webViewImpl: WebViewImpl) {
constructor(webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.PRELOAD, webViewImpl);
}
public getValue () {
public getValue() {
if (!this.webViewImpl.webviewNode.hasAttribute(this.name)) {
return this.value;
}
@@ -232,34 +238,37 @@ class PreloadAttribute extends WebViewAttribute {
// Attribute that specifies the blink features to be enabled.
class BlinkFeaturesAttribute extends WebViewAttribute {
constructor (webViewImpl: WebViewImpl) {
constructor(webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.BLINKFEATURES, webViewImpl);
}
}
// Attribute that specifies the blink features to be disabled.
class DisableBlinkFeaturesAttribute extends WebViewAttribute {
constructor (webViewImpl: WebViewImpl) {
constructor(webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.DISABLEBLINKFEATURES, webViewImpl);
}
}
// Attribute that specifies the web preferences to be enabled.
class WebPreferencesAttribute extends WebViewAttribute {
constructor (webViewImpl: WebViewImpl) {
constructor(webViewImpl: WebViewImpl) {
super(WEB_VIEW_ATTRIBUTES.WEBPREFERENCES, webViewImpl);
}
}
// Sets up all of the webview attributes.
export function setupWebViewAttributes (self: WebViewImpl) {
export function setupWebViewAttributes(self: WebViewImpl) {
return new Map<string, WebViewAttribute>([
[WEB_VIEW_ATTRIBUTES.PARTITION, new PartitionAttribute(self)],
[WEB_VIEW_ATTRIBUTES.SRC, new SrcAttribute(self)],
[WEB_VIEW_ATTRIBUTES.HTTPREFERRER, new HttpReferrerAttribute(self)],
[WEB_VIEW_ATTRIBUTES.USERAGENT, new UserAgentAttribute(self)],
[WEB_VIEW_ATTRIBUTES.NODEINTEGRATION, new BooleanAttribute(WEB_VIEW_ATTRIBUTES.NODEINTEGRATION, self)],
[WEB_VIEW_ATTRIBUTES.NODEINTEGRATIONINSUBFRAMES, new BooleanAttribute(WEB_VIEW_ATTRIBUTES.NODEINTEGRATIONINSUBFRAMES, self)],
[
WEB_VIEW_ATTRIBUTES.NODEINTEGRATIONINSUBFRAMES,
new BooleanAttribute(WEB_VIEW_ATTRIBUTES.NODEINTEGRATIONINSUBFRAMES, self)
],
[WEB_VIEW_ATTRIBUTES.PLUGINS, new BooleanAttribute(WEB_VIEW_ATTRIBUTES.PLUGINS, self)],
[WEB_VIEW_ATTRIBUTES.DISABLEWEBSECURITY, new BooleanAttribute(WEB_VIEW_ATTRIBUTES.DISABLEWEBSECURITY, self)],
[WEB_VIEW_ATTRIBUTES.ALLOWPOPUPS, new BooleanAttribute(WEB_VIEW_ATTRIBUTES.ALLOWPOPUPS, self)],

View File

@@ -12,7 +12,7 @@ export const enum WEB_VIEW_ATTRIBUTES {
USERAGENT = 'useragent',
BLINKFEATURES = 'blinkfeatures',
DISABLEBLINKFEATURES = 'disableblinkfeatures',
WEBPREFERENCES = 'webpreferences',
WEBPREFERENCES = 'webpreferences'
}
export const enum WEB_VIEW_ERROR_MESSAGES {

View File

@@ -17,7 +17,7 @@ const internals = new WeakMap<HTMLElement, WebViewImpl>();
// Return a WebViewElement class that is defined in this context.
const defineWebViewElement = (hooks: WebViewImplHooks) => {
return class WebViewElement extends HTMLElement {
static get observedAttributes () {
static get observedAttributes() {
return [
WEB_VIEW_ATTRIBUTES.PARTITION,
WEB_VIEW_ATTRIBUTES.SRC,
@@ -35,12 +35,12 @@ const defineWebViewElement = (hooks: WebViewImplHooks) => {
];
}
constructor () {
constructor() {
super();
internals.set(this, new WebViewImpl(this, hooks));
}
getWebContentsId () {
getWebContentsId() {
const internal = internals.get(this);
if (!internal || !internal.guestInstanceId) {
throw new Error(WEB_VIEW_ERROR_MESSAGES.NOT_ATTACHED);
@@ -48,7 +48,7 @@ const defineWebViewElement = (hooks: WebViewImplHooks) => {
return internal.guestInstanceId;
}
connectedCallback () {
connectedCallback() {
const internal = internals.get(this);
if (!internal) {
return;
@@ -62,14 +62,14 @@ const defineWebViewElement = (hooks: WebViewImplHooks) => {
}
}
attributeChangedCallback (name: string, oldValue: any, newValue: any) {
attributeChangedCallback(name: string, oldValue: any, newValue: any) {
const internal = internals.get(this);
if (internal) {
internal.handleWebviewAttributeMutation(name, oldValue, newValue);
}
}
disconnectedCallback () {
disconnectedCallback() {
const internal = internals.get(this);
if (!internal) {
return;

Some files were not shown because too many files have changed in this diff Show More