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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lpfreelance/electron-bridge-cli

v1.0.3

Published

Tool used to quickly generate bridges for Electron application using schemas.

Downloads

545

Readme

electron-bridge-cli

npm version coverage

electron-bridge-cli is a tool to quickly create bridges for electron-bridge. This is used internally to generate source files in electron-bridge. You can use this command line interface to create your own custom modules.

Install

$ npm install --save-dev @lpfreelance/electron-bridge-cli

Usage

$ eb generate ./bridge.config.json

Execute program from current working directory using given configuration file.

Configuration

You can provide a configuration file to electron-bridge-cli with the following object:

{
  "base": ".",
  "tsconfig": "tsconfig.json",
  "schemas": "schemas/",
  "output": "src/bridge/",
  "main": false,
  "verbose": false
}

| Key | Default | Description | |---------:|----------------:|-----------------------------------------------------------------------| | base | "." | Path used to target a directory other than current working directory. | | tsconfig | "tsconfig.json" | Path to the tsconfig.json file of your project. | | schemas | "schemas/" | Path where to look for schemas to parse. | | output | "src/bridge/" | Path where to generate output files. | | main | false | true when used within electron-bridge, false otherwise. | | verbose | false | true to show more logs, false otherwise. |

By reusing your project tsconfig.json file, electron-bridge-cli will generate files with the same configuration and therefore provides the same indentation, new line kind, etc. as your project.

When generating files from schemas, electron-bridge is imported for you:

  • when working on electron-bridge, you need to set "main": true to import modules relative to the package (e.g. import {Bridge} from './bridge.ts').
  • when working on your project, you need to set "main": false to import modules from the package (e.g. import {Bridge} from '@lpfreelance/electron-bridge/main').

Output files

Each schema will be generated in the output path using this structure:

${output}
├── main/           # contains bridge classes (*.bridge.ts)
├── preload/        # contains module classes (*.module.ts)
└── renderer/       # contains api interfaces (*.api.ts) and augmented Window (renderer.ts)

Schema

A schema is a single file you can write containing main process features to be exposed in the renderer process. It uses a valid Typescript syntax to generate a bridge file, a module file and an api interface file.

Let's see what it looks like with this example:

schemas/native-theme.ts

import {BrowserWindow, nativeTheme} from 'electron';
import {Schema, EventListener} from '@lpfreelance/electron-bridge-cli';

/**
 * Emitted when something in the underlying NativeTheme has changed.
 */
export interface ThemeUpdatedEvent {
  shouldUseDarkColors: boolean;
  shouldUseHighContrastColors: boolean;
  shouldUseInvertedColorScheme: boolean;
}

/**
 * Read and respond to changes in Chromium's native color theme.
 */
@Schema(false)
export class NativeTheme {
  
  constructor(private win: BrowserWindow) {
    
  }

  public register(): void {
    nativeTheme.on('updated', this.emitUpdated.bind(this));
  }

  public release(): void {
    nativeTheme.off('updated', this.emitUpdated.bind(this));
  }

  public async shouldUseDarkColors(): Promise<boolean> {
    return nativeTheme.shouldUseDarkColors;
  }

  public async themeSource(value: 'system' | 'light' | 'dark'): Promise<void> {
    nativeTheme.themeSource = value;
  }

  @EventListener('updated')
  public onUpdated(listener: (event: ThemeUpdatedEvent) => void): void {

  }

  private emitUpdated(): void {
    this.win.webContents.send('eb.nativeTheme.updated', {
      shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
      shouldUseHighContrastColors: nativeTheme.shouldUseHighContrastColors,
      shouldUseInvertedColorScheme: nativeTheme.shouldUseInvertedColorScheme,
    });
  }

}

You can see that the code is pretty simple to write and understand. You can look at the generated code that eb generate ./bridge.config.json command would produce, here.

Let's dive into the specifics of this format.

1. Schema decorator and class declaration

schemas/native-theme.ts

import {Schema} from '@lpfreelance/electron-bridge-cli';

@Schema(false)
export class NativeTheme {
    // ...
}

You must decorate the class for which you want to create a bridge, a module and an api interface using @Schema().

You must indicate a value for the parameter readonly:

  • true means this bridge behaves without using any write operations on the user's device.
  • false means this bridge behaves with the use of write operations on the user's device.

