'contextBridge.exposeInMainWorld and IPC with Typescript in Electron app: Cannot read property 'send' of undefined
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
If I set in App.tsx :
const sendProxy = window.api.send;
I get the same error and the window is not rendered :
What am I doing wrongly with Typescript and with Electron IPC? Looking forward to your kind help
Solution 1:[1]
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 project = await api.fetchX();
...
})();
}, []);
return <h1>My App</h1>
}
Solution 2:[2]
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
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | monki32 |