Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm build:code
- run: pnpm test:ci
# The site only deploys from main, so check that it still builds here.
- run: pnpm docs:build
- uses: codecov/codecov-action@v7

test-e2e:
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ npm install --save electron-trpc

## Preload Script

`createIPCHandler` registers electron-trpc's preload script on Electron's default session, so your app doesn't need one. If your windows use other sessions, pass them instead:
`createIPCHandler` registers electron-trpc's preload script on Electron's default session. If your windows use other sessions, pass them instead:

```ts
createIPCHandler({ router, preload: { sessions: [session.fromPartition('persist:app')] } });
```

The preload script is a file in the electron-trpc package, so your packaged app has to include it. That works without any setup when:
The preload script is a file in the electron-trpc package, so your packaged app has to include it. That doesn't need any setup when:

- Your app ships electron-trpc in `node_modules`, for example as a `dependency` packaged by electron-builder, whether or not your main process is bundled.
- Your main process is bundled with webpack, including electron-forge's webpack template. webpack copies the preload script into its output.
- Your app includes electron-trpc in `node_modules`, for example as a `dependency` packaged by electron-builder, whether or not your main process is bundled.
- Your main process is bundled with webpack, as in electron-forge's webpack template. webpack copies the preload script into its output.

If your build bundles electron-trpc into the main process without including its preload script, `createIPCHandler` throws an error saying it couldn't find the preload script. This usually happens when electron-trpc is a `devDependency`, or when your packager only ships your bundled code. It usually only shows up in packaged apps, so test a packaged build of your app.
If your build bundles electron-trpc into the main process without including its preload script, `createIPCHandler` throws an error saying it couldn't find the preload script. This usually happens when electron-trpc is a `devDependency`, or when your packager only includes your bundled code. It usually only shows up in packaged apps, so test a packaged build of your app.

To fix it, expose electron-trpc from your own preload script. Pass `preload: false` and call `exposeElectronTRPC` there:

Expand All @@ -101,7 +101,7 @@ exposeElectronTRPC();

## Restricting Access

The preload script exposes electron-trpc to every page in its sessions, including any remote content your app loads. To only handle requests from pages you trust, check the sender with `allow`. Requests it rejects fail with a `FORBIDDEN` error:
The preload script exposes electron-trpc to every page in its sessions. That includes remote content your app loads in those sessions. To only handle requests from pages you trust, check the sender with `allow`. Requests it rejects fail with a `FORBIDDEN` error:

