Compare commits

...

24 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
83 changed files with 2377 additions and 2002 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

@@ -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

@@ -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",

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

@@ -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

@@ -23,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",
@@ -43,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,
@@ -93,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",

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..455095a2cd63eabe4f267747070b443f0c49c1e8 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..455095a2cd63eabe4f267747070b443f
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..455095a2cd63eabe4f267747070b443f
// 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..455095a2cd63eabe4f267747070b443f
}
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..455095a2cd63eabe4f267747070b443f
}
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..455095a2cd63eabe4f267747070b443f
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,7 +332,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
ReleasePrinterQuery();
}
@@ -851,15 +886,33 @@ void PrintViewManagerBase::RemoveTestObserver(TestObserver& observer) {
@@ -851,15 +892,33 @@ void PrintViewManagerBase::RemoveTestObserver(TestObserver& observer) {
test_observers_.RemoveObserver(&observer);
}
@@ -360,7 +366,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
}
void PrintViewManagerBase::RenderFrameDeleted(
@@ -901,13 +954,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);
@@ -376,7 +382,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
}
void PrintViewManagerBase::OnDocDone(int job_id, PrintedDocument* document) {
@@ -921,18 +975,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.
@@ -405,7 +411,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
TerminatePrintJob(true);
}
@@ -942,7 +1004,7 @@ bool PrintViewManagerBase::RenderAllMissingPagesNow() {
@@ -942,7 +1010,7 @@ bool PrintViewManagerBase::RenderAllMissingPagesNow() {
// Is the document already complete?
if (print_job_->document() && print_job_->document()->IsComplete()) {
@@ -414,7 +420,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
return true;
}
@@ -995,7 +1057,10 @@ bool PrintViewManagerBase::SetupNewPrintJob(
@@ -995,7 +1063,10 @@ bool PrintViewManagerBase::SetupNewPrintJob(
// Disconnect the current `print_job_`.
auto weak_this = weak_ptr_factory_.GetWeakPtr();
@@ -426,7 +432,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
if (!weak_this)
return false;
@@ -1015,7 +1080,7 @@ bool PrintViewManagerBase::SetupNewPrintJob(
@@ -1015,7 +1086,7 @@ bool PrintViewManagerBase::SetupNewPrintJob(
#endif
print_job_->AddObserver(*this);
@@ -435,7 +441,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
return true;
}
@@ -1073,7 +1138,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.
@@ -444,7 +450,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
if (!analyzing_content_) {
UnregisterSystemPrintClient();
}
@@ -1083,6 +1148,11 @@ void PrintViewManagerBase::ReleasePrintJob() {
@@ -1083,6 +1154,11 @@ void PrintViewManagerBase::ReleasePrintJob() {
}
#endif
@@ -456,7 +462,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
if (!print_job_)
return;
@@ -1090,7 +1160,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());
@@ -465,7 +471,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
}
print_job_->RemoveObserver(*this);
@@ -1132,7 +1202,7 @@ bool PrintViewManagerBase::RunInnerMessageLoop() {
@@ -1132,7 +1208,7 @@ bool PrintViewManagerBase::RunInnerMessageLoop() {
}
bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
@@ -474,7 +480,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
return true;
if (!cookie) {
@@ -1155,7 +1225,7 @@ bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
@@ -1155,7 +1231,7 @@ bool PrintViewManagerBase::OpportunisticallyCreatePrintJob(int cookie) {
return false;
}
@@ -483,7 +489,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
// 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 +1356,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());
@@ -492,7 +498,7 @@ index aa79c324af2cec50019bca3bccff5d420fb30ffd..455095a2cd63eabe4f267747070b443f
std::unique_ptr<PrinterQuery> printer_query =
queue()->PopPrinterQuery(params->cookie);
if (!printer_query)
@@ -1296,10 +1368,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),
@@ -614,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(
@@ -626,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
@@ -675,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 @@
@@ -799,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,
@@ -835,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

@@ -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,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

@@ -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

@@ -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)

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();

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

@@ -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

@@ -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

@@ -210,7 +210,7 @@ void OpaqueFrameView::OnPaint(gfx::Canvas* canvas) {
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) {
@@ -342,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

@@ -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

@@ -1065,6 +1065,64 @@ describe('chromium features', () => {
expect(status).to.equal('granted');
});
it('concurrent getFileHandle calls on the same file do not stall', (done) => {
const writablePath = path.join(fixturesPath, 'file-system', 'test-perms.html');
const testDir = path.join(fixturesPath, 'file-system');
const testFile = path.join(testDir, 'test.txt');
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
sandbox: false
}
});
w.webContents.session.setPermissionRequestHandler((wc, permission, callback, details) => {
if (permission === 'fileSystem') {
const { href } = url.pathToFileURL(writablePath);
expect(details).to.deep.equal({
fileAccessType: 'readable',
isDirectory: false,
isMainFrame: true,
filePath: testFile,
requestingUrl: href
});
callback(true);
} else {
callback(false);
}
});
ipcMain.once('did-create-directory-handle', async () => {
const result = await w.webContents.executeJavaScript(`
new Promise(async (resolve, reject) => {
try {
const handles = await Promise.all([
handle.getFileHandle('test.txt'),
handle.getFileHandle('test.txt')
]);
resolve(handles.length === 2);
} catch (err) {
reject(err.message);
}
})
`, true);
expect(result).to.be.true();
done();
});
w.loadFile(writablePath);
w.webContents.once('did-finish-load', () => {
// @ts-expect-error Undocumented testing method.
clipboard._writeFilesForTesting([testDir]);
w.webContents.focus();
w.webContents.paste();
});
});
it('allows permission when trying to create a writable file handle', (done) => {
const writablePath = path.join(fixturesPath, 'file-system', 'test-perms.html');
const testFile = path.join(fixturesPath, 'file-system', 'test.txt');

View File

@@ -1,4 +1,4 @@
import { app, session, webFrameMain, BrowserWindow, ipcMain, WebContents, Extension, Session } from 'electron/main';
import { app, session, webFrameMain, BrowserWindow, ipcMain, WebContents, Extension, Session, ServiceWorkerInfo, ServiceWorkersRunningStatusChangedEventParams } from 'electron/main';
import { expect } from 'chai';
import * as WebSocket from 'ws';
@@ -10,7 +10,7 @@ import * as http from 'node:http';
import * as path from 'node:path';
import { emittedNTimes, emittedUntil } from './lib/events-helpers';
import { ifit, listen, waitUntil } from './lib/spec-helpers';
import { ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpers';
import { expectWarningMessages } from './lib/warning-helpers';
import { closeAllWindows, closeWindow, cleanupWebContents } from './lib/window-helpers';
@@ -816,6 +816,99 @@ describe('chrome extensions', () => {
expect(scope).equals(extension.url);
});
it('launches background service worker', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const launchPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'running') resolve();
});
});
const extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
await launchPromise;
const serviceWorkers = customSession.serviceWorkers.getAllRunning();
expect(Object.values(serviceWorkers).some(worker => worker.scope === extension.url)).equals(true);
});
it('launches background service worker when the extension is loaded again without restarting the app', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const extensionPath = path.join(fixtures, 'extensions', 'mv3-service-worker');
const isWorkerRunning = (extension: Extension) => {
const serviceWorkers = customSession.serviceWorkers.getAllRunning();
return Object.values(serviceWorkers).some(worker => worker.scope === extension.url);
};
const loadAndUnloadExtension = async () => {
const launchPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'running') resolve();
});
});
const extension = await customSession.extensions.loadExtension(extensionPath);
await launchPromise;
expect(isWorkerRunning(extension)).equals(true);
const stopPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'stopped') resolve();
});
});
customSession.extensions.removeExtension(extension.id);
await stopPromise;
expect(isWorkerRunning(extension)).equals(false);
};
for (let i = 0; i < 3; i++) {
await loadAndUnloadExtension();
}
});
// Regression test for https://github.com/electron/electron/issues/41613
it('launches background service worker after restarting the app', async () => {
const partition = `persist:${uuid.v4()}`;
const extensionPath = path.join(fixtures, 'extensions', 'mv3-service-worker');
const runRemoteControlApp = async () => {
const rc = await startRemoteControlApp();
const exitPromise = once(rc.process, 'exit');
const { workerScopes, extensionUrl } = await rc.remotely(async (partition: string, extensionPath: string) => {
const { session } = require('electron/main');
const { setTimeout } = require('node:timers/promises');
const customSession = session.fromPartition(partition);
const launchPromise = new Promise<void>(resolve => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }: ServiceWorkersRunningStatusChangedEventParams) => {
if (runningStatus === 'running') resolve();
});
});
const extension = await customSession.extensions.loadExtension(extensionPath);
await launchPromise;
const serviceWorkers = customSession.serviceWorkers.getAllRunning();
const workerScopes = Object.values(serviceWorkers).map(worker => (worker as ServiceWorkerInfo).scope);
const extensionUrl = extension.url;
// Give Chromium some time to update extensions::kPrefHasStartedServiceWorker on disk
await setTimeout(500);
global.setTimeout(() => require('electron').app.quit());
return { workerScopes, extensionUrl };
}, partition, extensionPath);
await exitPromise;
expect(workerScopes.includes(extensionUrl)).equals(true);
};
for (let i = 0; i < 3; i++) {
await runRemoteControlApp();
}
});
it('can run chrome extension APIs', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });

