refactor: remove code for non-native extensions shim (#23340)
This commit is contained in:
parent
3e5486323a
commit
8d0a612265
13 changed files with 5 additions and 1175 deletions
11
BUILD.gn
11
BUILD.gn
|
@ -139,15 +139,6 @@ webpack_build("electron_isolated_renderer_bundle") {
|
|||
out_file = "$target_gen_dir/js2c/isolated_bundle.js"
|
||||
}
|
||||
|
||||
webpack_build("electron_content_script_bundle") {
|
||||
deps = [ ":build_electron_definitions" ]
|
||||
|
||||
inputs = auto_filenames.content_script_bundle_deps
|
||||
|
||||
config_file = "//electron/build/webpack/webpack.config.content_script.js"
|
||||
out_file = "$target_gen_dir/js2c/content_script_bundle.js"
|
||||
}
|
||||
|
||||
copy("electron_js2c_copy") {
|
||||
sources = [
|
||||
"lib/common/asar.js",
|
||||
|
@ -159,7 +150,6 @@ copy("electron_js2c_copy") {
|
|||
action("electron_js2c") {
|
||||
deps = [
|
||||
":electron_browser_bundle",
|
||||
":electron_content_script_bundle",
|
||||
":electron_isolated_renderer_bundle",
|
||||
":electron_js2c_copy",
|
||||
":electron_renderer_bundle",
|
||||
|
@ -169,7 +159,6 @@ action("electron_js2c") {
|
|||
|
||||
webpack_sources = [
|
||||
"$target_gen_dir/js2c/browser_init.js",
|
||||
"$target_gen_dir/js2c/content_script_bundle.js",
|
||||
"$target_gen_dir/js2c/isolated_bundle.js",
|
||||
"$target_gen_dir/js2c/renderer_init.js",
|
||||
"$target_gen_dir/js2c/sandbox_bundle.js",
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
module.exports = require('./webpack.config.base')({
|
||||
target: 'content_script',
|
||||
alwaysHasNode: false
|
||||
})
|
|
@ -153,12 +153,6 @@ auto_filenames = {
|
|||
"lib/renderer/api/ipc-renderer.ts",
|
||||
"lib/renderer/api/remote.js",
|
||||
"lib/renderer/api/web-frame.ts",
|
||||
"lib/renderer/chrome-api.ts",
|
||||
"lib/renderer/content-scripts-injector.ts",
|
||||
"lib/renderer/extensions/event.ts",
|
||||
"lib/renderer/extensions/i18n.ts",
|
||||
"lib/renderer/extensions/storage.ts",
|
||||
"lib/renderer/extensions/web-navigation.ts",
|
||||
"lib/renderer/inspector.ts",
|
||||
"lib/renderer/ipc-renderer-internal-utils.ts",
|
||||
"lib/renderer/ipc-renderer-internal.ts",
|
||||
|
@ -190,24 +184,6 @@ auto_filenames = {
|
|||
"tsconfig.json",
|
||||
]
|
||||
|
||||
content_script_bundle_deps = [
|
||||
"lib/common/electron-binding-setup.ts",
|
||||
"lib/common/webpack-globals-provider.ts",
|
||||
"lib/content_script/init.js",
|
||||
"lib/renderer/api/context-bridge.ts",
|
||||
"lib/renderer/chrome-api.ts",
|
||||
"lib/renderer/extensions/event.ts",
|
||||
"lib/renderer/extensions/i18n.ts",
|
||||
"lib/renderer/extensions/storage.ts",
|
||||
"lib/renderer/extensions/web-navigation.ts",
|
||||
"lib/renderer/ipc-renderer-internal-utils.ts",
|
||||
"lib/renderer/ipc-renderer-internal.ts",
|
||||
"lib/renderer/window-setup.ts",
|
||||
"package.json",
|
||||
"tsconfig.electron.json",
|
||||
"tsconfig.json",
|
||||
]
|
||||
|
||||
browser_bundle_deps = [
|
||||
"lib/browser/api/app.ts",
|
||||
"lib/browser/api/auto-updater.js",
|
||||
|
@ -247,7 +223,6 @@ auto_filenames = {
|
|||
"lib/browser/api/web-contents-view.js",
|
||||
"lib/browser/api/web-contents.js",
|
||||
"lib/browser/chrome-extension-shim.js",
|
||||
"lib/browser/chrome-extension.js",
|
||||
"lib/browser/crash-reporter-init.js",
|
||||
"lib/browser/default-menu.ts",
|
||||
"lib/browser/desktop-capturer.ts",
|
||||
|
@ -308,12 +283,6 @@ auto_filenames = {
|
|||
"lib/renderer/api/module-list.ts",
|
||||
"lib/renderer/api/remote.js",
|
||||
"lib/renderer/api/web-frame.ts",
|
||||
"lib/renderer/chrome-api.ts",
|
||||
"lib/renderer/content-scripts-injector.ts",
|
||||
"lib/renderer/extensions/event.ts",
|
||||
"lib/renderer/extensions/i18n.ts",
|
||||
"lib/renderer/extensions/storage.ts",
|
||||
"lib/renderer/extensions/web-navigation.ts",
|
||||
"lib/renderer/init.ts",
|
||||
"lib/renderer/inspector.ts",
|
||||
"lib/renderer/ipc-renderer-internal-utils.ts",
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
// BrowserWindow-based extensions stuff to the new native-backed extensions
|
||||
// API.
|
||||
|
||||
if (!process.electronBinding('features').isExtensionsEnabled()) {
|
||||
throw new Error('Attempted to load JS chrome-extension shim without //extensions support enabled');
|
||||
}
|
||||
|
||||
const { app, session, BrowserWindow, deprecate } = require('electron');
|
||||
|
||||
app.whenReady().then(function () {
|
||||
|
|
|
@ -1,542 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
if (process.electronBinding('features').isExtensionsEnabled()) {
|
||||
throw new Error('Attempted to load JS chrome-extension polyfill with //extensions support enabled');
|
||||
}
|
||||
|
||||
const { app, webContents, BrowserWindow } = require('electron');
|
||||
const { getAllWebContents } = process.electronBinding('web_contents');
|
||||
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
|
||||
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils');
|
||||
|
||||
const { Buffer } = require('buffer');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const util = require('util');
|
||||
|
||||
// Mapping between extensionId(hostname) and manifest.
|
||||
const manifestMap = {}; // extensionId => manifest
|
||||
const manifestNameMap = {}; // name => manifest
|
||||
const devToolsExtensionNames = new Set();
|
||||
|
||||
const generateExtensionIdFromName = function (name) {
|
||||
return name.replace(/[\W_]+/g, '-').toLowerCase();
|
||||
};
|
||||
|
||||
const isWindowOrWebView = function (webContents) {
|
||||
const type = webContents.getType();
|
||||
return type === 'window' || type === 'webview';
|
||||
};
|
||||
|
||||
const isBackgroundPage = function (webContents) {
|
||||
return webContents.getType() === 'backgroundPage';
|
||||
};
|
||||
|
||||
// Create or get manifest object from |srcDirectory|.
|
||||
const getManifestFromPath = function (srcDirectory) {
|
||||
let manifest;
|
||||
let manifestContent;
|
||||
|
||||
try {
|
||||
manifestContent = fs.readFileSync(path.join(srcDirectory, 'manifest.json'));
|
||||
} catch (readError) {
|
||||
console.warn(`Reading ${path.join(srcDirectory, 'manifest.json')} failed.`);
|
||||
console.warn(readError.stack || readError);
|
||||
throw readError;
|
||||
}
|
||||
|
||||
try {
|
||||
manifest = JSON.parse(manifestContent);
|
||||
} catch (parseError) {
|
||||
console.warn(`Parsing ${path.join(srcDirectory, 'manifest.json')} failed.`);
|
||||
console.warn(parseError.stack || parseError);
|
||||
throw parseError;
|
||||
}
|
||||
|
||||
if (!manifestNameMap[manifest.name]) {
|
||||
const extensionId = generateExtensionIdFromName(manifest.name);
|
||||
manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest;
|
||||
|
||||
let extensionURL = url.format({
|
||||
protocol: 'chrome-extension',
|
||||
slashes: true,
|
||||
hostname: extensionId,
|
||||
pathname: manifest.devtools_page
|
||||
});
|
||||
|
||||
// Chromium requires that startPage matches '([^:]+:\/\/[^/]*)\/'
|
||||
// We also can't use the file:// protocol here since that would make Chromium
|
||||
// treat all extension resources as being relative to root which we don't want.
|
||||
if (!manifest.devtools_page) extensionURL += '/';
|
||||
|
||||
Object.assign(manifest, {
|
||||
srcDirectory: srcDirectory,
|
||||
extensionId: extensionId,
|
||||
startPage: extensionURL
|
||||
});
|
||||
|
||||
return manifest;
|
||||
} else if (manifest && manifest.name) {
|
||||
console.warn(`Attempted to load extension "${manifest.name}" that has already been loaded.`);
|
||||
return manifest;
|
||||
}
|
||||
};
|
||||
|
||||
// Manage the background pages.
|
||||
const backgroundPages = {};
|
||||
|
||||
const startBackgroundPages = function (manifest) {
|
||||
if (backgroundPages[manifest.extensionId] || !manifest.background) return;
|
||||
|
||||
let html;
|
||||
let name;
|
||||
if (manifest.background.page) {
|
||||
name = manifest.background.page;
|
||||
html = fs.readFileSync(path.join(manifest.srcDirectory, manifest.background.page));
|
||||
} else {
|
||||
name = '_generated_background_page.html';
|
||||
const scripts = manifest.background.scripts.map((name) => {
|
||||
return `<script src="${name}"></script>`;
|
||||
}).join('');
|
||||
html = Buffer.from(`<html><body>${scripts}</body></html>`);
|
||||
}
|
||||
|
||||
const contents = webContents.create({
|
||||
partition: 'persist:__chrome_extension',
|
||||
type: 'backgroundPage',
|
||||
sandbox: true,
|
||||
enableRemoteModule: false
|
||||
});
|
||||
backgroundPages[manifest.extensionId] = { html: html, webContents: contents, name: name };
|
||||
contents.loadURL(url.format({
|
||||
protocol: 'chrome-extension',
|
||||
slashes: true,
|
||||
hostname: manifest.extensionId,
|
||||
pathname: name
|
||||
}));
|
||||
};
|
||||
|
||||
const removeBackgroundPages = function (manifest) {
|
||||
if (!backgroundPages[manifest.extensionId]) return;
|
||||
|
||||
backgroundPages[manifest.extensionId].webContents.destroy();
|
||||
delete backgroundPages[manifest.extensionId];
|
||||
};
|
||||
|
||||
const sendToBackgroundPages = function (...args) {
|
||||
for (const page of Object.values(backgroundPages)) {
|
||||
if (!page.webContents.isDestroyed()) {
|
||||
page.webContents._sendInternalToAll(...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Dispatch web contents events to Chrome APIs
|
||||
const hookWebContentsEvents = function (webContents) {
|
||||
const tabId = webContents.id;
|
||||
|
||||
sendToBackgroundPages('CHROME_TABS_ONCREATED');
|
||||
|
||||
webContents.on('will-navigate', (event, url) => {
|
||||
sendToBackgroundPages('CHROME_WEBNAVIGATION_ONBEFORENAVIGATE', {
|
||||
frameId: 0,
|
||||
parentFrameId: -1,
|
||||
processId: webContents.getProcessId(),
|
||||
tabId: tabId,
|
||||
timeStamp: Date.now(),
|
||||
url: url
|
||||
});
|
||||
});
|
||||
|
||||
webContents.on('did-navigate', (event, url) => {
|
||||
sendToBackgroundPages('CHROME_WEBNAVIGATION_ONCOMPLETED', {
|
||||
frameId: 0,
|
||||
parentFrameId: -1,
|
||||
processId: webContents.getProcessId(),
|
||||
tabId: tabId,
|
||||
timeStamp: Date.now(),
|
||||
url: url
|
||||
});
|
||||
});
|
||||
|
||||
webContents.once('destroyed', () => {
|
||||
sendToBackgroundPages('CHROME_TABS_ONREMOVED', tabId);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle the chrome.* API messages.
|
||||
let nextId = 0;
|
||||
|
||||
ipcMainUtils.handleSync('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) {
|
||||
if (isBackgroundPage(event.sender)) {
|
||||
throw new Error('chrome.runtime.connect is not supported in background page');
|
||||
}
|
||||
|
||||
const page = backgroundPages[extensionId];
|
||||
if (!page || page.webContents.isDestroyed()) {
|
||||
throw new Error(`Connect to unknown extension ${extensionId}`);
|
||||
}
|
||||
|
||||
const tabId = page.webContents.id;
|
||||
const portId = ++nextId;
|
||||
|
||||
event.sender.once('render-view-deleted', () => {
|
||||
if (page.webContents.isDestroyed()) return;
|
||||
page.webContents._sendInternalToAll(`CHROME_PORT_DISCONNECT_${portId}`);
|
||||
});
|
||||
page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo);
|
||||
|
||||
return { tabId, portId };
|
||||
});
|
||||
|
||||
ipcMainUtils.handleSync('CHROME_EXTENSION_MANIFEST', function (event, extensionId) {
|
||||
const manifest = manifestMap[extensionId];
|
||||
if (!manifest) {
|
||||
throw new Error(`Invalid extensionId: ${extensionId}`);
|
||||
}
|
||||
return manifest;
|
||||
});
|
||||
|
||||
ipcMainInternal.handle('CHROME_RUNTIME_SEND_MESSAGE', async function (event, extensionId, message) {
|
||||
if (isBackgroundPage(event.sender)) {
|
||||
throw new Error('chrome.runtime.sendMessage is not supported in background page');
|
||||
}
|
||||
|
||||
const page = backgroundPages[extensionId];
|
||||
if (!page || page.webContents.isDestroyed()) {
|
||||
throw new Error(`Connect to unknown extension ${extensionId}`);
|
||||
}
|
||||
|
||||
return ipcMainUtils.invokeInWebContents(page.webContents, true, `CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message);
|
||||
});
|
||||
|
||||
ipcMainInternal.handle('CHROME_TABS_SEND_MESSAGE', async function (event, tabId, extensionId, message) {
|
||||
const contents = webContents.fromId(tabId);
|
||||
if (!contents) {
|
||||
throw new Error(`Sending message to unknown tab ${tabId}`);
|
||||
}
|
||||
|
||||
const senderTabId = isBackgroundPage(event.sender) ? null : event.sender.id;
|
||||
|
||||
return ipcMainUtils.invokeInWebContents(contents, true, `CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message);
|
||||
});
|
||||
|
||||
const getLanguage = () => {
|
||||
return app.getLocale().replace(/-.*$/, '').toLowerCase();
|
||||
};
|
||||
|
||||
const getMessagesPath = (extensionId) => {
|
||||
const metadata = manifestMap[extensionId];
|
||||
if (!metadata) {
|
||||
throw new Error(`Invalid extensionId: ${extensionId}`);
|
||||
}
|
||||
|
||||
const localesDirectory = path.join(metadata.srcDirectory, '_locales');
|
||||
const language = getLanguage();
|
||||
|
||||
try {
|
||||
const filename = path.join(localesDirectory, language, 'messages.json');
|
||||
fs.accessSync(filename, fs.constants.R_OK);
|
||||
return filename;
|
||||
} catch {
|
||||
const defaultLocale = metadata.default_locale || 'en';
|
||||
return path.join(localesDirectory, defaultLocale, 'messages.json');
|
||||
}
|
||||
};
|
||||
|
||||
ipcMainUtils.handleSync('CHROME_GET_MESSAGES', async function (event, extensionId) {
|
||||
const messagesPath = getMessagesPath(extensionId);
|
||||
return fs.promises.readFile(messagesPath, 'utf8');
|
||||
});
|
||||
|
||||
const validStorageTypes = new Set(['sync', 'local']);
|
||||
|
||||
const getChromeStoragePath = (storageType, extensionId) => {
|
||||
if (!validStorageTypes.has(storageType)) {
|
||||
throw new Error(`Invalid storageType: ${storageType}`);
|
||||
}
|
||||
|
||||
if (!manifestMap[extensionId]) {
|
||||
throw new Error(`Invalid extensionId: ${extensionId}`);
|
||||
}
|
||||
|
||||
return path.join(app.getPath('userData'), `/Chrome Storage/${extensionId}-${storageType}.json`);
|
||||
};
|
||||
|
||||
ipcMainInternal.handle('CHROME_STORAGE_READ', async function (event, storageType, extensionId) {
|
||||
const filePath = getChromeStoragePath(storageType, extensionId);
|
||||
|
||||
try {
|
||||
return await fs.promises.readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMainInternal.handle('CHROME_STORAGE_WRITE', async function (event, storageType, extensionId, data) {
|
||||
const filePath = getChromeStoragePath(storageType, extensionId);
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
} catch {
|
||||
// we just ignore the errors of mkdir
|
||||
}
|
||||
|
||||
return fs.promises.writeFile(filePath, data, 'utf8');
|
||||
});
|
||||
|
||||
const isChromeExtension = function (pageURL) {
|
||||
const { protocol } = url.parse(pageURL);
|
||||
return protocol === 'chrome-extension:';
|
||||
};
|
||||
|
||||
const assertChromeExtension = function (contents, api) {
|
||||
const pageURL = contents._getURL();
|
||||
if (!isChromeExtension(pageURL)) {
|
||||
console.error(`Blocked ${pageURL} from calling ${api}`);
|
||||
throw new Error(`Blocked ${api}`);
|
||||
}
|
||||
};
|
||||
|
||||
ipcMainInternal.handle('CHROME_TABS_EXECUTE_SCRIPT', async function (event, tabId, extensionId, details) {
|
||||
assertChromeExtension(event.sender, 'chrome.tabs.executeScript()');
|
||||
|
||||
const contents = webContents.fromId(tabId);
|
||||
if (!contents) {
|
||||
throw new Error(`Sending message to unknown tab ${tabId}`);
|
||||
}
|
||||
|
||||
let code, url;
|
||||
if (details.file) {
|
||||
const manifest = manifestMap[extensionId];
|
||||
code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file)));
|
||||
url = `chrome-extension://${extensionId}${details.file}`;
|
||||
} else {
|
||||
code = details.code;
|
||||
url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js`;
|
||||
}
|
||||
|
||||
return ipcMainUtils.invokeInWebContents(contents, false, 'CHROME_TABS_EXECUTE_SCRIPT', extensionId, url, code);
|
||||
});
|
||||
|
||||
exports.getContentScripts = () => {
|
||||
return Object.values(contentScripts);
|
||||
};
|
||||
|
||||
// Transfer the content scripts to renderer.
|
||||
const contentScripts = {};
|
||||
|
||||
const injectContentScripts = function (manifest) {
|
||||
if (contentScripts[manifest.name] || !manifest.content_scripts) return;
|
||||
|
||||
const readArrayOfFiles = function (relativePath) {
|
||||
return {
|
||||
url: `chrome-extension://${manifest.extensionId}/${relativePath}`,
|
||||
code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath)))
|
||||
};
|
||||
};
|
||||
|
||||
const contentScriptToEntry = function (script) {
|
||||
return {
|
||||
matches: script.matches,
|
||||
js: script.js ? script.js.map(readArrayOfFiles) : [],
|
||||
css: script.css ? script.css.map(readArrayOfFiles) : [],
|
||||
runAt: script.run_at || 'document_idle',
|
||||
allFrames: script.all_frames || false
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const entry = {
|
||||
extensionId: manifest.extensionId,
|
||||
contentScripts: manifest.content_scripts.map(contentScriptToEntry)
|
||||
};
|
||||
contentScripts[manifest.name] = entry;
|
||||
} catch (e) {
|
||||
console.error('Failed to read content scripts', e);
|
||||
}
|
||||
};
|
||||
|
||||
const removeContentScripts = function (manifest) {
|
||||
if (!contentScripts[manifest.name]) return;
|
||||
|
||||
delete contentScripts[manifest.name];
|
||||
};
|
||||
|
||||
// Transfer the |manifest| to a format that can be recognized by the
|
||||
// |DevToolsAPI.addExtensions|.
|
||||
const manifestToExtensionInfo = function (manifest) {
|
||||
return {
|
||||
startPage: manifest.startPage,
|
||||
srcDirectory: manifest.srcDirectory,
|
||||
name: manifest.name,
|
||||
exposeExperimentalAPIs: true
|
||||
};
|
||||
};
|
||||
|
||||
// Load the extensions for the window.
|
||||
const loadExtension = function (manifest) {
|
||||
startBackgroundPages(manifest);
|
||||
injectContentScripts(manifest);
|
||||
};
|
||||
|
||||
const loadDevToolsExtensions = function (win, manifests) {
|
||||
if (!win.devToolsWebContents) return;
|
||||
|
||||
manifests.forEach(loadExtension);
|
||||
|
||||
const extensionInfoArray = manifests.map(manifestToExtensionInfo);
|
||||
extensionInfoArray.forEach((extension) => {
|
||||
win.devToolsWebContents._grantOriginAccess(extension.startPage);
|
||||
});
|
||||
|
||||
extensionInfoArray.forEach((extensionInfo) => {
|
||||
const info = JSON.stringify(extensionInfo);
|
||||
win.devToolsWebContents.executeJavaScript(`Extensions.extensionServer._addExtension(${info})`);
|
||||
});
|
||||
};
|
||||
|
||||
app.on('web-contents-created', function (event, webContents) {
|
||||
if (!isWindowOrWebView(webContents)) return;
|
||||
|
||||
hookWebContentsEvents(webContents);
|
||||
webContents.on('devtools-opened', function () {
|
||||
loadDevToolsExtensions(webContents, Object.values(manifestMap));
|
||||
});
|
||||
});
|
||||
|
||||
// The chrome-extension: can map a extension URL request to real file path.
|
||||
const chromeExtensionHandler = function (request, callback) {
|
||||
const parsed = url.parse(request.url);
|
||||
if (!parsed.hostname || !parsed.path) return callback();
|
||||
|
||||
const manifest = manifestMap[parsed.hostname];
|
||||
if (!manifest) return callback();
|
||||
|
||||
const page = backgroundPages[parsed.hostname];
|
||||
if (page && parsed.path === `/${page.name}`) {
|
||||
// Disabled due to false positive in StandardJS
|
||||
// eslint-disable-next-line standard/no-callback-literal
|
||||
return callback({
|
||||
mimeType: 'text/html',
|
||||
data: page.html
|
||||
});
|
||||
}
|
||||
|
||||
fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) {
|
||||
if (err) {
|
||||
// Disabled due to false positive in StandardJS
|
||||
// eslint-disable-next-line standard/no-callback-literal
|
||||
return callback(-6); // FILE_NOT_FOUND
|
||||
} else {
|
||||
return callback(content);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
app.on('session-created', function (ses) {
|
||||
ses.protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler);
|
||||
});
|
||||
|
||||
// The persistent path of "DevTools Extensions" preference file.
|
||||
let loadedDevToolsExtensionsPath = null;
|
||||
|
||||
app.on('will-quit', function () {
|
||||
try {
|
||||
const loadedDevToolsExtensions = Array.from(devToolsExtensionNames)
|
||||
.map(name => manifestNameMap[name].srcDirectory);
|
||||
if (loadedDevToolsExtensions.length > 0) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(loadedDevToolsExtensionsPath));
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
fs.writeFileSync(loadedDevToolsExtensionsPath, JSON.stringify(loadedDevToolsExtensions));
|
||||
} else {
|
||||
fs.unlinkSync(loadedDevToolsExtensionsPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
});
|
||||
|
||||
// We can not use protocol or BrowserWindow until app is ready.
|
||||
app.whenReady().then(function () {
|
||||
// The public API to add/remove extensions.
|
||||
BrowserWindow.addExtension = function (srcDirectory) {
|
||||
const manifest = getManifestFromPath(srcDirectory);
|
||||
if (manifest) {
|
||||
loadExtension(manifest);
|
||||
for (const webContents of getAllWebContents()) {
|
||||
if (isWindowOrWebView(webContents)) {
|
||||
loadDevToolsExtensions(webContents, [manifest]);
|
||||
}
|
||||
}
|
||||
return manifest.name;
|
||||
}
|
||||
};
|
||||
|
||||
BrowserWindow.removeExtension = function (name) {
|
||||
const manifest = manifestNameMap[name];
|
||||
if (!manifest) return;
|
||||
|
||||
removeBackgroundPages(manifest);
|
||||
removeContentScripts(manifest);
|
||||
delete manifestMap[manifest.extensionId];
|
||||
delete manifestNameMap[name];
|
||||
};
|
||||
|
||||
BrowserWindow.getExtensions = function () {
|
||||
const extensions = {};
|
||||
Object.keys(manifestNameMap).forEach(function (name) {
|
||||
const manifest = manifestNameMap[name];
|
||||
extensions[name] = { name: manifest.name, version: manifest.version };
|
||||
});
|
||||
return extensions;
|
||||
};
|
||||
|
||||
BrowserWindow.addDevToolsExtension = function (srcDirectory) {
|
||||
const manifestName = BrowserWindow.addExtension(srcDirectory);
|
||||
if (manifestName) {
|
||||
devToolsExtensionNames.add(manifestName);
|
||||
}
|
||||
return manifestName;
|
||||
};
|
||||
|
||||
BrowserWindow.removeDevToolsExtension = function (name) {
|
||||
BrowserWindow.removeExtension(name);
|
||||
devToolsExtensionNames.delete(name);
|
||||
};
|
||||
|
||||
BrowserWindow.getDevToolsExtensions = function () {
|
||||
const extensions = BrowserWindow.getExtensions();
|
||||
const devExtensions = {};
|
||||
Array.from(devToolsExtensionNames).forEach(function (name) {
|
||||
if (!extensions[name]) return;
|
||||
devExtensions[name] = extensions[name];
|
||||
});
|
||||
return devExtensions;
|
||||
};
|
||||
|
||||
// Load persisted extensions.
|
||||
loadedDevToolsExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions');
|
||||
try {
|
||||
const loadedDevToolsExtensions = JSON.parse(fs.readFileSync(loadedDevToolsExtensionsPath));
|
||||
if (Array.isArray(loadedDevToolsExtensions)) {
|
||||
for (const srcDirectory of loadedDevToolsExtensions) {
|
||||
// Start background pages and set content scripts.
|
||||
BrowserWindow.addDevToolsExtension(srcDirectory);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.ELECTRON_ENABLE_LOGGING && error.code !== 'ENOENT') {
|
||||
console.error('Failed to load browser extensions from directory:', loadedDevToolsExtensionsPath);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -160,11 +160,7 @@ require('@electron/internal/browser/devtools');
|
|||
const features = process.electronBinding('features');
|
||||
|
||||
// Load the chrome extension support.
|
||||
if (features.isExtensionsEnabled()) {
|
||||
require('@electron/internal/browser/chrome-extension-shim');
|
||||
} else {
|
||||
require('@electron/internal/browser/chrome-extension');
|
||||
}
|
||||
require('@electron/internal/browser/chrome-extension-shim');
|
||||
|
||||
if (features.isRemoteModuleEnabled()) {
|
||||
require('@electron/internal/browser/remote/server');
|
||||
|
|
|
@ -96,21 +96,12 @@ const getPreloadScript = async function (preloadPath) {
|
|||
return { preloadPath, preloadSrc, preloadError };
|
||||
};
|
||||
|
||||
if (features.isExtensionsEnabled()) {
|
||||
ipcMainUtils.handleSync('ELECTRON_GET_CONTENT_SCRIPTS', () => []);
|
||||
} else {
|
||||
const { getContentScripts } = require('@electron/internal/browser/chrome-extension');
|
||||
ipcMainUtils.handleSync('ELECTRON_GET_CONTENT_SCRIPTS', () => getContentScripts());
|
||||
}
|
||||
ipcMainUtils.handleSync('ELECTRON_GET_CONTENT_SCRIPTS', () => []);
|
||||
|
||||
ipcMainUtils.handleSync('ELECTRON_BROWSER_SANDBOX_LOAD', async function (event) {
|
||||
const preloadPaths = event.sender._getPreloadPaths();
|
||||
|
||||
let contentScripts = [];
|
||||
if (!features.isExtensionsEnabled()) {
|
||||
const { getContentScripts } = require('@electron/internal/browser/chrome-extension');
|
||||
contentScripts = getContentScripts();
|
||||
}
|
||||
const contentScripts = [];
|
||||
|
||||
const webPreferences = event.sender.getLastWebPreferences() || {};
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/* global nodeProcess, isolatedWorld, worldId */
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
process.electronBinding = require('@electron/internal/common/electron-binding-setup').electronBindingSetup(nodeProcess._linkedBinding, 'renderer');
|
||||
|
||||
const v8Util = process.electronBinding('v8_util');
|
||||
|
||||
// The `lib/renderer/ipc-renderer-internal.js` module looks for the ipc object in the
|
||||
// "ipc-internal" hidden value
|
||||
v8Util.setHiddenValue(global, 'ipc-internal', v8Util.getHiddenValue(isolatedWorld, 'ipc-internal'));
|
||||
|
||||
// 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)) {
|
||||
if (Object.prototype.hasOwnProperty.call(process, prop)) {
|
||||
delete process[prop];
|
||||
}
|
||||
}
|
||||
Object.setPrototypeOf(process, EventEmitter.prototype);
|
||||
|
||||
const isolatedWorldArgs = v8Util.getHiddenValue(isolatedWorld, 'isolated-world-args');
|
||||
|
||||
if (isolatedWorldArgs) {
|
||||
const { guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen, rendererProcessReuseEnabled } = isolatedWorldArgs;
|
||||
const { windowSetup } = require('@electron/internal/renderer/window-setup');
|
||||
windowSetup(guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen, rendererProcessReuseEnabled);
|
||||
}
|
||||
|
||||
const extensionId = v8Util.getHiddenValue(isolatedWorld, `extension-${worldId}`);
|
||||
|
||||
if (extensionId) {
|
||||
const chromeAPI = require('@electron/internal/renderer/chrome-api');
|
||||
chromeAPI.injectTo(extensionId, window);
|
||||
}
|
|
@ -1,226 +0,0 @@
|
|||
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
|
||||
import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils';
|
||||
import * as url from 'url';
|
||||
|
||||
import { Event } from '@electron/internal/renderer/extensions/event';
|
||||
|
||||
class Tab {
|
||||
public id: number
|
||||
|
||||
constructor (tabId: number) {
|
||||
this.id = tabId;
|
||||
}
|
||||
}
|
||||
|
||||
class MessageSender {
|
||||
public tab: Tab | null
|
||||
public id: string
|
||||
public url: string
|
||||
|
||||
constructor (tabId: number, extensionId: string) {
|
||||
this.tab = tabId ? new Tab(tabId) : null;
|
||||
this.id = extensionId;
|
||||
this.url = `chrome-extension://${extensionId}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Port {
|
||||
public disconnected: boolean = false
|
||||
public onDisconnect = new Event()
|
||||
public onMessage = new Event()
|
||||
public sender: MessageSender
|
||||
|
||||
constructor (public tabId: number, public portId: number, extensionId: string, public name: string) {
|
||||
this.onDisconnect = new Event();
|
||||
this.onMessage = new Event();
|
||||
this.sender = new MessageSender(tabId, extensionId);
|
||||
|
||||
ipcRendererInternal.once(`CHROME_PORT_DISCONNECT_${portId}`, () => {
|
||||
this._onDisconnect();
|
||||
});
|
||||
|
||||
ipcRendererInternal.on(`CHROME_PORT_POSTMESSAGE_${portId}`, (
|
||||
_event: Electron.Event, message: any
|
||||
) => {
|
||||
const sendResponse = function () { console.error('sendResponse is not implemented'); };
|
||||
this.onMessage.emit(JSON.parse(message), this.sender, sendResponse);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
if (this.disconnected) return;
|
||||
|
||||
ipcRendererInternal.sendToAll(this.tabId, `CHROME_PORT_DISCONNECT_${this.portId}`);
|
||||
this._onDisconnect();
|
||||
}
|
||||
|
||||
postMessage (message: any) {
|
||||
ipcRendererInternal.sendToAll(this.tabId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, JSON.stringify(message));
|
||||
}
|
||||
|
||||
_onDisconnect () {
|
||||
this.disconnected = true;
|
||||
ipcRendererInternal.removeAllListeners(`CHROME_PORT_POSTMESSAGE_${this.portId}`);
|
||||
this.onDisconnect.emit();
|
||||
}
|
||||
}
|
||||
|
||||
// Inject chrome API to the |context|
|
||||
export function injectTo (extensionId: string, context: any) {
|
||||
if (process.electronBinding('features').isExtensionsEnabled()) {
|
||||
throw new Error('Attempted to load JS chrome-extension polyfill with //extensions support enabled');
|
||||
}
|
||||
|
||||
const chrome = context.chrome = context.chrome || {};
|
||||
|
||||
ipcRendererInternal.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, (
|
||||
_event: Electron.Event, tabId: number, portId: number, connectInfo: { name: string }
|
||||
) => {
|
||||
chrome.runtime.onConnect.emit(new Port(tabId, portId, extensionId, connectInfo.name));
|
||||
});
|
||||
|
||||
ipcRendererUtils.handle(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (
|
||||
_event: Electron.Event, tabId: number, message: string
|
||||
) => {
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId), resolve);
|
||||
});
|
||||
});
|
||||
|
||||
ipcRendererInternal.on('CHROME_TABS_ONCREATED', (_event: Electron.Event, tabId: number) => {
|
||||
chrome.tabs.onCreated.emit(new Tab(tabId));
|
||||
});
|
||||
|
||||
ipcRendererInternal.on('CHROME_TABS_ONREMOVED', (_event: Electron.Event, tabId: number) => {
|
||||
chrome.tabs.onRemoved.emit(tabId);
|
||||
});
|
||||
|
||||
chrome.runtime = {
|
||||
id: extensionId,
|
||||
|
||||
// https://developer.chrome.com/extensions/runtime#method-getURL
|
||||
getURL: function (path: string) {
|
||||
return url.format({
|
||||
protocol: 'chrome-extension',
|
||||
slashes: true,
|
||||
hostname: extensionId,
|
||||
pathname: path
|
||||
});
|
||||
},
|
||||
|
||||
// https://developer.chrome.com/extensions/runtime#method-getManifest
|
||||
getManifest: function () {
|
||||
const manifest = ipcRendererUtils.invokeSync('CHROME_EXTENSION_MANIFEST', extensionId);
|
||||
return manifest;
|
||||
},
|
||||
|
||||
// https://developer.chrome.com/extensions/runtime#method-connect
|
||||
connect (...args: Array<any>) {
|
||||
// Parse the optional args.
|
||||
let targetExtensionId = extensionId;
|
||||
let connectInfo = { name: '' };
|
||||
if (args.length === 1) {
|
||||
if (typeof args[0] === 'string') {
|
||||
targetExtensionId = args[0];
|
||||
} else {
|
||||
connectInfo = args[0];
|
||||
}
|
||||
} else if (args.length === 2) {
|
||||
[targetExtensionId, connectInfo] = args;
|
||||
}
|
||||
|
||||
const { tabId, portId } = ipcRendererUtils.invokeSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo);
|
||||
return new Port(tabId, portId, extensionId, connectInfo.name);
|
||||
},
|
||||
|
||||
// https://developer.chrome.com/extensions/runtime#method-sendMessage
|
||||
sendMessage (...args: Array<any>) {
|
||||
// Parse the optional args.
|
||||
const targetExtensionId = extensionId;
|
||||
let message: string;
|
||||
let options: Object | undefined;
|
||||
let responseCallback: Chrome.Tabs.SendMessageCallback = () => {};
|
||||
|
||||
if (typeof args[args.length - 1] === 'function') {
|
||||
responseCallback = args.pop();
|
||||
}
|
||||
|
||||
if (args.length === 1) {
|
||||
[message] = args;
|
||||
} else if (args.length === 2) {
|
||||
if (typeof args[0] === 'string') {
|
||||
[extensionId, message] = args;
|
||||
} else {
|
||||
[message, options] = args;
|
||||
}
|
||||
} else {
|
||||
[extensionId, message, options] = args;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
console.error('options are not supported');
|
||||
}
|
||||
|
||||
ipcRendererInternal.invoke('CHROME_RUNTIME_SEND_MESSAGE', targetExtensionId, message).then(responseCallback);
|
||||
},
|
||||
|
||||
onConnect: new Event(),
|
||||
onMessage: new Event(),
|
||||
onInstalled: new Event()
|
||||
};
|
||||
|
||||
chrome.tabs = {
|
||||
// https://developer.chrome.com/extensions/tabs#method-executeScript
|
||||
executeScript (
|
||||
tabId: number,
|
||||
details: Chrome.Tabs.ExecuteScriptDetails,
|
||||
resultCallback: Chrome.Tabs.ExecuteScriptCallback = () => {}
|
||||
) {
|
||||
ipcRendererInternal.invoke('CHROME_TABS_EXECUTE_SCRIPT', tabId, extensionId, details)
|
||||
.then((result: any) => resultCallback([result]));
|
||||
},
|
||||
|
||||
// https://developer.chrome.com/extensions/tabs#method-sendMessage
|
||||
sendMessage (
|
||||
tabId: number,
|
||||
message: any,
|
||||
_options: Chrome.Tabs.SendMessageDetails,
|
||||
responseCallback: Chrome.Tabs.SendMessageCallback = () => {}
|
||||
) {
|
||||
ipcRendererInternal.invoke('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, message).then(responseCallback);
|
||||
},
|
||||
|
||||
onUpdated: new Event(),
|
||||
onCreated: new Event(),
|
||||
onRemoved: new Event()
|
||||
};
|
||||
|
||||
chrome.extension = {
|
||||
getURL: chrome.runtime.getURL,
|
||||
connect: chrome.runtime.connect,
|
||||
onConnect: chrome.runtime.onConnect,
|
||||
sendMessage: chrome.runtime.sendMessage,
|
||||
onMessage: chrome.runtime.onMessage
|
||||
};
|
||||
|
||||
chrome.storage = require('@electron/internal/renderer/extensions/storage').setup(extensionId);
|
||||
|
||||
chrome.pageAction = {
|
||||
show () {},
|
||||
hide () {},
|
||||
setTitle () {},
|
||||
getTitle () {},
|
||||
setIcon () {},
|
||||
setPopup () {},
|
||||
getPopup () {}
|
||||
};
|
||||
|
||||
chrome.i18n = require('@electron/internal/renderer/extensions/i18n').setup(extensionId);
|
||||
chrome.webNavigation = require('@electron/internal/renderer/extensions/web-navigation').setup();
|
||||
|
||||
// Electron has no concept of a browserAction but we should stub these APIs for compatibility
|
||||
chrome.browserAction = {
|
||||
setIcon () {},
|
||||
setPopup () {}
|
||||
};
|
||||
}
|
|
@ -53,7 +53,6 @@ v8Util.setHiddenValue(global, 'ipcNative', {
|
|||
|
||||
// Use electron module after everything is ready.
|
||||
const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal');
|
||||
const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils');
|
||||
const { webFrameInit } = require('@electron/internal/renderer/web-frame-init');
|
||||
webFrameInit();
|
||||
|
||||
|
@ -100,10 +99,6 @@ switch (window.location.protocol) {
|
|||
break;
|
||||
}
|
||||
case 'chrome-extension:': {
|
||||
// Inject the chrome.* APIs that chrome extensions require
|
||||
if (!process.electronBinding('features').isExtensionsEnabled()) {
|
||||
require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'chrome:':
|
||||
|
@ -112,12 +107,6 @@ switch (window.location.protocol) {
|
|||
// Override default web functions.
|
||||
const { windowSetup } = require('@electron/internal/renderer/window-setup');
|
||||
windowSetup(guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen, rendererProcessReuseEnabled);
|
||||
|
||||
// Inject content scripts.
|
||||
if (!process.electronBinding('features').isExtensionsEnabled()) {
|
||||
const contentScripts = ipcRendererUtils.invokeSync('ELECTRON_GET_CONTENT_SCRIPTS') as Electron.ContentScriptEntry[];
|
||||
require('@electron/internal/renderer/content-scripts-injector')(contentScripts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -129,10 +129,6 @@ switch (window.location.protocol) {
|
|||
break;
|
||||
}
|
||||
case 'chrome-extension:': {
|
||||
// Inject the chrome.* APIs that chrome extensions require
|
||||
if (!process.electronBinding('features').isExtensionsEnabled()) {
|
||||
require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'chrome': {
|
||||
|
@ -142,11 +138,6 @@ switch (window.location.protocol) {
|
|||
// Override default web functions.
|
||||
const { windowSetup } = require('@electron/internal/renderer/window-setup');
|
||||
windowSetup(guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen, rendererProcessReuseEnabled);
|
||||
|
||||
// Inject content scripts.
|
||||
if (!process.electronBinding('features').isExtensionsEnabled()) {
|
||||
require('@electron/internal/renderer/content-scripts-injector')(contentScripts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,10 +23,6 @@ const main = async () => {
|
|||
name: 'isolated_bundle_deps',
|
||||
config: 'webpack.config.isolated_renderer.js'
|
||||
},
|
||||
{
|
||||
name: 'content_script_bundle_deps',
|
||||
config: 'webpack.config.content_script.js'
|
||||
},
|
||||
{
|
||||
name: 'browser_bundle_deps',
|
||||
config: 'webpack.config.browser.js'
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { expect } from 'chai';
|
||||
import { app, session, BrowserWindow, ipcMain, WebContents, Extension } from 'electron/main';
|
||||
import { session, BrowserWindow, ipcMain, WebContents, Extension } from 'electron/main';
|
||||
import { closeAllWindows, closeWindow } from './window-helpers';
|
||||
import * as http from 'http';
|
||||
import { AddressInfo } from 'net';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { ifdescribe } from './spec-helpers';
|
||||
import { emittedOnce, emittedNTimes } from './events-helpers';
|
||||
|
||||
const fixtures = path.join(__dirname, 'fixtures');
|
||||
|
||||
ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => {
|
||||
describe('chrome extensions', () => {
|
||||
// NB. extensions are only allowed on http://, https:// and ftp:// (!) urls by default.
|
||||
let server: http.Server;
|
||||
let url: string;
|
||||
|
@ -498,280 +497,3 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
ifdescribe(!process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => {
|
||||
const fixtures = path.resolve(__dirname, 'fixtures');
|
||||
let w: BrowserWindow;
|
||||
|
||||
before(() => {
|
||||
BrowserWindow.addExtension(path.join(fixtures, 'extensions/chrome-api'));
|
||||
});
|
||||
|
||||
after(() => {
|
||||
BrowserWindow.removeExtension('chrome-api');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow({ show: false });
|
||||
});
|
||||
|
||||
afterEach(() => closeWindow(w).then(() => { w = null as unknown as BrowserWindow; }));
|
||||
|
||||
it('chrome.runtime.connect parses arguments properly', async function () {
|
||||
await w.loadURL('about:blank');
|
||||
|
||||
const promise = emittedOnce(w.webContents, 'console-message');
|
||||
|
||||
const message = { method: 'connect' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await promise;
|
||||
const response = JSON.parse(responseString);
|
||||
|
||||
expect(response).to.be.true();
|
||||
});
|
||||
|
||||
it('runtime.getManifest returns extension manifest', async () => {
|
||||
const actualManifest = (() => {
|
||||
const data = fs.readFileSync(path.join(fixtures, 'extensions/chrome-api/manifest.json'), 'utf-8');
|
||||
return JSON.parse(data);
|
||||
})();
|
||||
|
||||
await w.loadURL('about:blank');
|
||||
|
||||
const promise = emittedOnce(w.webContents, 'console-message');
|
||||
|
||||
const message = { method: 'getManifest' };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, manifestString] = await promise;
|
||||
const manifest = JSON.parse(manifestString);
|
||||
|
||||
expect(manifest.name).to.equal(actualManifest.name);
|
||||
expect(manifest.content_scripts).to.have.lengthOf(actualManifest.content_scripts.length);
|
||||
});
|
||||
|
||||
it('chrome.tabs.sendMessage receives the response', async function () {
|
||||
await w.loadURL('about:blank');
|
||||
|
||||
const promise = emittedOnce(w.webContents, 'console-message');
|
||||
|
||||
const message = { method: 'sendMessage', args: ['Hello World!'] };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await promise;
|
||||
const response = JSON.parse(responseString);
|
||||
|
||||
expect(response.message).to.equal('Hello World!');
|
||||
expect(response.tabId).to.equal(w.webContents.id);
|
||||
});
|
||||
|
||||
it('chrome.tabs.executeScript receives the response', async function () {
|
||||
await w.loadURL('about:blank');
|
||||
|
||||
const promise = emittedOnce(w.webContents, 'console-message');
|
||||
|
||||
const message = { method: 'executeScript', args: ['1 + 2'] };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [,, responseString] = await promise;
|
||||
const response = JSON.parse(responseString);
|
||||
|
||||
expect(response).to.equal(3);
|
||||
});
|
||||
|
||||
describe('extensions and dev tools extensions', () => {
|
||||
let showPanelTimeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
const showLastDevToolsPanel = (w: BrowserWindow) => {
|
||||
w.webContents.once('devtools-opened', () => {
|
||||
const show = () => {
|
||||
if (w == null || w.isDestroyed()) return;
|
||||
const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined };
|
||||
if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showLastPanel = () => {
|
||||
// this is executed in the devtools context, where UI is a global
|
||||
const { UI } = (window as any);
|
||||
const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id;
|
||||
UI.inspectorView.showPanel(lastPanelId);
|
||||
};
|
||||
devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => {
|
||||
showPanelTimeoutId = setTimeout(show, 100);
|
||||
});
|
||||
};
|
||||
showPanelTimeoutId = setTimeout(show, 100);
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (showPanelTimeoutId != null) {
|
||||
clearTimeout(showPanelTimeoutId);
|
||||
showPanelTimeoutId = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe('BrowserWindow.addDevToolsExtension', () => {
|
||||
describe('for invalid extensions', () => {
|
||||
it('throws errors for missing manifest.json files', () => {
|
||||
const nonexistentExtensionPath = path.join(__dirname, 'does-not-exist');
|
||||
expect(() => {
|
||||
BrowserWindow.addDevToolsExtension(nonexistentExtensionPath);
|
||||
}).to.throw(/ENOENT: no such file or directory/);
|
||||
});
|
||||
|
||||
it('throws errors for invalid manifest.json files', () => {
|
||||
const badManifestExtensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'bad-manifest');
|
||||
expect(() => {
|
||||
BrowserWindow.addDevToolsExtension(badManifestExtensionPath);
|
||||
}).to.throw(/Unexpected token }/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for a valid extension', () => {
|
||||
const extensionName = 'foo';
|
||||
|
||||
before(() => {
|
||||
const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
|
||||
BrowserWindow.addDevToolsExtension(extensionPath);
|
||||
expect(BrowserWindow.getDevToolsExtensions()).to.have.property(extensionName);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
BrowserWindow.removeDevToolsExtension('foo');
|
||||
expect(BrowserWindow.getDevToolsExtensions()).to.not.have.property(extensionName);
|
||||
});
|
||||
|
||||
describe('when the devtools is docked', () => {
|
||||
let message: any;
|
||||
let w: BrowserWindow;
|
||||
before(async () => {
|
||||
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
|
||||
const p = new Promise(resolve => ipcMain.once('answer', (event, message) => {
|
||||
resolve(message);
|
||||
}));
|
||||
showLastDevToolsPanel(w);
|
||||
w.loadURL('about:blank');
|
||||
w.webContents.openDevTools({ mode: 'bottom' });
|
||||
message = await p;
|
||||
});
|
||||
after(closeAllWindows);
|
||||
|
||||
describe('created extension info', function () {
|
||||
it('has proper "runtimeId"', async function () {
|
||||
expect(message).to.have.ownProperty('runtimeId');
|
||||
expect(message.runtimeId).to.equal(extensionName);
|
||||
});
|
||||
it('has "tabId" matching webContents id', function () {
|
||||
expect(message).to.have.ownProperty('tabId');
|
||||
expect(message.tabId).to.equal(w.webContents.id);
|
||||
});
|
||||
it('has "i18nString" with proper contents', function () {
|
||||
expect(message).to.have.ownProperty('i18nString');
|
||||
expect(message.i18nString).to.equal('foo - bar (baz)');
|
||||
});
|
||||
it('has "storageItems" with proper contents', function () {
|
||||
expect(message).to.have.ownProperty('storageItems');
|
||||
expect(message.storageItems).to.deep.equal({
|
||||
local: {
|
||||
set: { hello: 'world', world: 'hello' },
|
||||
remove: { world: 'hello' },
|
||||
clear: {}
|
||||
},
|
||||
sync: {
|
||||
set: { foo: 'bar', bar: 'foo' },
|
||||
remove: { foo: 'bar' },
|
||||
clear: {}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the devtools is undocked', () => {
|
||||
let message: any;
|
||||
let w: BrowserWindow;
|
||||
before(async () => {
|
||||
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
|
||||
showLastDevToolsPanel(w);
|
||||
w.loadURL('about:blank');
|
||||
w.webContents.openDevTools({ mode: 'undocked' });
|
||||
message = await new Promise(resolve => ipcMain.once('answer', (event, message) => {
|
||||
resolve(message);
|
||||
}));
|
||||
});
|
||||
after(closeAllWindows);
|
||||
|
||||
describe('created extension info', function () {
|
||||
it('has proper "runtimeId"', function () {
|
||||
expect(message).to.have.ownProperty('runtimeId');
|
||||
expect(message.runtimeId).to.equal(extensionName);
|
||||
});
|
||||
it('has "tabId" matching webContents id', function () {
|
||||
expect(message).to.have.ownProperty('tabId');
|
||||
expect(message.tabId).to.equal(w.webContents.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works when used with partitions', async () => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
partition: 'temp'
|
||||
}
|
||||
});
|
||||
|
||||
const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
|
||||
BrowserWindow.addDevToolsExtension(extensionPath);
|
||||
try {
|
||||
showLastDevToolsPanel(w);
|
||||
|
||||
const p: Promise<any> = new Promise(resolve => ipcMain.once('answer', function (event, message) {
|
||||
resolve(message);
|
||||
}));
|
||||
|
||||
w.loadURL('about:blank');
|
||||
w.webContents.openDevTools({ mode: 'bottom' });
|
||||
const message = await p;
|
||||
expect(message.runtimeId).to.equal('foo');
|
||||
} finally {
|
||||
BrowserWindow.removeDevToolsExtension('foo');
|
||||
await closeAllWindows();
|
||||
}
|
||||
});
|
||||
|
||||
it('serializes the registered extensions on quit', () => {
|
||||
const extensionName = 'foo';
|
||||
const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', extensionName);
|
||||
const serializedPath = path.join(app.getPath('userData'), 'DevTools Extensions');
|
||||
|
||||
BrowserWindow.addDevToolsExtension(extensionPath);
|
||||
app.emit('will-quit');
|
||||
expect(JSON.parse(fs.readFileSync(serializedPath, 'utf8'))).to.deep.equal([extensionPath]);
|
||||
|
||||
BrowserWindow.removeDevToolsExtension(extensionName);
|
||||
app.emit('will-quit');
|
||||
expect(fs.existsSync(serializedPath)).to.be.false('file exists');
|
||||
});
|
||||
|
||||
describe('BrowserWindow.addExtension', () => {
|
||||
it('throws errors for missing manifest.json files', () => {
|
||||
expect(() => {
|
||||
BrowserWindow.addExtension(path.join(__dirname, 'does-not-exist'));
|
||||
}).to.throw('ENOENT: no such file or directory');
|
||||
});
|
||||
|
||||
it('throws errors for invalid manifest.json files', () => {
|
||||
expect(() => {
|
||||
BrowserWindow.addExtension(path.join(__dirname, 'fixtures', 'devtools-extensions', 'bad-manifest'));
|
||||
}).to.throw('Unexpected token }');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue