This article is part of a 5 articles series about the publication of an Electron application into the Mac AppStore, Fenêtre.
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.
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.
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
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
- 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
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.
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
Small tips on how to type special characters on a US International — PC layout:
Shift + 6then
Of course, you can combine the accent with many other letters.
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.
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.
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 🧙️:
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.
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)…
Going deeper with webpack, you can declare globals thanks to the
DefinePlugin built in.
Here’s a simplified version of my webpack’s configuration:
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
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:
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:
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:
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
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
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.
See? It wasn't all that bad, you're still here, up and reading. How about we Ship it now?