├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------