Compare commits

...

36 Commits

Author SHA1 Message Date
Sam Attard
225fc5aa08 chore: update lockfile 2026-04-05 05:46:21 +00:00
Sam Attard
43d61d4cd0 build: typecheck the Electron lib tree with tsgo during ninja
Adds a GN typescript_check template that runs tsgo --noEmit over a
tsconfig and writes a stamp file on success, wired into BUILD.gn as
electron_lib_typecheck. electron_js2c depends on it so a broken type
in lib/ fails 'e build'.

tsgo (@typescript/native-preview, the Go-based TypeScript compiler
preview) runs the full lib/ typecheck in ~400ms, about 6x faster than
tsc 6.0.2 (~2.3s). ts-loader previously typechecked implicitly inside
webpack and was removed in the esbuild migration, so this restores
typecheck coverage that was briefly absent on the bundle build path.

Because tsgo has no 'ignoreDiagnostics' option like ts-loader's, the
previously-silenced TS6059 and TS1111 errors needed to be fixed
properly:

  - tsconfig.electron.json drops 'rootDir: "lib"'. It was only
    meaningful for emit, and the TS 6 implicit rootDir inference plus
    the @node/* path alias was pulling Node's internal .js files
    (specifically internal/url.js with its #searchParams brand check)
    into the program.

  - tsconfig.json drops the @node/* path alias entirely. Every call
    site that used 'as typeof import("@node/lib/...")' is replaced
    with narrow structural types declared once in an ambient
    NodeInternalModules interface in typings/internal-ambient.d.ts.
    __non_webpack_require__ becomes an overloaded function that picks
    the right return type from a string-literal id argument, so call
    sites no longer need 'as' casts.

  - tsconfig.default_app.json: moduleResolution 'node' -> 'bundler'
    (TS 6 deprecates 'node').

  - typings/internal-ambient.d.ts: 'declare module NodeJS { }' ->
    'declare namespace NodeJS { }' (TS 6 rejects the module keyword
    for non-external declarations).

spec/ts-smoke/runner.js now invokes tsgo's bin instead of resolving
the 'typescript' package. The 'tsc' npm script is repointed from
'tsc' to 'tsgo' so the existing CI step
'node script/yarn.js tsc -p tsconfig.script.json' continues to run.

A small script/typecheck.js wrapper runs tsgo and writes the stamp
file for GN; the typescript_check template invokes it via a new
'tsc-check' npm script.
2026-04-05 05:46:21 +00:00
Sam Attard
646dfd24f7 build: replace webpack with esbuild for internal JS bundles
Replaces the webpack+ts-loader based bundler for Electron's 8 internal
init bundles (browser, renderer, worker, sandboxed_renderer,
isolated_renderer, node, utility, preload_realm) with a plain esbuild
driver under build/esbuild/. GN template now lives at
build/esbuild/esbuild.gni and is invoked via a new 'bundle' npm script.

Per-target configs move from build/webpack/webpack.config.<target>.js to
small data-only files under build/esbuild/configs/. ProvidePlugin's
global/Buffer/process/Promise capture moves to inject-shims under
build/esbuild/shims/. The wrapper-webpack-plugin try/catch and
___electron_webpack_init__ wrappers are applied as textual pre/postamble
inside the driver so shell/common/node_util.cc's CompileAndCall error
handling still works.

The old BUILDFLAG DefinePlugin pass is replaced with a small onLoad
regex that rewrites BUILDFLAG(NAME) to (true|false) using the
GN-generated buildflags.h. AccessDependenciesPlugin (used by
gen-filenames.ts to populate filenames.auto.gni) is replaced with
esbuild's built-in metafile via a new '--print-graph' flag.

A few source files needed small fixes to work under esbuild's
stricter CJS/ESM interop:

  - lib/browser/api/net.ts and lib/utility/api/net.ts mixed ESM
    'export function' with 'exports.x = ...' assignments, which
    esbuild treats as an ambiguous module. Switched to a single
    'module.exports = {}' with a getter for the dynamic 'online'
    property.

  - lib/common/timers-shim.ts is untouched; lib/common/init.ts no
    longer mutates the imported timers namespace (timers.setImmediate
    = wrap(...)). Under webpack the mutation applied to the bundled
    shim and was invisible to user apps; the refactor stores the
    wrapped values in local consts instead.

Drops webpack, webpack-cli, ts-loader, null-loader, and
wrapper-webpack-plugin from devDependencies. Adds esbuild.

Sequential build time for the 8 bundles drops from ~22s (webpack) to
~480ms (esbuild).
2026-04-05 05:46:21 +00:00
Sam Attard
f89c8efc9d fix: defer Wrappable destruction in SecondWeakCallback to a posted task
V8's second-pass weak callbacks run inside a
DisallowJavascriptExecutionScope: they may touch the V8 API but must
not invoke JS, directly or indirectly. Several Electron Wrappables
(WebContents in particular) emit JS events from their destructors,
so deleting synchronously inside SecondWeakCallback can crash with
"Invoke in DisallowJavascriptExecutionScope" when GC happens to
collect the JS wrapper during a foreground GC task — typically during
shutdown's uv_run drain after a leaked WebContentsView.

This was previously latent and timing-dependent (electron/electron#47420,
electron/electron#45416, podman-desktop/podman-desktop#12409). The
esbuild migration's keepNames option (which wraps every function/class
with an Object.defineProperty call) shifted heap layout enough to make
the spec/fixtures/crash-cases/webcontentsview-create-leak-exit case
reliably reproduce it on every run, giving a clean signal for the fix.

Both WrappableBase and DeprecatedWrappableBase SecondWeakCallback now
post the deletion via base::SequencedTaskRunner::GetCurrentDefault()
so the destructor (and any Emit it does) runs once V8 has left the GC
scope. Falls back to synchronous deletion if no task runner is
available (early/late process lifetime).

Fixes electron/electron#47420.
2026-04-05 05:46:19 +00:00
Charles Kerr
ec30e4cdae refactor: remove unused field ServiceWorkerMain.start_worker_promise_ (#50674)
added in a467d068 but never used.
2026-04-04 14:22:48 -05:00
Samuel Attard
40033db422 chore: resolve dependabot security alerts (#50680) 2026-04-04 11:56:48 -07:00
Samuel Attard
c3d441cf7d ci: add Datadog metrics to clean-src-cache job (#50642)
* ci: add Datadog metrics to clean-src-cache job

Report free space (before/after cleanup), space freed, and total space
for both cross-instance-cache and win-cache volumes to Datadog, matching
the pattern used in the macOS disk cleanup workflow.

https://claude.ai/code/session_013bpDsZLrFDpWMiARNFH4z9

* ci: use awk instead of bc, add workflow_dispatch trigger

- Replace bc with awk for KB-to-GB conversion since bc may not be
  available in the container image
- Add workflow_dispatch trigger for manual testing

https://claude.ai/code/session_013bpDsZLrFDpWMiARNFH4z9

* ci: remove workflow_dispatch, handled in another PR

https://claude.ai/code/session_013bpDsZLrFDpWMiARNFH4z9

* ci: move DD_API_KEY to job-level env for if-condition

The step-level env is not available when GitHub evaluates the step's
if expression, so env.DD_API_KEY was always empty. Move it to
job-level env so the conditional works correctly.

https://claude.ai/code/session_013bpDsZLrFDpWMiARNFH4z9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 22:17:29 -07:00
Charles Kerr
14583d22e6 refactor: remove use of deprecated API base::GetProc() (#50650)
* refactor: replace deprecated API base::GetProcId() in web_frame_main

* refactor: replace deprecated API base::GetProcId() in web_contents

* refactor: replace deprecated API base::GetProcId() in a11y ui

* refactor: frame.osProcessId now returns 0 instead of -1 for invalid processes.

This is consistent with WebContents.getOSProcessId
2026-04-03 15:48:41 -05:00
Charles Kerr
68bfe49120 refactor: remove never-used JS API (#50649)
* chore: do not expose v8Util.getObjectHash() to JS

Not used, documented, or typed. Added in ddad3e4846.

* chore: do not expose DownloadItem.isDone() to JS

Not used, documented, or typed. Added in dcad25c98c.

* chore: do not expose BrowserWindow.isWebViewFocused() to JS

Not used, documented, or typed. Added in a949e9542d.
2026-04-03 13:04:27 -05:00
Kunal Dubey
2c6332a7d6 fix: resolve getFileHandle concurrent stalling by queuing callbacks (#50597)
Previously, concurrent calls to FileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess
for the same file path would silently discard the subsequent callbacks because
the internal callback map used a single callback per file path and std::map::try_emplace
would drop the callback if the key already existed. This caused Promises in JS
(e.g., dirHandle.getFileHandle()) to stall indefinitely.

This commit updates the callback map to hold a vector of callbacks, so all
concurrent requesters for the same filepath are grouped together and resolved
once the asynchronous blocklist check completes.

Notes: Fixed an issue where concurrent `getFileHandle` requests on the same path could stall indefinitely.
2026-04-03 18:04:55 +02:00
Bohdan Tkachenko
ddc1bd9553 fix: forward activation token from libnotify on notification click (#50568)
* feat: forward activation token from libnotify notification clicks

When a notification action is clicked on Linux, retrieve the activation
token from libnotify (if available) via dlsym and set it using
`base::nix::SetActivationToken()`. This enables proper window focus
handling under Wayland, where the compositor requires a valid activation
token to grant focus to the application.

The `notify_notification_get_activation_token` symbol is resolved at
runtime to maintain compatibility with older libnotify versions that
do not expose this API.

* refactor: simplify libnotify soname loading and activation token lookup

Replace the chained Load() calls with a loop over a constexpr array of
sonames, and inline the lazy EnsureActivationTokenFunc() into
Initialize() since it is only called once and the library handle is
already known at that point.
2026-04-03 10:40:36 -05:00
Shelley Vohr
12109371d3 fix: validate dock_state_ against allowlist before JS execution (#50646)
fix: validate dock_state_ against allowlist before JS execution

The dock_state_ member was concatenated directly into a JavaScript
string and executed via ExecuteJavaScript() in the DevTools context.

We should validate against the four known dock states and fall back
to "right" for any unrecognized value for safety
2026-04-03 10:39:16 -05:00
Charles Kerr
69891d04bf test: improve cookie changed event coverage (#50655)
test: add tests for cookie changed overwrite and inserted

test: add tests for cookie changed inserted-no-value-change-overwrite

test: add tests for cookie changed expired-overwrite
2026-04-03 10:26:17 -05:00
David Sanders
188813e206 ci: fix pulling previous object checksums (#50635)
* ci: fix pulling previous object checksums

* chore: fix artifact finding

* chore: skip unpack

* refactor: dawidd6/action-download-artifact can't handle non-archived artifacts

Assisted-by: Claude Opus 4.6

* refactor: use Octokit in standalone script

Assisted-by: Claude Opus 4.6
2026-04-03 04:52:50 +00:00
Charles Kerr
8b768b8211 chore: stop exposing unused menu methods to JS (#50634)
* chore: remove unused undocumented API `menu.worksWhenHiddenAt()`.

Not used, documented, or typed. Added by 544d8a423c.

* chore: remove unused undocumented API `menu.getCommandIdAt()

Not used, documented, or typed. Added by dae98fa43f.

* chore: do not expose `menu.getIndexOfCommandId()` to JS

Added by dae98fa43f but not documented, typed, or used by JS code.

The C++ method is used by other shell code, but not in JS.

* chore: remove unused undocumented API `menu.getLabelAt()`

Not used, documented, or typed. Added by dae98fa43f.

* chore: remove unused undocumented API `menu.getToolTipAt()`

Not used, documented, or typed. Added by 06d48514c6.

* chore: remove unused undocumented API `menu.getSubLabelAt()`

Not used, documented, or typed. Added by dae98fa43f.
2026-04-02 22:04:18 -05:00
Mitchell Cohen
82b97ddf5b ci: run BrowserWindow test spec on Wayland (#50572)
add browserwindow test spec for wayland
2026-04-02 17:19:23 -05:00
Charles Kerr
16f408a502 chore: remove declaration for nonexistent method WebContents._getPrintersAsync() (#50628)
chore: remove declaration for nonexistent method WebContents._getPrintersAsync()

added in 8f51d3e1 but never implemented / never used
2026-04-02 15:29:10 -05:00
Michaela Laurencin
246aa63910 ci: correct contributing link and add link to ai tool policy (#50632)
* ci: correct contributing link and add link to ai tool policy

* add missing bracket
2026-04-02 13:54:13 -05:00
Shelley Vohr
230f02faf2 fix: don't force kFitToPrintableArea scaling when custom margins are set (#50615)
When silent printing with non-default margins (custom, no margins, or
printable area margins), the kFitToPrintableArea scaling option causes
double-marginalization: the custom margins define the content area, then
the scaling additionally fits content to the printer's printable area.

Only apply kFitToPrintableArea when using default margins in silent mode.
For non-default margins, use the same scaling as non-silent prints.
2026-04-02 20:41:21 +02:00
Charles Kerr
1362d7b94d refactor: remove unused internal method WebContents.equal() (#50626)
refactor: remove unused internal method WebContents.equal()

last use removed in Feb 2021 @ 51bb0ad36d
2026-04-02 12:46:39 -05:00
Mitchell Cohen
877fe479b5 fix: glitchy rendering and maximize behavior with different GTK themes (#50550)
* fix glitchy rendering with different gtk themes especially when maximizing

* use actual insets, not restored insets
2026-04-02 09:52:27 -05:00
Shelley Vohr
f41438ff73 fix: prefill native print dialog options on macOS with OOP printing (#50600)
Chromium enabled out-of-process (OOP) printing by default on macOS in
https://chromium-review.googlesource.com/c/chromium/src/+/6032774. This
broke webContents.print() option prefilling (e.g. copies, collate,
duplex) in two ways:

1. ScriptedPrint() silently aborted because RegisterSystemPrintClient()
   was only called from GetDefaultPrintSettings(), but Electron's flow
   calls UpdatePrintSettings() instead when options are provided.

2. PrinterQueryOop::UpdatePrintSettings() sends settings to the remote
   PrintBackend service, but on macOS the native dialog runs in-browser
   using the local PrintingContextMac::print_info_, which was never
   updated with the user's requested settings.

Fix by registering the system print client in UpdatePrintSettings() and
applying cached settings to the local printing context before showing
the in-browser system print dialog.
2026-04-02 16:06:35 +02:00
Shelley Vohr
c6e201c965 build: allow clearing src & cross mnt cache via dispatch (#50638) 2026-04-02 10:01:08 +00:00
Niklas Wenzel
156a4e610c fix: extension service workers not starting beyond first app launch (#50611)
* fix: extension service worker not starting beyond first app launch

* fix: set preference only for extensions with service workers
2026-04-02 10:02:06 +02:00
Charles Kerr
81f8fc1880 refactor: remove unused internal method contents.canGoToIndex() (#50606)
refactor: remove unused internal method contents.canGoToIndex()

refactor: make WebContents::CanGoToIndex() private

The JS binding has been unused since 2021-04-27 #28839 0a1b26b1
2026-04-01 22:37:41 +02:00
Charles Kerr
343d6e5f3f test: add tests for navigationHistory.goToIndex() (#50607)
test: add tests for navigationHistory.goToIndex()
2026-04-01 22:37:19 +02:00
Asish Kumar
e7080835f1 docs: add destroy method to native addon tutorials to prevent hang on quit (#50561)
Native addons that hold persistent references to callbacks, emitters,
and threadsafe functions prevent Electron from quitting cleanly since
Electron 40.5.0 due to changes in Node.js shutdown behavior. This adds
a `destroy()` method to all four native code tutorials (Swift macOS,
Obj-C macOS, C++ Linux, C++ Win32) that releases these resources and
must be called before app quit.

The destroy method resets callback and emitter references and aborts the
threadsafe function, allowing the addon's destructor to run properly.
An [!IMPORTANT] note is added to each tutorial's JavaScript wrapper
section explaining when and why to call destroy().

Fixes #50457

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-01 13:13:09 -05:00
LiRongWan
7c1a6f7e95 docs: recommend subdirectory for userData to avoid Chromium conflicts (#50563)
Fixes #45414

Storing files directly in the userData root can cause naming conflicts
with Chromium's own subdirectories (Cache, GPUCache, Local Storage, etc.).
Added a recommendation to use a subdirectory such as
path.join(app.getPath('userData'), 'my-app-data') instead.

Notes: no-notes
2026-04-01 09:54:08 -05:00
Calvin
22ac2b13fb fix: remove menu update debug log (#50608) 2026-04-01 17:06:26 +09:00
Samuel Attard
a8acb96608 build: replace npx with lockfile-pinned binaries (#50598)
* build: replace npx with lockfile-pinned binaries

- nan-spec-runner: reorder yarn install first, invoke nan node-gyp bin directly
- publish-to-npm: use host npm with E404 try/catch (closes existing TODO)
- upload-symbols: add @sentry/cli devDep, invoke from node_modules/.bin
- remove script/lib/npx.py (dead since #48243)

* build: bump @sentry/cli to 1.70.0 for arm support

* build: bump @sentry/cli to 1.72.0, skip CDN download on test jobs

@sentry/cli fetches its platform binary from Sentry CDN at postinstall.
Only upload-symbols.py (release pipeline) needs the binary; set
SENTRYCLI_SKIP_DOWNLOAD=1 in the two test-segment workflows that
call install-dependencies. The 64k variant uses pre-built artifacts
and does not install deps.
2026-03-31 20:23:43 +00:00
Mitchell Cohen
97773bf50c fix: prevent borders and smearing in transparent frameless/client frame windows on Linux (#50541)
fix the appearance of transparent frameless and client frame windows
2026-03-31 11:24:10 -05:00
Shelley Vohr
1e0846749b fix: invoke print callback directly when no print job exists (#50431)
ShowInvalidPrinterSettingsError() called TerminatePrintJob(true),
but when no print_job_ had been created yet (e.g. settings validation
failed before a job could start), TerminatePrintJob bails out
immediately without reaching ReleasePrintJob() where the callback
is invoked. This left the CompletionCallback stuck in callback_
until WebContents destruction, causing webContents.print() to only
fire its callback when the application closed.
2026-03-31 11:01:59 -05:00
electron-roller[bot]
8cd766ff53 chore: bump chromium to 148.0.7763.0 (main) (#50582) 2026-03-31 10:16:35 +02:00
dependabot[bot]
e5b20a11d2 build(deps): bump github/codeql-action from 4.34.1 to 4.35.1 (#50590)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.34.1 to 4.35.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](3869755554...c10b8064de)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:15:41 +02:00
Alexey
e0bd4ffc39 fix: add missing HandleScope in contentTracing.getTraceBufferUsage() (#50556)
The `OnTraceBufferUsageAvailable` callback creates V8 handles via
`Dictionary::CreateEmpty()` before `promise.Resolve()` enters its
`SettleScope` (which provides a `HandleScope`). When the callback
fires asynchronously from a Mojo response (i.e. when a trace session
is active), there is no `HandleScope` on the stack, causing a fatal
V8 error: "Cannot create a handle without a HandleScope".

Add an explicit `v8::HandleScope` at the top of the callback, matching
the pattern used by the other contentTracing APIs which resolve their
promises through `SettleScope` or the static `ResolvePromise` helper.

Made-with: Cursor
2026-03-31 10:21:43 +09:00
Samuel Attard
bbbcae1a12 fix: re-enable MacWebContentsOcclusion with embedder window fix (#50579)
* fix: re-enable MacWebContentsOcclusion with embedder window fix

Replace the full revert of Chromium's MacWebContentsOcclusion cleanup
with a targeted patch that handles embedder windows shown after
WebContentsViewCocoa attachment. This lets us drop the feature flag
disable in feature_list.cc and re-enable upstream occlusion tracking.

Adds tests for show/hide event counts on macOS and visibility tracking
across multiple child WebContentsViews.

* test: drop show/hide event count assertion

The assertion that 'show' fires exactly once per w.show() call is not
an API guarantee - macOS can send multiple occlusion state
notifications during a single show() when other windows are on screen
(common on CI after hundreds of prior tests). The
visibilitychange-count test in api-web-contents-view-spec.ts covers
the actual invariant we care about.

* fix: ignore WebContentsOcclusionCheckerMac synthetic notifications in window delegate

On macOS 13.3-25.x, Chromium's occlusion checker enables manual
frame-intersection detection and posts synthetic
NSWindowDidChangeOcclusionStateNotification tagged with its class name
in userInfo. These fire when the checker's NSContainsRect heuristic
decides a window is covered by another window's frame, but the real
-[NSWindow occlusionState] hasn't changed.

Our delegate was treating these the same as real macOS notifications
and emitting show/hide events based on occlusionState, which was
unchanged - resulting in spurious duplicate show events when e.g.
Quick Look opened and its frame intersected the BrowserWindow.
2026-03-30 14:13:00 -07:00
112 changed files with 3019 additions and 2400 deletions

View File

@@ -48,19 +48,15 @@ runs:
shell: bash
run: echo "::add-matcher::src/electron/.github/problem-matchers/clang.json"
- name: Download previous object checksums
uses: dawidd6/action-download-artifact@09b07ec687d10771279a426c79925ee415c12906 # v17
if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request') && inputs.is-asan != 'true' }}
with:
name: object_checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}
commit: ${{ case(github.event_name == 'push', github.event.before, github.event.pull_request.base.sha) }}
path: src
if_no_artifact_found: ignore
- name: Move previous object checksums
shell: bash
run: |
if [ -f src/object-checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json ]; then
mv src/object-checksums_${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json src/previous-object-checksums.json
fi
if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request') && inputs.is-asan != 'true' }}
env:
GITHUB_TOKEN: ${{ github.token }}
ARTIFACT_NAME: object-checksums.${{ inputs.artifact-platform }}_${{ inputs.target-arch }}.json
SEARCH_BRANCH: ${{ case(github.event_name == 'push', github.ref_name, github.event.pull_request.base.ref) }}
REPO: ${{ github.repository }}
OUTPUT_PATH: src/previous-object-checksums.json
run: node src/electron/.github/actions/build-electron/download-previous-object-checksums.mjs
- name: Build Electron ${{ inputs.step-suffix }}
if: ${{ inputs.target-platform != 'win' }}
shell: bash

View File

@@ -0,0 +1,82 @@
import { Octokit } from '@octokit/rest';
import { writeFileSync } from 'node:fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.REPO;
const artifactName = process.env.ARTIFACT_NAME;
const branch = process.env.SEARCH_BRANCH;
const outputPath = process.env.OUTPUT_PATH;
const required = { GITHUB_TOKEN: token, REPO: repo, ARTIFACT_NAME: artifactName, SEARCH_BRANCH: branch, OUTPUT_PATH: outputPath };
const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
if (missing.length > 0) {
console.error(`Missing required environment variables: ${missing.join(', ')}`);
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
async function main () {
console.log(`Searching for artifact '${artifactName}' on branch '${branch}'...`);
// Resolve the "Build" workflow name to an ID, mirroring how `gh run list --workflow` works
// under the hood (it uses /repos/{owner}/{repo}/actions/workflows/{id}/runs).
const { data: workflows } = await octokit.actions.listRepoWorkflows({ owner, repo: repoName });
const buildWorkflow = workflows.workflows.find((w) => w.name === 'Build');
if (!buildWorkflow) {
console.log('Could not find "Build" workflow, continuing without previous checksums');
return;
}
const { data: runs } = await octokit.actions.listWorkflowRuns({
owner,
repo: repoName,
workflow_id: buildWorkflow.id,
branch,
status: 'completed',
event: 'push',
per_page: 20,
exclude_pull_requests: true
});
for (const run of runs.workflow_runs) {
const { data: artifacts } = await octokit.actions.listWorkflowRunArtifacts({
owner,
repo: repoName,
run_id: run.id,
name: artifactName
});
if (artifacts.artifacts.length > 0) {
const artifact = artifacts.artifacts[0];
console.log(`Found artifact in run ${run.id} (artifact ID: ${artifact.id}), downloading...`);
// Non-archived artifacts are still downloaded from the /zip endpoint
const response = await octokit.actions.downloadArtifact({
owner,
repo: repoName,
artifact_id: artifact.id,
archive_format: 'zip'
});
if (response.headers['content-type'] !== 'application/json') {
console.error(`Unexpected content type for artifact download: ${response.headers['content-type']}`);
console.error('Expected application/json, continuing without previous checksums');
return;
}
writeFileSync(outputPath, JSON.stringify(response.data));
console.log('Downloaded previous object checksums successfully');
return;
}
}
console.log(`No previous object checksums found in last ${runs.workflow_runs.length} runs, continuing without them`);
}
main().catch((err) => {
console.error('Failed to download previous object checksums, continuing without them:', err.message);
process.exit(0);
});

View File

@@ -7,6 +7,7 @@ name: Clean Source Cache
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions: {}
@@ -16,6 +17,8 @@ jobs:
runs-on: electron-arc-centralus-linux-amd64-32core
permissions:
contents: read
env:
DD_API_KEY: ${{ secrets.DD_API_KEY }}
container:
image: ghcr.io/electron/build:bc2f48b2415a670de18d13605b1cf0eb5fdbaae1
options: --user root
@@ -23,12 +26,130 @@ jobs:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
- /mnt/win-cache:/mnt/win-cache
steps:
- name: Get Disk Space Before Cleanup
id: disk-before
shell: bash
run: |
echo "Disk space before cleanup:"
df -h /mnt/cross-instance-cache
df -h /mnt/win-cache
CROSS_FREE_BEFORE=$(df -k /mnt/cross-instance-cache | tail -1 | awk '{print $4}')
CROSS_TOTAL=$(df -k /mnt/cross-instance-cache | tail -1 | awk '{print $2}')
WIN_FREE_BEFORE=$(df -k /mnt/win-cache | tail -1 | awk '{print $4}')
WIN_TOTAL=$(df -k /mnt/win-cache | tail -1 | awk '{print $2}')
echo "cross_free_kb=$CROSS_FREE_BEFORE" >> $GITHUB_OUTPUT
echo "cross_total_kb=$CROSS_TOTAL" >> $GITHUB_OUTPUT
echo "win_free_kb=$WIN_FREE_BEFORE" >> $GITHUB_OUTPUT
echo "win_total_kb=$WIN_TOTAL" >> $GITHUB_OUTPUT
- name: Cleanup Source Cache
shell: bash
run: |
df -h /mnt/cross-instance-cache
find /mnt/cross-instance-cache -type f -mtime +15 -delete
find /mnt/win-cache -type f -mtime +15 -delete
- name: Get Disk Space After Cleanup
id: disk-after
shell: bash
run: |
echo "Disk space after cleanup:"
df -h /mnt/cross-instance-cache
df -h /mnt/win-cache
find /mnt/win-cache -type f -mtime +15 -delete
df -h /mnt/win-cache
CROSS_FREE_AFTER=$(df -k /mnt/cross-instance-cache | tail -1 | awk '{print $4}')
WIN_FREE_AFTER=$(df -k /mnt/win-cache | tail -1 | awk '{print $4}')
echo "cross_free_kb=$CROSS_FREE_AFTER" >> $GITHUB_OUTPUT
echo "win_free_kb=$WIN_FREE_AFTER" >> $GITHUB_OUTPUT
- name: Log Disk Space to Datadog
if: ${{ env.DD_API_KEY != '' }}
shell: bash
env:
CROSS_FREE_BEFORE: ${{ steps.disk-before.outputs.cross_free_kb }}
CROSS_FREE_AFTER: ${{ steps.disk-after.outputs.cross_free_kb }}
CROSS_TOTAL: ${{ steps.disk-before.outputs.cross_total_kb }}
WIN_FREE_BEFORE: ${{ steps.disk-before.outputs.win_free_kb }}
WIN_FREE_AFTER: ${{ steps.disk-after.outputs.win_free_kb }}
WIN_TOTAL: ${{ steps.disk-before.outputs.win_total_kb }}
run: |
TIMESTAMP=$(date +%s)
CROSS_FREE_BEFORE_GB=$(awk "BEGIN {printf \"%.2f\", $CROSS_FREE_BEFORE / 1024 / 1024}")
CROSS_FREE_AFTER_GB=$(awk "BEGIN {printf \"%.2f\", $CROSS_FREE_AFTER / 1024 / 1024}")
CROSS_FREED_GB=$(awk "BEGIN {printf \"%.2f\", ($CROSS_FREE_AFTER - $CROSS_FREE_BEFORE) / 1024 / 1024}")
CROSS_TOTAL_GB=$(awk "BEGIN {printf \"%.2f\", $CROSS_TOTAL / 1024 / 1024}")
WIN_FREE_BEFORE_GB=$(awk "BEGIN {printf \"%.2f\", $WIN_FREE_BEFORE / 1024 / 1024}")
WIN_FREE_AFTER_GB=$(awk "BEGIN {printf \"%.2f\", $WIN_FREE_AFTER / 1024 / 1024}")
WIN_FREED_GB=$(awk "BEGIN {printf \"%.2f\", ($WIN_FREE_AFTER - $WIN_FREE_BEFORE) / 1024 / 1024}")
WIN_TOTAL_GB=$(awk "BEGIN {printf \"%.2f\", $WIN_TOTAL / 1024 / 1024}")
echo "cross-instance-cache: free before=${CROSS_FREE_BEFORE_GB}GB, after=${CROSS_FREE_AFTER_GB}GB, freed=${CROSS_FREED_GB}GB, total=${CROSS_TOTAL_GB}GB"
echo "win-cache: free before=${WIN_FREE_BEFORE_GB}GB, after=${WIN_FREE_AFTER_GB}GB, freed=${WIN_FREED_GB}GB, total=${WIN_TOTAL_GB}GB"
curl -s -X POST "https://api.datadoghq.com/api/v2/series" \
-H "Content-Type: application/json" \
-H "DD-API-KEY: ${DD_API_KEY}" \
-d @- << EOF
{
"series": [
{
"metric": "electron.src_cache.disk.free_space_before_cleanup_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${CROSS_FREE_BEFORE_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:cross-instance-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.free_space_after_cleanup_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${CROSS_FREE_AFTER_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:cross-instance-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.space_freed_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${CROSS_FREED_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:cross-instance-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.total_space_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${CROSS_TOTAL_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:cross-instance-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.free_space_before_cleanup_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${WIN_FREE_BEFORE_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:win-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.free_space_after_cleanup_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${WIN_FREE_AFTER_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:win-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.space_freed_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${WIN_FREED_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:win-cache", "platform:linux"]
},
{
"metric": "electron.src_cache.disk.total_space_gb",
"points": [{"timestamp": ${TIMESTAMP}, "value": ${WIN_TOTAL_GB}}],
"type": 3,
"unit": "gigabyte",
"tags": ["volume:win-cache", "platform:linux"]
}
]
}
EOF
echo "Disk space metrics logged to Datadog"

View File

@@ -48,6 +48,8 @@ env:
ELECTRON_OUT_DIR: Default
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_STEP_DEBUG }}
# @sentry/cli is only needed by release upload-symbols.py; skip the ~17MB CDN download on test jobs
SENTRYCLI_SKIP_DOWNLOAD: 1
jobs:
test:

View File

@@ -36,6 +36,8 @@ env:
CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
ELECTRON_OUT_DIR: Default
ELECTRON_RBE_JWT: ${{ secrets.ELECTRON_RBE_JWT }}
# @sentry/cli is only needed by release upload-symbols.py; skip the ~17MB CDN download on test jobs
SENTRYCLI_SKIP_DOWNLOAD: 1
jobs:
node-tests:

View File

@@ -72,7 +72,7 @@ jobs:
Hello @${{ github.event.pull_request.user.login }}. Due to the high amount of AI spam PRs we receive, if a PR is detected to be majority AI-generated without disclosure and untested, we will automatically close the PR.
We welcome the use of AI tools, as long as the PR meets our quality standards and has clearly been built and tested. If you believe your PR was closed in error, we welcome you to resubmit. However, please read our [CONTRIBUTING.md](http://contributing.md/) carefully before reopening. Thanks for your contribution.
We welcome the use of AI tools, as long as the PR meets our quality standards and has clearly been built and tested. If you believe your PR was closed in error, we welcome you to resubmit. However, please read our [CONTRIBUTING.md](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) and [AI Tool Policy](https://github.com/electron/governance/blob/main/policy/ai.md) carefully before reopening. Thanks for your contribution.
- name: Close the pull request
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}

View File

@@ -51,6 +51,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v3.29.5
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
with:
sarif_file: results.sarif

View File

@@ -18,12 +18,12 @@ import("//tools/v8_context_snapshot/v8_context_snapshot.gni")
import("//v8/gni/snapshot_toolchain.gni")
import("build/asar.gni")
import("build/electron_paks.gni")
import("build/esbuild/esbuild.gni")
import("build/extract_symbols.gni")
import("build/js2c_toolchain.gni")
import("build/npm.gni")
import("build/templated_file.gni")
import("build/tsc.gni")
import("build/webpack/webpack.gni")
import("buildflags/buildflags.gni")
import("filenames.auto.gni")
import("filenames.gni")
@@ -162,75 +162,81 @@ npm_action("build_electron_definitions") {
outputs = [ "$target_gen_dir/tsc/typings/electron.d.ts" ]
}
webpack_build("electron_browser_bundle") {
typescript_check("electron_lib_typecheck") {
deps = [ ":build_electron_definitions" ]
tsconfig = "//electron/tsconfig.electron.json"
sources = auto_filenames.typecheck_sources
}
esbuild_build("electron_browser_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.browser_bundle_deps
config_file = "//electron/build/webpack/webpack.config.browser.js"
config_file = "//electron/build/esbuild/configs/browser.js"
out_file = "$target_gen_dir/js2c/browser_init.js"
}
webpack_build("electron_renderer_bundle") {
esbuild_build("electron_renderer_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.renderer_bundle_deps
config_file = "//electron/build/webpack/webpack.config.renderer.js"
config_file = "//electron/build/esbuild/configs/renderer.js"
out_file = "$target_gen_dir/js2c/renderer_init.js"
}
webpack_build("electron_worker_bundle") {
esbuild_build("electron_worker_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.worker_bundle_deps
config_file = "//electron/build/webpack/webpack.config.worker.js"
config_file = "//electron/build/esbuild/configs/worker.js"
out_file = "$target_gen_dir/js2c/worker_init.js"
}
webpack_build("electron_sandboxed_renderer_bundle") {
esbuild_build("electron_sandboxed_renderer_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.sandbox_bundle_deps
config_file = "//electron/build/webpack/webpack.config.sandboxed_renderer.js"
config_file = "//electron/build/esbuild/configs/sandboxed_renderer.js"
out_file = "$target_gen_dir/js2c/sandbox_bundle.js"
}
webpack_build("electron_isolated_renderer_bundle") {
esbuild_build("electron_isolated_renderer_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.isolated_bundle_deps
config_file = "//electron/build/webpack/webpack.config.isolated_renderer.js"
config_file = "//electron/build/esbuild/configs/isolated_renderer.js"
out_file = "$target_gen_dir/js2c/isolated_bundle.js"
}
webpack_build("electron_node_bundle") {
esbuild_build("electron_node_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.node_bundle_deps
config_file = "//electron/build/webpack/webpack.config.node.js"
config_file = "//electron/build/esbuild/configs/node.js"
out_file = "$target_gen_dir/js2c/node_init.js"
}
webpack_build("electron_utility_bundle") {
esbuild_build("electron_utility_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.utility_bundle_deps
config_file = "//electron/build/webpack/webpack.config.utility.js"
config_file = "//electron/build/esbuild/configs/utility.js"
out_file = "$target_gen_dir/js2c/utility_init.js"
}
webpack_build("electron_preload_realm_bundle") {
esbuild_build("electron_preload_realm_bundle") {
deps = [ ":build_electron_definitions" ]
inputs = auto_filenames.preload_realm_bundle_deps
config_file = "//electron/build/webpack/webpack.config.preload_realm.js"
config_file = "//electron/build/esbuild/configs/preload_realm.js"
out_file = "$target_gen_dir/js2c/preload_realm_bundle.js"
}
@@ -238,6 +244,7 @@ action("electron_js2c") {
deps = [
":electron_browser_bundle",
":electron_isolated_renderer_bundle",
":electron_lib_typecheck",
":electron_node_bundle",
":electron_preload_realm_bundle",
":electron_renderer_bundle",

2
DEPS
View File

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

326
build/esbuild/bundle.js Normal file
View File

@@ -0,0 +1,326 @@
#!/usr/bin/env node
// Driver script that replaces webpack for building Electron's internal
// JS bundles. Each bundle is a single esbuild invocation parameterized by
// the per-target configuration files under build/esbuild/configs.
//
// Invoked by the GN `esbuild_build` template via `npm run bundle -- …`.
'use strict';
const esbuild = require('esbuild');
const fs = require('node:fs');
const path = require('node:path');
const electronRoot = path.resolve(__dirname, '../..');
function parseArgs (argv) {
const args = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith('--')) {
const key = a.slice(2);
const next = argv[i + 1];
if (next === undefined || next.startsWith('--')) {
args[key] = true;
} else {
args[key] = next;
i++;
}
}
}
return args;
}
// Parse $target_gen_dir/buildflags/buildflags.h (a C++ header containing
// `#define BUILDFLAG_INTERNAL_NAME() (0|1)` lines) into a map of flag name
// to boolean. Used to seed the `define` table so that `BUILDFLAG(NAME)` call
// sites can be statically folded to `true`/`false` at build time.
function parseBuildflags (buildflagsPath) {
const flags = {};
if (!buildflagsPath) return flags;
const source = fs.readFileSync(buildflagsPath, 'utf8');
const re = /#define BUILDFLAG_INTERNAL_(.+?)\(\) \(([01])\)/g;
let match;
while ((match = re.exec(source)) !== null) {
const [, name, value] = match;
flags[name] = value === '1';
}
return flags;
}
// Return the list of esbuild `alias` entries used by every bundle. esbuild's
// alias matches the full module specifier (no `$` suffix trickery like
// webpack), so the bare `electron` alias also matches `electron/main`, etc.,
// because esbuild matches the leftmost segment first.
function buildAliases (electronAPIFile, { aliasTimers }) {
const aliases = {
electron: electronAPIFile,
'electron/main': electronAPIFile,
'electron/renderer': electronAPIFile,
'electron/common': electronAPIFile,
'electron/utility': electronAPIFile
};
// Only browser-platform bundles (sandboxed_renderer, isolated_renderer,
// preload_realm) need the timers shim — Node's `timers` builtin is not
// available there. For node-platform bundles (browser, renderer, worker,
// utility, node) the alias MUST NOT apply: lib/common/init.ts wraps the
// real Node timers and then assigns the wrappers onto globalThis. If
// those bundles saw the shim, the wrappers would recursively call back
// into globalThis.setTimeout and blow the stack.
if (aliasTimers) {
aliases.timers = path.resolve(electronRoot, 'lib', 'common', 'timers-shim.ts');
}
return aliases;
}
// esbuild's `alias` does not support wildcard prefixes like `@electron/internal/*`.
// We instead install a tiny resolve plugin that rewrites any import starting
// with that prefix to an absolute path under `lib/`. The plugin must also
// replicate esbuild's extension/index resolution because returning a path
// from onResolve bypasses the default resolver.
function internalAliasPlugin () {
const candidates = (base) => [
base,
`${base}.ts`,
`${base}.js`,
path.join(base, 'index.ts'),
path.join(base, 'index.js')
];
return {
name: 'electron-internal-alias',
setup (build) {
build.onResolve({ filter: /^@electron\/internal(\/|$)/ }, (args) => {
// Tolerate stray double slashes in import paths (webpack was lenient).
const rel = args.path.replace(/^@electron\/internal\/?/, '').replace(/^\/+/, '');
const base = path.resolve(electronRoot, 'lib', rel);
for (const c of candidates(base)) {
try {
if (fs.statSync(c).isFile()) return { path: c };
} catch { /* keep looking */ }
}
return { errors: [{ text: `Cannot resolve @electron/internal path: ${args.path}` }] };
});
}
};
}
// Rewrites `BUILDFLAG(NAME)` call-sites to `(true)` or `(false)` at load
// time, equivalent to the combination of webpack's DefinePlugin substitution
// (BUILDFLAG -> "" and NAME -> "true"/"false") that the old config used.
// Doing it in a single regex pass keeps the semantics identical and avoids
// fighting with esbuild's AST-level `define` quoting rules.
function buildflagPlugin (buildflags, { allowUnknown = false } = {}) {
return {
name: 'electron-buildflag',
setup (build) {
build.onLoad({ filter: /\.(ts|js)$/ }, async (args) => {
const source = await fs.promises.readFile(args.path, 'utf8');
if (!source.includes('BUILDFLAG(')) {
return { contents: source, loader: args.path.endsWith('.ts') ? 'ts' : 'js' };
}
const rewritten = source.replace(/BUILDFLAG\(([A-Z0-9_]+)\)/g, (_, name) => {
if (!Object.prototype.hasOwnProperty.call(buildflags, name)) {
if (allowUnknown) return '(false)';
throw new Error(`Unknown BUILDFLAG: ${name} (in ${args.path})`);
}
return `(${buildflags[name]})`;
});
return { contents: rewritten, loader: args.path.endsWith('.ts') ? 'ts' : 'js' };
});
}
};
}
// TODO(MarshallOfSound): drop this patch once evanw/esbuild#4441 lands and
// we bump esbuild — that PR adds a `__toCommonJSCached` helper for the
// inline-require path so identity is preserved upstream. Tracked at
// https://github.com/evanw/esbuild/issues/4440.
//
// esbuild's runtime emits `__toCommonJS = (mod) => __copyProps(__defProp({},
// "__esModule", { value: true }), mod)`, which allocates a fresh wrapper
// object every time `require()` resolves to a bundled ESM module. That
// breaks identity expectations our code relies on (e.g. sandboxed preloads
// expecting `require('timers') === require('node:timers')`, and the
// defineProperties getters in lib/common/define-properties.ts expecting
// stable namespaces). A cached WeakMap-backed version of __toCommonJS
// existed in older esbuild releases (see evanw/esbuild#2126) but was
// removed in evanw/esbuild@f4ff26d3 (0.14.27). Substitute a memoized
// variant in post-processing so every call site returns the same wrapper
// for the same underlying namespace, matching webpack's
// `__webpack_require__` cache semantics.
const ESBUILD_TO_COMMONJS_PATTERN =
/var __toCommonJS = \(mod\) => __copyProps\(__defProp\(\{\}, "__esModule", \{ value: true \}\), mod\);/;
const ESBUILD_TO_COMMONJS_REPLACEMENT =
'var __toCommonJS = /* @__PURE__ */ ((cache) => (mod) => {\n' +
' var cached = cache.get(mod);\n' +
' if (cached) return cached;\n' +
' var result = __copyProps(__defProp({}, "__esModule", { value: true }), mod);\n' +
' cache.set(mod, result);\n' +
' return result;\n' +
' })(new WeakMap());';
function patchToCommonJS (source) {
// Once evanw/esbuild#4441 lands, esbuild will emit `__toCommonJSCached`
// for inline require() — when we see that helper in the output, the
// upstream fix is active and this whole patch is a no-op (and should be
// deleted on the next esbuild bump).
if (source.includes('__toCommonJSCached')) {
return source;
}
if (!ESBUILD_TO_COMMONJS_PATTERN.test(source)) {
// Some bundles may not contain any ESM-shaped modules, in which case
// esbuild omits the helper entirely and there is nothing to patch.
if (source.includes('__toCommonJS')) {
throw new Error(
'esbuild bundle contains __toCommonJS but did not match the ' +
'expected pattern; the runtime helper has likely changed upstream. ' +
'Update ESBUILD_TO_COMMONJS_PATTERN in build/esbuild/bundle.js, or ' +
'delete patchToCommonJS entirely if evanw/esbuild#4441 has landed.'
);
}
return source;
}
return source.replace(ESBUILD_TO_COMMONJS_PATTERN, ESBUILD_TO_COMMONJS_REPLACEMENT);
}
// Wrap bundle source text in the same header/footer pairs webpack's
// wrapper-webpack-plugin used. The try/catch wrapper is load-bearing:
// shell/common/node_util.cc's CompileAndCall relies on it to prevent
// exceptions from tearing down bootstrap.
function applyWrappers (source, opts, outputFilename) {
let wrapped = patchToCommonJS(source);
if (opts.wrapInitWithProfilingTimeout) {
const header = 'function ___electron_webpack_init__() {';
const footer = '\n};\nif ((globalThis.process || binding.process).argv.includes("--profile-electron-init")) {\n setTimeout(___electron_webpack_init__, 0);\n} else {\n ___electron_webpack_init__();\n}';
wrapped = header + wrapped + footer;
}
if (opts.wrapInitWithTryCatch) {
const header = 'try {';
const footer = `\n} catch (err) {\n console.error('Electron ${outputFilename} script failed to run');\n console.error(err);\n}`;
wrapped = header + wrapped + footer;
}
return wrapped;
}
async function buildBundle (opts, cliArgs) {
const {
target,
alwaysHasNode,
loadElectronFromAlternateTarget,
wrapInitWithProfilingTimeout,
wrapInitWithTryCatch
} = opts;
const outputFilename = cliArgs['output-filename'] || `${target}.bundle.js`;
const outputPath = cliArgs['output-path'] || path.resolve(electronRoot, 'out');
const mode = cliArgs.mode || 'development';
const minify = mode === 'production';
const printGraph = !!cliArgs['print-graph'];
let entry = path.resolve(electronRoot, 'lib', target, 'init.ts');
if (!fs.existsSync(entry)) {
entry = path.resolve(electronRoot, 'lib', target, 'init.js');
}
const electronAPIFile = path.resolve(
electronRoot,
'lib',
loadElectronFromAlternateTarget || target,
'api',
'exports',
'electron.ts'
);
const buildflags = parseBuildflags(cliArgs.buildflags);
// Shims that stand in for webpack ProvidePlugin. Each target gets the
// minimum set of globals it needs; the capture files mirror the originals
// under lib/common so the behavior (grab globals before user code can
// delete them) is preserved exactly.
const inject = [];
if (opts.targetDeletesNodeGlobals) {
inject.push(path.resolve(__dirname, 'shims', 'node-globals-shim.js'));
}
if (!alwaysHasNode) {
inject.push(path.resolve(__dirname, 'shims', 'browser-globals-shim.js'));
}
inject.push(path.resolve(__dirname, 'shims', 'promise-shim.js'));
const result = await esbuild.build({
entryPoints: [entry],
bundle: true,
format: 'iife',
platform: alwaysHasNode ? 'node' : 'browser',
target: 'es2022',
minify,
// Preserve class/function names in both development and production so
// gin_helper-surfaced constructor names and stack traces stay readable.
// (Under webpack this only mattered when terser ran in is_official_build;
// esbuild applies the same rename pressure in dev too, so keep it on
// unconditionally for consistency.)
keepNames: true,
sourcemap: false,
logLevel: 'warning',
metafile: true,
write: false,
resolveExtensions: ['.ts', '.js'],
alias: buildAliases(electronAPIFile, { aliasTimers: !alwaysHasNode }),
inject,
define: {
__non_webpack_require__: 'require'
},
// Node internal modules we pull through __non_webpack_require__ at runtime.
// These must not be bundled — esbuild should leave the literal require()
// call alone so the outer Node scope resolves them.
external: [
'internal/modules/helpers',
'internal/modules/run_main',
'internal/fs/utils',
'internal/util',
'internal/validators',
'internal/url'
],
plugins: [
internalAliasPlugin(),
buildflagPlugin(buildflags, { allowUnknown: printGraph })
]
});
if (printGraph) {
const inputs = Object.keys(result.metafile.inputs)
.filter((p) => !p.includes('node_modules') && !p.startsWith('..'))
.map((p) => path.relative(electronRoot, path.resolve(electronRoot, p)));
process.stdout.write(JSON.stringify(inputs) + '\n');
return;
}
if (result.outputFiles.length !== 1) {
throw new Error(`Expected exactly one output file, got ${result.outputFiles.length}`);
}
const wrapped = applyWrappers(
result.outputFiles[0].text,
{ wrapInitWithProfilingTimeout, wrapInitWithTryCatch },
outputFilename
);
await fs.promises.mkdir(outputPath, { recursive: true });
await fs.promises.writeFile(path.join(outputPath, outputFilename), wrapped);
}
async function main () {
const cliArgs = parseArgs(process.argv.slice(2));
if (!cliArgs.config) {
console.error('Usage: bundle.js --config <path> [--output-filename X] [--output-path Y] [--mode development|production] [--buildflags path/to/buildflags.h] [--print-graph]');
process.exit(1);
}
const configPath = path.resolve(cliArgs.config);
const opts = require(configPath);
await buildBundle(opts, cliArgs);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,4 @@
module.exports = {
target: 'browser',
alwaysHasNode: true
};

View File

@@ -1,5 +1,5 @@
module.exports = require('./webpack.config.base')({
module.exports = {
target: 'isolated_renderer',
alwaysHasNode: false,
wrapInitWithTryCatch: true
});
};

View File

@@ -0,0 +1,4 @@
module.exports = {
target: 'node',
alwaysHasNode: true
};

View File

@@ -1,6 +1,6 @@
module.exports = require('./webpack.config.base')({
module.exports = {
target: 'preload_realm',
alwaysHasNode: false,
wrapInitWithProfilingTimeout: true,
wrapInitWithTryCatch: true
});
};

View File

@@ -1,7 +1,7 @@
module.exports = require('./webpack.config.base')({
module.exports = {
target: 'renderer',
alwaysHasNode: true,
targetDeletesNodeGlobals: true,
wrapInitWithProfilingTimeout: true,
wrapInitWithTryCatch: true
});
};

View File

@@ -1,6 +1,6 @@
module.exports = require('./webpack.config.base')({
module.exports = {
target: 'sandboxed_renderer',
alwaysHasNode: false,
wrapInitWithProfilingTimeout: true,
wrapInitWithTryCatch: true
});
};

View File

@@ -0,0 +1,4 @@
module.exports = {
target: 'utility',
alwaysHasNode: true
};

View File

@@ -1,7 +1,7 @@
module.exports = require('./webpack.config.base')({
module.exports = {
target: 'worker',
loadElectronFromAlternateTarget: 'renderer',
alwaysHasNode: true,
targetDeletesNodeGlobals: true,
wrapInitWithTryCatch: true
});
};

View File

@@ -1,9 +1,9 @@
import("../npm.gni")
template("webpack_build") {
assert(defined(invoker.config_file), "Need webpack config file to run")
template("esbuild_build") {
assert(defined(invoker.config_file), "Need esbuild config file to run")
assert(defined(invoker.out_file), "Need output file to run")
assert(defined(invoker.inputs), "Need webpack inputs to run")
assert(defined(invoker.inputs), "Need esbuild inputs to run")
npm_action(target_name) {
forward_variables_from(invoker,
@@ -11,11 +11,14 @@ template("webpack_build") {
"deps",
"public_deps",
])
script = "webpack"
script = "bundle"
inputs = [
invoker.config_file,
"//electron/build/webpack/webpack.config.base.js",
"//electron/build/esbuild/bundle.js",
"//electron/build/esbuild/shims/node-globals-shim.js",
"//electron/build/esbuild/shims/browser-globals-shim.js",
"//electron/build/esbuild/shims/promise-shim.js",
"//electron/tsconfig.json",
"//electron/yarn.lock",
"//electron/typings/internal-ambient.d.ts",
@@ -34,10 +37,10 @@ template("webpack_build") {
get_path_info(invoker.out_file, "file"),
"--output-path",
rebase_path(get_path_info(invoker.out_file, "dir")),
"--env",
"buildflags=" + rebase_path("$target_gen_dir/buildflags/buildflags.h"),
"--env",
"mode=" + mode,
"--buildflags",
rebase_path("$target_gen_dir/buildflags/buildflags.h"),
"--mode",
mode,
]
deps += [ "//electron/buildflags" ]

View File

@@ -0,0 +1,18 @@
// Injected into browser-platform bundles (sandboxed_renderer, isolated_renderer,
// preload_realm) where Node globals are not implicitly available. Supplies
// `Buffer`, `process`, and `global` — replacing webpack's ProvidePlugin
// polyfill injection plus webpack 5's built-in `global -> globalThis` rewrite
// that `target: 'web'` performed automatically.
//
// The `buffer` and `process/browser` imports below intentionally use the
// npm polyfill packages, not Node's built-in `node:buffer` / `node:process`
// modules, because these shims ship into browser-platform bundles that do
// not have Node globals available at runtime.
/* eslint-disable import/order, import/enforce-node-protocol-usage */
import { Buffer as _Buffer } from 'buffer';
import _process from 'process/browser';
const _global = globalThis;
export { _Buffer as Buffer, _process as process, _global as global };

View File

@@ -0,0 +1,14 @@
// Injected into renderer/worker bundles to replace webpack's ProvidePlugin
// that captured `Buffer`, `global`, and `process` before user code could
// delete them from the global scope. The Module.wrapper override in
// lib/renderer/init.ts re-injects these into user preload scripts later.
// Rip globals off of globalThis/self/window so they are captured in this
// module's closure and retained even if the caller later deletes them.
const _global = typeof globalThis !== 'undefined'
? globalThis.global
: (self || window).global;
const _process = _global.process;
const _Buffer = _global.Buffer;
export { _global as global, _process as process, _Buffer as Buffer };

View File

@@ -0,0 +1,7 @@
// Captures the original `Promise` constructor so that userland mutations of
// `global.Promise.resolve` do not affect Electron's internal code. Mirrors
// webpack's ProvidePlugin reference to lib/common/webpack-globals-provider.
const _Promise = globalThis.Promise;
export { _Promise as Promise };

View File

@@ -1,5 +1,42 @@
import("npm.gni")
# Runs `tsgo --noEmit` over a tsconfig via the `tsc-check` npm script (which
# wraps script/typecheck.js) and writes a stamp on success. Use this to gate
# downstream targets on a successful typecheck without emitting JS.
template("typescript_check") {
assert(defined(invoker.tsconfig), "Need tsconfig name to run")
assert(defined(invoker.sources), "Need tsc sources to run")
npm_action(target_name) {
forward_variables_from(invoker,
[
"deps",
"public_deps",
])
script = "tsc-check"
sources = invoker.sources
inputs = [
invoker.tsconfig,
"//electron/tsconfig.json",
"//electron/yarn.lock",
"//electron/script/typecheck.js",
"//electron/typings/internal-ambient.d.ts",
"//electron/typings/internal-electron.d.ts",
]
stamp_file = "$target_gen_dir/$target_name.stamp"
outputs = [ stamp_file ]
args = [
"--tsconfig",
rebase_path(invoker.tsconfig),
"--stamp",
rebase_path(stamp_file),
]
}
}
template("typescript_build") {
assert(defined(invoker.tsconfig), "Need tsconfig name to run")
assert(defined(invoker.sources), "Need tsc sources to run")

View File

@@ -1,172 +0,0 @@
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
const WrapperPlugin = require('wrapper-webpack-plugin');
const fs = require('node:fs');
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));
console.info(JSON.stringify(filePaths));
});
});
}
}
module.exports = ({
alwaysHasNode,
loadElectronFromAlternateTarget,
targetDeletesNodeGlobals,
target,
wrapInitWithProfilingTimeout,
wrapInitWithTryCatch
}) => {
let entry = path.resolve(electronRoot, 'lib', target, 'init.ts');
if (!fs.existsSync(entry)) {
entry = path.resolve(electronRoot, 'lib', target, 'init.js');
}
const electronAPIFile = path.resolve(electronRoot, 'lib', loadElectronFromAlternateTarget || target, 'api', 'exports', 'electron.ts');
return (env = {}, argv = {}) => {
const onlyPrintingGraph = !!env.PRINT_WEBPACK_GRAPH;
const outputFilename = argv['output-filename'] || `${target}.bundle.js`;
const defines = {
BUILDFLAG: onlyPrintingGraph ? '(a => a)' : ''
};
if (env.buildflags) {
const flagFile = fs.readFileSync(env.buildflags, 'utf8');
for (const line of flagFile.split(/(\r\n|\r|\n)/g)) {
const flagMatch = line.match(/#define BUILDFLAG_INTERNAL_(.+?)\(\) \(([01])\)/);
if (flagMatch) {
const [, flagName, flagValue] = flagMatch;
defines[flagName] = JSON.stringify(Boolean(parseInt(flagValue, 10)));
}
}
}
const ignoredModules = [];
const plugins = [];
if (onlyPrintingGraph) {
plugins.push(new AccessDependenciesPlugin());
}
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']
}));
}
// 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({
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: `
};
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: `
} catch (err) {
console.error('Electron ${outputFilename} script failed to run');
console.error(err);
}`
}));
}
return {
mode: 'development',
devtool: false,
entry,
target: alwaysHasNode ? 'node' : 'web',
output: {
filename: outputFilename
},
resolve: {
alias: {
'@electron/internal': path.resolve(electronRoot, 'lib'),
electron$: electronAPIFile,
'electron/main$': electronAPIFile,
'electron/renderer$': electronAPIFile,
'electron/common$': electronAPIFile,
'electron/utility$': electronAPIFile,
// Force timers to resolve to our own shim that doesn't use window.postMessage
timers: path.resolve(electronRoot, 'lib', 'common', 'timers-shim.ts')
},
extensions: ['.ts', '.js'],
fallback: {
// We provide our own "timers" import above, any usage of setImmediate inside
// one of our renderer bundles should import it from the 'timers' package
setImmediate: false
}
},
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
]
}
}]
},
node: {
__dirname: false,
__filename: false
},
optimization: {
minimize: env.mode === 'production',
minimizer: [
new TerserPlugin({
terserOptions: {
keep_classnames: true,
keep_fnames: true
}
})
]
},
plugins
};
};
};

View File

@@ -1,4 +0,0 @@
module.exports = require('./webpack.config.base')({
target: 'browser',
alwaysHasNode: true
});

View File

@@ -1,4 +0,0 @@
module.exports = require('./webpack.config.base')({
target: 'node',
alwaysHasNode: true
});

View File

@@ -1,4 +0,0 @@
module.exports = require('./webpack.config.base')({
target: 'utility',
alwaysHasNode: true
});

View File

@@ -615,7 +615,11 @@ Returns `string` - The current application directory.
by default is the `appData` directory appended with your app's name. By
convention files storing user data should be written to this directory, and
it is not recommended to write large files here because some environments
may backup this directory to cloud storage.
may backup this directory to cloud storage. It is recommended to store
app-specific files within a subdirectory of `userData` (e.g.,
`path.join(app.getPath('userData'), 'my-app-data')`) rather than directly
in `userData` itself, to avoid naming conflicts with Chromium's own
subdirectories (such as `Cache`, `GPUCache`, and `Local Storage`).
* `sessionData` The directory for storing data generated by `Session`, such
as localStorage, cookies, disk cache, downloaded dictionaries, network
state, DevTools files. By default this points to `userData`. Chromium may

View File

@@ -1097,7 +1097,8 @@ public:
Napi::Function func = DefineClass(env, "CppLinuxAddon", {
InstanceMethod("helloWorld", &CppAddon::HelloWorld),
InstanceMethod("helloGui", &CppAddon::HelloGui),
InstanceMethod("on", &CppAddon::On)
InstanceMethod("on", &CppAddon::On),
InstanceMethod("destroy", &CppAddon::Destroy)
});
Napi::FunctionReference *constructor = new Napi::FunctionReference();
@@ -1139,11 +1140,12 @@ private:
Here, we create a C++ class that inherits from `Napi::ObjectWrap<CppAddon>`:
`static Napi::Object Init` defines our JavaScript interface with three methods:
`static Napi::Object Init` defines our JavaScript interface with four methods:
* `helloWorld`: A simple function to test the bridge
* `helloGui`: The function to launch our GTK3 UI
* `on`: A method to register event callbacks
* `destroy`: A method to release all persistent references before app quit
The constructor initializes:
@@ -1354,7 +1356,8 @@ public:
Napi::Function func = DefineClass(env, "CppLinuxAddon", {
InstanceMethod("helloWorld", &CppAddon::HelloWorld),
InstanceMethod("helloGui", &CppAddon::HelloGui),
InstanceMethod("on", &CppAddon::On)
InstanceMethod("on", &CppAddon::On),
InstanceMethod("destroy", &CppAddon::Destroy)
});
Napi::FunctionReference *constructor = new Napi::FunctionReference();
@@ -1497,6 +1500,20 @@ private:
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo &info)
{
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr)
{
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports)
@@ -1547,6 +1564,10 @@ class CppLinuxAddon extends EventEmitter {
return this.addon.helloGui()
}
destroy() {
this.addon.destroy()
}
// Parse JSON and convert date to JavaScript Date object
parse(payload) {
const parsed = JSON.parse(payload)
@@ -1569,8 +1590,12 @@ This wrapper:
* Only loads on Linux platforms
* Forwards events from C++ to JavaScript
* Provides clean methods to call into C++
* Provides a `destroy()` method to release native resources
* Converts JSON data into proper JavaScript objects
> [!IMPORTANT]
> You must call `destroy()` before the app quits (e.g. in the `will-quit` or `before-quit` event handler). Without this, persistent references to callbacks and the threadsafe function will prevent the native addon's destructor from running, causing Electron to hang on quit.
## 7) Building and testing the addon
With all files in place, you can build the addon:

View File

@@ -1099,7 +1099,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "CppWin32Addon", {
InstanceMethod("helloWorld", &CppAddon::HelloWorld),
InstanceMethod("helloGui", &CppAddon::HelloGui),
InstanceMethod("on", &CppAddon::On)
InstanceMethod("on", &CppAddon::On),
InstanceMethod("destroy", &CppAddon::Destroy)
});
// ... rest of Init function
@@ -1117,9 +1118,21 @@ Napi::Value On(const Napi::CallbackInfo& info) {
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo& info) {
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
```
This allows JavaScript to register callbacks for specific event types.
This allows JavaScript to register callbacks for specific event types. The `Destroy` method releases all persistent references and aborts the threadsafe function, which must be called before the app quits to prevent the process from hanging.
### Putting the bridge together
@@ -1261,6 +1274,18 @@ private:
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo& info) {
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
@@ -1309,6 +1334,10 @@ class CppWin32Addon extends EventEmitter {
this.addon.helloGui()
}
destroy() {
this.addon.destroy()
}
#parse(payload) {
const parsed = JSON.parse(payload)
@@ -1323,6 +1352,9 @@ if (process.platform === 'win32') {
}
```
> [!IMPORTANT]
> You must call `destroy()` before the app quits (e.g. in the `will-quit` or `before-quit` event handler). Without this, persistent references to callbacks and the threadsafe function will prevent the native addon's destructor from running, causing Electron to hang on quit.
## 7) Building and Testing the Addon
With all files in place, you can build the addon:

View File

@@ -753,7 +753,8 @@ public:
Napi::Function func = DefineClass(env, "ObjcMacosAddon", {
InstanceMethod("helloWorld", &ObjcAddon::HelloWorld),
InstanceMethod("helloGui", &ObjcAddon::HelloGui),
InstanceMethod("on", &ObjcAddon::On)
InstanceMethod("on", &ObjcAddon::On),
InstanceMethod("destroy", &ObjcAddon::Destroy)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -915,6 +916,18 @@ Napi::Value On(const Napi::CallbackInfo& info) {
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo& info) {
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
```
Let's take a look at what we've added in this step:
@@ -922,10 +935,11 @@ Let's take a look at what we've added in this step:
* `HelloWorld()`: Takes a string input, calls our Objective-C function, and returns the result
* `HelloGui()`: A simple wrapper around the Objective-C `hello_gui` function
* `On`: Allows JavaScript to register event listeners that will be called when native events occur
* `Destroy`: Releases all persistent references (callbacks and emitter) and aborts the threadsafe function, allowing the addon to be properly cleaned up on quit
The `On` method is particularly important as it creates the event system that our JavaScript code will use to receive notifications from the native UI.
Together, these three components form a complete bridge between our Objective-C code and the JavaScript world, allowing bidirectional communication. Here's what the finished file should look like:
Together, these four components form a complete bridge between our Objective-C code and the JavaScript world, allowing bidirectional communication. Here's what the finished file should look like:
```objc title='src/objc_addon.mm'
#include <napi.h>
@@ -938,7 +952,8 @@ public:
Napi::Function func = DefineClass(env, "ObjcMacosAddon", {
InstanceMethod("helloWorld", &ObjcAddon::HelloWorld),
InstanceMethod("helloGui", &ObjcAddon::HelloGui),
InstanceMethod("on", &ObjcAddon::On)
InstanceMethod("on", &ObjcAddon::On),
InstanceMethod("destroy", &ObjcAddon::Destroy)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -1061,6 +1076,18 @@ private:
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo& info) {
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
@@ -1101,6 +1128,10 @@ class ObjcMacosAddon extends EventEmitter {
this.addon.helloGui()
}
destroy () {
this.addon.destroy()
}
parse (payload) {
const parsed = JSON.parse(payload)
@@ -1122,7 +1153,11 @@ This wrapper:
3. Loads the native addon
4. Sets up event listeners and forwards them
5. Provides a clean API for our functions
6. Parses JSON payloads and converts timestamps to JavaScript Date objects
6. Provides a `destroy()` method to release native resources
7. Parses JSON payloads and converts timestamps to JavaScript Date objects
> [!IMPORTANT]
> You must call `destroy()` before the app quits (e.g. in the `will-quit` or `before-quit` event handler). Without this, persistent references to callbacks and the threadsafe function will prevent the native addon's destructor from running, causing Electron to hang on quit.
## 7) Building and Testing the Addon

View File

@@ -752,7 +752,8 @@ public:
Napi::Function func = DefineClass(env, "SwiftAddon", {
InstanceMethod("helloWorld", &SwiftAddon::HelloWorld),
InstanceMethod("helloGui", &SwiftAddon::HelloGui),
InstanceMethod("on", &SwiftAddon::On)
InstanceMethod("on", &SwiftAddon::On),
InstanceMethod("destroy", &SwiftAddon::Destroy)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -770,7 +771,7 @@ This first part:
1. Defines a C++ class that inherits from `Napi::ObjectWrap`
2. Creates a static `Init` method to register our class with Node.js
3. Defines three methods: `helloWorld`, `helloGui`, and `on`
3. Defines four methods: `helloWorld`, `helloGui`, `on`, and `destroy`
### Callback Mechanism
@@ -919,6 +920,18 @@ private:
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo& info) {
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
@@ -934,7 +947,8 @@ This final part does multiple things:
2. The HelloWorld method implementation takes a string input from JavaScript, passes it to the Swift code, and returns the processed result back to the JavaScript environment.
3. The `HelloGui` method implementation provides a simple wrapper that calls the Swift UI creation function to display the native macOS window.
4. The `On` method implementation allows JavaScript code to register callback functions that will be invoked when specific events occur in the native Swift code.
5. The code sets up the module initialization process that registers the addon with Node.js and makes its functionality available to JavaScript.
5. The `Destroy` method releases all persistent references (callbacks and emitter) and aborts the threadsafe function. This must be called before the app quits to allow the destructor to run and prevent the process from hanging.
6. The code sets up the module initialization process that registers the addon with Node.js and makes its functionality available to JavaScript.
The final and full `src/swift_addon.mm` should look like:
@@ -949,7 +963,8 @@ public:
Napi::Function func = DefineClass(env, "SwiftAddon", {
InstanceMethod("helloWorld", &SwiftAddon::HelloWorld),
InstanceMethod("helloGui", &SwiftAddon::HelloGui),
InstanceMethod("on", &SwiftAddon::On)
InstanceMethod("on", &SwiftAddon::On),
InstanceMethod("destroy", &SwiftAddon::Destroy)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -1074,6 +1089,18 @@ private:
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
Napi::Value Destroy(const Napi::CallbackInfo& info) {
callbacks.Reset();
emitter.Reset();
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
tsfn_ = nullptr;
}
return info.Env().Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
@@ -1122,6 +1149,10 @@ class SwiftAddon extends EventEmitter {
this.addon.helloGui()
}
destroy () {
this.addon.destroy()
}
parse (payload) {
const parsed = JSON.parse(payload)
@@ -1143,7 +1174,11 @@ This wrapper:
3. Loads the native addon
4. Sets up event listeners and forwards them
5. Provides a clean API for our functions
6. Parses JSON payloads and converts timestamps to JavaScript Date objects
6. Provides a `destroy()` method to release native resources
7. Parses JSON payloads and converts timestamps to JavaScript Date objects
> [!IMPORTANT]
> You must call `destroy()` before the app quits (e.g. in the `will-quit` or `before-quit` event handler). Without this, persistent references to callbacks and the threadsafe function will prevent the native addon's destructor from running, causing Electron to hang on quit.
## 7) Building and Testing the Addon

View File

@@ -174,62 +174,10 @@ auto_filenames = {
"docs/api/structures/window-session-end-event.md",
]
sandbox_bundle_deps = [
"lib/common/api/native-image.ts",
"lib/common/define-properties.ts",
"lib/common/deprecate.ts",
"lib/common/ipc-messages.ts",
"lib/common/timers-shim.ts",
"lib/common/web-view-methods.ts",
"lib/common/webpack-globals-provider.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/api/shared-texture.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/api/web-utils.ts",
"lib/renderer/common-init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
"lib/renderer/web-frame-init.ts",
"lib/renderer/web-view/guest-view-internal.ts",
"lib/renderer/web-view/web-view-attributes.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"lib/renderer/web-view/web-view-init.ts",
"lib/renderer/window-setup.ts",
"lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts",
"lib/sandboxed_renderer/init.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
isolated_bundle_deps = [
"lib/common/web-view-methods.ts",
"lib/isolated_renderer/init.ts",
"lib/renderer/web-view/web-view-attributes.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
browser_bundle_deps = [
typecheck_sources = [
"build/esbuild/shims/browser-globals-shim.js",
"build/esbuild/shims/node-globals-shim.js",
"build/esbuild/shims/promise-shim.js",
"lib/browser/api/app.ts",
"lib/browser/api/auto-updater.ts",
"lib/browser/api/auto-updater/auto-updater-msix.ts",
@@ -300,8 +248,189 @@ auto_filenames = {
"lib/common/deprecate.ts",
"lib/common/init.ts",
"lib/common/ipc-messages.ts",
"lib/common/timers-shim.ts",
"lib/common/web-view-methods.ts",
"lib/isolated_renderer/init.ts",
"lib/node/asar-fs-wrapper.ts",
"lib/node/init.ts",
"lib/preload_realm/api/exports/electron.ts",
"lib/preload_realm/api/module-list.ts",
"lib/preload_realm/init.ts",
"lib/renderer/api/clipboard.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.ts",
"lib/renderer/api/exports/electron.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/api/module-list.ts",
"lib/renderer/api/shared-texture.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/api/web-utils.ts",
"lib/renderer/common-init.ts",
"lib/renderer/init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
"lib/renderer/web-frame-init.ts",
"lib/renderer/web-view/guest-view-internal.ts",
"lib/renderer/web-view/web-view-attributes.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"lib/renderer/web-view/web-view-init.ts",
"lib/renderer/window-setup.ts",
"lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts",
"lib/sandboxed_renderer/init.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"lib/utility/api/exports/electron.ts",
"lib/utility/api/module-list.ts",
"lib/utility/api/net.ts",
"lib/utility/init.ts",
"lib/utility/parent-port.ts",
"lib/worker/init.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
sandbox_bundle_deps = [
"build/esbuild/shims/browser-globals-shim.js",
"build/esbuild/shims/promise-shim.js",
"lib/common/api/native-image.ts",
"lib/common/define-properties.ts",
"lib/common/deprecate.ts",
"lib/common/ipc-messages.ts",
"lib/common/timers-shim.ts",
"lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/api/shared-texture.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/api/web-utils.ts",
"lib/renderer/common-init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
"lib/renderer/web-frame-init.ts",
"lib/renderer/web-view/guest-view-internal.ts",
"lib/renderer/web-view/web-view-attributes.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"lib/renderer/web-view/web-view-init.ts",
"lib/renderer/window-setup.ts",
"lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts",
"lib/sandboxed_renderer/init.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
isolated_bundle_deps = [
"build/esbuild/shims/browser-globals-shim.js",
"build/esbuild/shims/promise-shim.js",
"lib/common/web-view-methods.ts",
"lib/isolated_renderer/init.ts",
"lib/renderer/web-view/web-view-attributes.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
browser_bundle_deps = [
"build/esbuild/shims/promise-shim.js",
"lib/browser/api/app.ts",
"lib/browser/api/auto-updater.ts",
"lib/browser/api/auto-updater/auto-updater-msix.ts",
"lib/browser/api/auto-updater/auto-updater-native.ts",
"lib/browser/api/auto-updater/auto-updater-win.ts",
"lib/browser/api/auto-updater/msix-update-win.ts",
"lib/browser/api/auto-updater/squirrel-update-win.ts",
"lib/browser/api/base-window.ts",
"lib/browser/api/browser-view.ts",
"lib/browser/api/browser-window.ts",
"lib/browser/api/clipboard.ts",
"lib/browser/api/content-tracing.ts",
"lib/browser/api/crash-reporter.ts",
"lib/browser/api/desktop-capturer.ts",
"lib/browser/api/dialog.ts",
"lib/browser/api/exports/electron.ts",
"lib/browser/api/global-shortcut.ts",
"lib/browser/api/in-app-purchase.ts",
"lib/browser/api/ipc-main.ts",
"lib/browser/api/menu-item-roles.ts",
"lib/browser/api/menu-item.ts",
"lib/browser/api/menu-utils.ts",
"lib/browser/api/menu.ts",
"lib/browser/api/message-channel.ts",
"lib/browser/api/module-list.ts",
"lib/browser/api/native-theme.ts",
"lib/browser/api/net-fetch.ts",
"lib/browser/api/net-log.ts",
"lib/browser/api/net.ts",
"lib/browser/api/notification.ts",
"lib/browser/api/power-monitor.ts",
"lib/browser/api/power-save-blocker.ts",
"lib/browser/api/protocol.ts",
"lib/browser/api/push-notifications.ts",
"lib/browser/api/safe-storage.ts",
"lib/browser/api/screen.ts",
"lib/browser/api/service-worker-main.ts",
"lib/browser/api/session.ts",
"lib/browser/api/share-menu.ts",
"lib/browser/api/shared-texture.ts",
"lib/browser/api/system-preferences.ts",
"lib/browser/api/touch-bar.ts",
"lib/browser/api/tray.ts",
"lib/browser/api/utility-process.ts",
"lib/browser/api/view.ts",
"lib/browser/api/views/image-view.ts",
"lib/browser/api/web-contents-view.ts",
"lib/browser/api/web-contents.ts",
"lib/browser/api/web-frame-main.ts",
"lib/browser/default-menu.ts",
"lib/browser/devtools.ts",
"lib/browser/guest-view-manager.ts",
"lib/browser/guest-window-manager.ts",
"lib/browser/init.ts",
"lib/browser/ipc-dispatch.ts",
"lib/browser/ipc-main-impl.ts",
"lib/browser/ipc-main-internal-utils.ts",
"lib/browser/ipc-main-internal.ts",
"lib/browser/message-port-main.ts",
"lib/browser/parse-features-string.ts",
"lib/browser/rpc-server.ts",
"lib/browser/web-view-events.ts",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/net-client-request.ts",
"lib/common/api/shell.ts",
"lib/common/define-properties.ts",
"lib/common/deprecate.ts",
"lib/common/init.ts",
"lib/common/ipc-messages.ts",
"lib/common/timers-shim.ts",
"lib/common/web-view-methods.ts",
"lib/common/webpack-globals-provider.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
@@ -310,6 +439,8 @@ auto_filenames = {
]
renderer_bundle_deps = [
"build/esbuild/shims/node-globals-shim.js",
"build/esbuild/shims/promise-shim.js",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/shell.ts",
@@ -317,8 +448,8 @@ auto_filenames = {
"lib/common/deprecate.ts",
"lib/common/init.ts",
"lib/common/ipc-messages.ts",
"lib/common/timers-shim.ts",
"lib/common/web-view-methods.ts",
"lib/common/webpack-provider.ts",
"lib/renderer/api/clipboard.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.ts",
@@ -352,6 +483,8 @@ auto_filenames = {
]
worker_bundle_deps = [
"build/esbuild/shims/node-globals-shim.js",
"build/esbuild/shims/promise-shim.js",
"lib/common/api/module-list.ts",
"lib/common/api/native-image.ts",
"lib/common/api/shell.ts",
@@ -359,7 +492,7 @@ auto_filenames = {
"lib/common/deprecate.ts",
"lib/common/init.ts",
"lib/common/ipc-messages.ts",
"lib/common/webpack-provider.ts",
"lib/common/timers-shim.ts",
"lib/renderer/api/clipboard.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.ts",
@@ -381,6 +514,7 @@ auto_filenames = {
]
node_bundle_deps = [
"build/esbuild/shims/promise-shim.js",
"lib/node/asar-fs-wrapper.ts",
"lib/node/init.ts",
"package.json",
@@ -391,6 +525,7 @@ auto_filenames = {
]
utility_bundle_deps = [
"build/esbuild/shims/promise-shim.js",
"lib/browser/api/net-fetch.ts",
"lib/browser/api/system-preferences.ts",
"lib/browser/message-port-main.ts",
@@ -398,7 +533,7 @@ auto_filenames = {
"lib/common/define-properties.ts",
"lib/common/deprecate.ts",
"lib/common/init.ts",
"lib/common/webpack-globals-provider.ts",
"lib/common/timers-shim.ts",
"lib/utility/api/exports/electron.ts",
"lib/utility/api/module-list.ts",
"lib/utility/api/net.ts",
@@ -412,10 +547,11 @@ auto_filenames = {
]
preload_realm_bundle_deps = [
"build/esbuild/shims/browser-globals-shim.js",
"build/esbuild/shims/promise-shim.js",
"lib/common/api/native-image.ts",
"lib/common/define-properties.ts",
"lib/common/ipc-messages.ts",
"lib/common/webpack-globals-provider.ts",
"lib/preload_realm/api/exports/electron.ts",
"lib/preload_realm/api/module-list.ts",
"lib/preload_realm/init.ts",

View File

@@ -5,23 +5,27 @@ import type { ClientRequestConstructorOptions } from 'electron/main';
const { isOnline } = process._linkedBinding('electron_common_net');
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
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> {
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> {
function resolveHost (host: string, options?: Electron.ResolveHostOptions): Promise<Electron.ResolvedHost> {
return session.defaultSession.resolveHost(host, options);
}
exports.isOnline = isOnline;
Object.defineProperty(exports, 'online', {
get: () => isOnline()
});
module.exports = {
request,
fetch,
resolveHost,
isOnline,
get online () {
return isOnline();
}
};

View File

@@ -181,7 +181,7 @@ 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');
const main = (require('url') as typeof url).pathToFileURL(path.join(packagePath, mainStartupScript));
runEntryPointWithESMLoader(async (cascadedLoader: any) => {
try {

View File

@@ -1,4 +1,4 @@
import timers = require('timers');
import * as timers from 'timers';
import * as util from 'util';
import type * as stream from 'stream';
@@ -41,15 +41,15 @@ function wrap <T extends AnyFn> (func: T, wrapper: (fn: AnyFn) => T) {
// initiatively activate the uv loop once process.nextTick and setImmediate is
// called.
process.nextTick = wrapWithActivateUvLoop(process.nextTick);
global.setImmediate = timers.setImmediate = wrapWithActivateUvLoop(timers.setImmediate);
global.setImmediate = wrapWithActivateUvLoop(timers.setImmediate);
global.clearImmediate = timers.clearImmediate;
// setTimeout needs to update the polling timeout of the event loop, when
// called under Chromium's event loop the node's event loop won't get a chance
// to update the timeout, so we have to force the node's event loop to
// recalculate the timeout in the process.
timers.setTimeout = wrapWithActivateUvLoop(timers.setTimeout);
timers.setInterval = wrapWithActivateUvLoop(timers.setInterval);
const wrappedSetTimeout = wrapWithActivateUvLoop(timers.setTimeout);
const wrappedSetInterval = wrapWithActivateUvLoop(timers.setInterval);
// Update the global version of the timer apis to use the above wrapper
// only in the process that runs node event loop alongside chromium
@@ -57,8 +57,8 @@ timers.setInterval = wrapWithActivateUvLoop(timers.setInterval);
// are deleted in these processes, see renderer/init.js for reference.
if (process.type === 'browser' ||
process.type === 'utility') {
global.setTimeout = timers.setTimeout;
global.setInterval = timers.setInterval;
global.setTimeout = wrappedSetTimeout;
global.setInterval = wrappedSetInterval;
}
if (process.platform === 'win32') {

View File

@@ -1,5 +1,5 @@
// Drop-in replacement for timers-browserify@1.4.2.
// Provides the Node.js 'timers' API surface for renderer/web webpack bundles
// Provides the Node.js 'timers' API surface for renderer/web bundles
// without relying on window.postMessage (which the newer timers-browserify 2.x
// polyfill uses and can interfere with Electron IPC).

View File

@@ -1,8 +0,0 @@
// Captures original globals into a scope to ensure that userland modifications do
// not impact Electron. Note that users doing:
//
// global.Promise.resolve = myFn
//
// Will mutate this captured one as well and that is OK.
export const Promise = global.Promise;

View File

@@ -1,18 +0,0 @@
// This file provides the global, process and Buffer variables to internal
// Electron code once they have been deleted from the global scope.
//
// It does this through the ProvidePlugin in the webpack.config.base.js file
// Check out the Module.wrapper override in renderer/init.ts for more
// information on how this works and why we need it
// Rip global off of window (which is also global) so that webpack doesn't
// auto replace it with a looped reference to this file
const _global = typeof globalThis !== 'undefined' ? globalThis.global : (self || window).global;
const process = _global.process;
const Buffer = _global.Buffer;
export {
_global,
process,
Buffer
};

View File

@@ -52,20 +52,20 @@ const {
getValidatedPath,
getOptions,
getDirent
} = __non_webpack_require__('internal/fs/utils') as typeof import('@node/lib/internal/fs/utils');
} = __non_webpack_require__('internal/fs/utils');
const {
assignFunctionName
} = __non_webpack_require__('internal/util') as typeof import('@node/lib/internal/util');
} = __non_webpack_require__('internal/util');
const {
validateBoolean,
validateFunction
} = __non_webpack_require__('internal/validators') as typeof import('@node/lib/internal/validators');
} = __non_webpack_require__('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
const { URL: NodeURL } = __non_webpack_require__('internal/url') as typeof import('@node/lib/internal/url');
// the global URL instance. We need to do instanceof checks against the internal URL impl.
const { URL: NodeURL } = __non_webpack_require__('internal/url');
// Separate asar package's path from full path.
const splitPath = (archivePathOrBuffer: string | Buffer | URL) => {

View File

@@ -29,8 +29,9 @@ Module._load = function (request: string) {
// code with JavaScript.
//
// Note 3: We provide the equivalent extra variables internally through the
// webpack ProvidePlugin in webpack.config.base.js. If you add any extra
// variables to this wrapper please ensure to update that plugin as well.
// esbuild inject shim in build/esbuild/shims/node-globals-shim.js. If you
// add any extra variables to this wrapper please ensure to update that shim
// 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
@@ -65,9 +66,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');
global.module = new Module('electron/js2c/renderer_init');
global.require = makeRequireFunction(global.module) as NodeRequire;
global.require = makeRequireFunction(global.module);
// Set the __filename to the path of html file if it is file: protocol.
if (window.location.protocol === 'file:') {
@@ -152,7 +153,7 @@ 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');
runEntryPointWithESMLoader(async (cascadedLoader: any) => {
// Load the preload scripts.

View File

@@ -5,18 +5,20 @@ import type { ClientRequestConstructorOptions, IncomingMessage } from 'electron/
const { isOnline, resolveHost } = process._linkedBinding('electron_common_net');
export function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
function request (options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void) {
return new ClientRequest(options, callback);
}
export function fetch (input: RequestInfo, init?: RequestInit): Promise<Response> {
function fetch (input: RequestInfo, init?: RequestInit): Promise<Response> {
return fetchWithSession(input, init, undefined, request);
}
exports.resolveHost = resolveHost;
exports.isOnline = isOnline;
Object.defineProperty(exports, 'online', {
get: () => isOnline()
});
module.exports = {
request,
fetch,
resolveHost,
isOnline,
get online () {
return isOnline();
}
};

View File

@@ -36,7 +36,7 @@ parentPort.on('removeListener', (name: string) => {
});
// Finally load entry script.
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');
const mainEntry = pathToFileURL(entryScript);
runEntryPointWithESMLoader(async (cascadedLoader: any) => {

View File

@@ -13,9 +13,9 @@ require('@electron/internal/common/init');
const { hasSwitch, getSwitchValue } = process._linkedBinding('electron_common_command_line');
// 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');
global.module = new Module('electron/js2c/worker_init');
global.require = makeRequireFunction(global.module) as NodeRequire;
global.require = makeRequireFunction(global.module);
// See WebWorkerObserver::WorkerScriptReadyForEvaluation.
if ((globalThis as any).blinkfetch) {

View File

@@ -15,6 +15,7 @@
"@hurdlegroup/robotjs": "^0.12.3",
"@octokit/rest": "^20.1.2",
"@primer/octicons": "^10.0.0",
"@sentry/cli": "1.72.0",
"@types/minimist": "^1.2.5",
"@types/node": "^24.9.0",
"@types/semver": "^7.5.8",
@@ -22,10 +23,12 @@
"@types/temp": "^0.9.4",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.7.0",
"@typescript/native-preview": "^7.0.0-dev.20260324.1",
"@xmldom/xmldom": "^0.8.11",
"buffer": "^6.0.3",
"chalk": "^4.1.0",
"check-for-leaks": "^1.2.1",
"esbuild": "^0.25.0",
"eslint": "^8.57.1",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.32.0",
@@ -42,20 +45,15 @@
"markdownlint-cli2": "^0.18.0",
"minimist": "^1.2.8",
"node-gyp": "^11.4.2",
"null-loader": "^4.0.1",
"pre-flight": "^2.0.0",
"process": "^0.11.10",
"semver": "^7.6.3",
"stream-json": "^1.9.1",
"tap-xunit": "^2.4.1",
"temp": "^0.9.4",
"ts-loader": "^8.0.2",
"ts-node": "6.2.0",
"typescript": "^5.8.3",
"url": "^0.11.4",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"wrapper-webpack-plugin": "^2.2.0",
"yaml": "^2.8.1"
},
"private": true,
@@ -92,8 +90,9 @@
"repl": "node ./script/start.js --interactive",
"start": "node ./script/start.js",
"test": "node ./script/spec-runner.js",
"tsc": "tsc",
"webpack": "webpack"
"tsc": "tsgo",
"tsc-check": "node script/typecheck.js",
"bundle": "node build/esbuild/bundle.js"
},
"license": "MIT",
"author": "Electron Community",
@@ -153,6 +152,9 @@
"spec/fixtures/native-addon/*"
],
"dependenciesMeta": {
"@sentry/cli": {
"built": true
},
"abstract-socket": {
"built": true
}

View File

@@ -119,7 +119,7 @@ build_disable_thin_lto_mac.patch
feat_corner_smoothing_css_rule_and_blink_painting.patch
build_add_public_config_simdutf_config.patch
fix_multiple_scopedpumpmessagesinprivatemodes_instances.patch
revert_code_health_clean_up_stale_macwebcontentsocclusion.patch
fix_handle_embedder_windows_shown_after_webcontentsviewcocoa_attach.patch
feat_add_signals_when_embedder_cleanup_callbacks_run_for.patch
feat_separate_content_settings_callback_for_sync_and_async_clipboard.patch
fix_win32_synchronous_spellcheck.patch

View File

@@ -46,10 +46,10 @@ index 5768066ed65810d14d8ad4b6839c6c632af6bb57..d8d4e66f1c96f630e60001425d16fc4d
# than here in :chrome_dll.
deps += [ "//chrome:packed_resources_integrity_header" ]
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index c2a5a0e0d4af72e4fdbd373d24c102a7f1772a08..d548297578f38e1b1bc0ee3b21edd15920040285 100644
index 98ce26437751543c5c93574bc9561409e214d8a8..853eda4503954de04d50caba63f55fd74f390069 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -7717,9 +7717,12 @@ test("unit_tests") {
@@ -7718,9 +7718,12 @@ test("unit_tests") {
"//chrome/notification_helper",
]
@@ -63,7 +63,7 @@ index c2a5a0e0d4af72e4fdbd373d24c102a7f1772a08..d548297578f38e1b1bc0ee3b21edd159
"//chrome//services/util_win:unit_tests",
"//chrome/app:chrome_dll_resources",
"//chrome/app:win_unit_tests",
@@ -8722,6 +8725,10 @@ test("unit_tests") {
@@ -8723,6 +8726,10 @@ test("unit_tests") {
"../browser/performance_manager/policies/background_tab_loading_policy_unittest.cc",
]
@@ -74,7 +74,7 @@ index c2a5a0e0d4af72e4fdbd373d24c102a7f1772a08..d548297578f38e1b1bc0ee3b21edd159
sources += [
# The importer code is not used on Android.
"../common/importer/firefox_importer_utils_unittest.cc",
@@ -8778,7 +8785,6 @@ test("unit_tests") {
@@ -8779,7 +8786,6 @@ test("unit_tests") {
# TODO(crbug.com/417513088): Maybe merge with the non-android `deps` declaration above?
deps += [
"../browser/screen_ai:screen_ai_install_state",

View File

@@ -9,10 +9,10 @@ potentially prevent a window from being created.
TODO(loc): this patch is currently broken.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 8ae223b565be6088ed16102ef126dbb2a9e553c2..bc76d15ff66eb6535ad86cc306bdf46094603161 100644
index 914b06175825f79c03d34e0bdb1c3749a934bfdb..89f4af078c151adc1d9d471056bacab5dace0833 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -10161,6 +10161,7 @@ void RenderFrameHostImpl::CreateNewWindow(
@@ -10170,6 +10170,7 @@ void RenderFrameHostImpl::CreateNewWindow(
last_committed_origin_, params->window_container_type,
params->target_url, params->referrer.To<Referrer>(),
params->frame_name, params->disposition, *params->features,

View File

@@ -0,0 +1,81 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Attard <sattard@anthropic.com>
Date: Mon, 30 Mar 2026 03:05:40 -0700
Subject: fix: handle embedder windows shown after WebContentsViewCocoa attach
The occlusion checker assumes windows are shown before or at the same
time as a WebContentsViewCocoa is attached. Embedders like Electron
support creating a window hidden, attaching web contents, and showing
later. This breaks three assumptions:
1. updateWebContentsVisibility only checks -[NSWindow isOccluded], which
defaults to NO for never-shown windows, so viewDidMoveToWindow
incorrectly reports kVisible for hidden windows.
2. windowChangedOcclusionState: only responds to checker-originated
notifications, but setOccluded: early-returns when isOccluded doesn't
change. A hidden window's isOccluded is NO and stays NO after show(),
so no checker notification fires on show and the view never updates
to kVisible.
3. performOcclusionStateUpdates iterates orderedWindows and marks
not-yet-shown windows as occluded (their occlusionState lacks the
Visible bit), which stops painting before first show.
Fix by also checking occlusionState in updateWebContentsVisibility,
responding to macOS-originated notifications in
windowChangedOcclusionState:, and skipping non-visible windows in
performOcclusionStateUpdates.
This patch can be removed if the changes are upstreamed to Chromium.
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
index a5570988c3721d9f6bd05c402a7658d3af6f2c2c..54aaffde30c14a27068f89b6de6123abd6ea0660 100644
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
@@ -400,9 +400,11 @@ - (void)performOcclusionStateUpdates {
for (NSWindow* window in windowsFromFrontToBack) {
// The fullscreen transition causes spurious occlusion notifications.
// See https://crbug.com/1081229 . Also, ignore windows that don't have
- // web contentses.
+ // web contentses, and windows that aren't visible (embedders like
+ // Electron may create windows hidden with web contents already attached;
+ // marking these as occluded would stop painting before first show).
if (window == _windowReceivingFullscreenTransitionNotifications ||
- ![window containsWebContentsViewCocoa])
+ ![window isVisible] || ![window containsWebContentsViewCocoa])
continue;
[window setOccluded:[self isWindowOccluded:window
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
index 1ef2c9052262eccdbc40030746a858b7f30ac469..34708d45274f95b5f35cdefad98ad4a1c3c28e1c 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
@@ -477,7 +477,8 @@ - (void)updateWebContentsVisibility {
Visibility visibility = Visibility::kVisible;
if ([self isHiddenOrHasHiddenAncestor] || ![self window])
visibility = Visibility::kHidden;
- else if ([[self window] isOccluded])
+ else if ([[self window] isOccluded] ||
+ !([[self window] occlusionState] & NSWindowOcclusionStateVisible))
visibility = Visibility::kOccluded;
[self updateWebContentsVisibility:visibility];
@@ -521,11 +522,12 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
}
- (void)windowChangedOcclusionState:(NSNotification*)aNotification {
- // Only respond to occlusion notifications sent by the occlusion checker.
- NSDictionary* userInfo = [aNotification userInfo];
- NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className];
- if (userInfo[occlusionCheckerKey] != nil)
- [self updateWebContentsVisibility];
+ // Respond to occlusion notifications from both macOS and the occlusion
+ // checker. Embedders (e.g. Electron) may attach a WebContentsViewCocoa to
+ // a window that has not yet been shown; macOS will notify us when the
+ // window's occlusion state changes, but the occlusion checker will not
+ // because -[NSWindow isOccluded] remains NO before and after show.
+ [self updateWebContentsVisibility];
}
- (void)viewDidMoveToWindow {

View File

@@ -1342,10 +1342,10 @@ index d3ceb8cfbfe1fb63804232c3fd62bafcd90752bd..a82ca8b52a4b8f96ccb013abd5c0bf7c
if (is_ios) {
diff --git a/media/audio/apple/audio_low_latency_input.cc b/media/audio/apple/audio_low_latency_input.cc
index 3a079b0fc34031d062045510fe0e2444792ff942..1be75833d46aaa124e5467904f68e46cce22ead8 100644
index 75178516b53665c82195f795c5e4498c588e51c9..10e453a18813d3078dc4f01ab040acc66bf12bec 100644
--- a/media/audio/apple/audio_low_latency_input.cc
+++ b/media/audio/apple/audio_low_latency_input.cc
@@ -27,6 +27,7 @@
@@ -26,6 +26,7 @@
#include "base/strings/sys_string_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
@@ -1353,7 +1353,7 @@ index 3a079b0fc34031d062045510fe0e2444792ff942..1be75833d46aaa124e5467904f68e46c
#include "media/audio/apple/audio_manager_apple.h"
#include "media/audio/apple/scoped_audio_unit.h"
#include "media/base/audio_bus.h"
@@ -40,19 +41,23 @@
@@ -39,19 +40,23 @@
namespace {
extern "C" {

View File

@@ -68,7 +68,7 @@ index f91857eb0b6ad385721b8224100de26dfdd7dd8d..45e8766fcb8d46d8edc3bf8d21d3f826
: PdfRenderSettings::Mode::POSTSCRIPT_LEVEL3;
}
diff --git a/chrome/browser/printing/print_view_manager_base.cc b/chrome/browser/printing/print_view_manager_base.cc
index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a705b5d994 100644
index aa79c324af2cec50019bca3bccff5d420fb30ffd..0b85598f87673537eccdd0b310e8462e96990d04 100644
--- a/chrome/browser/printing/print_view_manager_base.cc
+++ b/chrome/browser/printing/print_view_manager_base.cc
@@ -80,6 +80,20 @@ namespace printing {
@@ -260,12 +260,18 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
if (prefs && prefs->HasPrefPath(prefs::kPrintRasterizePdfDpi)) {
int value = prefs->GetInteger(prefs::kPrintRasterizePdfDpi);
if (value > 0)
@@ -740,8 +765,22 @@ void PrintViewManagerBase::UpdatePrintSettings(
@@ -740,8 +765,28 @@ void PrintViewManagerBase::UpdatePrintSettings(
}
}
-#if BUILDFLAG(IS_WIN)
- // TODO(crbug.com/40260379): Remove this if the printable areas can be made
+#if BUILDFLAG(ENABLE_OOP_PRINTING)
+ if (ShouldPrintJobOop() && !query_with_ui_client_id().has_value()) {
+ RegisterSystemPrintClient();
+ }
+#endif
+
+ std::unique_ptr<PrinterQuery> query =
+ queue_->CreatePrinterQuery(GetCurrentTargetFrame()->GetGlobalId());
+ auto* query_ptr = query.get();
@@ -285,7 +291,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
// fully available from `PrintBackend::GetPrinterSemanticCapsAndDefaults()`
// for in-browser queries.
if (printer_type == mojom::PrinterType::kLocal) {
@@ -762,8 +801,6 @@ void PrintViewManagerBase::UpdatePrintSettings(
@@ -762,8 +807,6 @@ void PrintViewManagerBase::UpdatePrintSettings(
}
#endif
@@ -294,7 +300,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
}
void PrintViewManagerBase::SetAccessibilityTree(
@@ -779,7 +816,7 @@ void PrintViewManagerBase::SetAccessibilityTree(
@@ -779,7 +822,7 @@ void PrintViewManagerBase::SetAccessibilityTree(
void PrintViewManagerBase::IsPrintingEnabled(
IsPrintingEnabledCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -303,7 +309,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
}
void PrintViewManagerBase::ScriptedPrint(mojom::ScriptedPrintParamsPtr params,
@@ -805,7 +842,7 @@ void PrintViewManagerBase::ScriptedPrint(mojom::ScriptedPrintParamsPtr params,
@@ -805,7 +848,7 @@ void PrintViewManagerBase::ScriptedPrint(mojom::ScriptedPrintParamsPtr params,
return;
}
#endif
@@ -312,7 +318,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
std::optional<enterprise_connectors::ContentAnalysisDelegate::Data>
scanning_data = enterprise_data_protection::GetPrintAnalysisData(
web_contents(), enterprise_data_protection::PrintScanningContext::
@@ -835,11 +872,9 @@ void PrintViewManagerBase::PrintingFailed(int32_t cookie,
@@ -835,11 +878,9 @@ void PrintViewManagerBase::PrintingFailed(int32_t cookie,
// destroyed. In such cases the error notification to the user will
// have already been displayed, and a second message should not be
// shown.
@@ -326,14 +332,23 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
ReleasePrinterQuery();
}
@@ -851,15 +886,24 @@ void PrintViewManagerBase::RemoveTestObserver(TestObserver& observer) {
@@ -851,15 +892,33 @@ void PrintViewManagerBase::RemoveTestObserver(TestObserver& observer) {
test_observers_.RemoveObserver(&observer);
}
+void PrintViewManagerBase::ShowInvalidPrinterSettingsError() {
+ if (!callback_.is_null()) {
+ printing_status_ = PrintStatus::kInvalid;
+ TerminatePrintJob(true);
+ if (print_job_) {
+ TerminatePrintJob(true);
+ } else {
+ // No print job was created, so TerminatePrintJob would bail out
+ // without ever calling ReleasePrintJob (where the callback is
+ // invoked). Fire the callback directly to avoid leaking it until
+ // WebContents destruction.
+ std::move(callback_).Run(false,
+ PrintReasonFromPrintStatus(printing_status_));
+ }
+ }
+}
+
@@ -351,7 +366,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
}
void PrintViewManagerBase::RenderFrameDeleted(
@@ -901,13 +945,14 @@ void PrintViewManagerBase::SystemDialogCancelled() {
@@ -901,13 +960,14 @@ void PrintViewManagerBase::SystemDialogCancelled() {
// System dialog was cancelled. Clean up the print job and notify the
// BackgroundPrintingManager.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -367,7 +382,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
}
void PrintViewManagerBase::OnDocDone(int job_id, PrintedDocument* document) {
@@ -921,18 +966,26 @@ void PrintViewManagerBase::OnJobDone() {
@@ -921,18 +981,26 @@ void PrintViewManagerBase::OnJobDone() {
// Printing is done, we don't need it anymore.
// print_job_->is_job_pending() may still be true, depending on the order
// of object registration.
@@ -396,7 +411,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
TerminatePrintJob(true);
}
@@ -942,7 +995,7 @@ bool PrintViewManagerBase::RenderAllMissingPagesNow() {
@@ -942,7 +1010,7 @@ bool PrintViewManagerBase::RenderAllMissingPagesNow() {
// Is the document already complete?
if (print_job_->document() && print_job_->document()->IsComplete()) {
@@ -405,7 +420,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
return true;
}
@@ -995,7 +1048,10 @@ bool PrintViewManagerBase::SetupNewPrintJob(
@@ -995,7 +1063,10 @@ bool PrintViewManagerBase::SetupNewPrintJob(
// Disconnect the current `print_job_`.
auto weak_this = weak_ptr_factory_.GetWeakPtr();
@@ -417,7 +432,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
if (!weak_this)
return false;
@@ -1015,7 +1071,7 @@ bool PrintViewManagerBase::SetupNewPrintJob(
@@ -1015,7 +1086,7 @@ bool PrintViewManagerBase::SetupNewPrintJob(
#endif
print_job_->AddObserver(*this);
@@ -426,7 +441,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
return true;
}
@@ -1073,7 +1129,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
@@ -1073,7 +1144,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
// Ensure that any residual registration of printing client is released.
// This might be necessary in some abnormal cases, such as the associated
// render process having terminated.
@@ -435,7 +450,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
if (!analyzing_content_) {
UnregisterSystemPrintClient();
}
@@ -1083,6 +1139,11 @@ void PrintViewManagerBase::ReleasePrintJob() {
@@ -1083,6 +1154,11 @@ void PrintViewManagerBase::ReleasePrintJob() {
}
#endif
@@ -447,7 +462,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
if (!print_job_)
return;
@@ -1090,7 +1151,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
@@ -1090,7 +1166,7 @@ void PrintViewManagerBase::ReleasePrintJob() {
// printing_rfh_ should only ever point to a RenderFrameHost with a live
// RenderFrame.
DCHECK(rfh->IsRenderFrameLive());
@@ -456,7 +471,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
}
print_job_->RemoveObserver(*this);
@@ -1132,7 +1193,7 @@ bool PrintViewManagerBase::RunInnerMessageLoop() {
@@ -1132,7 +1208,7 @@ bool PrintViewManagerBase::RunInnerMessageLoop() {
}
bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
@@ -465,7 +480,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
return true;
if (!cookie) {
@@ -1155,7 +1216,7 @@ bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
@@ -1155,7 +1231,7 @@ bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
return false;
}
@@ -474,7 +489,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
// Don't start printing if enterprise checks are being performed to check if
// printing is allowed, or if content analysis is going to take place right
// before starting `print_job_`.
@@ -1286,6 +1347,8 @@ void PrintViewManagerBase::CompleteScriptedPrint(
@@ -1286,6 +1362,8 @@ void PrintViewManagerBase::CompleteScriptedPrint(
auto callback_wrapper = base::BindOnce(
&PrintViewManagerBase::ScriptedPrintReply, weak_ptr_factory_.GetWeakPtr(),
std::move(callback), render_process_host->GetDeprecatedID());
@@ -483,7 +498,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..eb76ee91743236d05c3a70a54d5345a7
std::unique_ptr<PrinterQuery> printer_query =
queue()->PopPrinterQuery(params->cookie);
if (!printer_query)
@@ -1296,10 +1359,10 @@ void PrintViewManagerBase::CompleteScriptedPrint(
@@ -1296,10 +1374,10 @@ void PrintViewManagerBase::CompleteScriptedPrint(
params->expected_pages_count, params->has_selection, params->margin_type,
params->is_scripted, !render_process_host->IsPdf(),
base::BindOnce(&OnDidScriptedPrint, queue_, std::move(printer_query),
@@ -605,7 +620,7 @@ index 2a477e820d9f0126a05f86cd44f02c2189275bad..a2e9442ff9f5acf8e301f457b1806251
#if BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/printing/printer_query_oop.cc b/chrome/browser/printing/printer_query_oop.cc
index dc2a15ab4d784b0b6c85b84a30c3c08a17ed8e3d..8facb5981cc421cad6bce71dfa8985b0a3270405 100644
index dc2a15ab4d784b0b6c85b84a30c3c08a17ed8e3d..e197026e8a7f132c1bf90a0f5f1eabb4f5f064ee 100644
--- a/chrome/browser/printing/printer_query_oop.cc
+++ b/chrome/browser/printing/printer_query_oop.cc
@@ -126,7 +126,7 @@ void PrinterQueryOop::OnDidAskUserForSettings(
@@ -617,6 +632,28 @@ index dc2a15ab4d784b0b6c85b84a30c3c08a17ed8e3d..8facb5981cc421cad6bce71dfa8985b0
// Want the same PrintBackend service as the query so that we use the same
// device context.
print_document_client_id_ =
@@ -189,6 +189,21 @@ void PrinterQueryOop::GetSettingsWithUI(uint32_t document_page_count,
// browser process.
// - Other platforms don't have a system print UI or do not use OOP
// printing, so this does not matter.
+
+ // Apply cached settings to the local printing context so that the in-browser
+ // system print dialog is prefilled with user-specified options (e.g. copies,
+ // collate, duplex). OOP UpdatePrintSettings only applies settings to the
+ // remote service context, not the local one used by the native dialog.
+ if (settings().dpi()) {
+ printing_context()->SetPrintSettings(settings());
+ printing_context()->UpdatePrinterSettings(PrintingContext::PrinterSettings{
+#if BUILDFLAG(IS_MAC)
+ .external_preview = false,
+#endif
+ .show_system_dialog = false,
+ });
+ }
+
PrinterQuery::GetSettingsWithUI(
document_page_count, has_selection, is_scripted,
base::BindOnce(&PrinterQueryOop::OnDidAskUserForSettings,
diff --git a/components/printing/browser/print_manager.cc b/components/printing/browser/print_manager.cc
index 21c81377d32ae8d4185598a7eba88ed1d2063ef0..0767f4e9369e926b1cea99178c1a1975941f1765 100644
--- a/components/printing/browser/print_manager.cc
@@ -666,7 +703,7 @@ index ac2f719be566020d9f41364560c12e6d6d0fe3d8..16d758a6936f66148a196761cfb875f6
PrintingFailed(int32 cookie, PrintFailureReason reason);
diff --git a/components/printing/renderer/print_render_frame_helper.cc b/components/printing/renderer/print_render_frame_helper.cc
index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..1320f3b10b07b2cee90f39f406604176c7575796 100644
index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..dd83b6cfb6e3f916e60f50402014cd931a4d8850 100644
--- a/components/printing/renderer/print_render_frame_helper.cc
+++ b/components/printing/renderer/print_render_frame_helper.cc
@@ -54,6 +54,7 @@
@@ -790,7 +827,7 @@ index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..1320f3b10b07b2cee90f39f406604176
// Check if `this` is still valid.
if (!self)
return;
@@ -2394,29 +2415,43 @@ void PrintRenderFrameHelper::IPCProcessed() {
@@ -2394,29 +2415,47 @@ void PrintRenderFrameHelper::IPCProcessed() {
}
bool PrintRenderFrameHelper::InitPrintSettings(blink::WebLocalFrame* frame,
@@ -826,8 +863,12 @@ index 60b5e83a8bc1ed07970be4cdfdc19962698bd754..1320f3b10b07b2cee90f39f406604176
- : mojom::PrintScalingOption::kSourceSize;
- RecordDebugEvent(settings.params->printed_doc_type ==
+ bool silent = new_settings.FindBool("silent").value_or(false);
+ if (silent) {
+ settings->params->print_scaling_option = mojom::PrintScalingOption::kFitToPrintableArea;
+ int margins_type = new_settings.FindInt(kSettingMarginsType)
+ .value_or(static_cast<int>(mojom::MarginType::kDefaultMargins));
+ if (silent &&
+ margins_type == static_cast<int>(mojom::MarginType::kDefaultMargins)) {
+ settings->params->print_scaling_option =
+ mojom::PrintScalingOption::kFitToPrintableArea;
+ } else {
+ settings->params->print_scaling_option =
+ center_on_paper ? mojom::PrintScalingOption::kCenterShrinkToFitPaper

View File

@@ -1,279 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: David Sanders <dsanders11@ucsbalum.com>
Date: Wed, 8 Jan 2025 23:53:27 -0800
Subject: Revert "Code Health: Clean up stale MacWebContentsOcclusion"
Chrome has removed this WebContentsOcclusion feature flag upstream,
which is now causing our visibility tests to break. This patch
restores the legacy occlusion behavior to ensure the roll can continue
while we debug the issue.
This patch can be removed when the root cause because the visibility
specs failing on MacOS only is debugged and fixed. It should be removed
before Electron 35's stable date.
Refs: https://chromium-review.googlesource.com/c/chromium/src/+/6078344
This partially (leaves the removal of the feature flag) reverts
ef865130abd5539e7bce12308659b19980368f12.
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
index 04c7635cc093d9d676869383670a8f2199f14ac6..52d76e804e47ab0b56016d26262d6d67cbc00875 100644
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h
@@ -11,6 +11,8 @@
#include "base/metrics/field_trial_params.h"
#import "content/app_shim_remote_cocoa/web_contents_view_cocoa.h"
+extern CONTENT_EXPORT const base::FeatureParam<bool>
+ kEnhancedWindowOcclusionDetection;
extern CONTENT_EXPORT const base::FeatureParam<bool>
kDisplaySleepAndAppHideDetection;
diff --git a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
index a5570988c3721d9f6bd05c402a7658d3af6f2c2c..0a2dba6aa2d48bc39d2a55c8b4d6606744c10ca7 100644
--- a/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
+++ b/content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.mm
@@ -14,9 +14,16 @@
#include "base/mac/mac_util.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
+#include "content/common/features.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
+using features::kMacWebContentsOcclusion;
+
+// Experiment features.
+const base::FeatureParam<bool> kEnhancedWindowOcclusionDetection{
+ &kMacWebContentsOcclusion, "EnhancedWindowOcclusionDetection", false};
+
namespace {
NSString* const kWindowDidChangePositionInWindowList =
@@ -125,7 +132,8 @@ - (void)dealloc {
- (BOOL)isManualOcclusionDetectionEnabled {
return [WebContentsOcclusionCheckerMac
- manualOcclusionDetectionSupportedForCurrentMacOSVersion];
+ manualOcclusionDetectionSupportedForCurrentMacOSVersion] &&
+ kEnhancedWindowOcclusionDetection.Get();
}
// Alternative implementation of orderWindow:relativeTo:. Replaces
diff --git a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
index 1ef2c9052262eccdbc40030746a858b7f30ac469..c7101b0d71826b05f61bfe0e74429d922769e792 100644
--- a/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/web_contents_view_cocoa.mm
@@ -15,6 +15,7 @@
#import "content/app_shim_remote_cocoa/web_drag_source_mac.h"
#import "content/browser/web_contents/web_contents_view_mac.h"
#import "content/browser/web_contents/web_drag_dest_mac.h"
+#include "content/common/features.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "ui/base/clipboard/clipboard_constants.h"
@@ -27,6 +28,7 @@
#include "ui/resources/grit/ui_resources.h"
using content::DropData;
+using features::kMacWebContentsOcclusion;
using remote_cocoa::mojom::DraggingInfo;
using remote_cocoa::mojom::SelectionDirection;
@@ -122,12 +124,15 @@ @implementation WebContentsViewCocoa {
WebDragSource* __strong _dragSource;
NSDragOperation _dragOperation;
+ BOOL _inFullScreenTransition;
BOOL _willSetWebContentsOccludedAfterDelay;
}
+ (void)initialize {
- // Create the WebContentsOcclusionCheckerMac shared instance.
- [WebContentsOcclusionCheckerMac sharedInstance];
+ if (base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ // Create the WebContentsOcclusionCheckerMac shared instance.
+ [WebContentsOcclusionCheckerMac sharedInstance];
+ }
}
- (instancetype)initWithViewsHostableView:(ui::ViewsHostableView*)v {
@@ -438,6 +443,7 @@ - (void)updateWebContentsVisibility:
(remote_cocoa::mojom::Visibility)visibility {
using remote_cocoa::mojom::Visibility;
+ DCHECK(base::FeatureList::IsEnabled(kMacWebContentsOcclusion));
if (!_host)
return;
@@ -483,6 +489,20 @@ - (void)updateWebContentsVisibility {
[self updateWebContentsVisibility:visibility];
}
+- (void)legacyUpdateWebContentsVisibility {
+ using remote_cocoa::mojom::Visibility;
+ if (!_host || _inFullScreenTransition)
+ return;
+ Visibility visibility = Visibility::kVisible;
+ if ([self isHiddenOrHasHiddenAncestor] || ![self window])
+ visibility = Visibility::kHidden;
+ else if ([[self window] occlusionState] & NSWindowOcclusionStateVisible)
+ visibility = Visibility::kVisible;
+ else
+ visibility = Visibility::kOccluded;
+ _host->OnWindowVisibilityChanged(visibility);
+}
+
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
// Subviews do not participate in auto layout unless the the size this view
// changes. This allows RenderWidgetHostViewMac::SetBounds(..) to select a
@@ -505,11 +525,39 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
NSWindow* oldWindow = [self window];
+ if (base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ if (oldWindow) {
+ [notificationCenter
+ removeObserver:self
+ name:NSWindowDidChangeOcclusionStateNotification
+ object:oldWindow];
+ }
+
+ if (newWindow) {
+ [notificationCenter
+ addObserver:self
+ selector:@selector(windowChangedOcclusionState:)
+ name:NSWindowDidChangeOcclusionStateNotification
+ object:newWindow];
+ }
+
+ return;
+ }
+
+ _inFullScreenTransition = NO;
if (oldWindow) {
- [notificationCenter
- removeObserver:self
- name:NSWindowDidChangeOcclusionStateNotification
- object:oldWindow];
+ NSArray* notificationsToRemove = @[
+ NSWindowDidChangeOcclusionStateNotification,
+ NSWindowWillEnterFullScreenNotification,
+ NSWindowDidEnterFullScreenNotification,
+ NSWindowWillExitFullScreenNotification,
+ NSWindowDidExitFullScreenNotification
+ ];
+ for (NSString* notificationName in notificationsToRemove) {
+ [notificationCenter removeObserver:self
+ name:notificationName
+ object:oldWindow];
+ }
}
if (newWindow) {
@@ -517,26 +565,66 @@ - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
selector:@selector(windowChangedOcclusionState:)
name:NSWindowDidChangeOcclusionStateNotification
object:newWindow];
+ // The fullscreen transition causes spurious occlusion notifications.
+ // See https://crbug.com/1081229
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionStarted:)
+ name:NSWindowWillEnterFullScreenNotification
+ object:newWindow];
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionComplete:)
+ name:NSWindowDidEnterFullScreenNotification
+ object:newWindow];
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionStarted:)
+ name:NSWindowWillExitFullScreenNotification
+ object:newWindow];
+ [notificationCenter addObserver:self
+ selector:@selector(fullscreenTransitionComplete:)
+ name:NSWindowDidExitFullScreenNotification
+ object:newWindow];
}
}
- (void)windowChangedOcclusionState:(NSNotification*)aNotification {
- // Only respond to occlusion notifications sent by the occlusion checker.
- NSDictionary* userInfo = [aNotification userInfo];
- NSString* occlusionCheckerKey = [WebContentsOcclusionCheckerMac className];
- if (userInfo[occlusionCheckerKey] != nil)
- [self updateWebContentsVisibility];
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+}
+
+- (void)fullscreenTransitionStarted:(NSNotification*)notification {
+ _inFullScreenTransition = YES;
+}
+
+- (void)fullscreenTransitionComplete:(NSNotification*)notification {
+ _inFullScreenTransition = NO;
}
- (void)viewDidMoveToWindow {
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+
[self updateWebContentsVisibility];
}
- (void)viewDidHide {
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+
[self updateWebContentsVisibility];
}
- (void)viewDidUnhide {
+ if (!base::FeatureList::IsEnabled(kMacWebContentsOcclusion)) {
+ [self legacyUpdateWebContentsVisibility];
+ return;
+ }
+
[self updateWebContentsVisibility];
}
diff --git a/content/common/features.cc b/content/common/features.cc
index 1272f3f588e00162a2b8665b23a4d23ef06e8ede..ff1a27d1928e4d1af10b46d570a13519a42f62d8 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -395,6 +395,14 @@ BASE_FEATURE(kInterestGroupUpdateIfOlderThan, base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kIOSurfaceCapturer, base::FEATURE_ENABLED_BY_DEFAULT);
#endif
+// Feature that controls whether WebContentsOcclusionChecker should handle
+// occlusion notifications.
+#if BUILDFLAG(IS_MAC)
+BASE_FEATURE(kMacWebContentsOcclusion,
+ "MacWebContentsOcclusion",
+ base::FEATURE_ENABLED_BY_DEFAULT);
+#endif
+
// When enabled, child process will not terminate itself when IPC is reset.
BASE_FEATURE(kKeepChildProcessAfterIPCReset, base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/content/common/features.h b/content/common/features.h
index dcfbeeab7b80b687ada74f6826107e26901e7cf1..980a2df8bb93c4cf569384fad1ed0982fc77ef68 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -150,6 +150,9 @@ CONTENT_EXPORT BASE_DECLARE_FEATURE(kInterestGroupUpdateIfOlderThan);
#if BUILDFLAG(IS_MAC)
CONTENT_EXPORT BASE_DECLARE_FEATURE(kIOSurfaceCapturer);
#endif
+#if BUILDFLAG(IS_MAC)
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kMacWebContentsOcclusion);
+#endif
CONTENT_EXPORT BASE_DECLARE_FEATURE(kKeepChildProcessAfterIPCReset);
CONTENT_EXPORT BASE_DECLARE_FEATURE(kLocalNetworkAccessForWorkers);

View File

@@ -10,10 +10,10 @@ on Windows. We should refactor our code so that this patch isn't
necessary.
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 1f99581fba292af11bf981154fe3fb5453019166..8560efae20c61e43cea64a8fa4e2591ed5e8402b 100644
index 3228d6570c30523ef32a0436c22e407c0f05a816..3a02e612e39208fda0ec42a4e37246ce1ad8ca20 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -22379,6 +22379,21 @@
@@ -22361,6 +22361,21 @@
]
}
],

View File

@@ -15,10 +15,10 @@ Note that we also need to manually update embedder's
`api::WebContents::IsFullscreenForTabOrPending` value.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index bc76d15ff66eb6535ad86cc306bdf46094603161..7572473a01f59cdd983a1783cc5ed2c952435d6e 100644
index 89f4af078c151adc1d9d471056bacab5dace0833..266126746bbb995c3aac7d4ba3604e98042c4c40 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9207,6 +9207,17 @@ void RenderFrameHostImpl::EnterFullscreen(
@@ -9216,6 +9216,17 @@ void RenderFrameHostImpl::EnterFullscreen(
}
}

View File

@@ -2,6 +2,8 @@
set -euo pipefail
export XDG_SESSION_TYPE=wayland
# On a Wayland desktop, the tests will use your active display and compositor.
# To run headlessly in weston like in CI, set WAYLAND_DISPLAY=wayland-99.
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-99}"
if [[ -z "${XDG_RUNTIME_DIR:-}" ]]; then

View File

@@ -1,6 +1,5 @@
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const rootPath = path.resolve(__dirname, '..');
@@ -16,52 +15,24 @@ const allDocs = fs.readdirSync(path.resolve(__dirname, '../docs/api'))
const typingFiles = fs.readdirSync(path.resolve(__dirname, '../typings')).map(child => `typings/${child}`);
const main = async () => {
const webpackTargets = [
{
name: 'sandbox_bundle_deps',
config: 'webpack.config.sandboxed_renderer.js'
},
{
name: 'isolated_bundle_deps',
config: 'webpack.config.isolated_renderer.js'
},
{
name: 'browser_bundle_deps',
config: 'webpack.config.browser.js'
},
{
name: 'renderer_bundle_deps',
config: 'webpack.config.renderer.js'
},
{
name: 'worker_bundle_deps',
config: 'webpack.config.worker.js'
},
{
name: 'node_bundle_deps',
config: 'webpack.config.node.js'
},
{
name: 'utility_bundle_deps',
config: 'webpack.config.utility.js'
},
{
name: 'preload_realm_bundle_deps',
config: 'webpack.config.preload_realm.js'
}
const bundleTargets = [
{ name: 'sandbox_bundle_deps', config: 'sandboxed_renderer.js' },
{ name: 'isolated_bundle_deps', config: 'isolated_renderer.js' },
{ name: 'browser_bundle_deps', config: 'browser.js' },
{ name: 'renderer_bundle_deps', config: 'renderer.js' },
{ name: 'worker_bundle_deps', config: 'worker.js' },
{ name: 'node_bundle_deps', config: 'node.js' },
{ name: 'utility_bundle_deps', config: 'utility.js' },
{ name: 'preload_realm_bundle_deps', config: 'preload_realm.js' }
];
const webpackTargetsWithDeps = await Promise.all(webpackTargets.map(async webpackTarget => {
const tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-filenames-'));
const targetsWithDeps = await Promise.all(bundleTargets.map(async bundleTarget => {
const child = cp.spawn('node', [
'./node_modules/webpack-cli/bin/cli.js',
'--config', `./build/webpack/${webpackTarget.config}`,
'--stats', 'errors-only',
'--output-path', tmpDir,
'--output-filename', `${webpackTarget.name}.measure.js`,
'--env', 'PRINT_WEBPACK_GRAPH'
'./build/esbuild/bundle.js',
'--config', `./build/esbuild/configs/${bundleTarget.config}`,
'--print-graph'
], {
cwd: path.resolve(__dirname, '..')
cwd: rootPath
});
let output = '';
child.stdout.on('data', chunk => {
@@ -71,32 +42,33 @@ const main = async () => {
await new Promise<void>((resolve, reject) => child.on('exit', (code) => {
if (code !== 0) {
console.error(output);
return reject(new Error(`Failed to list webpack dependencies for entry: ${webpackTarget.name}`));
return reject(new Error(`Failed to list bundle dependencies for entry: ${bundleTarget.name}`));
}
resolve();
}));
const webpackTargetWithDeps = {
...webpackTarget,
return {
...bundleTarget,
dependencies: (JSON.parse(output) as string[])
// Remove whitespace
.map(line => line.trim())
// Get the relative path
.map(line => path.relative(rootPath, line).replace(/\\/g, '/'))
// Only care about files in //electron
.map(line => path.relative(rootPath, path.resolve(rootPath, line)).replace(/\\/g, '/'))
.filter(line => !line.startsWith('..'))
// Only care about our own files
.filter(line => !line.startsWith('node_modules'))
// All webpack builds depend on the tsconfig and package json files
.concat(['tsconfig.json', 'tsconfig.electron.json', 'package.json', ...typingFiles])
// Make the generated list easier to read
.sort()
};
await fs.promises.rm(tmpDir, { force: true, recursive: true });
return webpackTargetWithDeps;
}));
// The typecheck step runs tsgo over tsconfig.electron.json which includes
// the whole lib/ + typings/ trees. For GN dependency tracking, list the
// union of every bundle's dependency set (lib files) plus typings, and
// dedupe.
const typecheckSources = Array.from(new Set([
...targetsWithDeps.flatMap(t => t.dependencies),
...typingFiles
])).sort();
fs.writeFileSync(
gniPath,
`# THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT BY HAND
@@ -105,7 +77,11 @@ auto_filenames = {
${allDocs.map(doc => ` "${doc}",`).join('\n')}
]
${webpackTargetsWithDeps.map(target => ` ${target.name} = [
typecheck_sources = [
${typecheckSources.map(src => ` "${src}",`).join('\n')}
]
${targetsWithDeps.map(target => ` ${target.name} = [
${target.dependencies.map(dep => ` "${dep}",`).join('\n')}
]`).join('\n\n')}
}

View File

@@ -1,21 +0,0 @@
import os
import subprocess
import sys
def npx(*npx_args):
npx_env = os.environ.copy()
npx_env['npm_config_yes'] = 'true'
call_args = [__get_executable_name()] + list(npx_args)
subprocess.check_call(call_args, env=npx_env)
def __get_executable_name():
executable = 'npx'
if sys.platform == 'win32':
executable += '.cmd'
return executable
if __name__ == '__main__':
npx(*sys.argv[1:])

View File

@@ -6,7 +6,7 @@ const path = require('node:path');
const BASE = path.resolve(__dirname, '../..');
const NAN_DIR = path.resolve(BASE, 'third_party', 'nan');
const NPX_CMD = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const NODE_GYP_BIN = path.join(NAN_DIR, 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
const utils = require('./lib/utils');
const { YARN_SCRIPT_PATH } = require('./yarn');
@@ -19,14 +19,6 @@ const args = minimist(process.argv.slice(2), {
string: ['only']
});
const getNodeGypVersion = () => {
const nanPackageJSONPath = path.join(NAN_DIR, 'package.json');
const nanPackageJSON = JSON.parse(fs.readFileSync(nanPackageJSONPath, 'utf8'));
const { devDependencies } = nanPackageJSON;
const nodeGypVersion = devDependencies['node-gyp'];
return nodeGypVersion || 'latest';
};
async function main () {
const outDir = utils.getOutDir({ shouldLog: true });
const nodeDir = path.resolve(BASE, 'out', outDir, 'gen', 'node_headers');
@@ -34,8 +26,7 @@ async function main () {
npm_config_msvs_version: '2022',
...process.env,
npm_config_nodedir: nodeDir,
npm_config_arch: process.env.NPM_CONFIG_ARCH,
npm_config_yes: 'true'
npm_config_arch: process.env.NPM_CONFIG_ARCH
};
const clangDir = path.resolve(BASE, 'third_party', 'llvm-build', 'Release+Asserts', 'bin');
@@ -105,30 +96,26 @@ async function main () {
env.LDFLAGS = ldflags;
}
const nodeGypVersion = getNodeGypVersion();
const { status: buildStatus, signal } = cp.spawnSync(NPX_CMD, [`node-gyp@${nodeGypVersion}`, 'rebuild', '--verbose', '--directory', 'test', '-j', 'max'], {
const { status: installStatus, signal: installSignal } = cp.spawnSync(process.execPath, [YARN_SCRIPT_PATH, 'install'], {
env,
cwd: NAN_DIR,
stdio: 'inherit',
shell: process.platform === 'win32'
stdio: 'inherit'
});
if (installStatus !== 0 || installSignal != null) {
console.error('Failed to install nan node_modules');
return process.exit(installStatus !== 0 ? installStatus : installSignal);
}
const { status: buildStatus, signal } = cp.spawnSync(process.execPath, [NODE_GYP_BIN, 'rebuild', '--verbose', '--directory', 'test', '-j', 'max'], {
env,
cwd: NAN_DIR,
stdio: 'inherit'
});
if (buildStatus !== 0 || signal != null) {
console.error('Failed to build nan test modules');
return process.exit(buildStatus !== 0 ? buildStatus : signal);
}
const { status: installStatus, signal: installSignal } = cp.spawnSync(process.execPath, [YARN_SCRIPT_PATH, 'install'], {
env,
cwd: NAN_DIR,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (installStatus !== 0 || installSignal != null) {
console.error('Failed to install nan node_modules');
return process.exit(installStatus !== 0 ? installStatus : installSignal);
}
const onlyTests = args.only?.split(',');
const DISABLED_TESTS = new Set([

View File

@@ -212,10 +212,15 @@ new Promise<string>((resolve, reject) => {
});
})
.then((tarballPath) => {
// TODO: Remove NPX
const existingVersionJSON = childProcess.execSync(`npx npm@7 view ${rootPackageJson.name}@${currentElectronVersion} --json`).toString('utf-8');
// It's possible this is a re-run and we already have published the package, if not we just publish like normal
if (!existingVersionJSON) {
let versionAlreadyPublished = false;
try {
childProcess.execSync(`npm view ${rootPackageJson.name}@${currentElectronVersion} --json`, { stdio: 'pipe' });
versionAlreadyPublished = true;
} catch (e: any) {
if (!e.stdout?.toString().includes('E404')) throw e;
}
if (!versionAlreadyPublished) {
childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag} --otp=${process.env.ELECTRON_NPM_OTP}`);
}
})