readonly is currently not reused but still required as safety information. It shall be implemented in the future to quickly filter safe bridges to register.

You must export the class along with the decorator in order to be parsed by the tool.

You must declare one and only one exported class with the @Schema decorator per file.

2. Class

schemas/native-theme.ts

// ...

/**
 * Read and respond to changes in Chromium's native color theme.
 */
@Schema(false)
export class NativeTheme {
    // ...
}

If you provide documentation for your class, it will be reused in the api interface file.

3. Constructor

schemas/native-theme.ts

// ...
export class NativeTheme {

    // ...

    constructor(private win: BrowserWindow) {

    }

    // ...
}

You can declare it or not, it will be reused as-this in the bridge file.

If you need external dependencies from your Electron app file (electron.dev.ts), you can add parameters to the constructor. You will then be able to call the constructor by passing such dependencies.

An example might be to pass your BrowserWindow instance in order to send events to the renderer process.

4. Lifecycle

schemas/native-theme.ts

// ...
export class NativeTheme {

    // ...

    public register(): void {
        nativeTheme.on('updated', this.emitUpdated.bind(this));
    }

    public release(): void {
        nativeTheme.off('updated', this.emitUpdated.bind(this));
    }

    // ...
}

In the main process, a bridge shall be initialized after a BrowserWindow is instantiated. It shall then be released after that same BrowserWindow closed.

BridgeService will call register() to initialize your bridge and call release() to release it.

You can override these two functions to listen to events, open / close a file, allocate / deallocate memory, etc.

Note: when generated register() and release() functions will contain calls to add and remove IPC handlers. If you override one of these, be aware that your code will be appended at the beginning of the function, while generated code will be appended after.

5. Public functions

schemas/native-theme.ts

import {nativeTheme} from 'electron';

// ...
export class NativeTheme {

  // ...

  /**
   * Returns true when system is defined with a dark theme, false when system is defined with a light theme.
   */
  public async shouldUseDarkColors(): Promise<boolean> {
    return nativeTheme.shouldUseDarkColors;
  }

  public async themeSource(value: 'system' | 'light' | 'dark'): Promise<void> {
    nativeTheme.themeSource = value;
  }

  // ...
}

Here is where all the fun is happening:

  • you must declare public a function you want to expose in the renderer process.
  • you declare a function's signature that you want to expose in the renderer process.
  • you write in the function the code that you want to be executed in the main process.
  • you must set async to a function and returns with a Promise<void> or Promise<something>.
  • you can declare a synchronous function and return void when it has a fire-and-forget kind logic.

The name of the function will be used to define a unique IPC handler. And electron-bridge-cli will take care of the rest for you!

Documentation of a public function will be included in the api interface file.

6. EventListener decorator

schemas/native-theme.ts

// ...
export class NativeTheme {

  // ...

  @EventListener('updated')
  public onUpdated(listener: (event: ThemeUpdatedEvent) => void): void {

  }

  // ...
}

You can expose event listeners by declaring a public function using a callback parameter:

  • you must decorate your function with @EventListener.
  • you must indicate the event name used for IPC channel.
  • any code in the function will be ignored.

Important: you are still responsible for sending an event from the main process to the renderer process. You can do so by using WebContents from your BrowserWindow:

this.win.webContents.send('eb.[myBridge].[event-name]'/*, ...args*/);.

  • [myBridge] becomes nativeTheme.
  • [event-name] becomes updated.

You can find an example in emitUpdated().

7. Private functions, classes and interfaces

schemas/native-theme.ts

import {nativeTheme} from 'electron';

// ...
export class NativeTheme {

  // ...

  private emitUpdated(): void {
    this.win.webContents.send('eb.nativeTheme.updated', {
      shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
      shouldUseHighContrastColors: nativeTheme.shouldUseHighContrastColors,
      shouldUseInvertedColorScheme: nativeTheme.shouldUseInvertedColorScheme,
    });
  }

  // ...
}
  • any private / protected functions
  • any properties / getters / setters
  • any interfaces not exported
  • any classes not exported

will only be included in the bridge class.

8. Exported classes and interfaces

schemas/native-theme.ts

/**
 * Emitted when something in the underlying NativeTheme has changed.
 */
