Skip to content

feat: service worker preload scripts for improved extensions support #44411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,21 @@ webpack_build("electron_utility_bundle") {
out_file = "$target_gen_dir/js2c/utility_init.js"
}

webpack_build("electron_preload_realm_bundle") {
deps = [ ":build_electron_definitions" ]

inputs = auto_filenames.preload_realm_bundle_deps

config_file = "//electron/build/webpack/webpack.config.preload_realm.js"
out_file = "$target_gen_dir/js2c/preload_realm_bundle.js"
}

action("electron_js2c") {
deps = [
":electron_browser_bundle",
":electron_isolated_renderer_bundle",
":electron_node_bundle",
":electron_preload_realm_bundle",
":electron_renderer_bundle",
":electron_sandboxed_renderer_bundle",
":electron_utility_bundle",
Expand All @@ -240,6 +250,7 @@ action("electron_js2c") {
"$target_gen_dir/js2c/browser_init.js",
"$target_gen_dir/js2c/isolated_bundle.js",
"$target_gen_dir/js2c/node_init.js",
"$target_gen_dir/js2c/preload_realm_bundle.js",
"$target_gen_dir/js2c/renderer_init.js",
"$target_gen_dir/js2c/sandbox_bundle.js",
"$target_gen_dir/js2c/utility_init.js",
Expand Down
6 changes: 6 additions & 0 deletions build/webpack/webpack.config.preload_realm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = require('./webpack.config.base')({
target: 'preload_realm',
alwaysHasNode: false,
wrapInitWithProfilingTimeout: true,
wrapInitWithTryCatch: true
});
75 changes: 75 additions & 0 deletions docs/api/ipc-main-service-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Class: IpcMainServiceWorker

> Communicate asynchronously from the main process to service workers.

Process: [Main](../glossary.md#main-process)

> [!NOTE]
> This API is a subtle variation of [`IpcMain`](ipc-main.md)—targeted for
> communicating with service workers. For communicating with web frames,
> consult the `IpcMain` documentation.

<!-- TODO(samuelmaddock): refactor doc gen to allow generics to reduce duplication -->

### Instance Methods

#### `ipcMainServiceWorker.on(channel, listener)`

* `channel` string
* `listener` Function
* `event` [IpcMainServiceWorkerEvent][ipc-main-service-worker-event]
* `...args` any[]

Listens to `channel`, when a new message arrives `listener` would be called with
`listener(event, args...)`.

#### `ipcMainServiceWorker.once(channel, listener)`

* `channel` string
* `listener` Function
* `event` [IpcMainServiceWorkerEvent][ipc-main-service-worker-event]
* `...args` any[]

Adds a one time `listener` function for the event. This `listener` is invoked
only the next time a message is sent to `channel`, after which it is removed.

#### `ipcMainServiceWorker.removeListener(channel, listener)`

* `channel` string
* `listener` Function
* `...args` any[]

Removes the specified `listener` from the listener array for the specified
`channel`.

#### `ipcMainServiceWorker.removeAllListeners([channel])`

* `channel` string (optional)

Removes listeners of the specified `channel`.

#### `ipcMainServiceWorker.handle(channel, listener)`

* `channel` string
* `listener` Function\<Promise\<any\> | any\>
* `event` [IpcMainServiceWorkerInvokeEvent][ipc-main-service-worker-invoke-event]
* `...args` any[]

#### `ipcMainServiceWorker.handleOnce(channel, listener)`

* `channel` string
* `listener` Function\<Promise\<any\> | any\>
* `event` [IpcMainServiceWorkerInvokeEvent][ipc-main-service-worker-invoke-event]
* `...args` any[]

Handles a single `invoke`able IPC message, then removes the listener. See
`ipcMainServiceWorker.handle(channel, listener)`.

#### `ipcMainServiceWorker.removeHandler(channel)`

* `channel` string

Removes any handler for `channel`, if present.

[ipc-main-service-worker-event]:../api/structures/ipc-main-service-worker-event.md
[ipc-main-service-worker-invoke-event]:../api/structures/ipc-main-service-worker-invoke-event.md
1 change: 1 addition & 0 deletions docs/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ A `string` representing the current process's type, can be:

* `browser` - The main process
* `renderer` - A renderer process
* `service-worker` - In a service worker
* `worker` - In a web worker
* `utility` - In a node process launched as a service

Expand Down
20 changes: 20 additions & 0 deletions docs/api/service-worker-main.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ _This class is not exported from the `'electron'` module. It is only available a

Returns `boolean` - Whether the service worker has been destroyed.

#### `serviceWorker.send(channel, ...args)` _Experimental_

- `channel` string
- `...args` any[]

Send an asynchronous message to the service worker process via `channel`, along with
arguments. Arguments will be serialized with the [Structured Clone Algorithm][SCA],
just like [`postMessage`][], so prototype chains will not be included.
Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception.

The service worker process can handle the message by listening to `channel` with the
[`ipcRenderer`](ipc-renderer.md) module.

#### `serviceWorker.startTask()` _Experimental_

Returns `Object`:
Expand All @@ -25,10 +38,17 @@ Initiate a task to keep the service worker alive until ended.

### Instance Properties

#### `serviceWorker.ipc` _Readonly_ _Experimental_

An [`IpcMainServiceWorker`](ipc-main-service-worker.md) instance scoped to the service worker.

#### `serviceWorker.scope` _Readonly_ _Experimental_

A `string` representing the scope URL of the service worker.

#### `serviceWorker.versionId` _Readonly_ _Experimental_

A `number` representing the ID of the specific version of the service worker script in its scope.

[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
5 changes: 2 additions & 3 deletions docs/api/service-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ Returns `Promise<ServiceWorkerMain>` - Resolves with the service worker when it'

Starts the service worker or does nothing if already running.

<!-- TODO(samuelmaddock): extend example to send IPC after starting worker -->

```js
const { app, session } = require('electron')
const { serviceWorkers } = session.defaultSession
Expand All @@ -120,7 +118,8 @@ app.on('browser-window-created', async (event, window) => {
for (const scope of workerScopes) {
try {
// Ensure worker is started
await serviceWorkers.startWorkerForScope(scope)
const serviceWorker = await serviceWorkers.startWorkerForScope(scope)
serviceWorker.send('window-created', { windowId: window.id })
} catch (error) {
console.error(`Failed to start service worker for ${scope}`)
console.error(error)
Expand Down
1 change: 1 addition & 0 deletions docs/api/structures/ipc-main-event.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# IpcMainEvent Object extends `Event`

* `type` String - Possible values include `frame`
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer - The ID of the renderer frame that sent this message
* `returnValue` any - Set this to the value to be returned in a synchronous message
Expand Down
1 change: 1 addition & 0 deletions docs/api/structures/ipc-main-invoke-event.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# IpcMainInvokeEvent Object extends `Event`

* `type` String - Possible values include `frame`
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer - The ID of the renderer frame that sent this message
* `sender` [WebContents](../web-contents.md) - Returns the `webContents` that sent the message
Expand Down
11 changes: 11 additions & 0 deletions docs/api/structures/ipc-main-service-worker-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# IpcMainServiceWorkerEvent Object extends `Event`

* `type` String - Possible values include `service-worker`.
* `serviceWorker` [ServiceWorkerMain](../service-worker-main.md) _Readonly_ - The service worker that sent this message
* `versionId` Number - The service worker version ID.
* `session` Session - The [`Session`](../session.md) instance with which the event is associated.
* `returnValue` any - Set this to the value to be returned in a synchronous message
* `ports` [MessagePortMain](../message-port-main.md)[] - A list of MessagePorts that were transferred with this message
* `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guarantee the reply will go to the correct process and frame.
* `channel` string
* `...args` any[]
6 changes: 6 additions & 0 deletions docs/api/structures/ipc-main-service-worker-invoke-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# IpcMainServiceWorkerInvokeEvent Object extends `Event`

* `type` String - Possible values include `service-worker`.
* `serviceWorker` [ServiceWorkerMain](../service-worker-main.md) _Readonly_ - The service worker that sent this message
* `versionId` Number - The service worker version ID.
* `session` Session - The [`Session`](../session.md) instance with which the event is associated.
2 changes: 1 addition & 1 deletion docs/api/structures/preload-script-registration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PreloadScriptRegistration Object

* `type` string - Context type where the preload script will be executed.
Possible values include `frame`.
Possible values include `frame` or `service-worker`.
* `id` string (optional) - Unique ID of preload script. Defaults to a random UUID.
* `filePath` string - Path of the script file. Must be an absolute path.
2 changes: 1 addition & 1 deletion docs/api/structures/preload-script.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PreloadScript Object

* `type` string - Context type where the preload script will be executed.
Possible values include `frame`.
Possible values include `frame` or `service-worker`.
* `id` string - Unique ID of preload script.
* `filePath` string - Path of the script file. Must be an absolute path.
32 changes: 32 additions & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ auto_filenames = {
"docs/api/global-shortcut.md",
"docs/api/in-app-purchase.md",
"docs/api/incoming-message.md",
"docs/api/ipc-main-service-worker.md",
"docs/api/ipc-main.md",
"docs/api/ipc-renderer.md",
"docs/api/menu-item.md",
Expand Down Expand Up @@ -95,6 +96,8 @@ auto_filenames = {
"docs/api/structures/input-event.md",
"docs/api/structures/ipc-main-event.md",
"docs/api/structures/ipc-main-invoke-event.md",
"docs/api/structures/ipc-main-service-worker-event.md",
"docs/api/structures/ipc-main-service-worker-invoke-event.md",
"docs/api/structures/ipc-renderer-event.md",
"docs/api/structures/jump-list-category.md",
"docs/api/structures/jump-list-item.md",
Expand Down Expand Up @@ -172,6 +175,8 @@ auto_filenames = {
"lib/renderer/api/web-utils.ts",
"lib/renderer/common-init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
Expand Down Expand Up @@ -261,6 +266,7 @@ auto_filenames = {
"lib/browser/guest-view-manager.ts",
"lib/browser/guest-window-manager.ts",
"lib/browser/init.ts",
"lib/browser/ipc-dispatch.ts",
"lib/browser/ipc-main-impl.ts",
"lib/browser/ipc-main-internal-utils.ts",
"lib/browser/ipc-main-internal.ts",
Expand Down Expand Up @@ -305,6 +311,8 @@ auto_filenames = {
"lib/renderer/common-init.ts",
"lib/renderer/init.ts",
"lib/renderer/inspector.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/security-warnings.ts",
Expand Down Expand Up @@ -339,6 +347,7 @@ auto_filenames = {
"lib/renderer/api/module-list.ts",
"lib/renderer/api/web-frame.ts",
"lib/renderer/api/web-utils.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/worker/init.ts",
Expand Down Expand Up @@ -379,4 +388,27 @@ auto_filenames = {
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]

preload_realm_bundle_deps = [
"lib/common/api/native-image.ts",
"lib/common/define-properties.ts",
"lib/common/ipc-messages.ts",
"lib/common/webpack-globals-provider.ts",
"lib/preload_realm/api/exports/electron.ts",
"lib/preload_realm/api/module-list.ts",
"lib/preload_realm/init.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/ipc-renderer.ts",
"lib/renderer/ipc-native-setup.ts",
"lib/renderer/ipc-renderer-bindings.ts",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
"typings/internal-ambient.d.ts",
"typings/internal-electron.d.ts",
]
}
13 changes: 13 additions & 0 deletions filenames.gni
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ filenames = {
"shell/browser/api/gpu_info_enumerator.h",
"shell/browser/api/gpuinfo_manager.cc",
"shell/browser/api/gpuinfo_manager.h",
"shell/browser/api/ipc_dispatcher.h",
"shell/browser/api/message_port.cc",
"shell/browser/api/message_port.h",
"shell/browser/api/process_metric.cc",
Expand Down Expand Up @@ -355,6 +356,8 @@ filenames = {
"shell/browser/draggable_region_provider.h",
"shell/browser/electron_api_ipc_handler_impl.cc",
"shell/browser/electron_api_ipc_handler_impl.h",
"shell/browser/electron_api_sw_ipc_handler_impl.cc",
"shell/browser/electron_api_sw_ipc_handler_impl.h",
"shell/browser/electron_autofill_driver.cc",
"shell/browser/electron_autofill_driver.h",
"shell/browser/electron_autofill_driver_factory.cc",
Expand Down Expand Up @@ -658,6 +661,8 @@ filenames = {
"shell/common/gin_helper/pinnable.h",
"shell/common/gin_helper/promise.cc",
"shell/common/gin_helper/promise.h",
"shell/common/gin_helper/reply_channel.cc",
"shell/common/gin_helper/reply_channel.h",
"shell/common/gin_helper/trackable_object.cc",
"shell/common/gin_helper/trackable_object.h",
"shell/common/gin_helper/wrappable.cc",
Expand Down Expand Up @@ -707,14 +712,22 @@ filenames = {
"shell/renderer/electron_api_service_impl.h",
"shell/renderer/electron_autofill_agent.cc",
"shell/renderer/electron_autofill_agent.h",
"shell/renderer/electron_ipc_native.cc",
"shell/renderer/electron_ipc_native.h",
"shell/renderer/electron_render_frame_observer.cc",
"shell/renderer/electron_render_frame_observer.h",
"shell/renderer/electron_renderer_client.cc",
"shell/renderer/electron_renderer_client.h",
"shell/renderer/electron_sandboxed_renderer_client.cc",
"shell/renderer/electron_sandboxed_renderer_client.h",
"shell/renderer/preload_realm_context.cc",
"shell/renderer/preload_realm_context.h",
"shell/renderer/preload_utils.cc",
"shell/renderer/preload_utils.h",
"shell/renderer/renderer_client_base.cc",
"shell/renderer/renderer_client_base.h",
"shell/renderer/service_worker_data.cc",
"shell/renderer/service_worker_data.h",
"shell/renderer/web_worker_observer.cc",
"shell/renderer/web_worker_observer.h",
"shell/services/node/node_service.cc",
Expand Down
22 changes: 22 additions & 0 deletions lib/browser/api/service-worker-main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl';

const { ServiceWorkerMain } = process._linkedBinding('electron_browser_service_worker_main');

Object.defineProperty(ServiceWorkerMain.prototype, 'ipc', {
get () {
const ipc = new IpcMainImpl();
Object.defineProperty(this, 'ipc', { value: ipc });
return ipc;
}
});

ServiceWorkerMain.prototype.send = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new TypeError('Missing required channel argument');
}

try {
return this._send(false /* internal */, channel, args);
} catch (e) {
console.error('Error sending from ServiceWorkerMain: ', e);
}
};

ServiceWorkerMain.prototype.startTask = function () {
// TODO(samuelmaddock): maybe make timeout configurable in the future
const hasTimeout = false;
Expand Down
5 changes: 5 additions & 0 deletions lib/browser/api/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
import { addIpcDispatchListeners } from '@electron/internal/browser/ipc-dispatch';
import * as deprecate from '@electron/internal/common/deprecate';

import { net } from 'electron/main';
Expand All @@ -21,6 +22,10 @@ Object.defineProperty(systemPickerVideoSource, 'id', {
systemPickerVideoSource.name = '';
Object.freeze(systemPickerVideoSource);

Session.prototype._init = function () {
addIpcDispatchListeners(this, this.serviceWorkers);
};

Session.prototype.fetch = function (input: RequestInfo, init?: RequestInit) {
return fetchWithSession(input, init, this, net.request);
};
Expand Down
Loading
Loading