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
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(npm run build:*)"
],
"deny": [],
"ask": []
}
}
14 changes: 6 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
"type": "git",
"url": "https://github.com/hbmartin/react-vscode-webview-ipc"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"host": ["./dist/host.d.ts"],
"client": ["./dist/client.d.ts"]
}
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./host": {
"types": "./dist/host.d.ts",
"import": "./dist/host.js",
Expand Down
10 changes: 5 additions & 5 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export { type WebviewContextValue } from './client/WebviewContext';
// export { type WebviewContextValue } from './client/WebviewContext';
Comment thread
hbmartin marked this conversation as resolved.
export { WebviewProvider } from './client/WebviewProvider';
export { createCtxKey, useWebviewApi } from './client/useWebviewApi';
export { useVscodeState } from './client/useVscodeState';
export { useLogger } from './client/useLogger';
export { isViewApiRequest, isViewApiResponse, isViewApiEvent, type CtxKey } from './types';
export type { ClientCalls, ViewApiResponse, ViewApiError } from './types';
export type { StateReducer, WebviewKey } from './types/ipcReducer';
export type { WebviewLayout } from './types';
export { isViewApiRequest, type CtxKey } from './types';
export type { ClientCalls, HostCalls, ViewApiResponse, ViewApiError } from './types';
export type { WebviewKey } from './types/reducer';
export type { StateReducer, WebviewLayout } from './client/types';
3 changes: 2 additions & 1 deletion src/lib/client/WebviewContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ClientCalls, HostCalls, VsCodeApi } from '../types';
import type { ClientCalls, HostCalls } from '../types';
import type { VsCodeApi } from './types';

/**
* Context value interface providing type-safe API access
Expand Down
27 changes: 2 additions & 25 deletions src/lib/host/WebviewLogger.ts → src/lib/client/WebviewLogger.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
import type { VsCodeApi } from '../types';
import { LogLevel, type ILogger } from './ILogger';

export interface LogMessage {
type: 'log';
level: LogLevel;
message: string;
data?: Record<string, unknown>;
}

export function isLogMessage(value: unknown): value is LogMessage {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
if (!('type' in value) || value.type !== 'log') {
return false;
}
if (!('level' in value) || typeof value.level !== 'number') {
return false;
}
if (!('message' in value) || typeof value.message !== 'string') {
return false;
}
return true;
}
import { LogLevel, type ILogger, type LogMessage } from '../types';
import type { VsCodeApi } from './types';

export class WebviewLogger implements ILogger {
constructor(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/client/WebviewProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
type HostCalls,
type RequestContext,
type ViewApiRequest,
type VsCodeApi,
} from '../types';
import { type VsCodeApi } from './types';

Check failure on line 13 in src/lib/client/WebviewProvider.tsx

View workflow job for this annotation

GitHub Actions / Lint, Type Check, and Test

TypeScript will only remove the inline type specifiers which will leave behind a side effect import at runtime. Convert this to a top-level type qualifier to properly remove the entire import
Comment thread
hbmartin marked this conversation as resolved.
import { generateId } from '../utils';

Check failure on line 14 in src/lib/client/WebviewProvider.tsx

View workflow job for this annotation

GitHub Actions / Lint, Type Check, and Test

`../utils` import should occur before import of `./types`
import { DeferredPromise } from './types';
import { TypedContexts } from './useWebviewApi';
import type { WebviewContextValue } from './WebviewContext';
Expand Down
10 changes: 10 additions & 0 deletions src/lib/client/ipcReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { FnKeys } from '../types/reducer';

export function isFnKey<T extends object>(
prop: string | symbol | number,
obj: T
): prop is FnKeys<T> {
return (
Object.prototype.hasOwnProperty.call(obj, prop) && typeof obj[prop as keyof T] === 'function'
);
}
15 changes: 15 additions & 0 deletions src/lib/client/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { FnKeys, Patches } from '../types/reducer';

/**
* Deferred promise for handling async responses with timeout management
*/
Expand Down Expand Up @@ -42,3 +44,16 @@ export class DeferredPromise<T> {
this.settled = true;
}
}

export type WebviewLayout = 'sidebar' | 'panel';
// VS Code webview API

export interface VsCodeApi {
postMessage(message: unknown): Thenable<boolean>;
getState(): unknown;
setState(state: unknown): void;
}

