From a8b4b78e19c2a7b2c2b8299f8a7334761c46f84b Mon Sep 17 00:00:00 2001 From: mato533 <35281743+mato533@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:26:28 +0900 Subject: [PATCH] feat: initial code (#4) * feat: add initial source code * docs: initial update for readme * test: add configrations for test and source code * chore: add generation logic for the dts file * chore: remove deep-equal * docs: add jsdocs to index.d.ts --- .github/workflows/test.yml | 2 +- README.md | 67 ++++++++++ package.json | 8 +- pnpm-lock.yaml | 181 +++----------------------- rollup.config.ts | 96 +++++++++----- src/__tests__/channel.spec.ts | 41 ++++++ src/__tests__/main.spec.ts | 110 ++++++++++++++++ src/__tests__/preload.spec.ts | 81 ++++++++++++ src/__tests__/types.spec.ts | 232 ++++++++++++++++++++++++++++++++++ src/channel.ts | 63 +++++++++ src/index.ts | 2 + src/main.ts | 107 ++++++++++++++++ src/preload.ts | 114 +++++++++++++++++ src/types/index.d.ts | 33 +++++ vitest.config.ts | 15 +++ 15 files changed, 949 insertions(+), 203 deletions(-) create mode 100644 src/__tests__/channel.spec.ts create mode 100644 src/__tests__/main.spec.ts create mode 100644 src/__tests__/preload.spec.ts create mode 100644 src/__tests__/types.spec.ts create mode 100644 src/channel.ts create mode 100644 src/main.ts create mode 100644 src/preload.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0b8214..ed11fe0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,5 @@ name: Run Test -run-name: 'TEST@${{github.ref_name}}: ${{github.event.head_commit.message}}' +run-name: 'TEST@${{github.ref_name}}' on: workflow_call: diff --git a/README.md b/README.md index 741582a..6f5ed22 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ # electron-context-bridge + Generate api on the bridge across isolated contexts of the electron. + +# Use + +## install + +``` +npm install --save-dev electron-context-bridge +``` + +# Implimentation + +1. create api on main script + + ```ex. main/api.ts + const api = { + hello: (to: string)=>console.log(`hellow ${to}!`), + calc: { + add: (a:number, b:number) => a + b, + minus: (a:number, b:number) => a - b, + } + } as const + + export type IpcBridgeApi = IpcBridgeApiTypeGenerator + + ``` + +1. add handler at main.ts + + ``` + registerIpcHandler(api) + ``` + +1. add invoker at preload.ts + + ``` + const api = await getApiInvoker() + + if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('electron', electronAPI) + contextBridge.exposeInMainWorld('api', api) + } catch (error) { + console.error(error) + } + } else { + window.electron = electronAPI + window.api = api + } + + ``` + +1. add type decolation + + ``` + import type { IpcBridgeApi } from '@main/api' + + import type { ElectronAPI } from '@electron-toolkit/preload' + + declare global { + interface Window { + electron: ElectronAPI + api: IpcBridgeApi + } + } + + ``` diff --git a/package.json b/package.json index 119f636..1cb7382 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "scripts": { "format": "prettier --write .", "lint": "eslint . --fix", + "test": "vitest --run", + "test:watch": "vitest", "coverage": "vitest run --coverage", "typecheck": "tsc --noEmit -p tsconfig.json --composite false", "build": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript" @@ -48,18 +50,16 @@ "license": "MIT", "packageManager": "pnpm@9.12.0", "peerDependencies": { - "electron": "^32.0.0" + "electron": "^32.0.0 || ^33.0.0" }, "devDependencies": { "@eslint/js": "^9.12.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-typescript": "^12.1.0", - "@types/deep-equal": "^1.0.4", "@types/eslint-config-prettier": "^6.11.3", "@types/eslint__js": "^8.42.3", "@types/node": "^22.7.5", "@vitest/coverage-v8": "^2.1.3", - "deep-equal": "^2.2.3", "electron": "^32.2.0", "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", @@ -67,8 +67,8 @@ "eslint-plugin-unicorn": "^56.0.0", "prettier": "^3.3.3", "rollup": "^4.24.0", - "rollup-plugin-copy": "^3.5.0", "rollup-plugin-delete": "^2.1.0", + "rollup-plugin-dts": "^6.1.1", "rollup-plugin-node-externals": "^7.1.3", "tslib": "^2.7.0", "typescript": "^5.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4bc99b..afca51c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@rollup/plugin-typescript': specifier: ^12.1.0 version: 12.1.0(rollup@4.24.0)(tslib@2.7.0)(typescript@5.6.3) - '@types/deep-equal': - specifier: ^1.0.4 - version: 1.0.4 '@types/eslint-config-prettier': specifier: ^6.11.3 version: 6.11.3 @@ -32,9 +29,6 @@ importers: '@vitest/coverage-v8': specifier: ^2.1.3 version: 2.1.3(vitest@2.1.3(@types/node@22.7.5)) - deep-equal: - specifier: ^2.2.3 - version: 2.2.3 electron: specifier: ^32.2.0 version: 32.2.0 @@ -56,12 +50,12 @@ importers: rollup: specifier: ^4.24.0 version: 4.24.0 - rollup-plugin-copy: - specifier: ^3.5.0 - version: 3.5.0 rollup-plugin-delete: specifier: ^2.1.0 version: 2.1.0(rollup@4.24.0) + rollup-plugin-dts: + specifier: ^6.1.1 + version: 6.1.1(rollup@4.24.0)(typescript@5.6.3) rollup-plugin-node-externals: specifier: ^7.1.3 version: 7.1.3(rollup@4.24.0) @@ -471,9 +465,6 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/deep-equal@1.0.4': - resolution: {integrity: sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==} - '@types/eslint-config-prettier@6.11.3': resolution: {integrity: sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==} @@ -486,9 +477,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/fs-extra@8.1.5': - resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} - '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -789,9 +777,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colorette@1.4.0: - resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -839,10 +824,6 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - deep-equal@2.2.3: - resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} - engines: {node: '>= 0.4'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -912,9 +893,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -1164,10 +1142,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@10.0.1: - resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} - engines: {node: '>=8'} - globby@10.0.2: resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} engines: {node: '>=8'} @@ -1255,10 +1229,6 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} - is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -1305,10 +1275,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -1329,18 +1295,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-plain-object@3.0.1: - resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} - engines: {node: '>=0.10.0'} - is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.3: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} engines: {node: '>= 0.4'} @@ -1357,17 +1315,9 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - is-weakset@2.0.3: - resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} - engines: {node: '>= 0.4'} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1535,10 +1485,6 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -1731,16 +1677,19 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} - rollup-plugin-copy@3.5.0: - resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==} - engines: {node: '>=8.3'} - rollup-plugin-delete@2.1.0: resolution: {integrity: sha512-TEbqJd7giLvzQDTu4jSPufwhTJs/iYVN2LfR/YIYkqjC/oZ0/h9Q0AeljifIhzBzJYZtHQTWKEbMms5fbh54pw==} engines: {node: '>=10'} peerDependencies: rollup: '*' + rollup-plugin-dts@6.1.1: + resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} + engines: {node: '>=16'} + peerDependencies: + rollup: ^3.29.4 || ^4 + typescript: ^4.5 || ^5.0 + rollup-plugin-node-externals@7.1.3: resolution: {integrity: sha512-RM+7tJAejAoRsCf93TptTSdqUhRA8S78DleihMiu54Kac+uLkd9VIegLPhGnaW3ehZTXh56+R301mFH6j2A7vw==} engines: {node: '>= 21 || ^20.6.0 || ^18.19.0'} @@ -1839,10 +1788,6 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - stop-iteration-iterator@1.0.0: - resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} - engines: {node: '>= 0.4'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2075,10 +2020,6 @@ packages: which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} @@ -2412,8 +2353,6 @@ snapshots: '@types/node': 22.7.5 '@types/responselike': 1.0.3 - '@types/deep-equal@1.0.4': {} - '@types/eslint-config-prettier@6.11.3': {} '@types/eslint@9.6.1': @@ -2427,10 +2366,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/fs-extra@8.1.5': - dependencies: - '@types/node': 22.7.5 - '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 @@ -2796,8 +2731,6 @@ snapshots: color-name@1.1.4: {} - colorette@1.4.0: {} - concat-map@0.0.1: {} core-js-compat@3.38.1: @@ -2842,27 +2775,6 @@ snapshots: deep-eql@5.0.2: {} - deep-equal@2.2.3: - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - es-get-iterator: 1.1.3 - get-intrinsic: 1.2.4 - is-arguments: 1.1.1 - is-array-buffer: 3.0.4 - is-date-object: 1.0.5 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.3 - isarray: 2.0.5 - object-is: 1.1.6 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.3 - side-channel: 1.0.6 - which-boxed-primitive: 1.0.2 - which-collection: 1.0.2 - which-typed-array: 1.1.15 - deep-is@0.1.4: {} defer-to-connect@2.0.1: {} @@ -2982,18 +2894,6 @@ snapshots: es-errors@1.3.0: {} - es-get-iterator@1.1.3: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - has-symbols: 1.0.3 - is-arguments: 1.1.1 - is-map: 2.0.3 - is-set: 2.0.3 - is-string: 1.0.7 - isarray: 2.0.5 - stop-iteration-iterator: 1.0.0 - es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -3344,17 +3244,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.0.1 - globby@10.0.1: - dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - glob: 7.2.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - globby@10.0.2: dependencies: '@types/glob': 7.2.0 @@ -3445,11 +3334,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 - is-arguments@1.1.1: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -3492,8 +3376,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-map@2.0.3: {} - is-negative-zero@2.0.3: {} is-number-object@1.0.7: @@ -3506,15 +3388,11 @@ snapshots: is-path-inside@3.0.3: {} - is-plain-object@3.0.1: {} - is-regex@1.1.4: dependencies: call-bind: 1.0.7 has-tostringtag: 1.0.2 - is-set@2.0.3: {} - is-shared-array-buffer@1.0.3: dependencies: call-bind: 1.0.7 @@ -3531,17 +3409,10 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-weakmap@2.0.2: {} - is-weakref@1.0.2: dependencies: call-bind: 1.0.7 - is-weakset@2.0.3: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 - isarray@2.0.5: {} isexe@2.0.0: {} @@ -3692,11 +3563,6 @@ snapshots: object-inspect@1.13.2: {} - object-is@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - object-keys@1.1.1: {} object.assign@4.1.5: @@ -3883,19 +3749,19 @@ snapshots: sprintf-js: 1.1.3 optional: true - rollup-plugin-copy@3.5.0: - dependencies: - '@types/fs-extra': 8.1.5 - colorette: 1.4.0 - fs-extra: 8.1.0 - globby: 10.0.1 - is-plain-object: 3.0.1 - rollup-plugin-delete@2.1.0(rollup@4.24.0): dependencies: del: 5.1.0 rollup: 4.24.0 + rollup-plugin-dts@6.1.1(rollup@4.24.0)(typescript@5.6.3): + dependencies: + magic-string: 0.30.11 + rollup: 4.24.0 + typescript: 5.6.3 + optionalDependencies: + '@babel/code-frame': 7.25.7 + rollup-plugin-node-externals@7.1.3(rollup@4.24.0): dependencies: rollup: 4.24.0 @@ -4011,10 +3877,6 @@ snapshots: std-env@3.7.0: {} - stop-iteration-iterator@1.0.0: - dependencies: - internal-slot: 1.0.7 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4265,13 +4127,6 @@ snapshots: is-string: 1.0.7 is-symbol: 1.0.4 - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.3 - which-typed-array@1.1.15: dependencies: available-typed-arrays: 1.0.7 diff --git a/rollup.config.ts b/rollup.config.ts index 98f2ed8..3e85747 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -3,10 +3,10 @@ import { readFileSync } from 'node:fs' import { dirname } from 'node:path' import typescript from '@rollup/plugin-typescript' -import copy from 'rollup-plugin-copy' import del from 'rollup-plugin-delete' import json from '@rollup/plugin-json' import nodeExternals from 'rollup-plugin-node-externals' +import { dts } from 'rollup-plugin-dts' import type { Plugin, WarningHandlerWithDefault } from 'rollup' @@ -18,6 +18,11 @@ const onwarn: WarningHandlerWithDefault = (warning) => { throw Object.assign(new Error(), warning) } +const onwarnGenDts: WarningHandlerWithDefault = (warning) => { + if (warning.code !== 'UNUSED_EXTERNAL_IMPORT') { + console.log(warning.message) + } +} const emitModulePackageFile = (): Plugin => { return { name: 'emit-module-package-file', @@ -31,45 +36,66 @@ const emitModulePackageFile = (): Plugin => { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const defineConfig = (pkg: Record) => { - return { - input: { index: 'src/index.ts', main: 'src/main.ts', preload: 'src/preload.ts' }, - external: Object.keys(pkg.dependencies || {}) - .concat(Object.keys(pkg.peerDependencies || {})) - .concat(builtinModules), - onwarn, - strictDeprecations: true, - output: [ - { - format: 'cjs', - dir: dirname(pkg.main), - exports: 'named', - footer: 'module.exports = Object.assign(exports.default, exports);', - sourcemap: true, - }, - { - format: 'es', - dir: dirname(pkg.module), - plugins: [emitModulePackageFile()], - sourcemap: true, - }, - ], - plugins: [ - typescript({ - sourceMap: true, - }), - copy({ - targets: [{ src: 'src/types/index.d.ts', dest: 'dist/types' }], +let isDeleted = false +const getPlugins = (plugins: Plugin[]): Plugin[] => { + if (!isDeleted) { + isDeleted = true + return plugins.concat([ + del({ + targets: 'dist/*', + runOnce: true, verbose: true, }), - del({ targets: 'dist/*', runOnce: true }), - json(), - nodeExternals(), - ], + ]) + } else { + return plugins } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const defineConfig = (pkg: Record) => { + const external = Object.keys(pkg.dependencies || {}) + .concat(Object.keys(pkg.peerDependencies || {})) + .concat(builtinModules) + return [ + { + input: { index: 'src/index.ts', main: 'src/main.ts', preload: 'src/preload.ts' }, + external, + onwarn, + strictDeprecations: true, + output: [ + { + format: 'cjs', + dir: dirname(pkg.main), + exports: 'named', + footer: 'module.exports = Object.assign(exports.default, exports);', + sourcemap: true, + }, + { + format: 'es', + dir: dirname(pkg.module), + plugins: [emitModulePackageFile()], + sourcemap: true, + }, + ], + plugins: getPlugins([ + typescript({ + sourceMap: true, + }), + json(), + nodeExternals(), + ]), + }, + { + input: 'src/types/index.d.ts', + output: [{ file: pkg.types }], + external, + onwarn: onwarnGenDts, + plugins: getPlugins([dts(), nodeExternals()]), + }, + ] +} + export default defineConfig( JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) ) diff --git a/src/__tests__/channel.spec.ts b/src/__tests__/channel.spec.ts new file mode 100644 index 0000000..aec483f --- /dev/null +++ b/src/__tests__/channel.spec.ts @@ -0,0 +1,41 @@ +import { getApiChannelMap } from '../channel' + +import type { IpcMainInvokeEvent } from 'electron' + +describe('Generate api channel map', () => { + vi.mock('crypto', () => { + return { + randomUUID: vi.fn().mockReturnValue('uuid'), + } + }) + + it('success', () => { + const apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (e: IpcMainInvokeEvent) => console.log(e), + name1: { + fn1: () => console.log(), + fn2: (e: IpcMainInvokeEvent) => console.log(e), + }, + }, + on: { + fn1: () => console.log('sss'), + }, + } as const + const result = getApiChannelMap(apiHandlers) + assert.deepEqual(result, { + invoke: { + fn1: 'uuid', + fn2: 'uuid', + name1: { + fn1: 'uuid', + fn2: 'uuid', + }, + }, + on: { + fn1: 'uuid', + }, + }) + }) +}) diff --git a/src/__tests__/main.spec.ts b/src/__tests__/main.spec.ts new file mode 100644 index 0000000..8eda021 --- /dev/null +++ b/src/__tests__/main.spec.ts @@ -0,0 +1,110 @@ +import { API_CHANNEL_MAP } from '../channel' +import { registerIpcHandler } from '../main' + +import type { BrowserWindow, IpcMainInvokeEvent } from 'electron' + +describe('main', () => { + const mocks = vi.hoisted(() => { + return { + ipcMain: { handle: vi.fn() }, + randomUUID: vi.fn().mockImplementation(() => { + const counter = mocks.randomUUID.mock.calls.length + return `uuid-${counter}` + }), + } + }) + vi.mock('electron', () => { + return { + ipcMain: mocks.ipcMain, + } + }) + + // disable console outputs + vi.spyOn(console, 'debug').mockImplementation(() => {}) + + it('Proper call of ipcMain.handle', () => { + const apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + name1: { + fn1: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + fn2: (e: IpcMainInvokeEvent) => console.log(e), + }, + }, + on: { + fn1: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + }, + } + + registerIpcHandler(apiHandlers) + + expect(mocks.ipcMain.handle.mock.calls.length).toBe(5) + const lastArgs = mocks.ipcMain.handle.mock.calls[0] + expect(lastArgs[0]).toBe(API_CHANNEL_MAP) + + const ipcBridgeApiChannelGetter = lastArgs[1] + const channelMap = ipcBridgeApiChannelGetter() + + expect(mocks.ipcMain.handle).toBeCalled() + expect(mocks.ipcMain.handle).toBeCalledTimes(5) + expect(mocks.ipcMain.handle).toHaveBeenNthCalledWith( + 2, + channelMap.invoke.fn1, + apiHandlers.invoke.fn1 + ) + expect(mocks.ipcMain.handle).toHaveBeenNthCalledWith( + 3, + channelMap.invoke.fn2, + apiHandlers.invoke.fn2 + ) + expect(mocks.ipcMain.handle).toHaveBeenNthCalledWith( + 4, + channelMap.invoke.name1.fn1, + apiHandlers.invoke.name1.fn1 + ) + expect(mocks.ipcMain.handle).toHaveBeenNthCalledWith( + 5, + channelMap.invoke.name1.fn2, + apiHandlers.invoke.name1.fn2 + ) + }) + afterEach(() => { + mocks.ipcMain.handle.mockClear() + }) + + it('sender test', () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + }, + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name1: { + fn2: (arg1: string) => arg1, + fn1: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, + } + + const mockSend = vi.fn() + const browserWindow = { + webContents: { + send: mockSend, + }, + } as unknown as BrowserWindow + const api = registerIpcHandler(_apiHandlers) + + const lastArgs = mocks.ipcMain.handle.mock.calls[0] + expect(lastArgs[0]).toBe(API_CHANNEL_MAP) + + const ipcBridgeApiChannelGetter = lastArgs[1] + const channelMap = ipcBridgeApiChannelGetter() + + api.send.fn2(browserWindow, 1, 2) + expect(mockSend).toBeCalled() + expect(mockSend).toHaveBeenLastCalledWith(channelMap.on.fn2, 3) + }) +}) diff --git a/src/__tests__/preload.spec.ts b/src/__tests__/preload.spec.ts new file mode 100644 index 0000000..ec9f80d --- /dev/null +++ b/src/__tests__/preload.spec.ts @@ -0,0 +1,81 @@ +import { getApiInvoker } from '../preload' +import { API_CHANNEL_MAP } from '../channel' +import { registerIpcHandler } from '../main' + +import type { IpcBridgeApiTypeGenerator } from '../preload' +import type { IpcMainInvokeEvent, IpcRendererEvent } from 'electron' + +describe('preload', () => { + const mocks = vi.hoisted(() => { + return { + ipcMain: { handle: vi.fn() }, + randomUUID: vi.fn().mockImplementation(() => { + const counter = mocks.randomUUID.mock.calls.length + return `uuid-${counter}` + }), + ipcRenderer: { + invoke: vi.fn(), + on: vi.fn(), + }, + } + }) + vi.mock('electron', () => { + return { + ipcMain: mocks.ipcMain, + ipcRenderer: mocks.ipcRenderer, + } + }) + + // disable console outputs + vi.spyOn(console, 'debug').mockImplementation(() => {}) + + it('sender test', async () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent, arg0: string) => `hello ${arg0}`, + }, + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name1: { + fn2: (arg1: string) => arg1, + fn1: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, + } + + const _api = registerIpcHandler(_apiHandlers) + + const lastArgs = mocks.ipcMain.handle.mock.calls[0] + expect(lastArgs[0]).toBe(API_CHANNEL_MAP) + + const ipcBridgeApiChannelGetter = lastArgs[1] + const channelMap = ipcBridgeApiChannelGetter() + mocks.ipcRenderer.invoke.mockImplementation((key: string) => { + if (key === API_CHANNEL_MAP) { + return channelMap + } + }) + const _apiPreload = await getApiInvoker>() + _apiPreload.invoke.fn2('BOB') + expect(mocks.ipcRenderer.invoke).toHaveBeenLastCalledWith(channelMap.invoke.fn2, 'BOB') + + let result: number + const callback = vi + .fn() + .mockImplementation((_e: IpcRendererEvent, value: number) => (result = value)) + _apiPreload.on.fn2(callback) + expect(mocks.ipcRenderer.on).toHaveBeenLastCalledWith( + channelMap.on.fn2, + mocks.ipcRenderer.on.mock.calls[0][1] + ) + // simulate call from main (arg1: event, arg2: return for api) + const expectedvalue = 3 + mocks.ipcRenderer.on.mock.calls[0][1]({}, expectedvalue) + + // test + expect(callback).toHaveBeenCalledOnce() + expect(result).toBe(expectedvalue) + }) +}) diff --git a/src/__tests__/types.spec.ts b/src/__tests__/types.spec.ts new file mode 100644 index 0000000..4c6d146 --- /dev/null +++ b/src/__tests__/types.spec.ts @@ -0,0 +1,232 @@ +import type { BrowserWindow, IpcMainInvokeEvent, IpcRendererEvent } from 'electron' +import type { RemoveEventArg, IpcBridgeApiTypeGenerator, IpcBridgeApiInvoker } from '../preload' +import type { ApiChannelMapGenerator } from '../channel' +import type { IpcBridgeApiSenderTypeGenerator } from '../main' + +describe('Type check', () => { + it('RemoveEventArg', () => { + const _fn = (e: IpcMainInvokeEvent, arg1: string) => arg1 + type ExpectedApiType = (arg1: string) => string + + expectTypeOf().toEqualTypeOf>() + }) + + it('IpcBridgeApiInvoker', () => { + const _apiHandlers = { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + name1: { + fn1: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + fn2: (e: IpcMainInvokeEvent) => console.log(e), + }, + } + + type IpcBridgeApi = IpcBridgeApiInvoker + type ExpectedType = { + fn1: () => void + fn2: () => string + name1: { + fn1: (arg1: string) => string + fn2: () => void + } + } + + expectTypeOf().toEqualTypeOf() + }) + + it('ApiChannelMapGenerator', () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + name1: { + fn1: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + fn2: (e: IpcMainInvokeEvent) => console.log(e), + }, + }, + on: { + fn1: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + }, + } + + type TestTarget = ApiChannelMapGenerator + type ExpectedType = { + invoke: { + fn1: string + fn2: string + name1: { + fn1: string + fn2: string + } + } + on: { + fn1: string + } + } + expectTypeOf().toEqualTypeOf() + }) + + describe('IpcBridgeApiSenderTypeGenerator', () => { + it('invoke and on is existed', () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + }, + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name1: { + fn2: (arg1: string) => arg1, + fn1: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, + } + + type TestTarget = IpcBridgeApiSenderTypeGenerator + type ExpectedType = { + send: { + fn1: (window: BrowserWindow, arg1: string) => void + fn2: (window: BrowserWindow, arg1: number, arg2: number) => void + name1: { + fn1: (window: BrowserWindow, arg1: number, arg2: number) => void + fn2: (window: BrowserWindow, arg1: string) => void + } + } + } + expectTypeOf().toEqualTypeOf() + }) + + it('only invoke is existed', () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + }, + } + + type TestTarget = IpcBridgeApiSenderTypeGenerator + + expectTypeOf().toEqualTypeOf() + }) + + it('only on is existed', () => { + const _apiHandlers = { + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name1: { + fn2: (arg1: string) => arg1, + fn1: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, + } + + type TestTarget = IpcBridgeApiSenderTypeGenerator + type ExpectedType = { + send: { + fn1: (window: BrowserWindow, arg1: string) => void + fn2: (window: BrowserWindow, arg1: number, arg2: number) => void + name1: { + fn1: (window: BrowserWindow, arg1: number, arg2: number) => void + fn2: (window: BrowserWindow, arg1: string) => void + } + } + } + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('IpcBridgeApiTypeGenerator', () => { + it('invoke and on is existed', () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + name1: { + fn1: (_e: IpcMainInvokeEvent, arg1: number, arg2: number) => arg1 + arg2, + fn2: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + }, + }, + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name2: { + fn1: (arg1: boolean) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, + } + type TestTarget = IpcBridgeApiTypeGenerator + type ExpectedType = { + invoke: { + fn1: () => void + fn2: () => string + name1: { + fn1: (arg1: number, arg2: number) => number + fn2: (arg1: string) => string + } + } + on: { + fn1: (callback: (event: IpcRendererEvent, arg1: string) => void) => void + fn2: (callback: (event: IpcRendererEvent, arg1: number) => void) => void + name2: { + fn1: (callback: (event: IpcRendererEvent, arg1: boolean) => void) => void + fn2: (callback: (event: IpcRendererEvent, arg1: number) => void) => void + } + } + } + expectTypeOf().toEqualTypeOf() + }) + + it('only invoke is existed', () => { + const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent) => `hello`, + name1: { + fn1: (_e: IpcMainInvokeEvent, arg1: number, arg2: number) => arg1 + arg2, + fn2: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + }, + }, + } + type TestTarget = IpcBridgeApiTypeGenerator + type ExpectedType = { + invoke: { + fn1: () => void + fn2: () => string + name1: { + fn1: (arg1: number, arg2: number) => number + fn2: (arg1: string) => string + } + } + } + expectTypeOf().toEqualTypeOf() + }) + + it('only on is existed', () => { + const _apiHandlers = { + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name2: { + fn1: (arg1: boolean) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, + } + type TestTarget = IpcBridgeApiTypeGenerator + type ExpectedType = { + on: { + fn1: (callback: (event: IpcRendererEvent, arg1: string) => void) => void + fn2: (callback: (event: IpcRendererEvent, arg1: number) => void) => void + name2: { + fn1: (callback: (event: IpcRendererEvent, arg1: boolean) => void) => void + fn2: (callback: (event: IpcRendererEvent, arg1: number) => void) => void + } + } + } + expectTypeOf().toEqualTypeOf() + }) + }) +}) diff --git a/src/channel.ts b/src/channel.ts new file mode 100644 index 0000000..0bf263f --- /dev/null +++ b/src/channel.ts @@ -0,0 +1,63 @@ +import { randomUUID } from 'node:crypto' + +import type { IpcMainInvokeEvent } from 'electron' + +export const API_CHANNEL_MAP = `06675f7b-d88f-a064-d3ba-6a60dcbc091c` as const + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ApiOnFunction = (...args: any[]) => any + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ApiInvokeFunction = (event: IpcMainInvokeEvent, ...args: any[]) => any + +export type ApiOnHandler = { + readonly [key: string]: ApiOnFunction | ApiOnHandler +} +export type ApiInvokeHandler = { + readonly [key: string]: ApiInvokeFunction | ApiInvokeHandler +} + +export type ApiHandler = ApiInvokeHandler | ApiOnHandler +export type ApiFunction = ApiInvokeFunction | ApiOnFunction + +export type IpcBridgeApiImplementation = { + on?: ApiOnHandler + invoke?: ApiInvokeHandler +} + +type ApiChannelMapItem = { + [key: string]: string | ApiChannelMapItem +} + +type ApiChannelMapItemTypeGenerator = { + [K in keyof T]: T[K] extends ApiFunction + ? string + : T[K] extends ApiHandler + ? ApiChannelMapItemTypeGenerator + : never +} + +export type ApiChannelMapGenerator = { + on: ApiChannelMapItemTypeGenerator + invoke: ApiChannelMapItemTypeGenerator +} + +function getApiChannelMap( + apiHandlers: T +): ApiChannelMapGenerator +function getApiChannelMap(apiHandlers: IpcBridgeApiImplementation) { + const _getApiChannelMap = (apiHandler: ApiHandler) => { + const channelMap: ApiChannelMapItem = {} + Object.keys(apiHandler).forEach((key) => { + if (typeof apiHandler[key] === 'object') { + channelMap[key] = _getApiChannelMap(apiHandler[key]) + } else { + channelMap[key] = randomUUID() + } + }) + return channelMap + } + return _getApiChannelMap(apiHandlers) +} + +export { getApiChannelMap } diff --git a/src/index.ts b/src/index.ts index e69de29..5ddb2be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,2 @@ +export { registerIpcHandler } from './main' +export { getApiInvoker } from './preload' diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..2751517 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,107 @@ +import { ipcMain } from 'electron' + +import { API_CHANNEL_MAP, getApiChannelMap } from './channel' + +import type { BrowserWindow } from 'electron' +import type { + ApiFunction, + IpcBridgeApiImplementation, + ApiHandler, + ApiOnFunction, + ApiOnHandler, +} from './channel' + +const isApiFunction = (value: unknown): value is ApiFunction => { + return typeof value === 'function' ? true : false +} + +const MODE = { + invoke: 0, + on: 1, +} as const +export type ApiMode = (typeof MODE)[keyof typeof MODE] + +type IpcBridgeApiSenderTypeConverter = { + [K in keyof T]: T[K] extends ApiFunction + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + T[K] extends (...args: infer Args) => any + ? (window: BrowserWindow, ...args: Args) => void + : never + : T[K] extends ApiHandler + ? IpcBridgeApiSenderTypeConverter + : never +} + +export type IpcBridgeApiSenderTypeGenerator = + 'on' extends keyof T + ? T['on'] extends undefined + ? undefined + : T['on'] extends ApiOnHandler + ? { + send: IpcBridgeApiSenderTypeConverter + } + : never + : undefined + +function registerIpcHandler( + ipcBridgeApi: T +): IpcBridgeApiSenderTypeGenerator +function registerIpcHandler(ipcBridgeApi: IpcBridgeApiImplementation) { + const channelMap = getApiChannelMap(ipcBridgeApi) + + let mode: ApiMode + const _registerIpcHandler = (api: ApiHandler = ipcBridgeApi, apiInfo = channelMap, level = 0) => { + const keys = Object.keys(apiInfo) + if (level === 0) { + console.debug('IpcBridgeAPI registration is stated.') + } + const sender = {} + keys.forEach((key) => { + if (level === 0) { + mode = MODE[key] + } + const senderKey = level === 0 ? 'send' : key + + if (typeof apiInfo[key] === 'object' && !isApiFunction(api[key])) { + console.debug(`${' '.repeat(level)} - ${key}`) + switch (mode) { + case MODE.invoke: + _registerIpcHandler(api[key], apiInfo[key], level + 1) + break + case MODE.on: + sender[senderKey] = _registerIpcHandler(api[key], apiInfo[key], level + 1) + break + default: + throw new Error(`implimentation error: ${apiInfo[key]}`) + } + } else if (typeof apiInfo[key] !== 'object' && isApiFunction(api[key])) { + console.debug(`${' '.repeat(level)} - ${key} (chanel: ${apiInfo[key]})`) + switch (mode) { + case MODE.invoke: + ipcMain.handle(apiInfo[key], api[key]) + break + case MODE.on: { + const _api = api[key] as ApiOnFunction + sender[senderKey] = (window: BrowserWindow, ...args: Parameters) => { + window.webContents.send(apiInfo[key], _api(...args)) + } + break + } + default: + throw new Error(`implimentation error: ${apiInfo[key]}`) + } + } else { + throw new Error(`implimentation error: ${apiInfo[key]}`) + } + }) + + if (level === 0) { + console.debug(` --> Finish`) + } + return sender + } + ipcMain.handle(API_CHANNEL_MAP, () => channelMap) + return _registerIpcHandler() +} + +export { registerIpcHandler, MODE } diff --git a/src/preload.ts b/src/preload.ts new file mode 100644 index 0000000..c2fec51 --- /dev/null +++ b/src/preload.ts @@ -0,0 +1,114 @@ +import { ipcRenderer } from 'electron' + +import { API_CHANNEL_MAP } from './channel' +import { MODE } from './main' + +import type { ApiMode } from './main' +import type { IpcMainInvokeEvent, IpcRendererEvent } from 'electron' +import type { ApiHandler, IpcBridgeApiImplementation } from './channel' + +type ApiChannelMap = { + [key: string]: string | ApiChannelMap +} + +export type RemoveEventArg = F extends ( + event: IpcMainInvokeEvent, + ...args: infer Args +) => infer R + ? (...args: Args) => R + : never + +export type IpcBridgeApiInvoker = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof T]: T[K] extends (...args: any[]) => any + ? RemoveEventArg + : IpcBridgeApiInvoker +} + +// export type IpcBridgeApiInvoker = IpcBridgeApiInvokerTypeGenerator + +type IpcBridgeApiReciver = { + [K in keyof T]: T[K] extends ApiHandler + ? IpcBridgeApiReciver + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + T[K] extends (...args: any[]) => infer R + ? (callback: (event: IpcRendererEvent, args: R) => void) => void + : never +} + +export type IpcBridgeApiTypeGenerator = keyof T extends never + ? undefined + : 'on' extends keyof T + ? 'invoke' extends keyof T + ? { + invoke: IpcBridgeApiInvoker + on: IpcBridgeApiReciver + } + : { + on: IpcBridgeApiReciver + } + : { + invoke: IpcBridgeApiInvoker + } + +export type IpcContextBridgeApi = + | { + invoke: IpcBridgeApiInvoker + on: IpcBridgeApiReciver + } + | { + on: IpcBridgeApiReciver + } + | { + invoke: IpcBridgeApiInvoker + } + +function getApiInvoker>(): Promise +async function getApiInvoker() { + console.debug('Generation IpcBridgeAPI Invoker is stated.') + const result = await ipcRenderer.invoke(API_CHANNEL_MAP) + if (!result) { + console.debug(` --> Faild to get mapping for api and channel `) + throw new Error(`'electron-context-bridge' is not working correctly`) + } + + let mode: ApiMode + const _getApiInvoker = (apiChannelMap: ApiChannelMap, level = 0) => { + const apiInvoker = {} + Object.keys(apiChannelMap).forEach((key) => { + if (level === 0) { + mode = MODE[key] + } + + const value = apiChannelMap[key] + if (typeof value === 'object') { + console.debug(`${' '.repeat(level)} - ${key}`) + apiInvoker[key] = _getApiInvoker(value, level + 1) + } else { + console.debug(`${' '.repeat(level)} - ${key} (chanel: ${value})`) + switch (mode) { + case MODE.invoke: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apiInvoker[key] = (...args: any[]) => { + return ipcRenderer.invoke(value, ...args) + } + break + case MODE.on: + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apiInvoker[key] = (callback: (event: IpcRendererEvent, arg0: any) => void) => + ipcRenderer.on(value, (event, value) => callback(event, value)) + break + default: + throw new Error(`implimentation error: ${value}`) + } + } + }) + if (level === 0) { + console.debug(` --> Finish`) + } + return apiInvoker + } + return _getApiInvoker(result) +} + +export { getApiInvoker } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index e69de29..4bdd286 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -0,0 +1,33 @@ +import type { IpcContextBridgeApi } from '../preload' +import type { IpcBridgeApiSenderTypeGenerator } from '../main' +import type { IpcBridgeApiImplementation } from '../channel' + +/** + * Type generator for api that will be exposed to renderer process + * Use at the preload sctipt + */ +export type { IpcBridgeApiTypeGenerator } from '../preload' + +/** + * Type generator for api to send message from main to renderer + * Use at the main process + */ +export type { IpcBridgeApiSenderTypeGenerator } from '../main' + +/** + * Resister IPC handler(for tow way) + * and generate api for send message to renderer process + * Use at the main process + * @param ipcBridgeApi Implementation for IPC api handlers + */ +export function registerIpcHandler( + ipcBridgeApi: T +): IpcBridgeApiSenderTypeGenerator + +/** + * Generate IPC api that will be exposed to renderer process. + * Use at the preload sctipt + */ +export function getApiInvoker< + T extends IpcContextBridgeApi, +>(): Promise diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..efaa311 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import path from 'node:path' + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + reporters: 'verbose', + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +})