I've made a small Electron app which needs to load a preload js file.
When I start the app with electron .
, it finds the file, but when the app is packaged, it doesn't.
The call is made here:
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
nativeWindowOpen: true,
webSecurity: false,
preload: path.join(__dirname, 'preload.js')
}
})
My simplified package.json:
"name": "app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-packager . --platform=win32 --arch=x64 --overwrite"
}
"devDependencies": {
"electron": "^1.8.4",
"electron-packager": "^12.0.1",
}
My project structure:
- node_modules
- main.js
- preload.js
- package.json
I've checked the result of the path.join
and in both cases, the path is correct, and the file is there.
For peoples using Electron Forge webpack typescript boilerplate :
preload
key in package.json
file:{
"config": {
"forge": {
"plugins": [
[
"@electron-forge/plugin-webpack",
{
"mainConfig": "./webpack.main.config.js",
"renderer": {
"config": "./webpack.renderer.config.js",
"entryPoints": [
{
"html": "./src/index.html",
"js": "./src/renderer.tsx",
"name": "main_window",
"preload": {
"js": "./src/preload.ts"
}
}
]
}
}
]
]
}
}
}
Preload script can be a typescript file.
MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY
constant as value for preload
:// Tell typescript about this magic constant
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: any;
// [...]
const mainWindow = new BrowserWindow({
height: 1000,
width: 1500,
webPreferences: {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
}
});
preload.ts
write :import {
contextBridge,
ipcRenderer
} from 'electron';
contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing')
}
)
index.d.ts
file, write :declare global {
interface Window {
electron: {
doThing(): void;
}
}
}
Start your app, in your dev console you can type electron
and view it is defined.
BONUS: getting the typing right for the contextBridge exposed API:
Why a separated fie ? Not sure if needed, but I prefer not having to import an interface from a file that contain main process code (like preload.ts) in renderer process.
// exposed-main-api.model.ts
export interface ExposedMainAPI {
doThat(data: string): Promise<number>;
}
// index.d.ts
declare global {
interface Window {
electron: ExposedMainAPI
}
}
// preload.ts
import {
contextBridge,
ipcRenderer
} from 'electron';
const exposedAPI: ExposedAPI = {
// You are free to omit parameters typing and return type if you feel so.
// TS know the function type thanks to exposedAPI typing.
doThat: (data) => ipcRenderer.invoke('do-that-and-return-promise', data)
};
// note: this assume you have a `ipcMain.handle('do-thing-and-return-promise', ...)`
// somewhere that return a number.
contextBridge.exposeInMainWorld('electron', exposedAPI);
Credits:
index.d.ts
typing and small example usage of contextBridge
: https://github.com/electron/electron/issues/9920#issuecomment-743803249
See also:
Preload script needs to be specified as an absolute path. Thus it will differ from the time you're running it in development versus running it packaged as an asar file.
const getSourceDirectory = () => isDev()
? path.join(process.cwd(), 'build', 'src') // or wherever your local build is compiled
: path.join(process.resourcesPath, 'app', 'src'); // asar location
const preload = path.resolve(getSourceDirectory(), 'preload.js');
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With