export type StateReducer<S, A> = {
[Key in FnKeys<A>]: (prevState: S, patch: Patches<A>[Key]) => S;
};
6 changes: 3 additions & 3 deletions src/lib/client/useLogger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable no-console */
import { useMemo } from 'react';
import type { VsCodeApi } from '../types';
import { WebviewLogger } from '../host/WebviewLogger';
import type { ILogger } from '../host/ILogger';
import type { ILogger } from '../types';
import { WebviewLogger } from './WebviewLogger';
import type { VsCodeApi } from './types';

/**
* React hook to get a logger instance for use in webview components.
Expand Down
13 changes: 3 additions & 10 deletions src/lib/client/useVscodeState.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { VsCodeApi } from '../types';
import {
ACT,
type Patch,
PATCH,
type Action,
type WebviewKey,
type StateReducer,
isFnKey,
} from '../types/ipcReducer';
import { ACT, type Patch, PATCH, type Action, type WebviewKey } from '../types/reducer';
import { isFnKey } from './ipcReducer';
import type { StateReducer, VsCodeApi } from './types';

type PostAction<A extends object> = Pick<Action<A>, 'key' | 'params'>;

Expand Down
8 changes: 3 additions & 5 deletions src/lib/host.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export type { ILogger } from './host/ILogger';
export { Logger, getLogger, disallowedLogKeys } from './host/logger';
export { WebviewApiProvider } from './host/WebviewApiProvider';
export { BaseWebviewViewProvider } from './host/BaseWebviewViewProvider';
export { isViewApiRequest, isViewApiResponse, isViewApiEvent, type CtxKey } from './types';
export type { HostCalls, ViewApiResponse, ViewApiError } from './types';
export type { ActionDelegate, StateReducer, WebviewKey } from './types/ipcReducer';
export type { WebviewLayout } from './types';
export { isViewApiRequest } from './types';
export type { ViewApiResponse, ViewApiError, ILogger } from './types';
export type { ActionDelegate } from './types/reducer';
8 changes: 3 additions & 5 deletions src/lib/host/BaseWebviewViewProvider.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import * as vscode from 'vscode';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

CI failing: add ‘vscode’ as a dependency and externalize for bundling

The build reports “Missing dependencies: 'vscode'…”. Add vscode as a devDependency (and optionally peerDependency) and ensure bundlers mark it external.

Minimal package.json patch:

 {
   "name": "react-vscode-webview-ipc",
   "version": "…",
+  "peerDependencies": {
+    "vscode": "*"
+  },
   "devDependencies": {
+    "vscode": "*"
   }
 }

Also configure your bundler (tsup/rollup/esbuild) to externalize "vscode" (e.g., tsup: { external: ['vscode'] }).


🏁 Script executed:

#!/bin/bash
# Verify presence and usage
rg -n "from 'vscode'" -g 'src/**'
jq '.peerDependencies, .devDependencies' package.json

Length of output: 1415


CI failing — add 'vscode' dev/peer dependency and externalize in bundler

Build reports "Missing dependencies: 'vscode'". Add "vscode" to package.json (devDependency and peerDependency) and mark it external in your bundler. Affected files importing vscode: src/lib/host/logger.ts, src/lib/host/WebviewApiProvider.ts, src/lib/host/BaseWebviewViewProvider.ts.

Minimal package.json patch:

 {
   "name": "react-vscode-webview-ipc",
   "version": "…",
+  "peerDependencies": {
+    "vscode": "*"
+  },
   "devDependencies": {
+    "vscode": "*"
   }
 }

Also configure your bundler (tsup/rollup/esbuild) to externalize "vscode" (e.g., tsup: { external: ['vscode'] }).

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: CI

[error] 1-1: Missing dependencies: 'vscode' required by ./src/lib/host/BaseWebviewViewProvider.ts.

import type { HostCalls } from '../types';
import { type HostCalls, type ILogger, isLogMessage, LogLevel, type LogMessage } from '../types';
import {
isMyActionMessage,
PATCH,
type ActionDelegate,
type FnKeys,
type Patch,
type Patches,
type WebviewKey,
} from '../types/ipcReducer';
import { LogLevel, type ILogger } from './ILogger';
} from '../types/reducer';
import { getLogger } from './logger';
import { isLogMessage, type LogMessage } from './WebviewLogger';
import { isMyActionMessage } from './utils';
import type { WebviewApiProvider } from './WebviewApiProvider';

export abstract class BaseWebviewViewProvider<A extends object>
Expand Down
19 changes: 0 additions & 19 deletions src/lib/host/ILogger.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/lib/host/WebviewApiProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { HostCalls, RequestContext, ViewApiEvent } from '../types';
import { generateId, getErrorMessage } from '../utils';
import { getLogger } from './logger';
import type { WebviewKey } from '../types/ipcReducer';
import type { WebviewKey } from '../types/reducer';
import type * as vscode from 'vscode';

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/host/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { LogLevel, type ILogger } from './ILogger';
import { LogLevel, type ILogger } from '../types';
Comment thread
hbmartin marked this conversation as resolved.

export const disallowedLogKeys = ['password', 'secret', 'token', 'apiKey', 'apiSecret', 'content'];

Expand Down
21 changes: 21 additions & 0 deletions src/lib/host/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ACT, type Action, type WebviewKey } from '../types/reducer';

export function isMyActionMessage<T extends object>(
message: unknown,
providerId: WebviewKey
): message is Action<T> {
return (
message !== null &&
message !== undefined &&
typeof message === 'object' &&
'providerId' in message &&
'type' in message &&
'key' in message &&
'params' in message &&
message.type === ACT &&
typeof message.providerId === 'string' &&
message.providerId === providerId &&
(typeof message.key === 'string' || typeof message.key === 'symbol') &&
Array.isArray(message.params)
);
}
Comment thread
hbmartin marked this conversation as resolved.
Comment thread
hbmartin marked this conversation as resolved.
15 changes: 0 additions & 15 deletions src/lib/index.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './log';
export * from './rpc';

export type Brand<T, B> = T & { readonly __brand: B };
42 changes: 42 additions & 0 deletions src/lib/types/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Interface for logging services that can be implemented
* by both the extension host (Logger) and webview (WebviewLogger)
*/

