webext-core

repository·main·Indexed 43 days ago

https://github.com/aklinker1/webext-core

A suite of specialized libraries for building cross-browser web extensions with a focus on type safety, testing, and common patterns. Includes packages for type-safe messaging (@webext-core/messaging), storage (@webext-core/storage), job scheduling (@webext-core/job-scheduler), UI isolation (@webext-core/isolated-element), and an in-memory API implementation for testing (@webext-core/fake-browser).

Tokens
30.8K
Snippets
103
Records
128
Agent score
55%

What's inside webext-core

  1. Overview of webext-core packages

    main

    webext-core is a collection of utilities designed to make writing and testing web extensions easier across all browsers. The project is modular, providing specialized packages for common extension development tasks:

    • @webext-core/storage: A type-safe API for accessing extension storage, similar to local storage.
    • @webext-core/messaging: A simplified, type-safe API for cross-context message passing.
    • @webext-core/job-scheduler: Tools to schedule and manage recurring jobs within an extension.
    • @webext-core/match-patterns: Utilities for handling and working with web extension match patterns.
    • @webext-core/proxy-service: Allows calling functions that execute in a different JavaScript context (e.g., executing logic in the background script from a content script).
    • @webext-core/isolated-element: Provides containers with styles isolated from the host page's CSS.
    • @webext-core/fake-browser: An in-memory implementation of web extension APIs used for testing extension logic without a real browser.
  2. Overview of webext-core libraries

    main

    webext-core is a collection of core libraries and tools designed to simplify building web extensions across all browsers. It provides type-safe wrappers and specialized utilities for common extension tasks like messaging, storage, and job scheduling.

    Key packages include:

    • @webext-core/messaging: A lightweight, type-safe wrapper for web extension messaging APIs.
    • @webext-core/storage: Type-safe wrappers around the storage API based on local storage.
    • @webext-core/job-scheduler: A utility to schedule recurring jobs using the Alarms API.
    • @webext-core/fake-browser: An in-memory implementation of web extension APIs, intended for testing environments.
    • @webext-core/proxy-service: Allows writing services that can be called from any JS context but execute within the background service worker.
    • @webext-core/isolated-element: Helps isolate content script UIs from the styles of the host page.
  3. Use @webext-core/proxy-service for cross-context function execution

    main
    @webext-core/proxy-service is a type-safe wrapper around web extension messaging APIs. It allows you to define a function in one context (e.g., the background script) and call it from any other context (e.g., content scripts, popups, or options pages) as if it were a local function call. It abstracts the underlying messaging implementation and works across all major browsers including Chrome, Firefox, and Safari.
  4. Understand the project layout and package structure

    main

    The webext-core repository is a monorepo containing all packages under the @webext-core scope.

    Directory Structure

    • docs/: The documentation website.
    • packages/*: Individual NPM packages.
    • packages/*-demo: Demo extensions for specific packages.

    Package Standards

    Every package follows a consistent structure:

    • Entrypoint: src/index.ts
    • Build Tool: tsdown is used to build the final NPM package.
    • Language: TypeScript.
    • Documentation: Each package contains its own README.md with specific setup and testing instructions.
  5. Define messaging types with Protocol Maps

    main

    Protocol maps allow you to define types for sendMessage and onMessage in a single interface. When using defineExtensionMessaging<ProtocolMap>(), the data and return types are automatically inferred, eliminating the need to manually pass type parameters to the messaging functions.

    In your ProtocolMap interface, define message types as functions where:

    • The function name is the message name.
    • The function parameters represent the data sent to the message.
    • The function return type represents the value returned by the message.

    Example interface definitions:

    • message1(): void; — No data and no return value.
    • message2(data: string): void; — Only data is sent.
    • message3(): boolean; — Only a return value is sent back.
    • message4(data: string): boolean; — Both data and a return value are used.
    interface ProtocolMap {
      message1(): void;
      message2(data: string): void;
      message3(): boolean;
      message4(data: string): boolean;
    }
    
    export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
    
    // Usage with automatic inference:
    // onMessage('message2', ({ data /* string */ }) /* : void */ => {});
    // const res /* : boolean */ = await sendMessage('message3', undefined);
  6. Compare @webext-core/storage with localStorage and browser.storage

    main

    The @webext-core/storage API is designed to bridge the gap between the synchronous localStorage API and the asynchronous browser.storage API, providing a more intuitive developer experience for extensions.

    Key behavioral differences:

    • Removal: Setting a value to undefined removes the key (matches localStorage, unlike browser.storage).
    • Missing Values: Returns null for missing values (matches localStorage, unlike browser.storage).
    • Types: Supports storing non-string values directly (matches browser.storage, unlike localStorage).
    • Concurrency: All operations are asynchronous (matches browser.storage, unlike localStorage).
  7. Handle null and optional values in storage schemas

    main

    When using a schema with defineExtensionStorage, the getItem method always returns T | null.

    • Required Fields: If a key is defined as a type (e.g., boolean), setItem will require a non-null value.
    • Optional Fields: To allow a key to be explicitly set to null or to represent a value that might not exist, include null in the type definition (e.g., boolean | null).
    export interface LocalExtStorageSchema {
      // Required: must be a number, cannot be set to null
      installDate: number;
    
      // Optional: can be set to null
      notificationsEnabled: boolean | null;
    
      favoriteUrls: string[];
    }
  8. How @webext-core/proxy-service works

    main

    The @webext-core/proxy-service allows you to execute code in the extension's background context from any other part of the extension (like content scripts or UI pages). This is particularly useful for accessing resources that are only available in the background, such as IndexedDB or specific extension APIs.

    The Workflow

    1. Define a Service: Create a class or object containing the logic you want to run in the background.
    2. Define a Key: Create a unique key using ProxyServiceKey<T> to ensure type-safety when calling the service.
    3. Register the Service: In your background script, use registerService to instantiate and register the service.
    4. Create a Proxy: In your client context (content script, etc.), use createProxyService(KEY) to get a proxy object. Calling methods on this proxy will automatically execute them in the background.
    import { createProxyService, registerService } from '@webext-core/proxy-service';
    
    // 1. Define service
    class MathService {
      async fibonacci(n: number): Promise<number> { return 0; }
    }
    
    // 2. Define key (import ONLY the type of the service)
    import type { ProxyServiceKey } from '@webext-core/proxy-service';
    import type { MathService } from './MathService';
    export const MATH_SERVICE_KEY = 'math-service' as ProxyServiceKey<MathService>;
    
    // 3. Register in background.ts
    registerService(MATH_SERVICE_KEY, new MathService());
    
    // 4. Use in anywhere-else.ts
    const mathService = createProxyService(MATH_SERVICE_KEY);
    await mathService.fibonacci(100);
  9. Communicate with web pages using Window or Custom Event messengers

    main

    To communicate between a content script and a webpage (or an injected script), use the specialized messengers from @webext-core/messaging/page.

    Choosing a Messenger

    • defineCustomEventMessaging: Use this if you do not need to communicate with iframes. It uses the CustomEvent API.
    • defineWindowMessaging: Use this if you do need to communicate with iframes. It uses the window.postMessage API.

    Configuration: namespace

    Both messengers require a namespace option (a unique string). This prevents your extension from accidentally reacting to messages from the page or other extensions.

    • For extensions, browser.runtime.id is recommended.
    • For injected scripts, you may need to hardcode the extension ID since browser is not available in the page context.

    Usage Example

    Content Script (Receiver/Sender):

    import { websiteMessenger } from './website-messaging';
    
    // ... inject script ...
    
    // Send message to injected script
    websiteMessenger.sendMessage('init', { ... });
    
    // Listen for messages from injected script
    websiteMessenger.onMessage('somethingHappened', (data) => {
      // React to messages
    });

    Injected Script (Sender/Receiver):

    import { websiteMessenger } from './website-messaging';
    
    // Listen for init
    websiteMessenger.onMessage('init', data => {
      // ...
    
      // Send back to content script
      // Params: action, data, targetOrigin (default '*'), window reference
      websiteMessenger.sendMessage('somethingHappened', { ... }, '*', window.parent);
    });
    import { defineWindowMessaging } from '@webext-core/messaging/page';
    
    export interface WebsiteMessengerSchema {
      init(data: unknown): void;
      somethingHappened(data: unknown): void;
    }
    
    export const websiteMessenger = defineWindowMessaging<WebsiteMessengerSchema>({
      namespace: '<some-unique-string>',
    });
  10. Pass dependencies to jobs using higher-order functions

    main

    The job scheduler does not support passing parameters directly into individual job executions. To provide dependencies (like service instances or configuration) to a job, use a higher-order function pattern. This allows you to 'inject' dependencies at the time of scheduling.

    // someJob.ts
    function someJob(someDependency: SomeDependency) {
      return async () => {
        // Use someDependency here
      };
    }
    
    // background.ts
    import { defineJobScheduler } from '@webext-core/job-scheduler';
    import { someJob } from './someJob.ts';
    
    const someDependency = new SomeDependency();
    const jobs = defineJobScheduler();
    
    jobs.scheduleJob({
      id: 'parameterized-job',
      type: 'once',
      date: Date.now() + 1000,
      execute: someJob(someDependency),
    });
  11. Install @webext-core/match-patterns

    main

    You can install the package via NPM using pnpm or download it via CDN for browser environments.

    NPM

    pnpm i @webext-core/match-patterns

    CDN

    Download the IIFE build using curl:

    curl -o match-patterns.js https://cdn.jsdelivr.net/npm/@webext-core/match-patterns/dist/index.iife.js

    Then include it in your HTML:

    <script src="/match-patterns.js"></script>
    <script>
      const { MatchPattern } = webExtCoreMatchPatterns;
    </script>
    pnpm i @webext-core/match-patterns
  12. Define a service using a Class

    main

    You can define a service by creating a class. All methods defined on the class will be available in other JavaScript contexts via the proxy service. This is useful for services that require internal state or complex initialization logic in a constructor.

    import { IDBPDatabase } from 'idb';
    
    class TodosRepo {
      constructor(private db: Promise<IDBPDatabase>) {}
    
      async getAll(): Promise<Todo[]> {
        const db = await this.db;
        return await db.getAll('todos');
      }
    }