1.0.3 • Published 1 year ago

@lpfreelance/electron-bridge-cli v1.0.3

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

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
}
KeyDefaultDescription
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.
mainfalsetrue when used within electron-bridge, false otherwise.
verbosefalsetrue 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 { return nativeTheme.shouldUseDarkColors; }

    public async themeSource(value: 'system' | 'light' | 'dark'): Promise { 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](#output).

Let's dive into the specifics of this format.

#### 1. Schema decorator and class declaration
> schemas/native-theme.ts
```typescript
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
```typescript
// ...
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](https://www.electronjs.org/docs/latest/api/browser-window#event-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
```typescript
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
```typescript
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
```typescript
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; themeSource(value: 'system' | 'light' | 'dark'): Promise; onUpdated(listener: (event: ThemeUpdatedEvent) => void): void;

}

> src/bridge/renderer/renderer.ts
```typescript
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.

FileNumber of lines
schemas/native-theme.ts52
native-theme.bridge.ts34
native-theme.module.ts21
native-theme.api.ts22
Total77
ROI~32 %

I have spoken.

Contributing

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

1.0.3

1 year ago

1.0.2

2 years ago

1.0.1

3 years ago

1.0.0

3 years ago