├── .changeset ├── README.md └── config.json ├── .envrc ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ ├── test.yml │ └── www.yml ├── .gitignore ├── .node_version ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── basic-react-superjson │ ├── CHANGELOG.md │ ├── electron │ │ ├── api.ts │ │ └── index.ts │ ├── index.html │ ├── package.json │ ├── preload │ │ └── preload.ts │ ├── src │ │ └── index.tsx │ ├── tsconfig.json │ └── vite.config.ts └── basic-react │ ├── CHANGELOG.md │ ├── electron │ ├── api.ts │ └── index.ts │ ├── index.e2e.ts │ ├── index.html │ ├── package.json │ ├── preload │ └── preload.ts │ ├── src │ └── index.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages └── electron-trpc │ ├── CHANGELOG.md │ ├── dts-bundle-generator.config.ts │ ├── package.json │ ├── src │ ├── constants.ts │ ├── main │ │ ├── __tests__ │ │ │ ├── handleIPCMessage.test.ts │ │ │ └── utils.test.ts │ │ ├── createIPCHandler.ts │ │ ├── exposeElectronTRPC.ts │ │ ├── handleIPCMessage.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── vite.config.ts │ ├── renderer │ │ ├── __tests__ │ │ │ └── ipcLink.test.ts │ │ ├── index.ts │ │ ├── ipcLink.ts │ │ ├── tsconfig.json │ │ ├── utils.ts │ │ └── vite.config.ts │ └── types.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── shell.nix └── www ├── astro.config.mjs ├── package.json ├── src ├── content │ ├── config.ts │ └── docs │ │ ├── getting-started.mdx │ │ └── index.mdx └── env.d.ts ├── tsconfig.json └── wrangler.toml /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "jsonnull/electron-trpc" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch" 10 | } 11 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Run lints 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | env: 11 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 12 | 13 | jobs: 14 | lint: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | node: ['18.x'] 19 | os: [ubuntu-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: pnpm/action-setup@v2 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node }} 27 | cache: 'pnpm' 28 | - run: pnpm install --frozen-lockfile 29 | - run: pnpm lint 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | env: 9 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v2 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: '18.x' 21 | cache: 'pnpm' 22 | - run: pnpm install --frozen-lockfile 23 | - uses: changesets/action@v1 24 | with: 25 | publish: pnpm release 26 | version: pnpm changeset:version 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push] 4 | 5 | env: 6 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | node: ['18.x'] 14 | os: [ubuntu-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: pnpm/action-setup@v2 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node }} 22 | cache: 'pnpm' 23 | - run: pnpm install --frozen-lockfile 24 | - run: pnpm build 25 | - run: pnpm test:ci 26 | - uses: codecov/codecov-action@v4 27 | 28 | test-e2e: 29 | runs-on: ${{ matrix.os }} 30 | strategy: 31 | matrix: 32 | node: ['18.x'] 33 | os: [ubuntu-latest] 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: pnpm/action-setup@v2 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ matrix.node }} 41 | cache: 'pnpm' 42 | - run: pnpm install --frozen-lockfile 43 | - run: pnpm build 44 | - run: xvfb-run --auto-servernum -- pnpm test:e2e 45 | -------------------------------------------------------------------------------- /.github/workflows/www.yml: -------------------------------------------------------------------------------- 1 | name: Deploy site 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | node: ['18.x'] 17 | os: [ubuntu-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: pnpm/action-setup@v2 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node }} 25 | cache: 'pnpm' 26 | - run: pnpm install --frozen-lockfile 27 | - run: pnpm docs:build 28 | - run: pnpm docs:publish 29 | env: 30 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} 31 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .astro 2 | node_modules 3 | dist/ 4 | dist-electron/ 5 | docs/.vitepress/cache/ 6 | coverage/ 7 | packages/electron-trpc/renderer.d.ts 8 | packages/electron-trpc/main.d.ts 9 | -------------------------------------------------------------------------------- /.node_version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | link-workspace-packages=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | **/dist-electron/ 3 | **/coverage/ 4 | 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | singleQuote: true, 5 | printWidth: 100, 6 | overrides: [ 7 | { 8 | files: 'docs/index.md', 9 | options: { 10 | printWidth: 60, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to electron-trpc 2 | 3 | We welcome contributions from the community. This guide assumes you're familiar with Git, GitHub, and pnpm. 4 | 5 | ## Quick Start 6 | 7 | 1. Fork and clone the repository 8 | 2. Install dependencies: `pnpm install` 9 | 3. Create a new branch for your feature 10 | 4. Make changes and commit 11 | 5. Run tests: `pnpm test` 12 | 6. Create a changeset: `pnpm changeset` 13 | 7. Push to your fork and submit a pull request 14 | 15 | ## Monorepo Structure 16 | 17 | This project uses a pnpm monorepo structure. The main directories are: 18 | 19 | - `examples/`: Contains examples of various use-cases and serves as the basis for e2e tests 20 | - `packages/`: Houses the main electron-trpc package 21 | 22 | ## Scripts 23 | 24 | These are the most likely scripts you'll want to use during development: 25 | 26 | - `pnpm install`: Install dependencies for all packages 27 | - `pnpm test`: Run tests across all packages 28 | - `pnpm test:e2e`: Run end-to-end tests 29 | - `pnpm build`: Build all packages 30 | - `pnpm changeset`: Create a changeset for your changes 31 | 32 | ## Reporting Issues 33 | 34 | Open an issue on GitHub for bugs or suggestions. 35 | 36 | ## License 37 | 38 | By contributing, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE) file. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jason Nall 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-trpc 2 | 3 |

4 | 5 | NPM 6 | 7 | 8 | 9 | 10 | 11 | MIT 12 | 13 |

14 | 15 |

