feat: redesign preload APIs (#45329)

* feat: redesign preload APIs

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* docs: remove service-worker mentions for now

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix lint

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* remove service-worker ipc code

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* add filename

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix: web preferences preload not included

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix: missing common init

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

* fix: preload bundle script error

Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>
This commit is contained in:
trop[bot] 2025-01-31 09:46:17 -05:00 committed by GitHub
parent e9b3e4cc91
commit 9d696ceffe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 459 additions and 149 deletions

View file

@ -1330,18 +1330,43 @@ the initial state will be `interrupted`. The download will start only when the
Returns `Promise<void>` - resolves when the sessions HTTP authentication cache has been cleared.
#### `ses.setPreloads(preloads)`
#### `ses.setPreloads(preloads)` _Deprecated_
* `preloads` string[] - An array of absolute path to preload scripts
Adds scripts that will be executed on ALL web contents that are associated with
this session just before normal `preload` scripts run.
#### `ses.getPreloads()`
**Deprecated:** Use the new `ses.registerPreloadScript` API.
#### `ses.getPreloads()` _Deprecated_
Returns `string[]` an array of paths to preload scripts that have been
registered.
**Deprecated:** Use the new `ses.getPreloadScripts` API. This will only return preload script paths
for `frame` context types.
#### `ses.registerPreloadScript(script)`
* `script` [PreloadScriptRegistration](structures/preload-script-registration.md) - Preload script
Registers preload script that will be executed in its associated context type in this session. For
`frame` contexts, this will run prior to any preload defined in the web preferences of a
WebContents.
Returns `string` - The ID of the registered preload script.
#### `ses.unregisterPreloadScript(id)`
* `id` string - Preload script ID
Unregisters script.
#### `ses.getPreloadScripts()`
Returns [`PreloadScript[]`](structures/preload-script.md): An array of paths to preload scripts that have been registered.
#### `ses.setCodeCachePath(path)`
* `path` String - Absolute path to store the v8 generated JS code cache from the renderer.

View file

@ -0,0 +1,6 @@
# PreloadScriptRegistration Object
* `type` string - Context type where the preload script will be executed.
Possible values include `frame`.
* `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.

View file

@ -0,0 +1,6 @@
# PreloadScript Object
* `type` string - Context type where the preload script will be executed.
Possible values include `frame`.
* `id` string - Unique ID of preload script.
* `filePath` string - Path of the script file. Must be an absolute path.

View file

@ -14,6 +14,25 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (35.0)
### Deprecated: `setPreloads`, `getPreloads` on `Session`
`registerPreloadScript`, `unregisterPreloadScript`, and `getPreloadScripts` are introduced as a
replacement for the deprecated methods. These new APIs allow third-party libraries to register
preload scripts without replacing existing scripts. Also, the new `type` option allows for
additional preload targets beyond `frame`.
```ts
// Deprecated
session.setPreloads([path.join(__dirname, 'preload.js')])
// Replace with:
session.registerPreloadScript({
type: 'frame',
id: 'app-preload',
filePath: path.join(__dirname, 'preload.js')
})
```
### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents`
The `console-message` event on `WebContents` has been updated to provide details on the `Event`

View file

@ -114,6 +114,8 @@ auto_filenames = {
"docs/api/structures/permission-request.md",
"docs/api/structures/point.md",
"docs/api/structures/post-body.md",
"docs/api/structures/preload-script-registration.md",
"docs/api/structures/preload-script.md",
"docs/api/structures/printer-info.md",
"docs/api/structures/process-memory-info.md",
"docs/api/structures/process-metric.md",
@ -183,6 +185,8 @@ auto_filenames = {
"lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts",
"lib/sandboxed_renderer/init.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",

View file

@ -474,6 +474,7 @@ filenames = {
"shell/browser/osr/osr_web_contents_view.h",
"shell/browser/plugins/plugin_utils.cc",
"shell/browser/plugins/plugin_utils.h",
"shell/browser/preload_script.h",
"shell/browser/protocol_registry.cc",
"shell/browser/protocol_registry.h",
"shell/browser/relauncher.cc",

View file

@ -1,4 +1,5 @@
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
import * as deprecate from '@electron/internal/common/deprecate';
import { net } from 'electron/main';
@ -36,6 +37,31 @@ Session.prototype.setDisplayMediaRequestHandler = function (handler, opts) {
}, opts);
};
const getPreloadsDeprecated = deprecate.warnOnce('session.getPreloads', 'session.getPreloadScripts');
Session.prototype.getPreloads = function () {
getPreloadsDeprecated();
return this.getPreloadScripts()
.filter((script) => script.type === 'frame')
.map((script) => script.filePath);
};
const setPreloadsDeprecated = deprecate.warnOnce('session.setPreloads', 'session.registerPreloadScript');
Session.prototype.setPreloads = function (preloads) {
setPreloadsDeprecated();
this.getPreloadScripts()
.filter((script) => script.type === 'frame')
.forEach((script) => {
this.unregisterPreloadScript(script.id);
});
preloads.map(filePath => ({
type: 'frame',
filePath,
_deprecated: true
}) as Electron.PreloadScriptRegistration).forEach(script => {
this.registerPreloadScript(script);
});
};
export default {
fromPartition,
fromPath,

View file

@ -5,6 +5,7 @@ import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { clipboard } from 'electron/common';
import * as fs from 'fs';
import * as path from 'path';
// Implements window.close()
ipcMainInternal.on(IPC_MESSAGES.BROWSER_WINDOW_CLOSE, function (event) {
@ -43,22 +44,40 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_CLIPBOARD_SYNC, function (event, me
return (clipboard as any)[method](...args);
});
const getPreloadScript = async function (preloadPath: string) {
let preloadSrc = null;
let preloadError = null;
const getPreloadScriptsFromEvent = (event: ElectronInternal.IpcMainInternalEvent) => {
const session: Electron.Session = event.sender.session;
const preloadScripts = session.getPreloadScripts();
const framePreloads = preloadScripts.filter(script => script.type === 'frame');
const webPrefPreload = event.sender._getPreloadScript();
if (webPrefPreload) framePreloads.push(webPrefPreload);
// TODO(samuelmaddock): Remove filter after Session.setPreloads is fully
// deprecated. The new API will prevent relative paths from being registered.
return framePreloads.filter(script => path.isAbsolute(script.filePath));
};
const readPreloadScript = async function (script: Electron.PreloadScript): Promise<ElectronInternal.PreloadScript> {
let contents;
let error;
try {
preloadSrc = await fs.promises.readFile(preloadPath, 'utf8');
} catch (error) {
preloadError = error;
contents = await fs.promises.readFile(script.filePath, 'utf8');
} catch (err) {
if (err instanceof Error) {
error = err;
}
}
return { preloadPath, preloadSrc, preloadError };
return {
...script,
contents,
error
};
};
ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event) {
const preloadPaths = event.sender._getPreloadPaths();
const preloadScripts = getPreloadScriptsFromEvent(event);
return {
preloadScripts: await Promise.all(preloadPaths.map(path => getPreloadScript(path))),
preloadScripts: await Promise.all(preloadScripts.map(readPreloadScript)),
process: {
arch: process.arch,
platform: process.platform,
@ -71,7 +90,8 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event
});
ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_NONSANDBOX_LOAD, function (event) {
return { preloadPaths: event.sender._getPreloadPaths() };
const preloadScripts = getPreloadScriptsFromEvent(event);
return { preloadPaths: preloadScripts.map(script => script.filePath) };
});
ipcMainInternal.on(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, function (event, preloadPath: string, error: Error) {

View file

@ -1,6 +1,7 @@
import '@electron/internal/sandboxed_renderer/pre-init';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import type * as ipcRendererInternalModule from '@electron/internal/renderer/ipc-renderer-internal';
import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils';
import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload';
import * as events from 'events';
import { setImmediate, clearImmediate } from 'timers';
@ -11,35 +12,14 @@ declare const binding: {
createPreloadScript: (src: string) => Function
};
const { EventEmitter } = events;
process._linkedBinding = binding.get;
const v8Util = process._linkedBinding('electron_common_v8_util');
// Expose Buffer shim as a hidden value. This is used by C++ code to
// deserialize Buffer instances sent from browser process.
v8Util.setHiddenValue(global, 'Buffer', Buffer);
// The process object created by webpack is not an event emitter, fix it so
// the API is more compatible with non-sandboxed renderers.
for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) {
if (Object.hasOwn(process, prop)) {
delete process[prop];
}
}
Object.setPrototypeOf(process, EventEmitter.prototype);
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule;
const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const {
preloadScripts,
process: processProps
} = ipcRendererUtils.invokeSync<{
preloadScripts: {
preloadPath: string;
preloadSrc: string | null;
preloadError: null | Error;
}[];
preloadScripts: ElectronInternal.PreloadScript[];
process: NodeJS.Process;
}>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
@ -60,8 +40,7 @@ const loadableModules = new Map<string, Function>([
['node:url', () => require('url')]
]);
// Pass different process object to the preload script.
const preloadProcess: NodeJS.Process = new EventEmitter() as any;
const preloadProcess = createPreloadProcessObject();
// InvokeEmitProcessEvent in ElectronSandboxedRendererClient will look for this
v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
@ -72,77 +51,22 @@ v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => {
Object.assign(preloadProcess, binding.process);
Object.assign(preloadProcess, processProps);
Object.assign(process, binding.process);
Object.assign(process, processProps);
process.getProcessMemoryInfo = preloadProcess.getProcessMemoryInfo = () => {
return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
};
Object.defineProperty(preloadProcess, 'noDeprecation', {
get () {
return process.noDeprecation;
},
set (value) {
process.noDeprecation = value;
}
});
// This is the `require` function that will be visible to the preload script
function preloadRequire (module: string) {
if (loadedModules.has(module)) {
return loadedModules.get(module);
}
if (loadableModules.has(module)) {
const loadedModule = loadableModules.get(module)!();
loadedModules.set(module, loadedModule);
return loadedModule;
}
throw new Error(`module not found: ${module}`);
}
// Process command line arguments.
const { hasSwitch } = process._linkedBinding('electron_common_command_line');
// Similar to nodes --expose-internals flag, this exposes _linkedBinding so
// that tests can call it to get access to some test only bindings
if (hasSwitch('unsafely-expose-electron-internals-for-testing')) {
preloadProcess._linkedBinding = process._linkedBinding;
}
// Common renderer initialization
require('@electron/internal/renderer/common-init');
// Wrap the script into a function executed in global scope. It won't have
// access to the current scope, so we'll expose a few objects as arguments:
//
// - `require`: The `preloadRequire` function
// - `process`: The `preloadProcess` object
// - `Buffer`: Shim of `Buffer` implementation
// - `global`: The window object, which is aliased to `global` by webpack.
function runPreloadScript (preloadSrc: string) {
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports, module) {
${preloadSrc}
})`;
// eval in window scope
const preloadFn = binding.createPreloadScript(preloadWrapperSrc);
const exports = {};
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate, exports, { exports });
}
for (const { preloadPath, preloadSrc, preloadError } of preloadScripts) {
try {
if (preloadSrc) {
runPreloadScript(preloadSrc);
} else if (preloadError) {
throw preloadError;
}
} catch (error) {
console.error(`Unable to load preload script: ${preloadPath}`);
console.error(error);
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, preloadPath, error);
executeSandboxedPreloadScripts({
loadedModules,
loadableModules,
process: preloadProcess,
createPreloadScript: binding.createPreloadScript,
exposeGlobals: {
Buffer,
// FIXME(samuelmaddock): workaround webpack bug replacing this with just
// `__webpack_require__.g,` which causes script error
global: globalThis,
setImmediate,
clearImmediate
}
}
}, preloadScripts);

View file

@ -0,0 +1,30 @@
// Pre-initialization code for sandboxed renderers.
import * as events from 'events';
declare const binding: {
get: (name: string) => any;
process: NodeJS.Process;
};
// Expose internal binding getter.
process._linkedBinding = binding.get;
const { EventEmitter } = events;
const v8Util = process._linkedBinding('electron_common_v8_util');
// Include properties from script 'binding' parameter.
Object.assign(process, binding.process);
// Expose Buffer shim as a hidden value. This is used by C++ code to
// deserialize Buffer instances sent from browser process.
v8Util.setHiddenValue(global, 'Buffer', Buffer);
// The process object created by webpack is not an event emitter, fix it so
// the API is more compatible with non-sandboxed renderers.
for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) {
if (Object.hasOwn(process, prop)) {
delete process[prop];
}
}
Object.setPrototypeOf(process, EventEmitter.prototype);

View file

@ -0,0 +1,101 @@
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import { EventEmitter } from 'events';
interface PreloadContext {
loadedModules: Map<string, any>;
loadableModules: Map<string, any>;
/** Process object to pass into preloads. */
process: NodeJS.Process;
createPreloadScript: (src: string) => Function
/** Globals to be exposed to preload context. */
exposeGlobals: any;
}
export function createPreloadProcessObject (): NodeJS.Process {
const preloadProcess: NodeJS.Process = new EventEmitter() as any;
preloadProcess.getProcessMemoryInfo = () => {
return ipcRendererInternal.invoke<Electron.ProcessMemoryInfo>(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO);
};
Object.defineProperty(preloadProcess, 'noDeprecation', {
get () {
return process.noDeprecation;
},
set (value) {
process.noDeprecation = value;
}
});
const { hasSwitch } = process._linkedBinding('electron_common_command_line');
// Similar to nodes --expose-internals flag, this exposes _linkedBinding so
// that tests can call it to get access to some test only bindings
if (hasSwitch('unsafely-expose-electron-internals-for-testing')) {
preloadProcess._linkedBinding = process._linkedBinding;
}
return preloadProcess;
}
// This is the `require` function that will be visible to the preload script
function preloadRequire (context: PreloadContext, module: string) {
if (context.loadedModules.has(module)) {
return context.loadedModules.get(module);
}
if (context.loadableModules.has(module)) {
const loadedModule = context.loadableModules.get(module)!();
context.loadedModules.set(module, loadedModule);
return loadedModule;
}
throw new Error(`module not found: ${module}`);
}
// Wrap the script into a function executed in global scope. It won't have
// access to the current scope, so we'll expose a few objects as arguments:
//
// - `require`: The `preloadRequire` function
// - `process`: The `preloadProcess` object
// - `Buffer`: Shim of `Buffer` implementation
// - `global`: The window object, which is aliased to `global` by webpack.
function runPreloadScript (context: PreloadContext, preloadSrc: string) {
const globalVariables = [];
const fnParameters = [];
for (const [key, value] of Object.entries(context.exposeGlobals)) {
globalVariables.push(key);
fnParameters.push(value);
}
const preloadWrapperSrc = `(function(require, process, exports, module, ${globalVariables.join(', ')}) {
${preloadSrc}
})`;
// eval in window scope
const preloadFn = context.createPreloadScript(preloadWrapperSrc);
const exports = {};
preloadFn(preloadRequire.bind(null, context), context.process, exports, { exports }, ...fnParameters);
}
/**
* Execute preload scripts within a sandboxed process.
*/
export function executeSandboxedPreloadScripts (context: PreloadContext, preloadScripts: ElectronInternal.PreloadScript[]) {
for (const { filePath, contents, error } of preloadScripts) {
try {
if (contents) {
runPreloadScript(context, contents);
} else if (error) {
throw error;
}
} catch (error) {
console.error(`Unable to load preload script: ${filePath}`);
console.error(error);
ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, filePath, error);
}
}
}

View file

@ -1065,16 +1065,72 @@ void Session::CreateInterruptedDownload(const gin_helper::Dictionary& options) {
base::Time::FromSecondsSinceUnixEpoch(start_time)));
}
void Session::SetPreloads(const std::vector<base::FilePath>& preloads) {
std::string Session::RegisterPreloadScript(
gin_helper::ErrorThrower thrower,
const PreloadScript& new_preload_script) {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs);
prefs->set_preloads(preloads);
auto& preload_scripts = prefs->preload_scripts();
auto it = std::find_if(preload_scripts.begin(), preload_scripts.end(),
[&new_preload_script](const PreloadScript& script) {
return script.id == new_preload_script.id;
});
if (it != preload_scripts.end()) {
thrower.ThrowError(base::StringPrintf(
"Cannot register preload script with existing ID '%s'",
new_preload_script.id.c_str()));
return "";
}
if (!new_preload_script.file_path.IsAbsolute()) {
// Deprecated preload scripts logged error without throwing.
if (new_preload_script.deprecated) {
LOG(ERROR) << "preload script must have absolute path: "
<< new_preload_script.file_path;
} else {
thrower.ThrowError(
base::StringPrintf("Preload script must have absolute path: %s",
new_preload_script.file_path.value().c_str()));
return "";
}
}
preload_scripts.push_back(new_preload_script);
return new_preload_script.id;
}
std::vector<base::FilePath> Session::GetPreloads() const {
void Session::UnregisterPreloadScript(gin_helper::ErrorThrower thrower,
const std::string& script_id) {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs);
return prefs->preloads();
auto& preload_scripts = prefs->preload_scripts();
// Find the preload script by its ID
auto it = std::find_if(preload_scripts.begin(), preload_scripts.end(),
[&script_id](const PreloadScript& script) {
return script.id == script_id;
});
// If the script is found, erase it from the vector
if (it != preload_scripts.end()) {
preload_scripts.erase(it);
return;
}
// If the script is not found, throw an error
thrower.ThrowError(base::StringPrintf(
"Cannot unregister preload script with non-existing ID '%s'",
script_id.c_str()));
}
std::vector<PreloadScript> Session::GetPreloadScripts() const {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs);
return prefs->preload_scripts();
}
/**
@ -1800,8 +1856,9 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("downloadURL", &Session::DownloadURL)
.SetMethod("createInterruptedDownload",
&Session::CreateInterruptedDownload)
.SetMethod("setPreloads", &Session::SetPreloads)
.SetMethod("getPreloads", &Session::GetPreloads)
.SetMethod("registerPreloadScript", &Session::RegisterPreloadScript)
.SetMethod("unregisterPreloadScript", &Session::UnregisterPreloadScript)
.SetMethod("getPreloadScripts", &Session::GetPreloadScripts)
.SetMethod("getSharedDictionaryUsageInfo",
&Session::GetSharedDictionaryUsageInfo)
.SetMethod("getSharedDictionaryInfo", &Session::GetSharedDictionaryInfo)

View file

@ -57,6 +57,7 @@ class ProxyConfig;
namespace electron {
class ElectronBrowserContext;
struct PreloadScript;
namespace api {
@ -141,8 +142,11 @@ class Session final : public gin::Wrappable<Session>,
const std::string& uuid);
void DownloadURL(const GURL& url, gin::Arguments* args);
void CreateInterruptedDownload(const gin_helper::Dictionary& options);
void SetPreloads(const std::vector<base::FilePath>& preloads);
std::vector<base::FilePath> GetPreloads() const;
std::string RegisterPreloadScript(gin_helper::ErrorThrower thrower,
const PreloadScript& new_preload_script);
void UnregisterPreloadScript(gin_helper::ErrorThrower thrower,
const std::string& script_id);
std::vector<PreloadScript> GetPreloadScripts() const;
v8::Local<v8::Promise> GetSharedDictionaryInfo(
const gin_helper::Dictionary& options);
v8::Local<v8::Promise> GetSharedDictionaryUsageInfo();

View file

@ -3757,16 +3757,15 @@ void WebContents::DoGetZoomLevel(
std::move(callback).Run(GetZoomLevel());
}
std::vector<base::FilePath> WebContents::GetPreloadPaths() const {
auto result = SessionPreferences::GetValidPreloads(GetBrowserContext());
std::optional<PreloadScript> WebContents::GetPreloadScript() const {
if (auto* web_preferences = WebContentsPreferences::From(web_contents())) {
if (auto preload = web_preferences->GetPreloadPath()) {
result.emplace_back(*preload);
auto preload_script = PreloadScript{
"", PreloadScript::ScriptType::kWebFrame, preload.value()};
return preload_script;
}
}
return result;
return std::nullopt;
}
v8::Local<v8::Value> WebContents::GetLastWebPreferences(
@ -4520,7 +4519,7 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("setZoomFactor", &WebContents::SetZoomFactor)
.SetMethod("getZoomFactor", &WebContents::GetZoomFactor)
.SetMethod("getType", &WebContents::type)
.SetMethod("_getPreloadPaths", &WebContents::GetPreloadPaths)
.SetMethod("_getPreloadScript", &WebContents::GetPreloadScript)
.SetMethod("getLastWebPreferences", &WebContents::GetLastWebPreferences)
.SetMethod("getOwnerBrowserWindow", &WebContents::GetOwnerBrowserWindow)
.SetMethod("inspectServiceWorker", &WebContents::InspectServiceWorker)

View file

@ -40,6 +40,7 @@
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/extended_web_contents_observer.h"
#include "shell/browser/osr/osr_paint_event.h"
#include "shell/browser/preload_script.h"
#include "shell/browser/ui/inspectable_web_contents_delegate.h"
#include "shell/browser/ui/inspectable_web_contents_view_delegate.h"
#include "shell/common/gin_helper/cleaned_up_at_exit.h"
@ -344,8 +345,8 @@ class WebContents final : public ExclusiveAccessContext,
const std::string& features,
const scoped_refptr<network::ResourceRequestBody>& body);
// Returns the preload script path of current WebContents.
std::vector<base::FilePath> GetPreloadPaths() const;
// Returns the preload script of current WebContents.
std::optional<PreloadScript> GetPreloadScript() const;
// Returns the web preferences of current WebContents.
v8::Local<v8::Value> GetLastWebPreferences(v8::Isolate* isolate) const;

View file

@ -0,0 +1,104 @@
// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_
#define ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_
#include <string_view>
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_path.h"
#include "base/uuid.h"
#include "gin/converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
namespace electron {
struct PreloadScript {
enum class ScriptType { kWebFrame, kServiceWorker };
std::string id;
ScriptType script_type;
base::FilePath file_path;
// If set, use the deprecated validation behavior of Session.setPreloads
bool deprecated = false;
};
} // namespace electron
namespace gin {
using electron::PreloadScript;
template <>
struct Converter<PreloadScript::ScriptType> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const PreloadScript::ScriptType& in) {
using Val = PreloadScript::ScriptType;
static constexpr auto Lookup =
base::MakeFixedFlatMap<Val, std::string_view>({
{Val::kWebFrame, "frame"},
{Val::kServiceWorker, "service-worker"},
});
return StringToV8(isolate, Lookup.at(in));
}
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
PreloadScript::ScriptType* out) {
using Val = PreloadScript::ScriptType;
static constexpr auto Lookup =
base::MakeFixedFlatMap<std::string_view, Val>({
{"frame", Val::kWebFrame},
{"service-worker", Val::kServiceWorker},
});
return FromV8WithLookup(isolate, val, Lookup, out);
}
};
template <>
struct Converter<PreloadScript> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const PreloadScript& script) {
gin::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("filePath", script.file_path.AsUTF8Unsafe());
dict.Set("id", script.id);
dict.Set("type", script.script_type);
return ConvertToV8(isolate, dict).As<v8::Object>();
}
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
PreloadScript* out) {
gin_helper::Dictionary options;
if (!ConvertFromV8(isolate, val, &options))
return false;
if (PreloadScript::ScriptType script_type;
options.Get("type", &script_type)) {
out->script_type = script_type;
} else {
return false;
}
if (base::FilePath file_path; options.Get("filePath", &file_path)) {
out->file_path = file_path;
} else {
return false;
}
if (std::string id; options.Get("id", &id)) {
out->id = id;
} else {
out->id = base::Uuid::GenerateRandomV4().AsLowercaseString();
}
if (bool deprecated; options.Get("_deprecated", &deprecated)) {
out->deprecated = deprecated;
}
return true;
}
};
} // namespace gin
#endif // ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_

View file

@ -30,22 +30,4 @@ SessionPreferences* SessionPreferences::FromBrowserContext(
return static_cast<SessionPreferences*>(context->GetUserData(&kLocatorKey));
}
// static
std::vector<base::FilePath> SessionPreferences::GetValidPreloads(
content::BrowserContext* context) {
std::vector<base::FilePath> result;
if (auto* self = FromBrowserContext(context)) {
for (const auto& preload : self->preloads()) {
if (preload.IsAbsolute()) {
result.emplace_back(preload);
} else {
LOG(ERROR) << "preload script must have absolute path: " << preload;
}
}
}
return result;
}
} // namespace electron

View file

@ -9,6 +9,7 @@
#include "base/files/file_path.h"
#include "base/supports_user_data.h"
#include "shell/browser/preload_script.h"
namespace content {
class BrowserContext;
@ -20,17 +21,12 @@ class SessionPreferences : public base::SupportsUserData::Data {
public:
static SessionPreferences* FromBrowserContext(
content::BrowserContext* context);
static std::vector<base::FilePath> GetValidPreloads(
content::BrowserContext* context);
static void CreateForBrowserContext(content::BrowserContext* context);
~SessionPreferences() override;
void set_preloads(const std::vector<base::FilePath>& preloads) {
preloads_ = preloads;
}
const std::vector<base::FilePath>& preloads() const { return preloads_; }
std::vector<PreloadScript>& preload_scripts() { return preload_scripts_; }
private:
SessionPreferences();
@ -38,7 +34,7 @@ class SessionPreferences : public base::SupportsUserData::Data {
// The user data key.
static int kLocatorKey;
std::vector<base::FilePath> preloads_;
std::vector<PreloadScript> preload_scripts_;
};
} // namespace electron

View file

@ -76,7 +76,7 @@ declare namespace Electron {
getOwnerBrowserWindow(): Electron.BrowserWindow | null;
getLastWebPreferences(): Electron.WebPreferences | null;
_getProcessMemoryInfo(): Electron.ProcessMemoryInfo;
_getPreloadPaths(): string[];
_getPreloadScript(): Electron.PreloadScript | null;
equal(other: WebContents): boolean;
browserWindowOptions: BrowserWindowConstructorOptions;
_windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null;
@ -330,6 +330,11 @@ declare namespace ElectronInternal {
class WebContents extends Electron.WebContents {
static create(opts?: Electron.WebPreferences): Electron.WebContents;
}
interface PreloadScript extends Electron.PreloadScript {
contents?: string;
error?: Error;
}
}
declare namespace Chrome {