npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kstonekuan/trpc-chrome

v1.1.3

Published

tRPC adapter for Web Extensions

Readme

trpc-chrome

Fork of trpc-chrome with updated dependencies

The original trpc-chrome repository is no longer actively maintained. This fork provides:

  • tRPC v11 support - Updated from v10 to latest v11.4.3
  • Modern build system - Vite with dual ESM/CJS output
  • Updated dependencies - All dependencies updated to latest versions
  • Biome integration - Modern linting and formatting
  • Better DX - Watch mode, source maps, improved error handling
  • Debug mode - Chrome DevTools integration for request/response logging
  • Connection management - Auto-reconnect and connection state monitoring
  • Port management - Centralized port handling for complex extensions
  • Type-safe ports - Compile-time and runtime port name validation

Chrome extension support for tRPC 🧩

  • Easy communication for web extensions.
  • Typesafe messaging between content & background scripts.
  • Ready for Manifest V3.

Usage

1. Install @kstonekuan/trpc-chrome.

# npm
npm install @kstonekuan/trpc-chrome
# yarn
yarn add @kstonekuan/trpc-chrome

2. Add createChromeHandler in your background script.

// background.ts
import { createChromeHandler } from '@kstonekuan/trpc-chrome/adapter';
import { initTRPCWithSuperjson } from '@kstonekuan/trpc-chrome/utils';

// Define your context type
type Context = {
  userId?: string;
  // Add other context properties as needed
};

const t = initTRPCWithSuperjson<Context>(); // Pre-configured with SuperJSON and context

const appRouter = t.router({
  // ...procedures
});

export type AppRouter = typeof appRouter;

createChromeHandler({
  router: appRouter,
  createContext: ({ req }) => {
    // req is the chrome.runtime.Port
    // You can extract user info, permissions, etc.
    return {
      userId: 'user-123', // Example: get from storage or port sender
    };
  },
  onError: ({ error }) => console.error(error),
});

3. Add a chromeLinkWithSuperjson to the client in your content script.

// content.ts
import { createTRPCClient } from '@trpc/client';
import { chromeLinkWithSuperjson } from '@kstonekuan/trpc-chrome/utils';

import type { AppRouter } from './background';

// Option 1: Named port connection (recommended)
export const chromeClient = createTRPCClient<AppRouter>({
  links: [
    chromeLinkWithSuperjson({ 
      portName: 'ui-to-background', // Named port for multi-channel communication
    })
  ],
});

// Option 2: Explicit port connection
const port = chrome.runtime.connect();
export const chromeClient2 = createTRPCClient<AppRouter>({
  links: [
    chromeLinkWithSuperjson({ 
      port,
    })
  ],
});

Type Safety Note: When using initTRPCWithSuperjson() on the server, always use chromeLinkWithSuperjson() on the client. This ensures the transformers match and your Date, Map, Set, and other complex types are properly serialized.

Using Custom Transformers

If you need to use a different transformer or no transformer at all:

// background.ts
import { initTRPC } from '@trpc/server';
import { createChromeHandler } from '@kstonekuan/trpc-chrome/adapter';

const t = initTRPC.create({
  // Custom transformer or none
  transformer: myCustomTransformer, // or omit for no transformer
});

// content.ts
import { chromeLink } from '@kstonekuan/trpc-chrome/link';

const chromeClient = createTRPCClient<AppRouter>({
  links: [
    chromeLink({ 
      portName: 'ui-to-background',
      transformer: myCustomTransformer, // Must match server!
    })
  ],
});

Requirements

Peer dependencies:

  • tRPC Server v11 (@trpc/server) must be installed.
  • tRPC Client v11 (@trpc/client) must be installed.

Advanced Features

Router Composition

// background.ts
import { initTRPCWithSuperjson, createNamespacedRouter } from '@kstonekuan/trpc-chrome/utils';

type Context = {
  userId: string;
  permissions: string[];
};

const t = initTRPCWithSuperjson<Context>(); // Pre-configured with SuperJSON and context

const userRouter = t.router({
  getProfile: t.procedure.query(({ ctx }) => {
    // Access context here
    return { name: 'John', userId: ctx.userId };
  }),
  updateProfile: t.procedure
    .input(z.object({ name: z.string() }))
    .mutation(({ input, ctx }) => {
      // Check permissions from context
      if (!ctx.permissions.includes('edit:profile')) {
        throw new Error('Unauthorized');
      }
      return input;
    }),
});

const settingsRouter = t.router({
  getTheme: t.procedure.query(() => 'dark'),
  setTheme: t.procedure.input(z.enum(['light', 'dark'])).mutation(({ input }) => input),
});