16 | 17 | **Build IPC for Electron with tRPC** 18 | 19 | - Expose APIs from Electron's main process to one or more render processes. 20 | - Build fully type-safe IPC. 21 | - Secure alternative to opening servers on localhost. 22 | - Full support for queries, mutations, and subscriptions. 23 | 24 | ## Installation 25 | 26 | ```sh 27 | # Using pnpm 28 | pnpm add electron-trpc 29 | 30 | # Using yarn 31 | yarn add electron-trpc 32 | 33 | # Using npm 34 | npm install --save electron-trpc 35 | ``` 36 | 37 | ## Basic Setup 38 | 39 | 1. Add your tRPC router to the Electron main process using `createIPCHandler`: 40 | 41 | ```ts 42 | import { app } from 'electron'; 43 | import { createIPCHandler } from 'electron-trpc/main'; 44 | import { router } from './api'; 45 | 46 | app.on('ready', () => { 47 | const win = new BrowserWindow({ 48 | webPreferences: { 49 | // Replace this path with the path to your preload file (see next step) 50 | preload: 'path/to/preload.js', 51 | }, 52 | }); 53 | 54 | createIPCHandler({ router, windows: [win] }); 55 | }); 56 | ``` 57 | 58 | 2. Expose the IPC to the render process from the [preload file](https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts): 59 | 60 | ```ts 61 | import { exposeElectronTRPC } from 'electron-trpc/main'; 62 | 63 | process.once('loaded', async () => { 64 | exposeElectronTRPC(); 65 | }); 66 | ``` 67 | 68 | > Note: `electron-trpc` depends on `contextIsolation` being enabled, which is the default. 69 | 70 | 3. When creating the client in the render process, use the `ipcLink` (instead of the HTTP or batch HTTP links): 71 | 72 | ```ts 73 | import { createTRPCProxyClient } from '@trpc/client'; 74 | import { ipcLink } from 'electron-trpc/renderer'; 75 | 76 | export const client = createTRPCProxyClient({ 77 | links: [ipcLink()], 78 | }); 79 | ``` 80 | 81 | 4. Now you can use the client in your render process as you normally would (e.g. using `@trpc/react`). 82 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # examples/basic 2 | 3 | ## 0.0.19 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`1c5caaa`](https://github.com/jsonnull/electron-trpc/commit/1c5caaa0e58cee2a8324b27cdf5c793a312f844b)]: 8 | - electron-trpc@0.7.1 9 | 10 | ## 0.0.18 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`95525ca`](https://github.com/jsonnull/electron-trpc/commit/95525ca1c2a28a0d657ec5063eb33a8dde32a289)]: 15 | - electron-trpc@0.7.0 16 | 17 | ## 0.0.17 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`f5f3ef3`](https://github.com/jsonnull/electron-trpc/commit/f5f3ef3b2ffd70d3daa899aaa0bb1745ce267951)]: 22 | - electron-trpc@0.6.2 23 | 24 | ## 0.0.16 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`fb7845f`](https://github.com/jsonnull/electron-trpc/commit/fb7845fbc771002309dea9d8b4c2079860350656), [`2bc0233`](https://github.com/jsonnull/electron-trpc/commit/2bc02333172b8a25a493c34c8e17434b8ffb4eea)]: 29 | - electron-trpc@0.6.1 30 | 31 | ## 0.0.15 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`a15c6c4d0c531b3596689b4cc470548a5228c989`](https://github.com/jsonnull/electron-trpc/commit/a15c6c4d0c531b3596689b4cc470548a5228c989), [`50953c7e5bcb69d4e5482405f4a621b229f0ca82`](https://github.com/jsonnull/electron-trpc/commit/50953c7e5bcb69d4e5482405f4a621b229f0ca82)]: 36 | - electron-trpc@0.6.0 37 | 38 | ## 0.0.14 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`0e72fe9`](https://github.com/jsonnull/electron-trpc/commit/0e72fe93b7605636b80cb3b3e47b6992cb4c097a), [`0e72fe9`](https://github.com/jsonnull/electron-trpc/commit/0e72fe93b7605636b80cb3b3e47b6992cb4c097a)]: 43 | - electron-trpc@0.5.2 44 | 45 | ## 0.0.13 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`c43ae93`](https://github.com/jsonnull/electron-trpc/commit/c43ae93df4af397986c602c432fc32178d62796b)]: 50 | - electron-trpc@0.5.1 51 | 52 | ## 0.0.12 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies [[`68ddf63`](https://github.com/jsonnull/electron-trpc/commit/68ddf63ff6b3560626bf78d45ca2bf7ed2851f22)]: 57 | - electron-trpc@0.5.0 58 | 59 | ## 0.0.11 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [[`70d13e4`](https://github.com/jsonnull/electron-trpc/commit/70d13e400d8b0678a359c633511b419736ef4b5d)]: 64 | - electron-trpc@0.4.5 65 | 66 | ## 0.0.10 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies [[`84d1139`](https://github.com/jsonnull/electron-trpc/commit/84d1139d6b6970b8863fdb1ba22a0aaa709045ec)]: 71 | - electron-trpc@0.4.4 72 | 73 | ## 0.0.9 74 | 75 | ### Patch Changes 76 | 77 | - Updated dependencies [[`42abc41`](https://github.com/jsonnull/electron-trpc/commit/42abc4182c260580e320e8ec61926ed3ad372940)]: 78 | - electron-trpc@0.4.3 79 | 80 | ## 0.0.8 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies [[`cbae157`](https://github.com/jsonnull/electron-trpc/commit/cbae1570ddeab2405950806656c0d4fc19d72855)]: 85 | - electron-trpc@0.4.2 86 | 87 | ## 0.0.7 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies [[`b73c1a8`](https://github.com/jsonnull/electron-trpc/commit/b73c1a89c77258bf4372991fda563d6fa0ba299f)]: 92 | - electron-trpc@0.4.1 93 | 94 | ## 0.0.6 95 | 96 | ### Patch Changes 97 | 98 | - Updated dependencies [[`70e8e5c`](https://github.com/jsonnull/electron-trpc/commit/70e8e5c5f3e2654d055663a286c4107a66f362e7)]: 99 | - electron-trpc@0.4.0 100 | 101 | ## 0.0.5 102 | 103 | ### Patch Changes 104 | 105 | - Updated dependencies [[`46d79ef`](https://github.com/jsonnull/electron-trpc/commit/46d79efde7ccc12cd1e99eb086413aa83bda29f8)]: 106 | - electron-trpc@0.3.2 107 | 108 | ## 0.0.4 109 | 110 | ### Patch Changes 111 | 112 | - Updated dependencies [[`25b6c5a`](https://github.com/jsonnull/electron-trpc/commit/25b6c5a5cb56a93a4facf7345a10c3bb2db37730), [`25b6c5a`](https://github.com/jsonnull/electron-trpc/commit/25b6c5a5cb56a93a4facf7345a10c3bb2db37730), [`25b6c5a`](https://github.com/jsonnull/electron-trpc/commit/25b6c5a5cb56a93a4facf7345a10c3bb2db37730)]: 113 | - electron-trpc@0.3.1 114 | 115 | ## 0.0.3 116 | 117 | ### Patch Changes 118 | 119 | - Updated dependencies [[`b67f2a7`](https://github.com/jsonnull/electron-trpc/commit/b67f2a7a87cd77b88d337e6996d78c6507a9c187)]: 120 | - electron-trpc@0.3.0 121 | 122 | ## 0.0.2 123 | 124 | ### Patch Changes 125 | 126 | - Updated dependencies [[`c9031f5`](https://github.com/jsonnull/electron-trpc/commit/c9031f5b521095d3c648fc905b642471e875d86f)]: 127 | - electron-trpc@0.2.1 128 | 129 | ## 0.0.1 130 | 131 | ### Patch Changes 132 | 133 | - Updated dependencies [[`231afea`](https://github.com/jsonnull/electron-trpc/commit/231afea9f21f0d4ba7f12c37fd781f22ca5d4141), [`960999f`](https://github.com/jsonnull/electron-trpc/commit/960999f5c2fec8b70152cfdf6cadc737c60edd48), [`3c76498`](https://github.com/jsonnull/electron-trpc/commit/3c76498c152e92fe1b084d3e7a5170d8f2c1dee3), [`7c7ee89`](https://github.com/jsonnull/electron-trpc/commit/7c7ee89b45c6c27527e26b0a6100fc0cb41d8ba6), [`ddc11cb`](https://github.com/jsonnull/electron-trpc/commit/ddc11cb1f1502568a028476acdefdb8d95d9562c), [`4615cf6`](https://github.com/jsonnull/electron-trpc/commit/4615cf63c382a0ea21781efb5093a531cc6378e6), [`006d01e`](https://github.com/jsonnull/electron-trpc/commit/006d01e73a995f756be622769192444bba3b4a87), [`c46f700`](https://github.com/jsonnull/electron-trpc/commit/c46f700b6171835a5b00d6d2c44061acdcd49874), [`42f2b09`](https://github.com/jsonnull/electron-trpc/commit/42f2b09efbaf322af42df176b74f72b972724f99), [`d2870a4`](https://github.com/jsonnull/electron-trpc/commit/d2870a4ef4429053c6a0d3e44bb204d0177adda9)]: 134 | - electron-trpc@0.2.0 135 | 136 | ## 0.0.1-next.2 137 | 138 | ### Patch Changes 139 | 140 | - Updated dependencies [[`169c47f`](https://github.com/jsonnull/electron-trpc/commit/169c47f325de8899784187af06140c29758b0c0a)]: 141 | - electron-trpc@0.2.0-next.7 142 | 143 | ## 0.0.1-next.1 144 | 145 | ### Patch Changes 146 | 147 | - Updated dependencies [[`a2103c4`](https://github.com/jsonnull/electron-trpc/commit/a2103c4e9789741aa98aa057fcebf78e4f339d9b)]: 148 | - electron-trpc@0.2.0-next.6 149 | 150 | ## 0.0.1-next.0 151 | 152 | ### Patch Changes 153 | 154 | - Updated dependencies [[`333197f`](https://github.com/jsonnull/electron-trpc/commit/333197fb3e567aa37f350af992d123f8f8ed6796)]: 155 | - electron-trpc@0.2.0-next.5 156 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/electron/api.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import { initTRPC } from '@trpc/server'; 3 | import { observable } from '@trpc/server/observable'; 4 | import { EventEmitter } from 'events'; 5 | import superjson from 'superjson'; 6 | 7 | const ee = new EventEmitter(); 8 | 9 | const t = initTRPC.create({ isServer: true, transformer: superjson }); 10 | 11 | export const router = t.router({ 12 | greeting: t.procedure.input(z.object({ name: z.string() })).query((req) => { 13 | const { input } = req; 14 | 15 | ee.emit('greeting', `Greeted ${input.name}`); 16 | return { 17 | text: `Hello ${input.name}` as const, 18 | }; 19 | }), 20 | subscription: t.procedure.subscription(() => { 21 | return observable((emit) => { 22 | function onGreet(text: string) { 23 | emit.next({ text }); 24 | } 25 | 26 | ee.on('greeting', onGreet); 27 | 28 | return () => { 29 | ee.off('greeting', onGreet); 30 | }; 31 | }); 32 | }), 33 | }); 34 | 35 | export type AppRouter = typeof router; 36 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/electron/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, BrowserWindow } from 'electron'; 3 | import { createIPCHandler } from 'electron-trpc/main'; 4 | import { router } from './api'; 5 | 6 | process.env.DIST = path.join(__dirname, '../dist'); 7 | process.env.PUBLIC = app.isPackaged ? process.env.DIST : path.join(process.env.DIST, '../public'); 8 | 9 | const preload = path.join(__dirname, './preload.js'); 10 | const url = process.env['VITE_DEV_SERVER_URL']; 11 | 12 | app.on('ready', () => { 13 | const win = new BrowserWindow({ 14 | webPreferences: { 15 | preload, 16 | }, 17 | }); 18 | 19 | createIPCHandler({ router, windows: [win] }); 20 | 21 | if (url) { 22 | win.loadURL(url); 23 | } else { 24 | win.loadFile(path.join(process.env.DIST, 'index.html')); 25 | } 26 | 27 | win.show(); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello from Electron renderer! 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples/basic-react-superjson", 3 | "version": "0.0.19", 4 | "private": true, 5 | "main": "dist-electron/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "vite", 9 | "build": "vite build" 10 | }, 11 | "dependencies": { 12 | "@tanstack/react-query": "^4.29.14", 13 | "@trpc/client": "10.33.1", 14 | "@trpc/react-query": "10.33.1", 15 | "@trpc/server": "10.33.1", 16 | "electron": "29.3.2", 17 | "electron-trpc": "0.7.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "superjson": "^1.12.3", 21 | "zod": "^3.21.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20.12.8", 25 | "@types/react": "^18.3.1", 26 | "@types/react-dom": "^18.3.0", 27 | "@vitejs/plugin-react": "^4.2.1", 28 | "vite": "^5.2.11", 29 | "vite-plugin-electron": "^0.28.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/preload/preload.ts: -------------------------------------------------------------------------------- 1 | import { exposeElectronTRPC } from 'electron-trpc/main'; 2 | 3 | process.once('loaded', async () => { 4 | exposeElectronTRPC(); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import { ipcLink } from 'electron-trpc/renderer'; 4 | import superjson from 'superjson'; 5 | import { createTRPCReact } from '@trpc/react-query'; 6 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 7 | import type { AppRouter } from '../electron/api'; 8 | 9 | const trpcReact = createTRPCReact(); 10 | 11 | function App() { 12 | const [queryClient] = useState(() => new QueryClient()); 13 | const [trpcClient] = useState(() => 14 | trpcReact.createClient({ 15 | links: [ipcLink()], 16 | transformer: superjson, 17 | }) 18 | ); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | function HelloElectron() { 30 | const { data } = trpcReact.greeting.useQuery({ name: 'Electron' }); 31 | trpcReact.subscription.useSubscription(undefined, { 32 | onData: (data) => { 33 | console.log(data); 34 | }, 35 | }); 36 | 37 | if (!data) { 38 | return null; 39 | } 40 | 41 | return
{data.text}
; 42 | } 43 | 44 | ReactDom.render(, document.getElementById('react-root')); 45 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "jsx": "react", 7 | "lib": ["dom", "esnext"], 8 | "module": "esnext", 9 | "moduleResolution": "node16", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "useDefineForClassFields": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "esnext" 19 | }, 20 | "include": ["./*.ts", "./*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/basic-react-superjson/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import electron from 'vite-plugin-electron'; 4 | 5 | export default defineConfig({ 6 | mode: 'development', 7 | plugins: [ 8 | react(), 9 | electron([ 10 | { 11 | entry: 'electron/index.ts', 12 | }, 13 | { 14 | entry: 'preload/preload.ts', 15 | onstart(options) { 16 | options.reload(); 17 | }, 18 | }, 19 | ]), 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /examples/basic-react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # examples/basic 2 | 3 | ## 0.0.19 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`1c5caaa`](https://github.com/jsonnull/electron-trpc/commit/1c5caaa0e58cee2a8324b27cdf5c793a312f844b)]: 8 | - electron-trpc@0.7.1 9 | 10 | ## 0.0.18 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`95525ca`](https://github.com/jsonnull/electron-trpc/commit/95525ca1c2a28a0d657ec5063eb33a8dde32a289)]: 15 | - electron-trpc@0.7.0 16 | 17 | ## 0.0.17 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`f5f3ef3`](https://github.com/jsonnull/electron-trpc/commit/f5f3ef3b2ffd70d3daa899aaa0bb1745ce267951)]: 22 | - electron-trpc@0.6.2 23 | 24 | ## 0.0.16 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`fb7845f`](https://github.com/jsonnull/electron-trpc/commit/fb7845fbc771002309dea9d8b4c2079860350656), [`2bc0233`](https://github.com/jsonnull/electron-trpc/commit/2bc02333172b8a25a493c34c8e17434b8ffb4eea)]: 29 | - electron-trpc@0.6.1 30 | 31 | ## 0.0.15 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`a15c6c4d0c531b3596689b4cc470548a5228c989`](https://github.com/jsonnull/electron-trpc/commit/a15c6c4d0c531b3596689b4cc470548a5228c989), [`50953c7e5bcb69d4e5482405f4a621b229f0ca82`](https://github.com/jsonnull/electron-trpc/commit/50953c7e5bcb69d4e5482405f4a621b229f0ca82)]: 36 | - electron-trpc@0.6.0 37 | 38 | ## 0.0.14 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`0e72fe9`](https://github.com/jsonnull/electron-trpc/commit/0e72fe93b7605636b80cb3b3e47b6992cb4c097a), [`0e72fe9`](https://github.com/jsonnull/electron-trpc/commit/0e72fe93b7605636b80cb3b3e47b6992cb4c097a)]: 43 | - electron-trpc@0.5.2 44 | 45 | ## 0.0.13 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`c43ae93`](https://github.com/jsonnull/electron-trpc/commit/c43ae93df4af397986c602c432fc32178d62796b)]: 50 | - electron-trpc@0.5.1 51 | 52 | ## 0.0.12 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies [[`68ddf63`](https://github.com/jsonnull/electron-trpc/commit/68ddf63ff6b3560626bf78d45ca2bf7ed2851f22)]: 57 | - electron-trpc@0.5.0 58 | 59 | ## 0.0.11 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [[`70d13e4`](https://github.com/jsonnull/electron-trpc/commit/70d13e400d8b0678a359c633511b419736ef4b5d)]: 64 | - electron-trpc@0.4.5 65 | 66 | ## 0.0.10 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies [[`84d1139`](https://github.com/jsonnull/electron-trpc/commit/84d1139d6b6970b8863fdb1ba22a0aaa709045ec)]: 71 | - electron-trpc@0.4.4 72 | 73 | ## 0.0.9 74 | 75 | ### Patch Changes 76 | 77 | - Updated dependencies [[`42abc41`](https://github.com/jsonnull/electron-trpc/commit/42abc4182c260580e320e8ec61926ed3ad372940)]: 78 | - electron-trpc@0.4.3 79 | 80 | ## 0.0.8 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies [[`cbae157`](https://github.com/jsonnull/electron-trpc/commit/cbae1570ddeab2405950806656c0d4fc19d72855)]: 85 | - electron-trpc@0.4.2 86 | 87 | ## 0.0.7 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies [[`b73c1a8`](https://github.com/jsonnull/electron-trpc/commit/b73c1a89c77258bf4372991fda563d6fa0ba299f)]: 92 | - electron-trpc@0.4.1 93 | 94 | ## 0.0.6 95 | 96 | ### Patch Changes 97 | 98 | - Updated dependencies [[`70e8e5c`](https://github.com/jsonnull/electron-trpc/commit/70e8e5c5f3e2654d055663a286c4107a66f362e7)]: 99 | - electron-trpc@0.4.0 100 | 101 | ## 0.0.5 102 | 103 | ### Patch Changes 104 | 105 | - Updated dependencies [[`46d79ef`](https://github.com/jsonnull/electron-trpc/commit/46d79efde7ccc12cd1e99eb086413aa83bda29f8)]: 106 | - electron-trpc@0.3.2 107 | 108 | ## 0.0.4 109 | 110 | ### Patch Changes 111 | 112 | - Updated dependencies [[`25b6c5a`](https://github.com/jsonnull/electron-trpc/commit/25b6c5a5cb56a93a4facf7345a10c3bb2db37730), [`25b6c5a`](https://github.com/jsonnull/electron-trpc/commit/25b6c5a5cb56a93a4facf7345a10c3bb2db37730), [`25b6c5a`](https://github.com/jsonnull/electron-trpc/commit/25b6c5a5cb56a93a4facf7345a10c3bb2db37730)]: 113 | - electron-trpc@0.3.1 114 | 115 | ## 0.0.3 116 | 117 | ### Patch Changes 118 | 119 | - Updated dependencies [[`b67f2a7`](https://github.com/jsonnull/electron-trpc/commit/b67f2a7a87cd77b88d337e6996d78c6507a9c187)]: 120 | - electron-trpc@0.3.0 121 | 122 | ## 0.0.2 123 | 124 | ### Patch Changes 125 | 126 | - Updated dependencies [[`c9031f5`](https://github.com/jsonnull/electron-trpc/commit/c9031f5b521095d3c648fc905b642471e875d86f)]: 127 | - electron-trpc@0.2.1 128 | 129 | ## 0.0.1 130 | 131 | ### Patch Changes 132 | 133 | - Updated dependencies [[`231afea`](https://github.com/jsonnull/electron-trpc/commit/231afea9f21f0d4ba7f12c37fd781f22ca5d4141), [`960999f`](https://github.com/jsonnull/electron-trpc/commit/960999f5c2fec8b70152cfdf6cadc737c60edd48), [`3c76498`](https://github.com/jsonnull/electron-trpc/commit/3c76498c152e92fe1b084d3e7a5170d8f2c1dee3), [`7c7ee89`](https://github.com/jsonnull/electron-trpc/commit/7c7ee89b45c6c27527e26b0a6100fc0cb41d8ba6), [`ddc11cb`](https://github.com/jsonnull/electron-trpc/commit/ddc11cb1f1502568a028476acdefdb8d95d9562c), [`4615cf6`](https://github.com/jsonnull/electron-trpc/commit/4615cf63c382a0ea21781efb5093a531cc6378e6), [`006d01e`](https://github.com/jsonnull/electron-trpc/commit/006d01e73a995f756be622769192444bba3b4a87), [`c46f700`](https://github.com/jsonnull/electron-trpc/commit/c46f700b6171835a5b00d6d2c44061acdcd49874), [`42f2b09`](https://github.com/jsonnull/electron-trpc/commit/42f2b09efbaf322af42df176b74f72b972724f99), [`d2870a4`](https://github.com/jsonnull/electron-trpc/commit/d2870a4ef4429053c6a0d3e44bb204d0177adda9)]: 134 | - electron-trpc@0.2.0 135 | 136 | ## 0.0.1-next.2 137 | 138 | ### Patch Changes 139 | 140 | - Updated dependencies [[`169c47f`](https://github.com/jsonnull/electron-trpc/commit/169c47f325de8899784187af06140c29758b0c0a)]: 141 | - electron-trpc@0.2.0-next.7 142 | 143 | ## 0.0.1-next.1 144 | 145 | ### Patch Changes 146 | 147 | - Updated dependencies [[`a2103c4`](https://github.com/jsonnull/electron-trpc/commit/a2103c4e9789741aa98aa057fcebf78e4f339d9b)]: 148 | - electron-trpc@0.2.0-next.6 149 | 150 | ## 0.0.1-next.0 151 | 152 | ### Patch Changes 153 | 154 | - Updated dependencies [[`333197f`](https://github.com/jsonnull/electron-trpc/commit/333197fb3e567aa37f350af992d123f8f8ed6796)]: 155 | - electron-trpc@0.2.0-next.5 156 | -------------------------------------------------------------------------------- /examples/basic-react/electron/api.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import { initTRPC } from '@trpc/server'; 3 | import { observable } from '@trpc/server/observable'; 4 | import { EventEmitter } from 'events'; 5 | 6 | const ee = new EventEmitter(); 7 | 8 | const t = initTRPC.create({ isServer: true }); 9 | 10 | export const router = t.router({ 11 | greeting: t.procedure.input(z.object({ name: z.string() })).query((req) => { 12 | const { input } = req; 13 | 14 | ee.emit('greeting', `Greeted ${input.name}`); 15 | return { 16 | text: `Hello ${input.name}` as const, 17 | }; 18 | }), 19 | subscription: t.procedure.subscription(() => { 20 | return observable((emit) => { 21 | function onGreet(text: string) { 22 | emit.next({ text }); 23 | } 24 | 25 | ee.on('greeting', onGreet); 26 | 27 | return () => { 28 | ee.off('greeting', onGreet); 29 | }; 30 | }); 31 | }), 32 | }); 33 | 34 | export type AppRouter = typeof router; 35 | -------------------------------------------------------------------------------- /examples/basic-react/electron/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { app, BrowserWindow } from 'electron'; 3 | import { createIPCHandler } from 'electron-trpc/main'; 4 | import { router } from './api'; 5 | 6 | process.env.DIST = path.join(__dirname, '../dist'); 7 | process.env.PUBLIC = app.isPackaged ? process.env.DIST : path.join(process.env.DIST, '../public'); 8 | 9 | const preload = path.join(__dirname, './preload.js'); 10 | const url = process.env['VITE_DEV_SERVER_URL']; 11 | 12 | app.on('ready', () => { 13 | const win = new BrowserWindow({ 14 | webPreferences: { 15 | preload, 16 | }, 17 | }); 18 | 19 | createIPCHandler({ router, windows: [win] }); 20 | 21 | if (url) { 22 | win.loadURL(url); 23 | } else { 24 | win.loadFile(path.join(process.env.DIST, 'index.html')); 25 | } 26 | 27 | win.show(); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/basic-react/index.e2e.ts: -------------------------------------------------------------------------------- 1 | import { _electron as electron, test, expect } from '@playwright/test'; 2 | 3 | test('Hello Electron', async () => { 4 | const electronApp = await electron.launch({ 5 | args: [`${__dirname}`], 6 | executablePath: process.env.PLAYWRIGHT_ELECTRON_PATH ?? undefined, 7 | }); 8 | 9 | const window = await electronApp.firstWindow(); 10 | expect(await window.title()).toBe('Hello from Electron renderer!'); 11 | 12 | const response = await window.textContent('[data-testid="greeting"]'); 13 | expect(response).toBe('Hello Electron'); 14 | 15 | await electronApp.close(); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/basic-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello from Electron renderer! 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/basic-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples/basic-react", 3 | "version": "0.0.19", 4 | "private": true, 5 | "main": "dist-electron/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "vite", 9 | "build": "vite build" 10 | }, 11 | "dependencies": { 12 | "@tanstack/react-query": "^4.29.14", 13 | "@trpc/client": "10.33.1", 14 | "@trpc/react-query": "10.33.1", 15 | "@trpc/server": "10.33.1", 16 | "electron": "29.3.2", 17 | "electron-trpc": "0.7.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "zod": "^3.21.4" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20.12.8", 24 | "@types/react": "^18.3.1", 25 | "@types/react-dom": "^18.3.0", 26 | "@vitejs/plugin-react": "^4.2.1", 27 | "vite": "^5.2.11", 28 | "vite-plugin-electron": "^0.28.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/basic-react/preload/preload.ts: -------------------------------------------------------------------------------- 1 | import { exposeElectronTRPC } from 'electron-trpc/main'; 2 | 3 | process.once('loaded', async () => { 4 | exposeElectronTRPC(); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/basic-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import { ipcLink } from 'electron-trpc/renderer'; 4 | import { createTRPCReact } from '@trpc/react-query'; 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 6 | import type { AppRouter } from '../electron/api'; 7 | 8 | const trpcReact = createTRPCReact(); 9 | 10 | function App() { 11 | const [queryClient] = useState(() => new QueryClient()); 12 | const [trpcClient] = useState(() => 13 | trpcReact.createClient({ 14 | links: [ipcLink()], 15 | }) 16 | ); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | function HelloElectron() { 28 | const { data } = trpcReact.greeting.useQuery({ name: 'Electron' }); 29 | trpcReact.subscription.useSubscription(undefined, { 30 | onData: (data) => { 31 | console.log(data); 32 | }, 33 | }); 34 | 35 | if (!data) { 36 | return null; 37 | } 38 | 39 | return
{data.text}
; 40 | } 41 | 42 | ReactDom.render(, document.getElementById('react-root')); 43 | -------------------------------------------------------------------------------- /examples/basic-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "jsx": "react", 7 | "lib": ["dom", "esnext"], 8 | "module": "esnext", 9 | "moduleResolution": "node16", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "useDefineForClassFields": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "esnext" 19 | }, 20 | "include": ["./*.ts", "./*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/basic-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import electron from 'vite-plugin-electron'; 4 | 5 | export default defineConfig({ 6 | mode: 'development', 7 | plugins: [ 8 | react(), 9 | electron([ 10 | { 11 | entry: 'electron/index.ts', 12 | }, 13 | { 14 | entry: 'preload/preload.ts', 15 | onstart(options) { 16 | options.reload(); 17 | }, 18 | }, 19 | ]), 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "scripts": { 4 | "build": "corepack pnpm -r build", 5 | "test": "corepack pnpm -r test", 6 | "test:e2e": "playwright test", 7 | "test:ci": "corepack pnpm -r test:ci", 8 | "lint": "prettier --check .", 9 | "lint:fix": "prettier --write .", 10 | "prepublishOnly": "corepack pnpm build", 11 | "changeset": "changeset", 12 | "changeset:version": "changeset version && pnpm install --no-frozen-lockfile", 13 | "release": "changeset publish", 14 | "docs:start": "corepack pnpm --filter www dev", 15 | "docs:build": "corepack pnpm --filter www build", 16 | "docs:preview": "corepack pnpm --filter www preview", 17 | "docs:publish": "corepack pnpm --filter www publish-pages" 18 | }, 19 | "packageManager": "pnpm@9.15.0", 20 | "devDependencies": { 21 | "@changesets/changelog-github": "^0.5.0", 22 | "@changesets/cli": "^2.27.1", 23 | "@playwright/test": "^1.43.1", 24 | "prettier": "^3.2.5", 25 | "typescript": "^5.4.5" 26 | }, 27 | "engines": { 28 | "node": ">=18", 29 | "pnpm": ">=9" 30 | }, 31 | "pnpm": { 32 | "requiredScripts": [ 33 | "build" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/electron-trpc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # electron-trpc 2 | 3 | ## 0.7.1 4 | 5 | ### Patch Changes 6 | 7 | - [#197](https://github.com/jsonnull/electron-trpc/pull/197) [`1c5caaa`](https://github.com/jsonnull/electron-trpc/commit/1c5caaa0e58cee2a8324b27cdf5c793a312f844b) Thanks [@dguenther](https://github.com/dguenther)! - Fix a crash when calling `.destroy()` on a BrowserWindow. 8 | 9 | ## 0.7.0 10 | 11 | ### Minor Changes 12 | 13 | - [#219](https://github.com/jsonnull/electron-trpc/pull/219) [`95525ca`](https://github.com/jsonnull/electron-trpc/commit/95525ca1c2a28a0d657ec5063eb33a8dde32a289) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix bug where soft-navigation events would cause subscription cleanup 14 | 15 | ## 0.6.2 16 | 17 | ### Patch Changes 18 | 19 | - [#217](https://github.com/jsonnull/electron-trpc/pull/217) [`f5f3ef3`](https://github.com/jsonnull/electron-trpc/commit/f5f3ef3b2ffd70d3daa899aaa0bb1745ce267951) Thanks [@jsonnull](https://github.com/jsonnull)! - Added README and metadata to published package. 20 | 21 | ## 0.6.1 22 | 23 | ### Patch Changes 24 | 25 | - [#190](https://github.com/jsonnull/electron-trpc/pull/190) [`fb7845f`](https://github.com/jsonnull/electron-trpc/commit/fb7845fbc771002309dea9d8b4c2079860350656) Thanks [@jsonnull](https://github.com/jsonnull)! - Update dev dependencies. 26 | 27 | - [#192](https://github.com/jsonnull/electron-trpc/pull/192) [`2bc0233`](https://github.com/jsonnull/electron-trpc/commit/2bc02333172b8a25a493c34c8e17434b8ffb4eea) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix issue when TRPC client tries to close a subscription. 28 | 29 | ## 0.6.0 30 | 31 | ### Minor Changes 32 | 33 | - [#165](https://github.com/jsonnull/electron-trpc/pull/165) [`a15c6c4d0c531b3596689b4cc470548a5228c989`](https://github.com/jsonnull/electron-trpc/commit/a15c6c4d0c531b3596689b4cc470548a5228c989) Thanks [@jsonnull](https://github.com/jsonnull)! - Subscriptions will now be cleaned up when the frame owning that subscription performs a navigation. 34 | 35 | - [#187](https://github.com/jsonnull/electron-trpc/pull/187) [`50953c7e5bcb69d4e5482405f4a621b229f0ca82`](https://github.com/jsonnull/electron-trpc/commit/50953c7e5bcb69d4e5482405f4a621b229f0ca82) Thanks [@jsonnull](https://github.com/jsonnull)! - Upgrade electron to v29. 36 | 37 | ## 0.5.2 38 | 39 | ### Patch Changes 40 | 41 | - [#149](https://github.com/jsonnull/electron-trpc/pull/149) [`3892db5`](https://github.com/jsonnull/electron-trpc/commit/3892db5342653e5c294477e3884b15c796fc26de) Thanks [@JoeHartzell](https://github.com/JoeHartzell)! - Update internal utils to use upstream tRPC shared helper. 42 | 43 | - [#152](https://github.com/jsonnull/electron-trpc/pull/152) [`0e72fe9`](https://github.com/jsonnull/electron-trpc/commit/0e72fe93b7605636b80cb3b3e47b6992cb4c097a) Thanks [@jsonnull](https://github.com/jsonnull)! - Update dependencies. 44 | 45 | ## 0.5.1 46 | 47 | ### Patch Changes 48 | 49 | - [#146](https://github.com/jsonnull/electron-trpc/pull/146) [`c43ae93`](https://github.com/jsonnull/electron-trpc/commit/c43ae93df4af397986c602c432fc32178d62796b) Thanks [@JoeHartzell](https://github.com/JoeHartzell)! - Fix `handleIPCMessage` only sending replies to Electron's main frame. 50 | 51 | ## 0.5.0 52 | 53 | ### Minor Changes 54 | 55 | - [#138](https://github.com/jsonnull/electron-trpc/pull/138) [`68ddf63`](https://github.com/jsonnull/electron-trpc/commit/68ddf63ff6b3560626bf78d45ca2bf7ed2851f22) Thanks [@jsonnull](https://github.com/jsonnull)! - Rework how subscriptions are cleaned up. 56 | 57 | ## 0.4.5 58 | 59 | ### Patch Changes 60 | 61 | - [#136](https://github.com/jsonnull/electron-trpc/pull/136) [`70d13e4`](https://github.com/jsonnull/electron-trpc/commit/70d13e400d8b0678a359c633511b419736ef4b5d) Thanks [@jsonnull](https://github.com/jsonnull)! - Update electron and trpc to latest. 62 | 63 | ## 0.4.4 64 | 65 | ### Patch Changes 66 | 67 | - [#134](https://github.com/jsonnull/electron-trpc/pull/134) [`84d1139`](https://github.com/jsonnull/electron-trpc/commit/84d1139d6b6970b8863fdb1ba22a0aaa709045ec) Thanks [@jsonnull](https://github.com/jsonnull)! - Update dependencies to latest. 68 | 69 | ## 0.4.3 70 | 71 | ### Patch Changes 72 | 73 | - [#125](https://github.com/jsonnull/electron-trpc/pull/125) [`1f601ee`](https://github.com/jsonnull/electron-trpc/commit/1f601ee985e1a969fa2cdacaf2ff20962a2edbd9) Thanks [@biw](https://github.com/biw)! - Fix transforms not running for subscriptions. 74 | 75 | ## 0.4.2 76 | 77 | ### Patch Changes 78 | 79 | - [#118](https://github.com/jsonnull/electron-trpc/pull/118) [`cbae157`](https://github.com/jsonnull/electron-trpc/commit/cbae1570ddeab2405950806656c0d4fc19d72855) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix crash when using subscriptions with some custom transformers. 80 | 81 | ## 0.4.1 82 | 83 | ### Patch Changes 84 | 85 | - [#111](https://github.com/jsonnull/electron-trpc/pull/111) [`b73c1a8`](https://github.com/jsonnull/electron-trpc/commit/b73c1a89c77258bf4372991fda563d6fa0ba299f) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix type of `createContext` in `createIPCHandler` 86 | 87 | ## 0.4.0 88 | 89 | ### Minor Changes 90 | 91 | - [#108](https://github.com/jsonnull/electron-trpc/pull/108) [`3cd3649`](https://github.com/jsonnull/electron-trpc/commit/3cd3649a498a8cdb50f295ee2f032ca75eccc8b3) Thanks [@eslym](https://github.com/eslym)! - Send responses back to process which made the request. 92 | 93 | ## 0.3.2 94 | 95 | ### Patch Changes 96 | 97 | - [#103](https://github.com/jsonnull/electron-trpc/pull/103) [`46d79ef`](https://github.com/jsonnull/electron-trpc/commit/46d79efde7ccc12cd1e99eb086413aa83bda29f8) Thanks [@jsonnull](https://github.com/jsonnull)! - Publish types for node TS resolution workaround. 98 | 99 | ## 0.3.1 100 | 101 | ### Patch Changes 102 | 103 | - [#96](https://github.com/jsonnull/electron-trpc/pull/96) [`797e5ba`](https://github.com/jsonnull/electron-trpc/commit/797e5baa47f867a2f128ace3f8186dd21b57820d) Thanks [@jsonnull](https://github.com/jsonnull)! - Do not force projects to use node16 TS resolution. 104 | 105 | - [#98](https://github.com/jsonnull/electron-trpc/pull/98) [`f92772a`](https://github.com/jsonnull/electron-trpc/commit/f92772a191a632f40c7a3cad46893d40a6715b48) Thanks [@jsonnull](https://github.com/jsonnull)! - Allow sync createContext to be passed. 106 | 107 | - [#93](https://github.com/jsonnull/electron-trpc/pull/93) [`3cb91a7`](https://github.com/jsonnull/electron-trpc/commit/3cb91a71da9b7da84149ff2c586e5f7ce2032030) Thanks [@BeeeQueue](https://github.com/BeeeQueue)! - Use input serializer in ipcLink. 108 | 109 | ## 0.3.0 110 | 111 | ### Minor Changes 112 | 113 | - [#88](https://github.com/jsonnull/electron-trpc/pull/88) [`b67f2a7`](https://github.com/jsonnull/electron-trpc/commit/b67f2a7a87cd77b88d337e6996d78c6507a9c187) Thanks [@jsonnull](https://github.com/jsonnull)! - Added support for subscriptions. 114 | 115 | ## 0.2.1 116 | 117 | ### Patch Changes 118 | 119 | - [#78](https://github.com/jsonnull/electron-trpc/pull/78) [`c9031f5`](https://github.com/jsonnull/electron-trpc/commit/c9031f5b521095d3c648fc905b642471e875d86f) Thanks [@jsonnull](https://github.com/jsonnull)! - Include type declarations in publish package. 120 | 121 | ## 0.2.0 122 | 123 | ### Minor Changes 124 | 125 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`006d01e`](https://github.com/jsonnull/electron-trpc/commit/006d01e73a995f756be622769192444bba3b4a87) Thanks [@jsonnull](https://github.com/jsonnull)! - Update electron-trpc to v10 release. 126 | 127 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`c46f700`](https://github.com/jsonnull/electron-trpc/commit/c46f700b6171835a5b00d6d2c44061acdcd49874) Thanks [@jsonnull](https://github.com/jsonnull)! - Move to tRPC v10. 128 | 129 | ### Patch Changes 130 | 131 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`231afea`](https://github.com/jsonnull/electron-trpc/commit/231afea9f21f0d4ba7f12c37fd781f22ca5d4141) Thanks [@jsonnull](https://github.com/jsonnull)! - Set minimum version for electron peer dependency. 132 | 133 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`960999f`](https://github.com/jsonnull/electron-trpc/commit/960999f5c2fec8b70152cfdf6cadc737c60edd48) Thanks [@jsonnull](https://github.com/jsonnull)! - Updated API to be simpler and require fewer steps. 134 | 135 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`3c76498`](https://github.com/jsonnull/electron-trpc/commit/3c76498c152e92fe1b084d3e7a5170d8f2c1dee3) Thanks [@jsonnull](https://github.com/jsonnull)! - Update tRPC to rc.7. 136 | 137 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`7c7ee89`](https://github.com/jsonnull/electron-trpc/commit/7c7ee89b45c6c27527e26b0a6100fc0cb41d8ba6) Thanks [@jsonnull](https://github.com/jsonnull)! - Upgrade to tRPC v10 rc.1. 138 | 139 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`ddc11cb`](https://github.com/jsonnull/electron-trpc/commit/ddc11cb1f1502568a028476acdefdb8d95d9562c) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix transformer path. 140 | 141 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`4615cf6`](https://github.com/jsonnull/electron-trpc/commit/4615cf63c382a0ea21781efb5093a531cc6378e6) Thanks [@jsonnull](https://github.com/jsonnull)! - Update tRPC to rc.2. 142 | 143 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`42f2b09`](https://github.com/jsonnull/electron-trpc/commit/42f2b09efbaf322af42df176b74f72b972724f99) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix server import from ipcLink. 144 | 145 | - [#71](https://github.com/jsonnull/electron-trpc/pull/71) [`d2870a4`](https://github.com/jsonnull/electron-trpc/commit/d2870a4ef4429053c6a0d3e44bb204d0177adda9) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix TypeScript type resolution. (Authored by @skyrpex, thanks!) 146 | 147 | ## 0.2.0-next.7 148 | 149 | ### Patch Changes 150 | 151 | - [#62](https://github.com/jsonnull/electron-trpc/pull/62) [`169c47f`](https://github.com/jsonnull/electron-trpc/commit/169c47f325de8899784187af06140c29758b0c0a) Thanks [@renovate](https://github.com/apps/renovate)! - Update tRPC to rc.7. 152 | 153 | ## 0.2.0-next.6 154 | 155 | ### Patch Changes 156 | 157 | - [#58](https://github.com/jsonnull/electron-trpc/pull/58) [`a2103c4`](https://github.com/jsonnull/electron-trpc/commit/a2103c4e9789741aa98aa057fcebf78e4f339d9b) Thanks [@jsonnull](https://github.com/jsonnull)! - Updated API to be simpler and require fewer steps. 158 | 159 | ## 0.2.0-next.5 160 | 161 | ### Patch Changes 162 | 163 | - [#45](https://github.com/jsonnull/electron-trpc/pull/45) [`333197f`](https://github.com/jsonnull/electron-trpc/commit/333197fb3e567aa37f350af992d123f8f8ed6796) Thanks [@renovate](https://github.com/apps/renovate)! - Update tRPC to rc.2. 164 | 165 | ## 0.2.0-next.4 166 | 167 | ### Patch Changes 168 | 169 | - [#41](https://github.com/jsonnull/electron-trpc/pull/41) [`6ff6963`](https://github.com/jsonnull/electron-trpc/commit/6ff696377187c19bc773153d17d8cba7bda25c50) Thanks [@jsonnull](https://github.com/jsonnull)! - Upgrade to tRPC v10 rc.1. 170 | 171 | - [#39](https://github.com/jsonnull/electron-trpc/pull/39) [`702b9af`](https://github.com/jsonnull/electron-trpc/commit/702b9afc595630b1a272c48ba86fc84f67e97909) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix TypeScript type resolution. (Authored by @skyrpex, thanks!) 172 | 173 | ## 0.2.0-next.3 174 | 175 | ### Patch Changes 176 | 177 | - [#29](https://github.com/jsonnull/electron-trpc/pull/29) [`6d5ef0a`](https://github.com/jsonnull/electron-trpc/commit/6d5ef0a0265957f322b91daebdd3e851f61f1333) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix transformer path. 178 | 179 | ## 0.2.0-next.2 180 | 181 | ### Patch Changes 182 | 183 | - [#26](https://github.com/jsonnull/electron-trpc/pull/26) [`073eecb`](https://github.com/jsonnull/electron-trpc/commit/073eecb504917ad7e8865a8f904827ca0a4ca2ba) Thanks [@jsonnull](https://github.com/jsonnull)! - Fix server import from ipcLink. 184 | 185 | ## 0.2.0-next.1 186 | 187 | ### Patch Changes 188 | 189 | - [#24](https://github.com/jsonnull/electron-trpc/pull/24) [`eba2b98`](https://github.com/jsonnull/electron-trpc/commit/eba2b98506bcf4590a3689397f445a0443fa9188) Thanks [@jsonnull](https://github.com/jsonnull)! - Set minimum version for electron peer dependency. 190 | 191 | ## 0.2.0-next.0 192 | 193 | ### Minor Changes 194 | 195 | - [`d392431`](https://github.com/jsonnull/electron-trpc/commit/d39243176897dd7cd209d768db68dc90cab92c58) Thanks [@jsonnull](https://github.com/jsonnull)! - Move to tRPC v10. 196 | 197 | ## 0.1.0 198 | 199 | ### Minor Changes 200 | 201 | - [#17](https://github.com/jsonnull/electron-trpc/pull/17) [`1dc8ac3`](https://github.com/jsonnull/electron-trpc/commit/1dc8ac3e3f54b471beb8bef6dea4fce4efafe5b4) Thanks [@jsonnull](https://github.com/jsonnull)! - Upgrade tRPC. 202 | 203 | ## 0.0.4 204 | 205 | ### Patch Changes 206 | 207 | - [#14](https://github.com/jsonnull/electron-trpc/pull/14) [`e314c71`](https://github.com/jsonnull/electron-trpc/commit/e314c715f5b2734c357a564d23b5717089adb7ef) Thanks [@jsonnull](https://github.com/jsonnull)! - Make createContext param optional. 208 | 209 | ## 0.0.3 210 | 211 | ### Patch Changes 212 | 213 | - [#10](https://github.com/jsonnull/electron-trpc/pull/10) [`357842e`](https://github.com/jsonnull/electron-trpc/commit/357842e81a8db0d089095a0cb91aa5b647c230d0) Thanks [@jsonnull](https://github.com/jsonnull)! - Update API for `exposeElectronTRPC`. 214 | 215 | ## 0.0.2 216 | 217 | ### Patch Changes 218 | 219 | - [#8](https://github.com/jsonnull/electron-trpc/pull/8) [`50e4718`](https://github.com/jsonnull/electron-trpc/commit/50e4718d75803a5f2ed4675cfc42f713d3dff62b) Thanks [@jsonnull](https://github.com/jsonnull)! - Update docs and package description. 220 | 221 | ## 0.0.1 222 | 223 | ### Patch Changes 224 | 225 | - [#2](https://github.com/jsonnull/electron-trpc/pull/2) [`693b1e0`](https://github.com/jsonnull/electron-trpc/commit/693b1e0e30d06c2cba6b1745967e1b3c38f3ed91) Thanks [@jsonnull](https://github.com/jsonnull)! - Initial version 226 | -------------------------------------------------------------------------------- /packages/electron-trpc/dts-bundle-generator.config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | compilationOptions: { 3 | preferredConfigPath: './tsconfig.json', 4 | }, 5 | entries: [ 6 | { 7 | filePath: './src/renderer/index.ts', 8 | outFile: `./dist/renderer.d.ts`, 9 | noCheck: true, 10 | }, 11 | { 12 | filePath: './src/main/index.ts', 13 | outFile: `./dist/main.d.ts`, 14 | noCheck: true, 15 | }, 16 | ], 17 | }; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /packages/electron-trpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-trpc", 3 | "description": "Electron support for tRPC", 4 | "version": "0.7.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/jsonnull/electron-trpc.git" 8 | }, 9 | "homepage": "https://electron-trpc.dev", 10 | "bugs": { 11 | "url": "https://github.com/jsonnull/electron-trpc/issues" 12 | }, 13 | "keywords": [ 14 | "electron", 15 | "trpc", 16 | "ipc", 17 | "rpc", 18 | "typescript" 19 | ], 20 | "exports": { 21 | "./main": { 22 | "require": "./dist/main.cjs", 23 | "import": "./dist/main.mjs", 24 | "types": "./dist/main.d.ts" 25 | }, 26 | "./renderer": { 27 | "require": "./dist/renderer.cjs", 28 | "import": "./dist/renderer.mjs", 29 | "types": "./dist/renderer.d.ts" 30 | } 31 | }, 32 | "author": "Jason Nall ", 33 | "license": "MIT", 34 | "files": [ 35 | "dist", 36 | "src", 37 | "main.d.ts", 38 | "renderer.d.ts", 39 | "README.md" 40 | ], 41 | "scripts": { 42 | "build": "tsc && vite build -c src/main/vite.config.ts && vite build -c src/renderer/vite.config.ts && corepack pnpm build:types", 43 | "build:types": "dts-bundle-generator --config ./dts-bundle-generator.config.ts && corepack pnpm run copy:types", 44 | "copy:readme": "cp ../../README.md ./", 45 | "copy:types": "cp dist/main.d.ts dist/renderer.d.ts ./", 46 | "test": "vitest -c vitest.config.ts", 47 | "test:ci": "vitest run -c vitest.config.ts --coverage", 48 | "prepublishOnly": "corepack pnpm copy:readme && corepack pnpm build", 49 | "changeset": "changeset", 50 | "release": "changeset publish" 51 | }, 52 | "devDependencies": { 53 | "@tanstack/react-query": "^5.32.1", 54 | "@trpc/client": "10.45.2", 55 | "@trpc/server": "10.45.2", 56 | "@types/debug": "^4.1.12", 57 | "@types/node": "^20.12.8", 58 | "@vitest/coverage-v8": "^1.6.0", 59 | "builtin-modules": "^4.0.0", 60 | "dts-bundle-generator": "9.5.1", 61 | "electron": "29.3.2", 62 | "react": "^18.3.1", 63 | "react-dom": "^18.3.1", 64 | "superjson": "^2.2.1", 65 | "vite": "^5.2.11", 66 | "vite-plugin-commonjs-externals": "^0.1.4", 67 | "vitest": "^1.6.0", 68 | "zod": "^3.23.6" 69 | }, 70 | "peerDependencies": { 71 | "@trpc/client": ">10.0.0", 72 | "@trpc/server": ">10.0.0", 73 | "electron": ">19.0.0" 74 | }, 75 | "dependencies": { 76 | "debug": "^4.3.4" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ELECTRON_TRPC_CHANNEL = 'electron-trpc'; 2 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/__tests__/handleIPCMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, MockedFunction, test, vi } from 'vitest'; 2 | import { z } from 'zod'; 3 | import * as trpc from '@trpc/server'; 4 | import { observable } from '@trpc/server/observable'; 5 | import { EventEmitter } from 'events'; 6 | import { handleIPCMessage } from '../handleIPCMessage'; 7 | import { IpcMainEvent } from 'electron'; 8 | 9 | interface MockEvent { 10 | reply: MockedFunction; 11 | sender: { 12 | isDestroyed: () => boolean; 13 | on: (event: string, cb: () => void) => void; 14 | }; 15 | } 16 | const makeEvent = (event: MockEvent) => event as unknown as IpcMainEvent & Pick; 17 | 18 | const ee = new EventEmitter(); 19 | 20 | const t = trpc.initTRPC.create(); 21 | const testRouter = t.router({ 22 | testQuery: t.procedure 23 | .input( 24 | z.object({ 25 | id: z.string(), 26 | }) 27 | ) 28 | .query(({ input }) => { 29 | return { id: input.id, isTest: true }; 30 | }), 31 | testSubscription: t.procedure.subscription(() => { 32 | return observable((emit) => { 33 | function testResponse() { 34 | emit.next('test response'); 35 | } 36 | 37 | ee.on('test', testResponse); 38 | return () => ee.off('test', testResponse); 39 | }); 40 | }), 41 | }); 42 | 43 | describe('api', () => { 44 | test('handles queries', async () => { 45 | const event = makeEvent({ 46 | reply: vi.fn(), 47 | sender: { 48 | isDestroyed: () => false, 49 | on: () => {}, 50 | }, 51 | }); 52 | 53 | await handleIPCMessage({ 54 | createContext: async () => ({}), 55 | event, 56 | internalId: '1-1:1', 57 | message: { 58 | method: 'request', 59 | operation: { 60 | context: {}, 61 | id: 1, 62 | input: { id: 'test-id' }, 63 | path: 'testQuery', 64 | type: 'query', 65 | }, 66 | }, 67 | router: testRouter, 68 | subscriptions: new Map(), 69 | }); 70 | 71 | expect(event.reply).toHaveBeenCalledOnce(); 72 | expect(event.reply.mock.lastCall![1]).toMatchObject({ 73 | id: 1, 74 | result: { 75 | data: { 76 | id: 'test-id', 77 | isTest: true, 78 | }, 79 | }, 80 | }); 81 | }); 82 | 83 | test('does not respond if sender is gone', async () => { 84 | const event = makeEvent({ 85 | reply: vi.fn(), 86 | sender: { 87 | isDestroyed: () => true, 88 | on: () => {}, 89 | }, 90 | }); 91 | 92 | await handleIPCMessage({ 93 | createContext: async () => ({}), 94 | event, 95 | internalId: '1-1:1', 96 | message: { 97 | method: 'request', 98 | operation: { 99 | context: {}, 100 | id: 1, 101 | input: { id: 'test-id' }, 102 | path: 'testQuery', 103 | type: 'query', 104 | }, 105 | }, 106 | router: testRouter, 107 | subscriptions: new Map(), 108 | }); 109 | 110 | expect(event.reply).not.toHaveBeenCalled(); 111 | }); 112 | 113 | test('handles subscriptions', async () => { 114 | const event = makeEvent({ 115 | reply: vi.fn(), 116 | sender: { 117 | isDestroyed: () => false, 118 | on: () => {}, 119 | }, 120 | }); 121 | 122 | await handleIPCMessage({ 123 | createContext: async () => ({}), 124 | message: { 125 | method: 'request', 126 | operation: { 127 | context: {}, 128 | id: 1, 129 | input: undefined, 130 | path: 'testSubscription', 131 | type: 'subscription', 132 | }, 133 | }, 134 | internalId: '1-1:1', 135 | subscriptions: new Map(), 136 | router: testRouter, 137 | event, 138 | }); 139 | 140 | expect(event.reply).not.toHaveBeenCalled(); 141 | 142 | ee.emit('test'); 143 | 144 | expect(event.reply).toHaveBeenCalledOnce(); 145 | expect(event.reply.mock.lastCall![1]).toMatchObject({ 146 | id: 1, 147 | result: { 148 | data: 'test response', 149 | }, 150 | }); 151 | }); 152 | 153 | test('subscription responds using custom serializer', async () => { 154 | const event = makeEvent({ 155 | reply: vi.fn(), 156 | sender: { 157 | isDestroyed: () => false, 158 | on: () => {}, 159 | }, 160 | }); 161 | 162 | const t = trpc.initTRPC.create({ 163 | transformer: { 164 | deserialize: (input: unknown) => { 165 | const serialized = (input as string).replace(/^serialized:/, ''); 166 | return JSON.parse(serialized); 167 | }, 168 | serialize: (input) => { 169 | return `serialized:${JSON.stringify(input)}`; 170 | }, 171 | }, 172 | }); 173 | 174 | const testRouter = t.router({ 175 | testSubscription: t.procedure.subscription(() => { 176 | return observable((emit) => { 177 | function testResponse() { 178 | emit.next('test response'); 179 | } 180 | 181 | ee.on('test', testResponse); 182 | return () => ee.off('test', testResponse); 183 | }); 184 | }), 185 | }); 186 | 187 | await handleIPCMessage({ 188 | createContext: async () => ({}), 189 | message: { 190 | method: 'request', 191 | operation: { 192 | context: {}, 193 | id: 1, 194 | input: undefined, 195 | path: 'testSubscription', 196 | type: 'subscription', 197 | }, 198 | }, 199 | internalId: '1-1:1', 200 | subscriptions: new Map(), 201 | router: testRouter, 202 | event, 203 | }); 204 | 205 | expect(event.reply).not.toHaveBeenCalled(); 206 | 207 | ee.emit('test'); 208 | 209 | expect(event.reply).toHaveBeenCalledOnce(); 210 | expect(event.reply.mock.lastCall![1]).toMatchObject({ 211 | id: 1, 212 | result: { 213 | type: 'data', 214 | data: 'serialized:"test response"', 215 | }, 216 | }); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { getTRPCErrorFromUnknown } from '../utils'; 3 | import { TRPCError } from '@trpc/server'; 4 | 5 | describe('getTRPCErrorFromUnknown', () => { 6 | test('should return a TRPCError when given a TRPCError', () => { 7 | const error = new TRPCError({ 8 | code: 'TIMEOUT', 9 | cause: new Error('test'), 10 | message: 'test', 11 | }); 12 | const result = getTRPCErrorFromUnknown(error); 13 | 14 | expect(result).toBe(error); 15 | }); 16 | 17 | test('should return a TRPCError when given an Error', () => { 18 | const error = new Error('test'); 19 | const result = getTRPCErrorFromUnknown(error); 20 | 21 | expect(result).toBeInstanceOf(TRPCError); 22 | expect(result).toMatchObject({ 23 | code: 'INTERNAL_SERVER_ERROR', 24 | cause: error, 25 | message: error.message, 26 | }); 27 | }); 28 | 29 | test('should return a TRPCError when given a string', () => { 30 | const error = 'test'; 31 | const result = getTRPCErrorFromUnknown(error); 32 | 33 | expect(result).toBeInstanceOf(TRPCError); 34 | expect(result).toMatchObject({ 35 | code: 'INTERNAL_SERVER_ERROR', 36 | cause: new Error(error), 37 | message: error, 38 | }); 39 | }); 40 | 41 | test('should use the stack from the given error', () => { 42 | const error = new Error('test'); 43 | error.stack = 'test stack'; 44 | const result = getTRPCErrorFromUnknown(error); 45 | 46 | expect(result.stack).toBe(error.stack); 47 | }); 48 | 49 | test.each([{ test: 'test' }, undefined, null])( 50 | 'should fallback to "Unknown error" when given an unknown type', 51 | (error: unknown) => { 52 | const result = getTRPCErrorFromUnknown(error); 53 | 54 | expect(result).toBeInstanceOf(TRPCError); 55 | expect(result).toMatchObject({ 56 | code: 'INTERNAL_SERVER_ERROR', 57 | cause: new Error('Unknown error'), 58 | message: 'Unknown error', 59 | }); 60 | } 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/createIPCHandler.ts: -------------------------------------------------------------------------------- 1 | import type { AnyRouter, inferRouterContext } from '@trpc/server'; 2 | import { ipcMain } from 'electron'; 3 | import type { BrowserWindow, IpcMainEvent } from 'electron'; 4 | import { handleIPCMessage } from './handleIPCMessage'; 5 | import { CreateContextOptions } from './types'; 6 | import { ELECTRON_TRPC_CHANNEL } from '../constants'; 7 | import { ETRPCRequest } from '../types'; 8 | import { Unsubscribable } from '@trpc/server/observable'; 9 | import debugFactory from 'debug'; 10 | 11 | const debug = debugFactory('electron-trpc:main:IPCHandler'); 12 | 13 | type Awaitable = T | Promise; 14 | 15 | const getInternalId = (event: IpcMainEvent, request: ETRPCRequest) => { 16 | const messageId = request.method === 'request' ? request.operation.id : request.id; 17 | return `${event.sender.id}-${event.senderFrame.routingId}:${messageId}`; 18 | }; 19 | 20 | class IPCHandler { 21 | #windows: BrowserWindow[] = []; 22 | #subscriptions: Map = new Map(); 23 | 24 | constructor({ 25 | createContext, 26 | router, 27 | windows = [], 28 | }: { 29 | createContext?: (opts: CreateContextOptions) => Awaitable>; 30 | router: TRouter; 31 | windows?: BrowserWindow[]; 32 | }) { 33 | windows.forEach((win) => this.attachWindow(win)); 34 | 35 | ipcMain.on(ELECTRON_TRPC_CHANNEL, (event: IpcMainEvent, request: ETRPCRequest) => { 36 | handleIPCMessage({ 37 | router, 38 | createContext, 39 | internalId: getInternalId(event, request), 40 | event, 41 | message: request, 42 | subscriptions: this.#subscriptions, 43 | }); 44 | }); 45 | } 46 | 47 | attachWindow(win: BrowserWindow) { 48 | if (this.#windows.includes(win)) { 49 | return; 50 | } 51 | 52 | debug('Attaching window', win.id); 53 | 54 | this.#windows.push(win); 55 | this.#attachSubscriptionCleanupHandlers(win); 56 | } 57 | 58 | detachWindow(win: BrowserWindow, webContentsId?: number) { 59 | debug('Detaching window', win.id); 60 | 61 | if (win.isDestroyed() && webContentsId === undefined) { 62 | throw new Error('webContentsId is required when calling detachWindow on a destroyed window'); 63 | } 64 | 65 | this.#windows = this.#windows.filter((w) => w !== win); 66 | this.#cleanUpSubscriptions({ webContentsId: webContentsId ?? win.webContents.id }); 67 | } 68 | 69 | #cleanUpSubscriptions({ 70 | webContentsId, 71 | frameRoutingId, 72 | }: { 73 | webContentsId: number; 74 | frameRoutingId?: number; 75 | }) { 76 | for (const [key, sub] of this.#subscriptions.entries()) { 77 | if (key.startsWith(`${webContentsId}-${frameRoutingId ?? ''}`)) { 78 | debug('Closing subscription', key); 79 | sub.unsubscribe(); 80 | this.#subscriptions.delete(key); 81 | } 82 | } 83 | } 84 | 85 | #attachSubscriptionCleanupHandlers(win: BrowserWindow) { 86 | const webContentsId = win.webContents.id; 87 | win.webContents.on('did-start-navigation', ({ isSameDocument, frame }) => { 88 | // Check if it's a hard navigation 89 | if (!isSameDocument) { 90 | debug( 91 | 'Handling hard navigation event', 92 | `webContentsId: ${webContentsId}`, 93 | `frameRoutingId: ${frame.routingId}` 94 | ); 95 | this.#cleanUpSubscriptions({ 96 | webContentsId: webContentsId, 97 | frameRoutingId: frame.routingId, 98 | }); 99 | } 100 | }); 101 | win.webContents.on('destroyed', () => { 102 | debug('Handling webContents `destroyed` event'); 103 | this.detachWindow(win, webContentsId); 104 | }); 105 | } 106 | } 107 | 108 | export const createIPCHandler = ({ 109 | createContext, 110 | router, 111 | windows = [], 112 | }: { 113 | createContext?: (opts: CreateContextOptions) => Promise>; 114 | router: TRouter; 115 | windows?: Electron.BrowserWindow[]; 116 | }) => { 117 | return new IPCHandler({ createContext, router, windows }); 118 | }; 119 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/exposeElectronTRPC.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, contextBridge } from 'electron'; 2 | import { ELECTRON_TRPC_CHANNEL } from '../constants'; 3 | import type { RendererGlobalElectronTRPC } from '../types'; 4 | 5 | export const exposeElectronTRPC = () => { 6 | const electronTRPC: RendererGlobalElectronTRPC = { 7 | sendMessage: (operation) => ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation), 8 | onMessage: (callback) => 9 | ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args)), 10 | }; 11 | contextBridge.exposeInMainWorld('electronTRPC', electronTRPC); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/handleIPCMessage.ts: -------------------------------------------------------------------------------- 1 | import { callProcedure, TRPCError } from '@trpc/server'; 2 | import type { AnyRouter, inferRouterContext } from '@trpc/server'; 3 | import type { TRPCResponseMessage } from '@trpc/server/rpc'; 4 | import type { IpcMainEvent } from 'electron'; 5 | import { isObservable, Unsubscribable } from '@trpc/server/observable'; 6 | import { transformTRPCResponse } from '@trpc/server/shared'; 7 | import { getTRPCErrorFromUnknown } from './utils'; 8 | import { CreateContextOptions } from './types'; 9 | import { ELECTRON_TRPC_CHANNEL } from '../constants'; 10 | import { ETRPCRequest } from '../types'; 11 | import debugFactory from 'debug'; 12 | 13 | const debug = debugFactory('electron-trpc:main:handleIPCMessage'); 14 | 15 | export async function handleIPCMessage({ 16 | router, 17 | createContext, 18 | internalId, 19 | message, 20 | event, 21 | subscriptions, 22 | }: { 23 | router: TRouter; 24 | createContext?: (opts: CreateContextOptions) => Promise>; 25 | internalId: string; 26 | message: ETRPCRequest; 27 | event: IpcMainEvent; 28 | subscriptions: Map; 29 | }) { 30 | if (message.method === 'subscription.stop') { 31 | const subscription = subscriptions.get(internalId); 32 | if (!subscription) { 33 | return; 34 | } 35 | 36 | subscription.unsubscribe(); 37 | subscriptions.delete(internalId); 38 | return; 39 | } 40 | 41 | const { type, input: serializedInput, path, id } = message.operation; 42 | const input = serializedInput 43 | ? router._def._config.transformer.input.deserialize(serializedInput) 44 | : undefined; 45 | 46 | const ctx = (await createContext?.({ event })) ?? {}; 47 | 48 | const respond = (response: TRPCResponseMessage) => { 49 | if (event.sender.isDestroyed()) return; 50 | event.reply(ELECTRON_TRPC_CHANNEL, transformTRPCResponse(router._def._config, response)); 51 | }; 52 | 53 | try { 54 | const result = await callProcedure({ 55 | ctx, 56 | path, 57 | procedures: router._def.procedures, 58 | rawInput: input, 59 | type, 60 | }); 61 | 62 | if (type !== 'subscription') { 63 | respond({ 64 | id, 65 | result: { 66 | type: 'data', 67 | data: result, 68 | }, 69 | }); 70 | return; 71 | } else { 72 | if (!isObservable(result)) { 73 | throw new TRPCError({ 74 | message: `Subscription ${path} did not return an observable`, 75 | code: 'INTERNAL_SERVER_ERROR', 76 | }); 77 | } 78 | } 79 | 80 | const subscription = result.subscribe({ 81 | next(data) { 82 | respond({ 83 | id, 84 | result: { 85 | type: 'data', 86 | data, 87 | }, 88 | }); 89 | }, 90 | error(err) { 91 | const error = getTRPCErrorFromUnknown(err); 92 | respond({ 93 | id, 94 | error: router.getErrorShape({ 95 | error, 96 | type, 97 | path, 98 | input, 99 | ctx, 100 | }), 101 | }); 102 | }, 103 | complete() { 104 | respond({ 105 | id, 106 | result: { 107 | type: 'stopped', 108 | }, 109 | }); 110 | }, 111 | }); 112 | 113 | debug('Creating subscription', internalId); 114 | subscriptions.set(internalId, subscription); 115 | } catch (cause) { 116 | const error: TRPCError = getTRPCErrorFromUnknown(cause); 117 | 118 | return respond({ 119 | id, 120 | error: router.getErrorShape({ 121 | error, 122 | type, 123 | path, 124 | input, 125 | ctx, 126 | }), 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../constants'; 2 | export * from './createIPCHandler'; 3 | export * from './exposeElectronTRPC'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/types.ts: -------------------------------------------------------------------------------- 1 | import type { IpcMainInvokeEvent } from 'electron'; 2 | 3 | export interface CreateContextOptions { 4 | event: IpcMainInvokeEvent; 5 | } 6 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/utils.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from '@trpc/server'; 2 | 3 | // modified from @trpc/server/src/error/utils 4 | export function getTRPCErrorFromUnknown(cause: unknown): TRPCError { 5 | if (cause instanceof TRPCError) { 6 | return cause; 7 | } 8 | 9 | const error = getErrorFromUnknown(cause); 10 | const trpcError = new TRPCError({ 11 | code: 'INTERNAL_SERVER_ERROR', 12 | cause: error, 13 | message: error.message, 14 | }); 15 | 16 | // Inherit stack from error 17 | trpcError.stack = error.stack; 18 | 19 | return trpcError; 20 | } 21 | 22 | // modified from @trpc/server/src/error/utils 23 | function getErrorFromUnknown(cause: unknown): Error { 24 | if (cause instanceof Error) { 25 | return cause; 26 | } 27 | 28 | if (typeof cause === 'string') { 29 | return new Error(cause); 30 | } 31 | 32 | return new Error('Unknown error'); 33 | } 34 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/main/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import path from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | module.exports = defineConfig({ 6 | base: './', 7 | build: { 8 | lib: { 9 | entry: path.resolve(__dirname, './index.ts'), 10 | name: 'electron-trpc', 11 | formats: ['es', 'cjs'], 12 | fileName: (format) => ({ es: 'main.mjs', cjs: 'main.cjs' })[format as 'es' | 'cjs'], 13 | }, 14 | outDir: path.resolve(__dirname, '../../dist'), 15 | rollupOptions: { 16 | external: ['electron'], 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/renderer/__tests__/ipcLink.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, test, vi } from 'vitest'; 2 | import { createTRPCProxyClient } from '@trpc/client'; 3 | import { initTRPC } from '@trpc/server'; 4 | import type { TRPCResponseMessage } from '@trpc/server/rpc'; 5 | import z from 'zod'; 6 | import type { RendererGlobalElectronTRPC } from '../../types'; 7 | import { ipcLink } from '../ipcLink'; 8 | import superjson from 'superjson'; 9 | 10 | const t = initTRPC.create(); 11 | const router = t.router({ 12 | testQuery: t.procedure.query(() => 'query success'), 13 | testMutation: t.procedure.input(z.string()).mutation(() => 'mutation success'), 14 | testSubscription: t.procedure.subscription(() => { 15 | return { 16 | next: () => {}, 17 | complete: () => {}, 18 | }; 19 | }), 20 | testInputs: t.procedure 21 | .input(z.object({ str: z.string(), date: z.date(), bigint: z.bigint() })) 22 | .query((input) => { 23 | return input; 24 | }), 25 | }); 26 | 27 | type Router = typeof router; 28 | 29 | const electronTRPC: RendererGlobalElectronTRPC = {} as any; 30 | let handlers: ((message: TRPCResponseMessage) => void)[] = []; 31 | beforeEach(() => { 32 | handlers = []; 33 | electronTRPC.sendMessage = vi.fn(); 34 | electronTRPC.onMessage = vi.fn().mockImplementation((handler) => { 35 | handlers.push(handler); 36 | }); 37 | }); 38 | 39 | vi.stubGlobal('electronTRPC', electronTRPC); 40 | 41 | describe('ipcLink', () => { 42 | test('can create ipcLink', () => { 43 | expect(() => createTRPCProxyClient({ links: [ipcLink()] })).not.toThrow(); 44 | }); 45 | 46 | describe('operations', () => { 47 | let client: ReturnType>; 48 | const mock = vi.mocked(electronTRPC); 49 | 50 | beforeEach(() => { 51 | client = createTRPCProxyClient({ links: [ipcLink()] }); 52 | }); 53 | 54 | test('routes query to/from', async () => { 55 | const queryResponse = vi.fn(); 56 | 57 | const query = client.testQuery.query().then(queryResponse); 58 | 59 | expect(mock.sendMessage).toHaveBeenCalledTimes(1); 60 | expect(mock.sendMessage).toHaveBeenCalledWith({ 61 | method: 'request', 62 | operation: { 63 | context: {}, 64 | id: 1, 65 | input: undefined, 66 | path: 'testQuery', 67 | type: 'query', 68 | }, 69 | }); 70 | 71 | expect(queryResponse).not.toHaveBeenCalled(); 72 | 73 | handlers[0]({ 74 | id: 1, 75 | result: { 76 | type: 'data', 77 | data: 'query success', 78 | }, 79 | }); 80 | 81 | await query; 82 | 83 | expect(queryResponse).toHaveBeenCalledTimes(1); 84 | expect(queryResponse).toHaveBeenCalledWith('query success'); 85 | }); 86 | 87 | test('routes mutation to/from', async () => { 88 | const mutationResponse = vi.fn(); 89 | 90 | const mutation = client.testMutation.mutate('test input').then(mutationResponse); 91 | 92 | expect(mock.sendMessage).toHaveBeenCalledTimes(1); 93 | expect(mock.sendMessage).toHaveBeenCalledWith({ 94 | method: 'request', 95 | operation: { 96 | context: {}, 97 | id: 1, 98 | input: 'test input', 99 | path: 'testMutation', 100 | type: 'mutation', 101 | }, 102 | }); 103 | 104 | mock.sendMessage.mockClear(); 105 | 106 | handlers[0]({ 107 | id: 1, 108 | result: { 109 | type: 'data', 110 | data: 'mutation success', 111 | }, 112 | }); 113 | 114 | await mutation; 115 | 116 | expect(mutationResponse).toHaveBeenCalledTimes(1); 117 | expect(mutationResponse).toHaveBeenCalledWith('mutation success'); 118 | }); 119 | 120 | test('routes subscription to/from', async () => { 121 | /* 122 | * Subscription is routed to the server 123 | */ 124 | const subscriptionResponse = vi.fn(); 125 | const subscriptionComplete = vi.fn(); 126 | 127 | const subscription = client.testSubscription.subscribe(undefined, { 128 | onData: subscriptionResponse, 129 | onComplete: subscriptionComplete, 130 | }); 131 | 132 | expect(mock.sendMessage).toHaveBeenCalledTimes(1); 133 | expect(mock.sendMessage).toHaveBeenCalledWith({ 134 | method: 'request', 135 | operation: { 136 | context: {}, 137 | id: 1, 138 | input: undefined, 139 | path: 'testSubscription', 140 | type: 'subscription', 141 | }, 142 | }); 143 | 144 | /* 145 | * Multiple responses from the server 146 | */ 147 | const respond = (str: string) => 148 | handlers[0]({ 149 | id: 1, 150 | result: { 151 | type: 'data', 152 | data: str, 153 | }, 154 | }); 155 | 156 | respond('test 1'); 157 | respond('test 2'); 158 | respond('test 3'); 159 | 160 | expect(subscriptionResponse).toHaveBeenCalledTimes(3); 161 | expect(subscriptionComplete).not.toHaveBeenCalled(); 162 | 163 | /* 164 | * Unsubscribe informs the server 165 | */ 166 | subscription.unsubscribe(); 167 | 168 | expect(mock.sendMessage).toHaveBeenCalledTimes(2); 169 | expect(mock.sendMessage.mock.calls[1]).toEqual([ 170 | { 171 | id: 1, 172 | method: 'subscription.stop', 173 | }, 174 | ]); 175 | 176 | expect(subscriptionComplete).toHaveBeenCalledTimes(1); 177 | 178 | /* 179 | * Should not receive any more messages after unsubscribing 180 | */ 181 | respond('test 4'); 182 | 183 | expect(subscriptionResponse).toHaveBeenCalledTimes(3); 184 | }); 185 | 186 | test('interlaces responses', async () => { 187 | const queryResponse1 = vi.fn(); 188 | const queryResponse2 = vi.fn(); 189 | const queryResponse3 = vi.fn(); 190 | 191 | const query1 = client.testQuery.query().then(queryResponse1); 192 | /* const query2 = */ client.testQuery.query().then(queryResponse2); 193 | const query3 = client.testQuery.query().then(queryResponse3); 194 | 195 | expect(mock.sendMessage).toHaveBeenCalledTimes(3); 196 | 197 | expect(queryResponse1).not.toHaveBeenCalled(); 198 | expect(queryResponse2).not.toHaveBeenCalled(); 199 | expect(queryResponse3).not.toHaveBeenCalled(); 200 | 201 | // Respond to queries in a different order 202 | handlers[0]({ 203 | id: 1, 204 | result: { 205 | type: 'data', 206 | data: 'query success 1', 207 | }, 208 | }); 209 | handlers[0]({ 210 | id: 3, 211 | result: { 212 | type: 'data', 213 | data: 'query success 3', 214 | }, 215 | }); 216 | 217 | await Promise.all([query1, query3]); 218 | 219 | expect(queryResponse1).toHaveBeenCalledTimes(1); 220 | expect(queryResponse1).toHaveBeenCalledWith('query success 1'); 221 | expect(queryResponse2).not.toHaveBeenCalled(); 222 | expect(queryResponse3).toHaveBeenCalledTimes(1); 223 | expect(queryResponse3).toHaveBeenCalledWith('query success 3'); 224 | }); 225 | }); 226 | 227 | test('serializes inputs/outputs', async () => { 228 | const client = createTRPCProxyClient({ 229 | transformer: superjson, 230 | links: [ipcLink()], 231 | }); 232 | 233 | const mock = vi.mocked(electronTRPC); 234 | const queryResponse = vi.fn(); 235 | 236 | const input = { 237 | str: 'my string', 238 | date: new Date('January 1, 2000 01:23:45'), 239 | bigint: BigInt(12345), 240 | }; 241 | 242 | const query = client.testInputs.query(input).then(queryResponse); 243 | 244 | expect(mock.sendMessage).toHaveBeenCalledTimes(1); 245 | expect(mock.sendMessage).toHaveBeenCalledWith({ 246 | method: 'request', 247 | operation: { 248 | context: {}, 249 | id: 1, 250 | input: superjson.serialize(input), 251 | path: 'testInputs', 252 | type: 'query', 253 | }, 254 | }); 255 | 256 | expect(queryResponse).not.toHaveBeenCalled(); 257 | 258 | handlers[0]({ 259 | id: 1, 260 | result: { 261 | type: 'data', 262 | data: superjson.serialize(input), 263 | }, 264 | }); 265 | 266 | await query; 267 | 268 | expect(queryResponse).toHaveBeenCalledTimes(1); 269 | expect(queryResponse).toHaveBeenCalledWith(input); 270 | }); 271 | 272 | test('serializes inputs with custom transformer', async () => { 273 | const client = createTRPCProxyClient({ 274 | transformer: { 275 | serialize: (input) => JSON.stringify(input), 276 | deserialize: (input) => JSON.parse(input), 277 | }, 278 | links: [ipcLink()], 279 | }); 280 | 281 | const mock = vi.mocked(electronTRPC); 282 | const queryResponse = vi.fn(); 283 | 284 | const input = { 285 | str: 'my string', 286 | date: new Date('January 1, 2000 01:23:45'), 287 | }; 288 | 289 | const query = client.testInputs.query(input).then(queryResponse); 290 | 291 | expect(mock.sendMessage).toHaveBeenCalledTimes(1); 292 | expect(mock.sendMessage).toHaveBeenCalledWith({ 293 | method: 'request', 294 | operation: { 295 | id: 1, 296 | context: {}, 297 | input: JSON.stringify(input), 298 | path: 'testInputs', 299 | type: 'query', 300 | }, 301 | }); 302 | 303 | expect(queryResponse).not.toHaveBeenCalled(); 304 | 305 | handlers[0]({ 306 | id: 1, 307 | result: { 308 | type: 'data', 309 | data: JSON.stringify(input), 310 | }, 311 | }); 312 | 313 | await query; 314 | 315 | expect(queryResponse).toHaveBeenCalledTimes(1); 316 | expect(queryResponse).toHaveBeenCalledWith({ ...input, date: input.date.toISOString() }); 317 | }); 318 | }); 319 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../constants'; 2 | export * from './ipcLink'; 3 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/renderer/ipcLink.ts: -------------------------------------------------------------------------------- 1 | import { Operation, TRPCClientError, TRPCLink } from '@trpc/client'; 2 | import type { AnyRouter, inferRouterContext, ProcedureType } from '@trpc/server'; 3 | import type { TRPCResponseMessage } from '@trpc/server/rpc'; 4 | import type { RendererGlobalElectronTRPC } from '../types'; 5 | import { observable, Observer } from '@trpc/server/observable'; 6 | import { transformResult } from './utils'; 7 | import debugFactory from 'debug'; 8 | 9 | const debug = debugFactory('electron-trpc:renderer:ipcLink'); 10 | 11 | type IPCCallbackResult = TRPCResponseMessage< 12 | unknown, 13 | inferRouterContext 14 | >; 15 | 16 | type IPCCallbacks = Observer< 17 | IPCCallbackResult, 18 | TRPCClientError 19 | >; 20 | 21 | type IPCRequest = { 22 | type: ProcedureType; 23 | callbacks: IPCCallbacks; 24 | op: Operation; 25 | }; 26 | 27 | const getElectronTRPC = () => { 28 | const electronTRPC: RendererGlobalElectronTRPC = (globalThis as any).electronTRPC; 29 | 30 | if (!electronTRPC) { 31 | throw new Error( 32 | 'Could not find `electronTRPC` global. Check that `exposeElectronTRPC` has been called in your preload file.' 33 | ); 34 | } 35 | 36 | return electronTRPC; 37 | }; 38 | 39 | class IPCClient { 40 | #pendingRequests = new Map(); 41 | #electronTRPC = getElectronTRPC(); 42 | 43 | constructor() { 44 | this.#electronTRPC.onMessage((response: TRPCResponseMessage) => { 45 | this.#handleResponse(response); 46 | }); 47 | } 48 | 49 | #handleResponse(response: TRPCResponseMessage) { 50 | debug('handling response', response); 51 | const request = response.id && this.#pendingRequests.get(response.id); 52 | if (!request) { 53 | return; 54 | } 55 | 56 | request.callbacks.next(response); 57 | 58 | if ('result' in response && response.result.type === 'stopped') { 59 | request.callbacks.complete(); 60 | } 61 | } 62 | 63 | request(op: Operation, callbacks: IPCCallbacks) { 64 | const { type, id } = op; 65 | 66 | this.#pendingRequests.set(id, { 67 | type, 68 | callbacks, 69 | op, 70 | }); 71 | 72 | this.#electronTRPC.sendMessage({ method: 'request', operation: op }); 73 | 74 | return () => { 75 | const callbacks = this.#pendingRequests.get(id)?.callbacks; 76 | 77 | this.#pendingRequests.delete(id); 78 | 79 | callbacks?.complete(); 80 | 81 | if (type === 'subscription') { 82 | this.#electronTRPC.sendMessage({ 83 | id, 84 | method: 'subscription.stop', 85 | }); 86 | } 87 | }; 88 | } 89 | } 90 | 91 | export function ipcLink(): TRPCLink { 92 | return (runtime) => { 93 | const client = new IPCClient(); 94 | 95 | return ({ op }) => { 96 | return observable((observer) => { 97 | op.input = runtime.transformer.serialize(op.input); 98 | 99 | const unsubscribe = client.request(op, { 100 | error(err) { 101 | observer.error(err as TRPCClientError); 102 | unsubscribe(); 103 | }, 104 | complete() { 105 | observer.complete(); 106 | }, 107 | next(response) { 108 | const transformed = transformResult(response, runtime); 109 | 110 | if (!transformed.ok) { 111 | observer.error(TRPCClientError.from(transformed.error)); 112 | return; 113 | } 114 | 115 | observer.next({ result: transformed.result }); 116 | 117 | if (op.type !== 'subscription') { 118 | unsubscribe(); 119 | observer.complete(); 120 | } 121 | }, 122 | }); 123 | 124 | return () => { 125 | unsubscribe(); 126 | }; 127 | }); 128 | }; 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "lib": ["dom", "es2017"], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "target": "esnext" 17 | }, 18 | "include": ["src/main/*.ts", "src/renderer/*.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/renderer/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AnyRouter, inferRouterError } from '@trpc/server'; 2 | import type { TRPCResponse, TRPCResponseMessage, TRPCResultMessage } from '@trpc/server/rpc'; 3 | import type { TRPCClientRuntime } from '@trpc/client'; 4 | 5 | // from @trpc/client/src/links/internals/transformResult 6 | // FIXME: 7 | // - the generics here are probably unnecessary 8 | // - the RPC-spec could probably be simplified to combine HTTP + WS 9 | /** @internal */ 10 | export function transformResult( 11 | response: 12 | | TRPCResponseMessage> 13 | | TRPCResponse>, 14 | runtime: TRPCClientRuntime 15 | ) { 16 | if ('error' in response) { 17 | const error = runtime.transformer.deserialize(response.error) as inferRouterError; 18 | return { 19 | ok: false, 20 | error: { 21 | ...response, 22 | error, 23 | }, 24 | } as const; 25 | } 26 | 27 | const result = { 28 | ...response.result, 29 | ...((!response.result.type || response.result.type === 'data') && { 30 | type: 'data', 31 | data: runtime.transformer.deserialize(response.result.data) as unknown, 32 | }), 33 | } as TRPCResultMessage['result']; 34 | return { ok: true, result } as const; 35 | } 36 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/renderer/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import path from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | module.exports = defineConfig({ 6 | base: './', 7 | build: { 8 | // Importantly, `main` build runs first and empties the out dir 9 | emptyOutDir: false, 10 | lib: { 11 | entry: path.resolve(__dirname, './index.ts'), 12 | name: 'electron-trpc', 13 | formats: ['es', 'cjs'], 14 | fileName: (format) => ({ es: 'renderer.mjs', cjs: 'renderer.cjs' })[format as 'es' | 'cjs'], 15 | }, 16 | outDir: path.resolve(__dirname, '../../dist'), 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/electron-trpc/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Operation } from '@trpc/client'; 2 | import type { TRPCResponseMessage } from '@trpc/server/rpc'; 3 | 4 | export type ETRPCRequest = 5 | | { method: 'request'; operation: Operation } 6 | | { method: 'subscription.stop'; id: number }; 7 | 8 | export interface RendererGlobalElectronTRPC { 9 | sendMessage: (args: ETRPCRequest) => void; 10 | onMessage: (callback: (args: TRPCResponseMessage) => void) => void; 11 | } 12 | -------------------------------------------------------------------------------- /packages/electron-trpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "isolatedModules": true, 7 | "lib": ["dom", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "target": "esnext" 17 | }, 18 | "include": ["src/main/*.ts", "src/renderer/*.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/electron-trpc/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import path from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | module.exports = defineConfig({ 6 | test: { 7 | coverage: { 8 | all: true, 9 | include: ['src/**/*'], 10 | reporter: ['text', 'cobertura', 'html'], 11 | reportsDirectory: path.resolve(__dirname, './coverage/'), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | testMatch: /.*\.e2e\.ts/, 5 | }); 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | - 'packages/*' 4 | - 'www' 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "ignorePaths": [], 5 | "ignoreDeps": ["electron-trpc"], 6 | "semanticCommits": "disabled", 7 | "packageRules": [ 8 | { 9 | "matchPackagePatterns": ["^@trpc/"], 10 | "groupName": "trpc monorepo" 11 | }, 12 | { 13 | "matchPackageNames": ["electron"], 14 | "groupName": "electron" 15 | }, 16 | { 17 | "matchDepTypes": ["devDependencies"], 18 | "excludePackagePatterns": ["^@trpc/"], 19 | "excludeDepNames": ["electron"], 20 | "groupName": "dev dependencies", 21 | "extends": ["schedule:weekly"] 22 | }, 23 | { 24 | "matchDepTypes": ["dependencies"], 25 | "excludePackagePatterns": ["^@trpc/"], 26 | "excludeDepNames": ["electron"], 27 | "groupName": "dependencies", 28 | "extends": ["schedule:weekly"] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import (fetchTarball 2 | "https://github.com/NixOS/nixpkgs/archive/8e65989a9972ce1da033f445d2598683590dfb8a.tar.gz") 3 | { } }: 4 | 5 | let 6 | nodejs = pkgs.nodejs_20; 7 | electron = pkgs.electron_30; 8 | pnpm = pkgs.nodePackages.pnpm.overrideAttrs (oldAttrs: rec { 9 | version = "9.15.0"; 10 | src = pkgs.fetchurl { 11 | url = "https://registry.npmjs.org/pnpm/-/pnpm-${version}.tgz"; 12 | sha512 = 13 | "sha512-duI3l2CkMo7EQVgVvNZije5yevN3mqpMkU45RBVsQpmSGon5djge4QfUHxLPpLZmgcqccY8GaPoIMe1MbYulbA=="; 14 | }; 15 | }); 16 | in pkgs.mkShell { 17 | buildInputs = [ 18 | nodejs 19 | electron 20 | pkgs.git 21 | pkgs.xvfb-run 22 | pnpm 23 | ]; 24 | 25 | PLAYWRIGHT_ELECTRON_PATH = "${electron}/bin/electron"; 26 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1; 27 | } 28 | -------------------------------------------------------------------------------- /www/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import starlight from '@astrojs/starlight'; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | site: 'https://electron-trpc.dev', 7 | integrations: [ 8 | starlight({ 9 | title: 'electron-trpc', 10 | social: { 11 | github: 'https://github.com/jsonnull/electron-trpc', 12 | }, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.1", 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro check && astro build", 10 | "preview": "astro preview", 11 | "astro": "astro", 12 | "publish-pages": "wrangler pages deploy" 13 | }, 14 | "dependencies": { 15 | "@astrojs/check": "^0.7.0", 16 | "@astrojs/starlight": "^0.23.2", 17 | "astro": "^4.8.6", 18 | "sharp": "^0.32.5", 19 | "typescript": "^5.4.5" 20 | }, 21 | "devDependencies": { 22 | "wrangler": "^3.58.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /www/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /www/src/content/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting Started' 3 | description: 'How to start using electron-trpc in an Electron app.' 4 | --- 5 | 6 | import { Code } from '@astrojs/starlight/components'; 7 | 8 | export const preloadCode = ` 9 | import { exposeElectronTRPC } from 'electron-trpc/main'; 10 | 11 | process.once('loaded', async () => { 12 | exposeElectronTRPC(); 13 | }); 14 | `.trim(); 15 | 16 | export const mainCode = ` 17 | import { app } from 'electron'; 18 | import { createIPCHandler } from 'electron-trpc/main'; 19 | import { router } from './api'; 20 | 21 | app.on('ready', () => { 22 | const win = new BrowserWindow({ 23 | webPreferences: { 24 | // Replace this path with the path to your BUILT preload file 25 | preload: 'path/to/preload.js', 26 | }, 27 | }); 28 | 29 | createIPCHandler({ router, windows: [win] }); 30 | }); 31 | `.trim(); 32 | 33 | export const rendererCode = ` 34 | import { createTRPCProxyClient } from '@trpc/client'; 35 | import { ipcLink } from 'electron-trpc/renderer'; 36 | 37 | export const client = createTRPCProxyClient({ 38 | links: [ipcLink()], 39 | }); 40 | `.trim(); 41 | 42 | ## Installation 43 | 44 | Follow installation instructions for [trpc](https://trpc.io/docs/quickstart#installation) to build your router and client of choice. 45 | 46 | #### pnpm 47 | 48 | ```sh 49 | pnpm add electron-trpc 50 | ``` 51 | 52 | #### yarn 53 | 54 | ```sh 55 | yarn add electron-trpc 56 | ``` 57 | 58 | #### npm 59 | 60 | ```sh 61 | npm install --save electron-trpc 62 | ``` 63 | 64 | #### TypeScript 65 | 66 | It's worth noting that you'll need to figure out how to get TypeScript working on both the main process and render process client code. For one example of how to do this with a good developer experience (minimal configuration, fast bundling, client hot-reloading) see our [basic example](https://github.com/jsonnull/electron-trpc/tree/main/examples/basic). 67 | 68 | ## Code 69 | 70 | ### Preload 71 | 72 | `electron-trpc` depends on Electron's [Context Isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) feature, and exposes the electron-trpc IPC channel to render processes using a [preload file](https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts). 73 | 74 | Some familiarization with these concepts can be helpful in case of unexpected issues during setup. 75 | 76 | This is the most minimal working preload file for using `electron-trpc`. Depending on your application, you may need to add this to an existing preload file or customize it later. 77 | 78 | 79 | 80 | ### Main 81 | 82 | In the main electron process, you will want to expose a tRPC router to one or more windows. These windows need to use the preload file you created. 83 | 84 | 85 | 86 | ### Renderer 87 | 88 | Windows you construct with the preload file and the IPC handler can reach the tRPC router in the main process over IPC. To do this, a script in the window needs to create a tRPC client using the IPC link: 89 | 90 | 91 | 92 | To use a different client, follow the appropriate usage instructions in the tRPC docs, ensuring that you substitute any HTTP or websocket links with the `ipcLink`. 93 | -------------------------------------------------------------------------------- /www/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: electron-trpc 3 | description: Ergonomic and type-safe solution for building IPC in Electron 4 | template: splash 5 | hero: 6 | tagline: Ergonomic and type-safe solution for building IPC in Electron 7 | actions: 8 | - text: Getting Started 9 | link: /getting-started/ 10 | icon: right-arrow 11 | variant: primary 12 | - text: Example Usage 13 | link: https://github.com/jsonnull/electron-trpc/tree/main/examples/ 14 | icon: external 15 | --- 16 | 17 | import { Card, CardGrid, Code } from '@astrojs/starlight/components'; 18 | 19 | 20 | 21 | Expose APIs from electron's main process to one or more renderer processes. 22 | 23 | 24 | Build electron IPC with all the benefits of tRPC, including inferred client types. 25 | 26 | 27 | Electron IPC is faster and more secure than opening local servers for IPC. 28 | 29 | 30 | Supports all tRPC features. No need to write complicated bi-directional IPC schemes. 31 | 32 | 33 | -------------------------------------------------------------------------------- /www/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } 4 | -------------------------------------------------------------------------------- /www/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "electron-trpc" 2 | pages_build_output_dir = "./dist" 3 | --------------------------------------------------------------------------------