build: drop a pile of unused / replaceable dev dependencies (#52744)

* build: drop unused dev dependencies and the abstract-socket native module

Removes dependencies that nothing in the tree uses any more:

* sinon / @types/sinon: no spec imports them.
* check-for-leaks: only wired to `prepack`, and the root package is
  private and never packed.
* null-loader: the only rule using it matched against an always-empty
  `ignoredModules` list in the webpack config.
* @types/uuid: uuid ships its own types since well before the version
  we depend on.
* q, coffeescript, mkdirp: each backed a single test of third-party
  module behaviour. The `require` app-directory test now uses
  dirty-chai (any module that only lives in spec/node_modules works,
  it is exercising module.paths, not the module), and the mkdirp asar
  test is replaced by a `fs.mkdirSync(..., { recursive: true })` case
  next to the existing mkdirSync test.

Also drops abstract-socket. dbus-native only used it to connect to
`unix:abstract=` bus addresses, and Node.js has supported abstract
sockets natively via a leading NUL byte since 20.8, so dbus-native is
now patched to use net.createConnection instead. That lets us delete
the abstract-socket patch, its resolutions entry, the dependenciesMeta
`built` opt-in, the packageExtensions block in .yarnrc.yml and the
Linux-only `npm rebuild abstract-socket` step in the spec runner.
abstract-socket itself still appears in the lockfile as dbus-native's
optional dependency (patches cannot change resolution), but it is no
longer built or required by anything.

No-Verification-Needed: dependency, test and build-tooling changes only; no product source touched

* build: replace uuid, basic-auth and temp with Node.js built-ins

* uuid -> crypto.randomUUID() across the specs.
* basic-auth -> a small parseBasicAuth() helper in spec/lib/net-helpers,
  used by the two specs that need to read Basic credentials.
* temp -> fs.mkdtempSync in release.ts and in the asar copyFile specs.

Also removes script/doc-only-change.js, whose last caller went away with
the AppVeyor configs.

No-Verification-Needed: spec, release-tooling and manifest changes only; no product source touched

* build: use webpack's BannerPlugin instead of wrapper-webpack-plugin

wrapper-webpack-plugin is a webpack 4 era plugin that still hooks the
deprecated optimizeChunkAssets hook. webpack 5's built-in BannerPlugin
can prepend a raw header and (with `footer: true`) append a raw footer,
and instances run in registration order, so the profiling wrapper and
the try/catch wrapper nest exactly as before. The only difference in the
emitted bundles is a newline between each wrapper and the bundle body.

No-Verification-Needed: webpack build config only; emitted bundle diffed against the previous plugin's output (whitespace-only), production output checked for nesting and syntax

* build: vendor the default app's octicons

The default app uses five icons and an 88 byte stylesheet out of
@primer/octicons, and pulling them out of node_modules was the only
place the GN build reached into node_modules for shipped content. Copy
the files (MIT licensed, notice included alongside) into
default_app/octicon so they are plain static sources like the rest of
the default app, and drop the dependency and the extra copy target. The
files end up at the same paths inside default_app.asar as before.

No-Verification-Needed: vendored files are byte-identical to the octicon files the existing build already ships at the same asar paths

* build: drop got and dedupe @electron/fiddle-core

The release scripts were the only direct users of got, and every call
was a plain GET/HEAD/POST that global fetch covers: HEAD requests use
`redirect: 'manual'` in place of `followRedirect: false`, and the
non-2xx handling that previously relied on `throwHttpErrors: false` now
checks `response.status` explicitly. The asset manifest in release.ts is
untouched.

got@11 was also still being kept alive by @electron/fiddle-core@1, which
the root package depended on while .github/workflows already used ^2.
Aligning the root on ^2 removes the second copy of fiddle-core and
@electron/get along with the whole got@11 dependency chain (41 packages).
fiddle-core 2 is ESM-only; the CommonJS spec runner loads it through
require(esm), which is available on the Node 22.21 CI uses and anything
newer than 22.12.

No-Verification-Needed: release tooling, spec runner and manifest changes only; no product source touched

* test: replace express in the autoUpdater specs with a small router

The two autoUpdater specs were the only users of express, and between
them they use `use()`, `get()` with one `:param` route, `res.status()`,
`res.send()`, `res.json()` and `res.download()`. spec/lib/http-server-
helpers.ts provides exactly that surface on top of node:http, with file
responses still going through `send` (what express uses internally) so
HEAD, Range and content-type behaviour towards the updaters is unchanged.
`send` is bumped to the 1.x line express 5 was already using, since 0.x
serves .msix files as application/octet-stream rather than
application/msix.

Removes express, @types/express and 43 transitive packages.

No-Verification-Needed: spec-only change; helper exercised over HTTP for GET/HEAD/Range/json/204/404 routing

* test: depend on resedit directly instead of @electron/packager

The asar integrity spec only used @electron/packager for its resedit
wrapper, and only the branch of that wrapper which embeds the integrity
resource on Windows. Do that directly with resedit (the library the
wrapper is built on), which is a dozen lines, and drop packager along
with the 24 signing/notarizing/downloading packages it brings in.

No-Verification-Needed: spec-only change; resource embedding round-tripped against a real PE locally

* test: replace the last temp usage in the protocol code cache spec

The `temp` package was dropped from the root package.json but
api-protocol-spec.ts still required it inline, so every test shard
that loads that file failed at spec-runner startup.

No-Verification-Needed: test-only change
This commit is contained in:
Samuel Attard authored and GitHub committed 2026-09-01 02:12:57 +00:00
1 parent d6529d65e5
commit 92d1a918d1
37 files changed
+515 -2237

No files matched your search

+1 -1
View File
@@ -102,7 +102,7 @@ When working on the `roller/chromium/main` branch for Chromium upgrades, use `e
### Testing
- Framework: Mocha + Chai + Sinon
- Framework: Mocha + Chai
- Test helpers in `spec/lib/` (e.g., `spec-helpers.ts`, `window-helpers.ts`)
- Use `defer()` from spec-helpers for cleanup, `closeAllWindows()` for window teardown
- Tests import from `electron/main` or `electron/renderer`
@@ -1,351 +0,0 @@
diff --git a/binding.gyp b/binding.gyp
index 61f1d0bd5b2fe4f43874f1115cd2fa0dced03207..13e34fd6a17d698449063f22456f43e445246959 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -1,10 +1,17 @@
{
'targets': [
{
- 'target_name': 'abstract_socket',
- 'sources': [ 'src/abstract_socket.cc' ],
+ 'target_name': 'bindings',
+ 'conditions': [
+ ['OS=="linux"', {
+ 'cflags!': [ '-fno-exceptions' ],
+ 'cflags_cc!': [ '-fno-exceptions' ],
+ 'defines': [ '_GNU_SOURCE=1' ],
+ 'sources': [ 'src/abstract_socket.cc' ],
+ }],
+ ],
'include_dirs': [
- '<!(node -e "require(\'nan\')")'
+ '<!(node -p "require(\'node-addon-api\').include_dir")',
]
}
]
diff --git a/lib/abstract_socket.js b/lib/abstract_socket.js
index a2434860d0f11d73beec855e42ad762a06f58734..557bc0dd1196a871a72a190823530ff938c40808 100644
--- a/lib/abstract_socket.js
+++ b/lib/abstract_socket.js
@@ -1,21 +1,18 @@
'use strict';
+if (process.platform !== 'linux') {
+ throw new Error('This module is only available on Linux');
+}
+
const net = require('net');
-const binding = require('bindings')('abstract_socket.node');
+const binding = require('bindings')();
const socket = binding.socket;
const bind = binding.bind;
const connect = binding.connect;
const close = binding.close;
-
-function errnoException(errorno, syscall) {
- // TODO: having the errno message here would be nice
- const e = new Error(syscall + ' ' + errorno);
- e.errno = e.code = errorno;
- e.syscall = syscall;
- return e;
-}
+const errnoException = require('util')._errnoException;
class AbstractSocketServer extends net.Server {
diff --git a/package.json b/package.json
index ea041a3855a38c050d8a0f20d88384f8aa78a1af..f2adb7f2092eca00aeb036b524ce3304f072c98f 100644
--- a/package.json
+++ b/package.json
@@ -22,14 +22,11 @@
},
"dependencies": {
"bindings": "^1.2.1",
- "nan": "^2.0.9"
+ "node-addon-api": "8.0.0"
},
"devDependencies": {
"should": "8.2.x",
"mocha": "2.4.x"
- },
- "os": [
- "linux"
- ]
+ }
}
diff --git a/src/abstract_socket.cc b/src/abstract_socket.cc
index 9370b9835efcb3d09589a3910bab40a20efa5216..c9e55af65aece42feb29497ffe5cc835562bc602 100644
--- a/src/abstract_socket.cc
+++ b/src/abstract_socket.cc
@@ -1,13 +1,10 @@
-// -D_GNU_SOURCE makes SOCK_NONBLOCK etc. available on linux
-#undef _GNU_SOURCE
-#define _GNU_SOURCE
-
#if !defined(__linux__)
# error "Only Linux is supported"
#endif
-#include <nan.h>
+#include <napi.h>
+#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
@@ -17,76 +14,33 @@
namespace {
-using v8::FunctionTemplate;
-using v8::Local;
-using v8::Object;
-using v8::String;
-using v8::Value;
-
-
-#if !defined(SOCK_NONBLOCK)
-void SetNonBlock(int fd) {
- int flags;
- int r;
-
- flags = fcntl(fd, F_GETFL);
- assert(flags != -1);
-
- r = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
- assert(r != -1);
-}
-#endif
-
-
-#if !defined(SOCK_CLOEXEC)
-void SetCloExec(int fd) {
- int flags;
- int r;
-
- flags = fcntl(fd, F_GETFD);
- assert(flags != -1);
-
- r = fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
- assert(r != -1);
-}
-#endif
+using Napi::FunctionReference;
+using Napi::Object;
+using Napi::String;
+using Napi::Value;
-NAN_METHOD(Socket) {
- Nan::HandleScope scope;
+Napi::Value Socket(const Napi::CallbackInfo& info) {
+ Napi::Env env(info.Env());
int fd;
int type;
assert(info.Length() == 0);
type = SOCK_STREAM;
-#if defined(SOCK_NONBLOCK)
- type |= SOCK_NONBLOCK;
-#endif
-#if defined(SOCK_CLOEXEC)
- type |= SOCK_CLOEXEC;
-#endif
+ type |= SOCK_NONBLOCK | SOCK_CLOEXEC;
fd = socket(AF_UNIX, type, 0);
if (fd == -1) {
fd = -errno;
- goto out;
}
-#if !defined(SOCK_NONBLOCK)
- SetNonBlock(fd);
-#endif
-#if !defined(SOCK_CLOEXEC)
- SetCloExec(fd);
-#endif
-
-out:
- info.GetReturnValue().Set(fd);
+ return Napi::Number::New(env, fd);
}
-NAN_METHOD(Bind) {
- Nan::HandleScope scope;
+Napi::Value Bind(const Napi::CallbackInfo& info) {
+ Napi::Env env(info.Env());
sockaddr_un s;
socklen_t namelen;
int err;
@@ -95,10 +49,10 @@ NAN_METHOD(Bind) {
assert(info.Length() == 2);
- fd = info[0]->Int32Value();
- String::Utf8Value path(info[1]);
+ fd = info[0].As<Napi::Number>().Int32Value();
+ std::string path = info[1].As<Napi::String>();
- if ((*path)[0] != '\0') {
+ if ((path)[0] != '\0') {
err = -EINVAL;
goto out;
}
@@ -110,7 +64,7 @@ NAN_METHOD(Bind) {
}
memset(&s, 0, sizeof s);
- memcpy(s.sun_path, *path, len);
+ memcpy(s.sun_path, path.c_str(), len);
s.sun_family = AF_UNIX;
namelen = offsetof(struct sockaddr_un, sun_path) + len;
@@ -119,12 +73,12 @@ NAN_METHOD(Bind) {
err = -errno;
out:
- info.GetReturnValue().Set(err);
+ return Napi::Number::New(env, err);
}
-NAN_METHOD(Connect) {
- Nan::HandleScope scope;
+Napi::Value Connect(const Napi::CallbackInfo& info) {
+ Napi::Env env(info.Env());
sockaddr_un s;
socklen_t namelen;
int err;
@@ -133,10 +87,10 @@ NAN_METHOD(Connect) {
assert(info.Length() == 2);
- fd = info[0]->Int32Value();
- String::Utf8Value path(info[1]);
+ fd = info[0].As<Napi::Number>().Int32Value();
+ std::string path = info[1].As<Napi::String>();
- if ((*path)[0] != '\0') {
+ if ((path)[0] != '\0') {
err = -EINVAL;
goto out;
}
@@ -148,7 +102,7 @@ NAN_METHOD(Connect) {
}
memset(&s, 0, sizeof s);
- memcpy(s.sun_path, *path, len);
+ memcpy(s.sun_path, path.c_str(), len);
s.sun_family = AF_UNIX;
namelen = offsetof(struct sockaddr_un, sun_path) + len;
@@ -157,59 +111,70 @@ NAN_METHOD(Connect) {
err = -errno;
out:
- info.GetReturnValue().Set(err);
+ return Napi::Number::New(env, err);
}
-NAN_METHOD(Close) {
- Nan::HandleScope scope;
+Napi::Value Close(const Napi::CallbackInfo& info) {
+ Napi::Env env(info.Env());
int err;
int fd;
assert(info.Length() == 1);
- fd = info[0]->Int32Value();
-
- // Suppress EINTR and EINPROGRESS. EINTR means that the close() system call
- // was interrupted by a signal. According to POSIX, the file descriptor is
- // in an undefined state afterwards. It's not safe to try closing it again
- // because it may have been closed, despite the signal. If we call close()
- // again, then it would either:
+ fd = info[0].As<Napi::Number>().Int32Value();
+
+ // POSIX 2008 states that it unspecified what the state of a file descriptor
+ // is if close() is interrupted by a signal and fails with EINTR. This is a
+ // problem for multi-threaded programs since if the fd was actually closed
+ // it may already be reused by another thread hence it is unsafe to attempt
+ // to close it again. In 2012, POSIX approved a clarification that aimed
+ // to deal with this mess: http://austingroupbugs.net/view.php?id=529#c1200
+ //
+ // The short summary is that if the fd is never valid after close(), as is
+ // the case in Linux, then <unistd.h> should add:
//
- // a) fail with EBADF, or
+ // #define POSIX_CLOSE_RESTART 0
//
- // b) close the wrong file descriptor if another thread or a signal handler
- // has reused it in the mean time.
+ // and the new posix_close() should be implemented as something like:
//
- // Neither is what we want but scenario B is particularly bad. Not retrying
- // the close() could, in theory, lead to file descriptor leaks but, in
- // practice, operating systems do the right thing and close the file
- // descriptor, regardless of whether the operation was interrupted by
- // a signal.
+ // int posix_close(int fd, int flags) {
+ // int r = close(fd);
+ // if (r < 0 && errno == EINTR)
+ // return 0 or set errno to EINPROGRESS;
+ // return r;
+ // }
//
- // EINPROGRESS is benign. It means the close operation was interrupted but
- // that the file descriptor has been closed or is being closed in the
- // background. It's informative, not an error.
+ // In contrast, on systems where EINTR means the close() didn't happen (like
+ // HP-UX), POSIX_CLOSE_RESTART should be non-zero and if passed as flag to
+ // posix_close() it should automatically retry close() on EINTR.
+ //
+ // Of course this is nice and all, but apparently adding one constant and
+ // a trivial wrapper was way too much effort for the glibc project:
+ // https://sourceware.org/bugzilla/show_bug.cgi?id=16302
+ //
+ // But since we actually only care about Linux, we don't have to worry about
+ // all this anyway. close() always means the fd is gone, even if an error
+ // occurred. This elevates EINTR to the status of real error, since it
+ // implies behaviour associated with close (e.g. flush) was aborted and can
+ // not be retried since the fd is gone.
+
err = 0;
if (close(fd))
- if (errno != EINTR && errno != EINPROGRESS)
err = -errno;
- info.GetReturnValue().Set(err);
+ return Napi::Number::New(env, err);
}
-void Initialize(Local<Object> target) {
- target->Set(Nan::New("socket").ToLocalChecked(),
- Nan::New<FunctionTemplate>(Socket)->GetFunction());
- target->Set(Nan::New("bind").ToLocalChecked(),
- Nan::New<FunctionTemplate>(Bind)->GetFunction());
- target->Set(Nan::New("connect").ToLocalChecked(),
- Nan::New<FunctionTemplate>(Connect)->GetFunction());
- target->Set(Nan::New("close").ToLocalChecked(),
- Nan::New<FunctionTemplate>(Close)->GetFunction());
+Napi::Object Initialize(Napi::Env env, Napi::Object exports) {
+ exports.Set("socket", Napi::Function::New(env, Socket));
+ exports.Set("bind", Napi::Function::New(env, Bind));
+ exports.Set("connect", Napi::Function::New(env, Connect));
+ exports.Set("close", Napi::Function::New(env, Close));
+ return exports;
}
} // anonymous namespace
-NODE_MODULE(abstract_socket, Initialize)
+NODE_API_MODULE(abstract_socket, Initialize)
@@ -0,0 +1,33 @@
diff --git a/index.js b/index.js
index 59f2d2be89a19ced84e5705af7247cd1aab6e136..e77b9909637f7a732ec912520c0de2e5351357f0 100644
--- a/index.js
+++ b/index.js
@@ -40,10 +40,10 @@ function createStream(opts) {
return net.createConnection(port, host);
case 'unix':
if (params.socket) return net.createConnection(params.socket);
- if (params.abstract) {
- var abs = require('abstract-socket');
- return abs.connect('\u0000' + params.abstract);
- }
+ // Node.js >= 20.8 supports Linux abstract sockets natively via a
+ // leading NUL byte, so the optional native abstract-socket module
+ // is not needed.
+ if (params.abstract) return net.createConnection('\u0000' + params.abstract);
if (params.path) return net.createConnection(params.path);
throw new Error(
"not enough parameters for 'unix' connection - you need to specify 'socket' or 'abstract' or 'path' parameter"
diff --git a/package.json b/package.json
index 2519ac38fad44569cb8a156a91af25a70c8166a5..f22f0318d8c469714153237bbe0a7e0ca83faacc 100644
--- a/package.json
+++ b/package.json
@@ -46,9 +46,6 @@
"safe-buffer": "^5.1.1",
"xml2js": "^0.4.17"
},
- "optionalDependencies": {
- "abstract-socket": "^2.0.0"
- },
"devDependencies": {
"eslint": "^5.0.0",
"eslint-config-prettier": "^3.0.0",
-5
View File
@@ -2,11 +2,6 @@ enableScripts: false
nmHoistingLimits: workspaces
packageExtensions:
abstract-socket@*:
dependencies:
node-addon-api: 8.0.0
nodeLinker: node-modules
npmMinimalAgeGate: 10080
+1 -8
View File
@@ -344,22 +344,15 @@ copy("default_app_static") {
outputs = [ "$target_gen_default_app_js/{{source}}" ]
}
copy("default_app_octicon_deps") {
sources = filenames.default_app_octicon_sources
outputs = [ "$target_gen_default_app_js/electron/default_app/octicon/{{source_file_part}}" ]
}
asar("default_app_asar") {
deps = [
":default_app_js",
":default_app_octicon_deps",
":default_app_static",
]
root = "$target_gen_default_app_js/electron/default_app"
sources = get_target_outputs(":default_app_js") +
get_target_outputs(":default_app_static") +
get_target_outputs(":default_app_octicon_deps")
get_target_outputs(":default_app_static")
outputs = [ "$root_out_dir/resources/default_app.asar" ]
}
+1 -1
View File
@@ -164,7 +164,7 @@ If no artifact exists (e.g. the 3-way merge itself failed), fall back to `e sync
e test # Run full test suite
```
**Test frameworks:** Mocha, Chai, Sinon
**Test frameworks:** Mocha, Chai
## Build Configuration
+16 -15
View File
@@ -1,12 +1,19 @@
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, '../..');
// Wraps the emitted bundle in `header` / `footer`. BannerPlugin instances run
// in registration order, so wrapping twice nests the later wrapper outside the
// earlier one.
const wrapBundle = (header, footer) => [
new webpack.BannerPlugin({ banner: header, raw: true }),
new webpack.BannerPlugin({ banner: footer, raw: true, footer: true })
];
class AccessDependenciesPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('AccessDependenciesPlugin', (compilation) => {
@@ -62,8 +69,6 @@ module.exports = ({
}
}
const ignoredModules = [];
const plugins = [];
if (onlyPrintingGraph) {
@@ -100,29 +105,29 @@ module.exports = ({
if (wrapInitWithProfilingTimeout) {
plugins.push(
new WrapperPlugin({
header: 'function ___electron_webpack_init__() {',
footer: `
...wrapBundle(
'function ___electron_webpack_init__() {',
`
};
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: `
...wrapBundle(
'try {',
`
} catch (err) {
console.error('Electron ${outputFilename} script failed to run');
console.error(err);
}`
})
)
);
}
@@ -154,10 +159,6 @@ if ((globalThis.process || binding.process).argv.includes("--profile-electron-in
},
module: {
rules: [
{
test: (moduleName) => !onlyPrintingGraph && ignoredModules.includes(moduleName),
loader: 'null-loader'
},
{
test: /\.ts$/,
loader: 'ts-loader',
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M0 3.75A.75.75 0 01.75 3h7.497c1.566 0 2.945.8 3.751 2.014A4.496 4.496 0 0115.75 3h7.5a.75.75 0 01.75.75v15.063a.75.75 0 01-.755.75l-7.682-.052a3 3 0 00-2.142.878l-.89.891a.75.75 0 01-1.061 0l-.902-.901a3 3 0 00-2.121-.879H.75a.75.75 0 01-.75-.75v-15zm11.247 3.747a3 3 0 00-3-2.997H1.5V18h6.947a4.5 4.5 0 012.803.98l-.003-11.483zm1.503 11.485V7.5a3 3 0 013-3h6.75v13.558l-6.927-.047a4.5 4.5 0 00-2.823.971z"/></svg>

After

Width:  |  Height:  |  Size: 527 B

+5
View File
@@ -0,0 +1,5 @@
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10.3 8.24a.75.75 0 01-.04 1.06L7.352 12l2.908 2.7a.75.75 0 11-1.02 1.1l-3.5-3.25a.75.75 0 010-1.1l3.5-3.25a.75.75 0 011.06.04zm3.44 1.06a.75.75 0 111.02-1.1l3.5 3.25a.75.75 0 010 1.1l-3.5 3.25a.75.75 0 11-1.02-1.1l2.908-2.7-2.908-2.7z"/><path fill-rule="evenodd" d="M2 3.75C2 2.784 2.784 2 3.75 2h16.5c.966 0 1.75.784 1.75 1.75v16.5A1.75 1.75 0 0120.25 22H3.75A1.75 1.75 0 012 20.25V3.75zm1.75-.25a.25.25 0 00-.25.25v16.5c0 .138.112.25.25.25h16.5a.25.25 0 00.25-.25V3.75a.25.25 0 00-.25-.25H3.75z"/></svg>

After

Width:  |  Height:  |  Size: 598 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M3.75 3.75c0 .844.279 1.623.75 2.25H2.75A1.75 1.75 0 001 7.75v2.5c0 .698.409 1.3 1 1.582v8.418c0 .966.784 1.75 1.75 1.75h16.5A1.75 1.75 0 0022 20.25v-8.418c.591-.281 1-.884 1-1.582v-2.5A1.75 1.75 0 0021.25 6H19.5a3.75 3.75 0 00-3-6c-1.456 0-3.436.901-4.5 3.11C10.936.901 8.955 0 7.5 0a3.75 3.75 0 00-3.75 3.75zM11.22 6c-.287-3.493-2.57-4.5-3.72-4.5a2.25 2.25 0 000 4.5h3.72zm9.28 6v8.25a.25.25 0 01-.25.25h-7.5V12h7.75zm-9.25 8.5V12H3.5v8.25c0 .138.112.25.25.25h7.5zm10-10a.25.25 0 00.25-.25v-2.5a.25.25 0 00-.25-.25h-8.5v3h8.5zm-18.5 0h8.5v-3h-8.5a.25.25 0 00-.25.25v2.5c0 .138.112.25.25.25zm16-6.75A2.25 2.25 0 0116.5 6h-3.72c.287-3.493 2.57-4.5 3.72-4.5a2.25 2.25 0 012.25 2.25z"/></svg>

After

Width:  |  Height:  |  Size: 802 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>

After

Width:  |  Height:  |  Size: 689 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12.672.668a.75.75 0 00-1.345 0L8.27 6.865l-6.838.994a.75.75 0 00-.416 1.279l4.948 4.823-1.168 6.811a.75.75 0 001.088.791L12 18.347l6.117 3.216a.75.75 0 001.088-.79l-1.168-6.812 4.948-4.823a.75.75 0 00-.416-1.28l-6.838-.993L12.672.668z"/></svg>

After

Width:  |  Height:  |  Size: 356 B

+7 -9
View File
@@ -8,19 +8,17 @@ filenames = {
default_app_static_sources = [
"default_app/icon.png",
"default_app/index.html",
"default_app/octicon/LICENSE",
"default_app/octicon/book-24.svg",
"default_app/octicon/build.css",
"default_app/octicon/code-square-24.svg",
"default_app/octicon/gift-24.svg",
"default_app/octicon/mark-github-16.svg",
"default_app/octicon/star-fill-24.svg",
"default_app/package.json",
"default_app/styles.css",
]
default_app_octicon_sources = [
"node_modules/@primer/octicons/build/build.css",
"node_modules/@primer/octicons/build/svg/book-24.svg",
"node_modules/@primer/octicons/build/svg/code-square-24.svg",
"node_modules/@primer/octicons/build/svg/gift-24.svg",
"node_modules/@primer/octicons/build/svg/mark-github-16.svg",
"node_modules/@primer/octicons/build/svg/star-fill-24.svg",
]
lib_sources_linux = [
"shell/browser/browser_linux.cc",
"shell/browser/lib/power_observer_linux.cc",
+1 -13
View File
@@ -8,31 +8,26 @@
"@datadog/datadog-ci": "^5.17.0",
"@electron/asar": "^4.3.0",
"@electron/docs-parser": "^3.0.1",
"@electron/fiddle-core": "^1.3.4",
"@electron/fiddle-core": "^2.0.1",
"@electron/github-app-auth": "^3.2.0",
"@electron/lint-roller": "^3.2.0",
"@electron/typescript-definitions": "^9.2.0",
"@hurdlegroup/robotjs": "^0.12.3",
"@octokit/rest": "^22.0.1",
"@primer/octicons": "^10.0.0",
"@sentry/cli": "3.6.2",
"@types/minimist": "^1.2.5",
"@types/node": "^24.9.0",
"@types/semver": "^7.5.8",
"@types/stream-json": "^1.7.8",
"@types/temp": "^0.9.4",
"@xmldom/xmldom": "^0.9.10",
"buffer": "^6.0.3",
"check-for-leaks": "^1.2.1",
"events": "^3.2.0",
"folder-hash": "^4.1.2",
"got": "^11.8.5",
"husky": "^9.1.7",
"lint-staged": "^17.0.8",
"markdownlint-cli2": "^0.18.0",
"minimist": "^1.2.8",
"node-gyp": "^12.3.0",
"null-loader": "^4.0.1",
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"pre-flight": "^2.0.0",
@@ -40,14 +35,12 @@
"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,
@@ -81,7 +74,6 @@
"precommit": "lint-staged",
"preinstall": "node -e 'process.exit(0)'",
"pretest": "npm run create-typescript-definitions",
"prepack": "check-for-leaks",
"prepare": "husky",
"repl": "node ./script/start.js --interactive",
"start": "node ./script/start.js",
@@ -141,7 +133,6 @@
},
"resolutions": {
"dbus-native/xml2js": "0.5.0",
"abstract-socket": "patch:abstract-socket@npm%3A2.0.0#~/.yarn/patches/abstract-socket-npm-2.0.0-b025d8ca5a.patch",
"minimist@npm:~0.0.1": "0.2.4",
"put": "npm:@nornagon/put@0.0.8",
"get-intrinsic": "^1.3.0",
@@ -159,9 +150,6 @@
"dependenciesMeta": {
"@sentry/cli": {
"built": true
},
"abstract-socket": {
"built": true
}
}
}
-54
View File
@@ -1,54 +0,0 @@
const { Octokit } = require('@octokit/rest');
const minimist = require('minimist');
const args = minimist(process.argv.slice(2));
const octokit = new Octokit();
async function checkIfDocOnlyChange() {
let { prNumber, prURL } = args;
if (prNumber || prURL) {
try {
// extract the PR number from the PR URL.
if (!prNumber || isNaN(prNumber)) {
if (args.prURL) {
prNumber = prURL.split('/').pop();
}
}
const filesChanged = await octokit.paginate(
octokit.pulls.listFiles.endpoint.merge({
owner: 'electron',
repo: 'electron',
pull_number: prNumber,
per_page: 100
})
);
console.log(
'Changed Files: ',
filesChanged.map((fileInfo) => fileInfo.filename)
);
const nonDocChange =
filesChanged.length === 0 ||
filesChanged.find(({ filename }) => {
const fileDirs = filename.split('/');
if (fileDirs[0] !== 'docs') return true;
return false;
});
process.exit(nonDocChange ? 1 : 0);
} catch (error) {
console.error('Error getting list of files changed: ', error);
process.exit(-1);
}
} else {
console.error(`Check if only the docs were changed for a commit.
Usage: doc-only-change.js --prNumber=PR_NUMBER || --prURL=PR_URL`);
process.exit(-1);
}
}
checkIfDocOnlyChange();
+12 -9
View File
@@ -1,5 +1,4 @@
import { createTokenAuth } from '@octokit/auth-token';
import got from 'got';
import { ElectronReleaseRepo } from './types';
@@ -14,11 +13,15 @@ async function getActionsIdToken(): Promise<string> {
'ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN not set — the job needs `permissions: id-token: write` to mint an OIDC token for the sudowoodo exchange'
);
}
const { value } = await got(ACTIONS_ID_TOKEN_REQUEST_URL + '&audience=' + SUDOWOODO_OIDC_AUDIENCE, {
const resp = await fetch(ACTIONS_ID_TOKEN_REQUEST_URL + '&audience=' + SUDOWOODO_OIDC_AUDIENCE, {
headers: {
authorization: 'Bearer ' + ACTIONS_ID_TOKEN_REQUEST_TOKEN
}
}).json<{ value: string }>();
});
if (!resp.ok) {
throw new Error(`Failed to request an Actions OIDC token, got status: ${resp.status}`);
}
const { value } = (await resp.json()) as { value: string };
return value;
}
@@ -32,18 +35,18 @@ async function ensureToken(repo: ElectronReleaseRepo) {
if (SUDOWOODO_EXCHANGE_URL) {
const idToken = await getActionsIdToken();
const resp = await got.post(SUDOWOODO_EXCHANGE_URL + '?repo=' + repo, {
const resp = await fetch(SUDOWOODO_EXCHANGE_URL + '?repo=' + repo, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + idToken
},
throwHttpErrors: false
}
});
if (resp.statusCode !== 200) {
console.error('bad sudowoodo exchange response code:', resp.statusCode);
if (resp.status !== 200) {
console.error('bad sudowoodo exchange response code:', resp.status);
throw new Error('non-200 status code received from sudowoodo exchange function');
}
try {
return JSON.parse(resp.body).token;
return JSON.parse(await resp.text()).token;
} catch {
// Swallow as the error could include the token
throw new Error('Unexpected error parsing sudowoodo exchange response');
-13
View File
@@ -613,19 +613,6 @@ async function installSpecModules(dir) {
console.log(`${fail} Failed to yarn install in '${dir}'`);
process.exit(1);
}
if (process.platform === 'linux') {
const { status: rebuildStatus } = childProcess.spawnSync('npm', ['rebuild', 'abstract-socket'], {
env,
cwd: dir,
stdio: 'inherit',
shell: process.platform === 'win32'
});
if (rebuildStatus !== 0) {
console.log(`${fail} Failed to rebuild abstract-socket native module`);
process.exit(1);
}
}
}
function getSpecHash() {
+9 -9
View File
@@ -1,11 +1,10 @@
import { autoUpdater, systemPreferences } from 'electron';
import { expect } from 'chai';
import express from 'express';
import psList from 'ps-list';
import * as uuid from 'uuid';
import * as cp from 'node:child_process';
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as http from 'node:http';
import { AddressInfo } from 'node:net';
@@ -21,6 +20,7 @@ import {
unsignApp
} from './lib/codesign-helpers';
import { withTempDirectory } from './lib/fs-helpers';
import { createRoutedServer, RoutedRequest, RoutedResponse, RoutedServer } from './lib/http-server-helpers';
import { ifdescribe, ifit } from './lib/spec-helpers';
// We can only test the auto updater on darwin non-component builds
@@ -222,13 +222,13 @@ ifdescribe(shouldRunCodesignTests && !process.env.IS_UBSAN)('autoUpdater behavio
describe('with update server', () => {
let port = 0;
let server: express.Application = null as any;
let server: RoutedServer = null as any;
let httpServer: http.Server = null as any;
let requests: express.Request[] = [];
let requests: RoutedRequest[] = [];
beforeEach((done) => {
requests = [];
server = express();
server = createRoutedServer();
server.use((req, res, next) => {
requests.push(req);
next();
@@ -648,7 +648,7 @@ ifdescribe(shouldRunCodesignTests && !process.env.IS_UBSAN)('autoUpdater behavio
},
async (appPath, updateZipPath) => {
let downloadCount = 0;
let stalledResponse: express.Response | null = null;
let stalledResponse: RoutedResponse | null = null;
server.get('/update-file', (req, res) => {
downloadCount++;
@@ -695,9 +695,9 @@ ifdescribe(shouldRunCodesignTests && !process.env.IS_UBSAN)('autoUpdater behavio
});
// Unblock the stalled response now that the initial app has exited
// so the express server can shut down cleanly.
// so the server can shut down cleanly.
if (stalledResponse) {
(stalledResponse as express.Response).status(500).end();
(stalledResponse as RoutedResponse).status(500).end();
}
// The originally staged update (2.0.0) must have been applied and
@@ -992,7 +992,7 @@ ifdescribe(shouldRunCodesignTests && !process.env.IS_UBSAN)('autoUpdater behavio
endFixture: 'update'
},
async (appPath, updateZipPath) => {
const randomID = uuid.v4();
const randomID = randomUUID();
cp.spawnSync('xattr', ['-w', 'spec-id', randomID, appPath]);
server.get('/update-file', (req, res) => {
res.download(updateZipPath);
+4 -4
View File
@@ -1,9 +1,9 @@
import { expect } from 'chai';
import express from 'express';
import * as http from 'node:http';
import { AddressInfo } from 'node:net';
import { createRoutedServer, RoutedRequest, RoutedServer } from './lib/http-server-helpers';
import {
getElectronExecutable,
getMainJsFixturePath,
@@ -71,13 +71,13 @@ ifdescribe(shouldRunMsixTests)('autoUpdater MSIX behavior', function () {
describe('with update server', () => {
let port = 0;
let server: express.Application = null as any;
let server: RoutedServer = null as any;
let httpServer: http.Server = null as any;
let requests: express.Request[] = [];
let requests: RoutedRequest[] = [];
beforeEach((done) => {
requests = [];
server = express();
server = createRoutedServer();
server.use((req, res, next) => {
requests.push(req);
next();
+2 -2
View File
@@ -2,9 +2,9 @@ import { app } from 'electron/main';
import Busboy from 'busboy';
import { expect } from 'chai';
import * as uuid from 'uuid';
import * as childProcess from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
@@ -672,7 +672,7 @@ ifdescribe(!process.mas && !process.env.DISABLE_CRASH_REPORTER_TESTS)('crashRepo
it('respects an overridden crash dump directory', async () => {
const { remotely } = await startRemoteControlApp();
const crashesDir = path.join(app.getPath('temp'), uuid.v4());
const crashesDir = path.join(app.getPath('temp'), randomUUID());
const remoteCrashesDir = await remotely((crashesDir: string) => {
const { crashReporter, app } = require('electron');
app.setPath('crashDumps', crashesDir);
+3 -2
View File
@@ -3,6 +3,7 @@ import { session, net } from 'electron/main';
import { expect } from 'chai';
import * as ChildProcess from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
@@ -88,7 +89,7 @@ describe('netLog module', () => {
it('should include cookies when requested', async () => {
await testNetLog().startLogging(dumpFileDynamic, { captureMode: 'includeSensitive' });
const unique = require('uuid').v4();
const unique = randomUUID();
await new Promise<void>((resolve) => {
const req = net.request(serverUrl);
req.setHeader('Cookie', `foo=${unique}`);
@@ -106,7 +107,7 @@ describe('netLog module', () => {
it('should include socket bytes when requested', async () => {
await testNetLog().startLogging(dumpFileDynamic, { captureMode: 'everything' });
const unique = require('uuid').v4();
const unique = randomUUID();
await new Promise<void>((resolve) => {
const req = net.request({ method: 'POST', url: serverUrl });
req.on('response', (response) => {
+7 -8
View File
@@ -1,9 +1,9 @@
import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain, net } from 'electron/main';
import { expect } from 'chai';
import { v4 } from 'uuid';
import * as ChildProcess from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { EventEmitter, once } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
@@ -390,7 +390,7 @@ describe('protocol module', () => {
defer(() => server.close());
const { url } = await listen(server);
const ses = session.fromPartition(`protocol-response-url-session-${v4()}`);
const ses = session.fromPartition(`protocol-response-url-session-${randomUUID()}`);
let upstreamSeenByHandlingSession = false;
let upstreamSeenByDefaultSession = false;
ses.webRequest.onBeforeRequest((details, callback) => {
@@ -980,15 +980,15 @@ describe('protocol module', () => {
after(() => protocol.unregisterProtocol(serviceWorkerScheme));
it('should fail when registering invalid service worker', async () => {
await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
await contents.loadURL(`${serviceWorkerScheme}://${randomUUID()}.com`);
await expect(
contents.executeJavaScript(`navigator.serviceWorker.register('${v4()}.notjs', {scope: './'})`)
contents.executeJavaScript(`navigator.serviceWorker.register('${randomUUID()}.notjs', {scope: './'})`)
).to.be.rejected();
});
it('should be able to register service worker for custom scheme', async () => {
await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
await contents.executeJavaScript(`navigator.serviceWorker.register('${v4()}.js', {scope: './'})`);
await contents.loadURL(`${serviceWorkerScheme}://${randomUUID()}.com`);
await contents.executeJavaScript(`navigator.serviceWorker.register('${randomUUID()}.js', {scope: './'})`);
});
});
@@ -1462,14 +1462,13 @@ describe('protocol module', () => {
});
describe('protocol.registerSchemesAsPrivileged codeCache', function () {
const temp = require('temp').track();
const appPath = path.join(fixturesPath, 'apps', 'refresh-page');
let w: BrowserWindow;
let codeCachePath: string;
beforeEach(async () => {
w = new BrowserWindow({ show: false });
codeCachePath = temp.path();
codeCachePath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'electron-code-cache-')), 'cache');
});
afterEach(async () => {
+2 -2
View File
@@ -1,8 +1,8 @@
import { session, webContents, WebContents } from 'electron/main';
import { expect } from 'chai';
import { v4 } from 'uuid';
import { randomUUID } from 'node:crypto';
import { on, once } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
@@ -24,7 +24,7 @@ describe('session.serviceWorkers', () => {
});
beforeEach(async () => {
const uuid = v4();
const uuid = randomUUID();
server = http.createServer((req, res) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
+2 -2
View File
@@ -10,7 +10,6 @@ import {
WebFrameMain
} from 'electron/main';
import auth from 'basic-auth';
import { expect } from 'chai';
import send from 'send';
@@ -22,6 +21,7 @@ import * as https from 'node:https';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { parseBasicAuth } from './lib/net-helpers';
import { defer, deferKillUtilityProcess, ifit, listen, waitUntil } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
@@ -1260,7 +1260,7 @@ describe('session module', () => {
it('can clear http auth info from cache', async () => {
const ses = session.fromPartition('auth-cache');
const server = http.createServer((req, res) => {
const credentials = auth(req);
const credentials = parseBasicAuth(req);
if (!credentials || credentials.name !== 'test' || credentials.pass !== 'test') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
+38 -11
View File
@@ -1,8 +1,8 @@
import { createPackage, getRawHeader } from '@electron/asar';
import { flipFuses, FuseV1Config, FuseV1Options, FuseVersion } from '@electron/fuses';
import { resedit } from '@electron/packager/resedit';
import { expect } from 'chai';
import { NtExecutable, NtExecutableResource, Resource } from 'resedit';
import * as cp from 'node:child_process';
import * as nodeCrypto from 'node:crypto';
@@ -63,6 +63,35 @@ const expectToHaveCrashed = (res: SpawnResult) => {
}
};
// Embeds the asar integrity config into a Windows executable's resources in
// the same shape @electron/packager produces (and shell/common/asar/archive_win.cc
// reads): an "Integrity"/"ElectronAsar" resource containing a JSON list of
// { file, alg, value } entries.
async function embedAsarIntegrity(exePath: string, integrity: Record<string, { algorithm: 'SHA256'; hash: string }>) {
const exe = NtExecutable.from(await fs.promises.readFile(exePath));
const resources = NtExecutableResource.from(exe);
// Attach the resource to the same language as the existing version info.
const [versionInfo] = Resource.VersionInfo.fromEntries(resources.entries);
const [{ lang, codepage }] = versionInfo.getAllLanguagesForStringValues();
const payload = Buffer.from(
JSON.stringify(
Object.entries(integrity).map(([file, { algorithm, hash }]) => ({ file, alg: algorithm, value: hash }))
)
);
resources.entries.push({
type: 'INTEGRITY',
id: 'ELECTRONASAR',
bin: payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength),
lang,
codepage
});
resources.outputResource(exe);
await fs.promises.writeFile(exePath, Buffer.from(exe.generate()));
}
describe('fuses', function () {
this.timeout(120000);
@@ -159,16 +188,14 @@ describe('fuses', function () {
// Register both archives in the app's integrity table. This has to
// happen before the fuses are flipped, which re-signs the app.
if (process.platform === 'win32') {
await resedit(appPath, {
asarIntegrity: {
'resources\\default_app.asar': {
algorithm: 'SHA256',
hash: headerHash(pathToAsar)
},
'resources\\integrity-reads.asar': {
algorithm: 'SHA256',
hash: headerHash(pathToReadsAsar)
}
await embedAsarIntegrity(appPath, {
'resources\\default_app.asar': {
algorithm: 'SHA256',
hash: headerHash(pathToAsar)
},
'resources\\integrity-reads.asar': {
algorithm: 'SHA256',
hash: headerHash(pathToReadsAsar)
}
});
} else {
+69 -87
View File
@@ -196,6 +196,12 @@ function promisify(_f: Function): any {
/* dummy for typescript */
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function tempPath(): string {
/* dummy for typescript */
return '';
}
describe('asar package', function () {
const fixtures = path.join(__dirname, 'fixtures');
const asarDir = path.join(fixtures, 'test.asar');
@@ -221,6 +227,9 @@ describe('asar package', function () {
fixtures = ${JSON.stringify(fixtures)}
asarDir = ${JSON.stringify(asarDir)}
// Returns a path to a not-yet-existing file inside a fresh temp directory.
tempPath = () => path.join(fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'electron-asar-spec-')), 'file')
// This is used instead of util.promisify for some tests to dodge the
// util.promisify.custom behavior.
promisify = (f) => {
@@ -453,8 +462,7 @@ describe('asar package', function () {
describe('fs.copyFile', function () {
itremote('copies a normal file', async function () {
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
await new Promise<void>((resolve, reject) => {
fs.copyFile(p, dest, (err) => {
if (err) reject(err);
@@ -466,8 +474,7 @@ describe('asar package', function () {
itremote('copies a unpacked file', async function () {
const p = path.join(asarDir, 'unpack.asar', 'a.txt');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
await new Promise<void>((resolve, reject) => {
fs.copyFile(p, dest, (err) => {
if (err) reject(err);
@@ -481,16 +488,14 @@ describe('asar package', function () {
describe('fs.promises.copyFile', function () {
itremote('copies a normal file', async function () {
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
await fs.promises.copyFile(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
itremote('copies a unpacked file', async function () {
const p = path.join(asarDir, 'unpack.asar', 'a.txt');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
await fs.promises.copyFile(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
@@ -499,16 +504,14 @@ describe('asar package', function () {
describe('fs.copyFileSync', function () {
itremote('copies a normal file', function () {
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
fs.copyFileSync(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
itremote('copies a unpacked file', function () {
const p = path.join(asarDir, 'unpack.asar', 'a.txt');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
fs.copyFileSync(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
@@ -518,8 +521,7 @@ describe('asar package', function () {
itremote('copies a normal file', function () {
if (!fs.cpSync) return;
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
fs.cpSync(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
@@ -529,8 +531,7 @@ describe('asar package', function () {
itremote('copies a normal file', async function () {
if (!fs.cp) return;
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
await new Promise<void>((resolve, reject) => {
fs.cp(p, dest, (err) => (err ? reject(err) : resolve()));
});
@@ -542,8 +543,7 @@ describe('asar package', function () {
itremote('copies a normal file', async function () {
if (!fs.promises.cp) return;
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
await fs.promises.cp(p, dest);
expect(fs.readFileSync(p).equals(fs.readFileSync(dest))).to.be.true();
});
@@ -2316,8 +2316,7 @@ describe('asar package', function () {
// FileHandle#close() runs close(2) on the threadpool; until then a real
// file opened right after may be handed the same number. It must never
// be served from the archive-backed reader.
const temp = require('temp').track();
const real = temp.path();
const real = tempPath();
fs.writeFileSync(real, 'real-file-content');
const p = path.join(asarDir, 'a.asar', 'file1');
for (let i = 0; i < 200; i++) {
@@ -2339,8 +2338,7 @@ describe('asar package', function () {
// without going through fs.close. Simulate that with original-fs and
// check the next owner of the number is served natively.
const originalFs = require('node:original-fs');
const temp = require('temp').track();
const real = temp.path();
const real = tempPath();
fs.writeFileSync(real, 'real-file-content');
const p = path.join(asarDir, 'a.asar', 'file1');
for (const variant of ['sync', 'async', 'handle']) {
@@ -2423,8 +2421,7 @@ describe('asar package', function () {
describe('fs.copyFile flags and errors', function () {
itremote('honours COPYFILE_EXCL and overwrites otherwise', async function () {
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
fs.copyFileSync(p, dest);
expect(fs.readFileSync(dest).toString()).to.equal('file1\n');
expect(() => fs.copyFileSync(p, dest, fs.constants.COPYFILE_EXCL)).to.throw(/EEXIST/);
@@ -2443,7 +2440,6 @@ describe('asar package', function () {
itremote('lets native fs decide about copy-on-write clones of unpacked files', function () {
const originalFs = require('node:original-fs');
const temp = require('temp').track();
const unpacked = path.join(asarDir, 'unpack.asar', 'a.txt');
const real = path.join(asarDir, 'unpack.asar.unpacked', 'a.txt');
const outcome = (fn: () => void) => {
@@ -2454,8 +2450,8 @@ describe('asar package', function () {
return e.code;
}
};
const viaAsar = temp.path();
const viaOriginal = temp.path();
const viaAsar = tempPath();
const viaOriginal = tempPath();
// Whatever the filesystem says about reflinks, the answer for an unpacked
// entry must match the answer for the real file behind it.
expect(outcome(() => fs.copyFileSync(unpacked, viaAsar, fs.constants.COPYFILE_FICLONE_FORCE))).to.equal(
@@ -2466,68 +2462,64 @@ describe('asar package', function () {
itremote('validates the mode argument like Node', function () {
const p = path.join(asarDir, 'a.asar', 'file1');
const temp = require('temp').track();
expect(() => fs.copyFileSync(p, temp.path(), 8))
expect(() => fs.copyFileSync(p, tempPath(), 8))
.to.throw()
.with.property('code', 'ERR_OUT_OF_RANGE');
expect(() => (fs.copyFileSync as any)(p, temp.path(), 'x'))
expect(() => (fs.copyFileSync as any)(p, tempPath(), 'x'))
.to.throw()
.with.property('code', 'ERR_INVALID_ARG_TYPE');
});
itremote('reports ENOENT / EISDIR for bad sources', async function () {
const temp = require('temp').track();
expect(() => fs.copyFileSync(path.join(asarDir, 'a.asar', 'nope'), temp.path())).to.throw(/ENOENT/);
expect(() => fs.copyFileSync(path.join(asarDir, 'a.asar', 'dir1'), temp.path())).to.throw(/EISDIR/);
expect(() => fs.copyFileSync(path.join(asarDir, 'a.asar', 'nope'), tempPath())).to.throw(/ENOENT/);
expect(() => fs.copyFileSync(path.join(asarDir, 'a.asar', 'dir1'), tempPath())).to.throw(/EISDIR/);
await expectToThrowErrorWithCode(
() => fs.promises.copyFile(path.join(asarDir, 'a.asar', 'nope'), temp.path()),
() => fs.promises.copyFile(path.join(asarDir, 'a.asar', 'nope'), tempPath()),
'ENOENT'
);
const err = await new Promise<any>((resolve) =>
fs.copyFile(path.join(asarDir, 'a.asar', 'dir1'), temp.path(), resolve)
fs.copyFile(path.join(asarDir, 'a.asar', 'dir1'), tempPath(), resolve)
);
expect(err.code).to.equal('EISDIR');
});
itremote('copies linked files, larger files and executable bits', async function () {
const temp = require('temp').track();
const link = path.join(asarDir, 'a.asar', 'link1');
const d1 = temp.path();
const d1 = tempPath();
fs.copyFileSync(link, d1);
expect(fs.readFileSync(d1).toString()).to.equal('file1\n');
const big = path.join(asarDir, 'video.asar', 'video.mp4');
const d2 = temp.path();
const d2 = tempPath();
await fs.promises.copyFile(big, d2);
expect(fs.readFileSync(d2).equals(fs.readFileSync(big))).to.be.true();
const d3 = temp.path();
const d3 = tempPath();
await promisify(fs.copyFile)(big, d3);
expect(fs.readFileSync(d3).equals(fs.readFileSync(big))).to.be.true();
if (process.platform !== 'win32') {
const d4 = temp.path();
const d4 = tempPath();
fs.copyFileSync(path.join(asarDir, 'echo.asar', 'echo'), d4);
expect(fs.statSync(d4).mode & 0o111).to.not.equal(0);
const d5 = temp.path();
const d5 = tempPath();
fs.copyFileSync(path.join(asarDir, 'a.asar', 'file1'), d5);
expect(fs.statSync(d5).mode & 0o111).to.equal(0);
// cp chmods the destination to the source's stat mode afterwards, so
// stat must agree with what copyFile preserved.
const d6 = temp.path();
const d6 = tempPath();
fs.cpSync(path.join(asarDir, 'echo.asar', 'echo'), d6);
expect(fs.statSync(d6).mode & 0o111).to.not.equal(0);
const d7 = temp.path();
const d7 = tempPath();
await fs.promises.cp(path.join(asarDir, 'echo.asar', 'echo'), d7);
expect(fs.statSync(d7).mode & 0o111).to.not.equal(0);
const d8 = temp.path();
const d8 = tempPath();
fs.cpSync(path.join(asarDir, 'echo.asar'), d8, { recursive: true, filter: () => true });
expect(fs.statSync(path.join(d8, 'echo')).mode & 0o111).to.not.equal(0);
}
});
itremote('fails when the destination is inside an archive', async function () {
const temp = require('temp').track();
const src = temp.path();
const src = tempPath();
fs.writeFileSync(src, 'x');
expect(() => fs.copyFileSync(src, path.join(asarDir, 'a.asar', 'new-file'))).to.throw(/ENOTDIR|EACCES|ENOENT/);
expect(() =>
@@ -2539,11 +2531,10 @@ describe('asar package', function () {
// Mirrors the test-fs-cp-* family (subset applicable to read-only sources).
describe('fs.cp / fs.cpSync / fs.promises.cp from archives', function () {
itremote('copies a directory tree recursively (sync, callback, promises)', async function () {
const temp = require('temp').track();
const src = path.join(asarDir, 'a.asar');
const expectedTop = ['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js'];
for (const variant of ['sync', 'callback', 'promises']) {
const dest = temp.path();
const dest = tempPath();
if (variant === 'sync') fs.cpSync(src, dest, { recursive: true });
else if (variant === 'callback') await promisify(fs.cp)(src, dest, { recursive: true });
else await fs.promises.cp(src, dest, { recursive: true });
@@ -2572,10 +2563,9 @@ describe('asar package', function () {
});
itremote('copies symlinks verbatim when asked, producing a self-contained tree', async function () {
const temp = require('temp').track();
const src = path.join(asarDir, 'a.asar');
for (const variant of ['sync', 'promises']) {
const dest = temp.path();
const dest = tempPath();
if (variant === 'sync') fs.cpSync(src, dest, { recursive: true, verbatimSymlinks: true });
else await fs.promises.cp(src, dest, { recursive: true, verbatimSymlinks: true });
expect(fs.readlinkSync(path.join(dest, 'link1')), variant).to.equal('file1');
@@ -2593,22 +2583,20 @@ describe('asar package', function () {
});
itremote('requires recursive for directories', async function () {
const temp = require('temp').track();
const src = path.join(asarDir, 'a.asar');
// Node reports this as the ERR_FS_EISDIR system error (with EISDIR as its info code).
expect(() => fs.cpSync(src, temp.path()))
expect(() => fs.cpSync(src, tempPath()))
.to.throw()
.with.property('code', 'ERR_FS_EISDIR');
await expectToThrowErrorWithCode(() => fs.promises.cp(src, temp.path()), 'ERR_FS_EISDIR');
const err = await new Promise<any>((resolve) => fs.cp(src, temp.path(), resolve));
await expectToThrowErrorWithCode(() => fs.promises.cp(src, tempPath()), 'ERR_FS_EISDIR');
const err = await new Promise<any>((resolve) => fs.cp(src, tempPath(), resolve));
expect(err.code).to.equal('ERR_FS_EISDIR');
expect(err.info.code).to.equal('EISDIR');
});
itremote('honours errorOnExist and force', async function () {
const temp = require('temp').track();
const src = path.join(asarDir, 'a.asar', 'file1');
const dest = temp.path();
const dest = tempPath();
fs.writeFileSync(dest, 'existing');
// force defaults to true
fs.cpSync(src, dest);
@@ -2629,7 +2617,7 @@ describe('asar package', function () {
expect(fs.readFileSync(dest).toString()).to.equal('file1\n');
// Directory variants
const dir = temp.path();
const dir = tempPath();
fs.mkdirSync(path.join(dir, 'dir1'), { recursive: true });
fs.writeFileSync(path.join(dir, 'dir1', 'file1'), 'existing');
fs.cpSync(path.join(asarDir, 'a.asar'), dir, { recursive: true, force: false });
@@ -2643,14 +2631,13 @@ describe('asar package', function () {
});
itremote('applies filter functions', async function () {
const temp = require('temp').track();
const src = path.join(asarDir, 'a.asar');
const dest = temp.path();
const dest = tempPath();
fs.cpSync(src, dest, { recursive: true, filter: (p: string) => !p.endsWith('file2') });
expect(fs.existsSync(path.join(dest, 'file1'))).to.be.true();
expect(fs.existsSync(path.join(dest, 'file2'))).to.be.false();
expect(fs.existsSync(path.join(dest, 'dir1', 'file2'))).to.be.false();
const dest2 = temp.path();
const dest2 = tempPath();
await fs.promises.cp(src, dest2, {
recursive: true,
filter: async (p: string) => !path.basename(p).startsWith('dir')
@@ -2661,19 +2648,18 @@ describe('asar package', function () {
});
itremote('dereferences symlinks when asked', async function () {
const temp = require('temp').track();
const src = path.join(asarDir, 'a.asar');
// Note: a.asar's directory links form a cycle (dir1/link2 -> dir1), so a
// dereferencing copy of the whole tree cannot terminate (Node behaves
// the same on a real tree); dereference the acyclic parts instead.
for (const variant of ['sync', 'promises']) {
const single = temp.path();
const single = tempPath();
if (variant === 'sync') fs.cpSync(path.join(src, 'link1'), single, { dereference: true });
else await fs.promises.cp(path.join(src, 'link1'), single, { dereference: true });
expect(fs.lstatSync(single).isFile(), variant).to.be.true();
expect(fs.readFileSync(single).toString(), variant).to.equal('file1\n');
const dir = temp.path();
const dir = tempPath();
if (variant === 'sync') fs.cpSync(path.join(src, 'dir2'), dir, { recursive: true, dereference: true });
else await fs.promises.cp(path.join(src, 'dir2'), dir, { recursive: true, dereference: true });
expect(fs.readdirSync(dir).sort(), variant).to.deep.equal(['file1', 'file2', 'file3']);
@@ -2681,13 +2667,12 @@ describe('asar package', function () {
});
itremote('errors on non-existent sources and file-to-directory mismatches', async function () {
const temp = require('temp').track();
expect(() => fs.cpSync(path.join(asarDir, 'a.asar', 'nope'), temp.path())).to.throw(/ENOENT/);
expect(() => fs.cpSync(path.join(asarDir, 'a.asar', 'nope'), tempPath())).to.throw(/ENOENT/);
await expectToThrowErrorWithCode(
() => fs.promises.cp(path.join(asarDir, 'a.asar', 'nope'), temp.path()),
() => fs.promises.cp(path.join(asarDir, 'a.asar', 'nope'), tempPath()),
'ENOENT'
);
const dir = temp.path();
const dir = tempPath();
fs.mkdirSync(dir);
expect(() => fs.cpSync(path.join(asarDir, 'a.asar', 'file1'), dir))
.to.throw()
@@ -2696,7 +2681,7 @@ describe('asar package', function () {
() => fs.promises.cp(path.join(asarDir, 'a.asar', 'file1'), dir),
'ERR_FS_CP_NON_DIR_TO_DIR'
);
const file = temp.path();
const file = tempPath();
fs.writeFileSync(file, 'x');
expect(() => fs.cpSync(path.join(asarDir, 'a.asar'), file, { recursive: true }))
.to.throw()
@@ -2708,8 +2693,7 @@ describe('asar package', function () {
});
itremote('copies unpacked files and mixed archives', async function () {
const temp = require('temp').track();
const dest = temp.path();
const dest = tempPath();
fs.cpSync(path.join(asarDir, 'unpack.asar'), dest, { recursive: true });
expect(
fs.readFileSync(path.join(dest, 'a.txt')).equals(fs.readFileSync(path.join(asarDir, 'unpack.asar', 'a.txt')))
@@ -3036,17 +3020,16 @@ describe('asar package', function () {
);
// Copies copy the archive file itself.
const temp = require('temp').track();
const d1 = temp.path();
const d1 = tempPath();
originalFs.copyFileSync(archive, d1);
expect(originalFs.readFileSync(d1).equals(raw)).to.be.true();
const d2 = temp.path();
const d2 = tempPath();
await originalFs.promises.copyFile(archive, d2);
expect(originalFs.readFileSync(d2).equals(raw)).to.be.true();
const d3 = temp.path();
const d3 = tempPath();
originalFs.cpSync(archive, d3);
expect(originalFs.readFileSync(d3).equals(raw)).to.be.true();
const d4 = temp.path();
const d4 = tempPath();
await originalFs.promises.cp(archive, d4);
expect(originalFs.readFileSync(d4).equals(raw)).to.be.true();
@@ -3064,7 +3047,10 @@ describe('asar package', function () {
expect(seen).to.be.true();
// Writing through original-fs to a path that merely mentions .asar works.
const scratch = path.join(temp.mkdirSync('asar-original-fs'), 'not-really.asar');
const scratch = path.join(
fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'asar-original-fs-')),
'not-really.asar'
);
originalFs.writeFileSync(scratch, 'hello');
expect(originalFs.readFileSync(scratch, 'utf8')).to.equal('hello');
const wfd = originalFs.openSync(scratch, 'w');
@@ -3096,6 +3082,13 @@ describe('asar package', function () {
fs.mkdirSync(p);
}).to.throw(/ENOTDIR/);
});
itremote('throws error when calling recursively inside asar archive', function () {
const p = path.join(asarDir, 'a.asar', 'not-exist');
expect(() => {
fs.mkdirSync(p, { recursive: true });
}).to.throw(/ENOTDIR/);
});
});
describe('fs.exists', function () {
@@ -3607,17 +3600,6 @@ describe('asar package', function () {
});
});
describe('mkdirp module', function () {
itremote('throws error when calling inside asar archive', function () {
const mkdirp = require('mkdirp');
const p = path.join(asarDir, 'a.asar', 'not-exist');
expect(() => {
mkdirp.sync(p);
}).to.throw(/ENOTDIR/);
});
});
describe('native-image', function () {
itremote('reads image from asar archive', function () {
const p = path.join(asarDir, 'logo.asar', 'logo.png');
+41 -42
View File
@@ -15,6 +15,7 @@ import { expect } from 'chai';
import * as WebSocket from 'ws';
import { spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { once } from 'node:events';
import * as fs from 'node:fs/promises';
import * as http from 'node:http';
@@ -25,8 +26,6 @@ import { ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpe
import { expectWarningMessages } from './lib/warning-helpers';
import { closeAllWindows, closeWindow, cleanupWebContents } from './lib/window-helpers';
const uuid = require('uuid');
const fixtures = path.join(__dirname, 'fixtures');
describe('chrome extensions', () => {
@@ -69,7 +68,7 @@ describe('chrome extensions', () => {
});
it('does not crash when using chrome.management', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } });
await w.loadURL('about:blank');
@@ -95,7 +94,7 @@ describe('chrome extensions', () => {
let w: BrowserWindow;
beforeEach(() => {
customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
w = new BrowserWindow({
show: false,
webPreferences: {
@@ -151,7 +150,7 @@ describe('chrome extensions', () => {
});
it('supports minimum_chrome_version manifest key', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const w = new BrowserWindow({
show: false,
webPreferences: {
@@ -174,7 +173,7 @@ describe('chrome extensions', () => {
}
it('bypasses CORS in requests made from extensions', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } });
const extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
await w.loadURL(`${extension.url}bare-page.html`);
@@ -186,7 +185,7 @@ describe('chrome extensions', () => {
// extension registry is redirected to the main session. so installing an
// extension in an in-memory session results in it being installed in the
// default session.
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
await w.loadURL(url);
@@ -195,13 +194,13 @@ describe('chrome extensions', () => {
});
it('does not crash when loading an extension with missing manifest', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const promise = customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'missing-manifest'));
await expect(promise).to.eventually.be.rejectedWith(/Manifest file is missing or unreadable/);
});
it('does not crash when failing to load an extension', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const promise = customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'load-error'));
await expect(promise).to.eventually.be.rejected();
});
@@ -209,7 +208,7 @@ describe('chrome extensions', () => {
it('serializes a loaded extension', async () => {
const extensionPath = path.join(fixtures, 'extensions', 'red-bg');
const manifest = JSON.parse(await fs.readFile(path.join(extensionPath, 'manifest.json'), 'utf-8'));
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const extension = await customSession.extensions.loadExtension(extensionPath);
expect(extension.id).to.be.a('string');
expect(extension.name).to.be.a('string');
@@ -220,7 +219,7 @@ describe('chrome extensions', () => {
});
it('removes an extension', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const { id } = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
{
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
@@ -238,7 +237,7 @@ describe('chrome extensions', () => {
});
it('emits extension lifecycle events', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const loadedPromise = once(customSession.extensions, 'extension-loaded');
const readyPromise = emittedUntil(
@@ -262,7 +261,7 @@ describe('chrome extensions', () => {
});
it('lists loaded extensions in getAllExtensions', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const e = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
expect(customSession.extensions.getAllExtensions()).to.deep.equal([e]);
customSession.extensions.removeExtension(e.id);
@@ -270,13 +269,13 @@ describe('chrome extensions', () => {
});
it('gets an extension by id', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const e = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
expect(customSession.extensions.getExtension(e.id)).to.deep.equal(e);
});
it('confines an extension to the session it was loaded in', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
const w = new BrowserWindow({ show: false }); // not in the session
await w.loadURL(url);
@@ -285,7 +284,7 @@ describe('chrome extensions', () => {
});
it('loading an extension in a temporary session throws an error', async () => {
const customSession = session.fromPartition(uuid.v4());
const customSession = session.fromPartition(randomUUID());
await expect(
customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))
).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session');
@@ -301,7 +300,7 @@ describe('chrome extensions', () => {
return result;
};
beforeEach(async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v2'));
w = new BrowserWindow({
show: false,
@@ -333,7 +332,7 @@ describe('chrome extensions', () => {
return result;
};
beforeEach(async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime'));
w = new BrowserWindow({
show: false,
@@ -369,7 +368,7 @@ describe('chrome extensions', () => {
describe('chrome.storage', () => {
it('stores and retrieves a key', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage'));
const w = new BrowserWindow({
show: false,
@@ -395,7 +394,7 @@ describe('chrome extensions', () => {
let w: BrowserWindow;
beforeEach(() => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
w = new BrowserWindow({
show: false,
webPreferences: { session: customSession, sandbox: true, contextIsolation: true }
@@ -472,7 +471,7 @@ describe('chrome extensions', () => {
describe('chrome.tabs', () => {
let customSession: Session;
before(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
});
afterEach(closeAllWindows);
@@ -494,7 +493,7 @@ describe('chrome extensions', () => {
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
await w.loadURL(url);
const portName = uuid.v4();
const portName = randomUUID();
const message = { method: 'connectTab', args: [portName] };
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
@@ -544,7 +543,7 @@ describe('chrome extensions', () => {
describe('background pages', () => {
afterEach(closeAllWindows);
it('loads a lazy background page when sending a message', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
const w = new BrowserWindow({
show: false,
@@ -563,7 +562,7 @@ describe('chrome extensions', () => {
});
it('can use extension.getBackgroundPage from a ui page', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const { id } = await customSession.extensions.loadExtension(
path.join(fixtures, 'extensions', 'lazy-background-page')
);
@@ -574,7 +573,7 @@ describe('chrome extensions', () => {
});
it('can use extension.getBackgroundPage from a ui page', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const { id } = await customSession.extensions.loadExtension(
path.join(fixtures, 'extensions', 'lazy-background-page')
);
@@ -585,7 +584,7 @@ describe('chrome extensions', () => {
});
it('can use runtime.getBackgroundPage from a ui page', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const { id } = await customSession.extensions.loadExtension(
path.join(fixtures, 'extensions', 'lazy-background-page')
);
@@ -596,7 +595,7 @@ describe('chrome extensions', () => {
});
it('has session in background page', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const promise = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
const { id } = await customSession.extensions.loadExtension(
path.join(fixtures, 'extensions', 'persistent-background-page')
@@ -609,7 +608,7 @@ describe('chrome extensions', () => {
});
it('can open devtools of background page', async () => {
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const promise = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
const [, bgPageContents] = await promise;
@@ -652,7 +651,7 @@ describe('chrome extensions', () => {
// TODO(jkleinsc) fix this flaky test on WOA
ifit(process.platform !== 'win32' || process.arch !== 'arm64')('loads a devtools extension', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
const winningMessage = once(ipcMain, 'winning');
const w = new BrowserWindow({
@@ -927,7 +926,7 @@ describe('chrome extensions', () => {
describe('manifest v3', () => {
afterEach(closeAllWindows);
it('registers background service worker', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const registrationPromise = new Promise<string>((resolve) => {
customSession.serviceWorkers.once('registration-completed', (event, { scope }) => resolve(scope));
});
@@ -939,7 +938,7 @@ describe('chrome extensions', () => {
});
it('launches background service worker', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const launchPromise = new Promise<void>((resolve) => {
customSession.serviceWorkers.on('running-status-changed', ({ runningStatus }) => {
if (runningStatus === 'running') resolve();
@@ -954,7 +953,7 @@ describe('chrome extensions', () => {
});
it('launches background service worker when the extension is loaded again without restarting the app', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const extensionPath = path.join(fixtures, 'extensions', 'mv3-service-worker');
const isWorkerRunning = (extension: Extension) => {
@@ -989,7 +988,7 @@ describe('chrome extensions', () => {
// 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 partition = `persist:${randomUUID()}`;
const extensionPath = path.join(fixtures, 'extensions', 'mv3-service-worker');
const runRemoteControlApp = async () => {
@@ -1041,7 +1040,7 @@ describe('chrome extensions', () => {
});
it('can run chrome extension APIs', async () => {
const customSession = session.fromPartition(`persist:${uuid.v4()}`);
const customSession = session.fromPartition(`persist:${randomUUID()}`);
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
@@ -1060,7 +1059,7 @@ describe('chrome extensions', () => {
let w = null as unknown as BrowserWindow;
before(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v3'));
});
@@ -1152,7 +1151,7 @@ describe('chrome extensions', () => {
let w = null as unknown as BrowserWindow;
before(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-action-fail'));
});
@@ -1209,7 +1208,7 @@ describe('chrome extensions', () => {
let w = null as unknown as BrowserWindow;
before(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
});
@@ -1307,7 +1306,7 @@ describe('chrome extensions', () => {
});
it('does not return privileged properties without tabs permission', async () => {
const noPrivilegeSes = session.fromPartition(`persist:${uuid.v4()}`);
const noPrivilegeSes = session.fromPartition(`persist:${randomUUID()}`);
await noPrivilegeSes.extensions.loadExtension(
path.join(fixtures, 'extensions', 'chrome-tabs', 'no-privileges')
);
@@ -1482,7 +1481,7 @@ describe('chrome extensions', () => {
sameSessionWin.webContents.setAudioMuted(true);
const newSession = session.fromPartition(`persist:${uuid.v4()}`);
const newSession = session.fromPartition(`persist:${randomUUID()}`);
const differentSessionWin = new BrowserWindow({
show: false,
webPreferences: {
@@ -1513,7 +1512,7 @@ describe('chrome extensions', () => {
let w = null as unknown as BrowserWindow;
before(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
customSession = session.fromPartition(`persist:${randomUUID()}`);
await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-scripting'));
});
@@ -1597,8 +1596,8 @@ describe('chrome extensions', () => {
let victim: BrowserWindow;
before(async () => {
extSession = session.fromPartition(`persist:${uuid.v4()}`);
otherSession = session.fromPartition(`persist:${uuid.v4()}`);
extSession = session.fromPartition(`persist:${randomUUID()}`);
otherSession = session.fromPartition(`persist:${randomUUID()}`);
await extSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'tabs-cross-session'));
});
-1
View File
@@ -1 +0,0 @@
module.exports = yes
+124
View File
@@ -0,0 +1,124 @@
import send from 'send';
import * as http from 'node:http';
import * as path from 'node:path';
/**
* A tiny express-shaped router for specs that need to stand up a fake
* server with a handful of routes (currently the autoUpdater specs). File
* responses go through `send`, which is what express uses under the hood, so
* HEAD requests, range requests, content types etc. behave the way the
* updaters expect.
*/
export type RoutedRequest = http.IncomingMessage & {
/** Returns the value of the named request header, like express' `req.header()`. */
header(name: string): string | undefined;
};
export type RoutedResponse = http.ServerResponse & {
/** Sets the status code and returns the response for chaining. */
status(code: number): RoutedResponse;
/** Ends the response, optionally with a plain text body. */
send(body?: string): void;
/** Ends the response with a JSON body. */
json(body: unknown): void;
/** Streams the given file as an attachment. */
download(filePath: string): void;
};
export type RouteHandler = (req: RoutedRequest, res: RoutedResponse) => void;
export type Middleware = (req: RoutedRequest, res: RoutedResponse, next: () => void) => void;
export type RoutedServer = {
/** Registers a middleware that runs before routing for every request. */
use(middleware: Middleware): void;
/**
* Registers a handler for GET (and, as in express, HEAD) requests whose
* path matches `pattern`. `:name` segments match any single path segment.
*/
get(pattern: string, handler: RouteHandler): void;
listen(port: number, host: string, callback: () => void): http.Server;
};
function compilePattern(pattern: string): RegExp {
const source = pattern
.split('/')
.map((segment) => (segment.startsWith(':') ? '[^/]+' : segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
.join('/');
return new RegExp(`^${source}/?$`);
}
function decorateRequest(req: http.IncomingMessage): RoutedRequest {
return Object.assign(req, {
header(name: string) {
const value = req.headers[name.toLowerCase()];
return Array.isArray(value) ? value.join(', ') : value;
}
});
}
function decorateResponse(req: http.IncomingMessage, res: http.ServerResponse): RoutedResponse {
const routed: RoutedResponse = Object.assign(res, {
status(code: number) {
res.statusCode = code;
return routed;
},
send(body?: string) {
if (body !== undefined) res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end(body);
},
json(body: unknown) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(body));
},
download(filePath: string) {
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(filePath)}"`);
send(req, encodeURI(filePath))
.on('error', (err: NodeJS.ErrnoException & { status?: number }) => {
res.statusCode = err.status ?? 500;
res.end();
})
.pipe(res);
}
});
return routed;
}
export function createRoutedServer(): RoutedServer {
const middlewares: Middleware[] = [];
const routes: { matcher: RegExp; handler: RouteHandler }[] = [];
const handleRequest = (rawReq: http.IncomingMessage, rawRes: http.ServerResponse) => {
const req = decorateRequest(rawReq);
const res = decorateResponse(rawReq, rawRes);
const route = () => {
const pathname = new URL(req.url!, 'http://localhost').pathname;
const isGetLike = req.method === 'GET' || req.method === 'HEAD';
const match = isGetLike ? routes.find(({ matcher }) => matcher.test(pathname)) : undefined;
if (match) {
match.handler(req, res);
} else {
res.status(404).send();
}
};
const runMiddleware = (index: number) => {
if (index < middlewares.length) {
middlewares[index](req, res, () => runMiddleware(index + 1));
} else {
route();
}
};
runMiddleware(0);
};
return {
use: (middleware) => {
middlewares.push(middleware);
},
get: (pattern, handler) => {
routes.push({ matcher: compilePattern(pattern), handler });
},
listen: (port, host, callback) => http.createServer(handleRequest).listen(port, host, callback)
};
}
+13
View File
@@ -26,6 +26,19 @@ export function randomString(length: number) {
return buffer.toString();
}
/**
* Parses the credentials out of an HTTP Basic `Authorization` header, or
* returns null if the request does not carry one.
*/
export function parseBasicAuth(request: http.IncomingMessage): { name: string; pass: string } | null {
const match = /^Basic (.+)$/i.exec(request.headers.authorization ?? '');
if (!match) return null;
const decoded = Buffer.from(match[1], 'base64').toString();
const separator = decoded.indexOf(':');
if (separator === -1) return null;
return { name: decoded.slice(0, separator), pass: decoded.slice(separator + 1) };
}
export async function getResponse(urlRequest: Electron.ClientRequest) {
return new Promise<Electron.IncomingMessage>((resolve, reject) => {
urlRequest.on('error', reject);
+5 -5
View File
@@ -1,8 +1,8 @@
import { app } from 'electron';
import { expect } from 'chai';
import * as uuid from 'uuid';
import { randomUUID } from 'node:crypto';
import { once } from 'node:events';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
@@ -121,7 +121,7 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
});
it('logs to the given file when --log-file is passed', async () => {
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4());
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + randomUUID());
const rc = await startRemoteControlApp(['--enable-logging', '--log-file=' + logFilePath]);
rc.remotely(() => {
process._linkedBinding('electron_common_testing').log(0, 'TEST_LOG');
@@ -137,7 +137,7 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
});
ifit(process.platform === 'win32')('child process logs to the given file when --log-file is passed', async () => {
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4());
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + randomUUID());
const preloadPath = path.resolve(__dirname, 'fixtures', 'log-test.js');
const rc = await startRemoteControlApp([
'--enable-logging',
@@ -171,7 +171,7 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
});
it('logs to the given file when ELECTRON_LOG_FILE is set', async () => {
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4());
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + randomUUID());
const rc = await startRemoteControlApp([], {
env: { ...process.env, ELECTRON_ENABLE_LOGGING: '1', ELECTRON_LOG_FILE: logFilePath }
});
@@ -189,7 +189,7 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
});
it('does not lose early log messages when logging to a given file with --log-file', async () => {
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4());
const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + randomUUID());
const rc = await startRemoteControlApp([
'--enable-logging',
'--log-file=' + logFilePath,
+3 -22
View File
@@ -67,18 +67,6 @@ describe('modules support', () => {
});
});
describe('q', () => {
describe('Q.when', () => {
it('emits the fulfil callback', (done) => {
const Q = require('q');
Q(true).then((val: boolean) => {
expect(val).to.be.true();
done();
});
});
});
});
describe("require('electron/...')", () => {
const utilityProcessFixturesPath = path.resolve(
__dirname,
@@ -215,15 +203,6 @@ describe('modules support', () => {
expect(code).to.equal(0);
});
});
describe('coffeescript', () => {
it('can be registered and used to require .coffee files', () => {
expect(() => {
require('coffeescript').register();
}).to.not.throw();
expect(require('./fixtures/module/test.coffee')).to.be.true();
});
});
});
describe('global variables', () => {
@@ -305,7 +284,9 @@ describe('modules support', () => {
webPreferences: { nodeIntegration: true, contextIsolation: false }
});
w.loadURL('about:blank');
const result = await w.webContents.executeJavaScript('typeof require("q").when');
// Any module that only exists in the spec app's node_modules will do here;
// this is checking that module.paths includes the app directory.
const result = await w.webContents.executeJavaScript('typeof require("dirty-chai")');
expect(result).to.equal('function');
});
});
+5 -16
View File
@@ -15,39 +15,28 @@
"@electron-ci/uv-dlopen": "*",
"@electron-ci/virtual-display": "*",
"@electron/fuses": "^2.1.2",
"@electron/packager": "^20.0.4",
"@types/basic-auth": "^1.1.8",
"@types/busboy": "^1.5.4",
"@types/chai": "^4.3.19",
"@types/chai-as-promised": "^7.1.3",
"@types/dirty-chai": "^2.0.5",
"@types/express": "^4.17.13",
"@types/mocha": "^7.0.2",
"@types/send": "^0.14.5",
"@types/sinon": "^9.0.4",
"@types/uuid": "^3.4.6",
"@types/send": "^1.2.0",
"@types/w3c-web-serial": "^1.0.7",
"@types/ws": "^8.18.1",
"abstract-socket": "2.0.0",
"basic-auth": "^2.0.1",
"busboy": "^1.6.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"coffeescript": "^2.4.1",
"dbus-native": "^0.4.0",
"dbus-native": "patch:dbus-native@npm%3A0.4.0#~/.yarn/patches/dbus-native-npm-0.4.0-fd59c31eb6.patch",
"dirty-chai": "^2.0.1",
"express": "^5.2.1",
"graceful-fs": "^4.1.15",
"mkdirp": "^0.5.1",
"mocha": "^10.0.0",
"mocha-junit-reporter": "^1.18.0",
"mocha-multi-reporters": "^1.1.7",
"pdfjs-dist": "4.10.38",
"plist": "^3.1.0",
"ps-list": "^7.0.0",
"q": "^1.5.1",
"send": "^0.19.0",
"sinon": "^9.0.1",
"uuid": "^14.0.1",
"resedit": "^2.0.3",
"send": "^1.2.0",
"winreg": "1.2.4",
"ws": "^8.21.0",
"yargs": "^16.0.3"
+2 -2
View File
@@ -1,6 +1,5 @@
import { BrowserWindow, session, ipcMain, app, WebContents } from 'electron/main';
import auth from 'basic-auth';
import { expect } from 'chai';
import { once } from 'node:events';
@@ -10,6 +9,7 @@ import { setTimeout } from 'node:timers/promises';
import * as url from 'node:url';
import { emittedUntil } from './lib/events-helpers';
import { parseBasicAuth } from './lib/net-helpers';
import { HexColors, ScreenCapture, hasCapturableScreen } from './lib/screen-helpers';
import { ifit, ifdescribe, defer, itremote, useRemoteContext, listen } from './lib/spec-helpers';
import { closeAllWindows } from './lib/window-helpers';
@@ -2413,7 +2413,7 @@ describe('<webview> tag', function () {
it('should authenticate with correct credentials', async () => {
const message = 'Authenticated';
const server = http.createServer((req, res) => {
const credentials = auth(req)!;
const credentials = parseBasicAuth(req)!;
if (credentials.name === 'test' && credentials.pass === 'test') {
res.end(message);
} else {
+83 -1543
View File
File diff suppressed because it is too large. Load diff