@cyia/chrome-trpc 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import { TRPCLink } from '@trpc/client';
2
+ import type { AnyRouter, AnyTRPCRouter, inferTRPCClientTypes } from '@trpc/server';
3
+ import { TransformerOptions } from '@trpc/client/unstable-internals';
4
+ export type ChromeLinkOptions<TRouter extends AnyTRPCRouter> = TransformerOptions<inferTRPCClientTypes<TRouter>>;
5
+ export declare function chromeLink<TRouter extends AnyRouter>(opts: ChromeLinkOptions<TRouter> & {
6
+ port: chrome.runtime.Port;
7
+ }): TRPCLink<TRouter>;
@@ -0,0 +1 @@
1
+ export * from './chromeLink';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cyia/chrome-trpc",
3
- "version": "1.0.0",
4
- "description": "tRPC integration for VS Code Extension-Webview communication",
3
+ "version": "1.0.1",
4
+ "description": "tRPC Adapter for Chrome Extension",
5
5
  "module": "esm",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -10,7 +10,9 @@
10
10
  },
11
11
  "author": "wszgrcy",
12
12
  "license": "MIT",
13
- "repository": {},
13
+ "repository": {
14
+ "url": "https://github.com/wszgrcy/chrome-trpc"
15
+ },
14
16
  "homepage": "https://github.com/wszgrcy/chrome-trpc/tree/main/packages/trpc#readme",