View File

@@ -1,6 +1,6 @@
import { Octokit } from '@octokit/rest';
import * as assert from 'node:assert';
import { strict as assert } from 'node:assert';
import { createGitHubTokenStrategy } from './github-token';
import { ELECTRON_ORG, ELECTRON_REPO } from './types';

View File

@@ -31,9 +31,9 @@ PDB_LIST = [
PDB_LIST += glob.glob(os.path.join(RELEASE_DIR, '*.dll.pdb'))
NPX_CMD = "npx"
SENTRY_CLI = os.path.join(ELECTRON_DIR, 'node_modules', '.bin', 'sentry-cli')
if sys.platform == "win32":
NPX_CMD += ".cmd"
SENTRY_CLI += ".cmd"
def main():
@@ -48,11 +48,8 @@ def main():
for symbol_file in files:
print("Generating Sentry src bundle for: " + symbol_file)
npx_env = os.environ.copy()
npx_env['npm_config_yes'] = 'true'
subprocess.check_output([
NPX_CMD, '@sentry/cli@1.62.0', 'difutil', 'bundle-sources',
symbol_file], env=npx_env)
SENTRY_CLI, 'difutil', 'bundle-sources', symbol_file])
files += glob.glob(SYMBOLS_DIR + '/*/*/*.src.zip')

