Electron spins: a special case of Chromium mods
Challenges of modifying Chromium, compounded
Chromium is a powerful project, implementing functionality that's critical to the web and web-related technologies. Perhaps the greatest credit to its quality and completeness is the fact that it's become an almost-de facto standard. So it's no surprise that other projects would want to inherit much of that codebase.
You can't upstream everything you'd want to do with it, as your needs are not necessarily the needs of everyone everywhere. Chromium would be a chaotic mess if everyone upstreamed their Chromium mods.
Chromium is also very active, with code changes happening nearly constantly, and some of them are quite important. So forking Chromium isn't attractive, since you'd want to benefit from the rest of the community.
This leaves a number of projects in the sometimes-hairy situation of being “downstream,” defining their code as a series of patches, file additions, and/or file deletions on top of Chromium's codebase. And of course this means a fair amount of work needs to be done to accommodate upstream changes. Electron is in this camp, and as a very active project in its own right, they do an excellent job of it.
As a testament to how effective and useful Electron is, just look at the applications using it. Millions of people are starting up Electron every day without even realizing it. Just consider these examples:
- Messaging apps’ desktop versions, like:
- Matrix
- Signal
- Slack
- Discord
- Developer tools like:
- Visual Studio Code
- Postman
- Github Desktop
- Hyper
- Most user-facing app categories, including:
- Loom
- Asana
- Twitch
- WordPress
So what if someone wanted a slightly modified Electron? Perhaps their application could use some feature that is too specific to be in-scope for the upstream Electron project. Should they create their own spin on Electron? They need to ask themselves a very serious question: are they really going to maintain a project that is a set of patches on top of a rapidly-moving set of patches on top of a rapidly-moving project? How difficult is this? Could we make it easier?
A proposal for sane native extensibility
What if, instead of upstreaming all our niche changes, we could upstream just some extension points to make this process easier?
Adding files wholesale is considerably easier than patching upstream files. We get to skip the merge conflicts, and it might be easier to test them in isolation. So for build-time extension, a build-script could change its behavior based on the presence of a particular file path. That way if you don't change anything, you get the normal build behavior, but if you do make changes, Electron hooks into your code without you changing anything.
On the downstream side, this means you would:
- Add a directory for your module, complete with its own BUILD.gn
- Have a source header with a predefined name that implements a predefined function signature for each hook you want to use
On the upstream (Electron) side, this means:
- The build script notes the presence of your BUILD.gn and adds a dependency so it can call into your code
- For each predefined header file name that exists, the build script flips a build flag
- In the appropriate part of the code, if the compile-time flag is set, it includes your header and compiles a call into your function
Some extension points
Presumably this proposed approach would/could change in the future. I haven't imagined every conceivable use case. But I did a few demonstrations of the technique, and for them I found these sufficient:
- Initializing (if needed) immediately after
PreMainMessageLoopRun
- Registering additional preferences
- Creating URL Loader Request interceptor(s)
- Creating sub-resource URL loader factories
- Returning a list of additional protocol schemes to consider “handled”
Example demos
To whet your appetite, here are a few demonstrations I cooked up.
Rendering markdown
This spin on Electron uses a URL Loader Request interceptor that kicks in when a user navigates (e.g., clicks a link to) a raw Markdown document. It uses CMark to convert it to HTML, and renders that. One can also substitute `cmark-gfm` if one prefers GitHub-flavored markdown.
Replacing resources with static data
This Electron tweak uses a sub-resources URL Loader to intercept requests from `<img>`, fetch, etc. whose URLs end in ".jpg" and then respond to them with a chunk of static data. Obviously this is a silly toy example, but replacing HTTP requests with static resources could have uses.
Custom protocols
This Electron spin uses all five of the above hooks and supports `ipfs://` and `ipns://` schemes.
Bottom line
If the Electron project adopted this approach to extensibility, it could open up a world where installable JavaScript apps can use their leading platform, but also have access to features which aren't suitable to be included in a global-scoped project like Chromium or Electron — features that are too new, too non-standard, too niche, or too integrated with the OS. This also applies to features with implications that are tricky in contexts outside of their app. And they can do this with only modest ongoing maintenance cost.
It also becomes quite reasonable to isolate just the feature and its modification into a project that a few JS apps could share simply by changing `electron-prebuilt` to `my-electron-prebuilt` in their build system.
Is your project already using a modified Electron? What use-case-specific functionality would you like your app to have? What do you think about this approach? Don't be afraid to join the conversation on GitHub or to reach out to me directly. I'd love to hear your thoughts!