// ✅ Recommended: Namespaced router with perfect type inference
const appRouter = t.router({
  user: userRouter,
  settings: settingsRouter,
});

// ✅ Alternative: Using helper function
const appRouter2 = createNamespacedRouter(t, {
  user: userRouter,
  settings: settingsRouter,
});

// Usage: trpc.user.getProfile.query(), trpc.settings.getTheme.query()

Multi-Channel Communication

// Different communication channels for complex extensions
const uiClient = createTRPCClient<AppRouter>({
  links: [chromeLink({ portName: 'ui-to-background' })],
});

const offscreenClient = createTRPCClient<AppRouter>({
  links: [chromeLink({ portName: 'background-to-offscreen' })],
});

const contentClient = createTRPCClient<AppRouter>({
  links: [chromeLink({ portName: 'content-to-background' })],
});

Lazy Loading with Dynamic Imports

const heavyProcedure = t.procedure
  .input(z.string())
  .mutation(async ({ input }) => {
    // Lazy-loaded handler
    const { handleHeavyOperation } = await import('./heavy-handler');
    return handleHeavyOperation(input);
  });

Debug Mode with Chrome DevTools Integration

// background.ts
import { createChromeHandler } from '@kstonekuan/trpc-chrome/adapter';

// Enable debug mode for development
createChromeHandler({
  router: appRouter,
  createContext: () => ({}),
  onError: ({ error }) => console.error(error),
  debug: true, // Enable debug logging
});

// Or with custom configuration
createChromeHandler({
  router: appRouter,
  createContext: () => ({}),
  onError: ({ error }) => console.error(error),
  debug: {
    enabled: true,
    logStyle: 'detailed', // 'simple' | 'detailed'
    colorize: true,
    measurePerformance: true,
    filter: (entry) => {
      // Filter out specific requests
      return entry.path !== 'heartbeat';
    },
  },
});

// Debug logs appear in Chrome DevTools console with:
// - Request/response timing
// - Procedure paths and methods
// - Input/output data
// - Error stack traces

Port Management

import { ChromePortManager } from '@kstonekuan/trpc-chrome/utils';

// Create a port manager for centralized connection handling
const portManager = new ChromePortManager();

// Get or create a port connection
const port = portManager.getPort('main-connection');

// Monitor connection state
portManager.onConnect('main-connection', (port) => {
  console.log('Port connected:', port.name);
});

portManager.onDisconnect('main-connection', (port) => {
  console.log('Port disconnected:', port.name);
});

// Check active connections
const activePorts = portManager.getActivePorts();
console.log(`Active connections: ${activePorts.length}`);

Connection State Management with Auto-Reconnect

import { createManagedChromeLink } from '@kstonekuan/trpc-chrome/utils';

// Create a link with automatic reconnection
const managedLink = createManagedChromeLink({
  portName: 'stable-connection',
  transformer: superjson,
  maxReconnectAttempts: 5,
  reconnectInterval: 1000,
  onStateChange: (state) => {
    console.log(`Connection state: ${state}`);
    // Handle UI updates based on connection state
  },
});

const chromeClient = createTRPCClient<AppRouter>({
  links: [managedLink],
});

Type-Safe Port Names

import { createPortNames, TypedPortRegistry } from '@kstonekuan/trpc-chrome/utils';

// Define your port names with full type safety
const PortNames = createPortNames([
  'content-to-background',
  'popup-to-background',
  'devtools-to-background',
  'options-to-background',
] as const);

// Use typed port names throughout your extension
const contentPort = chrome.runtime.connect({ 
  name: PortNames.contentToBackground // Type-safe!
});

// Create a typed registry for managing multiple ports
const portRegistry = new TypedPortRegistry(PortNames);

// Connect with type-safe keys
const port = portRegistry.connect('contentToBackground');

// Create typed links
const contentClient = createTRPCClient<AppRouter>({
  links: [portRegistry.createLink('contentToBackground', { transformer: superjson })],
});

const popupClient = createTRPCClient<AppRouter>({
  links: [portRegistry.createLink('popupToBackground', { transformer: superjson })],
});

// Validate port names at runtime
import { createPortValidator } from '@kstonekuan/trpc-chrome/utils';

const validator = createPortValidator(['content-to-background', 'popup-to-background'] as const);

chrome.runtime.onConnect.addListener((port) => {
  if (validator.isValid(port.name)) {
    // Handle known port types
    console.log(`Valid port connected: ${port.name}`);
  }
});

Debug Panel for Development

import { createDebugPanel, getGlobalDebugger } from '@kstonekuan/trpc-chrome/utils';

// Create a debug panel (usually in your extension's devtools or popup)
const cleanup = createDebugPanel(document.getElementById('debug-container'));