```ts
createIPCHandler({
Expand Down
4,925 changes: 1,538 additions & 3,387 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions www/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ export default defineConfig({
integrations: [
starlight({
title: 'electron-trpc',
social: {
github: 'https://github.com/jsonnull/electron-trpc',
},
social: [
{ icon: 'github', label: 'GitHub', href: 'https://github.com/jsonnull/electron-trpc' },
],
sidebar: [
'getting-started',
{ label: 'Guides', items: ['preload-script', 'request-lifetimes', 'security'] },
'upgrading',
],
}),
],
});
12 changes: 6 additions & 6 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
"publish-pages": "wrangler pages deploy"
},
"dependencies": {
"@astrojs/check": "^0.7.0",
"@astrojs/starlight": "^0.23.2",
"astro": "^4.8.6",
"sharp": "^0.32.5",
"typescript": "^5.4.5"
"@astrojs/check": "^0.9.12",
"@astrojs/starlight": "^0.42.7",
"astro": "^7.4.4",
"sharp": "^0.35.5",
"typescript": "^6.0.3"
},
"devDependencies": {
"wrangler": "^3.58.0"
"wrangler": "4.154.0"
}
}
3 changes: 2 additions & 1 deletion www/src/content/config.ts → www/src/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({ schema: docsSchema() }),
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};
140 changes: 83 additions & 57 deletions www/src/content/docs/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -1,92 +1,118 @@
---
title: 'Getting Started'
description: 'How to start using electron-trpc in an Electron app.'
title: Getting Started
description: How to start using electron-trpc in an Electron app.
---

import { Code } from '@astrojs/starlight/components';
import { Aside, Steps, TabItem, Tabs } from '@astrojs/starlight/components';

export const preloadCode = `
import { exposeElectronTRPC } from 'electron-trpc/main';
electron-trpc lets the renderer processes of your Electron app call a [tRPC](https://trpc.io) router in the main process, over Electron's IPC.

process.once('loaded', async () => {
exposeElectronTRPC();
});
`.trim();
## Requirements

export const mainCode = `
import { app } from 'electron';
import { createIPCHandler } from 'electron-trpc/main';
import { router } from './api';

app.on('ready', () => {
const win = new BrowserWindow({
webPreferences: {
// Replace this path with the path to your BUILT preload file
preload: 'path/to/preload.js',
},
});

createIPCHandler({ router, windows: [win] });
});
`.trim();

export const rendererCode = `
import { createTRPCProxyClient } from '@trpc/client';
import { ipcLink } from 'electron-trpc/renderer';

export const client = createTRPCProxyClient({
links: [ipcLink()],
});
`.trim();
- Electron 35 or later
- tRPC 11.10 or later
- [Context isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation), which Electron enables by default

## Installation

Follow installation instructions for [trpc](https://trpc.io/docs/quickstart#installation) to build your router and client of choice.

#### pnpm
<Tabs syncKey="package-manager">
<TabItem label="pnpm">

```sh
pnpm add electron-trpc
pnpm add electron-trpc @trpc/server @trpc/client
```

#### yarn
</TabItem>
<TabItem label="npm">

```sh
yarn add electron-trpc
npm install electron-trpc @trpc/server @trpc/client
```

#### npm
</TabItem>
<TabItem label="yarn">

```sh
npm install --save electron-trpc
yarn add electron-trpc @trpc/server @trpc/client
```

#### TypeScript
</TabItem>
</Tabs>

You'll need TypeScript to work across your main and renderer code to get inferred types in the renderer. The [examples](https://github.com/jsonnull/electron-trpc/tree/main/examples) show setups with Vite and with webpack.

## Setup

<Steps>

1. Define a tRPC router in the main process, as you would for any tRPC server:

```ts title="api.ts"
import { EventEmitter, on } from 'node:events';
import { initTRPC } from '@trpc/server';
import z from 'zod';

const ee = new EventEmitter();
const t = initTRPC.create({ isServer: true });

export const router = t.router({
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello ${input.name}`),
greet: t.procedure.input(z.object({ name: z.string() })).mutation(({ input }) => {
ee.emit('greeting', `Greeted ${input.name}`);
}),
greetings: t.procedure.subscription(async function* ({ signal }) {
for await (const [text] of on(ee, 'greeting', { signal })) {
yield text as string;
}
}),
});

export type AppRouter = typeof router;
```

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).
2. Create an IPC handler for the router in the main process. It also registers electron-trpc's [preload script](/preload-script/), so create it before any window loads a page:

## Code
```ts title="main.ts" {6}
import { app, BrowserWindow } from 'electron';
import { createIPCHandler } from 'electron-trpc/main';
import { router } from './api';

### Preload
app.whenReady().then(() => {
createIPCHandler({ router });

`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).
const win = new BrowserWindow();
win.loadFile('index.html');
});
```

Some familiarization with these concepts can be helpful in case of unexpected issues during setup.
The handler answers requests from all of your app's windows.

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.
3. Create a tRPC client in the renderer with `ipcLink`, which takes the place of the HTTP and WebSocket links:

<Code lang="ts" title="preload.ts" code={preloadCode} />
```ts title="renderer.ts"
import { createTRPCClient } from '@trpc/client';
import { ipcLink } from 'electron-trpc/renderer';
import type { AppRouter } from './api';