export interface ILogger {
debug(message: string, data?: Record<string, unknown>): void;
info(message: string, data?: Record<string, unknown>): void;
warn(message: string, data?: Record<string, unknown>): void;
error(message: string, data?: Record<string, unknown>): void;
dispose(): void;
}

export enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR,
}

export interface LogMessage {
type: 'log';
level: LogLevel;
message: string;
data?: Record<string, unknown>;
}

export function isLogMessage(value: unknown): value is LogMessage {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
if (!('type' in value) || value.type !== 'log') {
return false;
}
if (!('level' in value) || typeof value.level !== 'number') {
return false;
}
if (!('message' in value) || typeof value.message !== 'string') {
return false;
}
return true;
}
49 changes: 8 additions & 41 deletions src/lib/types/ipcReducer.ts → src/lib/types/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Brand } from '../types';
import type { Brand } from '.';

export type WebviewKey = Brand<string, 'WebviewKey'>;

export const PATCH = 'patch';
export const ACT = 'act';

Expand All @@ -10,26 +9,24 @@ export type FnKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

export function isFnKey<T extends object>(
prop: string | symbol | number,
obj: T
): prop is FnKeys<T> {
return (
Object.prototype.hasOwnProperty.call(obj, prop) && typeof obj[prop as keyof T] === 'function'
);
}

interface IpcMessage {
readonly type: string;
readonly providerId: WebviewKey;
}

export interface Action<T extends object, K extends FnKeys<T> = FnKeys<T>> extends IpcMessage {
readonly type: typeof ACT;
readonly key: K;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly params: T[K] extends (...a: infer A) => any ? Readonly<A> : never;
}

export type ActionDelegate<A> = {
[K in FnKeys<A>]: A[K] extends (...args: infer P) => infer R
? (...args: P) => R | Promise<R>
: never;
};

export interface Patch<A, K extends FnKeys<A> = FnKeys<A>> extends IpcMessage {
readonly type: typeof PATCH;
readonly key: K;
Expand All @@ -44,33 +41,3 @@ export type Patches<A> = {
: R
: never;
};

export function isMyActionMessage<T extends object>(
message: unknown,
providerId: WebviewKey
): message is Action<T> {
return (
message !== null &&
message !== undefined &&
typeof message === 'object' &&
'providerId' in message &&
'type' in message &&
'key' in message &&
'params' in message &&
message.type === ACT &&
typeof message.providerId === 'string' &&
message.providerId === providerId &&
(typeof message.key === 'string' || typeof message.key === 'symbol') &&
Array.isArray(message.params)
);
}

export type StateReducer<S, A> = {
[Key in FnKeys<A>]: (prevState: S, patch: Patches<A>[Key]) => S;
};

export type ActionDelegate<A> = {
[K in FnKeys<A>]: A[K] extends (...args: infer P) => infer R
? (...args: P) => R | Promise<R>
: never;
};
Loading
Loading