9

I defined contextBridge ( https://www.electronjs.org/docs/all#contextbridge ) in preload.js as follows:

const {
  contextBridge,
  ipcRenderer
} = require("electron")

contextBridge.exposeInMainWorld(
  "api", {
      send: (channel, data) => {
          ipcRenderer.invoke(channel, data).catch(e => console.log(e))
      },
      receive: (channel, func) => {
        console.log("preload-receive called. args: ");
        ipcRenderer.on(channel, (event, ...args) => func(...args));
      },
      // https://www.electronjs.org/docs/all#ipcrenderersendtowebcontentsid-channel-args
      electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
        ipcRenderer.sendTo(window_id, channel, arg);
      },
      // https://github.com/frederiksen/angular-electron-boilerplate/blob/master/src/preload
/preload.ts
      electronIpcSend: (channel: string, ...arg: any) => {
        ipcRenderer.send(channel, arg);
      },
      electronIpcSendSync: (channel: string, ...arg: any) => {
        return ipcRenderer.sendSync(channel, arg);
      },
      electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.on(channel, listener);
      },
      electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.once(channel, listener);
      },
      electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) => 
void) => {
        ipcRenderer.removeListener(channel, listener);
      },
      electronIpcRemoveAllListeners: (channel: string) => {
        ipcRenderer.removeAllListeners(channel);
      }
  }
)

I defined a global.ts :

export {}
declare global {
  interface Window {
    "api": {
      send: (channel: string, ...arg: any) => void;
      receive: (channel: string, func: (event: any, ...arg: any) => void) => void;
      // https://github.com/frederiksen/angular-electron-boilerplate/blob/master/src/preload
/preload.ts
      // https://www.electronjs.org/docs/all#ipcrenderersendtowebcontentsid-channel-args
      electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => void;
      electronIpcSend: (channel: string, ...arg: any) => void;
      electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => void;
      electronIpcSendSync: (channel: string, ...arg: any) => void;
      electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => 
void;
      electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) =>
 void) => void;
      electronIpcRemoveAllListeners: (channel: string) => void;
    }
  }
}

and in the renderer process App.tsx I call window.api.send :

window.api.send('open-type-A-window', ''); 

The typescript compilation looks fine:

yarn run dev
yarn run v1.22.5 
$ yarn run tsc && rimraf dist && cross-env NODE_ENV=development webpack --watch --progress 
--color
$ tsc
95% emitting emit(node:18180) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning:      
Compilation.assets will be frozen in future, all modifications are deprecated.

BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the 
Compilation.
    Do changes to assets earlier, e. g. in Compilation.hooks.processAssets.
    Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.
