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. 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 * `preloads` string[] - An array of absolute path to preload scripts
Adds scripts that will be executed on ALL web contents that are associated with Adds scripts that will be executed on ALL web contents that are associated with
this session just before normal `preload` scripts run. 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 Returns `string[]` an array of paths to preload scripts that have been
registered. 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)` #### `ses.setCodeCachePath(path)`
* `path` String - Absolute path to store the v8 generated JS code cache from the renderer. * `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) ## 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` ### 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` 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/permission-request.md",
"docs/api/structures/point.md", "docs/api/structures/point.md",
"docs/api/structures/post-body.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/printer-info.md",
"docs/api/structures/process-memory-info.md", "docs/api/structures/process-memory-info.md",
"docs/api/structures/process-metric.md", "docs/api/structures/process-metric.md",
@ -183,6 +185,8 @@ auto_filenames = {
"lib/sandboxed_renderer/api/exports/electron.ts", "lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts", "lib/sandboxed_renderer/api/module-list.ts",
"lib/sandboxed_renderer/init.ts", "lib/sandboxed_renderer/init.ts",
"lib/sandboxed_renderer/pre-init.ts",
"lib/sandboxed_renderer/preload.ts",
"package.json", "package.json",
"tsconfig.electron.json", "tsconfig.electron.json",
"tsconfig.json", "tsconfig.json",

View file

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

View file

@ -1,4 +1,5 @@
import { fetchWithSession } from '@electron/internal/browser/api/net-fetch'; import { fetchWithSession } from '@electron/internal/browser/api/net-fetch';
import * as deprecate from '@electron/internal/common/deprecate';
import { net } from 'electron/main'; import { net } from 'electron/main';
@ -36,6 +37,31 @@ Session.prototype.setDisplayMediaRequestHandler = function (handler, opts) {
}, 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 { export default {
fromPartition, fromPartition,
fromPath, fromPath,

View file

@ -5,6 +5,7 @@ import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
import { clipboard } from 'electron/common'; import { clipboard } from 'electron/common';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path';
// Implements window.close() // Implements window.close()
ipcMainInternal.on(IPC_MESSAGES.BROWSER_WINDOW_CLOSE, function (event) { 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); return (clipboard as any)[method](...args);
}); });
const getPreloadScript = async function (preloadPath: string) { const getPreloadScriptsFromEvent = (event: ElectronInternal.IpcMainInternalEvent) => {
let preloadSrc = null; const session: Electron.Session = event.sender.session;
let preloadError = null; 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 { try {
preloadSrc = await fs.promises.readFile(preloadPath, 'utf8'); contents = await fs.promises.readFile(script.filePath, 'utf8');
} catch (error) { } catch (err) {
preloadError = error; if (err instanceof Error) {
error = err;
} }
return { preloadPath, preloadSrc, preloadError }; }
return {
...script,
contents,
error
};
}; };
ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event) { ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event) {
const preloadPaths = event.sender._getPreloadPaths(); const preloadScripts = getPreloadScriptsFromEvent(event);
return { return {
preloadScripts: await Promise.all(preloadPaths.map(path => getPreloadScript(path))), preloadScripts: await Promise.all(preloadScripts.map(readPreloadScript)),
process: { process: {
arch: process.arch, arch: process.arch,
platform: process.platform, 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) { 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) { 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 { 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 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 * as events from 'events';
import { setImmediate, clearImmediate } from 'timers'; import { setImmediate, clearImmediate } from 'timers';
@ -11,35 +12,14 @@ declare const binding: {
createPreloadScript: (src: string) => Function createPreloadScript: (src: string) => Function
}; };
const { EventEmitter } = events;
process._linkedBinding = binding.get;
const v8Util = process._linkedBinding('electron_common_v8_util'); 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 ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule;
const { const {
preloadScripts, preloadScripts,
process: processProps process: processProps
} = ipcRendererUtils.invokeSync<{ } = ipcRendererUtils.invokeSync<{
preloadScripts: { preloadScripts: ElectronInternal.PreloadScript[];
preloadPath: string;
preloadSrc: string | null;
preloadError: null | Error;
}[];
process: NodeJS.Process; process: NodeJS.Process;
}>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD); }>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD);
@ -60,8 +40,7 @@ const loadableModules = new Map<string, Function>([
['node:url', () => require('url')] ['node:url', () => require('url')]
]); ]);
// Pass different process object to the preload script. const preloadProcess = createPreloadProcessObject();
const preloadProcess: NodeJS.Process = new EventEmitter() as any;
// InvokeEmitProcessEvent in ElectronSandboxedRendererClient will look for this // InvokeEmitProcessEvent in ElectronSandboxedRendererClient will look for this
v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => { 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, binding.process);
Object.assign(preloadProcess, processProps); Object.assign(preloadProcess, processProps);
Object.assign(process, binding.process);
Object.assign(process, processProps); 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 // Common renderer initialization
require('@electron/internal/renderer/common-init'); require('@electron/internal/renderer/common-init');
// Wrap the script into a function executed in global scope. It won't have executeSandboxedPreloadScripts({
// access to the current scope, so we'll expose a few objects as arguments: loadedModules,
// loadableModules,
// - `require`: The `preloadRequire` function process: preloadProcess,
// - `process`: The `preloadProcess` object createPreloadScript: binding.createPreloadScript,
// - `Buffer`: Shim of `Buffer` implementation exposeGlobals: {
// - `global`: The window object, which is aliased to `global` by webpack. Buffer,
function runPreloadScript (preloadSrc: string) { // FIXME(samuelmaddock): workaround webpack bug replacing this with just
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports, module) { // `__webpack_require__.g,` which causes script error
${preloadSrc} global: globalThis,
})`; setImmediate,
clearImmediate
// 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);
}
} }
}, 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))); 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()); auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs); 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 "";
} }
std::vector<base::FilePath> Session::GetPreloads() const { 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;
}
void Session::UnregisterPreloadScript(gin_helper::ErrorThrower thrower,
const std::string& script_id) {
auto* prefs = SessionPreferences::FromBrowserContext(browser_context()); auto* prefs = SessionPreferences::FromBrowserContext(browser_context());
DCHECK(prefs); 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("downloadURL", &Session::DownloadURL)
.SetMethod("createInterruptedDownload", .SetMethod("createInterruptedDownload",
&Session::CreateInterruptedDownload) &Session::CreateInterruptedDownload)
.SetMethod("setPreloads", &Session::SetPreloads) .SetMethod("registerPreloadScript", &Session::RegisterPreloadScript)
.SetMethod("getPreloads", &Session::GetPreloads) .SetMethod("unregisterPreloadScript", &Session::UnregisterPreloadScript)
.SetMethod("getPreloadScripts", &Session::GetPreloadScripts)
.SetMethod("getSharedDictionaryUsageInfo", .SetMethod("getSharedDictionaryUsageInfo",
&Session::GetSharedDictionaryUsageInfo) &Session::GetSharedDictionaryUsageInfo)
.SetMethod("getSharedDictionaryInfo", &Session::GetSharedDictionaryInfo) .SetMethod("getSharedDictionaryInfo", &Session::GetSharedDictionaryInfo)

View file

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

View file

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

View file

@ -40,6 +40,7 @@
#include "shell/browser/event_emitter_mixin.h" #include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/extended_web_contents_observer.h" #include "shell/browser/extended_web_contents_observer.h"
#include "shell/browser/osr/osr_paint_event.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_delegate.h"
#include "shell/browser/ui/inspectable_web_contents_view_delegate.h" #include "shell/browser/ui/inspectable_web_contents_view_delegate.h"
#include "shell/common/gin_helper/cleaned_up_at_exit.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 std::string& features,
const scoped_refptr<network::ResourceRequestBody>& body); const scoped_refptr<network::ResourceRequestBody>& body);
// Returns the preload script path of current WebContents. // Returns the preload script of current WebContents.
std::vector<base::FilePath> GetPreloadPaths() const; std::optional<PreloadScript> GetPreloadScript() const;
// Returns the web preferences of current WebContents. // Returns the web preferences of current WebContents.
v8::Local<v8::Value> GetLastWebPreferences(v8::Isolate* isolate) const; 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)); 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 } // namespace electron

View file

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

View file

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