View File

@@ -1,6 +1,6 @@
import * as minimist from 'minimist';
import * as streamChain from 'stream-chain';
import * as streamJson from 'stream-json';
import minimist = require('minimist');
import streamChain = require('stream-chain');
import streamJson = require('stream-json');
import { ignore as streamJsonIgnore } from 'stream-json/filters/Ignore';
import { streamArray as streamJsonStreamArray } from 'stream-json/streamers/StreamArray';

58
script/typecheck.js Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env node
// Runs `tsgo --noEmit -p <tsconfig>` and writes a stamp file on success,
// so GN can track typecheck results as a build output.
//
// Usage: node script/typecheck.js --tsconfig <path> --stamp <path>
'use strict';
const cp = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
function parseArgs (argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith('--')) {
const next = argv[i + 1];
if (next === undefined || next.startsWith('--')) {
out[a.slice(2)] = true;
} else {
out[a.slice(2)] = next;
i++;
}
}
}
return out;
}
const args = parseArgs(process.argv.slice(2));
if (!args.tsconfig || !args.stamp) {
console.error('Usage: typecheck.js --tsconfig <path> --stamp <path>');
process.exit(1);
}
const electronRoot = path.resolve(__dirname, '..');
// Resolve tsgo's bin entry directly from the package's `bin` map and run it
// via the current Node executable. We can't `require.resolve` the bin path
// (the package's `exports` field doesn't expose it) and we can't spawn
// `node_modules/.bin/tsgo` directly on Windows (it's a `.cmd` shim).
const tsgoPkgPath = require.resolve('@typescript/native-preview/package.json', {
paths: [electronRoot]
});
const tsgoPkg = JSON.parse(fs.readFileSync(tsgoPkgPath, 'utf8'));
const tsgoEntry = path.resolve(path.dirname(tsgoPkgPath), tsgoPkg.bin.tsgo);
const child = cp.spawnSync(
process.execPath,
[tsgoEntry, '--noEmit', '-p', path.resolve(args.tsconfig)],
{ cwd: electronRoot, stdio: 'inherit' }
);
if (child.status !== 0) {
process.exit(child.status || 1);
}
fs.mkdirSync(path.dirname(args.stamp), { recursive: true });
fs.writeFileSync(args.stamp, '');

