Browse Source

refactor: move extension APIs to session.extensions (#45597)

refactor: move extensions to session.extensions
Sam Maddock 1 month ago
parent
commit
e3f61b465d

+ 124 - 0
docs/api/extensions-api.md

@@ -0,0 +1,124 @@
+## Class: Extensions
+
+> Load and interact with extensions.
+
+Process: [Main](../glossary.md#main-process)<br />
+_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
+
+Instances of the `Extensions` class are accessed by using `extensions` property of
+a `Session`.
+
+### Instance Events
+
+The following events are available on instances of `Extensions`:
+
+#### Event: 'extension-loaded'
+
+Returns:
+
+* `event` Event
+* `extension` [Extension](structures/extension.md)
+
+Emitted after an extension is loaded. This occurs whenever an extension is
+added to the "enabled" set of extensions. This includes:
+
+* Extensions being loaded from `Extensions.loadExtension`.
+* Extensions being reloaded:
+  * from a crash.
+  * if the extension requested it ([`chrome.runtime.reload()`](https://developer.chrome.com/extensions/runtime#method-reload)).
+
+#### Event: 'extension-unloaded'
+
+Returns:
+
+* `event` Event
+* `extension` [Extension](structures/extension.md)
+
+Emitted after an extension is unloaded. This occurs when
+`Session.removeExtension` is called.
+
+#### Event: 'extension-ready'
+
+Returns:
+
+* `event` Event
+* `extension` [Extension](structures/extension.md)
+
+Emitted after an extension is loaded and all necessary browser state is
+initialized to support the start of the extension's background page.
+
+### Instance Methods
+
+The following methods are available on instances of `Extensions`:
+
+#### `extensions.loadExtension(path[, options])`
+
+* `path` string - Path to a directory containing an unpacked Chrome extension
+* `options` Object (optional)
+  * `allowFileAccess` boolean - Whether to allow the extension to read local files over `file://`
+    protocol and inject content scripts into `file://` pages. This is required e.g. for loading
+    devtools extensions on `file://` URLs. Defaults to false.
+
+Returns `Promise<Extension>` - resolves when the extension is loaded.
+
+This method will raise an exception if the extension could not be loaded. If
+there are warnings when installing the extension (e.g. if the extension
+requests an API that Electron does not support) then they will be logged to the
+console.
+
+Note that Electron does not support the full range of Chrome extensions APIs.
+See [Supported Extensions APIs](extensions.md#supported-extensions-apis) for
+more details on what is supported.
+
+Note that in previous versions of Electron, extensions that were loaded would
+be remembered for future runs of the application. This is no longer the case:
+`loadExtension` must be called on every boot of your app if you want the
+extension to be loaded.
+
+```js
+const { app, session } = require('electron')
+const path = require('node:path')
+
+app.whenReady().then(async () => {
+  await session.defaultSession.extensions.loadExtension(
+    path.join(__dirname, 'react-devtools'),
+    // allowFileAccess is required to load the devtools extension on file:// URLs.
+    { allowFileAccess: true }
+  )
+  // Note that in order to use the React DevTools extension, you'll need to
+  // download and unzip a copy of the extension.
+})
+```
+
+This API does not support loading packed (.crx) extensions.
+
+**Note:** This API cannot be called before the `ready` event of the `app` module
+is emitted.
+
+**Note:** Loading extensions into in-memory (non-persistent) sessions is not
+supported and will throw an error.
+
+#### `extensions.removeExtension(extensionId)`
+
+* `extensionId` string - ID of extension to remove
+
+Unloads an extension.
+
+**Note:** This API cannot be called before the `ready` event of the `app` module
+is emitted.
+
+#### `extensions.getExtension(extensionId)`
+
+* `extensionId` string - ID of extension to query
+
+Returns `Extension | null` - The loaded extension with the given ID.
+
+**Note:** This API cannot be called before the `ready` event of the `app` module
+is emitted.
+
+#### `extensions.getAllExtensions()`
+
+Returns `Extension[]` - A list of all loaded extensions.
+
+**Note:** This API cannot be called before the `ready` event of the `app` module
+is emitted.

+ 1 - 1
docs/api/extensions.md

@@ -14,7 +14,7 @@ but it also happens to support some other extension capabilities.
 
 Electron only supports loading unpacked extensions (i.e., `.crx` files do not
 work). Extensions are installed per-`session`. To load an extension, call
-[`ses.loadExtension`](session.md#sesloadextensionpath-options):
+[`ses.extensions.loadExtension`](extensions-api.md#extensionsloadextensionpath-options):
 
 ```js
 const { session } = require('electron')

+ 16 - 4
docs/api/session.md

@@ -1485,7 +1485,7 @@ will not work on non-persistent (in-memory) sessions.
 
 **Note:** On macOS and Windows 10 this word will be removed from the OS custom dictionary as well
 
-#### `ses.loadExtension(path[, options])`
+#### `ses.loadExtension(path[, options])` _Deprecated_
 
 * `path` string - Path to a directory containing an unpacked Chrome extension
 * `options` Object (optional)
@@ -1532,7 +1532,9 @@ is emitted.
 **Note:** Loading extensions into in-memory (non-persistent) sessions is not
 supported and will throw an error.
 
-#### `ses.removeExtension(extensionId)`
+**Deprecated:** Use the new `ses.extensions.loadExtension` API.
+
+#### `ses.removeExtension(extensionId)` _Deprecated_
 
 * `extensionId` string - ID of extension to remove
 
@@ -1541,7 +1543,9 @@ Unloads an extension.
 **Note:** This API cannot be called before the `ready` event of the `app` module
 is emitted.
 
-#### `ses.getExtension(extensionId)`
+**Deprecated:** Use the new `ses.extensions.removeExtension` API.
+
+#### `ses.getExtension(extensionId)` _Deprecated_
 
 * `extensionId` string - ID of extension to query
 
@@ -1550,13 +1554,17 @@ Returns `Extension | null` - The loaded extension with the given ID.
 **Note:** This API cannot be called before the `ready` event of the `app` module
 is emitted.
 
-#### `ses.getAllExtensions()`
+**Deprecated:** Use the new `ses.extensions.getExtension` API.
+
+#### `ses.getAllExtensions()` _Deprecated_
 
 Returns `Extension[]` - A list of all loaded extensions.
 
 **Note:** This API cannot be called before the `ready` event of the `app` module
 is emitted.
 
+**Deprecated:** Use the new `ses.extensions.getAllExtensions` API.
+
 #### `ses.getStoragePath()`
 
 Returns `string | null` - The absolute file system path where data for this
@@ -1619,6 +1627,10 @@ session is persisted on disk.  For in memory sessions this returns `null`.
 
 A [`Cookies`](cookies.md) object for this session.
 
+#### `ses.extensions` _Readonly_
+
+A [`Extensions`](extensions-api.md) object for this session.
+
 #### `ses.serviceWorkers` _Readonly_
 
 A [`ServiceWorkers`](service-workers.md) object for this session.

+ 7 - 0
docs/breaking-changes.md

@@ -14,6 +14,13 @@ This document uses the following convention to categorize breaking changes:
 
 ## Planned Breaking API Changes (36.0)
 
+### Deprecated: Extension methods and events on `session`
+
+`session.loadExtension`, `session.removeExtension`, `session.getExtension`,
+`session.getAllExtensions`, 'extension-loaded' event, 'extension-unloaded'
+event, and 'extension-ready' events have all moved to the new
+`session.extensions` class.
+
 ### Removed: `systemPreferences.isAeroGlassEnabled()`
 
 The `systemPreferences.isAeroGlassEnabled()` function has been removed without replacement.

+ 2 - 2
docs/tutorial/devtools-extension.md

@@ -96,9 +96,9 @@ of the extension is not working as expected.
 [devtools-extension]: https://developer.chrome.com/extensions/devtools
 [session]: ../api/session.md
 [react-devtools]: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
-[load-extension]: ../api/session.md#sesloadextensionpath-options
+[load-extension]: ../api/extensions-api.md#extensionsloadextensionpath-options
 [extension-structure]: ../api/structures/extension.md
-[remove-extension]: ../api/session.md#sesremoveextensionextensionid
+[remove-extension]: ../api/extensions-api.md#extensionsremoveextensionextensionid
 [electron-devtools-installer]: https://github.com/MarshallOfSound/electron-devtools-installer
 [supported-extension-apis]: ../api/extensions.md
 [issue-tracker]: https://github.com/electron/electron/issues

+ 1 - 0
filenames.auto.gni

@@ -21,6 +21,7 @@ auto_filenames = {
     "docs/api/dock.md",
     "docs/api/download-item.md",
     "docs/api/environment-variables.md",
+    "docs/api/extensions-api.md",
     "docs/api/extensions.md",
     "docs/api/global-shortcut.md",
     "docs/api/in-app-purchase.md",

+ 2 - 0
filenames.gni

@@ -739,6 +739,8 @@ filenames = {
   ]
 
   lib_sources_extensions = [
+    "shell/browser/api/electron_api_extensions.cc",
+    "shell/browser/api/electron_api_extensions.h",
     "shell/browser/extensions/api/extension_action/extension_action_api.cc",
     "shell/browser/extensions/api/extension_action/extension_action_api.h",
     "shell/browser/extensions/api/management/electron_management_api_delegate.cc",

+ 44 - 0
lib/browser/api/session.ts

@@ -24,6 +24,21 @@ Object.freeze(systemPickerVideoSource);
 
 Session.prototype._init = function () {
   addIpcDispatchListeners(this);
+
+  if (this.extensions) {
+    const rerouteExtensionEvent = (eventName: string) => {
+      const warn = deprecate.warnOnce(`${eventName} event`, `session.extensions ${eventName} event`);
+      this.extensions.on(eventName as any, (...args: any[]) => {
+        if (this.listenerCount(eventName) !== 0) {
+          warn();
+          this.emit(eventName, ...args);
+        }
+      });
+    };
+    rerouteExtensionEvent('extension-loaded');
+    rerouteExtensionEvent('extension-unloaded');
+    rerouteExtensionEvent('extension-ready');
+  }
 };
 
 Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
@@ -67,6 +82,35 @@ Session.prototype.setPreloads = function (preloads) {
   });
 };
 
+Session.prototype.getAllExtensions = deprecate.moveAPI(
+  function (this: Electron.Session) {
+    return this.extensions.getAllExtensions();
+  },
+  'session.getAllExtensions',
+  'session.extensions.getAllExtensions'
+);
+Session.prototype.getExtension = deprecate.moveAPI(
+  function (this: Electron.Session, extensionId) {
+    return this.extensions.getExtension(extensionId);
+  },
+  'session.getExtension',
+  'session.extensions.getExtension'
+);
+Session.prototype.loadExtension = deprecate.moveAPI(
+  function (this: Electron.Session, path, options) {
+    return this.extensions.loadExtension(path, options);
+  },
+  'session.loadExtension',
+  'session.extensions.loadExtension'
+);
+Session.prototype.removeExtension = deprecate.moveAPI(
+  function (this: Electron.Session, extensionId) {
+    return this.extensions.removeExtension(extensionId);
+  },
+  'session.removeExtension',
+  'session.extensions.removeExtension'
+);
+
 export default {
   fromPartition,
   fromPath,

+ 158 - 0
shell/browser/api/electron_api_extensions.cc

@@ -0,0 +1,158 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/api/electron_api_extensions.h"
+
+#include "chrome/browser/browser_process.h"
+#include "extensions/browser/extension_registry.h"
+#include "gin/data_object_builder.h"
+#include "gin/handle.h"
+#include "gin/object_template_builder.h"
+#include "shell/browser/api/electron_api_extensions.h"
+#include "shell/browser/electron_browser_context.h"
+#include "shell/browser/extensions/electron_extension_system.h"
+#include "shell/browser/javascript_environment.h"
+#include "shell/common/gin_converters/extension_converter.h"
+#include "shell/common/gin_converters/file_path_converter.h"
+#include "shell/common/gin_converters/gurl_converter.h"
+#include "shell/common/gin_converters/value_converter.h"
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/gin_helper/promise.h"
+#include "shell/common/node_util.h"
+
+namespace electron::api {
+
+gin::WrapperInfo Extensions::kWrapperInfo = {gin::kEmbedderNativeGin};
+
+Extensions::Extensions(v8::Isolate* isolate,
+                       ElectronBrowserContext* browser_context)
+    : browser_context_(browser_context) {
+  extensions::ExtensionRegistry::Get(browser_context)->AddObserver(this);
+}
+
+Extensions::~Extensions() {
+  extensions::ExtensionRegistry::Get(browser_context())->RemoveObserver(this);
+}
+
+// static
+gin::Handle<Extensions> Extensions::Create(
+    v8::Isolate* isolate,
+    ElectronBrowserContext* browser_context) {
+  return gin::CreateHandle(isolate, new Extensions(isolate, browser_context));
+}
+
+v8::Local<v8::Promise> Extensions::LoadExtension(
+    v8::Isolate* isolate,
+    const base::FilePath& extension_path,
+    gin::Arguments* args) {
+  gin_helper::Promise<const extensions::Extension*> promise(isolate);
+  v8::Local<v8::Promise> handle = promise.GetHandle();
+
+  if (!extension_path.IsAbsolute()) {
+    promise.RejectWithErrorMessage(
+        "The path to the extension in 'loadExtension' must be absolute");
+    return handle;
+  }
+
+  if (browser_context()->IsOffTheRecord()) {
+    promise.RejectWithErrorMessage(
+        "Extensions cannot be loaded in a temporary session");
+    return handle;
+  }
+
+  int load_flags = extensions::Extension::FOLLOW_SYMLINKS_ANYWHERE;
+  gin_helper::Dictionary options;
+  if (args->GetNext(&options)) {
+    bool allowFileAccess = false;
+    options.Get("allowFileAccess", &allowFileAccess);
+    if (allowFileAccess)
+      load_flags |= extensions::Extension::ALLOW_FILE_ACCESS;
+  }
+
+  auto* extension_system = static_cast<extensions::ElectronExtensionSystem*>(
+      extensions::ExtensionSystem::Get(browser_context()));
+  extension_system->LoadExtension(
+      extension_path, load_flags,
+      base::BindOnce(
+          [](gin_helper::Promise<const extensions::Extension*> promise,
+             const extensions::Extension* extension,
+             const std::string& error_msg) {
+            if (extension) {
+              if (!error_msg.empty())
+                util::EmitWarning(promise.isolate(), error_msg,
+                                  "ExtensionLoadWarning");
+              promise.Resolve(extension);
+            } else {
+              promise.RejectWithErrorMessage(error_msg);
+            }
+          },
+          std::move(promise)));
+
+  return handle;
+}
+
+void Extensions::RemoveExtension(const std::string& extension_id) {
+  auto* extension_system = static_cast<extensions::ElectronExtensionSystem*>(
+      extensions::ExtensionSystem::Get(browser_context()));
+  extension_system->RemoveExtension(extension_id);
+}
+
+v8::Local<v8::Value> Extensions::GetExtension(v8::Isolate* isolate,
+                                              const std::string& extension_id) {
+  auto* registry = extensions::ExtensionRegistry::Get(browser_context());
+  const extensions::Extension* extension =
+      registry->GetInstalledExtension(extension_id);
+  if (extension) {
+    return gin::ConvertToV8(isolate, extension);
+  } else {
+    return v8::Null(isolate);
+  }
+}
+
+v8::Local<v8::Value> Extensions::GetAllExtensions(v8::Isolate* isolate) {
+  auto* registry = extensions::ExtensionRegistry::Get(browser_context());
+  const extensions::ExtensionSet extensions =
+      registry->GenerateInstalledExtensionsSet();
+  std::vector<const extensions::Extension*> extensions_vector;
+  for (const auto& extension : extensions) {
+    if (extension->location() !=
+        extensions::mojom::ManifestLocation::kComponent)
+      extensions_vector.emplace_back(extension.get());
+  }
+  return gin::ConvertToV8(isolate, extensions_vector);
+}
+
+void Extensions::OnExtensionLoaded(content::BrowserContext* browser_context,
+                                   const extensions::Extension* extension) {
+  Emit("extension-loaded", extension);
+}
+
+void Extensions::OnExtensionUnloaded(
+    content::BrowserContext* browser_context,
+    const extensions::Extension* extension,
+    extensions::UnloadedExtensionReason reason) {
+  Emit("extension-unloaded", extension);
+}
+
+void Extensions::OnExtensionReady(content::BrowserContext* browser_context,
+                                  const extensions::Extension* extension) {
+  Emit("extension-ready", extension);
+}
+
+// static
+gin::ObjectTemplateBuilder Extensions::GetObjectTemplateBuilder(
+    v8::Isolate* isolate) {
+  return gin_helper::EventEmitterMixin<Extensions>::GetObjectTemplateBuilder(
+             isolate)
+      .SetMethod("loadExtension", &Extensions::LoadExtension)
+      .SetMethod("removeExtension", &Extensions::RemoveExtension)
+      .SetMethod("getExtension", &Extensions::GetExtension)
+      .SetMethod("getAllExtensions", &Extensions::GetAllExtensions);
+}
+
+const char* Extensions::GetTypeName() {
+  return "Extensions";
+}
+
+}  // namespace electron::api

+ 79 - 0
shell/browser/api/electron_api_extensions.h

@@ -0,0 +1,79 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_EXTENSIONS_H_
+#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_EXTENSIONS_H_
+
+#include "base/memory/raw_ptr.h"
+#include "extensions/browser/extension_registry.h"
+#include "extensions/browser/extension_registry_observer.h"
+#include "gin/wrappable.h"
+#include "shell/browser/event_emitter_mixin.h"
+
+namespace gin {
+template <typename T>
+class Handle;
+}  // namespace gin
+
+namespace electron {
+
+class ElectronBrowserContext;
+
+namespace api {
+
+class Extensions final : public gin::Wrappable<Extensions>,
+                         public gin_helper::EventEmitterMixin<Extensions>,
+                         private extensions::ExtensionRegistryObserver {
+ public:
+  static gin::Handle<Extensions> Create(
+      v8::Isolate* isolate,
+      ElectronBrowserContext* browser_context);
+
+  // gin::Wrappable
+  static gin::WrapperInfo kWrapperInfo;
+  gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
+      v8::Isolate* isolate) override;
+  const char* GetTypeName() override;
+
+  v8::Local<v8::Promise> LoadExtension(v8::Isolate* isolate,
+                                       const base::FilePath& extension_path,
+                                       gin::Arguments* args);
+  void RemoveExtension(const std::string& extension_id);
+  v8::Local<v8::Value> GetExtension(v8::Isolate* isolate,
+                                    const std::string& extension_id);
+  v8::Local<v8::Value> GetAllExtensions(v8::Isolate* isolate);
+
+  // extensions::ExtensionRegistryObserver:
+  void OnExtensionLoaded(content::BrowserContext* browser_context,
+                         const extensions::Extension* extension) override;
+  void OnExtensionReady(content::BrowserContext* browser_context,
+                        const extensions::Extension* extension) override;
+  void OnExtensionUnloaded(content::BrowserContext* browser_context,
+                           const extensions::Extension* extension,
+                           extensions::UnloadedExtensionReason reason) override;
+
+  // disable copy
+  Extensions(const Extensions&) = delete;
+  Extensions& operator=(const Extensions&) = delete;
+
+ protected:
+  explicit Extensions(v8::Isolate* isolate,
+                      ElectronBrowserContext* browser_context);
+  ~Extensions() override;
+
+ private:
+  content::BrowserContext* browser_context() const {
+    return browser_context_.get();
+  }
+
+  raw_ptr<content::BrowserContext> browser_context_;
+
+  base::WeakPtrFactory<Extensions> weak_ptr_factory_{this};
+};
+
+}  // namespace api
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_EXTENSIONS_H_

+ 13 - 114
shell/browser/api/electron_api_session.cc

@@ -97,9 +97,7 @@
 #include "url/origin.h"
 
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-#include "extensions/browser/extension_registry.h"
-#include "shell/browser/extensions/electron_extension_system.h"
-#include "shell/common/gin_converters/extension_converter.h"
+#include "shell/browser/api/electron_api_extensions.h"
 #endif
 
 #if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
@@ -569,10 +567,6 @@ Session::Session(v8::Isolate* isolate, ElectronBrowserContext* browser_context)
     service->SetHunspellObserver(this);
   }
 #endif
-
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-  extensions::ExtensionRegistry::Get(browser_context)->AddObserver(this);
-#endif
 }
 
 Session::~Session() {
@@ -584,10 +578,6 @@ Session::~Session() {
     service->SetHunspellObserver(nullptr);
   }
 #endif
-
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-  extensions::ExtensionRegistry::Get(browser_context())->RemoveObserver(this);
-#endif
 }
 
 void Session::OnDownloadCreated(content::DownloadManager* manager,
@@ -1305,103 +1295,6 @@ v8::Local<v8::Promise> Session::GetSharedDictionaryUsageInfo() {
   return handle;
 }
 
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-v8::Local<v8::Promise> Session::LoadExtension(
-    const base::FilePath& extension_path,
-    gin::Arguments* args) {
-  gin_helper::Promise<const extensions::Extension*> promise(isolate_);
-  v8::Local<v8::Promise> handle = promise.GetHandle();
-
-  if (!extension_path.IsAbsolute()) {
-    promise.RejectWithErrorMessage(
-        "The path to the extension in 'loadExtension' must be absolute");
-    return handle;
-  }
-
-  if (browser_context()->IsOffTheRecord()) {
-    promise.RejectWithErrorMessage(
-        "Extensions cannot be loaded in a temporary session");
-    return handle;
-  }
-
-  int load_flags = extensions::Extension::FOLLOW_SYMLINKS_ANYWHERE;
-  gin_helper::Dictionary options;
-  if (args->GetNext(&options)) {
-    bool allowFileAccess = false;
-    options.Get("allowFileAccess", &allowFileAccess);
-    if (allowFileAccess)
-      load_flags |= extensions::Extension::ALLOW_FILE_ACCESS;
-  }
-
-  auto* extension_system = static_cast<extensions::ElectronExtensionSystem*>(
-      extensions::ExtensionSystem::Get(browser_context()));
-  extension_system->LoadExtension(
-      extension_path, load_flags,
-      base::BindOnce(
-          [](gin_helper::Promise<const extensions::Extension*> promise,
-             const extensions::Extension* extension,
-             const std::string& error_msg) {
-            if (extension) {
-              if (!error_msg.empty())
-                util::EmitWarning(promise.isolate(), error_msg,
-                                  "ExtensionLoadWarning");
-              promise.Resolve(extension);
-            } else {
-              promise.RejectWithErrorMessage(error_msg);
-            }
-          },
-          std::move(promise)));
-
-  return handle;
-}
-
-void Session::RemoveExtension(const std::string& extension_id) {
-  auto* extension_system = static_cast<extensions::ElectronExtensionSystem*>(
-      extensions::ExtensionSystem::Get(browser_context()));
-  extension_system->RemoveExtension(extension_id);
-}
-
-v8::Local<v8::Value> Session::GetExtension(const std::string& extension_id) {
-  auto* registry = extensions::ExtensionRegistry::Get(browser_context());
-  const extensions::Extension* extension =
-      registry->GetInstalledExtension(extension_id);
-  if (extension) {
-    return gin::ConvertToV8(isolate_, extension);
-  } else {
-    return v8::Null(isolate_);
-  }
-}
-
-v8::Local<v8::Value> Session::GetAllExtensions() {
-  auto* registry = extensions::ExtensionRegistry::Get(browser_context());
-  const extensions::ExtensionSet extensions =
-      registry->GenerateInstalledExtensionsSet();
-  std::vector<const extensions::Extension*> extensions_vector;
-  for (const auto& extension : extensions) {
-    if (extension->location() !=
-        extensions::mojom::ManifestLocation::kComponent)
-      extensions_vector.emplace_back(extension.get());
-  }
-  return gin::ConvertToV8(isolate_, extensions_vector);
-}
-
-void Session::OnExtensionLoaded(content::BrowserContext* browser_context,
-                                const extensions::Extension* extension) {
-  Emit("extension-loaded", extension);
-}
-
-void Session::OnExtensionUnloaded(content::BrowserContext* browser_context,
-                                  const extensions::Extension* extension,
-                                  extensions::UnloadedExtensionReason reason) {
-  Emit("extension-unloaded", extension);
-}
-
-void Session::OnExtensionReady(content::BrowserContext* browser_context,
-                               const extensions::Extension* extension) {
-  Emit("extension-ready", extension);
-}
-#endif
-
 v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
   if (cookies_.IsEmpty()) {
     auto handle = Cookies::Create(isolate, browser_context());
@@ -1410,6 +1303,17 @@ v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
   return cookies_.Get(isolate);
 }
 
+v8::Local<v8::Value> Session::Extensions(v8::Isolate* isolate) {
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+  if (extensions_.IsEmpty()) {
+    v8::Local<v8::Value> handle;
+    handle = Extensions::Create(isolate, browser_context()).ToV8();
+    extensions_.Reset(isolate, handle);
+  }
+#endif
+  return extensions_.Get(isolate);
+}
+
 v8::Local<v8::Value> Session::Protocol(v8::Isolate* isolate) {
   return protocol_.Get(isolate);
 }
@@ -1872,12 +1776,6 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
                  &Session::ClearSharedDictionaryCache)
       .SetMethod("clearSharedDictionaryCacheForIsolationKey",
                  &Session::ClearSharedDictionaryCacheForIsolationKey)
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-      .SetMethod("loadExtension", &Session::LoadExtension)
-      .SetMethod("removeExtension", &Session::RemoveExtension)
-      .SetMethod("getExtension", &Session::GetExtension)
-      .SetMethod("getAllExtensions", &Session::GetAllExtensions)
-#endif
 #if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
       .SetMethod("getSpellCheckerLanguages", &Session::GetSpellCheckerLanguages)
       .SetMethod("setSpellCheckerLanguages", &Session::SetSpellCheckerLanguages)
@@ -1903,6 +1801,7 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
       .SetMethod("clearCodeCaches", &Session::ClearCodeCaches)
       .SetMethod("clearData", &Session::ClearData)
       .SetProperty("cookies", &Session::Cookies)
+      .SetProperty("extensions", &Session::Extensions)
       .SetProperty("netLog", &Session::NetLog)
       .SetProperty("protocol", &Session::Protocol)
       .SetProperty("serviceWorkers", &Session::ServiceWorkerContext)

+ 2 - 25
shell/browser/api/electron_api_session.h

@@ -29,11 +29,6 @@
 #include "chrome/browser/spellchecker/spellcheck_hunspell_dictionary.h"  // nogncheck
 #endif
 
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-#include "extensions/browser/extension_registry.h"
-#include "extensions/browser/extension_registry_observer.h"
-#endif
-
 class GURL;
 
 namespace base {
@@ -70,9 +65,6 @@ class Session final : public gin::Wrappable<Session>,
                       public IpcDispatcher<Session>,
 #if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
                       private SpellcheckHunspellDictionary::Observer,
-#endif
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-                      private extensions::ExtensionRegistryObserver,
 #endif
                       private content::DownloadManager::Observer {
  public:
@@ -156,6 +148,7 @@ class Session final : public gin::Wrappable<Session>,
   v8::Local<v8::Promise> ClearSharedDictionaryCacheForIsolationKey(
       const gin_helper::Dictionary& options);
   v8::Local<v8::Value> Cookies(v8::Isolate* isolate);
+  v8::Local<v8::Value> Extensions(v8::Isolate* isolate);
   v8::Local<v8::Value> Protocol(v8::Isolate* isolate);
   v8::Local<v8::Value> ServiceWorkerContext(v8::Isolate* isolate);
   v8::Local<v8::Value> WebRequest(v8::Isolate* isolate);
@@ -178,23 +171,6 @@ class Session final : public gin::Wrappable<Session>,
   bool IsSpellCheckerEnabled() const;
 #endif
 
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-  v8::Local<v8::Promise> LoadExtension(const base::FilePath& extension_path,
-                                       gin::Arguments* args);
-  void RemoveExtension(const std::string& extension_id);
-  v8::Local<v8::Value> GetExtension(const std::string& extension_id);
-  v8::Local<v8::Value> GetAllExtensions();
-
-  // extensions::ExtensionRegistryObserver:
-  void OnExtensionLoaded(content::BrowserContext* browser_context,
-                         const extensions::Extension* extension) override;
-  void OnExtensionReady(content::BrowserContext* browser_context,
-                        const extensions::Extension* extension) override;
-  void OnExtensionUnloaded(content::BrowserContext* browser_context,
-                           const extensions::Extension* extension,
-                           extensions::UnloadedExtensionReason reason) override;
-#endif
-
   // disable copy
   Session(const Session&) = delete;
   Session& operator=(const Session&) = delete;
@@ -223,6 +199,7 @@ class Session final : public gin::Wrappable<Session>,
 
   // Cached gin_helper::Wrappable objects.
   v8::Global<v8::Value> cookies_;
+  v8::Global<v8::Value> extensions_;
   v8::Global<v8::Value> protocol_;
   v8::Global<v8::Value> net_log_;
   v8::Global<v8::Value> service_worker_context_;

+ 2 - 2
spec/api-service-worker-main-spec.ts

@@ -401,7 +401,7 @@ describe('ServiceWorkerMain module', () => {
 
     it('can observe extension service workers', async () => {
       const serviceWorkerPromise = waitForServiceWorker();
-      const extension = await ses.loadExtension(testExtensionFixture);
+      const extension = await ses.extensions.loadExtension(testExtensionFixture);
       const serviceWorker = await serviceWorkerPromise;
       expect(serviceWorker.scope).to.equal(extension.url);
     });
@@ -409,7 +409,7 @@ describe('ServiceWorkerMain module', () => {
     it('has extension state available when preload runs', async () => {
       registerPreload('preload-send-extension.js');
       const serviceWorkerPromise = waitForServiceWorker();
-      const extensionPromise = ses.loadExtension(testExtensionFixture);
+      const extensionPromise = ses.extensions.loadExtension(testExtensionFixture);
       const serviceWorker = await serviceWorkerPromise;
       const result = await new Promise<any>((resolve) => {
         serviceWorker.ipc.handleOnce('preload-extension-result', (_event, result) => {

+ 57 - 57
spec/extensions-spec.ts

@@ -50,8 +50,8 @@ describe('chrome extensions', () => {
   });
   afterEach(closeAllWindows);
   afterEach(() => {
-    for (const e of session.defaultSession.getAllExtensions()) {
-      session.defaultSession.removeExtension(e.id);
+    for (const e of session.defaultSession.extensions.getAllExtensions()) {
+      session.defaultSession.extensions.removeExtension(e.id);
     }
   });
 
@@ -61,7 +61,7 @@ describe('chrome extensions', () => {
     await w.loadURL('about:blank');
 
     const promise = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
-    await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
+    await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
     const args: any = await promise;
     const wc: Electron.WebContents = args[1];
     await expect(wc.executeJavaScript(`
@@ -96,7 +96,7 @@ describe('chrome extensions', () => {
       await w.loadURL(url);
 
       const extPath = path.join(fixtures, 'extensions', 'host-permissions', 'malformed');
-      customSession.loadExtension(extPath);
+      customSession.extensions.loadExtension(extPath);
 
       const warning = await new Promise(resolve => { process.on('warning', resolve); });
 
@@ -107,7 +107,7 @@ describe('chrome extensions', () => {
 
     it('can grant special privileges to urls with host permissions', async () => {
       const extPath = path.join(fixtures, 'extensions', 'host-permissions', 'privileged-tab-info');
-      await customSession.loadExtension(extPath);
+      await customSession.extensions.loadExtension(extPath);
 
       await w.loadURL(url);
 
@@ -149,7 +149,7 @@ describe('chrome extensions', () => {
     await w.loadURL('about:blank');
 
     const extPath = path.join(fixtures, 'extensions', 'minimum-chrome-version');
-    const load = customSession.loadExtension(extPath);
+    const load = customSession.extensions.loadExtension(extPath);
     await expect(load).to.eventually.be.rejectedWith(
       `Loading extension at ${extPath} failed with: This extension requires Chromium version 999 or greater.`
     );
@@ -162,7 +162,7 @@ describe('chrome extensions', () => {
   it('bypasses CORS in requests made from extensions', async () => {
     const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
     const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } });
-    const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
+    const extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
     await w.loadURL(`${extension.url}bare-page.html`);
     await expect(fetch(w.webContents, `${url}/cors`)).to.not.be.rejectedWith(TypeError);
   });
@@ -173,7 +173,7 @@ describe('chrome extensions', () => {
     // extension in an in-memory session results in it being installed in the
     // default session.
     const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-    await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
+    await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
     await w.loadURL(url);
     const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
@@ -182,13 +182,13 @@ describe('chrome extensions', () => {
 
   it('does not crash when loading an extension with missing manifest', async () => {
     const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-    const promise = customSession.loadExtension(path.join(fixtures, 'extensions', 'missing-manifest'));
+    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 promise = customSession.loadExtension(path.join(fixtures, 'extensions', 'load-error'));
+    const promise = customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'load-error'));
     await expect(promise).to.eventually.be.rejected();
   });
 
@@ -196,7 +196,7 @@ describe('chrome extensions', () => {
     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 extension = await customSession.loadExtension(extensionPath);
+    const extension = await customSession.extensions.loadExtension(extensionPath);
     expect(extension.id).to.be.a('string');
     expect(extension.name).to.be.a('string');
     expect(extension.path).to.be.a('string');
@@ -207,14 +207,14 @@ describe('chrome extensions', () => {
 
   it('removes an extension', async () => {
     const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-    const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
+    const { id } = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     {
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(url);
       const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
       expect(bg).to.equal('red');
     }
-    customSession.removeExtension(id);
+    customSession.extensions.removeExtension(id);
     {
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(url);
@@ -226,40 +226,40 @@ describe('chrome extensions', () => {
   it('emits extension lifecycle events', async () => {
     const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
 
-    const loadedPromise = once(customSession, 'extension-loaded');
-    const readyPromise = emittedUntil(customSession, 'extension-ready', (event: Event, extension: Extension) => {
+    const loadedPromise = once(customSession.extensions, 'extension-loaded');
+    const readyPromise = emittedUntil(customSession.extensions, 'extension-ready', (event: Event, extension: Extension) => {
       return extension.name !== 'Chromium PDF Viewer';
     });
-    const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
+    const extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     const [, loadedExtension] = await loadedPromise;
     const [, readyExtension] = await readyPromise;
 
     expect(loadedExtension).to.deep.equal(extension);
     expect(readyExtension).to.deep.equal(extension);
 
-    const unloadedPromise = once(customSession, 'extension-unloaded');
-    await customSession.removeExtension(extension.id);
+    const unloadedPromise = once(customSession.extensions, 'extension-unloaded');
+    await customSession.extensions.removeExtension(extension.id);
     const [, unloadedExtension] = await unloadedPromise;
     expect(unloadedExtension).to.deep.equal(extension);
   });
 
   it('lists loaded extensions in getAllExtensions', async () => {
     const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-    const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
-    expect(customSession.getAllExtensions()).to.deep.equal([e]);
-    customSession.removeExtension(e.id);
-    expect(customSession.getAllExtensions()).to.deep.equal([]);
+    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);
+    expect(customSession.extensions.getAllExtensions()).to.deep.equal([]);
   });
 
   it('gets an extension by id', async () => {
     const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-    const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
-    expect(customSession.getExtension(e.id)).to.deep.equal(e);
+    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()}`);
-    await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
+    await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
     const w = new BrowserWindow({ show: false }); // not in the session
     await w.loadURL(url);
     const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
@@ -268,7 +268,7 @@ describe('chrome extensions', () => {
 
   it('loading an extension in a temporary session throws an error', async () => {
     const customSession = session.fromPartition(uuid.v4());
-    await expect(customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session');
+    await expect(customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session');
   });
 
   describe('chrome.i18n', () => {
@@ -282,7 +282,7 @@ describe('chrome extensions', () => {
     };
     beforeEach(async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v2'));
+      extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v2'));
       w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } });
       await w.loadURL(url);
     });
@@ -311,7 +311,7 @@ describe('chrome extensions', () => {
     };
     beforeEach(async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime'));
+      await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime'));
       w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } });
       await w.loadURL(url);
     });
@@ -343,7 +343,7 @@ describe('chrome extensions', () => {
   describe('chrome.storage', () => {
     it('stores and retrieves a key', async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage'));
+      await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } });
       try {
         const p = once(ipcMain, 'storage-success');
@@ -386,7 +386,7 @@ describe('chrome extensions', () => {
 
       it('can cancel http requests', async () => {
         await w.loadURL(url);
-        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
+        await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
         await expect(waitUntil(haveRejectedFetch)).to.eventually.be.fulfilled();
       });
 
@@ -405,7 +405,7 @@ describe('chrome extensions', () => {
           });
           await w.loadURL(url);
 
-          await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
+          await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest'));
           fetch(w.webContents, url);
         })();
       });
@@ -418,7 +418,7 @@ describe('chrome extensions', () => {
             resolve();
           });
           await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port: `${port}` } });
-          await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
+          await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
         })();
       });
     });
@@ -426,7 +426,7 @@ describe('chrome extensions', () => {
     describe('WebSocket', () => {
       it('can be proxied', async () => {
         await w.loadFile(path.join(fixtures, 'api', 'webrequest.html'), { query: { port: `${port}` } });
-        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
+        await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-webRequest-wss'));
         customSession.webRequest.onSendHeaders((details) => {
           if (details.url.startsWith('ws://')) {
             expect(details.requestHeaders.foo).be.equal('bar');
@@ -440,7 +440,7 @@ describe('chrome extensions', () => {
     let customSession: Session;
     before(async () => {
       customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
+      await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
     });
     afterEach(closeAllWindows);
 
@@ -512,7 +512,7 @@ describe('chrome extensions', () => {
     afterEach(closeAllWindows);
     it('loads a lazy background page when sending a message', async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
+      await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } });
       try {
         w.loadURL(url);
@@ -528,7 +528,7 @@ describe('chrome extensions', () => {
 
     it('can use extension.getBackgroundPage from a ui page', async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
+      const { id } = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(`chrome-extension://${id}/page-get-background.html`);
       const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise');
@@ -537,7 +537,7 @@ describe('chrome extensions', () => {
 
     it('can use extension.getBackgroundPage from a ui page', async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
+      const { id } = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(`chrome-extension://${id}/page-get-background.html`);
       const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise');
@@ -546,7 +546,7 @@ describe('chrome extensions', () => {
 
     it('can use runtime.getBackgroundPage from a ui page', async () => {
       const customSession = session.fromPartition(`persist:${uuid.v4()}`);
-      const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
+      const { id } = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
       const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
       await w.loadURL(`chrome-extension://${id}/page-runtime-get-background.html`);
       const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise');
@@ -556,7 +556,7 @@ describe('chrome extensions', () => {
     it('has session in background page', async () => {
       const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
       const promise = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
-      const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
+      const { id } = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
       const [, bgPageContents] = await promise;
       expect(bgPageContents.getType()).to.equal('backgroundPage');
       await once(bgPageContents, 'did-finish-load');
@@ -567,7 +567,7 @@ describe('chrome extensions', () => {
     it('can open devtools of background page', async () => {
       const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
       const promise = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
-      await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
+      await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
       const [, bgPageContents] = await promise;
       expect(bgPageContents.getType()).to.equal('backgroundPage');
       bgPageContents.openDevTools();
@@ -609,7 +609,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()}`);
-      customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
+      customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
       const winningMessage = once(ipcMain, 'winning');
       const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true, contextIsolation: false } });
       await w.loadURL(url);
@@ -623,10 +623,10 @@ describe('chrome extensions', () => {
     const fixtures = path.resolve(__dirname, 'fixtures');
     const extensionPath = path.resolve(fixtures, 'extensions');
 
-    const addExtension = (name: string) => session.defaultSession.loadExtension(path.resolve(extensionPath, name));
+    const addExtension = (name: string) => session.defaultSession.extensions.loadExtension(path.resolve(extensionPath, name));
     const removeAllExtensions = () => {
-      Object.keys(session.defaultSession.getAllExtensions()).forEach(extName => {
-        session.defaultSession.removeExtension(extName);
+      Object.keys(session.defaultSession.extensions.getAllExtensions()).forEach(extName => {
+        session.defaultSession.extensions.removeExtension(extName);
       });
     };
 
@@ -716,11 +716,11 @@ describe('chrome extensions', () => {
 
             ({ port } = await listen(server));
 
-            session.defaultSession.loadExtension(contentScript);
+            session.defaultSession.extensions.loadExtension(contentScript);
           });
 
           after(() => {
-            session.defaultSession.removeExtension('content-script-test');
+            session.defaultSession.extensions.removeExtension('content-script-test');
             server.close();
           });
 
@@ -779,14 +779,14 @@ describe('chrome extensions', () => {
 
   describe('extension ui pages', () => {
     afterEach(async () => {
-      for (const e of session.defaultSession.getAllExtensions()) {
-        session.defaultSession.removeExtension(e.id);
+      for (const e of session.defaultSession.extensions.getAllExtensions()) {
+        session.defaultSession.extensions.removeExtension(e.id);
       }
       await closeAllWindows();
     });
 
     it('loads a ui page of an extension', async () => {
-      const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
+      const { id } = await session.defaultSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
       const w = new BrowserWindow({ show: false });
       await w.loadURL(`chrome-extension://${id}/bare-page.html`);
       const textContent = await w.webContents.executeJavaScript('document.body.textContent');
@@ -794,7 +794,7 @@ describe('chrome extensions', () => {
     });
 
     it('can load resources', async () => {
-      const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
+      const { id } = await session.defaultSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
       const w = new BrowserWindow({ show: false });
       await w.loadURL(`chrome-extension://${id}/page-script-load.html`);
       const textContent = await w.webContents.executeJavaScript('document.body.textContent');
@@ -809,7 +809,7 @@ describe('chrome extensions', () => {
       const registrationPromise = new Promise<string>(resolve => {
         customSession.serviceWorkers.once('registration-completed', (event, { scope }) => resolve(scope));
       });
-      const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
+      const extension = await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
       const scope = await registrationPromise;
       expect(scope).equals(extension.url);
     });
@@ -817,7 +817,7 @@ describe('chrome extensions', () => {
     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 } });
-      await customSession.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
+      await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'mv3-service-worker'));
 
       await w.loadURL(url);
 
@@ -835,7 +835,7 @@ describe('chrome extensions', () => {
 
       before(async () => {
         customSession = session.fromPartition(`persist:${uuid.v4()}`);
-        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v3'));
+        await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n', 'v3'));
       });
 
       beforeEach(() => {
@@ -927,7 +927,7 @@ describe('chrome extensions', () => {
 
       before(async () => {
         customSession = session.fromPartition(`persist:${uuid.v4()}`);
-        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-action-fail'));
+        await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-action-fail'));
       });
 
       beforeEach(() => {
@@ -984,7 +984,7 @@ describe('chrome extensions', () => {
 
       before(async () => {
         customSession = session.fromPartition(`persist:${uuid.v4()}`);
-        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
+        await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
       });
 
       beforeEach(() => {
@@ -1082,7 +1082,7 @@ describe('chrome extensions', () => {
 
         it('does not return privileged properties without tabs permission', async () => {
           const noPrivilegeSes = session.fromPartition(`persist:${uuid.v4()}`);
-          await noPrivilegeSes.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'no-privileges'));
+          await noPrivilegeSes.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'no-privileges'));
 
           w = new BrowserWindow({ show: false, webPreferences: { session: noPrivilegeSes } });
           await w.loadURL(url);
@@ -1263,7 +1263,7 @@ describe('chrome extensions', () => {
 
       before(async () => {
         customSession = session.fromPartition(`persist:${uuid.v4()}`);
-        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-scripting'));
+        await customSession.extensions.loadExtension(path.join(fixtures, 'extensions', 'chrome-scripting'));
       });
 
       beforeEach(() => {