Electron on the AppStore, Pain & tears

Yoann Moinet
8 min readDec 5, 2017

--

This article is part of a 5 articles series about the publication of an Electron application into the Mac AppStore, Fenêtre.

Fenêtre, fənɛtʁ, lets you better multitask on your mac. It enables a picture-in-picture mode for any website/web-app, image, video or flat file.

You can find the free version and the paid version on the Mac App-Store.

Pain & tears

Development usually comes with some pain, this is not new, but I’ll try to cover what was particularly hard to fix and/or to find a solution for.

Custom schemes

I wanted to use the custom scheme fenetre:// to open links from the browser into the app. It seemed so easy following Electron’s and Apple’s documentation on the subject. And it worked flawlessly in development.

Once sand-boxed, it stopped working. And it wasn’t an easy found bug, since it took 3 fully published and reviewed versions to figure this one out.

You could accurately follow my descent into the abyss through the @FenetreApp twitter feed.

But, eventually… in the end.

Instead of using a custom scheme, I had to run a server in the app, on a specific port. Then, the browser extension would call a route on this server to open the URL, passed as argument, into the app 🤮.

And I hate this so much.

DRM Content

When delivering content from the web, especially videos, you’ll be hit in the face with DRM. Netflix, for example, won’t let you play videos anywhere you like. You need a decoding plugin, called Widevine. It’s already embedded in your day-to-day browser, but when you’re using Chromium (Electron’s core) you’ll need to get it yourself.

The best way is to look for the Chromium’s major version your current Electron uses via process.versions in the renderer process. Then download the same version of Chrome and go spelunking into the .app file.

At the time of this writing, it can be found here:

Google Chrome.app/Contents/Versions/[version]/Google Chrome Framework.framework/Versions/A/Libraries/WidevineCdm/

Finally, activate it in your app, as early as you can, before app.on(‘ready’):

You can do the same with the PepperFlash plugin, to read flash content.

Important notes:

  • to be updated alongside Electron.
  • to be manually copied into your package.
  • to be referenced as an absolute path.

The French tartine de caca

Since I’m French, I wanted something that sounded French. That’s where this ê came in, busting everything I did.

Fenêtre was a fun name, pronounced fənɛtʁ or Fonaytre, it means window in French, so it was very relevant to the project and it sounded putain de French. But nothing prepared me to how painful it would be to use a non-ASCII character in today's internet. I already knew it was stupid, but not that stupid.

  • APFS vs HFS+

Some time during the development, I decided to upgrade my machine to High Sierra, what a mistake that was.

The file system changed from HFS+ to APFS, and now, the system doesn’t normalize filenames like it used to. So if you have non-ASCII characters in your filenames, you might be fucked. I could not sign my app with codesign through electron-osx-sign for a few days before finding a solution.

The solution I’ve found, with the help of Zhuo Lu, was to get the name from the Finder and copy the special character from there to use it where needed in the code. Simply because I'm not that well versed in normalization matters, it was an easy enough way of fixing this annoyance once and for all.

  • Domain Name

Internationalized domain name are around for some time now. You’d think it should be well supported all around the internet… BOOM, wake up, it’s not.

First, in most forms where you have to enter a domain name, you won’t be able to use the special form fenêt.re, it will be rejected by the validation, instead you'll have to use the xn--fent-ipa.re form. So, developers, please update your validations so I can submit my website in its best form.

Second, now that it passes the form validation, it will be displayed either badly, without the special char like fent.re, or simply will be swapped back to the xn--fent-ipa.re form.

Third, it won’t always be recognized to fetch open-graph data and you might not get this fancy card with your website’s name/description/visual.

Don’t think it’s just small, underground platforms that don’t support it yet. It happened on ProductHunt, Google Chrome WebStore, CloudFront, Twitter, Facebook, Slack, to name a few and it really doesn’t help the internationalization of domain names.

  • Keyboards

This one is just minimal, and nothing can be done for this, I think. But some keyboards make it very difficult to type special characters, especially the US one. That’s why I also bought the getfenet.re domain.