15
17
  "bugs": {
16
18
  "url": "https://github.com/wszgrcy/chrome-trpc/issues"
package/readme.md CHANGED
@@ -1,192 +1,192 @@
1
- # @cyia/chrome-trpc
2
-
3
- [English](./readme.md) | [中文](./readme.zh-hans.md)
4
-
5
- [![npm version](https://badge.fury.io/js/%40cyia%2Fchrome-trpc.svg)](https://badge.fury.io/js/%40cyia%2Fchrome-trpc)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- A **tRPC adapter** tailored for Chrome extensions, enabling you to make type-safe remote procedure calls securely and conveniently between different extension contexts (popup, content script, background).
9
- It deeply integrates Chrome’s internal `runtime.connect` / `tabs.connect` channels and includes built-in binary data compression, greatly improving the transmission efficiency of large `Uint8Array` payloads.
10
-
11
- ## ✨ Features
12
-
13
- - **Full type safety**: End-to-end type inference powered by tRPC, eliminating manual message parsing.
14
- - **Three communication modes out of the box**:
15
- - Popup → Content Script
16
- - Content Script → Background
17
- - Popup → Background
18
- - **Binary compression**: Automatically compresses `Uint8Array` using Snappy, with fine-grained control over compression scope to reduce message size and speed up transmission.
19
- - **Zero-configuration server**: The server (receiving end) only needs a single line of code to start listening for connections.
20
- - **Lightweight**: Core logic is concise and does not intrude into your existing architecture.
21
- - **Native Promise support**: Calling a remote function feels just like a local async call.
22
-
23
- ## 📦 Installation
24
-
25
- ```bash
26
- npm install @cyia/chrome-trpc @trpc/client @trpc/server
27
- ```
28
-
29
- Or use yarn/pnpm.
30
- `@trpc/client` and `@trpc/server` are peer dependencies; please ensure compatible versions (^11.18.0).
31
-
32
- ## 🚀 Quick Start
33
-
34
- ### 1. Define a shared router (types)
35
-
36
- Define your router in a shared `router.ts` file using `@trpc/server` so that both ends can reuse the types.
37
-
38
- ```typescript
39
- import { initTRPC } from '@trpc/server';
40
- import { z } from 'zod'; // or another validation library like valibot
41
-
42
- const t = initTRPC.create();
43
-
44
- export const appRouter = t.router({
45
- greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
46
- getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
47
- });
48
-
49
- export type AppRouter = typeof appRouter;
50
- ```
51
-
52
- ### 2. Server (receiving end)
53
-
54
- In the script that provides the service (e.g., background or content script), use `createChromeHandler` to listen for connections from other parts.
55
-
56
- ```typescript
57
- // background.ts or content-script.ts
58
- import { createChromeHandler } from '@cyia/chrome-trpc/server';
59
- import { appRouter } from './router';
60
-
61
- // One-liner to start, automatically handles chrome.runtime.onConnect connections
62
- createChromeHandler({ router: appRouter });
63
- ```
64
-
65
- > 💡 Which script acts as the "server" depends on your communication direction.
66
- > For example: Popup → Background – Background is the server;
67
- > Popup → Content Script – Content Script is the server.
68
-
69
- ### 3. Client (calling end)
70
-
71
- In the script that initiates calls (e.g., popup or content script), create a tRPC client and pass in a Chrome port.
72
-
73
- ```typescript
74
- // popup.ts or content-script.ts
75
- import { createTRPCProxyClient } from '@trpc/client';
76
- import { chromeLink } from '@cyia/chrome-trpc/client';
77
- import type { AppRouter } from './router';
78
-
79
- // Establish connection to the target context (depends on communication mode)
80
- const port = chrome.runtime.connect();
81
-
82
- const trpc = createTRPCProxyClient<AppRouter>({
83
- links: [chromeLink({ port })],
84
- });
85
-
86
- // Now you can call remote procedures like local functions
87
- const greeting = await trpc.greet.query('World');
88
- console.log(greeting); // "Hello World"
89
- ```
90
-
91
- ## 🔌 Three Communication Modes Explained
92
-
93
- | Direction | Client (caller) | Server (receiver) | How to obtain the `port` |
94
- | ------------------------------ | --------------- | ----------------- | --------------------------------------------------------------------------- |
95
- | **Popup → Background** | Popup | Background | `chrome.runtime.connect()` |
96
- | **Content Script → Background** | Content Script | Background | `chrome.runtime.connect()` |
97
- | **Popup → Content Script** | Popup | Content Script | First get the tabId via `chrome.tabs.query`,<br>then `chrome.tabs.connect(tabId)` |
98
-
99
- ### Example: Popup calling Content Script
100
-
101
- **content-script.ts (server)**
102
-
103
- ```typescript
104
- import { createChromeHandler } from '@cyia/chrome-trpc/server';
105
- import { appRouter } from './router';
106
- createChromeHandler({ router: appRouter });
107
- ```
108
-
109
- **popup.ts (client)**
110
-
111
- ```typescript
112
- import { chromeLink } from '@cyia/chrome-trpc/client';
113
- import { createTRPCProxyClient } from '@trpc/client';
114
- import type { AppRouter } from './router';
115
-
116
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
117
- if (tab.id) {
118
- const port = chrome.tabs.connect(tab.id);
119
- const trpc = createTRPCProxyClient<AppRouter>({
120
- links: [chromeLink({ port })],
121
- });
122
- const data = await trpc.getData.query();
123
- }
124
- ```
125
-
126
- ## 📦 Binary Compression
127
-
128
- The adapter uses the [Snappy](https://github.com/zhipeng-jia/snappyjs) algorithm to compress and decompress `Uint8Array` data, significantly reducing the transfer size.
129
-
130
- ### Response Compression (server configuration)
131
-
132
- In some scenarios you may want to explicitly specify which response paths should be compressed, or a procedure may return data that is not a plain `Uint8Array` but is still large.
133
- In such cases you can pass the `compressPathObj` option to `createChromeHandler`. The keys are the procedure route paths, and a value of `true` enables compression.
134
-
135
- For example, suppose you extended your router with a `res` sub-router and a `cmp` query:
136
-
137
- ```typescript
138
- // router.ts extended
139
- export const appRouter = t.router({
140
- greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
141
- getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
142
- res: t.router({
143
- cmp: t.procedure.query(() => {
144
- return new Uint8Array([1, 2, 3, 4]);
145
- }),
146
- }),
147
- });
148
- ```
149
-
150
- Server configuration compressing the path `'res.cmp'`:
151
-
152
- ```typescript
153
- createChromeHandler({
154
- router: appRouter,
155
- // Explicitly declare the procedure paths whose responses should be compressed (relative to appRouter)
156
- compressPathObj: { 'res.cmp': true },
157
- });
158
- ```
159
-
160
- > Paths use `.` to separate router levels, e.g., `'res.cmp'` corresponds to `appRouter.res.cmp`.
161
- > When `compressPathObj` is specified, only the listed paths will have their responses forcibly compressed.
162
-
163
- ### Request Compression (client configuration)
164
-
165
- If the client needs to send large parameters (e.g., big arrays, complex objects), you can enable request compression via `context` at call time, without any server-side changes.
166
-
167
- ```typescript
168
- // Assume the router has a clientCmp procedure that accepts complex parameters
169
- // On the client side, pass { context: { compress: true } }
170
- const result = await trpc.clientCmp.query(
171
- { arr1: [1, 2, 3], num1: 1, str1: '1', b1: true },
172
- { context: { compress: true } },
173
- );
174
- ```
175
-
176
- This way, `Uint8Array` or serializable data in the request body will be automatically compressed before transmission. The server decompresses it transparently, making the process invisible to your business logic.
177
-
178
- By combining response compression and request compression, you can efficiently exchange large amounts of data between different parts of your Chrome extension.
179
-
180
- ## 💡 Notes
181
-
182
- - **Type sharing**: Make sure the `AppRouter` type is shared between client and server (usually extracted as a common file).
183
- - **Content script environment**: If the content script acts as the server, ensure it is properly injected via `content_scripts` in `manifest.json` and runs at the appropriate time.
184
- - **Manifest permissions**: Ensure the required permissions are declared in `manifest.json` (e.g., `"tabs"` for Popup → Content Script connections).
185
- - **Compression paths**: The paths in `compressPathObj` must exactly match the router structure, otherwise the configuration will not take effect.
186
- - **Request compression**: When enabling `compress: true` on the client, ensure the input parameters contain compressible data (like `Uint8Array`), otherwise the compression effect may be negligible.
187
-
188
- ## 🔗 Related Links
189
-
190
- - [GitHub Repository](https://github.com/wszgrcy/chrome-trpc)
191
- - [Issue Tracker](https://github.com/wszgrcy/chrome-trpc/issues)
192
- - [tRPC Official Documentation](https://trpc.io)
1
+ # @cyia/chrome-trpc
2
+
3
+ [English](./readme.md) | [中文](./readme.zh-hans.md)
4
+
5
+ [![npm version](https://badge.fury.io/js/%40cyia%2Fchrome-trpc.svg)](https://badge.fury.io/js/%40cyia%2Fchrome-trpc)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ A **tRPC adapter** tailored for Chrome extensions, enabling you to make type-safe remote procedure calls securely and conveniently between different extension contexts (popup, content script, background).
9
+ It deeply integrates Chrome’s internal `runtime.connect` / `tabs.connect` channels and includes built-in binary data compression, greatly improving the transmission efficiency of large `Uint8Array` payloads.
10
+
11
+ ## ✨ Features
12
+
13
+ - **Full type safety**: End-to-end type inference powered by tRPC, eliminating manual message parsing.
14
+ - **Three communication modes out of the box**:
15
+ - Popup → Content Script
16
+ - Content Script → Background
17
+ - Popup → Background
18
+ - **Binary compression**: Automatically compresses `Uint8Array` using Snappy, with fine-grained control over compression scope to reduce message size and speed up transmission.
19
+ - **Zero-configuration server**: The server (receiving end) only needs a single line of code to start listening for connections.
20
+ - **Lightweight**: Core logic is concise and does not intrude into your existing architecture.
21
+ - **Native Promise support**: Calling a remote function feels just like a local async call.
22
+
23
+ ## 📦 Installation
24
+
25
+ ```bash
26
+ npm install @cyia/chrome-trpc @trpc/client @trpc/server
27
+ ```
28
+
29
+ Or use yarn/pnpm.
30
+ `@trpc/client` and `@trpc/server` are peer dependencies; please ensure compatible versions (^11.18.0).
31
+
32
+ ## 🚀 Quick Start
33
+
34
+ ### 1. Define a shared router (types)
35
+
36
+ Define your router in a shared `router.ts` file using `@trpc/server` so that both ends can reuse the types.
37
+
38
+ ```typescript
39
+ import { initTRPC } from '@trpc/server';
40
+ import { z } from 'zod'; // or another validation library like valibot
41
+
42
+ const t = initTRPC.create();
43
+
44
+ export const appRouter = t.router({
45
+ greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
46
+ getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
47
+ });
48
+
49
+ export type AppRouter = typeof appRouter;
50
+ ```
51
+
52
+ ### 2. Server (receiving end)
53
+
54
+ In the script that provides the service (e.g., background or content script), use `createChromeHandler` to listen for connections from other parts.
55
+
56
+ ```typescript
57
+ // background.ts or content-script.ts
58
+ import { createChromeHandler } from '@cyia/chrome-trpc/server';
59
+ import { appRouter } from './router';
60
+
61
+ // One-liner to start, automatically handles chrome.runtime.onConnect connections
62
+ createChromeHandler({ router: appRouter });
63
+ ```
64
+
65
+ > 💡 Which script acts as the "server" depends on your communication direction.
66
+ > For example: Popup → Background – Background is the server;
67
+ > Popup → Content Script – Content Script is the server.
68
+
69
+ ### 3. Client (calling end)
70
+
71
+ In the script that initiates calls (e.g., popup or content script), create a tRPC client and pass in a Chrome port.
72
+
73
+ ```typescript
74
+ // popup.ts or content-script.ts
75
+ import { createTRPCProxyClient } from '@trpc/client';
76
+ import { chromeLink } from '@cyia/chrome-trpc/client';
77
+ import type { AppRouter } from './router';
78
+
79
+ // Establish connection to the target context (depends on communication mode)
80
+ const port = chrome.runtime.connect();
81
+
82
+ const trpc = createTRPCProxyClient<AppRouter>({
83
+ links: [chromeLink({ port })],
84
+ });
85
+
86
+ // Now you can call remote procedures like local functions
87
+ const greeting = await trpc.greet.query('World');
88
+ console.log(greeting); // "Hello World"
89
+ ```
90
+
91
+ ## 🔌 Three Communication Modes Explained
92
+
93
+ | Direction | Client (caller) | Server (receiver) | How to obtain the `port` |
94
+ | ------------------------------ | --------------- | ----------------- | --------------------------------------------------------------------------- |
95
+ | **Popup → Background** | Popup | Background | `chrome.runtime.connect()` |
96
+ | **Content Script → Background** | Content Script | Background | `chrome.runtime.connect()` |
97
+ | **Popup → Content Script** | Popup | Content Script | First get the tabId via `chrome.tabs.query`,<br>then `chrome.tabs.connect(tabId)` |
98
+
99
+ ### Example: Popup calling Content Script
100
+
101
+ **content-script.ts (server)**
102
+
103
+ ```typescript
104
+ import { createChromeHandler } from '@cyia/chrome-trpc/server';
105
+ import { appRouter } from './router';
106
+ createChromeHandler({ router: appRouter });
107
+ ```
108
+
109
+ **popup.ts (client)**
110
+
111
+ ```typescript
112
+ import { chromeLink } from '@cyia/chrome-trpc/client';
113
+ import { createTRPCProxyClient } from '@trpc/client';
114
+ import type { AppRouter } from './router';
115
+
116
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
117
+ if (tab.id) {
118
+ const port = chrome.tabs.connect(tab.id);
119
+ const trpc = createTRPCProxyClient<AppRouter>({
120
+ links: [chromeLink({ port })],
121
+ });
122
+ const data = await trpc.getData.query();
123
+ }
124
+ ```
125
+
126
+ ## 📦 Binary Compression
127
+
128
+ The adapter uses the [Snappy](https://github.com/zhipeng-jia/snappyjs) algorithm to compress and decompress `Uint8Array` data, significantly reducing the transfer size.
129
+
130
+ ### Response Compression (server configuration)
131
+
132
+ In some scenarios you may want to explicitly specify which response paths should be compressed, or a procedure may return data that is not a plain `Uint8Array` but is still large.
133
+ In such cases you can pass the `compressPathObj` option to `createChromeHandler`. The keys are the procedure route paths, and a value of `true` enables compression.
134
+
135
+ For example, suppose you extended your router with a `res` sub-router and a `cmp` query:
136
+
137
+ ```typescript
138
+ // router.ts extended
139
+ export const appRouter = t.router({
140
+ greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
141
+ getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
142
+ res: t.router({
143
+ cmp: t.procedure.query(() => {
144
+ return new Uint8Array([1, 2, 3, 4]);
145
+ }),
146
+ }),
147
+ });
148
+ ```
149
+
150
+ Server configuration compressing the path `'res.cmp'`:
151
+
152
+ ```typescript
153
+ createChromeHandler({
154
+ router: appRouter,
155
+ // Explicitly declare the procedure paths whose responses should be compressed (relative to appRouter)
156
+ compressPathObj: { 'res.cmp': true },
157
+ });
158
+ ```
159
+
160
+ > Paths use `.` to separate router levels, e.g., `'res.cmp'` corresponds to `appRouter.res.cmp`.
161
+ > When `compressPathObj` is specified, only the listed paths will have their responses forcibly compressed.
162
+
163
+ ### Request Compression (client configuration)
164
+
165
+ If the client needs to send large parameters (e.g., big arrays, complex objects), you can enable request compression via `context` at call time, without any server-side changes.
166
+
167
+ ```typescript
168
+ // Assume the router has a clientCmp procedure that accepts complex parameters
169
+ // On the client side, pass { context: { compress: true } }
170
+ const result = await trpc.clientCmp.query(
171
+ { arr1: [1, 2, 3], num1: 1, str1: '1', b1: true },
172
+ { context: { compress: true } },
173
+ );
174
+ ```
175
+
176
+ This way, `Uint8Array` or serializable data in the request body will be automatically compressed before transmission. The server decompresses it transparently, making the process invisible to your business logic.
177
+
178
+ By combining response compression and request compression, you can efficiently exchange large amounts of data between different parts of your Chrome extension.
179
+
180
+ ## 💡 Notes
181
+
182
+ - **Type sharing**: Make sure the `AppRouter` type is shared between client and server (usually extracted as a common file).
183
+ - **Content script environment**: If the content script acts as the server, ensure it is properly injected via `content_scripts` in `manifest.json` and runs at the appropriate time.
184
+ - **Manifest permissions**: Ensure the required permissions are declared in `manifest.json` (e.g., `"tabs"` for Popup → Content Script connections).
185
+ - **Compression paths**: The paths in `compressPathObj` must exactly match the router structure, otherwise the configuration will not take effect.
186
+ - **Request compression**: When enabling `compress: true` on the client, ensure the input parameters contain compressible data (like `Uint8Array`), otherwise the compression effect may be negligible.
187
+
188
+ ## 🔗 Related Links
189
+
190
+ - [GitHub Repository](https://github.com/wszgrcy/chrome-trpc)
191
+ - [Issue Tracker](https://github.com/wszgrcy/chrome-trpc/issues)
192
+ - [tRPC Official Documentation](https://trpc.io)
package/readme.zh-hans.md CHANGED
@@ -1,192 +1,192 @@
1
- # @cyia/chrome-trpc
2
-
3
- [English](./readme.md) | [中文](./readme.zh-hans.md)
4
-
5
- [![npm version](https://badge.fury.io/js/%40cyia%2Fchrome-trpc.svg)](https://badge.fury.io/js/%40cyia%2Fchrome-trpc)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- 为 Chrome 扩展(插件)量身定制的 **tRPC 适配器**,让您能在扩展的不同运行时(popup、content script、background)之间安全、便捷地进行类型安全的远程过程调用。
9
- 它深度整合了 Chrome 内部的 `runtime.connect` / `tabs.connect` 通道,并内置二进制数据压缩,大幅提升大型 `Uint8Array` 的传输效率。
10
-
11
- ## ✨ 特性
12
-
13
- - **全类型安全**:基于 tRPC 实现端到端类型推导,杜绝手动解析消息。
14
- - **三种通讯方式开箱即用**:
15
- - Popup → Content Script
16
- - Content Script → Background
17
- - Popup → Background
18
- - **二进制压缩**:自动使用 Snappy 压缩 `Uint8Array`,并可精细控制压缩范围,减少消息体积,加快传输。
19
- - **零配置服务端**:服务端(接收端)只需一行代码即可监听连接。
20
- - **轻量级**:核心逻辑简洁,不侵入现有架构。
21
- - **原生 Promise 支持**:调用远程函数如同本地异步调用。
22
-
23
- ## 📦 安装
24
-
25
- ```bash
26
- npm install @cyia/chrome-trpc @trpc/client @trpc/server
27
- ```
28
-
29
- 或使用 yarn/pnpm。
30
- `@trpc/client` 与 `@trpc/server` 为 peer dependencies,请确保版本兼容(^11.18.0)。
31
-
32
- ## 🚀 快速开始
33
-
34
- ### 1. 定义共享路由(类型)
35
-
36
- 在一个公共的 `router.ts` 文件中使用 `@trpc/server` 定义路由,以便两端复用类型。
37
-
38
- ```typescript
39
- import { initTRPC } from '@trpc/server';
40
- import { z } from 'zod'; // 或其他验证库,如 valibot
41
-
42
- const t = initTRPC.create();
43
-
44
- export const appRouter = t.router({
45
- greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
46
- getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
47
- });
48
-
49
- export type AppRouter = typeof appRouter;
50
- ```
51
-
52
- ### 2. 服务端(接收端)
53
-
54
- 在需要提供服务的脚本(如 background 或 content script)中,使用 `createChromeHandler` 监听来自其他部分的连接。
55
-
56
- ```typescript
57
- // background.ts 或 content-script.ts
58
- import { createChromeHandler } from '@cyia/chrome-trpc/server';
59
- import { appRouter } from './router';
60
-
61
- // 一行启动,自动处理 chrome.runtime.onConnect 连接
62
- createChromeHandler({ router: appRouter });
63
- ```
64
-
65
- > 💡 哪个脚本作为“服务端”,取决于您的通讯方向。
66
- > 例如:Popup → Background 时,Background 为服务端;
67
- > Popup → Content Script 时,Content Script 为服务端。
68
-
69
- ### 3. 客户端(调用端)
70
-
71
- 在需要发起调用的脚本(如 popup 或 content script)中,创建 tRPC 客户端并传入 Chrome 端口。
72
-
73
- ```typescript
74
- // popup.ts 或 content-script.ts
75
- import { createTRPCProxyClient } from '@trpc/client';
76
- import { chromeLink } from '@cyia/chrome-trpc/client';
77
- import type { AppRouter } from './router';
78
-
79
- // 建立与目标运行时的连接(取决于通讯方式)
80
- const port = chrome.runtime.connect();
81
-
82
- const trpc = createTRPCProxyClient<AppRouter>({
83
- links: [chromeLink({ port })],
84
- });
85
-
86
- // 现在即可像调用本地函数一样调用远程过程
87
- const greeting = await trpc.greet.query('世界');
88
- console.log(greeting); // "Hello 世界"
89
- ```
90
-
91
- ## 🔌 三种通讯方式详解
92
-
93
- | 方向 | 客户端(调用方) | 服务端(接收方) | 如何获取 `port` |
94
- | ------------------------------- | ---------------- | ---------------- | ---------------------------------------------------------------------------- |
95
- | **Popup → Background** | Popup | Background | `chrome.runtime.connect()` |
96
- | **Content Script → Background** | Content Script | Background | `chrome.runtime.connect()` |
97
- | **Popup → Content Script** | Popup | Content Script | 先通过 `chrome.tabs.query` 获取 tabId,<br>然后 `chrome.tabs.connect(tabId)` |
98
-
99
- ### 示例:Popup 调用 Content Script
100
-
101
- **content-script.ts (服务端)**
102
-
103
- ```typescript
104
- import { createChromeHandler } from '@cyia/chrome-trpc/server';
105
- import { appRouter } from './router';
106
- createChromeHandler({ router: appRouter });
107
- ```
108
-
109
- **popup.ts (客户端)**
110
-
111
- ```typescript
112
- import { chromeLink } from '@cyia/chrome-trpc/client';
113
- import { createTRPCProxyClient } from '@trpc/client';
114
- import type { AppRouter } from './router';
115
-
116
- const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
117
- if (tab.id) {
118
- const port = chrome.tabs.connect(tab.id);
119
- const trpc = createTRPCProxyClient<AppRouter>({
120
- links: [chromeLink({ port })],
121
- });
122
- const data = await trpc.getData.query();
123
- }
124
- ```
125
-
126
- ## 📦 二进制压缩
127
-
128
- 适配器使用 [Snappy](https://github.com/zhipeng-jia/snappyjs) 算法对 `Uint8Array` 数据进行压缩与解压,大幅降低传输体积。
129
-
130
- ### 响应内容压缩(服务端配置)
131
-
132
- 某些场景下,您可能希望显式指定需要压缩的响应路径,或者过程返回的数据并非纯 `Uint8Array` 但体积较大。
133
- 此时可在 `createChromeHandler` 中传入 `compressPathObj` 参数,键为过程的路由路径,值为 `true` 表示启用压缩。
134
-
135
- 例如,假设您的路由中定义了 `res` 子路由和 `cmp` 查询:
136
-
137
- ```typescript
138
- // router.ts 扩展
139
- export const appRouter = t.router({
140
- greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
141
- getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
142
- res: t.router({
143
- cmp: t.procedure.query(() => {
144
- return new Uint8Array([1, 2, 3, 4]);
145
- }),
146
- }),
147
- });
148
- ```
149
-
150
- 服务端配置压缩路径 `'res.cmp'`:
151
-
152
- ```typescript
153
- createChromeHandler({
154
- router: appRouter,
155
- // 显式声明需要压缩响应的过程路径(相对 appRouter)
156
- compressPathObj: { 'res.cmp': true },
157
- });
158
- ```
159
-
160
- > 路径使用 `.` 分隔路由层级,如 `'res.cmp'` 对应 `appRouter.res.cmp`。
161
- > 指定了 `compressPathObj` ,只有列表中标记的会强制压缩响应体
162
-
163
- ### 请求参数压缩(客户端配置)
164
-
165
- 如果客户端需要发送大型参数(如大数组、复杂对象),可以在调用时通过 `context` 启用请求压缩,无需修改服务端配置。
166
-
167
- ```typescript
168
- // 假设路由中存在 clientCmp 过程,接受复杂参数
169
- // 客户端调用时,传入 { context: { compress: true } }
170
- const result = await trpc.clientCmp.query(
171
- { arr1: [1, 2, 3], num1: 1, str1: '1', b1: true },
172
- { context: { compress: true } },
173
- );
174
- ```
175
-
176
- 这样,请求体中的 `Uint8Array` 或可序列化数据会被自动压缩后再传输,服务端收到后自动解压,整个过程对业务逻辑透明。
177
-
178
- 结合响应压缩与请求压缩,您可以高效地在 Chrome 扩展的不同部分之间交换大量数据。
179
-
180
- ## 💡 注意事项
181
-
182
- - **类型共享**:务必确保 `AppRouter` 类型在客户端与服务端之间共享(通常抽取为公共文件)。
183
- - **Content Script 环境**:若 Content Script 作为服务端,请确保它在 `manifest.json` 的 `content_scripts` 中正确注入,且运行在适当的时机。
184
- - **Manifest 权限**:确保 `manifest.json` 中已声明所需的权限(如 `"tabs"` 用于 Popup → Content Script 连接)。
185
- - **压缩路径**:`compressPathObj` 中的路径必须与路由结构精确匹配,否则配置不会生效。
186
- - **请求压缩**:客户端启用 `compress: true` 时,请确保输入参数中包含可压缩的数据(如 `Uint8Array`),否则压缩效果不明显。
187
-
188
- ## 🔗 相关链接
189
-
190
- - [GitHub 仓库](https://github.com/wszgrcy/chrome-trpc)
191
- - [问题反馈](https://github.com/wszgrcy/chrome-trpc/issues)
192
- - [tRPC 官方文档](https://trpc.io)
1
+ # @cyia/chrome-trpc
2
+
3
+ [English](./readme.md) | [中文](./readme.zh-hans.md)
4
+
5
+ [![npm version](https://badge.fury.io/js/%40cyia%2Fchrome-trpc.svg)](https://badge.fury.io/js/%40cyia%2Fchrome-trpc)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ 为 Chrome 扩展(插件)量身定制的 **tRPC 适配器**,让您能在扩展的不同运行时(popup、content script、background)之间安全、便捷地进行类型安全的远程过程调用。
9
+ 它深度整合了 Chrome 内部的 `runtime.connect` / `tabs.connect` 通道,并内置二进制数据压缩,大幅提升大型 `Uint8Array` 的传输效率。
10
+
11
+ ## ✨ 特性
12
+
13
+ - **全类型安全**:基于 tRPC 实现端到端类型推导,杜绝手动解析消息。
14
+ - **三种通讯方式开箱即用**:
15
+ - Popup → Content Script
16
+ - Content Script → Background
17
+ - Popup → Background
18
+ - **二进制压缩**:自动使用 Snappy 压缩 `Uint8Array`,并可精细控制压缩范围,减少消息体积,加快传输。
19
+ - **零配置服务端**:服务端(接收端)只需一行代码即可监听连接。
20
+ - **轻量级**:核心逻辑简洁,不侵入现有架构。
21
+ - **原生 Promise 支持**:调用远程函数如同本地异步调用。
22
+
23
+ ## 📦 安装
24
+
25
+ ```bash
26
+ npm install @cyia/chrome-trpc @trpc/client @trpc/server
27
+ ```
28
+
29
+ 或使用 yarn/pnpm。
30
+ `@trpc/client` 与 `@trpc/server` 为 peer dependencies,请确保版本兼容(^11.18.0)。
31
+
32
+ ## 🚀 快速开始
33
+
34
+ ### 1. 定义共享路由(类型)
35
+
36
+ 在一个公共的 `router.ts` 文件中使用 `@trpc/server` 定义路由,以便两端复用类型。
37
+
38
+ ```typescript
39
+ import { initTRPC } from '@trpc/server';
40
+ import { z } from 'zod'; // 或其他验证库,如 valibot
41
+
42
+ const t = initTRPC.create();
43
+
44
+ export const appRouter = t.router({
45
+ greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
46
+ getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
47
+ });
48
+
49
+ export type AppRouter = typeof appRouter;
50
+ ```
51
+
52
+ ### 2. 服务端(接收端)
53
+
54
+ 在需要提供服务的脚本(如 background 或 content script)中,使用 `createChromeHandler` 监听来自其他部分的连接。
55
+
56
+ ```typescript
57
+ // background.ts 或 content-script.ts
58
+ import { createChromeHandler } from '@cyia/chrome-trpc/server';
59
+ import { appRouter } from './router';
60
+
61
+ // 一行启动,自动处理 chrome.runtime.onConnect 连接
62
+ createChromeHandler({ router: appRouter });
63
+ ```
64
+
65
+ > 💡 哪个脚本作为“服务端”,取决于您的通讯方向。
66
+ > 例如:Popup → Background 时,Background 为服务端;
67
+ > Popup → Content Script 时,Content Script 为服务端。
68
+
69
+ ### 3. 客户端(调用端)
70
+
71
+ 在需要发起调用的脚本(如 popup 或 content script)中,创建 tRPC 客户端并传入 Chrome 端口。
72
+
73
+ ```typescript
74
+ // popup.ts 或 content-script.ts
75
+ import { createTRPCProxyClient } from '@trpc/client';
76
+ import { chromeLink } from '@cyia/chrome-trpc/client';
77
+ import type { AppRouter } from './router';
78
+
79
+ // 建立与目标运行时的连接(取决于通讯方式)
80
+ const port = chrome.runtime.connect();
81
+
82
+ const trpc = createTRPCProxyClient<AppRouter>({
83
+ links: [chromeLink({ port })],
84
+ });
85
+
86
+ // 现在即可像调用本地函数一样调用远程过程
87
+ const greeting = await trpc.greet.query('世界');
88
+ console.log(greeting); // "Hello 世界"
89
+ ```
90
+
91
+ ## 🔌 三种通讯方式详解
92
+
93
+ | 方向 | 客户端(调用方) | 服务端(接收方) | 如何获取 `port` |
94
+ | ------------------------------- | ---------------- | ---------------- | ---------------------------------------------------------------------------- |
95
+ | **Popup → Background** | Popup | Background | `chrome.runtime.connect()` |
96
+ | **Content Script → Background** | Content Script | Background | `chrome.runtime.connect()` |
97
+ | **Popup → Content Script** | Popup | Content Script | 先通过 `chrome.tabs.query` 获取 tabId,<br>然后 `chrome.tabs.connect(tabId)` |
98
+
99
+ ### 示例:Popup 调用 Content Script
100
+
101
+ **content-script.ts (服务端)**
102
+
103
+ ```typescript
104
+ import { createChromeHandler } from '@cyia/chrome-trpc/server';
105
+ import { appRouter } from './router';
106
+ createChromeHandler({ router: appRouter });
107
+ ```
108
+
109
+ **popup.ts (客户端)**
110
+
111
+ ```typescript
112
+ import { chromeLink } from '@cyia/chrome-trpc/client';
113
+ import { createTRPCProxyClient } from '@trpc/client';
114
+ import type { AppRouter } from './router';
115
+
116
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
117
+ if (tab.id) {
118
+ const port = chrome.tabs.connect(tab.id);
119
+ const trpc = createTRPCProxyClient<AppRouter>({
120
+ links: [chromeLink({ port })],
121
+ });
122
+ const data = await trpc.getData.query();
123
+ }
124
+ ```
125
+
126
+ ## 📦 二进制压缩
127
+
128
+ 适配器使用 [Snappy](https://github.com/zhipeng-jia/snappyjs) 算法对 `Uint8Array` 数据进行压缩与解压,大幅降低传输体积。
129
+
130
+ ### 响应内容压缩(服务端配置)
131
+
132
+ 某些场景下,您可能希望显式指定需要压缩的响应路径,或者过程返回的数据并非纯 `Uint8Array` 但体积较大。
133
+ 此时可在 `createChromeHandler` 中传入 `compressPathObj` 参数,键为过程的路由路径,值为 `true` 表示启用压缩。
134
+
135
+ 例如,假设您的路由中定义了 `res` 子路由和 `cmp` 查询:
136
+
137
+ ```typescript
138
+ // router.ts 扩展
139
+ export const appRouter = t.router({
140
+ greet: t.procedure.input(z.string()).query(({ input }) => `Hello ${input}`),
141
+ getData: t.procedure.query(() => ({ payload: new Uint8Array([1, 2, 3]) })),
142
+ res: t.router({
143
+ cmp: t.procedure.query(() => {
144
+ return new Uint8Array([1, 2, 3, 4]);
145
+ }),
146
+ }),
147
+ });
148
+ ```
149
+
150
+ 服务端配置压缩路径 `'res.cmp'`:
151
+
152
+ ```typescript
153
+ createChromeHandler({
154
+ router: appRouter,
155
+ // 显式声明需要压缩响应的过程路径(相对 appRouter)
156
+ compressPathObj: { 'res.cmp': true },
157
+ });
158
+ ```
159
+
160
+ > 路径使用 `.` 分隔路由层级,如 `'res.cmp'` 对应 `appRouter.res.cmp`。
161
+ > 指定了 `compressPathObj` ,只有列表中标记的会强制压缩响应体
162
+
163
+ ### 请求参数压缩(客户端配置)
164
+
165
+ 如果客户端需要发送大型参数(如大数组、复杂对象),可以在调用时通过 `context` 启用请求压缩,无需修改服务端配置。
166
+
167
+ ```typescript
168
+ // 假设路由中存在 clientCmp 过程,接受复杂参数
169
+ // 客户端调用时,传入 { context: { compress: true } }
170
+ const result = await trpc.clientCmp.query(
171
+ { arr1: [1, 2, 3], num1: 1, str1: '1', b1: true },
172
+ { context: { compress: true } },
173
+ );
174
+ ```
175
+
176
+ 这样,请求体中的 `Uint8Array` 或可序列化数据会被自动压缩后再传输,服务端收到后自动解压,整个过程对业务逻辑透明。
177
+
178
+ 结合响应压缩与请求压缩,您可以高效地在 Chrome 扩展的不同部分之间交换大量数据。
179
+
180
+ ## 💡 注意事项
181
+
182
+ - **类型共享**:务必确保 `AppRouter` 类型在客户端与服务端之间共享(通常抽取为公共文件)。
183
+ - **Content Script 环境**:若 Content Script 作为服务端,请确保它在 `manifest.json` 的 `content_scripts` 中正确注入,且运行在适当的时机。
184
+ - **Manifest 权限**:确保 `manifest.json` 中已声明所需的权限(如 `"tabs"` 用于 Popup → Content Script 连接)。
185
+ - **压缩路径**:`compressPathObj` 中的路径必须与路由结构精确匹配,否则配置不会生效。
186
+ - **请求压缩**:客户端启用 `compress: true` 时,请确保输入参数中包含可压缩的数据(如 `Uint8Array`),否则压缩效果不明显。
187
+
188
+ ## 🔗 相关链接
189
+
190
+ - [GitHub 仓库](https://github.com/wszgrcy/chrome-trpc)
191
+ - [问题反馈](https://github.com/wszgrcy/chrome-trpc/issues)
192
+ - [tRPC 官方文档](https://trpc.io)
@@ -0,0 +1,15 @@
1
+ import type { AnyRouter, inferRouterContext } from '@trpc/server';
2
+ type Awaitable<T> = T | Promise<T>;
3
+ type ChromHandleOptions<TRouter extends AnyRouter> = {
4
+ createContext?: () => Awaitable<inferRouterContext<TRouter>>;
5
+ router: TRouter;
6
+ compressPathObj?: Record<string, boolean>;
7
+ };
8
+ declare class ChromeHandler<TRouter extends AnyRouter> {
9
+ #private;
10
+ private options;
11
+ constructor(options: ChromHandleOptions<TRouter>);
12
+ init(): void;
13
+ }
14
+ export declare const createChromeHandler: <TRouter extends AnyRouter>(options: ChromHandleOptions<TRouter>) => ChromeHandler<TRouter>;
15
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { AnyTRPCRouter, inferRouterContext } from '@trpc/server';
2
+ import { Message } from '../type';
3
+ export declare function handleChromeMessage<TRouter extends AnyTRPCRouter>({ router, createContext, message, subscriptions, compressPathObj, port, }: {
4
+ router: TRouter;
5
+ createContext?: () => Promise<inferRouterContext<TRouter>>;
6
+ message: Message;
7
+ subscriptions: Map<string, AbortController>;
8
+ compressPathObj?: Record<string, boolean>;
9
+ port: chrome.runtime.Port;
10
+ }): Promise<void>;
@@ -0,0 +1,2 @@
1
+ export * from './createChromeHandler';
2
+ export * from './type';
@@ -0,0 +1,3 @@
1
+ export interface CreateContextOptions {
2
+ [name: string]: any;
3
+ }
@@ -0,0 +1,2 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ export declare function getTRPCErrorFromUnknown(cause: unknown): TRPCError;
package/type.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Operation } from '@trpc/client';
2
+ export interface Message {
3
+ method: string;
4
+ operation: Operation;
5
+ id: string;
6
+ }
package/util/json.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { SuperJSON, SuperJSONResult } from 'superjson';
2
+ declare class DefaultSuperJSON {
3
+ superJson: SuperJSON;
4
+ constructor();
5
+ }
6
+ declare const defaultSuperJSON: DefaultSuperJSON;
7
+ export declare function initJsonConvert(): void;
8
+ export declare function serialize(object: Parameters<typeof defaultSuperJSON.superJson.serialize>[0], compress?: boolean): string | SuperJSONResult;
9
+ export declare function deserialize<T = unknown>(payload: SuperJSONResult & {
10
+ __compress?: boolean;
11
+ }): T;
12
+ export {};