├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .lintstagedrc.json ├── .npmignore ├── LICENSE ├── README.md ├── example └── electron-app │ ├── .editorconfig │ ├── .eslintrc │ ├── .gitignore │ ├── .husky │ ├── .gitignore │ └── pre-commit │ ├── .lintstagedrc.json │ ├── LICENSE │ ├── README.md │ ├── app │ └── assets │ │ └── icons │ │ ├── icon-32x32.png │ │ ├── icon.png │ │ └── mac │ │ ├── icon.png │ │ └── icon@2x.png │ ├── img │ └── app-demo.gif │ ├── import-sorter.json │ ├── package.json │ ├── src │ ├── main │ │ ├── index.ts │ │ ├── ipc-main-api.ts │ │ ├── model.ts │ │ ├── tray.ts │ │ ├── tsconfig.json │ │ └── window.ts │ ├── renderer │ │ ├── browser-window-preload │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ └── browser-window │ │ │ ├── electron-exposure.d.ts │ │ │ ├── index.html │ │ │ ├── index.scss │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ └── shared │ │ ├── ipc-main-api-definition.ts │ │ └── model.ts │ ├── tsconfig.checks.json │ ├── tsconfig.json │ ├── webpack-configs │ ├── require-import.ts │ └── webpack.config.ts │ └── yarn.lock ├── img └── README-img1.gif ├── import-sorter.json ├── package.json ├── src └── lib │ ├── index.ts │ ├── ipc-main-api-service.ts │ ├── private │ ├── electron-require.ts │ ├── model.ts │ └── util.ts │ ├── tsconfig.json │ └── webview-api-service.ts ├── test ├── api.spec.ts ├── rewiremock.ts ├── tsconfig.json └── tslint.json ├── tsconfig.checks.json ├── tsconfig.json ├── tslint-extending ├── tslint-consistent-codestyle.json └── tslint-eslint-rules.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length=140 13 | 14 | [{*.json,*.yml}] 15 | indent_size = 2 16 | 17 | [*.md] 18 | max_line_length = off 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "GitHub Actions CI" 2 | on: { push: { branches: [ master ] }, pull_request: { branches: [ master ] } } 3 | jobs: 4 | lib: 5 | if: github.event_name == 'push' 6 | strategy: { matrix: { os: [ ubuntu-latest, windows-latest, macos-latest ], node: [ 18, 20 ] }, fail-fast: false } 7 | runs-on: ${{ matrix.os }} 8 | steps: 9 | - { uses: actions/setup-node@v1, with: { node-version: "${{ matrix.node }}" } } 10 | - { uses: actions/checkout@v2 } 11 | - name: lib 12 | run: | 13 | node --version 14 | npm --version 15 | yarn --version 16 | npx envinfo 17 | yarn --pure-lockfile --network-timeout 60000 18 | yarn add electron@12 19 | yarn run lib 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Idea 2 | .idea 3 | *.iml 4 | 5 | # Node.js 6 | node_modules 7 | npm-debug.log 8 | yarn-error.log 9 | 10 | /lib/ 11 | /output/ 12 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "npx format-imports", 4 | "npx tslint -p ./tsconfig.json --fix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | !/lib/ 4 | !/LICENSE 5 | !/package.json 6 | !/README.md 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vladimir Yakovlev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-rpc-api 2 | 3 | [![GitHub Actions CI](https://github.com/vladimiry/electron-rpc-api/workflows/GitHub%20Actions%20CI/badge.svg?branch=master)](https://github.com/vladimiry/electron-rpc-api/actions) 4 | 5 | Is a wrapper around the Electron's IPC for building type-safe API based RPC-like and reactive interactions. 6 | 7 | You describe an API structure and communication channel only once creating an API Service definition and then you share that definition between provider and client. It means that API method names and types of the input/return parameters on the client side are the same as on the provider side, so you get a type-safety on both sides having no overhead in runtime, thanks to TypeScript. 8 | 9 | The module provides `createIpcMainApiService` and `createWebViewApiService` factory-like functions that can be used to create respective service instances. 10 | 11 | ## Getting started 12 | 13 | Your project needs `rxjs` module to be installed, which is a peer dependency of this project. 14 | 15 | Method resolving and method calling are type-safe actions here: 16 | 17 | ![type-safety](img/README-img1.gif) 18 | 19 | `IpcMainApiService` usage example is shown below. It's based on the [example app](example/electron-app), so you can jump there and run the app. 20 | 21 | - First of all an API structure needs to be defined ([example/electron-app/src/shared/ipc-main-api-definition.ts](example/electron-app/src/shared/ipc-main-api-definition.ts)): 22 | ```typescript 23 | // no need to put API implementation logic here 24 | // but only API definition and service instance creating 25 | // as this file is supposed to be shared between the provider and client implementations 26 | import {ActionType, ScanService, createIpcMainApiService} from "electron-rpc-api"; 27 | 28 | const apiDefinition = { 29 | ping: ActionType.SubscribableLike<{ domain: string, times: number }, { domain: string, value: number }>(), 30 | sanitizeHtml: ActionType.Promise(), 31 | quitApp: ActionType.Promise(), 32 | }; 33 | 34 | export const IPC_MAIN_API_SERVICE = createIpcMainApiService({ 35 | channel: "some-event-name", // event name used to communicate between the event emitters 36 | apiDefinition, 37 | }); 38 | 39 | // optionally exposing inferred API structure 40 | export type ScannedIpcMainApiService = ScanService; 41 | ``` 42 | 43 | - API methods implementation and registration in `main` process using previously created `IPC_MAIN_API_SERVICE` service instance ([example/electron-app/src/main/ipc-main-api.ts](example/electron-app/src/main/ipc-main-api.ts)): 44 | ```typescript 45 | import sanitizeHtml from "sanitize-html"; 46 | import tcpPing from "tcp-ping"; 47 | import {app} from "electron"; 48 | import {interval} from "rxjs"; 49 | import {map, mergeMap, take} from "rxjs/operators"; 50 | import {observableToSubscribableLike} from "electron-rpc-api"; 51 | import {promisify} from "util"; 52 | 53 | import {IPC_MAIN_API_SERVICE, ScannedIpcMainApiService} from "src/shared/ipc-main-api-definition"; 54 | 55 | export function register(): ScannedIpcMainApiService["ApiClient"] { 56 | const api: ScannedIpcMainApiService["ApiImpl"] = { 57 | ping: ({domain, times}) => { 58 | return observableToSubscribableLike( 59 | interval(/*one second*/ 1000).pipe( 60 | take(times), 61 | mergeMap(() => promisify(tcpPing.ping)({address: domain, attempts: times})), 62 | map(({avg: value}) => { 63 | if (typeof value === "undefined" || isNaN(value)) { 64 | throw new Error(`Host "${domain}" is unreachable`); 65 | } 66 | return {domain, value}; 67 | }), 68 | ) 69 | ); 70 | }, 71 | async sanitizeHtml(input) { 72 | return sanitizeHtml( 73 | input, 74 | { 75 | allowedTags: sanitizeHtml.defaults.allowedTags.concat(["span"]), 76 | allowedClasses: { 77 | span: ["badge", "badge-light", "badge-danger"], 78 | }, 79 | }, 80 | ); 81 | }, 82 | async quitApp() { 83 | app.quit(); 84 | }, 85 | }; 86 | 87 | IPC_MAIN_API_SERVICE.register(api); 88 | 89 | return api; 90 | } 91 | ``` 92 | 93 | - Exposing the API Client factory function to `renderer` process as `window.__ELECTRON_EXPOSURE__` property using `contextBridge.exposeInMainWorld` call in `preload` script ([example/electron-app/src/renderer/browser-window-preload/index.ts](example/electron-app/src/renderer/browser-window-preload/index.ts)): 94 | ```typescript 95 | import {contextBridge} from "electron"; 96 | 97 | import {ElectronWindow} from "src/shared/model"; 98 | import {IPC_MAIN_API_SERVICE} from "src/shared/ipc-main-api-definition"; 99 | 100 | const electronWindow: ElectronWindow = { 101 | __ELECTRON_EXPOSURE__: { 102 | buildIpcMainClient: IPC_MAIN_API_SERVICE.client.bind(IPC_MAIN_API_SERVICE), 103 | }, 104 | }; 105 | 106 | const exposeKey: keyof typeof electronWindow = "__ELECTRON_EXPOSURE__"; 107 | 108 | contextBridge.exposeInMainWorld(exposeKey, electronWindow[exposeKey]); 109 | ``` 110 | 111 | - And finally calling the API methods in `renderer` process using exposed in preload script `window.__ELECTRON_EXPOSURE__` property ([example/electron-app/src/renderer/browser-window/index.ts](example/electron-app/src/renderer/browser-window/index.ts)): 112 | ```typescript 113 | import {subscribableLikeToObservable} from "electron-rpc-api"; 114 | 115 | import "./index.scss"; 116 | 117 | // the below code block is recommended for adding if you create/destroy 118 | // the renderer processes dynamically (multiple times) 119 | const cleanupPromise = new Promise((resolve) => { 120 | // don't call ".destroy()" on the BrowserWindow instance in the main process but ".close()" 121 | // since the app needs "window.beforeunload" event handler to be triggered 122 | window.addEventListener("beforeunload", () => resolve()); 123 | }); 124 | 125 | const ipcMainApiClient = __ELECTRON_EXPOSURE__.buildIpcMainClient({ 126 | // the below code line is recommended for adding if you create/destroy 127 | // the renderer processes dynamically (multiple times) 128 | options: {finishPromise: cleanupPromise}, 129 | }); 130 | 131 | // resolved methods 132 | const ipcMainPingMethod = ipcMainApiClient("ping"); // type-safe API method resolving 133 | const ipcMainSanitizeHtmlMethod = ipcMainApiClient("sanitizeHtml"); // type-safe API method resolving 134 | 135 | window.addEventListener("DOMContentLoaded", () => { 136 | const form = document.querySelector("form") as HTMLFormElement; 137 | const fieldset = form.querySelector("fieldset") as HTMLFieldSetElement; 138 | const input = form.querySelector("[name=domain]") as HTMLInputElement; 139 | const times = form.querySelector("[name=times]") as HTMLInputElement; 140 | const quitBtn = form.querySelector(`[type="button"]`) as HTMLFormElement; 141 | const disableForm = (disable: boolean) => { 142 | disable 143 | ? fieldset.setAttribute("disabled", "disabled") 144 | : fieldset.removeAttribute("disabled") 145 | }; 146 | 147 | form.addEventListener("submit", async (event) => { 148 | event.preventDefault(); 149 | disableForm(true); 150 | 151 | // type-safe API method calling 152 | subscribableLikeToObservable( 153 | ipcMainPingMethod({domain: input.value, times: Number(times.value)}) 154 | ).subscribe( 155 | async ({domain, value}) => { 156 | await append(`${domain} ${value}`); 157 | }, 158 | // "error" handler 159 | async ({message}) => { 160 | disableForm(false); 161 | await append(`${message}`); 162 | }, 163 | // "complete" handler 164 | () => { 165 | disableForm(false); 166 | }, 167 | ); 168 | }); 169 | 170 | quitBtn.addEventListener("click", async () => { 171 | await ipcMainApiClient("quitApp")(); 172 | }); 173 | }); 174 | 175 | async function append(html: string) { 176 | document.body 177 | .appendChild(document.createElement("div")) 178 | .innerHTML = await ipcMainSanitizeHtmlMethod(html); 179 | } 180 | ``` 181 | -------------------------------------------------------------------------------- /example/electron-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length=140 13 | 14 | [*.json] 15 | indent_size = 2 16 | 17 | [*.md] 18 | max_line_length = off 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /example/electron-app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "project": "./tsconfig.json" 7 | }, 8 | "overrides": [ 9 | { 10 | "files": [ 11 | "*.ts" 12 | ], 13 | "plugins": [ 14 | "import", 15 | "@typescript-eslint", 16 | "sonarjs" 17 | ], 18 | "extends": [ 19 | "eslint:recommended", 20 | "plugin:import/errors", 21 | "plugin:import/warnings", 22 | "plugin:sonarjs/recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 25 | ], 26 | "rules": { 27 | "max-len": [ 28 | "error", 29 | { 30 | "code": 140 31 | } 32 | ], 33 | "no-console": "error", 34 | "no-else-return": "error", 35 | "no-lonely-if": "error", 36 | "no-return-await": "error", 37 | "no-unused-expressions": "error", 38 | "no-useless-return": "error", 39 | "no-restricted-imports": [ 40 | "error", 41 | { 42 | "patterns": [ 43 | "rxjs/*", 44 | "!rxjs/operators" 45 | ] 46 | } 47 | ], 48 | "prefer-destructuring": "error", 49 | "semi": "error", 50 | "import/no-unresolved": "off", 51 | "import/no-relative-parent-imports": "error", 52 | "sonarjs/prefer-immediate-return": "off", 53 | "sonarjs/cognitive-complexity": "off", 54 | "sonarjs/no-duplicate-string": "off", 55 | "@typescript-eslint/no-floating-promises": "error", 56 | "@typescript-eslint/no-unsafe-return": "error", 57 | "@typescript-eslint/promise-function-async": "error", 58 | "@typescript-eslint/require-await": "off", 59 | "@typescript-eslint/no-misused-promises": [ 60 | "error", 61 | { 62 | "checksVoidReturn": false 63 | } 64 | ], 65 | "@typescript-eslint/explicit-function-return-type": [ 66 | "warn", 67 | { 68 | "allowExpressions": true 69 | } 70 | ] 71 | } 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /example/electron-app/.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Idea 2 | .idea 3 | *.iml 4 | 5 | # Node.js 6 | node_modules 7 | npm-debug.log 8 | yarn-error.log 9 | 10 | /app/generated/ 11 | /dist/ 12 | /output/ 13 | -------------------------------------------------------------------------------- /example/electron-app/.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /example/electron-app/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /example/electron-app/.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "format-imports", 4 | "yarn lint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/electron-app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vladimir Yakovlev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/electron-app/README.md: -------------------------------------------------------------------------------- 1 | Electron.js app with [electron-rpc-api](https://github.com/vladimiry/electron-rpc-api) module usage examples. 2 | 3 | ## Getting started 4 | 5 | 0. Make sure you run `npm v7+` (comes with `Node.js v15+`). 6 | 1. Run `yarn --pure-lockfile` console command to install the dependencies. 7 | 2. Run `yarn start` console command to build and start the app. 8 | 9 | ![app-demo](img/app-demo.gif) 10 | -------------------------------------------------------------------------------- /example/electron-app/app/assets/icons/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladimiry/electron-rpc-api/5ad2e6f58012af4dd79f1c4a052fd40d9ce53cd2/example/electron-app/app/assets/icons/icon-32x32.png -------------------------------------------------------------------------------- /example/electron-app/app/assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladimiry/electron-rpc-api/5ad2e6f58012af4dd79f1c4a052fd40d9ce53cd2/example/electron-app/app/assets/icons/icon.png -------------------------------------------------------------------------------- /example/electron-app/app/assets/icons/mac/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladimiry/electron-rpc-api/5ad2e6f58012af4dd79f1c4a052fd40d9ce53cd2/example/electron-app/app/assets/icons/mac/icon.png -------------------------------------------------------------------------------- /example/electron-app/app/assets/icons/mac/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladimiry/electron-rpc-api/5ad2e6f58012af4dd79f1c4a052fd40d9ce53cd2/example/electron-app/app/assets/icons/mac/icon@2x.png -------------------------------------------------------------------------------- /example/electron-app/img/app-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladimiry/electron-rpc-api/5ad2e6f58012af4dd79f1c4a052fd40d9ce53cd2/example/electron-app/img/app-demo.gif -------------------------------------------------------------------------------- /example/electron-app/import-sorter.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "bracketSpacing": false, 4 | "sortImportsBy": "names", 5 | "groupRules": [ 6 | {}, 7 | "^(src/|webpack-configs/|\\./|\\.\\./)" 8 | ], 9 | "wrappingStyle": { 10 | "maxBindingNamesPerLine": 0, 11 | "maxDefaultAndBindingNamesPerLine": 0, 12 | "maxExportNamesPerLine": 0, 13 | "maxNamesPerWrappedLine": 0, 14 | "ignoreComments": false 15 | }, 16 | "keepUnused": [ 17 | ".*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /example/electron-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-app", 3 | "version": "0.1.0", 4 | "description": "Electron.js app with electron-rpc-api module usage examples", 5 | "author": "Vladimir Yakovlev (https://github.com/vladimiry)", 6 | "license": "MIT", 7 | "repository": "git@github.com:vladimiry/electron-rpc-api.git", 8 | "engines": { 9 | "node": ">=20" 10 | }, 11 | "main": "./app/generated/main/index.js", 12 | "scripts": { 13 | "build": "yarn webpack:shortcut --config ./webpack-configs/webpack.config.ts", 14 | "build:watch": "yarn webpack:shortcut --config ./webpack-configs/webpack.config.ts -w", 15 | "build:watch:dev": "cross-env NODE_ENV=development yarn webpack:shortcut --config ./webpack-configs/webpack.config.ts -w", 16 | "clean": "rimraf ./app/generated", 17 | "electron-builder:directory": "electron-builder --dir", 18 | "electron-builder:package": "electron-builder", 19 | "electron:start": "npx --no-install electron ./app/generated/main/index.js", 20 | "lint": "eslint \"./src/**/*.ts\" \"./webpack-configs/webpack.config.ts\"", 21 | "start": "npm-run-all lint build electron:start", 22 | "webpack:shortcut": "cross-env TS_NODE_FILES=true npm exec --package=webpack-cli --node-options=\"--require tsconfig-paths/register\" -- webpack" 23 | }, 24 | "dependencies": { 25 | "electron-rpc-api": "10.0.0", 26 | "rxjs": "7.8.1", 27 | "sanitize-html": "2.13.0", 28 | "tcp-ping": "0.1.1" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "20.14.10", 32 | "@types/sanitize-html": "2.11.0", 33 | "@types/tcp-ping": "0.1.6", 34 | "@typescript-eslint/eslint-plugin": "6.18.1", 35 | "@typescript-eslint/parser": "6.18.1", 36 | "bootstrap": "5.3.3", 37 | "cross-env": "7.0.3", 38 | "css-loader": "7.1.2", 39 | "electron": "31.2.0", 40 | "electron-builder": "24.13.3", 41 | "eslint": "8.56.0", 42 | "eslint-plugin-import": "2.29.1", 43 | "eslint-plugin-sonarjs": "0.23.0", 44 | "format-imports": "4.0.4", 45 | "html-webpack-plugin": "5.6.0", 46 | "husky": "9.0.11", 47 | "lint-staged": "15.2.7", 48 | "mini-css-extract-plugin": "2.9.0", 49 | "npm-run-all2": "6.2.2", 50 | "ping": "0.4.4", 51 | "rimraf": "6.0.1", 52 | "sass": "1.77.8", 53 | "sass-loader": "14.2.1", 54 | "ts-loader": "9.5.1", 55 | "ts-node": "10.9.2", 56 | "tsconfig-paths": "4.2.0", 57 | "tsconfig-paths-webpack-plugin": "4.1.0", 58 | "typescript": "5.5.3", 59 | "wait-on": "7.2.0", 60 | "webpack": "5.93.0", 61 | "webpack-cli": "5.1.4", 62 | "webpack-dev-server": "5.0.4", 63 | "webpack-merge": "6.0.1" 64 | }, 65 | "resolutions": { 66 | "*/**/tslib": "^2.x" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/electron-app/src/main/index.ts: -------------------------------------------------------------------------------- 1 | import {app} from "electron"; 2 | import os from "os"; 3 | import path from "path"; 4 | import url from "url"; 5 | 6 | import {AppContext} from "./model"; 7 | import {init as initBrowserWindow} from "./window"; 8 | import {init as initTray} from "./tray"; 9 | import {register as registerApi} from "./ipc-main-api"; 10 | import {ScannedIpcMainApiService} from "src/shared/ipc-main-api-definition"; 11 | 12 | const development = process.env.NODE_ENV === "development"; 13 | 14 | (async () => { // eslint-disable-line @typescript-eslint/no-floating-promises 15 | await initApp( 16 | initContext(), 17 | registerApi(), 18 | ); 19 | })(); 20 | 21 | async function initApp( 22 | ctx: AppContext, 23 | api: ScannedIpcMainApiService["ApiClient"], 24 | ): Promise { 25 | if (development) { 26 | app.on("web-contents-created", (...[, contents]) => { 27 | contents.openDevTools(); 28 | }); 29 | } 30 | 31 | app.on("ready", async () => { 32 | const uiContext = ctx.uiContext = { 33 | browserWindow: await initBrowserWindow(ctx), 34 | tray: await initTray(ctx, api), 35 | }; 36 | 37 | app.on("activate", async () => { 38 | if (!uiContext.browserWindow || uiContext.browserWindow.isDestroyed()) { 39 | uiContext.browserWindow = await initBrowserWindow(ctx); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | function initContext(): AppContext { 46 | const appRoot = path.join(__dirname, "./../../../app"); 47 | const appRelative = (value = ""): string => path.join(appRoot, value); 48 | const formatFileUrl = (pathname: string): string => url.format({pathname, protocol: "file:", slashes: true}); 49 | const browserWindowIcon = "./assets/icons/icon.png"; 50 | const browserWindowPage = development 51 | ? "http://localhost:8080/renderer/browser-window/index.html" 52 | : formatFileUrl(appRelative("./generated/renderer/browser-window/index.html")); 53 | const trayIcon = appRelative( 54 | os.platform() === "darwin" 55 | ? "./assets/icons/mac/icon.png" 56 | : ( 57 | os.platform() !== "win32" 58 | // 32x32 on non-macOS/non-Windows systems (eg Linux) 59 | // https://github.com/electron/electron/issues/21445#issuecomment-565710027 60 | ? "./assets/icons/icon-32x32.png" 61 | : browserWindowIcon 62 | ), 63 | ); 64 | 65 | return { 66 | locations: { 67 | app: appRelative(), 68 | browserWindowIcon: appRelative(browserWindowIcon), 69 | browserWindowPage, 70 | browserWindowPreload: appRelative("./generated/renderer/browser-window-preload/index.js"), 71 | trayIcon, 72 | renderer: { 73 | browserWindow: appRelative("./generated/renderer/browser-window/index.js"), 74 | }, 75 | }, 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /example/electron-app/src/main/ipc-main-api.ts: -------------------------------------------------------------------------------- 1 | import {app} from "electron"; 2 | import {interval} from "rxjs"; 3 | import {map, mergeMap, take} from "rxjs/operators"; 4 | import {observableToSubscribableLike} from "electron-rpc-api"; 5 | import {promisify} from "util"; 6 | import sanitizeHtml from "sanitize-html"; 7 | import tcpPing from "tcp-ping"; 8 | 9 | import {IPC_MAIN_API_SERVICE, ScannedIpcMainApiService} from "src/shared/ipc-main-api-definition"; 10 | 11 | export function register(): ScannedIpcMainApiService["ApiClient"] { 12 | const api: ScannedIpcMainApiService["ApiImpl"] = { 13 | ping: ({domain, times}) => { 14 | return observableToSubscribableLike( 15 | interval(/*one second*/ 1000).pipe( 16 | take(times), 17 | mergeMap(async () => promisify(tcpPing.ping)({address: domain, attempts: times})), 18 | map(({avg: value}) => { 19 | if (typeof value === "undefined" || isNaN(value)) { 20 | throw new Error(`Host "${domain}" is unreachable`); 21 | } 22 | return {domain, value}; 23 | }), 24 | ) 25 | ); 26 | }, 27 | async sanitizeHtml(input) { 28 | return sanitizeHtml( 29 | input, 30 | { 31 | allowedTags: sanitizeHtml.defaults.allowedTags.concat(["span"]), 32 | allowedClasses: { 33 | span: ["badge", "bg-secondary", "bg-danger"], 34 | }, 35 | }, 36 | ); 37 | }, 38 | async quitApp() { 39 | app.quit(); 40 | }, 41 | }; 42 | 43 | IPC_MAIN_API_SERVICE.register(api); 44 | 45 | return api; 46 | } 47 | -------------------------------------------------------------------------------- /example/electron-app/src/main/model.ts: -------------------------------------------------------------------------------- 1 | export interface AppContext { 2 | readonly locations: AppContextLocations; 3 | uiContext?: AppUiContext; 4 | } 5 | 6 | export interface AppContextLocations { 7 | readonly app: string; 8 | readonly browserWindowIcon: string; 9 | readonly browserWindowPage: string; 10 | readonly browserWindowPreload: string; 11 | readonly trayIcon: string; 12 | readonly renderer: { 13 | readonly browserWindow: string; 14 | }; 15 | } 16 | 17 | export interface AppUiContext { 18 | browserWindow: Electron.BrowserWindow; 19 | readonly tray: Electron.Tray; 20 | } 21 | -------------------------------------------------------------------------------- /example/electron-app/src/main/tray.ts: -------------------------------------------------------------------------------- 1 | import {app, Menu, Tray} from "electron"; 2 | 3 | import {AppContext} from "./model"; 4 | import {ScannedIpcMainApiService} from "src/shared/ipc-main-api-definition"; 5 | import {toggle as toggleBrowserWindow} from "./window"; 6 | 7 | export async function init(ctx: AppContext, api: ScannedIpcMainApiService["ApiClient"]): Promise { 8 | const tray = new Tray(ctx.locations.trayIcon); 9 | const toggleWindow = (): void => toggleBrowserWindow(ctx.uiContext); 10 | const contextMenu = Menu.buildFromTemplate([ 11 | { 12 | label: "Toggle Window", 13 | click: toggleWindow, 14 | }, 15 | { 16 | type: "separator", 17 | }, 18 | { 19 | label: "Quit", 20 | async click() { 21 | await api.quitApp(); 22 | }, 23 | }, 24 | ]); 25 | 26 | tray.setContextMenu(contextMenu); 27 | tray.on("click", toggleWindow); 28 | 29 | app.on("before-quit", () => tray.destroy()); 30 | 31 | return tray; 32 | } 33 | -------------------------------------------------------------------------------- /example/electron-app/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "./index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/electron-app/src/main/window.ts: -------------------------------------------------------------------------------- 1 | import {app, BrowserWindow} from "electron"; 2 | 3 | import {AppContext, AppUiContext} from "./model"; 4 | 5 | export async function init({locations}: AppContext): Promise { 6 | const browserWindow = new BrowserWindow({ 7 | icon: locations.browserWindowIcon, 8 | webPreferences: { 9 | sandbox: true, 10 | contextIsolation: true, 11 | nodeIntegration: false, 12 | preload: locations.browserWindowPreload, 13 | }, 14 | }); 15 | 16 | browserWindow.on("closed", () => { 17 | browserWindow.destroy(); 18 | 19 | if (process.platform !== "darwin") { 20 | app.quit(); 21 | } 22 | }); 23 | 24 | await browserWindow.loadURL(locations.browserWindowPage); 25 | browserWindow.setMenu(null); 26 | 27 | return browserWindow; 28 | } 29 | 30 | export function activate(uiContext?: AppUiContext): void { 31 | if (!uiContext || !uiContext.browserWindow) { 32 | return; 33 | } 34 | 35 | uiContext.browserWindow.show(); 36 | uiContext.browserWindow.focus(); 37 | } 38 | 39 | export function toggle(uiContext?: AppUiContext, forceVisibilityState?: boolean): void { 40 | if (!uiContext || !uiContext.browserWindow) { 41 | return; 42 | } 43 | 44 | if (typeof forceVisibilityState !== "undefined" ? forceVisibilityState : !uiContext.browserWindow.isVisible()) { 45 | activate(uiContext); 46 | } else { 47 | uiContext.browserWindow.hide(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window-preload/index.ts: -------------------------------------------------------------------------------- 1 | import {contextBridge} from "electron"; 2 | 3 | import {ElectronWindow} from "src/shared/model"; 4 | import {IPC_MAIN_API_SERVICE} from "src/shared/ipc-main-api-definition"; 5 | 6 | const electronWindow: ElectronWindow = { 7 | __ELECTRON_EXPOSURE__: { 8 | buildIpcMainClient: IPC_MAIN_API_SERVICE.client.bind(IPC_MAIN_API_SERVICE), 9 | }, 10 | }; 11 | 12 | const exposeKey: keyof typeof electronWindow = "__ELECTRON_EXPOSURE__"; 13 | 14 | contextBridge.exposeInMainWorld(exposeKey, electronWindow[exposeKey]); 15 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window-preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": [ 4 | "./index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window/electron-exposure.d.ts: -------------------------------------------------------------------------------- 1 | import {ElectronWindow} from "src/shared/model"; 2 | 3 | declare global { 4 | const __ELECTRON_EXPOSURE__: ElectronWindow["__ELECTRON_EXPOSURE__"]; 5 | } 6 | 7 | declare var window: Window; // eslint-disable-line no-var 8 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 |
10 |
11 | 12 | 17 | 20 |
21 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window/index.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | 3 | body { 4 | margin: 0; 5 | padding: 15px; 6 | } 7 | 8 | .btn { 9 | padding-left: 20px; 10 | padding-right: 20px; 11 | } 12 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | 3 | import {subscribableLikeToObservable} from "electron-rpc-api"; 4 | 5 | // the below code block is recommended for adding if you create/destroy 6 | // the renderer processes dynamically (multiple times) 7 | const cleanupPromise = new Promise((resolve) => { 8 | // don't call ".destroy()" on the BrowserWindow instance in the main process but ".close()" 9 | // since the app needs "window.beforeunload" event handler to be triggered 10 | window.addEventListener("beforeunload", () => resolve()); 11 | }); 12 | 13 | const ipcMainApiClient = __ELECTRON_EXPOSURE__.buildIpcMainClient({ 14 | // the below code line is recommended for adding if you create/destroy 15 | // the renderer processes dynamically (multiple times) 16 | options: {finishPromise: cleanupPromise}, 17 | }); 18 | 19 | // resolved methods 20 | const ipcMainPingMethod = ipcMainApiClient("ping"); // type-safe API method resolving 21 | const ipcMainSanitizeHtmlMethod = ipcMainApiClient("sanitizeHtml"); // type-safe API method resolving 22 | 23 | window.addEventListener("DOMContentLoaded", () => { 24 | const form = document.querySelector("form") as HTMLFormElement; 25 | const fieldset = form.querySelector("fieldset") as HTMLFieldSetElement; 26 | const input = form.querySelector("[name=domain]") as HTMLInputElement; 27 | const times = form.querySelector("[name=times]") as HTMLInputElement; 28 | const quitBtn = form.querySelector(`[type="button"]`) as HTMLFormElement; 29 | const disableForm = (disable: boolean): void => { 30 | fieldset[disable ? "setAttribute" : "removeAttribute"]("disabled", "disabled"); 31 | }; 32 | 33 | form.addEventListener("submit", async (event) => { 34 | event.preventDefault(); 35 | disableForm(true); 36 | 37 | // type-safe API method calling 38 | subscribableLikeToObservable( 39 | ipcMainPingMethod({domain: input.value, times: Number(times.value)}) 40 | ).subscribe( 41 | async ({domain, value}) => { 42 | await append(`${domain} ${value}`); 43 | }, 44 | // "error" handler 45 | async ({message}) => { 46 | disableForm(false); 47 | await append(`${String(message)}`); 48 | }, 49 | // "complete" handler 50 | () => { 51 | disableForm(false); 52 | }, 53 | ); 54 | }); 55 | 56 | quitBtn.addEventListener("click", async () => { 57 | await ipcMainApiClient("quitApp")(); 58 | }); 59 | }); 60 | 61 | async function append(html: string): Promise { 62 | document.body 63 | .appendChild(document.createElement("div")) 64 | .innerHTML = await ipcMainSanitizeHtmlMethod(html); 65 | } 66 | -------------------------------------------------------------------------------- /example/electron-app/src/renderer/browser-window/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "files": [ 4 | "./electron-exposure.d.ts", 5 | "./index.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /example/electron-app/src/shared/ipc-main-api-definition.ts: -------------------------------------------------------------------------------- 1 | // no need to put API implementation logic here 2 | // but only API definition and service instance creating 3 | // as this file is supposed to be shared between the provider and client implementations 4 | import {ActionType, createIpcMainApiService, ScanService} from "electron-rpc-api"; 5 | 6 | const apiDefinition = { 7 | ping: ActionType.SubscribableLike<{ domain: string, times: number }, { domain: string, value: number }>(), 8 | sanitizeHtml: ActionType.Promise(), 9 | quitApp: ActionType.Promise(), 10 | }; 11 | 12 | export const IPC_MAIN_API_SERVICE = createIpcMainApiService({ 13 | channel: "some-event-name", // event name used to communicate between the event emitters 14 | apiDefinition, 15 | }); 16 | 17 | // optionally exposing inferred API structure 18 | export type ScannedIpcMainApiService = ScanService; 19 | -------------------------------------------------------------------------------- /example/electron-app/src/shared/model.ts: -------------------------------------------------------------------------------- 1 | export interface ElectronWindow { 2 | readonly __ELECTRON_EXPOSURE__: { 3 | readonly buildIpcMainClient: (typeof import("./ipc-main-api-definition").IPC_MAIN_API_SERVICE)["client"]; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /example/electron-app/tsconfig.checks.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "strict": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/electron-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://www.typescriptlang.org/docs/handbook/tsconfig-json.html 3 | // https://www.typescriptlang.org/docs/handbook/compiler-options.html 4 | "extends": "./tsconfig.checks.json", 5 | "compilerOptions": { 6 | "target": "es2019", 7 | "module": "commonjs", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "noEmitOnError": true, 11 | "declaration": false, 12 | "sourceMap": true, 13 | "lib": [ 14 | "dom", 15 | "es6" 16 | ], 17 | "outDir": "./app", 18 | "baseUrl": ".", 19 | "typeRoots": [ 20 | "./node_modules/@types", 21 | "./src/@types" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/electron-app/webpack-configs/require-import.ts: -------------------------------------------------------------------------------- 1 | // TODO webpack v5: remove "require/var-requires"-based imports 2 | 3 | declare class MiniCssExtractPluginClass { 4 | public static readonly loader: string; 5 | 6 | constructor(); 7 | 8 | apply(compiler: import("webpack").Compiler): void; 9 | } 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 12 | export const MiniCssExtractPlugin: typeof MiniCssExtractPluginClass 13 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires 14 | = require("mini-css-extract-plugin"); 15 | -------------------------------------------------------------------------------- /example/electron-app/webpack-configs/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import {Configuration} from "webpack"; 2 | import HtmlWebpackPlugin from "html-webpack-plugin"; 3 | import {Options} from "tsconfig-paths-webpack-plugin/lib/options"; 4 | import packageJson from "package.json"; 5 | import path from "path"; 6 | import {TsconfigPathsPlugin} from "tsconfig-paths-webpack-plugin"; 7 | import {merge as webpackMerge} from "webpack-merge"; 8 | 9 | import {MiniCssExtractPlugin} from "webpack-configs/require-import"; 10 | 11 | const production = process.env.NODE_ENV !== "development"; 12 | const rootPath = (value = ""): string => path.join(process.cwd(), value); 13 | const outputPath = (value = ""): string => path.join(rootPath("./app/generated"), value); 14 | 15 | const buildConfig = (configPatch: Configuration, tsOptions: Partial = {}): Configuration => { 16 | return webpackMerge( 17 | configPatch, 18 | { 19 | mode: production ? "production" : "development", 20 | devtool: false, 21 | output: { 22 | filename: "[name].js", 23 | path: outputPath(), 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.ts$/, 29 | use: { 30 | loader: "ts-loader", 31 | options: { 32 | configFile: tsOptions.configFile, 33 | }, 34 | }, 35 | }, 36 | ], 37 | }, 38 | resolve: { 39 | extensions: ["*", ".js", ".ts"], 40 | alias: { 41 | "msgpackr": rootPath("./node_modules/msgpackr/index.js"), 42 | }, 43 | plugins: [ 44 | new TsconfigPathsPlugin(tsOptions), 45 | ], 46 | fallback: { 47 | "path": false, 48 | "fs": false, 49 | }, 50 | }, 51 | node: { 52 | __dirname: false, 53 | __filename: false, 54 | }, 55 | optimization: { 56 | minimize: false, 57 | chunkIds: "named", 58 | moduleIds: "named", 59 | }, 60 | }, 61 | ); 62 | }; 63 | 64 | const configurations = [ 65 | buildConfig( 66 | { 67 | target: "electron-main", 68 | entry: { 69 | "main/index": rootPath("./src/main/index.ts"), 70 | }, 71 | externals: Object.keys(packageJson.dependencies).map((value) => `commonjs ${value}`), 72 | }, 73 | { 74 | configFile: rootPath("./src/main/tsconfig.json"), 75 | }, 76 | ), 77 | buildConfig( 78 | { 79 | entry: { 80 | "renderer/browser-window-preload/index": rootPath("./src/renderer/browser-window-preload/index.ts"), 81 | }, 82 | target: "electron-renderer", 83 | }, 84 | { 85 | configFile: rootPath("./src/renderer/browser-window-preload/tsconfig.json"), 86 | }, 87 | ), 88 | buildConfig( 89 | { 90 | entry: { 91 | "renderer/browser-window/index": rootPath("./src/renderer/browser-window/index.ts"), 92 | }, 93 | ...(production ? {} : { 94 | devServer: { 95 | client: { 96 | logging: "error", 97 | progress: true, 98 | }, 99 | devMiddleware: { 100 | stats: "minimal", 101 | }, 102 | }, 103 | }), 104 | target: "web", 105 | module: { 106 | rules: [ 107 | { 108 | test: /\.css$/, 109 | use: [ 110 | MiniCssExtractPlugin.loader, 111 | { 112 | loader: "css-loader", 113 | options: { 114 | esModule: false, 115 | }, 116 | }, 117 | ], 118 | }, 119 | { 120 | test: /\.scss/, 121 | use: [ 122 | MiniCssExtractPlugin.loader, 123 | { 124 | loader: "css-loader", 125 | options: { 126 | esModule: false, 127 | }, 128 | }, 129 | "sass-loader", 130 | ], 131 | }, 132 | ], 133 | }, 134 | plugins: [ 135 | new MiniCssExtractPlugin(), 136 | new HtmlWebpackPlugin({ 137 | template: rootPath("./src/renderer/browser-window/index.html"), 138 | filename: outputPath("./renderer/browser-window/index.html"), 139 | title: packageJson.description, 140 | minify: false, 141 | hash: false, 142 | }), 143 | ], 144 | }, 145 | { 146 | configFile: rootPath("./src/renderer/browser-window/tsconfig.json"), 147 | }, 148 | ), 149 | ]; 150 | 151 | export default configurations; 152 | -------------------------------------------------------------------------------- /img/README-img1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladimiry/electron-rpc-api/5ad2e6f58012af4dd79f1c4a052fd40d9ce53cd2/img/README-img1.gif -------------------------------------------------------------------------------- /import-sorter.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "bracketSpacing": false, 4 | "sortImportsBy": "names", 5 | "groupRules": [ 6 | {}, 7 | "^(src/|test/|lib|\\./|\\.\\./)" 8 | ], 9 | "wrappingStyle": { 10 | "maxBindingNamesPerLine": 0, 11 | "maxDefaultAndBindingNamesPerLine": 0, 12 | "maxExportNamesPerLine": 0, 13 | "maxNamesPerWrappedLine": 0, 14 | "ignoreComments": false 15 | }, 16 | "keepUnused": [ 17 | ".*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-rpc-api", 3 | "version": "10.0.0", 4 | "description": "Wrapper around the Electron's IPC for building type-safe API based RPC-like and reactive interactions", 5 | "author": "Vladimir Yakovlev (https://github.com/vladimiry)", 6 | "license": "MIT", 7 | "repository": "git@github.com:vladimiry/electron-rpc-api.git", 8 | "engines": { 9 | "node": ">=18" 10 | }, 11 | "keywords": [ 12 | "electron", 13 | "pubsub", 14 | "ipc", 15 | "rpc" 16 | ], 17 | "main": "./lib/index.js", 18 | "scripts": { 19 | "lib": "npm-run-all lib:clean lint lib:compile test", 20 | "lib:clean": "rimraf ./lib", 21 | "lib:compile": "tsc --listEmittedFiles -p ./src/lib/tsconfig.json", 22 | "lib:compile:watch": "tsc -w -p ./src/lib/tsconfig.json", 23 | "lint": "npm-run-all lint:lib lint:test", 24 | "lint:lib": "tslint -p ./src/lib/tsconfig.json -c ./tslint.json \"./src/{lib,@types}/**/*.ts\"", 25 | "lint:test": "tslint -p ./test/tsconfig.json -c ./test/tslint.json \"./test/**/*.ts\"", 26 | "output:clean": "rimraf ./output", 27 | "test": "cross-env TS_NODE_PROJECT=./test/tsconfig.json ava --verbose \"./test/**/*.{spec,test}.ts\"", 28 | "prepare": "husky install" 29 | }, 30 | "ava": { 31 | "extensions": [ 32 | "js", 33 | "ts" 34 | ], 35 | "files": [ 36 | "./test/**/*.{spec,test}.{ts,js}" 37 | ], 38 | "require": [ 39 | "ts-node/register", 40 | "tsconfig-paths/register" 41 | ], 42 | "verbose": true 43 | }, 44 | "peerDependencies": { 45 | "electron": ">=5.0.0" 46 | }, 47 | "dependencies": { 48 | "pubsub-to-rpc-api": "^8.0.2", 49 | "pure-uuid": "^1.8.1", 50 | "rxjs": "^7.8.1", 51 | "tslib": "^2.6.2" 52 | }, 53 | "devDependencies": { 54 | "@types/node": "^18.7.16", 55 | "@types/sinon": "^10.0.13", 56 | "ava": "^4.3.3", 57 | "cross-env": "^7.0.3", 58 | "format-imports": "^4.0.0", 59 | "husky": "^8.0.3", 60 | "install-peers": "^1.0.4", 61 | "lint-staged": "^15.2.0", 62 | "npm-run-all2": "^6.1.1", 63 | "rewiremock": "^3.14.5", 64 | "rimraf": "^5.0.5", 65 | "sinon": "^17.0.1", 66 | "ts-node": "^10.9.2", 67 | "tsconfig-paths": "^4.2.0", 68 | "tslint": "^6.1.3", 69 | "tslint-consistent-codestyle": "^1.16.0", 70 | "tslint-eslint-rules": "^5.4.0", 71 | "tslint-rules-bunch": "^1.0.0", 72 | "typescript": "^5.3.3" 73 | }, 74 | "resolutions": { 75 | "*/**/tslib": "^2.x" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import {ActionType, observableToSubscribableLike, ScanService, subscribableLikeToObservable} from "pubsub-to-rpc-api"; 2 | 3 | import {createIpcMainApiService} from "./ipc-main-api-service"; 4 | import {createWebViewApiService} from "./webview-api-service"; 5 | 6 | export { 7 | ActionType, createIpcMainApiService, createWebViewApiService, observableToSubscribableLike, ScanService, subscribableLikeToObservable, 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/ipc-main-api-service.ts: -------------------------------------------------------------------------------- 1 | import {IpcMain, IpcMainEvent, IpcRenderer} from "electron"; 2 | import * as Lib from "pubsub-to-rpc-api"; 3 | 4 | import {curryOwnFunctionMembers} from "./private/util"; 5 | import * as PM from "./private/model"; 6 | import {requireIpcMain, requireIpcRenderer} from "./private/electron-require"; 7 | 8 | // TODO infer from Electron.IpcMain["on"] listener arguments 9 | type DefACA = [IpcMainEvent, ...PM.Any[]]; 10 | 11 | type IpcMainEventEmittersCache = Pick; 12 | type IpcRendererEventEmittersCache = Pick; 13 | 14 | const ipcMainEventEmittersCache = new WeakMap(); 15 | const ipcRendererEventEmittersCache = new WeakMap(); 16 | 17 | export function createIpcMainApiService, ACA2 extends DefACA = DefACA>( 18 | createServiceArg: Lib.Model.CreateServiceInput, 19 | ): { 20 | register: ( 21 | actions: PM.Arguments["register"]>[0], 22 | options?: { 23 | ipcMain?: IpcMainEventEmittersCache; 24 | logger?: Lib.Model.Logger; 25 | }, 26 | ) => ReturnType["register"]>; 27 | client: ( 28 | clientOptions?: { 29 | ipcRenderer?: IpcRendererEventEmittersCache; 30 | options?: PM.Omit>, "onEventResolver">; 31 | }, 32 | ) => ReturnType["caller"]>; 33 | } { 34 | const baseService = Lib.createService(createServiceArg); 35 | 36 | return { 37 | register(actions, options) { 38 | const { 39 | ipcMain = requireIpcMain(), 40 | logger, 41 | } = options || {} as Exclude; 42 | const cachedEm: Lib.Model.CombinedEventEmitter = ( 43 | ipcMainEventEmittersCache.get(ipcMain) 44 | || 45 | (() => { 46 | const em: Lib.Model.CombinedEventEmitter = { 47 | on: ipcMain.on.bind(ipcMain), 48 | removeListener: ipcMain.removeListener.bind(ipcMain), 49 | emit: ipcMain.emit.bind(ipcMain), 50 | }; 51 | 52 | ipcMainEventEmittersCache.set(ipcMain, em); 53 | 54 | return em; 55 | })() 56 | ); 57 | 58 | return baseService.register( 59 | actions, 60 | cachedEm, 61 | { 62 | onEventResolver: (...[event, payload]) => { 63 | return { 64 | payload, 65 | emitter: { 66 | emit: (...args) => { 67 | if (!event.sender.isDestroyed()) { 68 | event.reply(...args); 69 | return; 70 | } 71 | if (logger) { 72 | logger.debug(`[${PM.MODULE_NAME}]`, `Object has been destroyed: "sender"`); 73 | } 74 | }, 75 | }, 76 | }; 77 | }, 78 | logger, 79 | }, 80 | ); 81 | }, 82 | client( 83 | { 84 | ipcRenderer = requireIpcRenderer(), 85 | options: { 86 | timeoutMs = PM.BASE_TIMEOUT_MS, 87 | logger: _logger_ = createServiceArg.logger || PM.LOG_STUB, // tslint:disable-line:variable-name 88 | ...callOptions 89 | } = {}, 90 | }: { 91 | ipcRenderer?: IpcRendererEventEmittersCache; 92 | options?: PM.Omit>, "onEventResolver">; 93 | } = {}, 94 | ) { 95 | const logger = curryOwnFunctionMembers(_logger_, `[${PM.MODULE_NAME}]`, "createIpcMainApiService() [client]"); 96 | const cachedEm: Lib.Model.CombinedEventEmitter = ( 97 | ipcRendererEventEmittersCache.get(ipcRenderer) 98 | || 99 | (() => { 100 | const em: Lib.Model.CombinedEventEmitter = { 101 | on: ipcRenderer.on.bind(ipcRenderer), 102 | removeListener: ipcRenderer.removeListener.bind(ipcRenderer), 103 | emit: ipcRenderer.send.bind(ipcRenderer), 104 | }; 105 | 106 | ipcRendererEventEmittersCache.set(ipcRenderer, em); 107 | 108 | return em; 109 | })() 110 | ); 111 | 112 | return baseService.caller( 113 | { 114 | emitter: cachedEm, 115 | listener: cachedEm, 116 | }, 117 | { 118 | ...callOptions, 119 | timeoutMs, 120 | logger, 121 | onEventResolver: (...[/* event */, payload]) => { 122 | return {payload}; 123 | }, 124 | }, 125 | ); 126 | }, 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /src/lib/private/electron-require.ts: -------------------------------------------------------------------------------- 1 | import {IpcMain, IpcRenderer} from "electron"; 2 | 3 | export function requireIpcMain(): IpcMain { 4 | return require("electron").ipcMain; 5 | } 6 | 7 | export function requireIpcRenderer(): IpcRenderer { 8 | return require("electron").ipcRenderer; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/private/model.ts: -------------------------------------------------------------------------------- 1 | import * as Lib from "pubsub-to-rpc-api"; 2 | import {Observable} from "rxjs"; 3 | 4 | // tslint:disable-next-line:no-any 5 | export type Any = any; 6 | 7 | export type Arguments Any> = 8 | F extends (...args: infer A) => Any ? A : never; 9 | 10 | export type Unpacked = 11 | T extends Promise ? U2 : 12 | T extends Observable ? U3 : 13 | T; 14 | 15 | export type Omit = Pick>; 16 | 17 | export const MODULE_NAME = "electron-rpc-api"; 18 | 19 | export const ONE_SECOND_MS = 1000; 20 | 21 | export const BASE_TIMEOUT_MS = 0; 22 | 23 | export const EMPTY_FN: Lib.Model.LoggerFn = () => {}; // tslint:disable-line:no-empty 24 | 25 | export const LOG_STUB: Readonly = { 26 | error: EMPTY_FN, 27 | warn: EMPTY_FN, 28 | info: EMPTY_FN, 29 | verbose: EMPTY_FN, 30 | debug: EMPTY_FN, 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/private/util.ts: -------------------------------------------------------------------------------- 1 | import * as PM from "../private/model"; 2 | 3 | export function curryOwnFunctionMembers PM.Any)>( 4 | src: T, 5 | ...args: PM.Any[] 6 | ): T { 7 | const dest: T = typeof src === "function" 8 | ? src.bind(undefined) : 9 | Object.create(null); 10 | 11 | for (const key of Object.getOwnPropertyNames(src)) { 12 | const srcMember = (src as PM.Any)[key]; 13 | 14 | if (typeof srcMember === "function") { 15 | (dest as PM.Any)[key] = srcMember.bind(src, ...args); 16 | } 17 | } 18 | 19 | return dest; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "files": [ 4 | "./index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/webview-api-service.ts: -------------------------------------------------------------------------------- 1 | import {IpcMessageEvent, IpcRenderer} from "electron"; 2 | import * as Lib from "pubsub-to-rpc-api"; 3 | import UUID from "pure-uuid"; 4 | 5 | import {curryOwnFunctionMembers} from "./private/util"; 6 | import * as PM from "./private/model"; 7 | import {requireIpcRenderer} from "./private/electron-require"; 8 | 9 | // TODO infer as PM.Arguments<(PM.Arguments)[1]> (listener is currently defined by Electron as a raw function) 10 | type ACA2 = [IpcMessageEvent, ...PM.Any[]]; // used by "pubsub-to-rpc-api" like Lib.Model.ActionContext 11 | 12 | type RegisterApiIpcRenderer = Pick; 13 | 14 | const ipcRendererEventEmittersCache = new WeakMap(); 15 | const webViewTagEventEmittersCache = new WeakMap(); 16 | 17 | const clientIpcMessageEventName = "ipc-message"; 18 | const clientIpcMessageListenerBundleProp = Symbol(`[${PM.MODULE_NAME}] clientIpcMessageListenerBundleProp symbol`); 19 | 20 | export function createWebViewApiService>( 21 | createServiceArg: Lib.Model.CreateServiceInput, 22 | ): { 23 | register: ( 24 | actions: PM.Arguments["register"]>[0], 25 | options?: Lib.Model.CreateServiceRegisterOptions & { ipcRenderer?: RegisterApiIpcRenderer; }, 26 | ) => ReturnType["register"]>; 27 | client: ( 28 | webView: Electron.WebviewTag, 29 | params?: { options?: Partial> }, 30 | ) => ReturnType["caller"]>; 31 | } { 32 | const baseService = Lib.createService(createServiceArg); 33 | const clientIpcMessageOnEventResolver: Lib.Model.ClientOnEventResolver = (ipcMessageEvent) => { 34 | const [payload] = ipcMessageEvent.args; 35 | return {payload}; 36 | }; 37 | 38 | return { 39 | register( 40 | actions, 41 | { 42 | logger, 43 | ipcRenderer = requireIpcRenderer(), 44 | }: Lib.Model.CreateServiceRegisterOptions & { ipcRenderer?: RegisterApiIpcRenderer; } = {}, 45 | ) { 46 | const cachedEm: Lib.Model.CombinedEventEmitter = ( 47 | ipcRendererEventEmittersCache.get(ipcRenderer) 48 | || 49 | (() => { 50 | const em: Lib.Model.CombinedEventEmitter = { 51 | on: ipcRenderer.on.bind(ipcRenderer), 52 | removeListener: ipcRenderer.removeListener.bind(ipcRenderer), 53 | emit: ipcRenderer.sendToHost.bind(ipcRenderer), 54 | }; 55 | 56 | ipcRendererEventEmittersCache.set(ipcRenderer, em); 57 | 58 | return em; 59 | })() 60 | ); 61 | 62 | return baseService.register( 63 | actions, 64 | cachedEm, 65 | { 66 | logger, 67 | onEventResolver: (...[/* event */, payload]) => { 68 | return { 69 | payload, 70 | emitter: cachedEm, 71 | }; 72 | }, 73 | }, 74 | ); 75 | }, 76 | client( 77 | webView, 78 | { 79 | options: { 80 | timeoutMs = PM.BASE_TIMEOUT_MS, 81 | logger: _logger_ = createServiceArg.logger || PM.LOG_STUB, // tslint:disable-line:variable-name 82 | ...callOptions 83 | } = {}, 84 | }: { 85 | options?: Partial>; 86 | } = {}, 87 | ) { 88 | const logger = curryOwnFunctionMembers(_logger_, `[${PM.MODULE_NAME}]`, "createWebViewApiService() [client]"); 89 | const cachedEm: Lib.Model.CombinedEventEmitter = ( 90 | webViewTagEventEmittersCache.get(webView) 91 | || 92 | (() => { 93 | type IpcMessageListenerBundleProp = Readonly<{ 94 | uid: string; 95 | created: Date; 96 | originalEventName: PM.Arguments[0]; 97 | actualListener: [ 98 | typeof clientIpcMessageEventName, 99 | (...args: PM.Arguments) => void]; 100 | }>; 101 | 102 | interface IpcMessageListenerBundlePropAware { 103 | [clientIpcMessageListenerBundleProp]?: IpcMessageListenerBundleProp; 104 | } 105 | 106 | const em: Lib.Model.CombinedEventEmitter = { 107 | on: (originalEventName, originalListener) => { 108 | const ipcMessageListenerBundle: IpcMessageListenerBundleProp = { 109 | uid: new UUID(4).format(), 110 | created: new Date(), 111 | originalEventName, 112 | actualListener: [ 113 | clientIpcMessageEventName, 114 | (ipcMessageEvent) => { 115 | if (ipcMessageEvent.channel !== originalEventName) { 116 | return; 117 | } 118 | originalListener(clientIpcMessageOnEventResolver(ipcMessageEvent).payload); 119 | }, 120 | ], 121 | }; 122 | 123 | webView.addEventListener(...ipcMessageListenerBundle.actualListener); 124 | 125 | // TODO consider keeping actual listeners in a WeakMap 126 | // link actual listener to the original listener, so we then could remove the actual listener 127 | // we know that "listener" function is not locked for writing props as it's constructed by "pubsub-to-rpc-api" 128 | (originalListener as IpcMessageListenerBundlePropAware)[clientIpcMessageListenerBundleProp] 129 | = ipcMessageListenerBundle; 130 | 131 | logger.debug( 132 | `[cache] add event listener`, 133 | JSON.stringify({ 134 | originalEventName, 135 | uid: ipcMessageListenerBundle.uid, 136 | created: ipcMessageListenerBundle.created, 137 | }), 138 | ); 139 | 140 | return em; 141 | }, 142 | removeListener: (...[originalEventName, originalListener]) => { 143 | const ipcMessageListenerBundlePropAware = originalListener as IpcMessageListenerBundlePropAware; 144 | const ipcMessageListenerBundle = ipcMessageListenerBundlePropAware[clientIpcMessageListenerBundleProp]; 145 | 146 | if ( 147 | !ipcMessageListenerBundle 148 | || 149 | ipcMessageListenerBundle.originalEventName !== originalEventName 150 | ) { 151 | return em; 152 | } 153 | 154 | const logData = JSON.stringify({ 155 | originalEventName, 156 | uid: ipcMessageListenerBundle.uid, 157 | created: ipcMessageListenerBundle.created, 158 | }); 159 | 160 | if (webView.isConnected) { 161 | webView.removeEventListener(...ipcMessageListenerBundle.actualListener); 162 | logger.debug(`[cache] remove event listener`, logData); 163 | } else { 164 | logger.debug(`[cache] remove event listener: skipped since "webView" is not attached to the DOM`, logData); 165 | } 166 | 167 | delete ipcMessageListenerBundlePropAware[clientIpcMessageListenerBundleProp]; 168 | 169 | return em; 170 | }, 171 | emit: (...args) => { 172 | if (webView.isConnected) { 173 | // tslint:disable-next-line:no-floating-promises 174 | webView.send(...args); 175 | } else { 176 | logger.debug(`"webView.send()" call skipped since "webView" is not attached to the DOM`); 177 | } 178 | }, 179 | }; 180 | 181 | webViewTagEventEmittersCache.set(webView, em); 182 | 183 | return em; 184 | })() 185 | ); 186 | 187 | return baseService.caller( 188 | {emitter: cachedEm, listener: cachedEm}, 189 | { 190 | ...callOptions, 191 | timeoutMs, 192 | }, 193 | ); 194 | }, 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /test/api.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestFn} from "ava"; 2 | import * as sinon from "sinon"; 3 | 4 | import {ActionType, ScanService} from "lib"; 5 | import * as PM from "lib/private/model"; 6 | import {rewiremock} from "./rewiremock"; 7 | 8 | const test = anyTest as TestFn; 9 | 10 | const apiDefinition = { 11 | stringToNumber: ActionType.Promise<[string], number>(), 12 | }; 13 | 14 | test.beforeEach(async (t) => { 15 | t.context.mocks = await buildMocks(); 16 | }); 17 | 18 | test("createIpcMainApiService", async (t) => { 19 | const {createIpcMainApiService: createIpcMainApiServiceMocked} = await rewiremock.around( 20 | () => import("lib"), 21 | (mock) => { 22 | mock(() => import("lib/private/electron-require")).with(t.context.mocks["lib/private/electron-require"] as PM.Any); 23 | mock(() => import("pubsub-to-rpc-api")).with(t.context.mocks["pubsub-to-rpc-api"]); 24 | }, 25 | ); 26 | const apiService = createIpcMainApiServiceMocked({ 27 | channel: "ch1", 28 | apiDefinition, 29 | }); 30 | const actions: ScanService["ApiImpl"] = { 31 | stringToNumber: async (input) => Number(input), 32 | }; 33 | const registerSpy = sinon.spy(apiService, "register"); 34 | const createServiceSpy = t.context.mocks._mockData["pubsub-to-rpc-api"].createService; 35 | const {requireIpcMain: ipcMain, requireIpcRenderer: ipcRenderer} = t.context.mocks._mockData["lib/private/electron-require"]; 36 | 37 | // register 38 | t.true(registerSpy.notCalled); 39 | apiService.register(actions); 40 | t.is(1, registerSpy.callCount); 41 | t.true((registerSpy.calledWith as PM.Any)(actions)); 42 | t.true(ipcMain.on.bind.calledWithExactly(ipcMain)); 43 | t.true(ipcMain.emit.bind.calledWithExactly(ipcMain)); 44 | t.true(ipcMain.removeListener.bind.calledWithExactly(ipcMain)); 45 | 46 | // register with custom ipcMain 47 | const {requireIpcMain: ipcMainOption} = (await buildMocks())._mockData["lib/private/electron-require"]; 48 | apiService.register(actions, {ipcMain: ipcMainOption} as PM.Any); 49 | t.is(2, registerSpy.callCount); 50 | t.true(ipcMainOption.on.bind.calledWithExactly(ipcMainOption)); 51 | t.true(ipcMainOption.emit.bind.calledWithExactly(ipcMainOption)); 52 | t.true(ipcMainOption.removeListener.bind.calledWithExactly(ipcMainOption)); 53 | 54 | // should be called after "register" got called 55 | const callerSpy = sinon.spy(createServiceSpy.returnValues[0], "caller"); 56 | 57 | // client 58 | t.true(callerSpy.notCalled); 59 | apiService.client(); 60 | t.is(1, callerSpy.callCount); 61 | t.true(ipcRenderer.removeListener.bind.calledWithExactly(ipcRenderer)); 62 | t.true(ipcRenderer.send.bind.calledWithExactly(ipcRenderer)); 63 | 64 | // client with custom ipcRenderer 65 | const {requireIpcRenderer: ipcRendererOption} = (await buildMocks())._mockData["lib/private/electron-require"]; 66 | apiService.client({ipcRenderer: ipcRendererOption} as PM.Any); 67 | t.is(2, callerSpy.callCount); 68 | t.true(ipcRendererOption.removeListener.bind.calledWithExactly(ipcRendererOption)); 69 | t.true(ipcRendererOption.send.bind.calledWithExactly(ipcRendererOption)); 70 | }); 71 | 72 | test.serial("createWebViewApiService", async (t) => { 73 | const {createWebViewApiService: createWebViewApiServiceMocked} = await rewiremock.around( 74 | () => import("lib"), 75 | (mock) => { 76 | mock(() => import("lib/private/electron-require")).with(t.context.mocks["lib/private/electron-require"] as PM.Any); 77 | mock(() => import("pubsub-to-rpc-api")).with(t.context.mocks["pubsub-to-rpc-api"]); 78 | }, 79 | ); 80 | const apiService = createWebViewApiServiceMocked({ 81 | channel: "ch1", 82 | apiDefinition, 83 | }); 84 | const actions: ScanService["ApiImpl"] = { 85 | stringToNumber: async (input) => Number(input), 86 | }; 87 | const registerSpy = sinon.spy(apiService, "register"); 88 | const createServiceSpy = t.context.mocks._mockData["pubsub-to-rpc-api"].createService; 89 | const {requireIpcRenderer: ipcRenderer} = t.context.mocks._mockData["lib/private/electron-require"]; 90 | const {webView} = t.context.mocks._mockData; 91 | 92 | // register 93 | t.true(registerSpy.notCalled); 94 | t.true(ipcRenderer.on.bind.notCalled); 95 | t.true(ipcRenderer.removeListener.bind.notCalled); 96 | t.true(ipcRenderer.sendToHost.bind.notCalled); 97 | apiService.register(actions); 98 | t.is(1, registerSpy.callCount); 99 | t.true((registerSpy.calledWithExactly as PM.Any)(actions)); 100 | t.true(ipcRenderer.on.bind.calledWithExactly(ipcRenderer)); 101 | t.true(ipcRenderer.removeListener.bind.calledWithExactly(ipcRenderer)); 102 | t.true(ipcRenderer.sendToHost.bind.calledWithExactly(ipcRenderer)); 103 | 104 | // register with custom ipcRenderer 105 | const {requireIpcRenderer: ipcRendererOption} = (await buildMocks())._mockData["lib/private/electron-require"]; 106 | apiService.register(actions, {ipcRenderer: ipcRendererOption} as PM.Any); 107 | t.is(2, registerSpy.callCount); 108 | t.true(ipcRendererOption.removeListener.bind.calledWithExactly(ipcRendererOption)); 109 | 110 | // should be called after "register" got called 111 | const callerSpy = sinon.spy(createServiceSpy.returnValues[0], "caller"); 112 | 113 | // client 114 | t.true(callerSpy.notCalled); 115 | apiService.client(webView as PM.Any); 116 | t.is(1, callerSpy.callCount); 117 | // t.true(webView.send.bind.calledWithExactly(webView)); 118 | }); 119 | 120 | interface TestContext { 121 | mocks: PM.Unpacked>; 122 | } 123 | 124 | function emptyFn() {} // tslint:disable-line:no-empty 125 | 126 | async function buildMocks() { 127 | const constructBindStub = () => ({bind: sinon.stub().returns(emptyFn)}); 128 | 129 | const lib = {...await import("pubsub-to-rpc-api")}; 130 | 131 | const _mockData = { 132 | "lib/private/electron-require": { 133 | requireIpcMain: { 134 | on: constructBindStub(), 135 | emit: constructBindStub(), 136 | removeListener: constructBindStub(), 137 | }, 138 | requireIpcRenderer: { 139 | on: constructBindStub(), 140 | removeListener: constructBindStub(), 141 | send: constructBindStub(), 142 | sendToHost: constructBindStub(), 143 | }, 144 | }, 145 | "pubsub-to-rpc-api": { 146 | createService: sinon.spy(lib, "createService"), 147 | }, 148 | "webView": { 149 | addEventListener: constructBindStub(), 150 | removeEventListener: constructBindStub(), 151 | send: constructBindStub(), 152 | isConnected: true, 153 | }, 154 | }; 155 | 156 | return { 157 | _mockData, 158 | "lib/private/electron-require": { 159 | requireIpcMain: () => _mockData["lib/private/electron-require"].requireIpcMain, 160 | requireIpcRenderer: () => _mockData["lib/private/electron-require"].requireIpcRenderer, 161 | }, 162 | "pubsub-to-rpc-api": lib, 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /test/rewiremock.ts: -------------------------------------------------------------------------------- 1 | import rewiremock, {plugins} from "rewiremock"; 2 | 3 | rewiremock.overrideEntryPoint(module); // this is important 4 | 5 | // and all stub names would be a relative 6 | // rewiremock.addPlugin(plugins.relative); 7 | 8 | // and all stubs should be used. Lets make it default! 9 | rewiremock.addPlugin(plugins.usedByDefault); 10 | 11 | export {rewiremock}; 12 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../tsconfig.json", 3 | "files": [ 4 | "./api.spec.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../tslint.json", 3 | "rulesDirectory": [ 4 | "./../node_modules/tslint-rules-bunch/rules" 5 | ], 6 | "rules": { 7 | "no-import-zones": [ 8 | true, 9 | { 10 | "zones": [ 11 | { 12 | "patterns": [ 13 | { 14 | "target": "test/**/*", 15 | "from": [ 16 | "src/**/*" 17 | ] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.checks.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noImplicitReturns": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "strict": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.checks.json", 3 | "compilerOptions": { 4 | /* 5 | resolving & capabilities 6 | */ 7 | "esModuleInterop": true, 8 | /* 9 | emitting 10 | */ 11 | "target": "es6", 12 | "module": "commonjs", 13 | "importHelpers": true, 14 | "noEmitOnError": true, 15 | "declaration": true, 16 | "sourceMap": true, 17 | /* 18 | paths 19 | */ 20 | "lib": [ 21 | "dom", 22 | "es6" 23 | ], 24 | "baseUrl": ".", 25 | "outDir": "./lib", 26 | "typeRoots": [ 27 | "./node_modules/@types", 28 | "./src/@types" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tslint-extending/tslint-consistent-codestyle.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "./../node_modules/tslint-consistent-codestyle/rules" 4 | ], 5 | "rules": { 6 | "early-exit": [ 7 | true 8 | ], 9 | "no-collapsible-if": [ 10 | true 11 | ], 12 | "no-unnecessary-else": [ 13 | true 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tslint-extending/tslint-eslint-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "./../node_modules/tslint-eslint-rules/dist/rules" 4 | ], 5 | "rules": { 6 | "object-curly-spacing": [ 7 | true, 8 | "never" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "./node_modules/tslint/lib/configs/recommended", 4 | "./tslint-extending/tslint-eslint-rules.json", 5 | "./tslint-extending/tslint-consistent-codestyle.json" 6 | ], 7 | "rules": { 8 | "no-shadowed-variable": false, 9 | "max-line-length": [ 10 | true, 11 | 140 12 | ], 13 | "prefer-for-of": true, 14 | "await-promise": true, 15 | "no-floating-promises": true, 16 | "no-empty-interface": false, 17 | "variable-name": [ 18 | true, 19 | "ban-keywords", 20 | "check-format", 21 | "allow-leading-underscore" 22 | ], 23 | "object-literal-sort-keys": false, 24 | "ordered-imports": false, 25 | "member-access": false, 26 | "interface-name": false, 27 | "arrow-parens": [ 28 | true 29 | ] 30 | } 31 | } 32 | --------------------------------------------------------------------------------