Small tips on how to type special characters on a US International — PC layout:

  • ' then e = é
  • ` then a = à
  • Shift + 6 then e = ê
  • " then i = ï
  • ' then c = ç

Of course, you can combine the accent with many other letters.

Clipboard Watch

There is no event for the clipboard in Electron (Chromium), so you’ll need to watch it yourself. And if you’re using a setInterval for this, you’ll see it slowly dying with your inactive app.

That’s where powerSaveBlocker enters.

Destroyed BrowserWindow

When manipulating or doing stuff with an opened BrowserWindow, be very careful that it’s still alive, especially if it’s asynchronous.

Or you’ll get hit by an exception.

Use this in every asynchronous piece of code.

Transparent Windows

I wanted to implement a see-through feature, being able to keep the window in front, but the cursor would cut through it to reveal what’s behind. And let the user click through it as well.

It was even easier than what I first thought (or I was just being an idiot), it’s actually just a combination of BrowserWindow's configurations and some CSS sorcery 🧙‍️:

Main and Renderer process code for see-through feature.

Using the app as a MacOS service

In my journey to make this app the most deeply integrated into the OS as possible, I wanted to have it registered as a MacOS service.

Unfortunately, Electron’s team doesn’t find it important enough to put it in the core (yet?).

Which is a shame, or maybe, just not enough people care about it yet.

Next step will be to implement a native Node module I guess.

Reducing package size

When shipping Electron with your app, you’re getting a pretty huge deal of a package. Electron alone will add ~117MB to your package 🏋️‍♀️. So the more you’ll remove, the better.

  • Webpack

A good way to have a smaller sized bundle would be to have a build system. I’ve chosen Webpack, because I’m familiar with it. But any other would have worked of course. Grunt, Gulp or any basic concatenation of files (if you’re that barbaric)…

Webpack lets you target both electron-main and electron-renderer. This way, with only one webpack.config.js you can output both your main process and your renderer process.

Going deeper with webpack, you can declare globals thanks to the DefinePlugin built in.

And, if you need to use absolute paths from within your app's package using node's path (for plugins for example), you should deactivate webpack's dirname mock.

Here’s a simplified version of my webpack’s configuration:

Snippet of my webpack config. Showing dual output, _dirname obfuscation and globals definition.

Then run webpack --env.IS_PRO --env.IS_PROD --env.IS_PACKAGED depending on which build you need to create.

Having those globals helped considerably keeping a single codebase with different codepaths:

  • IS_PACKAGED : helped with the declaration of absolute paths. For plugins for example.
  • IS_PROD: helped with adding debug points and debugger only in development.
  • IS_PRO: helped with obfuscating pro features.

Closing tip. Register all your dependencies as a devDependency will help with the packaging. Using electron-packager it will completely discard your node_modules folder and only keep your bundled JavaScript when packaging your app, reducing the size considerably.

  • Languages
Without cleaning, you’ll end up having all languages listed on your app’s page.

Electron adds a .lproj folder for each supported language, for reasons. It will clutter your application’s page on the Mac AppStore and will communicate wrong information about your app being internationalized in all these languages.

You can remove them yourself after the packaging of your app. To only keep the ones you support:

Icons

When you iterate on your designs, you might need to update your icons quite a lot. And generating those can be a pain, since you need many size and format. Especially this icon.icns for which many apps can ask up to 5$ to generate.

To ease this process, I’ve used this script coming from this awesome SO answer:

Here’s the 5$ script.

Basically, just use it as ./icons.sh <input_file> <output_folder>, it is important to note that your input file must be at least 1024px in both directions.

If you need to upscale it to a 1024px square, you can use ImageMagick:

Unsupported videos

Chromium only support a small set of video format. Mostly mp4 and its derivatives. So if a user wants to play an .avi video, it won't work, because it doesn't work in Chromium… bummer.

Since I’m just using a basic <video> tag to load all local videos, I'm stuck with this. Except… that's my app, and I can do whatever the fuck I want, if I want to support more video types, I will, try and stop me.

Fortunately for us, we can listen to errors on the video, and even luckier for us, we can target missing support errors:

From there, in Fenêtre, I’m sending a ping back to the main process saying that I can’t support this video type. The local server will create a new route for this video file and decode it on the fly using fluent-ffmpeg and stream it back to the renderer process:

Finally, simply update your <video>'s src attribute with the newly created route.

The only down side is that you need to ship ffmpeg with your app. And note that you have to compile it yourself with the --disable-securetransport flag, otherwise it will be rejected by Apple since it's using the Security API that isn't available while sand-boxed.

I was stuck at this point for a really long time, since I couldn’t compile a static executable of ffmpeg.

But the issue was that OSX kept dynamic libraries in /usr/local/bin which all take precedence over everything else. So even if you try to compile your ffmpeg statically, it won't work with these libraries on the way as they will be linked to your executable.

So you have to move all those /usr/local/bin/*.dylib somewhere else, compile the static executable, and TADAAaa… the build will work in the sandbox.

--

--