Bundle ES6 backend for Electron app with WebPack & obfuscation

This article is a brief guide about code organization plus configuration of WebPack & UglifyJS. Advices for smaller bundles on production builds.

Output of Gulp task for Webpack

Pretty popular subject nowadays to build a hybrid desktop application with Electron (and, in our case, React). Basically it uses Node.js for backend and a Chromium renderer for frontend, so the project structure reminds a lot any other web application with Node.js.

Corporate applications may be not open source, in this case the code should not be available, at least “as is”. Electron app’s resources are available maximum within ASAR file, which is a single file container without compression, so everything required for app’s work is available to anyone - both frontend and backend. If first part is pretty clear and common, second part might be a bit tricky, especially for complex application.

Bundle sources with WebPack

Should be quite simple, but here are few tips to keep in mind while working on the project:

  1. All “require()” usage should contain simple string paths. Expressions here may lead to array maps of file paths inside the bundled file, so all file structure is easily revealed. Plus, it maps all possible files to be included within directory recursively, so could be much and much more than you need for current entry.
  2. Its better to use a separate module or global function to read JSON files. You’ll get a mess in the bundled file with regular and easy “require()”, and it won’t work for dynamically created files (user settings, DB files, etc.) because in the place of require it will simply generate throwing an error.
  3. No reason to bundle NPM modules. They are open source, bundling will most probably lead to the huge JS file (see “externals” section in WebPack configuration). Good chances to get exception during packaging or running because of missing files etc.

Configuration is pretty self-explanatory, so here is an example. Please notice “entry”, “target” and “node” sections first. Last one is helpful to update all paths according to the new location — as result, we’ll get bundled file “app/main.js”. The “output.pathinfo” adds source file paths in comments, pretty helpful.

'use strict';
const
webpack = require('webpack'),
loader = __dirname + '/webpack.loader',
path = require('path');
const APP_PATH = path.join(__dirname, 'app');
const APP_SRC_PATH = path.join(APP_PATH, 'src');
const PKG = require(path.join(APP_PATH, 'package.json'));
const MODULES = PKG.dependencies;
const EXCLUDED_SRC = /(__tests__|node_modules)/;
module.exports = {
cache: false,
context: APP_PATH,
entry: [
path.join(APP_PATH, 'index.js')
],
target: 'electron',
output: {
path: APP_PATH,
pathinfo: true,
filename: 'main.js'
},
node: {
global: true,
__dirname: true,
__filename: true
},
module: {
loaders: [
{
test: /\.js$/,
exclude: EXCLUDED_SRC,
include: [
APP_SRC_PATH
],
loader: loader
}
]
},
externals: [
function (context, request, callback) {
let moduleName = request.split('/')[0];
let moduleParam = MODULES[moduleName] || false;
if (moduleParam) {
return callback(null, 'commonjs2 ' + request);
}
callback();
}
],
plugins: [
new webpack.IgnorePlugin(/\.(css|html|json|md|txt)$/)
]
};

Preferred WebPack loader is specified for JS files by the mask and in our case it is the custom loader — a local file in project root (“webpack.loader.js”). Don’t use one of many available NPM modules because in our case it may be pretty simple: don’t need any transformations yet for ES6 and use code as is. Just few lines may be enough:

'use strict';
module.exports = function (source) {
return source;
};

Plus, any other code can be added on demand, like removing anything from sources out of bundle, transpiling, etc:

'use strict';
const REGEX_SOMETHING = /something\s+(like_this)/gm;
module.exports = function (source) {
if (!this.request) {
return '';
}
    let filePath = this.request.indexOf('!')
? this.request.split('!').pop()
: this.request;
    return source
.replace(REGEX_SOMETHING, '// SOMETHING $1');
};

Uglify ES6/harmony

Few words on this only because popular solution like UglifyJS is not ready yet for ES6/harmony code. But if bundled version of Node.js (v6+) supports all code you write — no reason to transpile it to ES5, right? That was the reason not using pretty standard “babel-loader” as well.

For this case there’s an option to use “harmony” branch of UglifyJS git repo, simply specifying it in your “package.json”. All required features for our code are already implemented, but better check here.

"devDependencies": {
"gulp": "*",
"gulp-uglify": "*",
"uglify-js": "mishoo/UglifyJS2#harmony",
"webpack": "*"
}

With “gulp-uglify” it’s easy to use it as different UglifyJS as well (docs). Suggest to have extra task for this, to have minified code for builds and just “glued” for development. At least, it’s a lot faster. And no need in SourceMap.

Electron build: smaller bundles = goodness

Don’t forget to ignore all sources, tests and unused files in packager params. In case of “electron-packager”, here is an example:

asar: true,
ignore: [
/^\/__tests__/,
/.*test\.js$/,
/^\/src/,
/README\.md$/
],
prune: true, // now true by default

ASAR stores everything inside app dir as a single file. Essential, especially for heavy NPM module usage. Makes it much faster to copy/unpack application as well and more.

Also, check an option to remove everything you don’t need if sources are bundled, especially for front-end. For example, bundle vendors like React, ReactBootstrap, ReactIntl, etc. (usually there’s a “dist” inside NPM module dir) into something like “app/js/vendors.js” and have them in “devDependencies” section. In this case with “prune”, them and all their dependencies won’t be packed, only code you need.

Packaging built Electron app into DMG/Zip file

A bit off topic, but related and might be useful: we released “pack-dir” module some time ago to help with packaging for different platforms while working on Electron based apps this year and use it on production builds. DMG for OS X, Zip file for Windows, etc.

Conclusion

That’s it. Hope this article helps not spending extra time, avoiding few possible mistakes. But better check build stats and bundled non-minified file if there’s all you need, but nothing extra.

P.S. Special thanks to GitHub and community for Electron and it’s active development.

By Deathigner