diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.js index 2612e2b190fa..20ee4a4025df 100644 --- a/config/webpack/webpack.desktop.js +++ b/config/webpack/webpack.desktop.js @@ -28,8 +28,8 @@ module.exports = (env) => { name: 'desktop-main', target: 'electron-main', entry: { - main: './desktop/main.js', - contextBridge: './desktop/contextBridge.js', + main: './desktop/main.ts', + contextBridge: './desktop/contextBridge.ts', }, output: { filename: '[name].js', diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.ts similarity index 90% rename from desktop/ELECTRON_EVENTS.js rename to desktop/ELECTRON_EVENTS.ts index ee8c0521892e..de0bd655e12c 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.ts @@ -9,6 +9,6 @@ const ELECTRON_EVENTS = { KEYBOARD_SHORTCUTS_PAGE: 'keyboard-shortcuts-page', START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', -}; +} as const; -module.exports = ELECTRON_EVENTS; +export default ELECTRON_EVENTS; diff --git a/desktop/README.md b/desktop/README.md index 77abff97a898..2fc074153d87 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -31,15 +31,15 @@ The New Expensify desktop app is built using [Electron.js](https://www.electronj The desktop app is organized in three pieces: 1. The Electron main process - - Implemented in https://github.com/Expensify/App/blob/main/desktop/main.js. + - Implemented in https://github.com/Expensify/App/blob/main/desktop/main.ts. - This file has access to the full set of Electron and Node.JS APIs. 2. The Electron renderer process - This is the webpack-bundled version of our react-native-web app (except using `index.desktop.js` files instead of `index.website.js`, where applicable) - This is _very_ similar to our web app, and code in this process should assume it will be run in the context of a browser (no access to `require`, Electron, or Node.js APis) 3. The context bridge - - Implemented in https://github.com/Expensify/App/blob/main/desktop/contextBridge.js + - Implemented in https://github.com/Expensify/App/blob/main/desktop/contextBridge.ts - The context bridge enables communication between the main and renderer processes. For example, if the renderer process needs to make use of a Node.js or Electron API it must: - 1. Define an event in https://github.com/Expensify/App/blob/main/desktop/ELECTRON_EVENTS.js + 1. Define an event in https://github.com/Expensify/App/blob/main/desktop/ELECTRON_EVENTS.ts 2. Add that event to the whitelist defined in the context bridge 3. Set up a handler for the event in the main process that can respond to the renderer process back through the bridge, if necessary. @@ -131,7 +131,7 @@ The root [package.json](../package.json) serves for `devDependencies` and shared The [desktop/package.json](./package.json) serves for desktop (electron-main) specific dependencies We use Webpack with a [desktop specific config](../config/webpack/webpack.desktop.js) to bundle our js code Half of the config takes care of packaging root package dependencies - everything related to rendering App in the Electron window. Packaged under `dist/www` -The other half is about bundling the `main.js` script which initializes Electron and renders `www` +The other half is about bundling the `main.ts` script which initializes Electron and renders `www` ## See what is getting packaged in the app If you suspect unnecessary items might be getting packaged you can inspect the package content in `desktop-build/` diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js deleted file mode 100644 index a8b89cdc0b64..000000000000 --- a/desktop/contextBridge.js +++ /dev/null @@ -1,98 +0,0 @@ -const _ = require('underscore'); -const {contextBridge, ipcRenderer} = require('electron'); -const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); - -const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ - ELECTRON_EVENTS.REQUEST_DEVICE_ID, - ELECTRON_EVENTS.REQUEST_FOCUS_APP, - ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, - ELECTRON_EVENTS.REQUEST_VISIBILITY, - ELECTRON_EVENTS.START_UPDATE, - ELECTRON_EVENTS.LOCALE_UPDATED, -]; - -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR]; - -const getErrorMessage = (channel) => `Electron context bridge cannot be used with channel '${channel}'`; - -/** - * The following methods will be available in the renderer process under `window.electron`. - */ -contextBridge.exposeInMainWorld('electron', { - /** - * Send data asynchronously from renderer process to main process. - * Note that this is a one-way channel – main will not respond. In order to get a response from main, either: - * - * - Use `sendSync` - * - Or implement `invoke` if you want to maintain asynchronous communication: https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args - * - * @param {String} channel - * @param {*} data - */ - send: (channel, data) => { - if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { - throw new Error(getErrorMessage(channel)); - } - - ipcRenderer.send(channel, data); - }, - - /** - * Send data synchronously from renderer process to main process. Main process may return a result. - * - * @param {String} channel - * @param {*} data - * @returns {*} - */ - sendSync: (channel, data) => { - if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { - throw new Error(getErrorMessage(channel)); - } - - return ipcRenderer.sendSync(channel, data); - }, - - /** - * Execute a function in the main process and return a promise that resolves with its response. - * - * @param {String} channel - * @param {*} args - * @returns {Promise} - */ - invoke: (channel, ...args) => { - if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { - throw new Error(getErrorMessage(channel)); - } - - return ipcRenderer.invoke(channel, ...args); - }, - - /** - * Set up a listener for events emitted from the main process and sent to the renderer process. - * - * @param {String} channel - * @param {Function} func - */ - on: (channel, func) => { - if (!_.contains(WHITELIST_CHANNELS_MAIN_TO_RENDERER, channel)) { - throw new Error(getErrorMessage(channel)); - } - - // Deliberately strip event as it includes `sender` - ipcRenderer.on(channel, (event, ...args) => func(...args)); - }, - - /** - * Remove listeners for a single channel from the main process and sent to the renderer process. - * - * @param {String} channel - * @param {Function} func - */ - removeAllListeners: (channel) => { - if (!_.contains(WHITELIST_CHANNELS_MAIN_TO_RENDERER, channel)) { - throw new Error(getErrorMessage(channel)); - } - - ipcRenderer.removeAllListeners(channel); - }, -}); diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts new file mode 100644 index 000000000000..689c69de0cc8 --- /dev/null +++ b/desktop/contextBridge.ts @@ -0,0 +1,82 @@ +import {contextBridge, ipcRenderer} from 'electron'; +import ELECTRON_EVENTS from './ELECTRON_EVENTS'; + +type ContextBridgeApi = { + send: (channel: string, data?: unknown) => void; + sendSync: (channel: string, data?: unknown) => unknown; + invoke: (channel: string, ...args: unknown[]) => Promise; + on: (channel: string, func: (...args: unknown[]) => void) => void; + removeAllListeners: (channel: string) => void; +}; + +const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ + ELECTRON_EVENTS.REQUEST_DEVICE_ID, + ELECTRON_EVENTS.REQUEST_FOCUS_APP, + ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, + ELECTRON_EVENTS.REQUEST_VISIBILITY, + ELECTRON_EVENTS.START_UPDATE, + ELECTRON_EVENTS.LOCALE_UPDATED, +] as const; + +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR] as const; + +const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; + +/** + * The following methods will be available in the renderer process under `window.electron`. + */ +contextBridge.exposeInMainWorld('electron', { + /** + * Send data asynchronously from renderer process to main process. + * Note that this is a one-way channel – main will not respond. In order to get a response from main, either: + * + * - Use `sendSync` + * - Or implement `invoke` if you want to maintain asynchronous communication: https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args + */ + send: (channel: string, data: unknown) => { + if (!WHITELIST_CHANNELS_RENDERER_TO_MAIN.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + ipcRenderer.send(channel, data); + }, + + /** Send data synchronously from renderer process to main process. Main process may return a result. */ + sendSync: (channel: string, data: unknown): unknown => { + if (!WHITELIST_CHANNELS_RENDERER_TO_MAIN.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + return ipcRenderer.sendSync(channel, data); + }, + + /** Execute a function in the main process and return a promise that resolves with its response. */ + invoke: (channel: string, ...args: unknown[]): Promise => { + if (!WHITELIST_CHANNELS_RENDERER_TO_MAIN.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + return ipcRenderer.invoke(channel, ...args); + }, + + /** Set up a listener for events emitted from the main process and sent to the renderer process. */ + on: (channel: string, func: (...args: unknown[]) => void) => { + if (!WHITELIST_CHANNELS_MAIN_TO_RENDERER.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + // Deliberately strip event as it includes `sender` + ipcRenderer.on(channel, (event, ...args) => func(...args)); + }, + + /** Remove listeners for a single channel from the main process and sent to the renderer process. */ + removeAllListeners: (channel: string) => { + if (!WHITELIST_CHANNELS_MAIN_TO_RENDERER.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + ipcRenderer.removeAllListeners(channel); + }, +}); + +export default ContextBridgeApi; diff --git a/desktop/main.js b/desktop/main.ts similarity index 86% rename from desktop/main.js rename to desktop/main.ts index 4b38c5d36ab3..cbc12d9d2608 100644 --- a/desktop/main.js +++ b/desktop/main.ts @@ -1,17 +1,21 @@ -const {app, dialog, clipboard, BrowserWindow, Menu, MenuItem, shell, ipcMain} = require('electron'); -const _ = require('underscore'); -const serve = require('electron-serve'); -const contextMenu = require('electron-context-menu'); -const {autoUpdater} = require('electron-updater'); -const log = require('electron-log'); -const {machineId} = require('node-machine-id'); -const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); -const checkForUpdates = require('../src/libs/checkForUpdates'); -const CONFIG = require('../src/CONFIG').default; -const CONST = require('../src/CONST').default; -const Localize = require('../src/libs/Localize'); - -const port = process.env.PORT || 8082; +import {app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell} from 'electron'; +import type {BrowserView, MenuItem, MenuItemConstructorOptions, WebContents, WebviewTag} from 'electron'; +import contextMenu from 'electron-context-menu'; +import log from 'electron-log'; +import type {ElectronLog} from 'electron-log'; +import serve from 'electron-serve'; +import {autoUpdater} from 'electron-updater'; +import {machineId} from 'node-machine-id'; +import checkForUpdates from '@libs/checkForUpdates'; +import * as Localize from '@libs/Localize'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type PlatformSpecificUpdater from '@src/setup/platformSetup/types'; +import type {Locale} from '@src/types/onyx'; +import ELECTRON_EVENTS from './ELECTRON_EVENTS'; + +const port = process.env.PORT ?? 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; // Setup google api key in process environment, we are setting it this way intentionally. It is required by the @@ -34,22 +38,24 @@ app.commandLine.appendSwitch('enable-network-information-downlink-max'); /** * Inserts the plain text from the clipboard into the provided browser window's web contents. * - * @param {BrowserWindow} browserWindow - The Electron BrowserWindow instance where the text should be inserted. + * @param browserWindow - The Electron BrowserWindow instance where the text should be inserted. */ -function pasteAsPlainText(browserWindow) { +function pasteAsPlainText(browserWindow: BrowserWindow | BrowserView | WebviewTag | WebContents) { const text = clipboard.readText(); - browserWindow.webContents.insertText(text); + + if ('webContents' in browserWindow) { + browserWindow.webContents.insertText(text); + } } /** * Initialize the right-click menu * See https://github.com/sindresorhus/electron-context-menu * - * @param {String} preferredLocale - The current user language to be used for translating menu labels. - * @returns {Function} A dispose function to clean up the created context menu. + * @param preferredLocale - The current user language to be used for translating menu labels. + * @returns A dispose function to clean up the created context menu. */ - -function createContextMenu(preferredLocale = LOCALES.DEFAULT) { +function createContextMenu(preferredLocale: Locale = LOCALES.DEFAULT): () => void { return contextMenu({ labels: { cut: Localize.translate(preferredLocale, 'desktopApplicationMenu.cut'), @@ -57,19 +63,19 @@ function createContextMenu(preferredLocale = LOCALES.DEFAULT) { copy: Localize.translate(preferredLocale, 'desktopApplicationMenu.copy'), }, append: (defaultActions, parameters, browserWindow) => [ - new MenuItem({ + { // Only enable the menu item for Editable context which supports paste visible: parameters.isEditable && parameters.editFlags.canPaste, role: 'pasteAndMatchStyle', accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AND_MATCH_STYLE, label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAndMatchStyle'), - }), - new MenuItem({ + }, + { label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAsPlainText'), visible: parameters.isEditable && parameters.editFlags.canPaste && clipboard.readText().length > 0, accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AS_PLAIN_TEXT, click: () => pasteAsPlainText(browserWindow), - }), + }, ], }); } @@ -79,11 +85,11 @@ let disposeContextMenu = createContextMenu(); // Send all autoUpdater logs to a log file: ~/Library/Logs/new.expensify.desktop/main.log // See https://www.npmjs.com/package/electron-log autoUpdater.logger = log; -autoUpdater.logger.transports.file.level = 'info'; +(autoUpdater.logger as ElectronLog).transports.file.level = 'info'; // Send all Console logs to a log file: ~/Library/Logs/new.expensify.desktop/main.log // See https://www.npmjs.com/package/electron-log -_.assign(console, log.functions); +Object.assign(console, log.functions); // This sets up the command line arguments used to manage the update. When // the --expected-update-version flag is set, the app will open pre-hidden @@ -92,23 +98,24 @@ _.assign(console, log.functions); const EXPECTED_UPDATE_VERSION_FLAG = '--expected-update-version'; const APP_DOMAIN = __DEV__ ? `https://dev.new.expensify.com:${port}` : 'app://-'; -let expectedUpdateVersion; -for (let i = 0; i < process.argv.length; i++) { - const arg = process.argv[i]; - if (arg.startsWith(`${EXPECTED_UPDATE_VERSION_FLAG}=`)) { - expectedUpdateVersion = arg.substr(`${EXPECTED_UPDATE_VERSION_FLAG}=`.length); +let expectedUpdateVersion: string; +process.argv.forEach((arg) => { + if (!arg.startsWith(`${EXPECTED_UPDATE_VERSION_FLAG}=`)) { + return; } -} + + expectedUpdateVersion = arg.substr(`${EXPECTED_UPDATE_VERSION_FLAG}=`.length); +}); // Add the listeners and variables required to ensure that auto-updating // happens correctly. let hasUpdate = false; -let downloadedVersion; +let downloadedVersion: string; // Note that we have to subscribe to this separately and cannot use Localize.translateLocal, // because the only way code can be shared between the main and renderer processes at runtime is via the context bridge // So we track preferredLocale separately via ELECTRON_EVENTS.LOCALE_UPDATED -const preferredLocale = CONST.LOCALES.DEFAULT; +const preferredLocale: Locale = CONST.LOCALES.DEFAULT; const appProtocol = CONST.DEEPLINK_BASE_URL.replace('://', ''); @@ -120,12 +127,8 @@ const quitAndInstallWithUpdate = () => { autoUpdater.quitAndInstall(); }; -/** - * Menu Item callback to triggers an update check - * @param {MenuItem} menuItem - * @param {BrowserWindow} browserWindow - */ -const manuallyCheckForUpdates = (menuItem, browserWindow) => { +/** Menu Item callback to triggers an update check */ +const manuallyCheckForUpdates = (menuItem: MenuItem, browserWindow?: BrowserWindow) => { // Disable item until the check (and download) is complete // eslint: menu item flags like enabled or visible can be dynamically toggled by mutating the object // eslint-disable-next-line no-param-reassign @@ -135,7 +138,11 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { .checkForUpdates() .catch((error) => ({error})) .then((result) => { - const downloadPromise = result && result.downloadPromise; + const downloadPromise = result && 'downloadPromise' in result ? result.downloadPromise : undefined; + + if (!browserWindow) { + return; + } if (downloadPromise) { dialog.showMessageBox(browserWindow, { @@ -144,7 +151,7 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.message'), buttons: [Localize.translate(preferredLocale, 'checkForUpdatesModal.available.soundsGood')], }); - } else if (result && result.error) { + } else if (result && 'error' in result && result.error) { dialog.showMessageBox(browserWindow, { type: 'error', message: Localize.translate(preferredLocale, 'checkForUpdatesModal.error.title'), @@ -170,25 +177,30 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { }); }; -/** - * Trigger event to show keyboard shortcuts - * @param {BrowserWindow} browserWindow - */ -const showKeyboardShortcutsPage = (browserWindow) => { +/** Trigger event to show keyboard shortcuts */ +const showKeyboardShortcutsPage = (browserWindow: BrowserWindow) => { if (!browserWindow.isVisible()) { return; } browserWindow.webContents.send(ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE); }; -// Actual auto-update listeners -const electronUpdater = (browserWindow) => ({ +/** Actual auto-update listeners */ +const electronUpdater = (browserWindow: BrowserWindow): PlatformSpecificUpdater => ({ init: () => { autoUpdater.on(ELECTRON_EVENTS.UPDATE_DOWNLOADED, (info) => { const systemMenu = Menu.getApplicationMenu(); + const updateMenuItem = systemMenu?.getMenuItemById(`update`); + const checkForUpdatesMenuItem = systemMenu?.getMenuItemById(`checkForUpdates`); + downloadedVersion = info.version; - systemMenu.getMenuItemById(`update`).visible = true; - systemMenu.getMenuItemById(`checkForUpdates`).visible = false; + + if (updateMenuItem) { + updateMenuItem.visible = true; + } + if (checkForUpdatesMenuItem) { + checkForUpdatesMenuItem.visible = false; + } if (browserWindow.isVisible()) { browserWindow.webContents.send(ELECTRON_EVENTS.UPDATE_DOWNLOADED, info.version); } else { @@ -204,29 +216,26 @@ const electronUpdater = (browserWindow) => ({ }, }); -/* - * @param {Menu} systemMenu - */ -const localizeMenuItems = (submenu, updatedLocale) => - _.map(submenu, (menu) => { - const newMenu = _.clone(menu); +const localizeMenuItems = (submenu: MenuItemConstructorOptions[], updatedLocale: Locale): MenuItemConstructorOptions[] => + submenu.map((menu) => { + const newMenu: MenuItemConstructorOptions = {...menu}; if (menu.id) { - const labelTranslation = Localize.translate(updatedLocale, `desktopApplicationMenu.${menu.id}`); + const labelTranslation = Localize.translate(updatedLocale, `desktopApplicationMenu.${menu.id}` as TranslationPaths); if (labelTranslation) { newMenu.label = labelTranslation; } } if (menu.submenu) { - newMenu.submenu = localizeMenuItems(menu.submenu, updatedLocale); + newMenu.submenu = localizeMenuItems(menu.submenu as MenuItemConstructorOptions[], updatedLocale); } return newMenu; }); -const mainWindow = () => { - let deeplinkUrl; - let browserWindow; +const mainWindow = (): Promise => { + let deeplinkUrl: string; + let browserWindow: BrowserWindow; - const loadURL = __DEV__ ? (win) => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); + const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); // Prod and staging set the icon in the electron-builder config, so only update it here for dev if (__DEV__) { @@ -296,17 +305,17 @@ const mainWindow = () => { if (!__DEV__) { // Modify the origin and referer for requests sent to our API webRequest.onBeforeSendHeaders(validDestinationFilters, (details, callback) => { - details.requestHeaders.origin = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH; - details.requestHeaders.referer = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH; callback({requestHeaders: details.requestHeaders}); }); } // Modify access-control-allow-origin header and CSP for the response webRequest.onHeadersReceived(validDestinationFilters, (details, callback) => { - details.responseHeaders['access-control-allow-origin'] = [APP_DOMAIN]; - if (details.responseHeaders['content-security-policy']) { - details.responseHeaders['content-security-policy'] = _.map(details.responseHeaders['content-security-policy'], (value) => + if (details.responseHeaders) { + details.responseHeaders['access-control-allow-origin'] = [APP_DOMAIN]; + } + if (details.responseHeaders?.['content-security-policy']) { + details.responseHeaders['content-security-policy'] = details.responseHeaders['content-security-policy'].map((value) => value.startsWith('frame-ancestors') ? `${value} ${APP_DOMAIN}` : value, ); } @@ -319,7 +328,7 @@ const mainWindow = () => { browserWindow.setTitle('New Expensify'); } - const initialMenuTemplate = [ + const initialMenuTemplate: MenuItemConstructorOptions[] = [ { id: 'mainMenu', label: Localize.translate(preferredLocale, `desktopApplicationMenu.mainMenu`), @@ -404,14 +413,13 @@ const mainWindow = () => { submenu: [ { id: 'back', - role: 'back', accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[', click: () => { browserWindow.webContents.goBack(); }, }, { - role: 'back', + label: 'backWithKeyShortcut', visible: false, accelerator: process.platform === 'darwin' ? 'Cmd+Left' : 'Shift+Left', click: () => { @@ -420,14 +428,13 @@ const mainWindow = () => { }, { id: 'forward', - role: 'forward', accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]', click: () => { browserWindow.webContents.goForward(); }, }, { - role: 'forward', + label: 'forwardWithKeyShortcut', visible: false, accelerator: process.platform === 'darwin' ? 'Cmd+Right' : 'Shift+Right', click: () => { @@ -485,7 +492,7 @@ const mainWindow = () => { // When the user clicks a link that has target="_blank" (which is all external links) // open the default browser instead of a new electron window browserWindow.webContents.setWindowOpenHandler(({url}) => { - const denial = {action: 'deny'}; + const denial = {action: 'deny'} as const; // Make sure local urls stay in electron perimeter if (url.substr(0, 'file://'.length).toLowerCase() === 'file://') { diff --git a/desktop/start.js b/desktop/start.ts similarity index 83% rename from desktop/start.js rename to desktop/start.ts index 05a1b031350d..17ec690507ed 100644 --- a/desktop/start.js +++ b/desktop/start.ts @@ -1,7 +1,9 @@ #!/usr/bin/env node -const portfinder = require('portfinder'); -const concurrently = require('concurrently'); -require('dotenv').config(); +import concurrently from 'concurrently'; +import {config as configDotenv} from 'dotenv'; +import portfinder from 'portfinder'; + +configDotenv(); const basePort = 8082; @@ -9,7 +11,7 @@ portfinder .getPortPromise({ port: basePort, }) - .then((port) => { + .then((port): Promise => { const devServer = `webpack-dev-server --config config/webpack/webpack.dev.js --port ${port} --env platform=desktop`; const buildMain = 'webpack watch --config config/webpack/webpack.desktop.js --config-name desktop-main --mode=development'; @@ -39,13 +41,15 @@ portfinder }, ]; - return concurrently(processes, { + const {result} = concurrently(processes, { inputStream: process.stdin, prefix: 'name', // Like Harry Potter and he-who-must-not-be-named, "neither can live while the other survives" killOthers: ['success', 'failure'], - }).then( + }); + + return result.then( () => process.exit(0), () => process.exit(1), ); diff --git a/package-lock.json b/package-lock.json index d67a015a563b..a84a72dd5167 100644 --- a/package-lock.json +++ b/package-lock.json @@ -199,7 +199,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", - "concurrently": "^5.3.0", + "concurrently": "^8.2.2", "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", @@ -2514,19 +2514,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@callstack/reassure-cli/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@callstack/reassure-cli/node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -2562,39 +2549,6 @@ "node": ">=8" } }, - "node_modules/@callstack/reassure-cli/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@callstack/reassure-cli/node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@callstack/reassure-cli/node_modules/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/@callstack/reassure-compare": { "version": "0.6.0", "dev": true, @@ -23136,63 +23090,16 @@ } }, "node_modules/cliui": { - "version": "5.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "7.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "5.1.0", - "dev": true, - "license": "MIT", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=12" } }, "node_modules/clone": { @@ -23510,36 +23417,115 @@ } }, "node_modules/concurrently": { - "version": "5.3.0", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", "dev": true, - "license": "MIT", "dependencies": { - "chalk": "^2.4.2", - "date-fns": "^2.0.1", - "lodash": "^4.17.15", - "read-pkg": "^4.0.1", - "rxjs": "^6.5.2", - "spawn-command": "^0.0.2-1", - "supports-color": "^6.1.0", + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", "tree-kill": "^1.2.2", - "yargs": "^13.3.0" + "yargs": "^17.7.2" }, "bin": { - "concurrently": "bin/concurrently.js" + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": ">=6.0.0" + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/concurrently/node_modules/supports-color": { - "version": "6.1.0", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/config-file-ts": { @@ -25593,19 +25579,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/electron-builder/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/electron-builder/node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -25654,39 +25627,6 @@ "node": ">=8" } }, - "node_modules/electron-builder/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-builder/node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/electron-publish": { "version": "24.13.1", "dev": true, @@ -31601,18 +31541,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-cli/node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", "license": "MIT", @@ -31644,36 +31572,6 @@ "node": ">=8" } }, - "node_modules/jest-cli/node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "17.7.2", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-cli/node_modules/yargs-parser": { - "version": "21.1.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/jest-config": { "version": "29.4.1", "license": "MIT", @@ -35252,15 +35150,6 @@ "tslib": "2" } }, - "node_modules/memfs/node_modules/rxjs": { - "version": "7.8.1", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/memoize-one": { "version": "5.2.1", "license": "MIT" @@ -35907,18 +35796,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/metro/node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/metro/node_modules/color-convert": { "version": "2.0.1", "license": "MIT", @@ -36031,36 +35908,6 @@ } } }, - "node_modules/metro/node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/metro/node_modules/yargs": { - "version": "17.7.2", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/metro/node_modules/yargs-parser": { - "version": "21.1.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/microevent.ts": { "version": "0.1.1", "dev": true, @@ -39709,18 +39556,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/react-native/node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/react-native/node_modules/color-convert": { "version": "2.0.1", "license": "MIT", @@ -39802,36 +39637,6 @@ "async-limiter": "~1.0.0" } }, - "node_modules/react-native/node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/react-native/node_modules/yargs": { - "version": "17.7.2", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/react-native/node_modules/yargs-parser": { - "version": "21.1.1", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/react-pdf": { "version": "7.3.3", "license": "MIT", @@ -41389,21 +41194,14 @@ "license": "BSD-3-Clause" }, "node_modules/rxjs": { - "version": "6.6.7", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, "node_modules/safe-array-concat": { "version": "1.0.0", "dev": true, @@ -42341,9 +42139,10 @@ } }, "node_modules/spawn-command": { - "version": "0.0.2-1", - "dev": true, - "license": "MIT" + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true }, "node_modules/spdx-correct": { "version": "3.1.1", @@ -46384,20 +46183,20 @@ } }, "node_modules/yargs": { - "version": "13.3.2", - "dev": true, - "license": "MIT", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, "node_modules/yargs-parser": { @@ -46408,106 +46207,20 @@ "node": ">=10" } }, - "node_modules/yargs/node_modules/camelcase": { - "version": "5.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "7.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/find-up": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/yargs/node_modules/yargs-parser": { - "version": "13.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index e26c0ea5a65a..e0f357fd0f8a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", - "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.js", + "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.ts", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.js", @@ -250,7 +250,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", - "concurrently": "^5.3.0", + "concurrently": "^8.2.2", "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", diff --git a/src/types/modules/electron.d.ts b/src/types/modules/electron.d.ts index 09e33f29ba38..7b2ecaa3866d 100644 --- a/src/types/modules/electron.d.ts +++ b/src/types/modules/electron.d.ts @@ -1,11 +1,4 @@ -// TODO: Move this type to desktop/contextBridge.js once it is converted to TS -type ContextBridgeApi = { - send: (channel: string, data?: unknown) => void; - sendSync: (channel: string, data?: unknown) => unknown; - invoke: (channel: string, ...args: unknown) => Promise; - on: (channel: string, func: () => void) => void; - removeAllListeners: (channel: string) => void; -}; +import type ContextBridgeApi from '@desktop/contextBridge'; declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions