I have an electron app that builds and runs in development, but when packaging the app with electron-builder, the preload script is not packaged in the right location.
This is a well documented issue and there are very similar questions here and here for example, but none of the replies or solutions are working in my case.
From my electron.js file:
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(app.getAppPath(), 'src/preload.js'),
contextIsolation: true,
},
});
// In production, set the initial browser path to the local bundle generated
// by the Create React App build process.
// In development, set it to localhost to allow live/hot-reloading.
const appURL = app.isPackaged
? url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true,
})
: 'http://localhost:3000';
mainWindow.loadURL(appURL);
mainWindow.webContents.openDevTools();
}
My preload script:
const { contextBridge, shell } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
openBrowserWindow: (url) => shell.openExternal(url)
}
)
And my Electron app package.json:
"build": {
"extends": null,
"appId": "com.app",
"productName": "App",
"directories": {
"output": "dist"
},
"mac": {
"target": {
"target": "pkg",
"arch": [
"universal"
]
},
"darkModeSupport": "true",
"extendInfo": "app"
},
"pkg": {
"installLocation": "/Applications",
"overwriteAction": "upgrade"
},
"files": [
"**",
"../app/src/*",
"src/preload.js"
],
"extraResources": [
"../app/src/*",
"src/preload.js"
],
"extraFiles": [
"../app/src/*",
"src/preload.js"
]
}
Above I have tried to make sure the "src/preload.js" file is copied over in different ways, but I still get the error:
Unable to load preload script: ...app/Contents/Resources/app.asar/src/preload.js
Error: Cannot find module '...app/Contents/Resources/app.asar/src/preload.js'
The preload script is in fact copied over, but it is not part of the app.asar file. It is copied in to a src folder outside of the Resources folder which contains the app.asar file:
How do I correctly configure electron-builder so this file is in the right location and can be accessed at package runtime?
If you do:
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.resolve(app.getAppPath(), 'preload.js'),
contextIsolation: true,
},
});
Does it works ? (worked for me with electron-webpack and electron-builder)
preload: path.join(app.getAppPath(), 'src/preload.js'),
As you are not packaging the preload.js into the app package file (asar default), this won't work like this. app.getAppPath() will indicate the app package file(or directory, in case you are setting asar as false)
Your code is indicating /xxx.app/Contents/Resources/app.asar/src/preload.js or /xxx.app/Contents/Resources/app/src/preload.js
Your preload script file is not there but in the 2nd parent's directory.
So here is the correct path in your case,
path.join(app.getAppPath(), '..', '..', 'src', 'preload.js');
First, add console logs for testing.
console.log({dirname: __dirname})
console.log({getAppPath: app.getAppPath()})
console.log({resourcesPath: process.resourcesPath})
const mainWindow = new BrowserWindow({ ... })
Second, you have to add contextIsolation: true.
If you are using electron-builder and for some reason you cannot add contextIsolation: true you can use this workaround:
package.json
"build": {
...
"extraResources": [
...
"app/preload.js" // <---- add your path
],
}
electron.js
const preloadPath =
process.env.NODE_ENV === 'development'
? path.join(__dirname, '../preload.js') // <---- add your path
: path.join(process.resourcesPath, '/app/preload.js'); // <---- add your path
const mainWindow = new BrowserWindow({
...
webPreferences: {
contextIsolation: false,
preload: preloadPath,
...
}
})
What is path.join(process.resourcesPath, '/app/preload.js') ?
After building your app you can find your extra resources here
C:\Users\<user>\AppData\Local\Programs\<app>\resources - for Windows.
For MacOS you can right click on your app and click on Show Package Contents > Resources
Related
I'm using revealjs to create responsive presentations. The problem with revealjs is that all the slides code is written in a single HTML file which can be messy to some level (Some presentations' HTML code reached about 3500 lines of HTML in that single file).
I'm now restructuring this system and I would like to have a directory named slides that contains each slide HTML file. Each of these files is named slidenumber.html. Finally, I want to bundle all of the files with webpack 5 into a single HTML file in dist. I managed to achieve this but it has an issue with the dev server.
webpack.config.js
// ... imports ....
module.exports = {
...,
plugins: [
....,
new HtmlWebpackPlugin({
filename: "index.html",
inject: true,
templateContent: getTemplate(),
}),
new WatchExternalFilesPlugin({
files: ["./slides/*.html"],
}),
],
module: {
rules: [...],
},
devServer: {
port: 8080,
},
};
The getTemplate function loops over the HTML files in the slides directory and returns them wrapped with the template boilerplate
This is the function for reference
getTemplate.js
const fs = require("fs/promises");
const path = require("path");
const { parse } = require("node-html-parser");
module.exports = async () => {
const template = parse(
await fs.readFile(path.join(__dirname, "../templates/index.html"))
);
const files = await fs.readdir(path.join(__dirname, "../slides"));
for await (const fileName of files) {
const slide = parse(
await fs.readFile(path.join(__dirname, `../slides/${fileName}`))
);
template.querySelector("#slides").appendChild(slide);
}
return template.toString();
};
all of the above code is working fine on build but when running the dev server, I can't get the HtmlWebpackPlugin to re-execute the templateContent: getTemplate() on the change of any HTML slide file in the slides directory and as a result, when I edit any file of the slides HTML files in the slides directory, I don't get any update.
I'm aware that templateContent is supposed to run only on the start of the server but I'm asking if there is any other feature that can get me to the required behavior.
Thanks if you made it to here and excuse my English, I'm not a native speaker.
I could achieve the behavior I described in the question by setting a middleware from the dev server options that catches any request and returns the output of the getTemplate function.
This is the configurations for the dev server
webpack.config.dev.js
// ...imports...
module.exports = {
mode: "development",
entry: { index: "./main.js" },
output: {...},
module: {
rules: [...],
},
devServer: {
port: 8080,
watchFiles: ["./slides/*.html"],
hot: true,
onBeforeSetupMiddleware: function (devServer) {
devServer.app.get("/", async function (req, res) {
res.send(await getTemplate());
});
},
},
};
These are the configurations for the production server
webpack.config.production.js
// ...imports...
module.exports = {
mode: "production",
entry: { index: "./main.js" },
output: {...},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
templateContent: getTemplate(),
inject: false,
}),
],
module: {
rules: [...],
},
};
I used the webpackHtmlPlugin in production as usual but in development, I didn't use it at all since it can't reload the templates on the build
In development, though I lost the ability to add the hash number to the compiled js file as I won't be able to predict the hash and inject its script tag. The compiled file had the same name as the original file and I added the script tag manually in the HTML template file.
Hope this helps anyone!
I`m trying to ignore all *.html files so that webpack devserver wont reload when the files changes.
My config looks like this
const path = require('path');
module.exports = {
pages:
{
index:
{
entry: 'src/main.js',
template: 'public/Index.html'
}
},
outputDir: "wwwroot",
filenameHashing: true,
configureWebpack:
{
devServer: {
static: {
directory: path.join(__dirname, 'public'),
watch:
{
ignored: '*.html',
usePolling: false,
}
},
},
watchOptions:
{
aggregateTimeout: 3000,
ignored: /.*\.html/
}
}
}
I`m using #vue/cli-service: 5.0.4 which uses webpack 5. And it doesnt work, when I change html file webpack dev-server still reloads page. How I can make it work so changing html pages will not reload page?
Type regex expression in devServer.static.watch.ignored instead string
ignored: /.*\.html/
I wanted to use the following code to send a message from the renderer to the main process, which then writes it to a log file using electron-log. My main.js looks like this:
import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const { ipcMain } = require('electron');
const log = require('electron-log');
const isDevelopment = process.env.NODE_ENV !== 'production'
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
async function createWindow() {
// Create the browser window.
const win = new BrowserWindow({
frame: true,
width: 400,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false
}
})
ipcMain.on('infoLog', (event, args) => {
log.info(args)
});
....
Now I tried to address the IPC in my App.vue accordingly:
import Navbar from '#/components/Navbar'
const { ipcRenderer } = require('electron')
export default {
name: 'App',
components: {
Navbar
},
created: function () {
ipcRenderer.send('infoLog','A async message to main')
}
}
When I start it with yarn electron:serve I see this error in the console of the window:
Uncaught ReferenceError: __dirname is not defined
at eval (webpack-internal:///./node_modules/electron/index.js:4)
at Object../node_modules/electron/index.js (chunk-vendors.js:2778)
at __webpack_require__ (app.js:849)
at fn (app.js:151)
at eval (webpack-internal:///./node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader-v16/dist/index.js?!./src/App.vue?vue&type=script&lang=js:5)
at Module../node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader-v16/dist/index.js?!./src/App.vue?vue&type=script&lang=js (app.js:938)
at __webpack_require__ (app.js:849)
at fn (app.js:151)
at eval (webpack-internal:///./src/App.vue?vue&type=script&lang=js:2)
at Module../src/App.vue?vue&type=script&lang=js (app.js:1099)
What I don't understand is that I set it up exactly like Electron's doc:
https://www.electronjs.org/docs/api/ipc-main
You have 2 different issues here:
the correct webpack configuration to support node.js code
missing node integration to use node API like require
The stacktrace you are seeing here likely comes from an incorrect webpack configuration. Unless told otherwise, webpack tries to replace __dirname with something different. Here we don't want that - node provides __dirname and we want to use it, so we have to tell webpack to leave __dirname alone.
You'll find an example in the webpack documentation.
For webpack 5 adding a node section should help:
module.exports = {
//...
node: {
global: false,
__filename: false,
__dirname: false,
}
};
After you solved this problem you'll likely fall over the issue that your browser window does not know require. You can reintroduce specific node API like the IPC by using a preload script. Don't activate the full node integration without knowing what you are doing.
For an example, have a look at this answer.
I'm struggling to understand how to correctly import ipcRenderer in a .vue file.
I put in /src/background.js file :
webPreferences: {
nodeIntegration:false,
contextIsolation: true, // protects against prototype pollution
preload: path.join(__dirname, "../dist_electron/preload.js"),
}
And, based on https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration I put in preload.js :
window.ipcRenderer = ipcRenderer
webpack.config.js :
module.exports = {
entry: './src/background.js',
target: 'node',
output: {
path: path.join(__dirname, 'build'),
filename: 'background.js'
}
}
In order to facilitate the debugging, I created a github repo.
You can git clone the repo from here: https://github.com/raphael10-collab/ElectronVueTypeScriptScaffolding.git
After executing yarn -> yarn electron:serve
you will get the correct page.
But when activating in /src/views/Home.vue this line:
//import { ipcRenderer } from 'electron'
you will get this error:
__dirname is not defined
Environment Info:
System:
OS: Linux 5.4 Ubuntu 18.04.5 LTS (Bionic Beaver)
CPU: (8) x64 Intel(R) Core(TM) i7-4790K CPU # 4.00GHz
Binaries:
Node: 14.5.0 - ~/.nvm/versions/node/v14.5.0/bin/node
Yarn: 1.22.4 - /usr/bin/yarn
npm: 6.14.5 - ~/.nvm/versions/node/v14.5.0/bin/npm
Browsers:
Chrome: 85.0.4183.83
Firefox: 79.0
npmPackages:
#vue/babel-helper-vue-jsx-merge-props: 1.0.0
#vue/babel-plugin-transform-vue-jsx: 1.1.2
#vue/babel-preset-app: 4.4.6
#vue/babel-preset-jsx: 1.1.2
#vue/babel-sugar-functional-vue: 1.1.2
#vue/babel-sugar-inject-h: 1.1.2
#vue/babel-sugar-v-model: 1.1.2
#vue/babel-sugar-v-on: 1.1.2
#vue/cli-overlay: 4.4.6
#vue/cli-plugin-babel: ~4.4.0 => 4.4.6
#vue/cli-plugin-e2e-cypress: ~4.4.0 => 4.4.6
#vue/cli-plugin-router: ~4.4.0 => 4.4.6
#vue/cli-plugin-typescript: ~4.4.0 => 4.4.6
#vue/cli-plugin-unit-mocha: ~4.4.0 => 4.4.6
#vue/cli-plugin-vuex: ~4.4.0 => 4.4.6
#vue/cli-service: ~4.4.0 => 4.4.6
#vue/cli-shared-utils: 4.4.6
#vue/component-compiler-utils: 3.2.0
#vue/preload-webpack-plugin: 1.1.2
#vue/test-utils: ^1.0.3 => 1.0.3
#vue/web-component-wrapper: 1.2.0
babel-helper-vue-jsx-merge-props: 2.0.3
typescript: ^3.9.7 => 3.9.7
vue: ^2.6.11 => 2.6.11
vue-class-component: ^7.2.5 => 7.2.5
vue-cli-plugin-electron-builder: ~2.0.0-rc.4 => 2.0.0-rc.4
vue-hot-reload-api: 2.3.4
vue-i18n: ^8.20.0 => 8.20.0
vue-loader: 15.9.3
vue-property-decorator: ^9.0.0 => 9.0.0
vue-router: ^3.2.0 => 3.3.4
vue-style-loader: 4.1.2
vue-template-compiler: ^2.6.11 => 2.6.11
vue-template-es2015-compiler: 1.9.1
vuex: ^3.5.1 => 3.5.1
vuex-class: ^0.3.2 => 0.3.2
npmGlobalPackages:
#vue/cli: 4.4.6
node version: v14.5.0
Update 1)
I tried to set webPreferences as follows (with nodeIntegration: true) :
webPreferences: {
nodeIntegration: true,
//contextIsolation: true, // protects against prototype pollution
//preload: path.join(__dirname, "../dist_electron/preload.js"),
},
and got this error:
fs.existsSync is not a function
Searching around for info about this kind of problem, I found this post:
How to resolve fs.existsSync is not a function
With this link: https://webpack.js.org/concepts/targets/
But I already specified in webpack.config.js the target ‘node’:
in webpack.config.js :
module.exports = {
entry: './src/background.js',
target: 'node',
output: {
path: path.join(__dirname, 'build'),
filename: 'background.js'
}
}
So... how to solve this new problem?
By the way,
Why must I put
webPreferences: {
nodeIntegration: true,
}
if, for security reasons, it is more secure to have:
webPreferences: {
nodeIntegration:false,
contextIsolation: true, // protects against prototype pollution
preload: path.join(__dirname, "../dist_electron/preload.js"),
}
dist_electron/preload.js :
const {
contextBridge,
ipcRenderer
} = require("electron");
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
let validChannels = ["toMain"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["fromMain"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) =>
func(...args));
}
}
}
);
window.ipcRenderer = ipcRenderer
https://www.electronjs.org/docs/tutorial/security#electron-security-warnings
Update 2)
in vue.config.js I've put:
module.exports = {
pluginOptions: {
electronBuilder: {
preload: 'dist_electron/preload.js',
// Or, for multiple preload files:
//preload: { preload: 'src/preload.js', otherPreload:
//'src/preload2.js' }
}
}
}
But I get the same error when I do
yarn electron:serve
UncaughtReferenceError: __dirname is not defined
When setting nodeIntegration: true (but I would prefer to set it to false, and use preload.js file), I get this other error (as above):
Uncaught TypeError: fs.existsSync is not a function
Uncaught TypeError: fs.existsSync is not a function
How to solve the problem?
Looking forward to your kind help
Updated Answer - Nodeintegration disabled and contextIsolation enabled
In order to use the ipcRenderer with Vue CLI plugin Electron Builder you need to first setup electron to utilize a preload.js file.
Inside your vue.config.js file you need to add the preload.js path like this:
// vue.config.js - project root
module.exports = {
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
// Or, for multiple preload files:
preload: { preload: 'src/preload.js', otherPreload: 'src/preload2.js' }
}
}
}
Next you need to updated your background.js file to use preload.js in the web preferences like this:
// src/background.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
enableRemoteModule: true,
preload: path.join(__dirname, 'preload.js'),
},
})
Note: nodeIntegration is disabled and contextIsolation is enabled by default
Once you have that complete you can create the preload.js file in your src directory.
With contextIsolation enabled you need to import the contextBridge along with ipcRenderer. Then you can can expose the ipcRenderer to your client.
Then add this to the file:
// src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose ipcRenderer to the client
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
let validChannels = ['nameOfClientChannel'] // <-- Array of all ipcRenderer Channels used in the client
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['nameOfElectronChannel'] // <-- Array of all ipcMain Channels used in the electron
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
Note: You need to make sure your preload.js file is in the src folder and not dist_electron
To test and make sure the preload file is working you can also create an alert in the preload.js file
// src/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// Expose ipcRenderer to the client
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, data) => {
let validChannels = ['nameOfClientChannel'] // <-- Array of all ipcRenderer Channels used in the client
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
receive: (channel, func) => {
let validChannels = ['nameOfElectronChannel'] // <-- Array of all ipcMain Channels used in the electron
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
alert("It Worked!") // Remove this line once you confirm it worked
When you have verified that your preload script is working correctly, you can access the ipcRenderer from your vue app.
like this:
// src/App.vue
<template>
\\ Some html
</template>
<script>
export default {
name: "App",
methods: {
test(){
window.ipcRenderer.send(channel, args...) // or any other ipcRenderer method you want to invoke
}
};
</script>
In electron you can listen for those events
// background.js
ipcMain.on(channel, (event, args) => {
// Do stuff
});
Sources:
Preload Files
Node Integration
With contextIsolation = true, is it possible to use ipcRenderer?
Original Answer
In order to use the ipcRenderer with Vue CLI plugin Electron Builder you need to first setup electron to utilize a preload.js file.
Inside your vue.config.js file you need to add the preload.js path like this:
// vue.config.js - project root
module.exports = {
pluginOptions: {
electronBuilder: {
preload: 'src/preload.js',
// Or, for multiple preload files:
preload: { preload: 'src/preload.js', otherPreload: 'src/preload2.js' }
}
}
}
Next you need to updated your background.js file to use preload.js in the web preferences like this:
// src/background.js
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#node-integration for more info
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
+ preload: path.join(__dirname, 'preload.js')
}
})
Once you have that complete you can create the preload.js file in your src directory
Then add this to the file:
// src/preload.js
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer
Note: You need to make sure your preload.js file is in the src folder and not dist_electron
To test and make sure the preload file is working you can also create an alert in the preload.js file
// src/preload.js
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer
alert("It Worked!") // Remove this line once you confirm it worked
When you have verified that your preload script is working correctly, you can access the ipcRenderer from your vue app.
like this:
// src/App.vue
<template>
\\ Some html
</template>
<script>
export default {
name: "App",
methods: {
test(){
window.ipcRenderer.send(channel, args...) // or any other ipcRenderer method you want to invoke
}
};
</script>
Sources:
Preload Files
Node Integration
What worked for me was setting the electron window's contextIsolation to false.
So in your main.js wherever you create a BrowserWindow it would look like this:
const win = new BrowserWindow({
webPreferences: {
contextIsolation: false,
preload: path.join(__dirname, 'preload.js'),
},
})
And then in preload.js you can simply do
const { ipcRenderer } = require('electron')
window.ipcRenderer = ipcRenderer
And then you'll have access to ipcRenderer anywhere in your vue code.
It seems that in the current version of electron contextIsolation defaults to true, which makes the window that preload.js sees different from the one your vue app sees.
You need to set nodeIntegration to true.
This enables NodeJs in the renderer process (i.e the front-end) so you can use stuff like fs (FileSystem) and other NodeJs-only features in your Vue code.
As ipcRenderer requires NodeJs's environnement too (__dirname is a global variable for NodeJs only), it needs to be activated.
I'm getting the following error when trying to build:
Building for production...Error: ENOENT: no such file or directory, stat '/Users/me/Code/project/index.html'
Package: "prerender-spa-plugin": "^3.1.0"
File: vue.config.js:
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');
module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV !== 'production') return;
return {
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname),
routes: ['/'],
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
keepClosingSlash: true,
sortAttributes: true,
},
renderer: new Renderer({
renderAfterDocumentEvent: 'render-event',
}),
}),
],
};
},
};
I don't have any routes, only a single index.html page.
Also, when I run yarn build and get that error, I try to kill the process in terminal but it keeps returning Building for production... without anything happening, and I have to quit terminal for it to stop.
Edit: I've also tried adding staticDir: path.join(__dirname, './public') but the build hangs without any errors.
Try adding headless: false to your renderer options. This will open a Chromium browser with your specified array of routes. Open the inspector in the Chromium browser and its very likely that you will see errors in the console.