├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── fixtures └── Icon.png ├── license ├── media └── screenshot-about-window-linux.png ├── package.json ├── readme.md ├── source ├── main │ ├── about-window.ts │ ├── active-window.ts │ ├── app-menu.ts │ ├── dark-mode.ts │ ├── debug-info.ts │ ├── dev.ts │ ├── enforce-macos-app-location.ts │ ├── first-app-launch.ts │ ├── index.ts │ ├── security-policy.ts │ └── window.ts ├── node │ └── index.ts └── shared │ ├── app-launch-timestamp.ts │ ├── chrome.ts │ ├── disable-zoom.ts │ ├── github-issue.ts │ ├── index.ts │ ├── is.ts │ ├── platform.ts │ ├── run-js.ts │ ├── system-preferences.ts │ └── url-menu-item.ts ├── test-d ├── main.test-d.ts └── shared.test-d.ts ├── test ├── example.js ├── index.html ├── preload.cjs └── renderer.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: macos-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | - 16 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | distribution/ 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /fixtures/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/electron-util/6c37341e43cdaa890e9145d6065f14b864c8befc/fixtures/Icon.png -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /media/screenshot-about-window-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/electron-util/6c37341e43cdaa890e9145d6065f14b864c8befc/media/screenshot-about-window-linux.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-util", 3 | "version": "0.18.1", 4 | "description": "Useful utilities for Electron apps and modules", 5 | "license": "MIT", 6 | "repository": "sindresorhus/electron-util", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | ".": { 16 | "types": "./distribution/shared/index.d.ts", 17 | "default": "./distribution/shared/index.js" 18 | }, 19 | "./main": { 20 | "types": "./distribution/main/index.d.ts", 21 | "default": "./distribution/main/index.js" 22 | }, 23 | "./node": { 24 | "types": "./distribution/node/index.d.ts", 25 | "default": "./distribution/node/index.js" 26 | } 27 | }, 28 | "sideEffects": false, 29 | "engines": { 30 | "node": ">=18" 31 | }, 32 | "scripts": { 33 | "start": "npm run build && electron test/example.js", 34 | "build": "tsc", 35 | "prepare": "npm run build", 36 | "//test": "npm run build && xo && tsd", 37 | "test": "npm run build && xo" 38 | }, 39 | "files": [ 40 | "distribution" 41 | ], 42 | "keywords": [ 43 | "electron", 44 | "app", 45 | "dev", 46 | "development", 47 | "utility", 48 | "utilities", 49 | "util", 50 | "utils", 51 | "useful" 52 | ], 53 | "dependencies": { 54 | "electron-is-dev": "^3.0.1", 55 | "new-github-issue-url": "^1.0.0" 56 | }, 57 | "devDependencies": { 58 | "@sindresorhus/tsconfig": "^5.0.0", 59 | "electron": "^28.1.3", 60 | "tsd": "^0.30.3", 61 | "type-fest": "^4.9.0", 62 | "typescript": "^5.3.3", 63 | "xo": "^0.56.0" 64 | }, 65 | "xo": { 66 | "envs": [ 67 | "node", 68 | "browser" 69 | ], 70 | "ignores": [ 71 | "distribution" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # electron-util 2 | 3 | > Useful utilities for Electron apps and modules 4 | 5 | You can use this package directly in both the [main and renderer process](https://www.electronjs.org/docs/latest/tutorial/quick-start/#main-process). 6 | 7 | There are three parts of this package, “shared”, “main”, and “node”. The “shared” part works in both the main or [rendered process](https://www.electronjs.org/docs/latest/tutorial/process-model#the-renderer-process). The “main” part works only in the [main process](https://www.electronjs.org/docs/latest/tutorial/quick-start/#run-the-main-process). The “node” part is for Node.js-only APIs (not Electron). 8 | 9 | To use features from the “main” part in the renderer process, you will need to set up [IPC channels](https://www.electronjs.org/docs/latest/tutorial/ipc). 10 | 11 | ## Install 12 | 13 | ```sh 14 | npm install electron-util 15 | ``` 16 | 17 | > [!NOTE] 18 | > Requires Electron 28 or later. 19 | 20 | ## Usage 21 | 22 | The “shared” API you can access directly: 23 | 24 | ```ts 25 | import {is} from 'electron-util'; 26 | 27 | console.log(is.macos && is.main); 28 | //=> true 29 | ``` 30 | 31 | For the “main” API, use the `/main` sub-export: 32 | 33 | ```ts 34 | import {isDev} from 'electron-util/main'; 35 | 36 | console.log(isDev); 37 | //=> false 38 | ``` 39 | 40 | ## API 41 | 42 | ~~[Documentation](https://tsdocs.dev/docs/electron-util)~~ (The service is broken) 43 | 44 | Look at the [types](source) for now. 45 | 46 | ## Related 47 | 48 | - [electron-store](https://github.com/sindresorhus/electron-store) - Save and load data like user preferences, app state, cache, etc 49 | - [electron-debug](https://github.com/sindresorhus/electron-debug) - Adds useful debug features to your Electron app 50 | - [electron-context-menu](https://github.com/sindresorhus/electron-context-menu) - Context menu for your Electron app 51 | - [electron-dl](https://github.com/sindresorhus/electron-dl) - Simplified file downloads for your Electron app 52 | - [electron-unhandled](https://github.com/sindresorhus/electron-unhandled) - Catch unhandled errors and promise rejections in your Electron app 53 | -------------------------------------------------------------------------------- /source/main/about-window.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | dialog, 4 | type AboutPanelOptionsOptions, 5 | type MenuItemConstructorOptions, 6 | } from 'electron'; 7 | import {is} from '../shared/index.js'; 8 | 9 | export type ShowAboutWindowOptions = { 10 | /** 11 | An absolute path to the app icon. 12 | 13 | Only used on Linux and Windows. 14 | */ 15 | readonly icon?: string; 16 | 17 | /** 18 | The copyright text. 19 | */ 20 | readonly copyright?: string; 21 | 22 | /** 23 | The URL to the app's website. 24 | 25 | Only used on Linux. 26 | */ 27 | readonly website?: string; 28 | 29 | /** 30 | Some additional text if needed. 31 | 32 | Only used on Windows. 33 | */ 34 | readonly text?: string; 35 | 36 | /** 37 | Customizable for localization. Used in the menu item label and window title (Windows-only). 38 | 39 | The app name is automatically appended at runtime. 40 | 41 | Only used on Linux and Windows. 42 | 43 | @default 'About' 44 | */ 45 | readonly title?: string; 46 | }; 47 | 48 | export type AboutMenuItemOptions = ShowAboutWindowOptions; 49 | 50 | /** 51 | Shows an 'About' window. On macOS and Linux, the native 'About' window is shown, and on Windows, a simple custom dialog is shown. 52 | On macOS, the `icon`, `text`, `title`, and `website` options are ignored. 53 | 54 | _It will show `Electron` as the app name until you build your app for production._ 55 | 56 | @param options 57 | 58 | @example 59 | ``` 60 | import {showAboutWindow} from 'electron-util'; 61 | 62 | showAboutWindow({ 63 | icon: path.join(__dirname, 'static/Icon.png'), 64 | copyright: 'Copyright © Sindre Sorhus', 65 | text: 'Some more info.' 66 | }); 67 | ``` 68 | */ 69 | export const showAboutWindow = (options: ShowAboutWindowOptions) => { 70 | if (!is.windows) { 71 | if ( 72 | options.copyright 73 | ?? (is.linux && options.icon) 74 | ?? (is.linux && options.website) 75 | ) { 76 | const aboutPanelOptions: AboutPanelOptionsOptions = { 77 | copyright: options.copyright, 78 | }; 79 | 80 | if (is.linux && options.icon) { 81 | aboutPanelOptions.iconPath = options.icon; 82 | } 83 | 84 | app.setAboutPanelOptions(aboutPanelOptions); 85 | } 86 | 87 | app.showAboutPanel(); 88 | 89 | return; 90 | } 91 | 92 | options = { 93 | title: 'About', 94 | ...options, 95 | }; 96 | 97 | const text = options.text 98 | ? `${options.copyright ? '\n\n' : ''}${options.text}` 99 | : ''; 100 | 101 | void dialog.showMessageBox({ 102 | 103 | title: `${options.title} ${app.name}`, 104 | message: `Version ${app.getVersion()}`, 105 | detail: (options.copyright ?? '') + text, 106 | icon: options.icon, 107 | 108 | // This is needed for Linux, since at least Ubuntu does not show a close button 109 | buttons: ['OK'], 110 | }); 111 | }; 112 | 113 | /** 114 | Accepts the same options as `.showAboutWindow()`. 115 | 116 | @returns A `MenuItemConstructorOptions` that creates a menu item, which shows the about dialog when clicked. 117 | 118 | @example 119 | ``` 120 | import {Menu} from 'electron'; 121 | import {aboutMenuItem} from 'electron-util'; 122 | 123 | const menu = Menu.buildFromTemplate([ 124 | { 125 | label: 'Help', 126 | submenu: [ 127 | aboutMenuItem({ 128 | icon: path.join(__dirname, 'static/Icon.png'), 129 | copyright: 'Copyright © Sindre Sorhus', 130 | text: 'Some more info.' 131 | }) 132 | ] 133 | } 134 | ]); 135 | 136 | Menu.setApplicationMenu(menu); 137 | */ 138 | export const aboutMenuItem = ( 139 | options?: AboutMenuItemOptions, 140 | ): MenuItemConstructorOptions => { 141 | options = { 142 | title: 'About', 143 | ...options, 144 | }; 145 | 146 | // TODO: When https://github.com/electron/electron/issues/15589 is fixed, 147 | // handle the macOS case here, so the user doesn't need a conditional 148 | // when used in a cross-platform app 149 | 150 | return { 151 | 152 | label: `${options.title} ${app.name}`, 153 | click() { 154 | showAboutWindow(options ?? {}); 155 | }, 156 | }; 157 | }; 158 | -------------------------------------------------------------------------------- /source/main/active-window.ts: -------------------------------------------------------------------------------- 1 | import {BrowserWindow} from 'electron'; 2 | 3 | export const activeWindow = () => BrowserWindow.getFocusedWindow(); 4 | -------------------------------------------------------------------------------- /source/main/app-menu.ts: -------------------------------------------------------------------------------- 1 | import {app, type MenuItemConstructorOptions} from 'electron'; 2 | 3 | /** 4 | Creating the [app menu](https://developer.apple.com/design/human-interface-guidelines/macos/menus/menu-bar-menus/) (the first menu) on macOS requires [a lot of boilerplate](https://github.com/sindresorhus/caprine/blob/5361289d1058b9463946f274cbfef587e6ad24a3/menu.js#L381-L431). 5 | This method includes the default boilerplate and lets you add additional menu items in the correct place. 6 | 7 | @param menuItems - Menu items to add below the `About App Name` menu item. Usually, you would add at least a `Preferences…` menu item. 8 | @returns All menu items for the app menu. 9 | 10 | @example 11 | ``` 12 | import {Menu} from 'electron'; 13 | import {appMenu} from 'electron-util'; 14 | 15 | const menu = Menu.buildFromTemplate([ 16 | appMenu([ 17 | { 18 | label: 'Preferences…', 19 | accelerator: 'Command+,', 20 | click() {} 21 | } 22 | ]) 23 | ]); 24 | 25 | Menu.setApplicationMenu(menu); 26 | ``` 27 | */ 28 | export const appMenu = ( 29 | menuItems?: readonly MenuItemConstructorOptions[], 30 | ): MenuItemConstructorOptions => 31 | // TODO: When https://github.com/electron/electron/issues/15589 is fixed, 32 | // handle the macOS case here, so the user doesn't need a conditional 33 | // when used in a cross-platform app 34 | ({ 35 | label: app.name, 36 | submenu: [ 37 | { 38 | role: 'about', 39 | }, 40 | { 41 | type: 'separator', 42 | }, 43 | ...(menuItems ?? []), 44 | { 45 | type: 'separator', 46 | }, 47 | { 48 | role: 'services', 49 | }, 50 | { 51 | type: 'separator', 52 | }, 53 | { 54 | role: 'hide', 55 | }, 56 | { 57 | role: 'hideOthers', 58 | }, 59 | { 60 | role: 'unhide', 61 | }, 62 | { 63 | type: 'separator', 64 | }, 65 | { 66 | role: 'quit', 67 | }, 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /source/main/dark-mode.ts: -------------------------------------------------------------------------------- 1 | import {nativeTheme} from 'electron'; 2 | import {is} from '../shared/is.js'; 3 | 4 | export type DarkMode = { 5 | /** 6 | Whether the macOS dark mode is enabled. 7 | On Windows and Linux, it's `false`. 8 | */ 9 | readonly isEnabled: boolean; 10 | 11 | /** 12 | The `callback` function is called when the macOS dark mode is toggled. 13 | 14 | @returns A function, that when called, unsubscribes the listener. Calling it on Window and Linux works, but it just returns a no-op function. 15 | */ 16 | readonly onChange: (callback: () => void) => () => void; 17 | }; 18 | 19 | /** 20 | @example 21 | ``` 22 | import {darkMode} from 'electron-util'; 23 | 24 | console.log(darkMode.isEnabled); 25 | //=> false 26 | 27 | darkMode.onChange(() => { 28 | console.log(darkMode.isEnabled); 29 | //=> true 30 | }); 31 | ``` 32 | */ 33 | export const darkMode: DarkMode = { 34 | get isEnabled() { 35 | if (!is.macos) { 36 | return false; 37 | } 38 | 39 | return nativeTheme.shouldUseDarkColors; 40 | }, 41 | 42 | onChange(callback) { 43 | if (!is.macos) { 44 | // eslint-disable-next-line @typescript-eslint/no-empty-function 45 | return () => {}; 46 | } 47 | 48 | const handler = () => { 49 | callback(); 50 | }; 51 | 52 | nativeTheme.on('updated', handler); 53 | 54 | return () => { 55 | nativeTheme.off('updated', handler); 56 | }; 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /source/main/debug-info.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import os from 'node:os'; 3 | import {app} from 'electron'; 4 | import {electronVersion} from '../node/index.js'; 5 | 6 | /** 7 | For example, use this in the `body` option of the `.openNewGitHubIssue()` method. 8 | 9 | @returns A string with debug info suitable for inclusion in bug reports. 10 | 11 | @example 12 | ``` 13 | import {debugInfo} from 'electron-util'; 14 | 15 | console.log(debugInfo()); 16 | //=> 'AppName 2.21.0\nElectron 3.0.6\ndarwin 18.2.0\nLocale: en-US' 17 | ``` 18 | */ 19 | export const debugInfo = () => 20 | ` 21 | ${app.name} ${app.getVersion()} 22 | Electron ${electronVersion} 23 | ${process.platform} ${os.release()} 24 | Locale: ${app.getLocale()} 25 | `.trim(); 26 | -------------------------------------------------------------------------------- /source/main/dev.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Check if the app is running in development. 3 | */ 4 | export {default as isDev} from 'electron-is-dev'; 5 | -------------------------------------------------------------------------------- /source/main/enforce-macos-app-location.ts: -------------------------------------------------------------------------------- 1 | import {app, dialog} from 'electron'; 2 | import {is} from '../shared/index.js'; 3 | import {isDev} from './dev.js'; 4 | 5 | /** 6 | On macOS, for [security reasons](https://github.com/potionfactory/LetsMove/issues/56), if an app is launched outside the Applications folder, it will run in a read-only disk image, which could cause subtle problems for your app. 7 | Use this method to ensure the app lives in the Applications folder. 8 | 9 | It must not be used until the `app.whenReady()` promise is resolved. 10 | 11 | It will be a no-op during development and on other systems than macOS. 12 | 13 | It will offer to automatically move the app for the user. 14 | */ 15 | // eslint-disable-next-line @typescript-eslint/naming-convention 16 | export const enforceMacOSAppLocation = () => { 17 | if (isDev || !is.macos) { 18 | return; 19 | } 20 | 21 | if (app.isInApplicationsFolder()) { 22 | return; 23 | } 24 | 25 | const clickedButtonIndex = dialog.showMessageBoxSync({ 26 | type: 'error', 27 | message: 'Move to Applications folder?', 28 | detail: `${app.name} must live in the Applications folder to be able to run correctly.`, 29 | buttons: ['Move to Applications folder', `Quit ${app.name}`], 30 | defaultId: 0, 31 | cancelId: 1, 32 | }); 33 | 34 | if (clickedButtonIndex === 1) { 35 | app.quit(); 36 | return; 37 | } 38 | 39 | app.moveToApplicationsFolder({ 40 | conflictHandler(conflict) { 41 | if (conflict === 'existsAndRunning') { 42 | // Can't replace the active version of the app 43 | dialog.showMessageBoxSync({ 44 | type: 'error', 45 | message: `Another version of ${app.name} is currently running. Quit it, then launch this version of the app again.`, 46 | buttons: ['OK'], 47 | }); 48 | 49 | app.quit(); 50 | } 51 | 52 | return true; 53 | }, 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /source/main/first-app-launch.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import {app} from 'electron'; 4 | 5 | function isError(error: any): error is NodeJS.ErrnoException { 6 | return error instanceof Error; 7 | } 8 | 9 | /** 10 | It works by writing a file to `app.getPath('userData')` if it doesn't exist and checks that. 11 | That means it will return true the first time you add this check to your app. 12 | 13 | @returns A `boolean` of whether it's the first time your app is launched. 14 | */ 15 | export const isFirstAppLaunch = (): boolean => { 16 | const checkFile = path.join( 17 | app.getPath('userData'), 18 | '.electron-util--has-app-launched', 19 | ); 20 | 21 | if (fs.existsSync(checkFile)) { 22 | return false; 23 | } 24 | 25 | try { 26 | fs.writeFileSync(checkFile, ''); 27 | } catch (error) { 28 | if (isError(error)) { 29 | if (error.code === 'ENOENT') { 30 | fs.mkdirSync(app.getPath('userData')); 31 | return isFirstAppLaunch(); 32 | } 33 | } else { 34 | throw error; 35 | } 36 | } 37 | 38 | return true; 39 | }; 40 | -------------------------------------------------------------------------------- /source/main/index.ts: -------------------------------------------------------------------------------- 1 | export {menuBarHeight} from './window.js'; 2 | 3 | export { 4 | type AboutMenuItemOptions, 5 | type ShowAboutWindowOptions, 6 | aboutMenuItem, 7 | showAboutWindow, 8 | } from './about-window.js'; 9 | 10 | export {activeWindow} from './active-window.js'; 11 | export {appMenu} from './app-menu.js'; 12 | export {darkMode, type DarkMode} from './dark-mode.js'; 13 | 14 | export {debugInfo} from './debug-info.js'; 15 | export {enforceMacOSAppLocation} from './enforce-macos-app-location.js'; 16 | export {isFirstAppLaunch} from './first-app-launch.js'; 17 | export { 18 | electronVersion, 19 | isElectron, 20 | isUsingAsar, 21 | fixPathForAsarUnpack, 22 | } from '../node/index.js'; 23 | export {isDev} from './dev.js'; 24 | 25 | export {setContentSecurityPolicy} from './security-policy.js'; 26 | export { 27 | type CenterWindowOptions, 28 | type GetWindowBoundsCenteredOptions, 29 | centerWindow, 30 | getWindowBoundsCentered, 31 | } from './window.js'; 32 | -------------------------------------------------------------------------------- /source/main/security-policy.ts: -------------------------------------------------------------------------------- 1 | import {type Session, app, session} from 'electron'; 2 | 3 | export type SetContentSecurityPolicyOptions = { 4 | /** 5 | The session to apply the policy to. 6 | 7 | Default: [`electron.session.defaultSession`](https://electronjs.org/docs/api/session#sessiondefaultsession) 8 | */ 9 | readonly session?: Session; 10 | }; 11 | 12 | /** 13 | Set a [Content Security Policy](https://developers.google.com/web/fundamentals/security/csp/) for your app. 14 | Don't forget to [validate the policy](https://csp-evaluator.withgoogle.com) after changes. 15 | 16 | @param policy - You can put rules on separate lines, but lines must end in a semicolon. 17 | 18 | @example 19 | ``` 20 | import setContentSecurityPolicy from 'electron-util'; 21 | 22 | setContentSecurityPolicy(` 23 | default-src 'none'; 24 | script-src 'self'; 25 | img-src 'self' data:; 26 | style-src 'self'; 27 | font-src 'self'; 28 | connect-src 'self' https://api.example.com; 29 | base-uri 'none'; 30 | form-action 'none'; 31 | frame-ancestors 'none'; 32 | `); 33 | */ 34 | export const setContentSecurityPolicy = async ( 35 | policy: string, 36 | options?: SetContentSecurityPolicyOptions, 37 | ) => { 38 | await app.whenReady(); 39 | 40 | if ( 41 | !policy 42 | .split('\n') 43 | .filter(line => line.trim()) 44 | .every(line => line.endsWith(';')) 45 | ) { 46 | throw new Error('Each line must end in a semicolon'); 47 | } 48 | 49 | policy = policy.replaceAll(/[\t\n]/g, '').trim(); 50 | 51 | options = { 52 | session: session.defaultSession, 53 | ...options, 54 | }; 55 | 56 | options.session!.webRequest.onHeadersReceived((details, callback) => { 57 | callback({ 58 | responseHeaders: { 59 | ...details.responseHeaders, 60 | 'Content-Security-Policy': [policy], 61 | }, 62 | }); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /source/main/window.ts: -------------------------------------------------------------------------------- 1 | import {type BrowserWindow, type Rectangle, type Size, screen} from 'electron'; 2 | import {is} from '../shared/index.js'; 3 | import {activeWindow} from './active-window.js'; 4 | 5 | export type GetWindowBoundsCenteredOptions = { 6 | /** 7 | The window to get the bounds of. 8 | 9 | Default: Current window 10 | */ 11 | readonly window?: BrowserWindow; 12 | 13 | /** 14 | Set a new window size. 15 | 16 | Default: Size of `window` 17 | 18 | @example 19 | ``` 20 | {width: 600, height: 400} 21 | ``` 22 | */ 23 | readonly size?: Size; 24 | 25 | /** 26 | Use the full display size when calculating the position. 27 | By default, only the workable screen area is used, which excludes the Windows taskbar and macOS dock. 28 | 29 | @default false 30 | */ 31 | readonly useFullBounds?: boolean; 32 | }; 33 | 34 | export type CenterWindowOptions = { 35 | /** 36 | The window to center. 37 | 38 | Default: Current window 39 | */ 40 | readonly window?: BrowserWindow; 41 | 42 | /** 43 | Set a new window size. 44 | 45 | Default: Size of `window` 46 | 47 | @example 48 | ``` 49 | {width: 600, height: 400} 50 | ``` 51 | */ 52 | readonly size?: Size; 53 | 54 | /** 55 | Animate the change. 56 | 57 | @default false 58 | */ 59 | readonly animated?: boolean; 60 | 61 | /** 62 | Use the full display size when calculating the position. 63 | By default, only the workable screen area is used, which excludes the Windows taskbar and macOS dock. 64 | 65 | @default false 66 | */ 67 | readonly useFullBounds?: boolean; 68 | }; 69 | 70 | /** 71 | @returns The height of the menu bar on macOS, or `0` if not macOS. 72 | */ 73 | export const menuBarHeight = () => 74 | is.macos ? screen.getPrimaryDisplay().workArea.y : 0; 75 | 76 | /** 77 | Get the [bounds](https://electronjs.org/docs/api/browser-window#wingetbounds) of a window as if it was centered on the screen. 78 | 79 | @returns Bounds of a window. 80 | */ 81 | export const getWindowBoundsCentered = ( 82 | options?: GetWindowBoundsCenteredOptions, 83 | ): Rectangle => { 84 | const window = options?.window ?? activeWindow(); 85 | if (!window) { 86 | throw new Error('No active window'); 87 | } 88 | 89 | const [width, height] = window.getSize(); 90 | // TODO: Why are width and height undefined? 91 | // This is just a workaround 92 | const windowSize = (options?.size ?? {width, height}) as Size; 93 | const screenSize = screen.getDisplayNearestPoint( 94 | screen.getCursorScreenPoint(), 95 | ).workArea; 96 | const x = Math.floor( 97 | (screenSize.x + (screenSize.width / 2) - ((windowSize.width ?? 0) / 2)), 98 | ); 99 | const y = Math.floor( 100 | ((screenSize.height + screenSize.y) / 2) - ((windowSize.height ?? 0) / 2), 101 | ); 102 | 103 | return { 104 | x, 105 | y, 106 | ...windowSize, 107 | }; 108 | }; 109 | 110 | /** 111 | Center a window on the screen. 112 | */ 113 | export const centerWindow = (options?: CenterWindowOptions) => { 114 | const window = options?.window ?? activeWindow(); 115 | if (!window) { 116 | throw new Error('No active window'); 117 | } 118 | 119 | options = { 120 | window, 121 | animated: false, 122 | ...options, 123 | }; 124 | 125 | const bounds = getWindowBoundsCentered(options); 126 | window.setBounds(bounds, options.animated); 127 | }; 128 | -------------------------------------------------------------------------------- /source/node/index.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | 3 | export const isElectron = 'electron' in process.versions; 4 | 5 | /** 6 | Check the app is using [ASAR](https://electronjs.org/docs/tutorial/application-packaging/). 7 | */ 8 | export const isUsingAsar = isElectron 9 | && process.argv.length > 1 10 | && process.argv[1]?.includes('app.asar'); 11 | 12 | /** 13 | Electron version. 14 | 15 | @example 16 | ``` 17 | '1.7.9' 18 | ``` 19 | */ 20 | export const electronVersion = process.versions.electron ?? '0.0.0'; 21 | 22 | /** 23 | ASAR is great, but it has [limitations when it comes to executing binaries](https://electronjs.org/docs/tutorial/application-packaging/#executing-binaries-inside-asar-archive). 24 | For example, [`child_process.spawn()` is not automatically handled](https://github.com/electron/electron/issues/9459). 25 | So you would have to unpack the binary, for example, with the [`asarUnpack`](https://www.electron.build/configuration/configuration#configuration-asarUnpack) option in `electron-builder`. 26 | This creates a problem as the path to the binary changes, but your `path.join(__dirname, 'binary')` is not changed. 27 | To make it work you need to fix the path. That's the purpose of this method. 28 | 29 | Before: 30 | /Users/sindresorhus/Kap.app/Contents/Resources/app.asar/node_modules/foo/binary 31 | 32 | After: 33 | /Users/sindresorhus/Kap.app/Contents/Resources/app.asar.unpacked/node_modules/foo/binary 34 | 35 | @param path - A path in your app. 36 | @returns The fixed path. 37 | */ 38 | export const fixPathForAsarUnpack = (path: string): string => 39 | isUsingAsar ? path.replace('app.asar', 'app.asar.unpacked') : path; 40 | -------------------------------------------------------------------------------- /source/shared/app-launch-timestamp.ts: -------------------------------------------------------------------------------- 1 | export const appLaunchTimestamp = Date.now(); 2 | -------------------------------------------------------------------------------- /source/shared/chrome.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | 3 | export const chromeVersion = process.versions.chrome.replace(/\.\d+$/, ''); 4 | -------------------------------------------------------------------------------- /source/shared/disable-zoom.ts: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import {activeWindow} from '../main/active-window.js'; 3 | import {is} from './is.js'; 4 | 5 | /** 6 | Disable zooming, usually caused by pinching the trackpad on macOS or Ctrl+ on Windows. 7 | 8 | @param window - Default: webContents from current window. 9 | */ 10 | export const disableZoom = ( 11 | web = is.main ? activeWindow()?.webContents : electron.webFrame, 12 | ) => { 13 | if (!web) { 14 | throw new Error('No active window'); 15 | } 16 | 17 | const run = () => { 18 | web.setZoomFactor(1); 19 | }; 20 | 21 | // TODO: Look into the `any` here. 22 | (web as any).on('did-finish-load', run); // eslint-disable-line @typescript-eslint/no-unsafe-call 23 | run(); 24 | }; 25 | -------------------------------------------------------------------------------- /source/shared/github-issue.ts: -------------------------------------------------------------------------------- 1 | import {shell} from 'electron'; 2 | import newGithubIssueUrl, { 3 | type Options as OpenNewGitHubIssueOptions, 4 | } from 'new-github-issue-url'; 5 | 6 | /** 7 | Opens the new issue view on the given GitHub repo in the browser. 8 | Optionally, with some fields like title and body prefilled. 9 | 10 | @param options - The options are passed to the [`new-github-issue-url`](https://github.com/sindresorhus/new-github-issue-url#options) package. 11 | 12 | @example 13 | ``` 14 | import {openNewGitHubIssue} from 'electron-util'; 15 | 16 | openNewGitHubIssue({ 17 | user: 'sindresorhus', 18 | repo: 'playground', 19 | body: 'Hello' 20 | }); 21 | */ 22 | export const openNewGitHubIssue = (options: OpenNewGitHubIssueOptions) => { 23 | const url = newGithubIssueUrl(options); 24 | void shell.openExternal(url); 25 | }; 26 | -------------------------------------------------------------------------------- /source/shared/index.ts: -------------------------------------------------------------------------------- 1 | export {appLaunchTimestamp} from './app-launch-timestamp.js'; 2 | export {chromeVersion} from './chrome.js'; 3 | export {disableZoom} from './disable-zoom.js'; 4 | export {openNewGitHubIssue} from './github-issue.js'; 5 | export {is} from './is.js'; 6 | export {type Choices, platform} from './platform.js'; 7 | 8 | export {runJS} from './run-js.js'; 9 | export { 10 | type SystemPreferencesMacOsPanes, 11 | type SystemPreferencesWindowsPanes, 12 | openSystemPreferences, 13 | } from './system-preferences.js'; 14 | 15 | export {type OpenUrlMenuItemOptions, openUrlMenuItem} from './url-menu-item.js'; 16 | -------------------------------------------------------------------------------- /source/shared/is.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | 3 | /* 4 | Check for various things. 5 | */ 6 | type Is = { 7 | /** 8 | Running on macOS. 9 | */ 10 | readonly macos: boolean; 11 | 12 | /** 13 | Running on Linux. 14 | */ 15 | readonly linux: boolean; 16 | 17 | /** 18 | Running on Windows. 19 | */ 20 | readonly windows: boolean; 21 | 22 | /** 23 | Running on the [main process](https://electronjs.org/docs/tutorial/quick-start/#main-process). 24 | */ 25 | readonly main: boolean; 26 | 27 | /** 28 | Running on the [renderer process](https://electronjs.org/docs/tutorial/quick-start/#renderer-process). 29 | */ 30 | readonly renderer: boolean; 31 | 32 | /** 33 | The app is an Mac App Store build. 34 | */ 35 | readonly macAppStore: boolean; 36 | 37 | /** 38 | The app is a Windows Store AppX build. 39 | */ 40 | readonly windowsStore: boolean; 41 | }; 42 | 43 | export const is: Is = { 44 | macos: process.platform === 'darwin', 45 | linux: process.platform === 'linux', 46 | windows: process.platform === 'win32', 47 | main: process.type === 'browser', 48 | renderer: process.type === 'renderer', 49 | macAppStore: process.mas, 50 | windowsStore: process.windowsStore, 51 | }; 52 | -------------------------------------------------------------------------------- /source/shared/platform.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {type RequireAtLeastOne, type ValueOf} from 'type-fest'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/naming-convention 5 | type _Choices = { 6 | readonly macos?: Macos | (() => Macos); 7 | readonly windows?: Windows | (() => Windows); 8 | readonly linux?: Linux | (() => Linux); 9 | readonly default?: Default | (() => Default); 10 | }; 11 | 12 | export type Choices = RequireAtLeastOne< 13 | _Choices, 14 | 'macos' | 'windows' | 'linux' 15 | >; 16 | 17 | // eslint-disable-next-line @typescript-eslint/naming-convention 18 | type _Platform = 'macos' | 'windows' | 'linux' | 'default'; 19 | 20 | /** 21 | Accepts an object with the keys as either `macos`, `windows`, `linux`, or `default`, and picks the appropriate key depending on the current platform. 22 | If no platform key is matched, the `default` key is used if it exists. 23 | If the value is a function, it will be executed, and the returned value will be used. 24 | 25 | @example 26 | ``` 27 | init({ 28 | enableUnicorn: util.platform({ 29 | macos: true, 30 | windows: false, 31 | linux: () => false 32 | }) 33 | }); 34 | ``` 35 | */ 36 | export const platform = < 37 | Macos = never, 38 | Windows = never, 39 | Linux = never, 40 | Default = undefined, 41 | >( 42 | choices: Choices, 43 | ) => { 44 | const {platform: _platform} = process; 45 | 46 | let platform: _Platform; 47 | 48 | switch (_platform) { 49 | case 'darwin': { 50 | platform = 'macos'; 51 | break; 52 | } 53 | 54 | case 'win32': { 55 | platform = 'windows'; 56 | break; 57 | } 58 | 59 | case 'linux': { 60 | platform = 'linux'; 61 | break; 62 | } 63 | 64 | default: { 65 | platform = 'default'; 66 | } 67 | } 68 | 69 | // TODO: This can't return undefined, but TypeScript doesn't know that 70 | const fn: ValueOf> 71 | = platform in choices ? choices[platform] : choices.default; 72 | 73 | return fn instanceof Function ? fn() : fn; 74 | }; 75 | -------------------------------------------------------------------------------- /source/shared/run-js.ts: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import {activeWindow} from '../main/active-window.js'; 3 | import {is} from './is.js'; 4 | 5 | /** 6 | 122,130d33 7 | Run some JavaScript in the active or given window. 8 | 9 | @param code - JavaScript code to be executed. 10 | @param web - Default: webContents from current window. 11 | @returns A promise for the result of the executed code or a rejected promise if the result is a rejected promise. 12 | */ 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | export const runJS = async ( 15 | code: string, 16 | web = is.main ? activeWindow()?.webContents : electron.webFrame, 17 | ): Promise => { 18 | if (!web) { 19 | throw new Error('No active window'); 20 | } 21 | 22 | await web.executeJavaScript(code); 23 | }; 24 | -------------------------------------------------------------------------------- /source/shared/system-preferences.ts: -------------------------------------------------------------------------------- 1 | import {shell} from 'electron'; 2 | import {is} from './is.js'; 3 | 4 | export type SystemPreferencesMacOsPanes = { 5 | universalaccess: 6 | | 'Captioning' 7 | | 'Hearing' 8 | | 'Keyboard' 9 | | 'Media_Descriptions' 10 | | 'Mouse' 11 | | 'Seeing_Display' 12 | | 'Seeing_VoiceOver' 13 | | 'Seeing_Zoom' 14 | | 'SpeakableItems' 15 | | 'Switch'; 16 | security: 17 | | 'Advanced' 18 | | 'FDE' 19 | | 'Firewall' 20 | | 'General' 21 | | 'Privacy' 22 | | 'Privacy_Accessibility' 23 | | 'Privacy_Advertising' 24 | /** 25 | Full Disk Access. 26 | */ 27 | | 'Privacy_AllFiles' 28 | | 'Privacy_Assistive' 29 | | 'Privacy_Automation' 30 | | 'Privacy_Calendars' 31 | | 'Privacy_Camera' 32 | | 'Privacy_Contacts' 33 | | 'Privacy_DesktopFolder' 34 | | 'Privacy_Diagnostics' 35 | | 'Privacy_DocumentsFolder' 36 | | 'Privacy_DownloadsFolder' 37 | | 'Privacy_LocationServices' 38 | | 'Privacy_Microphone' 39 | | 'Privacy_Photos' 40 | | 'Privacy_Reminders' 41 | | 'Privacy_ScreenCapture'; 42 | speech: 'Dictation' | 'TTS'; 43 | sharing: 44 | | 'Internet' 45 | | 'Services_ARDService' 46 | | 'Services_BluetoothSharing' 47 | | 'Services_PersonalFileSharing' 48 | | 'Services_PrinterSharing' 49 | | 'Services_RemoteAppleEvent' 50 | | 'Services_RemoteLogin' 51 | | 'Services_ScreenSharing'; 52 | }; 53 | 54 | export type SystemPreferencesWindowsPanes = 55 | /** 56 | System 57 | */ 58 | | 'display' 59 | | 'sound' // Build 17063+ 60 | | 'notifications' 61 | | 'quiethours' // Build 17074+ 62 | | 'powersleep' 63 | | 'batterysaver' 64 | | 'storagesense' 65 | | 'tabletmode' 66 | | 'multitasking' 67 | | 'project' 68 | | 'crossdevice' 69 | | 'clipboard' // Build 17666+ 70 | | 'remotedesktop' 71 | | 'about' 72 | /** 73 | Devices 74 | */ 75 | | 'bluetooth' 76 | | 'connecteddevices' 77 | | 'printers' 78 | | 'mousetouchpad' 79 | | 'devices-touchpad' 80 | | 'typing' 81 | | 'wheel' 82 | | 'pen' 83 | | 'autoplay' 84 | | 'usb' 85 | | 'mobile-devices' // Build 16251+ 86 | /** 87 | Network & Internet 88 | */ 89 | | 'network' 90 | | 'network-status' 91 | | 'network-cellular' 92 | | 'network-wifi' 93 | | 'network-wificalling' 94 | | 'network-ethernet' 95 | | 'network-dialup' 96 | | 'network-vpn' 97 | | 'network-airplanemode' 98 | | 'network-mobilehotspot' 99 | | 'nfctransactions' 100 | | 'datausage' 101 | | 'network-proxy' 102 | /** 103 | Personalization 104 | */ 105 | | 'personalization' 106 | | 'personalization-background' 107 | | 'personalization-colors' 108 | | 'lockscreen' 109 | | 'themes' 110 | | 'fonts' // Build 17083+ 111 | | 'personalization-start' 112 | | 'taskbar' 113 | /** 114 | Apps 115 | */ 116 | | 'appsfeatures' 117 | | 'optionalfeatures' 118 | | 'defaultapps' 119 | | 'maps' 120 | | 'appsforwebsites' 121 | | 'videoplayback' // Build 16215+ 122 | | 'startupapps' // Build 17017+ 123 | /** 124 | Accounts 125 | */ 126 | | 'yourinfo' 127 | | 'emailandaccounts' 128 | | 'signinoptions' 129 | | 'workplace' 130 | | 'otherusers' 131 | | 'sync' 132 | /** 133 | Time & language 134 | */ 135 | | 'dateandtime' 136 | | 'regionformatting' 137 | | 'regionlanguage' 138 | | 'speech' 139 | /** 140 | Gaming 141 | */ 142 | | 'gaming-gamebar' 143 | | 'gaming-gamedvr' 144 | | 'gaming-broadcasting' 145 | | 'gaming-gamemode' 146 | | 'gaming-xboxnetworking' // Build 16226+ 147 | /** 148 | Ease of Access 149 | */ 150 | | 'easeofaccess-display' // Build 17025+ 151 | | 'easeofaccess-cursorandpointersize' // Build 17040+ 152 | | 'easeofaccess-cursor' 153 | | 'easeofaccess-magnifier' 154 | | 'easeofaccess-colorfilter' // Build 17025+ 155 | | 'easeofaccess-highcontrast' 156 | | 'easeofaccess-narrator' 157 | | 'easeofaccess-audio' // Build 17035+ 158 | | 'easeofaccess-closedcaptioning' 159 | | 'easeofaccess-speechrecognition' // Build 17035+ 160 | | 'easeofaccess-keyboard' 161 | | 'easeofaccess-mouse' 162 | | 'easeofaccess-eyecontrol' // Build 17035+ 163 | /** 164 | Search & Cortana 165 | */ 166 | | 'search-permissions' // Version 1903+ 167 | | 'cortana-windowssearch' // Version 1903+ 168 | | 'cortana' // Build 16188+ 169 | | 'cortana-talktocortana' // Build 16188+ 170 | | 'cortana-permissions' // Build 16188+ 171 | /** 172 | Privacy 173 | */ 174 | | 'privacy' 175 | | 'privacy-speech' 176 | | 'privacy-speechtyping' 177 | | 'privacy-feedback' 178 | | 'privacy-activityhistory' // Build 17040+ 179 | | 'privacy-location' 180 | | 'privacy-webcam' 181 | | 'privacy-microphone' 182 | | 'privacy-voiceactivation' 183 | | 'privacy-notifications' 184 | | 'privacy-accountinfo' 185 | | 'privacy-contacts' 186 | | 'privacy-calendar' 187 | | 'privacy-phonecalls' 188 | | 'privacy-callhistory' 189 | | 'privacy-email' 190 | | 'privacy-eyetracker' 191 | | 'privacy-tasks' 192 | | 'privacy-messaging' 193 | | 'privacy-radios' 194 | | 'privacy-customdevices' 195 | | 'privacy-backgroundapps' 196 | | 'privacy-appdiagnostics' 197 | | 'privacy-automaticfiledownloads' 198 | | 'privacy-documents' 199 | | 'privacy-pictures' 200 | | 'privacy-videos' 201 | | 'privacy-broadfilesystemaccess' 202 | /** 203 | Update & security 204 | */ 205 | | 'windowsupdate' 206 | | 'delivery-optimization' 207 | | 'windowsdefender' 208 | | 'backup' 209 | | 'troubleshoot' 210 | | 'recovery' 211 | | 'activation' 212 | | 'findmydevice' 213 | | 'developers' 214 | | 'windowsinsider'; 215 | 216 | /** 217 | Open the System Preferences on macOS. 218 | 219 | This method does nothing on other systems. 220 | 221 | Optionally provide a pane and section. 222 | 223 | @example 224 | ``` 225 | import {openSystemPreferences} from 'electron-util'; 226 | 227 | openSystemPreferences(); 228 | 229 | // or 230 | 231 | openSystemPreferences('security', 'Firewall'); 232 | ``` 233 | 234 | @param pane - The pane to open. 235 | @param section - The section within that pane. 236 | @returns A Promise that resolves when the preferences window is opened. 237 | */ 238 | export const openSystemPreferences = async < 239 | T extends keyof SystemPreferencesMacOsPanes | SystemPreferencesWindowsPanes, 240 | >( 241 | ...args: T extends keyof SystemPreferencesMacOsPanes 242 | ? [T, SystemPreferencesMacOsPanes[T]] 243 | : [T] 244 | ) => { 245 | const [pane, section] = args; 246 | 247 | if (is.macos) { 248 | await shell.openExternal( 249 | `x-apple.systempreferences:com.apple.preference.${pane}${ 250 | section ? `?${section}` : '' 251 | }`, 252 | ); 253 | } else if (is.windows) { 254 | await shell.openExternal(`ms-settings:${pane}`); 255 | } 256 | }; 257 | -------------------------------------------------------------------------------- /source/shared/url-menu-item.ts: -------------------------------------------------------------------------------- 1 | import {type MenuItemConstructorOptions, shell} from 'electron'; 2 | 3 | export type OpenUrlMenuItemOptions = { 4 | /** 5 | URL to be opened when the menu item is clicked. 6 | */ 7 | readonly url: string; 8 | } & Readonly; 9 | 10 | /** 11 | Accepts the same options as [`new MenuItem()`](https://electronjs.org/docs/api/menu-item) in addition to a `url` option. 12 | If you specify the `click` option, its handler will be called before the URL is opened. 13 | 14 | @returns A `MenuItemConstructorOptions` that creates a menu item, which opens the given URL in the browser when clicked. 15 | 16 | @example 17 | ``` 18 | import {Menu} from 'electron'; 19 | import {openUrlMenuItem} from 'electron-util'; 20 | 21 | const menu = Menu.buildFromTemplate([ 22 | { 23 | label: 'Help', 24 | submenu: [ 25 | openUrlMenuItem({ 26 | label: 'Website', 27 | url: 'https://sindresorhus.com' 28 | }) 29 | ] 30 | } 31 | ]); 32 | 33 | Menu.setApplicationMenu(menu); 34 | */ 35 | export const openUrlMenuItem = ( 36 | options: Readonly, 37 | ): MenuItemConstructorOptions => { 38 | const {url, ...optionsWithoutUrl} = options; 39 | 40 | const click: MenuItemConstructorOptions['click'] = (...arguments_) => { 41 | if (optionsWithoutUrl.click) { 42 | optionsWithoutUrl.click(...arguments_); 43 | } 44 | 45 | void shell.openExternal(url); 46 | }; 47 | 48 | return { 49 | ...optionsWithoutUrl, 50 | click, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /test-d/main.test-d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type BrowserWindow, 3 | type Rectangle, 4 | type MenuItemConstructorOptions, 5 | } from 'electron'; 6 | import {expectType} from 'tsd'; 7 | import { 8 | electronVersion, 9 | activeWindow, 10 | fixPathForAsarUnpack, 11 | enforceMacOSAppLocation, 12 | menuBarHeight, 13 | getWindowBoundsCentered, 14 | centerWindow, 15 | isFirstAppLaunch, 16 | darkMode, 17 | setContentSecurityPolicy, 18 | showAboutWindow, 19 | aboutMenuItem, 20 | debugInfo, 21 | appMenu, 22 | } from '../source/main/index.js'; 23 | 24 | /* Idk 25 | expectType(api); 26 | expectType(api.app.isPackaged); 27 | */ 28 | expectType(electronVersion); 29 | 30 | // eslint-disable-next-line @typescript-eslint/ban-types 31 | expectType(activeWindow()); 32 | expectType(fixPathForAsarUnpack('/path')); 33 | 34 | expectType(enforceMacOSAppLocation()); 35 | expectType(menuBarHeight()); 36 | expectType(getWindowBoundsCentered()); 37 | expectType(getWindowBoundsCentered({useFullBounds: true})); 38 | 39 | expectType(centerWindow({})); 40 | expectType(isFirstAppLaunch()); 41 | expectType(darkMode.isEnabled); 42 | 43 | expectType>(setContentSecurityPolicy('default-src \'none\';')); 44 | 45 | expectType(showAboutWindow({title: 'App name'})); 46 | expectType(aboutMenuItem()); 47 | expectType(debugInfo()); 48 | expectType( 49 | appMenu([ 50 | { 51 | label: 'Preferences…', 52 | accelerator: 'Command+,', 53 | }, 54 | ]), 55 | ); 56 | -------------------------------------------------------------------------------- /test-d/shared.test-d.ts: -------------------------------------------------------------------------------- 1 | import {type MenuItemConstructorOptions} from 'electron'; 2 | import {expectType, expectError} from 'tsd'; 3 | import { 4 | is, 5 | chromeVersion, 6 | platform, 7 | runJS, 8 | disableZoom, 9 | appLaunchTimestamp, 10 | openNewGitHubIssue, 11 | openUrlMenuItem, 12 | openSystemPreferences, 13 | } from '../source/shared/index.js'; 14 | 15 | expectType(is.macos); 16 | expectType(chromeVersion); 17 | 18 | expectType( 19 | platform({ 20 | macos: 1, 21 | default: () => 'test', 22 | }), 23 | ); 24 | 25 | expectError(platform({})); 26 | expectError(platform({default: 1})); 27 | 28 | expectType>(runJS('a=1')); 29 | 30 | expectType(disableZoom()); 31 | expectType(appLaunchTimestamp); 32 | 33 | expectType( 34 | openNewGitHubIssue({user: 'sindresorhus', repo: 'electron-util'}), 35 | ); 36 | expectType( 37 | openUrlMenuItem({url: 'https://sindresorhus.com'}), 38 | ); 39 | 40 | expectError(openSystemPreferences()); 41 | expectType>( 42 | openSystemPreferences('security', 'Privacy_Microphone'), 43 | ); 44 | expectError(openSystemPreferences('security', 'Bad_Section')); 45 | expectType>(openSystemPreferences('windowsupdate')); 46 | expectError(openSystemPreferences('windowsupdate', 'Non_Existent_Section')); 47 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import assert from 'node:assert'; 4 | import {app, BrowserWindow, Menu, dialog} from 'electron'; 5 | import { 6 | openNewGitHubIssue, 7 | openUrlMenuItem, 8 | openSystemPreferences, 9 | runJS, 10 | platform, 11 | } from '../distribution/shared/index.js'; 12 | import { 13 | showAboutWindow, 14 | aboutMenuItem, 15 | debugInfo, 16 | appMenu, 17 | } from '../distribution/main/index.js'; 18 | 19 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 20 | 21 | const createMenu = () => { 22 | const items = [ 23 | { 24 | label: 'openNewGitHubIssue() test', 25 | click() { 26 | openNewGitHubIssue({ 27 | user: 'sindresorhus', 28 | repo: 'playground', 29 | body: 'Test 🦄', 30 | }); 31 | }, 32 | }, 33 | { 34 | label: 'openSystemPreferences() test', 35 | click() { 36 | openSystemPreferences(); 37 | }, 38 | }, 39 | openUrlMenuItem({ 40 | label: 'openUrlMenuItem() test', 41 | url: 'https://sindresorhus.com', 42 | click() { 43 | console.log('Executed before opening the URL'); 44 | }, 45 | }), 46 | { 47 | label: 'showAboutWindow() test', 48 | click() { 49 | showAboutWindow({ 50 | icon: path.join(__dirname, 'fixtures/Icon.png'), 51 | copyright: 'Copyright © Sindre Sorhus', 52 | text: 'Some more info.', 53 | }); 54 | }, 55 | }, 56 | aboutMenuItem({ 57 | icon: path.join(__dirname, 'fixtures/Icon.png'), 58 | copyright: 'Copyright © Sindre Sorhus', 59 | text: 'Some more info.', 60 | }), 61 | { 62 | label: 'debugInfo() test', 63 | click() { 64 | dialog.showErrorBox('', debugInfo()); 65 | }, 66 | }, 67 | ]; 68 | 69 | const appMenuItems = appMenu([ 70 | { 71 | label: 'Extra item', 72 | enabled: false, 73 | }, 74 | ]); 75 | 76 | const menu = Menu.buildFromTemplate([ 77 | appMenuItems, 78 | { 79 | label: 'Test', 80 | submenu: items, 81 | }, 82 | ]); 83 | 84 | Menu.setApplicationMenu(menu); 85 | }; 86 | 87 | // eslint-disable-next-line unicorn/prefer-top-level-await -- whenReady is broken in top-level await. 88 | (async () => { 89 | await app.whenReady(); 90 | 91 | createMenu(); 92 | 93 | const mainWindow = new BrowserWindow({ 94 | webPreferences: { 95 | preload: path.join(__dirname, 'preload.cjs'), 96 | }, 97 | }); 98 | await mainWindow.loadFile('index.html'); 99 | 100 | mainWindow.webContents.openDevTools('undocked'); 101 | 102 | assert.strictEqual(await runJS('2 + 2'), 4); 103 | 104 | const platformTestCases = [ 105 | [ 106 | 'basic', 107 | { 108 | linux: 1, 109 | macos: 2, 110 | default: 3, 111 | }, 112 | 2, 113 | ], 114 | [ 115 | 'function', 116 | { 117 | linux: 1, 118 | macos: () => 2, 119 | default: 3, 120 | }, 121 | 2, 122 | ], 123 | [ 124 | 'default', 125 | { 126 | linux: 1, 127 | default: 3, 128 | }, 129 | 3, 130 | ], 131 | [ 132 | 'undefined', 133 | { 134 | linux: 1, 135 | }, 136 | undefined, 137 | ], 138 | ]; 139 | 140 | setTimeout(() => { 141 | for (const [name, input, expected] of platformTestCases) { 142 | const actual = platform(input); 143 | 144 | if (actual === expected) { 145 | mainWindow.webContents.send( 146 | 'test-platform', 147 | name, 148 | actual, 149 | expected, 150 | true, 151 | ); 152 | } else { 153 | mainWindow.webContents.send( 154 | 'test-platform', 155 | name, 156 | actual, 157 | expected, 158 | false, 159 | ); 160 | } 161 | } 162 | }, 1000); 163 | })(); 164 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example 7 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | platform({
25 | 	linux: 1,
26 | 	macos: 2,
27 | 	default: 3,
28 | });
29 | 			
30 |
31 | 			
32 |
33 |
34 |
35 | platform({
36 | 	linux: 1,
37 | 	macos: () => 2,
38 | 	default: 3,
39 | });
40 | 			
41 |
42 | 			
43 |
44 |
45 |
46 | platform({
47 | 	linux: 1,
48 | 	default: 3,
49 | });
50 | 			
51 |
52 | 			
53 |
54 |
55 |
56 | platform({
57 | 	linux: 1,
58 | });
59 | 			
60 |
61 | 			
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /test/preload.cjs: -------------------------------------------------------------------------------- 1 | const {ipcRenderer: ipc, contextBridge} = require('electron'); 2 | 3 | contextBridge.exposeInMainWorld('electronAPI', { 4 | handlePlatformTestResult: callback => ipc.on('test-platform', callback), 5 | }); 6 | -------------------------------------------------------------------------------- /test/renderer.js: -------------------------------------------------------------------------------- 1 | window.electronAPI.handlePlatformTestResult( 2 | // eslint-disable-next-line max-params 3 | (_event, testName, actual, expect, result) => { 4 | console.log(testName, actual, expect, result); 5 | 6 | const resultElement = document.querySelector( 7 | `#test-platform-${testName} .result`, 8 | ); 9 | 10 | resultElement.innerHTML = `expected ${expect}, got ${actual}`; 11 | resultElement.classList.add(result ? 'success' : 'error'); 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "distribution" 5 | }, 6 | "include": [ 7 | "source/**/index.ts" 8 | ] 9 | } 10 | --------------------------------------------------------------------------------