View File

@@ -1,4 +1,5 @@
spec/parse-features-string-spec.ts
spec/types-spec.ts
spec/version-bump-spec.ts
spec/api-app-spec.ts
spec/api-app-spec.ts
spec/api-browser-window-spec.ts

View File

@@ -264,11 +264,6 @@ void BrowserWindow::BlurWebView() {
web_contents()->GetRenderViewHost()->GetWidget()->Blur();
}
bool BrowserWindow::IsWebViewFocused() {
auto* host_view = web_contents()->GetRenderViewHost()->GetWidget()->GetView();
return host_view && host_view->HasFocus();
}
v8::Local<v8::Value> BrowserWindow::GetWebContents(v8::Isolate* isolate) {
if (web_contents_.IsEmpty())
return v8::Null(isolate);
@@ -332,7 +327,6 @@ void BrowserWindow::BuildPrototype(v8::Isolate* isolate,
gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("focusOnWebView", &BrowserWindow::FocusOnWebView)
.SetMethod("blurWebView", &BrowserWindow::BlurWebView)
.SetMethod("isWebViewFocused", &BrowserWindow::IsWebViewFocused)
.SetProperty("webContents", &BrowserWindow::GetWebContents);
}

View File

@@ -73,7 +73,6 @@ class BrowserWindow : public BaseWindow,
// BrowserWindow APIs.
void FocusOnWebView();
void BlurWebView();
bool IsWebViewFocused();
v8::Local<v8::Value> GetWebContents(v8::Isolate* isolate);
private:

View File

@@ -151,7 +151,10 @@ void OnTraceBufferUsageAvailable(
gin_helper::Promise<gin_helper::Dictionary> promise,
float percent_full,
size_t approximate_count) {
auto dict = gin_helper::Dictionary::CreateEmpty(promise.isolate());
v8::Isolate* isolate = promise.isolate();
v8::HandleScope handle_scope(isolate);
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
dict.Set("percentage", percent_full);
dict.Set("value", approximate_count);

View File

@@ -220,12 +220,6 @@ download::DownloadItem::DownloadState DownloadItem::GetState() const {
return download_item_->GetState();
}
bool DownloadItem::IsDone() const {
if (!CheckAlive())
return false;
return download_item_->IsDone();
}
void DownloadItem::SetSavePath(const base::FilePath& path) {
save_path_ = path;
}
@@ -289,7 +283,6 @@ gin::ObjectTemplateBuilder DownloadItem::GetObjectTemplateBuilder(
.SetMethod("getURL", &DownloadItem::GetURL)
.SetMethod("getURLChain", &DownloadItem::GetURLChain)
.SetMethod("getState", &DownloadItem::GetState)
.SetMethod("isDone", &DownloadItem::IsDone)
.SetMethod("setSavePath", &DownloadItem::SetSavePath)
.SetMethod("getSavePath", &DownloadItem::GetSavePath)
.SetProperty("savePath", &DownloadItem::GetSavePath,

View File

@@ -78,7 +78,6 @@ class DownloadItem final : public gin_helper::DeprecatedWrappable<DownloadItem>,
const GURL& GetURL() const;
v8::Local<v8::Value> GetURLChain() const;
download::DownloadItem::DownloadState GetState() const;
bool IsDone() const;
void SetSaveDialogOptions(const file_dialog::DialogSettings& options);
std::string GetLastModifiedTime() const;
std::string GetETag() const;

View File

@@ -264,22 +264,6 @@ int Menu::GetItemCount() const {
return model_->GetItemCount();
}
int Menu::GetCommandIdAt(int index) const {
return model_->GetCommandIdAt(index);
}
std::u16string Menu::GetLabelAt(int index) const {
return model_->GetLabelAt(index);
}
std::u16string Menu::GetSublabelAt(int index) const {
return model_->GetSecondaryLabelAt(index);
}
std::u16string Menu::GetToolTipAt(int index) const {
return model_->GetToolTipAt(index);
}
std::u16string Menu::GetAcceleratorTextAtForTesting(int index) const {
ui::Accelerator accelerator;
model_->GetAcceleratorAtWithParams(index, true, &accelerator);
@@ -298,10 +282,6 @@ bool Menu::IsVisibleAt(int index) const {
return model_->IsVisibleAt(index);
}
bool Menu::WorksWhenHiddenAt(int index) const {
return model_->WorksWhenHiddenAt(index);
}
void Menu::OnMenuWillClose() {
keep_alive_.Clear();
Emit("menu-will-close");
@@ -325,15 +305,9 @@ void Menu::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("setRole", &Menu::SetRole)
.SetMethod("setCustomType", &Menu::SetCustomType)
.SetMethod("clear", &Menu::Clear)
.SetMethod("getIndexOfCommandId", &Menu::GetIndexOfCommandId)
.SetMethod("getItemCount", &Menu::GetItemCount)
.SetMethod("getCommandIdAt", &Menu::GetCommandIdAt)
.SetMethod("getLabelAt", &Menu::GetLabelAt)
.SetMethod("getSublabelAt", &Menu::GetSublabelAt)
.SetMethod("getToolTipAt", &Menu::GetToolTipAt)
.SetMethod("isItemCheckedAt", &Menu::IsItemCheckedAt)
.SetMethod("isEnabledAt", &Menu::IsEnabledAt)
.SetMethod("worksWhenHiddenAt", &Menu::WorksWhenHiddenAt)
.SetMethod("isVisibleAt", &Menu::IsVisibleAt)
.SetMethod("popupAt", &Menu::PopupAt)
.SetMethod("closePopupAt", &Menu::ClosePopupAt)

View File

@@ -131,14 +131,9 @@ class Menu : public gin::Wrappable<Menu>,
void Clear();
int GetIndexOfCommandId(int command_id) const;
int GetItemCount() const;
int GetCommandIdAt(int index) const;
std::u16string GetLabelAt(int index) const;
std::u16string GetSublabelAt(int index) const;
std::u16string GetToolTipAt(int index) const;
bool IsItemCheckedAt(int index) const;
bool IsEnabledAt(int index) const;
bool IsVisibleAt(int index) const;
bool WorksWhenHiddenAt(int index) const;
gin_helper::SelfKeepAlive<Menu> keep_alive_{this};
};

View File

@@ -163,8 +163,6 @@ class ServiceWorkerMain final
raw_ptr<content::ServiceWorkerContext> service_worker_context_;
mojo::AssociatedRemote<mojom::ElectronRenderer> remote_;
std::unique_ptr<gin_helper::Promise<void>> start_worker_promise_;
};
} // namespace electron::api

View File

@@ -2450,16 +2450,9 @@ int32_t WebContents::GetProcessID() const {
}
base::ProcessId WebContents::GetOSProcessID() const {
base::ProcessHandle process_handle = web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetProcess()
.Handle();
return base::GetProcId(process_handle);
}
bool WebContents::Equal(const WebContents* web_contents) const {
return ID() == web_contents->ID();
const auto& process =
web_contents()->GetPrimaryMainFrame()->GetProcess()->GetProcess();
return process.IsValid() ? process.Pid() : base::kNullProcessId;
}
GURL WebContents::GetURL() const {
@@ -4588,7 +4581,6 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
&WebContents::SetBackgroundThrottling)
.SetMethod("getProcessId", &WebContents::GetProcessID)
.SetMethod("getOSProcessId", &WebContents::GetOSProcessID)
.SetMethod("equal", &WebContents::Equal)
.SetMethod("_loadURL", &WebContents::LoadURL)
.SetMethod("reload", &WebContents::Reload)
.SetMethod("reloadIgnoringCache", &WebContents::ReloadIgnoringCache)
@@ -4605,7 +4597,6 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("_goForward", &WebContents::GoForward)
.SetMethod("_canGoToOffset", &WebContents::CanGoToOffset)
.SetMethod("_goToOffset", &WebContents::GoToOffset)
.SetMethod("canGoToIndex", &WebContents::CanGoToIndex)
.SetMethod("_goToIndex", &WebContents::GoToIndex)
.SetMethod("_getActiveIndex", &WebContents::GetActiveIndex)
.SetMethod("_getNavigationEntryAtIndex",

View File

@@ -195,7 +195,6 @@ class WebContents final : public ExclusiveAccessContext,
int32_t GetProcessID() const;
base::ProcessId GetOSProcessID() const;
[[nodiscard]] Type type() const { return type_; }
bool Equal(const WebContents* web_contents) const;
void LoadURL(const GURL& url, const gin_helper::Dictionary& options);
void Reload();
void ReloadIgnoringCache();
@@ -212,7 +211,6 @@ class WebContents final : public ExclusiveAccessContext,
void GoForward();
bool CanGoToOffset(int offset) const;
void GoToOffset(int offset);
bool CanGoToIndex(int index) const;
void GoToIndex(int index);
int GetActiveIndex() const;
content::NavigationEntry* GetNavigationEntryAtIndex(int index) const;
@@ -784,6 +782,8 @@ class WebContents final : public ExclusiveAccessContext,
content::GlobalRenderFrameHostId render_frame_host_id,
std::vector<std::u16string> types);
[[nodiscard]] bool CanGoToIndex(int index) const;
cppgc::Persistent<api::Session> session_;
v8::Global<v8::Value> devtools_web_contents_;
cppgc::Persistent<api::Debugger> debugger_;

View File

@@ -417,10 +417,13 @@ std::string WebFrameMain::FrameToken() const {
base::ProcessId WebFrameMain::OSProcessID() const {
if (!CheckRenderFrame())
return -1;
base::ProcessHandle process_handle =
render_frame_host()->GetProcess()->GetProcess().Handle();
return base::GetProcId(process_handle);
return base::kNullProcessId;
const auto& process = render_frame_host()->GetProcess()->GetProcess();
if (!process.IsValid())
return base::kNullProcessId;
return process.Pid();
}
int32_t WebFrameMain::ProcessID() const {

View File

@@ -20,6 +20,7 @@
#include "base/time/time.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_pref_names.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/pref_names.h"
@@ -27,6 +28,7 @@
#include "extensions/common/error_utils.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/background_info.h"
namespace extensions {
@@ -143,6 +145,19 @@ void ElectronExtensionLoader::FinishExtensionLoad(
std::pair<scoped_refptr<const Extension>, std::string> result) {
scoped_refptr<const Extension> extension = result.first;
if (extension) {
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
if (BackgroundInfo::IsServiceWorkerBased(extension.get())) {
// Tell Chromium that it needs to start the extension's service worker.
// Chromium usually does this only when an extension is first installed
// because Chrome will restart the service worker when the browser
// relaunches. In Electron, we make a fresh install on every app start,
// so we need to run the fresh install logic again.
extension_prefs->UpdateExtensionPref(
extension.get()->id(), extensions::kPrefHasStartedServiceWorker,
base::Value(false));
}
extension_registrar_->AddExtension(extension);
// Write extension install time to ExtensionPrefs.
@@ -152,7 +167,6 @@ void ElectronExtensionLoader::FinishExtensionLoad(
// Implementation for writing the pref was based on
// PreferenceAPIBase::SetExtensionControlledPref.
{
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
ExtensionPrefs::ScopedDictionaryUpdate update(
extension_prefs, extension.get()->id(),
extensions::pref_names::kPrefPreferences);

View File

@@ -87,13 +87,6 @@ void InitializeFeatureList() {
std::string(",") + sandbox::policy::features::kNetworkServiceSandbox.name;
#endif
#if BUILDFLAG(IS_MAC)
disable_features +=
// MacWebContentsOcclusion is causing some odd visibility
// issues with multiple web contents
std::string(",") + features::kMacWebContentsOcclusion.name;
#endif
#if BUILDFLAG(ENABLE_PDF_VIEWER)
// Enable window.showSaveFilePicker api for saving pdf files.
// Refs https://issues.chromium.org/issues/373852607

View File

@@ -697,7 +697,11 @@ void FileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess(
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveEntryResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
callback_map_.try_emplace(path_info.path, std::move(callback));
auto [it, inserted] = callback_map_.try_emplace(path_info.path);
it->second.push_back(std::move(callback));
if (!inserted)
return;
auto after_blocklist_check_callback = base::BindOnce(
&FileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist,
@@ -769,8 +773,11 @@ void FileSystemAccessPermissionContext::PerformAfterWriteChecks(
void FileSystemAccessPermissionContext::RunRestrictedPathCallback(
const base::FilePath& file_path,
SensitiveEntryResult result) {
if (auto val = callback_map_.extract(file_path))
std::move(val.mapped()).Run(result);
if (auto val = callback_map_.extract(file_path)) {
for (auto& callback : val.mapped()) {
std::move(callback).Run(result);
}
}
}
void FileSystemAccessPermissionContext::OnRestrictedPathResult(

View File

@@ -196,7 +196,8 @@ class FileSystemAccessPermissionContext
std::map<url::Origin, base::DictValue> id_pathinfo_map_;
std::map<base::FilePath, base::OnceCallback<void(SensitiveEntryResult)>>
std::map<base::FilePath,
std::vector<base::OnceCallback<void(SensitiveEntryResult)>>>
callback_map_;
std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>

View File

@@ -4,12 +4,16 @@
#include "shell/browser/notifications/linux/libnotify_notification.h"
#include <dlfcn.h>
#include <array>
#include <string>
#include "base/containers/flat_set.h"
#include "base/files/file_enumerator.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/nix/xdg_util.h"
#include "base/no_destructor.h"
#include "base/process/process_handle.h"
#include "base/strings/utf_string_conversions.h"
@@ -50,6 +54,9 @@ bool NotifierSupportsActions() {
return HasCapability("actions");
}
using GetActivationTokenFunc = const char* (*)(NotifyNotification*);
GetActivationTokenFunc g_get_activation_token = nullptr;
void log_and_clear_error(GError* error, const char* context) {
LOG(ERROR) << context << ": domain=" << error->domain
<< " code=" << error->code << " message=\"" << error->message
@@ -61,18 +68,40 @@ void log_and_clear_error(GError* error, const char* context) {
// static
bool LibnotifyNotification::Initialize() {
if (!GetLibNotifyLoader().Load("libnotify.so.4") && // most common one
!GetLibNotifyLoader().Load("libnotify.so.5") &&
!GetLibNotifyLoader().Load("libnotify.so.1") &&
!GetLibNotifyLoader().Load("libnotify.so")) {
constexpr std::array kLibnotifySonames = {
"libnotify.so.4",
"libnotify.so.5",
"libnotify.so.1",
"libnotify.so",
};
const char* loaded_soname = nullptr;
for (const char* soname : kLibnotifySonames) {
if (GetLibNotifyLoader().Load(soname)) {
loaded_soname = soname;
break;
}
}
if (!loaded_soname) {
LOG(WARNING) << "Unable to find libnotify; notifications disabled";
return false;
}
if (!GetLibNotifyLoader().notify_is_initted() &&
!GetLibNotifyLoader().notify_init(GetApplicationName().c_str())) {
LOG(WARNING) << "Unable to initialize libnotify; notifications disabled";
return false;
}
// Safe to cache the symbol after dlclose(handle) because libnotify remains
// loaded via GetLibNotifyLoader() for the process lifetime.
if (void* handle = dlopen(loaded_soname, RTLD_LAZY)) {
g_get_activation_token = reinterpret_cast<GetActivationTokenFunc>(
dlsym(handle, "notify_notification_get_activation_token"));
dlclose(handle);
}
return true;
}
@@ -192,6 +221,14 @@ void LibnotifyNotification::OnNotificationView(NotifyNotification* notification,
gpointer user_data) {
LibnotifyNotification* that = static_cast<LibnotifyNotification*>(user_data);
DCHECK(that);
if (g_get_activation_token) {
const char* token = g_get_activation_token(notification);
if (token && *token) {
base::nix::SetActivationToken(std::string(token));
}
}
that->NotificationClicked();
}

View File

@@ -478,7 +478,6 @@ NSArray* ConvertSharingItemToNS(const SharingItem& item) {
if (![represented
isKindOfClass:[WeakPtrToElectronMenuModelAsNSObject class]]) {
NSLog(@"representedObject is not a WeakPtrToElectronMenuModelAsNSObject");
return;
}

View File

@@ -43,6 +43,14 @@ using TitleBarStyle = electron::NativeWindowMac::TitleBarStyle;
#pragma mark - NSWindowDelegate
- (void)windowDidChangeOcclusionState:(NSNotification*)notification {
// Chromium's WebContentsOcclusionCheckerMac posts synthetic occlusion
// notifications tagged with its class name in userInfo. These reflect the
// checker's manual frame-intersection heuristic, not an actual macOS
// occlusion state change, so the real occlusionState hasn't changed and
// emitting show/hide in response would be spurious.
if (notification.userInfo[@"WebContentsOcclusionCheckerMac"] != nil)
return;
// notification.object is the window that changed its state.
// It's safe to use self.window instead if you don't assign one delegate to
// many windows

View File

@@ -239,7 +239,9 @@ void ElectronDesktopWindowTreeHostLinux::UpdateFrameHints() {
if (ui::OzonePlatform::GetInstance()->IsWindowCompositingSupported()) {
// Set the opaque region.
std::vector<gfx::Rect> opaque_region;
if (IsShowingFrame(window_state)) {
if (native_window_view_->IsTranslucent()) {
// Leave opaque_region empty.
} else if (IsShowingFrame(window_state)) {
// The opaque region is a list of rectangles that contain only fully
// opaque pixels of the window. We need to convert the clipping
// rounded-rect into this format.

View File

@@ -12,6 +12,7 @@
#include <utility>
#include "base/base64.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/span.h"
#include "base/dcheck_is_on.h"
#include "base/memory/raw_ptr.h"
@@ -160,6 +161,13 @@ void OnOpenItemComplete(const base::FilePath& path, const std::string& result) {
constexpr base::TimeDelta kInitialBackoffDelay = base::Milliseconds(250);
constexpr base::TimeDelta kMaxBackoffDelay = base::Seconds(10);
constexpr auto kValidDockStates = base::MakeFixedFlatSet<std::string_view>(
{"bottom", "left", "right", "undocked"});
bool IsValidDockState(const std::string& state) {
return kValidDockStates.contains(state);
}
} // namespace
class InspectableWebContents::NetworkResourceLoader
@@ -394,7 +402,7 @@ void InspectableWebContents::SetDockState(const std::string& state) {
can_dock_ = false;
} else {
can_dock_ = true;
dock_state_ = state;
dock_state_ = IsValidDockState(state) ? state : "right";
}
}
@@ -559,7 +567,13 @@ void InspectableWebContents::LoadCompleted() {
pref_service_->GetDict(kDevToolsPreferences);
const std::string* current_dock_state =
prefs.FindString("currentDockState");
base::RemoveChars(*current_dock_state, "\"", &dock_state_);
if (current_dock_state) {
std::string sanitized;
base::RemoveChars(*current_dock_state, "\"", &sanitized);
dock_state_ = IsValidDockState(sanitized) ? sanitized : "right";
} else {
dock_state_ = "right";
}
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
auto* api_web_contents = api::WebContents::From(GetWebContents());

View File

@@ -163,7 +163,7 @@ int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
gfx::Rect client_bounds = bounds();
if (!frame_->IsFullscreen()) {
client_bounds.Inset(RestoredFrameBorderInsets());
client_bounds.Inset(linux_frame_layout_->FrameBorderInsets(false));
client_bounds.Inset(
gfx::Insets::TLBR(GetTitlebarBounds().height(), 0, 0, 0));
}
@@ -236,6 +236,21 @@ void ClientFrameViewLinux::Layout(PassKey) {
}
void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
if (frame_->IsFullscreen()) {
return;
}
if (frame_->IsMaximized()) {
// Some GTK themes (Breeze) still render shadow/border assets when
// maximized, and we don't need a border when maximized anyway. Chromium
// switches on this too: OpaqueBrowserFrameView::PaintMaximizedFrameBorder.
PaintMaximizedFrameBorder(canvas);
} else {
PaintRestoredFrameBorder(canvas);
}
}
void ClientFrameViewLinux::PaintRestoredFrameBorder(gfx::Canvas* canvas) {
if (auto* frame_provider = linux_frame_layout_->GetFrameProvider()) {
frame_provider->PaintWindowFrame(
canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
@@ -243,6 +258,18 @@ void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
}
}
void ClientFrameViewLinux::PaintMaximizedFrameBorder(gfx::Canvas* canvas) {
ui::NativeTheme::FrameTopAreaExtraParams frame_top_area;
frame_top_area.use_custom_frame = true;
frame_top_area.is_active = ShouldPaintAsActive();
frame_top_area.default_background_color = SK_ColorTRANSPARENT;
ui::NativeTheme::ExtraParams params(frame_top_area);
GetNativeTheme()->Paint(
canvas->sk_canvas(), GetColorProvider(), ui::NativeTheme::kFrameTopArea,
ui::NativeTheme::kNormal,
gfx::Rect(0, 0, width(), GetTitlebarBounds().bottom()), params);
}
void ClientFrameViewLinux::PaintAsActiveChanged() {
UpdateThemeValues();
}
@@ -251,23 +278,15 @@ void ClientFrameViewLinux::UpdateThemeValues() {
gtk::GtkCssContext window_context =
gtk::AppendCssNodeToStyleContext({}, "window.background.csd");
gtk::GtkCssContext headerbar_context = gtk::AppendCssNodeToStyleContext(
{}, "headerbar.default-decoration.titlebar");
window_context, "headerbar.default-decoration.titlebar");
gtk::GtkCssContext title_context =
gtk::AppendCssNodeToStyleContext(headerbar_context, "label.title");
gtk::GtkCssContext button_context = gtk::AppendCssNodeToStyleContext(
headerbar_context, "button.image-button");
gtk_style_context_set_parent(headerbar_context, window_context);
gtk_style_context_set_parent(title_context, headerbar_context);
gtk_style_context_set_parent(button_context, headerbar_context);
// ShouldPaintAsActive asks the widget, so assume active if the widget is not
// set yet.
if (GetWidget() != nullptr && !ShouldPaintAsActive()) {
gtk_style_context_set_state(window_context, GTK_STATE_FLAG_BACKDROP);
gtk_style_context_set_state(headerbar_context, GTK_STATE_FLAG_BACKDROP);
gtk_style_context_set_state(title_context, GTK_STATE_FLAG_BACKDROP);
gtk_style_context_set_state(button_context, GTK_STATE_FLAG_BACKDROP);
}
theme_values_.window_border_radius =
@@ -281,10 +300,6 @@ void ClientFrameViewLinux::UpdateThemeValues() {
theme_values_.title_color = gtk::GtkStyleContextGetColor(title_context);
theme_values_.title_padding = gtk::GtkStyleContextGetPadding(title_context);
gtk::GtkStyleContextGet(button_context, "min-height",
&theme_values_.button_min_size, nullptr);
theme_values_.button_padding = gtk::GtkStyleContextGetPadding(button_context);
title_->SetEnabledColor(theme_values_.title_color);
InvalidateLayout();
@@ -299,8 +314,9 @@ ClientFrameViewLinux::GetButtonTypeToSkip() const {
}
void ClientFrameViewLinux::UpdateButtonImages() {
nav_button_provider_->RedrawImages(theme_values_.button_min_size,
frame_->IsMaximized(),
int top_area_height = theme_values_.titlebar_min_height +
theme_values_.titlebar_padding.height();
nav_button_provider_->RedrawImages(top_area_height, frame_->IsMaximized(),
ShouldPaintAsActive());
ui::NavButtonProvider::FrameButtonDisplayType skip_type =
@@ -368,7 +384,14 @@ void ClientFrameViewLinux::LayoutButtonsOnSide(
button->button->SetVisible(true);
int button_width = theme_values_.button_min_size;
// CSS min-size/height/width is not enough to determine the actual size of
// the buttons, so we sample the rendered image. See Chromium's
// BrowserFrameViewLinuxNative::MaybeUpdateCachedFrameButtonImages.
int button_width =
nav_button_provider_
->GetImage(button->type,
ui::NavButtonProvider::ButtonState::kNormal)
.width();
int next_button_offset =
button_width + nav_button_provider_->GetInterNavButtonSpacing();
@@ -404,7 +427,7 @@ gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const {
std::max(font_height, theme_values_.titlebar_min_height) +
GetTitlebarContentInsets().height();
gfx::Insets decoration_insets = RestoredFrameBorderInsets();
gfx::Insets decoration_insets = linux_frame_layout_->FrameBorderInsets(false);
// We add the inset height here, so the .Inset() that follows won't reduce it
// to be too small.

View File

@@ -91,12 +91,11 @@ class ClientFrameViewLinux : public FramelessView,
SkColor title_color;
gfx::Insets title_padding;
int button_min_size;
gfx::Insets button_padding;
};
void PaintAsActiveChanged();
void PaintRestoredFrameBorder(gfx::Canvas* canvas);
void PaintMaximizedFrameBorder(gfx::Canvas* canvas);
void UpdateThemeValues();

View File

@@ -83,6 +83,12 @@ gfx::Insets LinuxFrameLayout::RestoredFrameBorderInsets() const {
return gfx::Insets();
}
gfx::Insets LinuxFrameLayout::FrameBorderInsets(bool restored) const {
return !restored && (window_->IsMaximized() || window_->IsFullscreen())
? gfx::Insets()
: RestoredFrameBorderInsets();
}
gfx::Insets LinuxFrameLayout::GetInputInsets() const {
return gfx::Insets(kResizeInsideBoundsSize);
}
@@ -106,7 +112,7 @@ void LinuxFrameLayout::set_tiled(bool tiled) {
gfx::Rect LinuxFrameLayout::GetWindowBounds() const {
gfx::Rect bounds = window_->widget()->GetWindowBoundsInScreen();
bounds.Inset(RestoredFrameBorderInsets());
bounds.Inset(FrameBorderInsets(false));
return bounds;
}

View File

@@ -44,6 +44,9 @@ class LinuxFrameLayout {
CSDStyle csd_style);
// Insets from the transparent widget border to the opaque part of the window.
// Returns empty insets when maximized or fullscreen unless |restored| is
// true. Matches Chromium's OpaqueBrowserFrameViewLayout::FrameBorderInsets.
gfx::Insets FrameBorderInsets(bool restored) const;
virtual gfx::Insets RestoredFrameBorderInsets() const;
// Insets for parts of the surface that should be counted for user input.
virtual gfx::Insets GetInputInsets() const;

View File

@@ -203,8 +203,14 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
if (frame()->IsFullscreen())
return;
if (window()->IsWindowControlsOverlayEnabled())
UpdateFrameCaptionButtons();
if (window()->IsTranslucent())
return;
const bool active = ShouldPaintAsActive();
const gfx::Insets border = RestoredFrameBorderInsets();
const gfx::Insets border = FrameBorderInsets(false);
const bool showing_shadow = linux_frame_layout_->IsShowingShadow();
gfx::RectF bounds_dip(GetLocalBounds());
if (showing_shadow) {
@@ -228,11 +234,6 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
::PaintRestoredFrameBorderLinux(*canvas, *this, frame_background_.get(), clip,
showing_shadow, active, border, shadow_values,
linux_frame_layout_->tiled());
if (!window()->IsWindowControlsOverlayEnabled())
return;
UpdateFrameCaptionButtons();
}
void OpaqueFrameView::PaintAsActiveChanged() {
@@ -341,9 +342,7 @@ views::Button* OpaqueFrameView::CreateButton(
}
gfx::Insets OpaqueFrameView::FrameBorderInsets(bool restored) const {
return !restored && IsFrameCondensed()
? gfx::Insets()
: linux_frame_layout_->RestoredFrameBorderInsets();
return linux_frame_layout_->FrameBorderInsets(restored);
}
int OpaqueFrameView::FrameTopBorderThickness(bool restored) const {

View File

@@ -101,13 +101,13 @@ base::DictValue BuildTargetDescriptor(
int process_id,
int routing_id,
ui::AXMode accessibility_mode,
base::ProcessHandle handle = base::kNullProcessHandle) {
base::ProcessId pid = base::kNullProcessId) {
base::DictValue target_data;
target_data.Set(kProcessIdField, process_id);
target_data.Set(kRoutingIdField, routing_id);
target_data.Set(kUrlField, url.spec());
target_data.Set(kNameField, base::EscapeForHTML(name));
target_data.Set(kPidField, static_cast<int>(base::GetProcId(handle)));
target_data.Set(kPidField, static_cast<int>(pid));
target_data.Set(kFaviconUrlField, favicon_url.spec());
target_data.Set(kAccessibilityModeField,
static_cast<int>(accessibility_mode.flags()));
@@ -138,9 +138,12 @@ base::DictValue BuildTargetDescriptor(content::RenderViewHost* rvh) {
accessibility_mode = web_contents->GetAccessibilityMode();
}
const auto& process = rvh->GetProcess()->GetProcess();
const auto pid = process.IsValid() ? process.Pid() : base::kNullProcessId;
return BuildTargetDescriptor(url, title, favicon_url,
rvh->GetProcess()->GetDeprecatedID(),
rvh->GetRoutingID(), accessibility_mode);
rvh->GetRoutingID(), accessibility_mode, pid);
}
base::DictValue BuildTargetDescriptor(electron::NativeWindow* window) {

View File

@@ -66,10 +66,6 @@ void SetHiddenValue(v8::Isolate* isolate,
object->SetPrivate(context, privateKey, value);
}
int32_t GetObjectHash(v8::Local<v8::Object> object) {
return object->GetIdentityHash();
}
void TakeHeapSnapshot(v8::Isolate* isolate) {
isolate->GetHeapProfiler()->TakeHeapSnapshot();
}
@@ -103,7 +99,6 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict{isolate, exports};
dict.SetMethod("getHiddenValue", &GetHiddenValue);
dict.SetMethod("setHiddenValue", &SetHiddenValue);
dict.SetMethod("getObjectHash", &GetObjectHash);
dict.SetMethod("takeHeapSnapshot", &TakeHeapSnapshot);
dict.SetMethod("requestGarbageCollectionForTesting",
&RequestGarbageCollectionForTesting);

View File

@@ -4,6 +4,7 @@
#include "shell/common/gin_helper/wrappable.h"
#include "base/task/sequenced_task_runner.h"
#include "gin/object_template_builder.h"
#include "gin/public/isolate_holder.h"
#include "shell/common/gin_helper/dictionary.h"
@@ -90,7 +91,22 @@ void WrappableBase::SecondWeakCallback(
if (gin::IsolateHolder::DestroyedMicrotasksRunner()) {
return;
}
delete static_cast<WrappableBase*>(data.GetInternalField(0));
// Defer destruction to a posted task. V8's second-pass weak callbacks run
// inside a DisallowJavascriptExecutionScope (they may touch the V8 API but
// must not invoke JS). Several Electron Wrappables (e.g. WebContents) emit
// JS events from their destructors, so deleting synchronously here can
// crash with "Invoke in DisallowJavascriptExecutionScope" — see
// https://github.com/electron/electron/issues/47420. Posting via the
// current sequence's task runner ensures the destructor runs once V8 has
// left the GC scope. If no task runner is available (e.g. early/late in
// process lifetime), fall back to synchronous deletion.
auto* wrappable = static_cast<WrappableBase*>(data.GetInternalField(0));
if (base::SequencedTaskRunner::HasCurrentDefault()) {
base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
wrappable);
} else {
delete wrappable;
}
}
DeprecatedWrappableBase::DeprecatedWrappableBase() = default;
@@ -126,9 +142,19 @@ void DeprecatedWrappableBase::SecondWeakCallback(
const v8::WeakCallbackInfo<DeprecatedWrappableBase>& data) {
if (gin::IsolateHolder::DestroyedMicrotasksRunner())
return;
// See WrappableBase::SecondWeakCallback for why deletion is posted: V8's
// second-pass weak callbacks run inside a DisallowJavascriptExecutionScope,
// and several Wrappables emit JS events from their destructors.
// https://github.com/electron/electron/issues/47420
DeprecatedWrappableBase* wrappable = data.GetParameter();
if (wrappable)
if (!wrappable)
return;
if (base::SequencedTaskRunner::HasCurrentDefault()) {
base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
wrappable);
} else {
delete wrappable;
}
}
v8::MaybeLocal<v8::Object> DeprecatedWrappableBase::GetWrapperImpl(

View File

@@ -6,6 +6,7 @@
#define ELECTRON_SHELL_COMMON_GIN_HELPER_WRAPPABLE_BASE_H_
#include "base/memory/raw_ptr.h"
#include "base/task/sequenced_task_runner_helpers.h"
#include "v8/include/v8-forward.h"
namespace gin {
@@ -75,6 +76,11 @@ class DeprecatedWrappableBase {
DeprecatedWrappableBase();
virtual ~DeprecatedWrappableBase();
// SecondWeakCallback posts destruction via DeleteSoon so that destructors
// (which may emit JS events) run outside V8's GC scope. DeleteSoon needs
// access to the protected destructor.
friend class base::DeleteHelper<DeprecatedWrappableBase>;
// Overrides of this method should be declared final and not overridden again.
virtual gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate);

View File

@@ -53,7 +53,8 @@ v8::MaybeLocal<v8::Value> CompileAndCall(
context, v8::Null(isolate), arguments->size(), arguments->data());
// This will only be caught when something has gone terrible wrong as all
// electron scripts are wrapped in a try {} catch {} by webpack
// electron scripts are wrapped in a try {} catch {} by the esbuild bundler
// (see build/esbuild/bundle.js applyWrappers).
if (try_catch.HasCaught()) {
std::string msg = "no error message";
if (!try_catch.Message().IsEmpty()) {

View File

@@ -17,7 +17,7 @@ import * as nodeUrl from 'node:url';
import { emittedUntil, emittedNTimes } from './lib/events-helpers';
import { randomString } from './lib/net-helpers';
import { HexColors, hasCapturableScreen, ScreenCapture } from './lib/screen-helpers';
import { ifit, ifdescribe, defer, listen, waitUntil } from './lib/spec-helpers';
import { ifit, ifdescribe, defer, listen, waitUntil, isWayland } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixtures = path.resolve(__dirname, 'fixtures');
@@ -1204,7 +1204,45 @@ describe('BrowserWindow module', () => {
});
}
describe('focus and visibility', () => {
describe('visibility', () => {
let w: BrowserWindow;
beforeEach(() => {
w = new BrowserWindow({ show: false });
});
afterEach(async () => {
await closeWindow(w);
w = null as unknown as BrowserWindow;
});
describe('BrowserWindow.show()', () => {
it('should make the window visible', async () => {
const show = once(w, 'show');
w.show();
await show;
expect(w.isVisible()).to.equal(true);
});
});
describe('BrowserWindow.hide()', () => {
it('should make the window not visible', () => {
w.show();
w.hide();
expect(w.isVisible()).to.equal(false);
});
it('emits when window is hidden', async () => {
const shown = once(w, 'show');
w.show();
await shown;
const hidden = once(w, 'hide');
w.hide();
await hidden;
expect(w.isVisible()).to.equal(false);
});
});
});
// Wayland does not allow focus and z-order to be controlled without user input
ifdescribe(!isWayland)('focus, blur, and z-order', () => {
let w: BrowserWindow;
beforeEach(() => {
w = new BrowserWindow({ show: false });
@@ -1221,18 +1259,12 @@ describe('BrowserWindow module', () => {
await p;
expect(w.isFocused()).to.equal(true);
});
it('should make the window visible', async () => {
it('emits focus event and makes the window visible', async () => {
const p = once(w, 'focus');
w.show();
await p;
expect(w.isVisible()).to.equal(true);
});
it('emits when window is shown', async () => {
const show = once(w, 'show');
w.show();
await show;
expect(w.isVisible()).to.equal(true);
});
});
describe('BrowserWindow.hide()', () => {
@@ -1240,20 +1272,6 @@ describe('BrowserWindow module', () => {
w.hide();
expect(w.isFocused()).to.equal(false);
});
it('should make the window not visible', () => {
w.show();
w.hide();
expect(w.isVisible()).to.equal(false);
});
it('emits when window is hidden', async () => {
const shown = once(w, 'show');
w.show();
await shown;
const hidden = once(w, 'hide');
w.hide();
await hidden;
expect(w.isVisible()).to.equal(false);
});
});
describe('BrowserWindow.minimize()', () => {
@@ -1626,6 +1644,20 @@ describe('BrowserWindow module', () => {
await closeWindow(w2, { assertNotWindows: false });
});
});
describe('window.webContents.focus()', () => {
afterEach(closeAllWindows);
it('focuses window', async () => {
const w1 = new BrowserWindow({ x: 100, y: 300, width: 300, height: 200 });
w1.loadURL('about:blank');
const w2 = new BrowserWindow({ x: 300, y: 300, width: 300, height: 200 });
w2.loadURL('about:blank');
const w1Focused = once(w1, 'focus');
w1.webContents.focus();
await w1Focused;
expect(w1.webContents.isFocused()).to.be.true('focuses window');
});
});
});
describe('sizing', () => {
@@ -1814,7 +1846,8 @@ describe('BrowserWindow module', () => {
});
});
describe('BrowserWindow.setContentBounds(bounds)', () => {
// Windows cannot be programmatically moved on Wayland
ifdescribe(!isWayland)('BrowserWindow.setContentBounds(bounds)', () => {
it('sets the content size and position', async () => {
const bounds = { x: 10, y: 10, width: 250, height: 250 };
const resize = once(w, 'resize');
@@ -3285,6 +3318,19 @@ describe('BrowserWindow module', () => {
});
});
// On Wayland, hidden windows may not have mapped surfaces or finalized geometry
// until shown. Tests that depend on real geometry or frame events may need
// to show the window first.
const showWindowForWayland = async (w: BrowserWindow) => {
if (!isWayland || w.isVisible()) {
return;
}
const shown = once(w, 'show');
w.show();
await shown;
};
describe('"titleBarStyle" option', () => {
const testWindowsOverlay = async (style: any) => {
const w = new BrowserWindow({
@@ -3304,8 +3350,10 @@ describe('BrowserWindow module', () => {
} else {
const overlayReady = once(ipcMain, 'geometrychange');
await w.loadFile(overlayHTML);
await showWindowForWayland(w);
await overlayReady;
}
const overlayEnabled = await w.webContents.executeJavaScript('navigator.windowControlsOverlay.visible');
expect(overlayEnabled).to.be.true('overlayEnabled');
const overlayRect = await w.webContents.executeJavaScript('getJSOverlayProperties()');
@@ -3418,6 +3466,7 @@ describe('BrowserWindow module', () => {
} else {
const overlayReady = once(ipcMain, 'geometrychange');
await w.loadFile(overlayHTML);
await showWindowForWayland(w);
await overlayReady;
}
@@ -3491,6 +3540,7 @@ describe('BrowserWindow module', () => {
const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html');
const overlayReady = once(ipcMain, 'geometrychange');
await w.loadFile(overlayHTML);
await showWindowForWayland(w);
if (firstRun) {
await overlayReady;
}
@@ -4771,7 +4821,9 @@ describe('BrowserWindow module', () => {
const w = new BrowserWindow({ show: false });
let called = false;
w.loadFile(path.join(fixtures, 'api', 'frame-subscriber.html'));
w.webContents.on('dom-ready', () => {
w.webContents.on('dom-ready', async () => {
await showWindowForWayland(w);
w.webContents.beginFrameSubscription(function () {
// This callback might be called twice.
if (called) return;
@@ -4791,7 +4843,9 @@ describe('BrowserWindow module', () => {
const w = new BrowserWindow({ show: false });
let called = false;
w.loadFile(path.join(fixtures, 'api', 'frame-subscriber.html'));
w.webContents.on('dom-ready', () => {
w.webContents.on('dom-ready', async () => {
await showWindowForWayland(w);
w.webContents.beginFrameSubscription(function (data) {
// This callback might be called twice.
if (called) return;
@@ -4815,7 +4869,9 @@ describe('BrowserWindow module', () => {
let called = false;
let gotInitialFullSizeFrame = false;
const [contentWidth, contentHeight] = w.getContentSize();
w.webContents.on('did-finish-load', () => {
w.webContents.on('did-finish-load', async () => {
await showWindowForWayland(w);
w.webContents.beginFrameSubscription(true, (image, rect) => {
if (image.isEmpty()) {
// Chromium sometimes sends a 0x0 frame at the beginning of the
@@ -5396,55 +5452,57 @@ describe('BrowserWindow module', () => {
await createTwo();
});
ifit(process.platform !== 'darwin')('can disable and enable a window', () => {
const w = new BrowserWindow({ show: false });
w.setEnabled(false);
expect(w.isEnabled()).to.be.false('w.isEnabled()');
w.setEnabled(true);
expect(w.isEnabled()).to.be.true('!w.isEnabled()');
});
ifdescribe(process.platform !== 'darwin' && !isWayland)('disabling parent windows', () => {
it('can disable and enable a window', () => {
const w = new BrowserWindow({ show: false });
w.setEnabled(false);
expect(w.isEnabled()).to.be.false('w.isEnabled()');
w.setEnabled(true);
expect(w.isEnabled()).to.be.true('!w.isEnabled()');
});
ifit(process.platform !== 'darwin')('disables parent window', () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
expect(w.isEnabled()).to.be.true('w.isEnabled');
c.show();
expect(w.isEnabled()).to.be.false('w.isEnabled');
});
it('disables parent window', () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
expect(w.isEnabled()).to.be.true('w.isEnabled');
c.show();
expect(w.isEnabled()).to.be.false('w.isEnabled');
});
ifit(process.platform !== 'darwin')('re-enables an enabled parent window when closed', async () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
const closed = once(c, 'closed');
c.show();
c.close();
await closed;
expect(w.isEnabled()).to.be.true('w.isEnabled');
});
it('re-enables an enabled parent window when closed', async () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
const closed = once(c, 'closed');
c.show();
c.close();
await closed;
expect(w.isEnabled()).to.be.true('w.isEnabled');
});
ifit(process.platform !== 'darwin')('does not re-enable a disabled parent window when closed', async () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
const closed = once(c, 'closed');
w.setEnabled(false);
c.show();
c.close();
await closed;
expect(w.isEnabled()).to.be.false('w.isEnabled');
});
it('does not re-enable a disabled parent window when closed', async () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
const closed = once(c, 'closed');
w.setEnabled(false);
c.show();
c.close();
await closed;
expect(w.isEnabled()).to.be.false('w.isEnabled');
});
ifit(process.platform !== 'darwin')('disables parent window recursively', () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
const c2 = new BrowserWindow({ show: false, parent: w, modal: true });
c.show();
expect(w.isEnabled()).to.be.false('w.isEnabled');
c2.show();
expect(w.isEnabled()).to.be.false('w.isEnabled');
c.destroy();
expect(w.isEnabled()).to.be.false('w.isEnabled');
c2.destroy();
expect(w.isEnabled()).to.be.true('w.isEnabled');
it('disables parent window recursively', () => {
const w = new BrowserWindow({ show: false });
const c = new BrowserWindow({ show: false, parent: w, modal: true });
const c2 = new BrowserWindow({ show: false, parent: w, modal: true });
c.show();
expect(w.isEnabled()).to.be.false('w.isEnabled');
c2.show();
expect(w.isEnabled()).to.be.false('w.isEnabled');
c.destroy();
expect(w.isEnabled()).to.be.false('w.isEnabled');
c2.destroy();
expect(w.isEnabled()).to.be.true('w.isEnabled');
});
});
});
});
@@ -5684,7 +5742,7 @@ describe('BrowserWindow module', () => {
});
});
ifdescribe(process.platform !== 'win32')('visibleOnAllWorkspaces state', () => {
ifdescribe(process.platform !== 'win32' && !isWayland)('visibleOnAllWorkspaces state', () => {
describe('with properties', () => {
it('can be changed', () => {
const w = new BrowserWindow({ show: false });
@@ -6835,20 +6893,6 @@ describe('BrowserWindow module', () => {
});
});
describe('window.webContents.focus()', () => {
afterEach(closeAllWindows);
it('focuses window', async () => {
const w1 = new BrowserWindow({ x: 100, y: 300, width: 300, height: 200 });
w1.loadURL('about:blank');
const w2 = new BrowserWindow({ x: 300, y: 300, width: 300, height: 200 });
w2.loadURL('about:blank');
const w1Focused = once(w1, 'focus');
w1.webContents.focus();
await w1Focused;
expect(w1.webContents.isFocused()).to.be.true('focuses window');
});
});
describe('offscreen rendering', () => {
let w: BrowserWindow;
beforeEach(function () {

View File

@@ -132,6 +132,36 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
});
});
describe('getTraceBufferUsage', function () {
this.timeout(10e3);
it('does not crash and returns valid usage data', async () => {
await app.whenReady();
await contentTracing.startRecording({
categoryFilter: '*',
traceOptions: 'record-until-full'
});
// Yield to the event loop so the JS HandleScope from this tick is gone.
// When the Mojo response arrives it fires OnTraceBufferUsageAvailable
// as a plain Chromium task — if that callback lacks its own HandleScope
// the process will crash with "Cannot create a handle without a HandleScope".
const result = await contentTracing.getTraceBufferUsage();
expect(result).to.have.property('percentage').that.is.a('number');
expect(result).to.have.property('value').that.is.a('number');
await contentTracing.stopRecording();
});
it('returns zero usage when no trace is active', async () => {
await app.whenReady();
const result = await contentTracing.getTraceBufferUsage();
expect(result).to.have.property('percentage').that.is.a('number');
expect(result.percentage).to.equal(0);
});
});
describe('captured events', () => {
it('include V8 samples from the main process', async function () {
this.timeout(60000);

View File

@@ -12,6 +12,7 @@ import { app } from 'electron/main';
import { expect } from 'chai';
import * as dbus from 'dbus-native';
import { once } from 'node:events';
import * as path from 'node:path';
import { promisify } from 'node:util';
@@ -25,7 +26,7 @@ const skip = process.platform !== 'linux' ||
!process.env.DBUS_SESSION_BUS_ADDRESS;
ifdescribe(!skip)('Notification module (dbus)', () => {
let mock: any, Notification, getCalls: any, reset: any;
let mock: any, Notification: any, getCalls: any, emitSignal: any, reset: any;
const realAppName = app.name;
const realAppVersion = app.getVersion();
const appName = 'api-notification-dbus-spec';
@@ -45,7 +46,17 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
const getInterface = promisify(service.getInterface.bind(service));
mock = await getInterface(path, iface);
getCalls = promisify(mock.GetCalls.bind(mock));
emitSignal = promisify(mock.EmitSignal.bind(mock));
reset = promisify(mock.Reset.bind(mock));
// Override GetCapabilities to include "actions" so that libnotify
// registers the "default" action callback on notifications.
const addMethod = promisify(mock.AddMethod.bind(mock));
await addMethod(
serviceName, 'GetCapabilities', '', 'as',
'ret = ["body", "body-markup", "icon-static", "image/svg+xml", ' +
'"private-synchronous", "append", "private-icon-only", "truncation", "actions"]'
);
});
after(async () => {
@@ -122,7 +133,7 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
app_icon: '',
title: 'title',
body: 'body',
actions: [],
actions: ['default', 'View'],
hints: {
append: 'true',
image_data: [3, 3, 12, true, 8, 4, Buffer.from([255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 76, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 38, 255, 255, 0, 0, 0, 255, 0, 0, 0, 0])],
@@ -133,4 +144,30 @@ ifdescribe(!skip)('Notification module (dbus)', () => {
});
});
});
describe('ActivationToken on notification click', () => {
it('should emit click when ActionInvoked is sent by the daemon', async () => {
const n = new Notification({ title: 'activation-token-test', body: 'test' });
const clicked = once(n, 'click');
n.show();
// getCalls returns all D-Bus method calls. The mock assigns sequential
// notification IDs starting at 1 for each Notify call.
const calls = await getCalls();
const notifyCalls = calls.filter((c: any) => c[1] === 'Notify');
const notificationId = notifyCalls.length;
// Simulate the notification daemon emitting ActivationToken (FDN 1.2)
// followed by ActionInvoked for a "default" click.
emitSignal(
'org.freedesktop.Notifications', 'ActivationToken',
'us', [['u', notificationId], ['s', 'test-activation-token']]);
emitSignal(
'org.freedesktop.Notifications', 'ActionInvoked',
'us', [['u', notificationId], ['s', 'default']]);
await clicked;
});
});
});

View File

@@ -53,6 +53,26 @@ describe('session module', () => {
}
});
const collectCookieChanges = async (cookies: Electron.Cookies, action: () => Promise<void>, count: number) => {
const changes: Array<{ cause: string, cookie: Electron.Cookie, removed: boolean }> = [];
let listener: ((event: Electron.Event, cookie: Electron.Cookie, cause: string, removed: boolean) => void) | undefined;
const changesPromise = new Promise<typeof changes>(resolve => {
listener = (_event, cookie, cause, removed) => {
changes.push({ cause, cookie, removed });
if (changes.length === count) resolve(changes);
};
cookies.on('changed', listener);
});
try {
await action();
return await changesPromise;
} finally {
if (listener) cookies.removeListener('changed', listener);
}
};
it('should get cookies', async () => {
const server = http.createServer((req, res) => {
res.setHeader('Set-Cookie', [`${name}=${value}`]);
@@ -223,6 +243,60 @@ describe('session module', () => {
expect(removeEventRemoved).to.equal(true);
});
it('emits overwrite and inserted events when a cookie is overwritten with a new value', async () => {
const { cookies } = session.fromPartition('cookies-overwrite-changed');
const name = 'foo';
const oldVal = 'bar';
const newVal = 'baz';
const expected = [
{ cause: 'overwrite', name, removed: true, value: oldVal },
{ cause: 'inserted', name, removed: false, value: newVal }
];
await cookies.set({ url, name, value: oldVal });
const changes = await collectCookieChanges(cookies, async () => {
await cookies.set({ url, name, value: newVal });
}, 2);
const actual = changes.map(({ cookie: { name, value }, cause, removed }) => ({ cause, name, removed, value }));
expect(actual).to.deep.equal(expected);
});
it('emits inserted-no-value-change-overwrite when a cookie is overwritten with the same value', async () => {
const { cookies } = session.fromPartition('cookies-same-value-overwrite-changed');
const name = 'foo';
const value = 'bar';
const nowSec = Date.now() / 1000;
const expected = [
{ cause: 'overwrite', name, removed: true, value },
{ cause: 'inserted-no-value-change-overwrite', name, removed: false, value }
];
await cookies.set({ url, name, value, expirationDate: nowSec + 120 });
const changes = await collectCookieChanges(cookies, async () => {
await cookies.set({ url, name, value, expirationDate: nowSec + 240 });
}, 2);
const actual = changes.map(({ cookie: { name, value }, cause, removed }) => ({ cause, name, removed, value }));
expect(actual).to.deep.equal(expected);
});
it('emits expired-overwrite when a cookie is overwritten by an already-expired cookie', async () => {
const { cookies } = session.fromPartition('cookies-expired-overwrite-changed');
const name = 'foo';
const value = 'bar';
const nowSec = Date.now() / 1000;
const expected = [{ cause: 'expired-overwrite', name, removed: true, value }];
await cookies.set({ url, name, value, expirationDate: nowSec + 120 });
const changes = await collectCookieChanges(cookies, async () => {
await cookies.set({ url, name, value, expirationDate: nowSec - 10 });
}, 1);
const actual = changes.map(({ cookie: { name, value }, cause, removed }) => ({ cause, name, removed, value }));
expect(actual).to.deep.equal(expected);
});
describe('ses.cookies.flushStore()', async () => {
it('flushes the cookies to disk', async () => {
const name = 'foo';

View File

@@ -802,6 +802,65 @@ describe('webContents module', () => {
});
});
describe('navigationHistory.goToIndex(index) API', () => {
beforeEach(async () => {
await w.loadURL(urlPage1);
await w.loadURL(urlPage2);
await w.loadURL(urlPage3);
expect(w.webContents.navigationHistory.getActiveIndex()).to.equal(2);
});
it('should be able to go to a valid earlier index', async () => {
w.webContents.navigationHistory.goToIndex(0);
await waitUntil(() => w.webContents.navigationHistory.getActiveIndex() === 0);
await waitUntil(() => w.webContents.getTitle() === 'Page 1');
});
it('should be able to go to a valid later index', async () => {
w.webContents.navigationHistory.goToIndex(0);
await waitUntil(() => w.webContents.navigationHistory.getActiveIndex() === 0);
await waitUntil(() => w.webContents.getTitle() === 'Page 1');
w.webContents.navigationHistory.goToIndex(2);
await waitUntil(() => w.webContents.navigationHistory.getActiveIndex() === 2);
await waitUntil(() => w.webContents.getTitle() === 'Page 3');
});
const expectNoEffect = async (fn: () => void) => {
const activeIndex = w.webContents.navigationHistory.getActiveIndex();
const title = w.webContents.getTitle();
let didStartNavigationCount = 0;
let didFinishLoadCount = 0;
w.webContents.on('did-start-navigation', () => didStartNavigationCount++);
w.webContents.on('did-finish-load', () => didFinishLoadCount++);
fn();
await setTimeout();
expect(w.webContents.navigationHistory.getActiveIndex()).to.equal(activeIndex);
expect(w.webContents.getTitle()).to.equal(title);
expect(didStartNavigationCount).to.equal(0);
expect(didFinishLoadCount).to.equal(0);
};
it('should do nothing when given the current active index', async () => {
const activeIndex = w.webContents.navigationHistory.getActiveIndex();
await expectNoEffect(() => w.webContents.navigationHistory.goToIndex(activeIndex));
});
it('should do nothing when given an invalid index', async () => {
await expectNoEffect(() => {
w.webContents.navigationHistory.goToIndex(-1);
w.webContents.navigationHistory.goToIndex(w.webContents.navigationHistory.length());
});
});
});
describe('navigationHistory.clear API', () => {
it('should be able clear history', async () => {
await w.loadURL(urlPage1);

View File

@@ -3,6 +3,7 @@ import { BaseWindow, BrowserWindow, View, WebContentsView, webContents, screen }
import { expect } from 'chai';
import { once } from 'node:events';
import { setTimeout as setTimeoutAsync } from 'node:timers/promises';
import { HexColors, ScreenCapture, hasCapturableScreen, nextFrameTime } from './lib/screen-helpers';
import { defer, ifdescribe, waitUntil } from './lib/spec-helpers';
@@ -309,6 +310,94 @@ describe('WebContentsView', () => {
}
expect(visibilityState).to.equal('visible');
});
it('tracks visibility for multiple child WebContentsViews', async () => {
const w = new BaseWindow({ show: false });
const cv = new View();
w.setContentView(cv);
const v1 = new WebContentsView();
const v2 = new WebContentsView();
cv.addChildView(v1);
cv.addChildView(v2);
v1.setBounds({ x: 0, y: 0, width: 400, height: 300 });
v2.setBounds({ x: 0, y: 300, width: 400, height: 300 });
await v1.webContents.loadURL('about:blank');
await v2.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
w.show();
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
w.hide();
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'hidden'))).to.eventually.be.fulfilled();
});
it('tracks visibility independently when a child WebContentsView is hidden via setVisible', async () => {
const w = new BaseWindow();
const cv = new View();
w.setContentView(cv);
const v1 = new WebContentsView();
const v2 = new WebContentsView();
cv.addChildView(v1);
cv.addChildView(v2);
v1.setBounds({ x: 0, y: 0, width: 400, height: 300 });
v2.setBounds({ x: 0, y: 300, width: 400, height: 300 });
await v1.webContents.loadURL('about:blank');
await v2.webContents.loadURL('about:blank');
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
await expect(waitUntil(async () => await haveVisibilityState(v2, 'visible'))).to.eventually.be.fulfilled();
v1.setVisible(false);
await expect(waitUntil(async () => await haveVisibilityState(v1, 'hidden'))).to.eventually.be.fulfilled();
// v2 should remain visible while v1 is hidden
expect(await v2.webContents.executeJavaScript('document.visibilityState')).to.equal('visible');
v1.setVisible(true);
await expect(waitUntil(async () => await haveVisibilityState(v1, 'visible'))).to.eventually.be.fulfilled();
});
it('fires a single visibilitychange event per show/hide transition', async () => {
const w = new BaseWindow({ show: false });
const v = new WebContentsView();
w.setContentView(v);
await v.webContents.loadURL('about:blank');
await v.webContents.executeJavaScript(`
window.__visChanges = [];
document.addEventListener('visibilitychange', () => {
window.__visChanges.push(document.visibilityState);
});
`);
w.show();
await expect(waitUntil(async () => await haveVisibilityState(v, 'visible'))).to.eventually.be.fulfilled();
// Give any delayed/queued occlusion updates time to fire.
await setTimeoutAsync(1500);
w.hide();
await expect(waitUntil(async () => await haveVisibilityState(v, 'hidden'))).to.eventually.be.fulfilled();
await setTimeoutAsync(1500);
const changes = await v.webContents.executeJavaScript('window.__visChanges');
// Expect exactly one 'visible' followed by one 'hidden'. Extra events
// would indicate the occlusion checker is causing spurious transitions.
expect(changes).to.deep.equal(['visible', 'hidden']);
});
});
describe('setBorderRadius', () => {

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