From 420d08f172d5b29903ffe98b7246a5db7d15d6ef Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Mon, 16 Dec 2024 19:34:28 -0500 Subject: [PATCH] feat: support chrome_url_overrides for custom newtab page --- packages/electron-chrome-extensions/README.md | 21 +++++++++++ .../src/browser/index.ts | 17 +++++++++ .../src/browser/manifest.ts | 35 +++++++++++++++++++ .../src/browser/store.ts | 2 ++ .../src/browser/index.ts | 12 +++---- packages/shell/browser/main.js | 34 +++++++++++++----- packages/shell/browser/ui/manifest.json | 5 ++- 7 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 packages/electron-chrome-extensions/src/browser/manifest.ts diff --git a/packages/electron-chrome-extensions/README.md b/packages/electron-chrome-extensions/README.md index 94d0fd9a..e8c9c12a 100644 --- a/packages/electron-chrome-extensions/README.md +++ b/packages/electron-chrome-extensions/README.md @@ -169,6 +169,19 @@ Notify the extension system that a tab has been selected as the active tab. Returns [`Electron.MenuItem[]`](https://www.electronjs.org/docs/api/menu-item#class-menuitem) - An array of all extension context menu items given the context. +##### `extensions.getURLOverrides()` + +Returns `Object` which maps special URL types to an extension URL. See [chrome_urls_overrides](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides) for a list of +supported URL types. + +Example: + +``` +{ + newtab: 'chrome-extension:///newtab.html' +} +``` + #### Instance Events ##### Event: 'browser-action-popup-created' @@ -179,6 +192,14 @@ Returns: Emitted when a popup is created by the `chrome.browserAction` API. +##### Event: 'url-overrides-updated' + +Returns: + +- `urlOverrides` Object - A map of url types to extension URLs. + +Emitted after an extension is loaded with [chrome_urls_overrides](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides) set. + ### Element: `` diff --git a/packages/electron-chrome-extensions/src/browser/index.ts b/packages/electron-chrome-extensions/src/browser/index.ts index d4403a9e..16a1bcc8 100644 --- a/packages/electron-chrome-extensions/src/browser/index.ts +++ b/packages/electron-chrome-extensions/src/browser/index.ts @@ -17,6 +17,7 @@ import { CommandsAPI } from './api/commands' import { ExtensionContext } from './context' import { ExtensionRouter } from './router' import { checkLicense, License } from './license' +import { readLoadedExtensionManifest } from './manifest' export interface ChromeExtensionOptions extends ChromeExtensionImpl { /** @@ -98,9 +99,16 @@ export class ElectronChromeExtensions extends EventEmitter { windows: new WindowsAPI(this.ctx), } + this.listenForExtensions() this.prependPreload() } + private listenForExtensions() { + this.ctx.session.addListener('extension-loaded', (_event, extension) => { + readLoadedExtensionManifest(this.ctx, extension) + }) + } + private async prependPreload() { const { session } = this.ctx @@ -169,6 +177,15 @@ export class ElectronChromeExtensions extends EventEmitter { return this.api.contextMenus.buildMenuItemsForParams(webContents, params) } + /** + * Gets map of special pages to extension override URLs. + * + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides + */ + getURLOverrides(): Record { + return this.ctx.store.urlOverrides + } + /** * Add extensions to be visible as an extension action button. * diff --git a/packages/electron-chrome-extensions/src/browser/manifest.ts b/packages/electron-chrome-extensions/src/browser/manifest.ts new file mode 100644 index 00000000..1add2626 --- /dev/null +++ b/packages/electron-chrome-extensions/src/browser/manifest.ts @@ -0,0 +1,35 @@ +import { getExtensionUrl, validateExtensionResource } from './api/common' +import { ExtensionContext } from './context' + +export async function readUrlOverrides(ctx: ExtensionContext, extension: Electron.Extension) { + const manifest = extension.manifest as chrome.runtime.Manifest + const urlOverrides = ctx.store.urlOverrides + let updated = false + + if (typeof manifest.chrome_url_overrides === 'object') { + for (const [name, uri] of Object.entries(manifest.chrome_url_overrides!)) { + const validatedPath = await validateExtensionResource(extension, uri) + if (!validatedPath) { + console.error( + `Extension ${extension.id} attempted to override ${name} with invalid resource: ${uri}`, + ) + continue + } + + const url = getExtensionUrl(extension, uri)! + const currentUrl = urlOverrides[name] + if (currentUrl !== url) { + urlOverrides[name] = url + updated = true + } + } + } + + if (updated) { + ctx.emit('url-overrides-updated', urlOverrides) + } +} + +export function readLoadedExtensionManifest(ctx: ExtensionContext, extension: Electron.Extension) { + readUrlOverrides(ctx, extension) +} diff --git a/packages/electron-chrome-extensions/src/browser/store.ts b/packages/electron-chrome-extensions/src/browser/store.ts index 756dbd3c..7699de69 100644 --- a/packages/electron-chrome-extensions/src/browser/store.ts +++ b/packages/electron-chrome-extensions/src/browser/store.ts @@ -29,6 +29,8 @@ export class ExtensionStore extends EventEmitter { tabDetailsCache = new Map>() windowDetailsCache = new Map>() + urlOverrides: Record = {} + constructor(public impl: ChromeExtensionImpl) { super() } diff --git a/packages/electron-chrome-web-store/src/browser/index.ts b/packages/electron-chrome-web-store/src/browser/index.ts index 4af37486..2e3a9a8a 100644 --- a/packages/electron-chrome-web-store/src/browser/index.ts +++ b/packages/electron-chrome-web-store/src/browser/index.ts @@ -522,7 +522,7 @@ interface ElectronChromeWebStoreOptions { * * @param options Chrome Web Store configuration options. */ -export function installChromeWebStore(opts: ElectronChromeWebStoreOptions = {}) { +export async function installChromeWebStore(opts: ElectronChromeWebStoreOptions = {}) { const session = opts.session || electronSession.defaultSession const extensionsPath = opts.extensionsPath || path.join(app.getPath('userData'), 'Extensions') const modulePath = opts.modulePath || __dirname @@ -554,9 +554,9 @@ export function installChromeWebStore(opts: ElectronChromeWebStoreOptions = {}) addIpcListeners(webStoreState) - app.whenReady().then(() => { - if (loadExtensions) { - loadAllExtensions(session, extensionsPath, { allowUnpacked: allowUnpackedExtensions }) - } - }) + await app.whenReady() + + if (loadExtensions) { + await loadAllExtensions(session, extensionsPath, { allowUnpacked: allowUnpackedExtensions }) + } } diff --git a/packages/shell/browser/main.js b/packages/shell/browser/main.js index a465d9c5..277c5d45 100644 --- a/packages/shell/browser/main.js +++ b/packages/shell/browser/main.js @@ -1,5 +1,4 @@ const path = require('path') -const { promises: fs } = require('fs') const { app, session, BrowserWindow } = require('electron') const { Tabs } = require('./tabs') @@ -51,7 +50,7 @@ class TabbedBrowserWindow { const self = this this.tabs.on('tab-created', function onTabCreated(tab) { - if (options.initialUrl) tab.webContents.loadURL(options.initialUrl) + tab.loadURL(options.urls.newtab) // Track tab that may have been created outside of the extensions API. self.extensions.addTab(tab.webContents, tab.window) @@ -63,7 +62,11 @@ class TabbedBrowserWindow { queueMicrotask(() => { // Create initial tab - this.tabs.create() + const tab = this.tabs.create() + + if (options.initialUrl) { + tab.loadURL(options.initialUrl) + } }) } @@ -80,6 +83,10 @@ class TabbedBrowserWindow { class Browser { windows = [] + urls = { + newtab: 'about:blank', + } + constructor() { app.whenReady().then(this.init.bind(this)) @@ -153,7 +160,7 @@ class Browser { const tab = win.tabs.create() - if (details.url) tab.loadURL(details.url || newTabUrl) + if (details.url) tab.loadURL(details.url) if (typeof details.active === 'boolean' ? details.active : true) win.tabs.select(tab.id) return [tab.webContents, tab.window] @@ -169,7 +176,7 @@ class Browser { createWindow: (details) => { const win = this.createWindow({ - initialUrl: details.url || newTabUrl, + initialUrl: details.url, }) // if (details.active) tabs.select(tab.id) return win.window @@ -184,16 +191,25 @@ class Browser { this.popup = popup }) + // Allow extensions to override new tab page + this.extensions.on('url-overrides-updated', (urlOverrides) => { + if (urlOverrides.newtab) { + this.urls.newtab = urlOverrides.newtab + } + }) + const webuiExtension = await this.session.loadExtension(PATHS.WEBUI) webuiExtensionId = webuiExtension.id - installChromeWebStore({ + // Wait for web store extensions to finish loading as they may change the + // newtab URL. + await installChromeWebStore({ session: this.session, modulePath: path.join(__dirname, 'electron-chrome-web-store'), }) if (!app.isPackaged) { - loadAllExtensions(this.session, PATHS.LOCAL_EXTENSIONS, true) + await loadAllExtensions(this.session, PATHS.LOCAL_EXTENSIONS, true) } this.createInitialWindow() @@ -213,6 +229,7 @@ class Browser { createWindow(options) { const win = new TabbedBrowserWindow({ ...options, + urls: this.urls, extensions: this.extensions, window: { width: 1280, @@ -243,8 +260,7 @@ class Browser { } createInitialWindow() { - const newTabUrl = path.join('chrome-extension://', webuiExtensionId, 'new-tab.html') - this.createWindow({ initialUrl: newTabUrl }) + this.createWindow() } async onWebContentsCreated(event, webContents) { diff --git a/packages/shell/browser/ui/manifest.json b/packages/shell/browser/ui/manifest.json index 3142d6d5..c082ac51 100644 --- a/packages/shell/browser/ui/manifest.json +++ b/packages/shell/browser/ui/manifest.json @@ -2,5 +2,8 @@ "name": "WebUI", "version": "1.0.0", "manifest_version": 3, - "permissions": [] + "permissions": [], + "chrome_url_overrides": { + "newtab": "new-tab.html" + } }