// Access debug logs programmatically
const debugger = getGlobalDebugger();
const logs = debugger?.getLogs() || [];
console.log('Total requests:', logs.length);

// Filter logs
const errors = logs.filter(log => log.type === 'error');
console.log('Errors:', errors);

// Clear logs
debugger?.clearLogs();

// Cleanup when done
cleanup();

Helper Utilities

For better type safety and consistency, we provide pre-configured helpers:

// Server-side: Pre-configured tRPC with SuperJSON
import { initTRPCWithSuperjson } from '@kstonekuan/trpc-chrome/utils';
const t = initTRPCWithSuperjson();

// Client-side: Matching Chrome link with SuperJSON
import { chromeLinkWithSuperjson } from '@kstonekuan/trpc-chrome/utils';
const trpc = createTRPCClient<AppRouter>({
  links: [
    chromeLinkWithSuperjson({ 
      portName: 'my-connection'
      // transformer is already set to superjson
    })
  ],
});

These helpers ensure your transformers always match between server and client.

TypeScript Best Practices

For maximum type safety when using transformers:

  1. Always use matching pairs:

    • initTRPCWithSuperjson() + chromeLinkWithSuperjson()
    • initTRPC.create() + chromeLink() (with same transformer)
  2. Avoid transformer mismatches which cause runtime errors:

    // ❌ BAD: Mismatched transformers
    const t = initTRPCWithSuperjson(); // Uses SuperJSON
    // ...
    chromeLink({ portName: 'port' }) // No transformer!
       
    // ✅ GOOD: Matching transformers
    const t = initTRPCWithSuperjson(); // Uses SuperJSON
    // ...
    chromeLinkWithSuperjson({ portName: 'port' }) // Also uses SuperJSON

Real-time Data with Observables (tRPC v11)

tRPC v11 allows any procedure (query/mutation) to return observables for real-time data:

// background.ts
import { observable } from '@trpc/server/observable';

const appRouter = t.router({
  // Real-time data stream
  watchPrice: t.procedure
    .input(z.object({ symbol: z.string() }))
    .query(({ input }) => {
      return observable<{ price: number; timestamp: Date }>((emit) => {
        // Initial price
        emit.next({ price: 100, timestamp: new Date() });
        
        // Simulate price updates
        const interval = setInterval(() => {
          const price = 100 + Math.random() * 10;
          emit.next({ price, timestamp: new Date() });
        }, 1000);
        
        // Cleanup on unsubscribe
        return () => clearInterval(interval);
      });
    }),
    
  // Long-running operation with progress
  processLargeFile: t.procedure
    .input(z.object({ fileId: z.string() }))
    .mutation(({ input }) => {
      return observable<{ progress: number; status: string }>((emit) => {
        emit.next({ progress: 0, status: 'Starting...' });
        
        // Simulate processing
        let progress = 0;
        const interval = setInterval(() => {
          progress += 10;
          emit.next({ progress, status: `Processing... ${progress}%` });
          
          if (progress >= 100) {
            emit.next({ progress: 100, status: 'Complete!' });
            clearInterval(interval);
            emit.complete();
          }
        }, 500);
        
        return () => clearInterval(interval);
      });
    }),
});

// content.ts - The adapter automatically handles observable streaming
// Just call .query() or .mutate() as normal - the adapter detects observables
const priceData = await client.watchPrice.query({ symbol: 'BTC' });
console.log('Initial price:', priceData);

// For progress tracking
const result = await client.processLargeFile.mutate({ fileId: 'abc123' });
console.log('Processing complete:', result);

Example

Please see full example here.

For advanced use-cases, please find examples in our complete test suite.

Types

ChromeLinkOptions

Please see full typings here.

| Property | Type | Description | Required | | ------------- | --------------------- | ------------------------------------------------------------------------------------------------- | -------- | | port | chrome.runtime.Port | An open web extension port between content & background scripts. | false | | portName | string | Name for the port connection (e.g., 'ui-to-background'). If provided, a new port will be created. | false | | transformer | superjson | Data transformer for serializing/deserializing data (e.g., superjson). | false |

Note: Either port or portName must be provided, but not both.

CreateChromeHandlerOptions

Please see full typings here.

| Property | Type | Description | Required | | --------------- | ---------- | ------------------------------------------------------ | -------- | | router | Router | Your application tRPC router. | true | | createContext | Function | Passes contextual (ctx) data to procedure resolvers. | true | | onError | Function | Called if error occurs inside handler. | true | | debug | boolean \| DebugOptions | Enable debug logging with Chrome DevTools integration. | false |


License

Distributed under the MIT License. See LICENSE for more information.