Skip to content

Commit 4432ff7

Browse files
committed
config vitest and add tests
1 parent e1e6f26 commit 4432ff7

28 files changed

Lines changed: 4855 additions & 25 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css,md}"
3535

3636
- name: Run tests
37-
run: npm test -- --run --passWithNoTests
37+
run: npm test -- --run
3838

3939
- name: Build library
4040
run: npm run build

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
run: npm run lint
3737

3838
- name: Run tests
39-
run: npm test -- --run 2>/dev/null
39+
run: npm test -- --run
4040

4141
- name: Build library
4242
run: npm run build

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
coverage
12
# Logs
23
logs
34
*.log

README.md

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
# react-vscode-webview-ipc
1+
# React + VSCode Webview = IPC
2+
3+
[![npm version](https://badge.fury.io/js/react-vscode-webview-ipc.svg)](https://www.npmjs.com/package/react-vscode-webview-ipc)
4+
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hbmartin/react-vscode-webview-ipc)
5+
[![CI](https://github.com/hbmartin/react-vscode-webview-ipc/actions/workflows/ci.yml/badge.svg)](https://github.com/hbmartin/react-vscode-webview-ipc/actions/workflows/ci.yml)
6+
[![NPM License](https://img.shields.io/npm/l/react-vscode-webview-ipc?color=blue)](https://github.com/hbmartin/react-vscode-webview-ipc/blob/main/LICENSE.txt)
27

38
A small library to make two-way communication between a VS Code extension host and a React webview simple and type‑safe.
49

510
Two complementary paradigms are supported (you can use one or both):
11+
612
- UDF reducer IPC: Dispatch actions from the webview; the host computes a patch; the webview applies it via a reducer. (unidirectional dataflow)
713
- RPC promises IPC: Call host functions from the webview and await typed results; the host can also push typed events to all connected webviews.
814

915
This README explains how to implement both and shows how they can coexist.
1016

11-
1217
## Install
1318

1419
- Install the package in your VS Code extension project.
@@ -21,7 +26,6 @@ npm i react-vscode-webview-ipc
2126
- `react-vscode-webview-ipc/host` for your extension host code
2227
- `react-vscode-webview-ipc/client` for your React webview code
2328

24-
2529
## Concepts Overview
2630

2731
- WebviewKey: a branded string identifying your view instance. Use a stable id (often your view type).
@@ -30,12 +34,12 @@ npm i react-vscode-webview-ipc
3034
- RPC IPC: `{ type: 'request' }` from webview → host; `{ type: 'response'|'error' }` from host → webview; `{ type: 'event' }` from host → webview broadcast.
3135
- Logging: webview logs are forwarded to the host’s Output channel.
3236

33-
3437
## UDF Reducer IPC (Action → Patch → Reduce)
3538

3639
Use this when your webview wants unidirectional state updates managed via a reducer.
3740

3841
### Types and Building Blocks
42+
3943
- On the webview:
4044
- `useVscodeState<S, A>(vscode, providerId, postReducer, initialState)` returns `[state, actor]`.
4145
- `state: S` – your current state
@@ -51,6 +55,7 @@ Use this when your webview wants unidirectional state updates managed via a redu
5155
### Minimal Example
5256

5357
Host (extension):
58+
5459
```ts
5560
// src/extension/MyViewProvider.ts
5661
import * as vscode from 'vscode';
@@ -109,6 +114,7 @@ export class MyViewProvider extends BaseWebviewViewProvider<MyActions> {
109114
```
110115

111116
Register the provider in your extension activation:
117+
112118
```ts
113119
// src/extension/activate.ts
114120
import * as vscode from 'vscode';
@@ -117,25 +123,31 @@ import { MyViewProvider } from './MyViewProvider';
117123
export function activate(context: vscode.ExtensionContext) {
118124
const viewType = 'myExtension.myView' as unknown as WebviewKey; // brand to WebviewKey
119125
const provider = new MyViewProvider(viewType, context);
120-
context.subscriptions.push(
121-
vscode.window.registerWebviewViewProvider(viewType, provider)
122-
);
126+
context.subscriptions.push(vscode.window.registerWebviewViewProvider(viewType, provider));
123127
}
124128
```
125129

126130
Webview (React):
131+
127132
```tsx
128133
// src/webview/App.tsx
129134
import { useMemo } from 'react';
130-
import { useVscodeState, type StateReducer, type WebviewKey } from 'react-vscode-webview-ipc/client';
135+
import {
136+
useVscodeState,
137+
type StateReducer,
138+
type WebviewKey,
139+
} from 'react-vscode-webview-ipc/client';
131140

132141
declare function acquireVsCodeApi(): {
133142
postMessage(message: unknown): Thenable<boolean>;
134143
getState(): unknown;
135144
setState(state: unknown): void;
136145
};
137146

138-
interface State { count: number; message: string }
147+
interface State {
148+
count: number;
149+
message: string;
150+
}
139151
interface MyActions {
140152
increment: (by: number) => number;
141153
setMessage: (msg: string) => { message: string };
@@ -165,6 +177,7 @@ export default function App() {
165177
```
166178

167179
### UDF Flow (Sequence)
180+
168181
```mermaid
169182
sequenceDiagram
170183
participant W as Webview (React)
@@ -178,12 +191,12 @@ sequenceDiagram
178191
W->>W: postReducer[key](prev, patch) => newState
179192
```
180193

181-
182194
## RPC Promises IPC (Typed Requests/Responses + Events)
183195

184196
Use this when your webview needs to call host functions and await results. The host can also broadcast typed events back to all connected webviews.
185197

186198
### Types and Building Blocks
199+
187200
- On the webview:
188201
- Wrap your app in `<WebviewProvider viewType contextKey>`.
189202
- Use `createCtxKey<T>()` to create a unique key tying the context to your API type `T`.
@@ -198,6 +211,7 @@ Use this when your webview needs to call host functions and await results. The h
198211
### Minimal Example
199212

200213
Shared types:
214+
201215
```ts
202216
// Host receives these requests from the webview (must return promises)
203217
import type { ClientCalls } from 'react-vscode-webview-ipc/client';
@@ -215,6 +229,7 @@ export interface MyHostEvents extends HostCalls {
215229
```
216230

217231
Host (extension):
232+
218233
```ts
219234
import * as vscode from 'vscode';
220235
import {
@@ -253,7 +268,9 @@ export class MyRpcViewProvider extends BaseWebviewViewProvider<{}> {
253268
const [name] = message.params;
254269
const value = `Hello, ${name}!`;
255270
const response: ViewApiResponse<MyClientApi, 'fetchGreeting'> = {
256-
type: 'response', id: message.id, value,
271+
type: 'response',
272+
id: message.id,
273+
value,
257274
};
258275
await webview.postMessage(response);
259276
return;
@@ -262,19 +279,24 @@ export class MyRpcViewProvider extends BaseWebviewViewProvider<{}> {
262279
const [count] = message.params;
263280
// persist count...
264281
const response: ViewApiResponse<MyClientApi, 'saveCount'> = {
265-
type: 'response', id: message.id,
282+
type: 'response',
283+
id: message.id,
266284
};
267285
await webview.postMessage(response);
268286
return;
269287
}
270288
}
271289
const error: ViewApiError = {
272-
type: 'error', id: message.id, value: `Unknown method: ${String(message.key)}`,
290+
type: 'error',
291+
id: message.id,
292+
value: `Unknown method: ${String(message.key)}`,
273293
};
274294
await webview.postMessage(error);
275295
} catch (e) {
276296
const error: ViewApiError = {
277-
type: 'error', id: message.id, value: e instanceof Error ? e.message : String(e),
297+
type: 'error',
298+
id: message.id,
299+
value: e instanceof Error ? e.message : String(e),
278300
};
279301
await webview.postMessage(error);
280302
}
@@ -287,6 +309,7 @@ export class MyRpcViewProvider extends BaseWebviewViewProvider<{}> {
287309
```
288310

289311
Webview (React):
312+
290313
```tsx
291314
import React, { useEffect } from 'react';
292315
import {
@@ -316,7 +339,7 @@ function Inner() {
316339
})();
317340
}, [api]);
318341

319-
return <div/>
342+
return <div />;
320343
}
321344

322345
export default function App() {
@@ -329,6 +352,7 @@ export default function App() {
329352
```
330353

331354
### RPC Flow (Sequence)
355+
332356
```mermaid
333357
sequenceDiagram
334358
participant W as Webview (React)
@@ -347,7 +371,6 @@ sequenceDiagram
347371
VS-->>W: Invoke registered listeners for key
348372
```
349373

350-
351374
## Using Both Paradigms Together
352375

353376
- They are designed to coexist. The webview can dispatch reducer actions for state, and call RPC methods for imperative operations.
@@ -356,20 +379,19 @@ sequenceDiagram
356379
- `WebviewProvider` listens for `{ type: 'response'|'error'|'event' }` messages and ignores messages with `providerId` present.
357380
- In your host provider, `resolveWebviewView` (from the base class) handles reducer `act/patch` automatically; implement `handleMessage` for RPC requests.
358381

359-
360382
## Logging
361383

362384
- Webview: `useLogger(tag, vscode)` returns a logger that posts to the host output channel.
363385
- Host: `getLogger(tag)` returns an Output channel logger; `BaseWebviewViewProvider` automatically routes webview log messages to it.
364386

365387
Webview example:
388+
366389
```ts
367390
import { useLogger } from 'react-vscode-webview-ipc/client';
368391
const logger = useLogger('MyView', acquireVsCodeApi());
369392
logger.info('hello');
370393
```
371394

372-
373395
## Tips & Troubleshooting
374396

375397
- Brand your view type to `WebviewKey` at the edges to keep types happy: `const id = 'ext.view' as unknown as WebviewKey`.
@@ -378,23 +400,23 @@ logger.info('hello');
378400
- When posting RPC responses/errors from the host, always echo the same `id` you received.
379401
- If you use both paradigms, keep your reducer patches focused on state updates and use RPC for IO or long‑running tasks.
380402

381-
382403
## API Surface (Quick Reference)
383404

384405
Host exports (`react-vscode-webview-ipc/host`):
406+
385407
- `BaseWebviewViewProvider<A>`
386408
- `WebviewApiProvider<T extends HostCalls>`
387409
- `isViewApiRequest(message)`
388410
- `Logger`, `getLogger`, `disallowedLogKeys`
389411

390412
Client exports (`react-vscode-webview-ipc/client`):
413+
391414
- `WebviewProvider<T extends ClientCalls>`
392415
- `useWebviewApi(ctxKey)` and `createCtxKey<T>()`
393416
- `useVscodeState<S, A>(vscode, providerId, postReducer, initial)`
394417
- `useLogger(tag, vscode)`
395418
- Types: `ClientCalls`, `HostCalls`, `CtxKey`, `WebviewKey`, `StateReducer`
396419

397-
398420
## License
399421

400422
Apache-2.0

eslint.config.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,70 @@ export default defineConfig([
217217
},
218218
{
219219
// Test files - more relaxed rules
220-
files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
220+
files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
221+
languageOptions: {
222+
globals: {
223+
...globals.browser,
224+
...globals.node,
225+
React: 'readonly',
226+
vi: 'readonly',
227+
describe: 'readonly',
228+
it: 'readonly',
229+
expect: 'readonly',
230+
beforeEach: 'readonly',
231+
afterEach: 'readonly',
232+
},
233+
parserOptions: {
234+
projectService: true,
235+
tsconfigRootDir: import.meta.dirname,
236+
},
237+
},
238+
settings: {
239+
'import/resolver': {
240+
typescript: {
241+
project: './tsconfig.test.json',
242+
},
243+
node: {
244+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
245+
},
246+
},
247+
},
221248
rules: {
222249
'@typescript-eslint/no-explicit-any': 'off',
250+
'@typescript-eslint/no-unsafe-assignment': 'off',
251+
'@typescript-eslint/no-unsafe-member-access': 'off',
252+
'@typescript-eslint/no-unsafe-call': 'off',
253+
'@typescript-eslint/no-unsafe-return': 'off',
254+
'@typescript-eslint/no-unsafe-argument': 'off',
255+
'@typescript-eslint/restrict-template-expressions': 'off',
256+
'@typescript-eslint/require-await': 'off',
257+
'@typescript-eslint/no-unnecessary-condition': 'off',
258+
'@typescript-eslint/strict-boolean-expressions': 'off',
259+
'@typescript-eslint/member-ordering': 'off',
223260
'no-console': 'off',
224261
'unicorn/consistent-function-scoping': 'off',
262+
'unicorn/no-null': 'off',
263+
'unicorn/prefer-global-this': 'off',
264+
'unicorn/consistent-existence-index-check': 'off',
265+
'unicorn/prefer-type-error': 'off',
266+
'unicorn/prefer-structured-clone': 'off',
267+
'code-complete/no-magic-numbers-except-zero-one': 'off',
268+
'code-complete/enforce-meaningful-names': 'off',
269+
'code-complete/no-boolean-params': 'off',
270+
'react-refresh/only-export-components': 'off',
271+
'import/export': 'off',
272+
'import/order': 'off',
273+
'prefer-template': 'off',
274+
'@typescript-eslint/unbound-method': 'off',
275+
'@typescript-eslint/await-thenable': 'off',
276+
'sonarjs/no-unused-vars': 'off',
277+
'sonarjs/no-dead-store': 'off',
278+
'sonarjs/no-nested-functions': 'off',
279+
'sonarjs/no-hardcoded-passwords': 'off',
280+
'unicorn/no-unused-properties': 'off',
281+
'unicorn/error-message': 'off',
282+
'unused-imports/no-unused-vars': 'off',
283+
'import/no-unresolved': 'off', // Vitest globals might not resolve
225284
},
226285
},
227286
{

0 commit comments

Comments
 (0)