(Use `node --trace-deprecation ...` to show where the warning was created)
asset main.bundle.js 32.6 KiB [emitted] (name: main) 1 related asset
asset package.json 632 bytes [emitted] [from: package.json] [copied]
cacheable modules 26.2 KiB
  modules by path ./node_modules/electron-squirrel-startup/ 18.7 KiB
    modules by path ./node_modules/electron-squirrel-startup/node_modules/debug/src/*.js 15 
KiB 4 modules
    ./node_modules/electron-squirrel-startup/index.js 1 KiB [built] [code generated]
    ./node_modules/electron-squirrel-startup/node_modules/ms/index.js 2.7 KiB [built] [code 
generated]
  ./src/main/main.ts 6.82 KiB [built] [code generated]
  ./node_modules/file-url/index.js 684 bytes [built] [code generated]
external "path" 42 bytes [built] [code generated]
external "url" 42 bytes [built] [code generated]
external "electron" 42 bytes [built] [code generated]
external "child_process" 42 bytes [built] [code generated]
external "tty" 42 bytes [built] [code generated]
external "util" 42 bytes [built] [code generated]
external "fs" 42 bytes [built] [code generated]
external "net" 42 bytes [built] [code generated]
webpack 5.21.2 compiled successfully in 4313 ms

asset renderer.bundle.js 1000 KiB [emitted] (name: main) 1 related asset
asset index.html 196 bytes [emitted]
runtime modules 937 bytes 4 modules
modules by path ./node_modules/ 990 KiB
  modules by path ./node_modules/scheduler/ 31.8 KiB 4 modules
  modules by path ./node_modules/react/ 70.6 KiB 2 modules
  modules by path ./node_modules/react-dom/ 875 KiB 2 modules
  modules by path ./node_modules/css-loader/dist/runtime/*.js 3.78 KiB 2 modules
  ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] 
[code generated]
  ./node_modules/object-assign/index.js 2.06 KiB [built] [code generated]
modules by path ./src/ 5 KiB
  modules by path ./src/app/styles/*.less 3.16 KiB
    ./src/app/styles/index.less 385 bytes [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/app
/styles/index.less 2.78 KiB [built] [code generated]
  ./src/renderer/renderer.tsx 373 bytes [built] [code generated]
  ./src/app/components/App.tsx 1.48 KiB [built] [code generated]
webpack 5.21.2 compiled successfully in 4039 ms

But I get Cannot read property 'send' of undefined

enter image description here

If I set in App.tsx :

const sendProxy = window.api.send;

I get the same error and the window is not rendered :

enter image description here

What am I doing wrongly with Typescript and with Electron IPC? Looking forward to your kind help

Raphael10
  • 2,508
  • 7
  • 22
  • 50

2 Answers2

19

Below is my setup based on https://www.electronforge.io, which also adds typings for the exposed api. Hope it helps, even if not a focused answer.

In package.json (using @electron-forge package.json setup, webpack + typescript template), under entryPoints, make sure you have:

"preload": {
    "js": "./src/preload.ts"
}

In src/index.ts where you create your BrowserWindow, use the magic webpack constant to reference the bundled preload script (maybe your preload script didn't get bundled?):

const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY
    }
  });

Contents of src/preload.ts:

import { contextBridge } from "electron";
import api from './api'

contextBridge.exposeInMainWorld("api", api);

src/api/index.ts just exports all features of the api. Example:

import * as myFeature from "./my-feature";

// api exports functions that make up the frontend api, ie that in
// turn either do IPC calls to main for db communication or use
// allowed nodejs features like file i/o.
// Example `my-feature.ts`: 
// export const fetchX = async (): Promise<X> => { ... }

export default {
    ...myFeature
}

Typescript 2.9+ can recognise your api functions like api.fetchX by adding a global declaration, e.g. src/index.d.ts (reference):

declare const api: typeof import("./api").default;

...which you need to reference from tsconfig.json:

{ 
  ...
  "files": [
    "src/index.d.ts"
  ]
}

All that done and you should be good to call api.fetchX with typing support (ymmv by IDE) from renderer-side without importing anything. Example App.tsx:

import * as React from 'react'
// do not import api here, it should be globally available

export const App = () => {
  useEffect(() => {
    (async () => {
      const x = await api.fetchX();
      ...
    })();
  }, []);

  return <h1>My App</h1>
}
corolla
  • 5,386
  • 1
  • 23
  • 21
  • Thanks for the super detailed answer. I'm assuming when you say we can call `api.fetchX` anywhere, that's on the main process, not in the renderer/client-side context? To access the api from the client-side, you need to use `window.api.fetchX`, is my understanding correct? – kennyvh May 01 '22 at 06:03
  • `api` is actually available from renderer context, courtesy of `exposeInMainWorld('api', api)`, despite this method being weirdly named. Also, make sure _not_ to import any of the features making up api from source code on renderer side. I'll add a bit to the answer. – corolla May 01 '22 at 18:55
  • Ahh you're right, `api` is indeed available! Thank you for the little bit you've added to your answer. – kennyvh May 02 '22 at 00:11
  • 1
    Also, just in case you want to update your answer or anyone else runs into this problem. In the global type declaration, since in your example we are importing the default export from `./api`, I needed to make the type declaration `declare const api: typeof import("./api").default` – kennyvh May 08 '22 at 18:50
  • perfect, thanks @hkennyv, will update – corolla May 09 '22 at 20:41
  • I've done it this way but my render files can't locate the type from this kind of declaration – Gabriel Silva Jul 26 '22 at 20:30
1

Have you required the preload file inside main.ts?

webPreferences: {
  nodeIntegration: false,
  contextIsolation: true,
  preload: path.resolve(path.join(__dirname, "preload.js"))
},

You must place this on the main window.

Depending on your webpack config, there may be one entry point bundle, and you will need to configure an additional webpack output for the preload.js file.

There is an example answer here: How to use preload.js properly in Electron