### Main
export const client = createTRPCClient<AppRouter>({
links: [ipcLink()],
});

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.
const greeting = await client.greeting.query({ name: 'Electron' });
```

<Code lang="ts" title="main.ts" code={mainCode} mark={[7, 8, 9, 10, 13]} />
</Steps>

### Renderer
From here, use the client however you would with tRPC, including with [TanStack React Query](https://trpc.io/docs/client/tanstack-react-query/setup).

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:
<Aside title="Transformers">
If your router uses a data transformer like [superjson](https://github.com/flightcontrolhq/superjson), pass it to `ipcLink` as you would to tRPC's other links: `ipcLink({ transformer: superjson })`.
</Aside>

<Code lang="ts" title="renderer.ts" code={rendererCode} />
## Next steps

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`.
- [Preload script](/preload-script/): how electron-trpc reaches your pages, and what to do if your build can't include its preload script.
- [Request lifetimes](/request-lifetimes/): when requests and subscriptions end.
- [Security](/security/): limiting which pages can call your router.
14 changes: 9 additions & 5 deletions www/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,28 @@ hero:
link: /getting-started/
icon: right-arrow
variant: primary
- text: Example Usage
- text: Upgrading from 0.x
link: /upgrading/
variant: secondary
- text: Examples
link: https://github.com/jsonnull/electron-trpc/tree/main/examples/
icon: external
variant: minimal
---

import { Card, CardGrid, Code } from '@astrojs/starlight/components';
import { Card, CardGrid } from '@astrojs/starlight/components';

<CardGrid>
<Card title="Main <-> Renderer" icon="random">
Expose APIs from electron's main process to one or more renderer processes.
Expose APIs from Electron's main process to every window automatically.
</Card>
<Card title="Fully type-safe IPC" icon="seti:typescript">
Build electron IPC with all the benefits of tRPC, including inferred client types.
Build Electron IPC with all the benefits of tRPC 11, including inferred client types.
</Card>
<Card title="Secure platform alternative to localhost" icon="rocket">
Electron IPC is faster and more secure than opening local servers for IPC.
</Card>
<Card title="Queries, mutations, and subscriptions" icon="seti:graphql">
Supports all tRPC features. No need to write complicated bi-directional IPC schemes.
Supports tRPC's procedures, including async generator subscriptions.
</Card>
</CardGrid>
61 changes: 61 additions & 0 deletions www/src/content/docs/preload-script.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: Preload Script
description: How electron-trpc reaches your pages, and how to provide its preload script yourself.
---

import { Aside } from '@astrojs/starlight/components';

Renderers talk to the main process through a [preload script](https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts), which exposes electron-trpc's IPC channel to the page. `createIPCHandler` registers electron-trpc's own preload script.

## Sessions

The preload script is registered on Electron's default session, so it runs in every page that uses it. If your windows use [other sessions](https://www.electronjs.org/docs/latest/api/session), pass those instead:

```ts title="main.ts"
import { session } from 'electron';

createIPCHandler({
router,
preload: { sessions: [session.fromPartition('persist:app')] },
});
```

Register the preload script before your windows load their pages, since a page only gets the preload scripts registered when it loads. Disposing the handler unregisters it.

Your own preload scripts, set with `webPreferences.preload`, run alongside electron-trpc's.

## Packaged apps

The preload script is a file in the electron-trpc package, so your packaged app has to include it. That doesn't need any setup when:

- Your app includes electron-trpc in `node_modules`, for example as a `dependency` packaged by electron-builder, whether or not your main process is bundled.
- Your main process is bundled with webpack, as in electron-forge's webpack template. webpack copies the preload script into its output.

If your build bundles electron-trpc into the main process without including its preload script, `createIPCHandler` throws an error saying it couldn't find the preload script. This usually happens when electron-trpc is a `devDependency`, or when your packager only includes your bundled code.

<Aside type="caution" title="Test a packaged build">
This error usually only shows up in packaged apps, because the preload script is still on disk
while you develop. Run a packaged build of your app once you've set it up.
</Aside>

## Using your own preload script

To expose electron-trpc from a preload script of your own, pass `preload: false` and call `exposeElectronTRPC` from it:

```ts title="main.ts"
createIPCHandler({ router, preload: false });

const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
```

```ts title="preload.ts"
import { exposeElectronTRPC } from 'electron-trpc/preload';

exposeElectronTRPC();
```

Electron sandboxes renderers by default, and a sandboxed preload script can only `require` Electron's own modules. Bundle your preload script so that `electron-trpc/preload` is included in it, as electron-forge's templates do and the [manual preload example](https://github.com/jsonnull/electron-trpc/tree/main/examples/manual-preload-react) shows with Vite.
Loading
Loading