Is there a good solution on how to include third party pre compiled binaries like imagemagick into an electron app? there are node.js modules but they are all wrappers or native binding to the system wide installed libraries. I wonder if it's possible to bundle precompiled binaries within the distribution.
Here's another method, tested with Mac and Windows so far. Requires 'app-root-dir' package, doesn't require adding anything manually to node_modules dir.
Put your files under resources/$os/, where $os is either "mac", "linux", or "win". The build process will copy files from those directories as per build target OS.
Put extraFiles option in your build configs as follows:
package.json
"build": {
"extraFiles": [
{
"from": "resources/${os}",
"to": "Resources/bin",
"filter": ["**/*"]
}
],
Use something like this to determine the current platform.
get-platform.js
import { platform } from 'os';
export default () => {
switch (platform()) {
case 'aix':
case 'freebsd':
case 'linux':
case 'openbsd':
case 'android':
return 'linux';
case 'darwin':
case 'sunos':
return 'mac';
case 'win32':
return 'win';
}
};
Call the executable from your app depending on env and OS. Here I am assuming built versions are in production mode and source versions in other modes, but you can create your own calling logic.
import { join as joinPath, dirname } from 'path';
import { exec } from 'child_process';
import appRootDir from 'app-root-dir';
import env from './env';
import getPlatform from './get-platform';
const execPath = (env.name === 'production') ?
joinPath(dirname(appRootDir.get()), 'bin'):
joinPath(appRootDir.get(), 'resources', getPlatform());
const cmd = `${joinPath(execPath, 'my-executable')}`;
exec(cmd, (err, stdout, stderr) => {
// do things
});
I think I was using electron-builder as base, the env file generation comes with it. Basically it's just a JSON config file.
See UPDATE below (this method isn't ideal now).
I did find a solution to this, but I have no idea if this is considered best practice. I couldn't find any good documentation for including 3rd party precompiled binaries, so I just fiddled with it until it finally worked with my ffmpeg binary. Here's what I did (starting with the electron quick start, node.js v6):
Mac OS X method
From the app directory I ran the following commands in Terminal to include the ffmpeg binary as a module:
mkdir node_modules/ffmpeg
cp /usr/local/bin/ffmpeg node_modules/ffmpeg/
cd node_modules/.bin
ln -s ../ffmpeg/ffmpeg ffmpeg
(replace /usr/local/bin/ffmpeg with your current binary path, download it from here) Placing the link allowed electron-packager to include the binary I saved to node_modules/ffmpeg/.
Then to get the bundled app path (so that I could use an absolute path for my binary... relative paths didn't seem to work no matter what I did) I installed the npm package app-root-dir by running the following command:
npm i -S app-root-dir
Now that I had the root app directory, I just append the subfolder for my binary and spawned from there. This is the code that I placed in renderer.js:.
var appRootDir = require('app-root-dir').get();
var ffmpegpath=appRootDir+'/node_modules/ffmpeg/ffmpeg';
console.log(ffmpegpath);
const
spawn = require( 'child_process' ).spawn,
ffmpeg = spawn( ffmpegpath, ['-i',clips_input[0]]); //add whatever switches you need here
ffmpeg.stdout.on( 'data', data => {
console.log( `stdout: ${data}` );
});
ffmpeg.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
});
Windows Method
Open your electron base folder (electron-quick-start is the default name), then go into the node_modules folder. Create a folder there called ffmpeg, and copy your static binary into this directory. Note: it must be the static version of your binary, for ffmpeg I grabbed the latest Windows build here.
To get the bundled app path (so that I could use an absolute path for my binary... relative paths didn't seem to work no matter what I did) I installed the npm package app-root-dir by running the following command from a command prompt in my app directory:
npm i -S app-root-dir
Within your node_modules folder, navigate to the .bin subfolder. You need to create a couple of text files here to tell node to include the binary exe file you just copied. Use your favorite text editor and create two files, one named ffmpeg with the following contents:
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../ffmpeg/ffmpeg" "$#"
ret=$?
else
node "$basedir/../ffmpeg/ffmpeg" "$#"
ret=$?
fi
exit $ret
And the the second text file, named ffmpeg.cmd:
#IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" "%~dp0\..\ffmpeg\ffmpeg" %*
) ELSE (
#SETLOCAL
#SET PATHEXT=%PATHEXT:;.JS;=;%
node "%~dp0\..\ffmpeg\ffmpeg" %*
)
Next you can run ffmpeg in your Windows electron distribution (in renderer.js) as follows (I'm using the app-root-dir node module as well). Note the quotes added to the binary path, if your app is installed to a directory with spaces (eg C:\Program Files\YourApp) it won't work without these.
var appRootDir = require('app-root-dir').get();
var ffmpegpath = appRootDir + '\\node_modules\\ffmpeg\\ffmpeg';
const
spawn = require( 'child_process' ).spawn;
var ffmpeg = spawn( 'cmd.exe', ['/c', '"'+ffmpegpath+ '"', '-i', clips_input[0]]); //add whatever switches you need here, test on command line first
ffmpeg.stdout.on( 'data', data => {
console.log( `stdout: ${data}` );
});
ffmpeg.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
});
UPDATE: Unified Simple Method
Well, as time as rolled on and Node has updated, this method is no longer the easiest way to include precompiled binaries. It still works, but when npm install is run the binary folders under node_modules will be deleted and have to be replaced again. The below method works for Node v12.
This new method obviates the need to symlink, and works similarly for Mac and Windows. Relative paths seem to work now.
You will still need appRootDir: npm i -S app-root-dir
Create a folder under your app's root directory named bin and place your precompiled static binaries here, I'm using ffmpeg as an example.
Use the following code in your renderer script:
const appRootDir = require('app-root-dir').get();
const ffmpegpath = appRootDir + '/bin/ffmpeg';
const spawn = require( 'child_process' ).spawn;
const child = spawn( ffmpegpath, ['-i', inputfile, 'out.mp4']); //add whatever switches you need here, test on command line first
child.stdout.on( 'data', data => {
console.log( `stdout: ${data}` );
});
child.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
});
The above answers helped me figure out how it could done but there is a much efficient way to distribute binary files.
Taking cues from tsuriga's answer, here is my code:
Note: replace or add OS path as required.
Update - 4 Dec 2020
This answer has been updated. Find the previous code to the bottom of this answer.
Download the needed packages
yarn add electron-root-path electron-is-packaged
# or
npm i electron-root-path electron-is-packaged
Create a directory ./resources/mac/bin
Place you binaries inside this folder
Create a file ./app/binaries.js and paste the following code:
import path from 'path';
import { rootPath as root } from 'electron-root-path';
import { isPackaged } from 'electron-is-packaged';
import { getPlatform } from './getPlatform';
const IS_PROD = process.env.NODE_ENV === 'production';
const binariesPath =
IS_PROD && isPackaged // the path to a bundled electron app.
? path.join(root, './Contents', './Resources', './bin')
: path.join(root, './build', getPlatform(), './bin');
export const execPath = path.resolve(
path.join(binariesPath, './exec-file-name')
);
Create a file ./app/get-platform.js and paste the following code:
'use strict';
import { platform } from 'os';
export default () => {
switch (platform()) {
case 'aix':
case 'freebsd':
case 'linux':
case 'openbsd':
case 'android':
return 'linux';
case 'darwin':
case 'sunos':
return 'mac';
case 'win32':
return 'win';
}
};
Add these lines inside the ./package.json file:
"build": {
....
"extraFiles": [
{
"from": "resources/mac/bin",
"to": "Resources/bin",
"filter": [
"**/*"
]
}
],
....
},
import binary as:
import { execPath } from './binaries';
#your program code:
var command = spawn(execPath, arg, {});
Why this is better?
The above answers require an additional package called app-root-dir
tsuriga's answer doesn't handle the (env=production) build or the pre-packed versions properly. He/she has only taken care of development and post-packaged versions.
Previous answer
Avoid using electron.remote as it is getting depreciated
app.getAppPath might throw errors in the main process.
./app/binaries.js
'use strict';
import path from 'path';
import { remote } from 'electron';
import getPlatform from './get-platform';
const IS_PROD = process.env.NODE_ENV === 'production';
const root = process.cwd();
const { isPackaged, getAppPath } = remote.app;
const binariesPath =
IS_PROD && isPackaged
? path.join(path.dirname(getAppPath()), '..', './Resources', './bin')
: path.join(root, './resources', getPlatform(), './bin');
export const execPath = path.resolve(path.join(binariesPath, './exec-file-name'));
tl;dr:
yes you can! but it requires you to write your own self-contained addon which does not make any assumptions on system libraries. Moreover in some cases you have to make sure that your addon is compiled for the desired OS.
Lets break this question in several parts:
- Addons (Native modules):
Addons are dynamically linked shared objects.
In other words you can just write your own addon with no dependency on system wide libraries (e.g. by statically linking required modules) containing all the code you need.
You have to consider that such approach is OS-specific, meaning that you need to compile your addon for each OS that you want to support! (depending on what other libraries you may use)
- Native modules for electron:
The native Node modules are supported by Electron, but since Electron is using a different V8 version from official Node, you have to manually specify the location of Electron's headers when building native modules
This means that a native module which has been built against node headers must be rebuilt to be used inside electron. You can find how in electron docs.
- Bundle modules with electron app:
I suppose you want to have your app as a stand-alone executable without requiring users to install electron on their machines. If so, I can suggest using electron-packager.
following Ganesh answer's which was really a great help, in my case what was working in binaries.js (for a mac build - did not test for windows or linux) was:
"use strict";
import path from "path";
import { app } from "electron";
const IS_PROD = process.env.NODE_ENV === "production";
const root = process.cwd();
const { isPackaged } = app;
const binariesPath =
IS_PROD && isPackaged
? path.join(process.resourcesPath, "./bin")
: path.join(root, "./external");
export const execPath = path.join(binariesPath, "./my_exec_name");
Considering that my_exec_name was in the folder ./external/bin and copied in the app package in ./Resources/bin. I did not use the get_platforms.js script (not needed in my case). app.getAppPath() was generating a crash when the app was packaged.
Hope it can help.
Heavily based on Ganesh's answer, but simplified somewhat. Also I am using the Vue CLI Electron Builder Plugin so the config has to go in a slightly different place.
Create a resources directory. Place all your files in there.
Add this to vue.config.js:
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
...
"extraResources": [
{
"from": "resources",
"to": ".",
"filter": "**/*"
}
],
...
}
}
}
}
Create a file called resources.ts in your src folder, with these contents:
import path from 'path';
import { remote } from 'electron';
// Get the path that `extraResources` are sent to. This is `<app>/Resources`
// on macOS. remote.app.getAppPath() returns `<app>/Resources/app.asar` so
// we just get the parent directory. If the app is not packaged we just use
// `<current working directory>/resources`.
export const resourcesPath = remote.app.isPackaged ?
path.dirname(remote.app.getAppPath()) :
path.resolve('resources');
Note I haven't tested this on Windows/Linux but it should work assuming app.asar is in the resources directory on those platforms (I assume so).
Use it like this:
import { resourcesPath } from '../resources'; // Path to resources.ts
...
loadFromFile(resourcesPath + '/your_file');
I am running discord.js on Heroku. I wrote a module, but in the logs says "Error: Cannot find module '.\modules**.js'".
The code itself:
const { MessageEmbed, MessageAttachment } = require('discord.js');
const demotivator = require('C:\\app\\modules\\demotivator.js')
var fs = require('fs');
module.exports = {
config: {
name: `demo`,
aliases: [`demo`]
}
I don't use heroku but heroku certainly doesn't have a C drive. I'm assuming you have copied and pasted this code from your computer. Correct me if I'm wrong but you would need to find the path of demotivator.js and then paste it in the const statement. If the demotivator is in the same directory then it would be
const demotivator=require('./demotivator.js')
if it is in a folder in that same directory it would be
const demotivator=require('./<foldername>/demotivator.js')
If demotivator is behind the current directory it would be
const demotivator=require('../demotivator.js')
if it it 2 directories behind you can do
const demotivator=require('../../demotivator.js')
you can keep doing ../../ if you were wondering
Hello i need to download an audio file from an external URL and it works but without the .mp3 extension, which is required for the player to recognize it, here is my code:
getAudio(token:String,interestPointId:string,link:string, audioId):string{
const downloadManager = new Downloader();
var path:string;
const imageDownloaderId = downloadManager.createDownload({
url:link
});
downloadManager
.start(imageDownloaderId, (progressData: ProgressEventData) => {
console.log(`Progress : ${progressData.value}%`);
console.log(`Current Size : ${progressData.currentSize}%`);
console.log(`Total Size : ${progressData.totalSize}%`);
console.log(`Download Speed in bytes : ${progressData.speed}%`);
})
.then((completed: DownloadEventData) => {
path=completed.path;
console.log(`Image : ${completed.path}`);
})
.catch(error => {
console.log(error.message);
});
return path;
}
is just a modified version from the example (https://market.nativescript.org/plugins/nativescript-downloader)
the goal is to download the file and get the path to it while the name should be something like ~path/idAudio.mp3
any help is welcome, thanks in advance!
You can set the fileName and the path on the createDownload it is not documented (I forgot) there is interface you can use so what you need to is the following also i would recommend you not try to save the file to the project root e.g ~/blah/blah.mp3 that would fail on a real ios device
downloadManager.createDownload({
url:link
path:somePath,
fileName: 'idAudio.mp3'
}
I'm new to bundlers and am currently learning about Fusebox. I really like it so far except that I can't figure out how to use it for a multi-page project. So far I've only been able to find a tutorial on how to do this using webpack, not for fusebox.
Input files in src folder:
index.html
index2.html
index.ts
Desired output in dist folder:
app.js
vendor.js
index.html
index2.html
Actual output in dist folder:
app.js
vendor.js
index.html
Here is my config in the fuse.js file:
Sparky.task("config", () => {
fuse = FuseBox.init({
homeDir: "src",
output: "dist/$name.js",
hash: isProduction,
sourceMaps: !isProduction,
plugins: [
[SassPlugin(), CSSPlugin()],
CSSPlugin(),
WebIndexPlugin({
title: "Welcome to FuseBox index",
template: "src/index.html"
},
WebIndexPlugin({
title: "Welcome to FuseBox index2",
template: "src/index2.html"
},
isProduction && UglifyJSPlugin()
]
});
// vendor should come first
vendor = fuse.bundle("vendor")
.instructions("~ index.ts");
// out main bundle
app = fuse.bundle("app")
.instructions(`!> [index.ts]`);
if (!isProduction) {
fuse.dev();
}
});
Setting WebIndexPlugin twice within plugins doesn't work. What is the correct way to set up a multi-html page project with fusebox?
The WebIndexPlugin can not be configured, to output more than one html file.
But if you don't use a hash for the generated bundles (e.g.: output: "dist/$name.$hash.js"), you don't need the WebIndexPlugin -- you can remove it completly from the plugins option. Because you already know the names of the generated bundles (vendor.js and app.js) you can just include the following lines
<script src="vendor.js"></script>
<script src="app.js"></script>
instead of the placeholder $bundles.
If you want, that both html files are copied from your src directory into your dist directory, you can add the following lines to your fuse.js script:
const fs = require('fs-extra');
fs.copySync('src/index.html', 'dist/index.html');
fs.copySync('src/index2.html', 'dist/index2.html');
Note: Don't forget to add fs-extra:^5.0.0 to your package.json
Might not been the case when the question was asked, but WebIndexPlugin now can be specified multiple times and also takes optional bundles parameter where list of bundles to be included in html can be specified (all bundles are included by default).
For example 2 html files (app1.html, app2.html) where each includes a common library (vendor.js), and different entry points (app1.js and app2.js)
app1.html
vendor.js
app1.js
app2.html
vendor.js
app2.js
Config would look like this:
const fuse = FuseBox.init({
homeDir : "src",
target : 'browser#es6',
output : "dist/$name.js",
plugins: [
WebIndexPlugin({
target: 'app1.html',
bundles:['vendor', 'app1']
}),
WebIndexPlugin({
target: 'app2.html',
bundles:['vendor', 'app2']
})
]
})
// vendor bundle, extracts dependencies from index1 and index2:
fuse.bundle("vendor").instructions("~[index1.ts,index2.ts]")
// app1 and app2, bundled separately without dependencies:
fuse.bundle("app1").instructions("!>index1.ts")
fuse.bundle("app2").instructions("!>index2.ts")
I'm using node-windows to set up my application to run as a Windows Service. I am using node-config to manage configuration settings. Of course, everything is working fine when I run my application manually using node app.js command. When I install it as a service and it starts, the configuration settings are empty. I have production.json file in ./config folder, and I can set NODE_ENV to production in the install script. I can confirm that the variable is set correctly and still nothing. log.info('CONFIG_DIR: ' + config.util.getEnv('CONFIG_DIR')); produces undefined even if I explicitly set it in env value for the service. Looking for any insight.
install script:
var Service = require('node-windows').Service;
var path = require('path');
// Create a new service object
var svc = new Service({
name:'Excel Data Import',
description: 'Excel Data Import Service.',
script: path.join(__dirname, "app.js"), // path application file
env:[
{name:"NODE_ENV", value:"production"},
{name:"CONFIG_DIR", value: "./config"},
{name:"$NODE_CONFIG_DIR", value: "./config"}
]
});
// Listen for the "install" event, which indicates the
// process is available as a service.
svc.on('install',function(){
svc.start();
});
svc.install();
app script:
var config = require('config');
var path = require('path');
var EventLogger = require('node-windows').EventLogger;
var log = new EventLogger('Excel Data Import');
init();
function init() {
log.info("init");
if(config.has("File.fileFolder")){
var pathConfig = config.get("File.fileFolder");
log.info(pathConfig);
var DirectoryWatcher = require('directory-watcher');
DirectoryWatcher.create(pathConfig, function (err, watcher) {
//...
});
}else{
log.info("config doesn't have File.fileFolder");
}
}
I know this response is very late, but also i had the same problem, and here is how i solved it :
var svc = new Service({
name:'ProcessName',
description: 'Process Description',
script: require('path').join(__dirname,'bin\\www'),
env:[
{name: "NODE_ENV", value: "development"},
{name: "PORT", value: PORT},
{name: "NODE_CONFIG_DIR", value: "c:\\route-to-your-proyect\\config"}
]
});
When you are using windows, prefixing your enviroment variables with $ , is not required.
Also, when your run script isnĀ“t on the same dir as your config dir, you have to provide a full path to your config dir.
When you have errors with node-windows , is also helpful dig into the error log. It is located on rundirectory/daemon/processname.err.log
I hope this will help somebody.