View File

@@ -0,0 +1,29 @@
const { app, WebContentsView } = require('electron');
const v8 = require('node:v8');
// Force V8 to schedule incremental-marking finalization steps as foreground
// tasks on every allocation. Those tasks run inside V8's
// DisallowJavascriptExecutionScope. Prior to the fix in
// shell/common/gin_helper/wrappable.cc, gin_helper::SecondWeakCallback would
// `delete` the Wrappable synchronously inside that scope, and Wrappables
// whose destructors emit JS events (WebContents emits 'will-destroy' /
// 'destroyed') would crash with "Invoke in DisallowJavascriptExecutionScope".
//
// Regression test for https://github.com/electron/electron/issues/47420.
v8.setFlagsFromString('--stress-incremental-marking');
app.whenReady().then(() => {
// Leak several WebContentsView instances so at least one wrapper's
// collection lands in a foreground GC task during app.quit()'s uv_run
// drain. Three is enough for the crash to reproduce 10/10 on a Linux
// testing build before the fix.
// eslint-disable-next-line no-new
new WebContentsView();
// eslint-disable-next-line no-new
new WebContentsView();
// eslint-disable-next-line no-new
new WebContentsView();
app.quit();
});

View File

@@ -0,0 +1,18 @@
/* eslint-disable no-self-compare, import/enforce-node-protocol-usage */
// Regression test fixture for the esbuild __toCommonJS identity break.
// Each pair must be the exact same object reference — see the
// "module identity" describe block in spec/modules-spec.ts. The comparisons
// are intentionally self-referential and intentionally use both the bare and
// `node:`-prefixed module names, which is why no-self-compare and
// import/enforce-node-protocol-usage are disabled for this file.
const { ipcRenderer } = require('electron');
ipcRenderer.send('require-identity', {
electron: require('electron') === require('electron'),
electronCommon: require('electron/common') === require('electron'),
events: require('events') === require('node:events'),
timers: require('timers') === require('node:timers'),
url: require('url') === require('node:url'),
ipcRenderer: require('electron').ipcRenderer === require('electron').ipcRenderer,
contextBridge: require('electron').contextBridge === require('electron').contextBridge
});