export interface ThemeUpdatedEvent {
  shouldUseDarkColors: boolean;
  shouldUseHighContrastColors: boolean;
  shouldUseInvertedColorScheme: boolean;
}

You can write exported classes and interfaces. They will only be included in the api interface file. If you use them in the bridge class, they will be imported from the api interface module.

Output

For one schema, three files are generated: a bridge class, a module interface and an api interface. With our current schema, electron-bridge-cli would create the following files after executing eb generate ./bridge.config.json:

src/bridge/main/native-theme.bridge.ts

import {BrowserWindow, ipcMain, IpcMainInvokeEvent, nativeTheme} from 'electron';
import {Bridge} from '@lpfreelance/electron-bridge-cli/main';

export class NativeThemeBridge implements Bridge {

  constructor(private win: BrowserWindow) {
    
  }

  public register(): void {
    nativeTheme.on('updated', this.emitUpdated.bind(this));
    ipcMain.handle('eb.nativeTheme.shouldUseDarkColors', async () => {
      return nativeTheme.shouldUseDarkColors;
    });
    ipcMain.handle('eb.nativeTheme.themeSource', async (_: IpcMainInvokeEvent, value: 'system' | 'light' | 'dark') => {
      nativeTheme.themeSource = value;
    });
  }

  public release(): void {
    nativeTheme.off('updated', this.emitUpdated.bind(this));
    ipcMain.removeHandler('eb.nativeTheme.shouldUseDarkColors');
    ipcMain.removeHandler('eb.nativeTheme.themeSource');
  }

  private emitUpdated(): void {
    this.win.webContents.send('eb.nativeTheme.updated', {
      shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
      shouldUseHighContrastColors: nativeTheme.shouldUseHighContrastColors,
      shouldUseInvertedColorScheme: nativeTheme.shouldUseInvertedColorScheme,
    });
  }

}

src/bridge/preload/native-theme.module.ts

import {ipcRenderer, IpcRendererEvent} from 'electron';
import {BridgeModule} from '@lpfreelance/electron-bridge-cli/preload';
import {ThemeUpdatedEvent} from "../renderer/native-theme.api";

export const NativeThemeModule: BridgeModule = {
  name: 'nativeTheme',
  readonly: false,
  api: {
    shouldUseDarkColors: async () => {
      return await ipcRenderer.invoke('eb.nativeTheme.shouldUseDarkColors');
    },
    themeSource: async (value: 'system' | 'light' | 'dark') => {
      return await ipcRenderer.invoke('eb.nativeTheme.themeSource', value);
    },
    onUpdated: (listener: (event: ThemeUpdatedEvent) => void) => {
      ipcRenderer.on('eb.nativeTheme.updated', (_: IpcRendererEvent, event: ThemeUpdatedEvent) => {
        listener(event);
      });
    }
  }
};

src/bridge/renderer/native-theme.api.ts

/**
 * Emitted when something in the underlying NativeTheme has changed.
 */
export interface ThemeUpdatedEvent {
  shouldUseDarkColors: boolean;
  shouldUseHighContrastColors: boolean;
  shouldUseInvertedColorScheme: boolean;
}

/**
 * Read and respond to changes in Chromium's native color theme.
 */
export interface NativeThemeApi {

  /**
   * Returns true when system is defined with a dark theme, false when system is defined with a light theme.
   */
  shouldUseDarkColors(): Promise<boolean>;
  themeSource(value: 'system' | 'light' | 'dark'): Promise<void>;
  onUpdated(listener: (event: ThemeUpdatedEvent) => void): void;

}

src/bridge/renderer/renderer.ts

import {NativeThemeApi} from './native-theme.api';

declare global {
  interface Window {
    nativeTheme: NativeThemeApi;
  }
}

ROI

Why not?

For each schema, the number of lines is compared to the total number of lines from generated files. You'll know the rate of lines you didn't have to write. It doesn't take into account the time you could have wasted jumping from one file back to another one.

| File | Number of lines | |------------------------:|----------------:| | schemas/native-theme.ts | 52 | | | | | native-theme.bridge.ts | 34 | | native-theme.module.ts | 21 | | native-theme.api.ts | 22 | | | | | Total | 77 | | ROI | ~32 % |

I have spoken.

Contributing

Feel free to contribute by creating an issue / submitting a pull-request.