View File

@@ -284,6 +284,41 @@ describe('modules support', () => {
expect(result).to.equal('function');
});
});
// Regression test for the esbuild __toCommonJS identity break: esbuild's
// runtime helper for `require()`-of-ESM allocates a fresh wrapper on
// every call (see evanw/esbuild@f4ff26d3). The bundle driver patches it
// with a WeakMap-memoized variant so module identity matches webpack's
// __webpack_require__ cache semantics. Without the patch, every getter
// on `require('electron')` that resolves to a non-default-exporting ESM
// module would yield a fresh namespace on each access.
describe('module identity', () => {
it('returns the same object for repeated require("electron") accesses in the main process', () => {
const electron = require('electron');
for (const key of Object.keys(electron)) {
// Touching `electron.net` before app ready throws — handled by the
// sandboxed-preload identity test below; everything else must be
// strictly equal across two reads.
if (key === 'net') continue;
expect((electron as any)[key]).to.equal((electron as any)[key],
`require('electron').${key} must be identity-stable across reads`);
}
});
it('returns the same object for repeated require() in a sandboxed preload', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: { sandbox: true, contextIsolation: false, preload: path.join(fixtures, 'module', 'preload-require-identity.js') }
});
const result = once(w.webContents.ipc, 'require-identity');
await w.loadURL('about:blank');
const [, identities] = await result;
for (const [name, same] of Object.entries(identities)) {
expect(same).to.equal(true, `${name} must be identity-stable in sandboxed preload`);
}
w.destroy();
});
});
});
describe('esm', () => {

View File

@@ -2,8 +2,8 @@ const childProcess = require('node:child_process');
const path = require('node:path');
const typeCheck = () => {
const tscExec = path.resolve(require.resolve('typescript'), '../../bin/tsc');
const tscChild = childProcess.spawn(process.execPath, [tscExec, '--project', './ts-smoke/tsconfig.json'], {
const tsgoExec = path.resolve(require.resolve('@typescript/native-preview/package.json'), '..', 'bin', 'tsgo.js');
const tscChild = childProcess.spawn(process.execPath, [tsgoExec, '--project', './ts-smoke/tsconfig.json'], {
cwd: path.resolve(__dirname, '../')
});
tscChild.stdout.on('data', d => console.log(d.toString()));

View File

@@ -3,7 +3,7 @@
"compilerOptions": {
"rootDir": "default_app",
"module": "ESNext",
"moduleResolution": "node"
"moduleResolution": "bundler"
},
"include": [
"default_app",

View File

@@ -1,7 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "lib"
// rootDir intentionally omitted: some lib/ files pull type declarations
// out of ../third_party/electron_node via the `@node/*` path alias (see
// lib/node/asar-fs-wrapper.ts) and a rootDir of 'lib' rejects those
// as outside-rootDir imports.
},
"include": [
"lib",

View File

@@ -10,14 +10,12 @@
"sourceMap": true,
"experimentalDecorators": true,
"strict": true,
"baseUrl": ".",
"allowJs": true,
"noUnusedLocals": true,
"outDir": "ts-gen",
"typeRoots" : ["./node_modules/@types", "./spec/node_modules/@types"],
"paths": {
"@electron/internal/*": ["lib/*"],
"@node/*": ["../third_party/electron_node/*"]
"@electron/internal/*": ["./lib/*"]
}
},
"exclude": [

View File

@@ -1,7 +1,37 @@
/// <reference types="webpack/module" />
declare const BUILDFLAG: (flag: boolean) => boolean;
// esbuild build/esbuild/bundle.js rewrites calls to this identifier into
// literal `require()` calls so that consumers can reach Node internal modules
// (e.g. 'internal/modules/helpers') without the bundler trying to resolve
// them. Overloads below pin the known internal-module IDs to narrow types;
// the final catch-all signature keeps less-common paths usable at the cost
// of no static type information.
interface NodeInternalModules {
'internal/modules/helpers': {
makeRequireFunction: (mod: NodeModule) => NodeRequire;
};
'internal/modules/run_main': {
runEntryPointWithESMLoader: (cb: (cascadedLoader: any) => any) => Promise<void>;
};
'internal/fs/utils': {
getValidatedPath: (path: any, ...args: any[]) => string;
getOptions: (options: any, defaultOptions?: any) => any;
getDirent: (path: string, name: string | Buffer, type: number, callback?: Function) => any;
};
'internal/util': {
assignFunctionName: (name: string | symbol, fn: Function) => Function;
};
'internal/validators': {
validateBoolean: (value: unknown, name: string) => asserts value is boolean;
validateFunction: (value: unknown, name: string) => void;
};
'internal/url': {
URL: typeof URL;
};
}
declare function __non_webpack_require__<K extends keyof NodeInternalModules>(id: K): NodeInternalModules[K];
declare function __non_webpack_require__(id: string): unknown;
declare namespace NodeJS {
interface ModuleInternal extends NodeJS.Module {
new(id: string, parent?: NodeJS.Module | null): NodeJS.Module;
@@ -282,7 +312,7 @@ declare namespace NodeJS {
}
}
declare module NodeJS {
declare namespace NodeJS {
interface Global {
require: NodeRequire;
module: NodeModule;

View File

@@ -93,7 +93,6 @@ declare namespace Electron {
getLastWebPreferences(): Electron.WebPreferences | null;
_getProcessMemoryInfo(): Electron.ProcessMemoryInfo;
_getPreloadScript(): Electron.PreloadScript | null;
equal(other: WebContents): boolean;
browserWindowOptions: BrowserWindowConstructorOptions;
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null;
_callWindowOpenHandler(event: any, details: Electron.HandlerDetails): {browserWindowConstructorOptions: Electron.BrowserWindowConstructorOptions | null, outlivesOpener: boolean, createWindow?: Electron.CreateWindowFunction};
@@ -102,7 +101,6 @@ declare namespace Electron {
_sendInternal(channel: string, ...args: any[]): void;
_printToPDF(options: any): Promise<Buffer>;
_print(options: any, callback?: (success: boolean, failureReason: string) => void): void;
_getPrintersAsync(): Promise<Electron.PrinterInfo[]>;
_init(): void;
_getNavigationEntryAtIndex(index: number): Electron.NavigationEntry | null;
_getActiveIndex(): number;

1924
yarn.lock

File diff suppressed because it is too large Load Diff