TypeScript and Electron for desktop & web apps (e.g. SketchEl2)

ts-electron-1This is rare post that is not directly related to chemistry or informatics, even though the screenshot to the right suggests otherwise. Probably the most exciting trends in software is that the web runtime has finally matured into a development target that is somewhat on par with native options, and that is a huge breakthrough for people who want to code up their product just once. I’m going to describe some of the getting started issues & gotchas that I went through with getting SketchEl2 to work using Electron, TypeScript and Visual Studio Code.

People have been writing impressive web-based applications for quite some time, using clever JavaScript & CSS mixed with serverside technology, using familiar acronyms like AJAX, JSP, REST, and numerous others. Until fairly recently, though, it was quite rare to have a heavyweight application that ran entirely on the client. This was partly because of the nature of a web page: resources go from nothing to something on page opening, so it makes sense to share the work between client & server, and parcel up chunks of functionality. But it was also much to do with browser incompatibility (i.e. if you do something on the server, you can be sure of that part) and the general inadequacies of JavaScript as a language, and the HTML document object model framework. The framework problems are hard to summarise because there were so many of them, and any one of them may have seemed minor until it turned out to be the showstopper that forced you to start again with a whole new approach. The contemporary language problems are mainly caused by JavaScript being an un-typed dynamic language, which is great for scripting (hence the name, JavaScript). Scripting languages are numerous, and they tend to be great for writing short programs to solve small problems, and tend to be easy to learn. Software engineering languages are fewer, harder to learn, and have a lot more project overhead – but once your codebase starts getting large, that pays off by making it possible to keep the maintenance burden under control – or put another way, the difference between solid progress and paralysing frustration.

The JavaScript language has undergone a number of internal improvements up to the latest version (ES6) taking it beyond a language suitable only for single-file scripting, by adding proper classes, an import mechanism, consistent libraries, and numerous other tweaks. The biggest problem though – absence of types – is still only solvable with a wrapper language. Enter TypeScript, from Microsoft of all companies (remember them – the guys who broke the web for more than a decade with IE6?) Essentially the language captures its entire value proposition in its name: it adds compile-time typing to JavaScript. The value of this is immense; it removes most bugs, basically. And it also makes development a lot faster, because the IDE has the ability to provide information about what each object is, and what its properties are.

Speaking of IDEs, the other important piece is Visual Studio Code, also from Microsoft. It has almost nothing to do with its namesake: it’s a completely different codebase, open source, and is itself coded up in TypeScript. It is very lightweight, and really rather good, though it does suffer from some of the growing pains of fast development with an open source ethos (e.g. the balance between progress vs. regression testing is a different from a commercial release process that is beholden to paying customers). The bottom line is that building for the web runtime with TypeScript is a lot more like Java than it is like JavaScript. For example, consider a snippet of the primary window panel for the SketchEl2 web app (cf. GitHub):

ts-electron-2

There are some syntactical differences, but in general it is quite easy to transliterate between Java and TypeScript, commonly resulting in a line-for-line equivalence. Generally the same rules apply: primitive types, collection types, class objects and functions/lambdas have more or less the same characteristics in both languages, and they are both checked by the compiler while you’re coding, rather than after you execute.

One of the things that initially surprised me about Visual Studio Code is that it runs with the Electron framework, which is a mixture of Node.js and a stripped down version of the Chrome browser. It is essentially a repurposing of existing tools for the purpose of wrapping a JavaScript project into a desktop app that is a first class citizen: it can be deployed as a standalone executable; it has control over its window and icon; and it can access the local filesystem and perform several other desktop interaction tasks that are normally off limits to applications that are running in a browser.

Another very noteworthy detail about the web runtime: it’s actually getting rather fast. JavaScript has a major problem with performance optimisation (and that unfortunately includes languages that cross compile to it, like TypeScript) because types are not known ahead of time,  meaning that the final executable code only knows what it’s getting when it arrives, giving it no definitive opportunities to prepare. But even despite this limitation, busy algorithms running on the single allowed thread certainly feel like they get the job done on a very similar timescale to a natively compiled implementation, which is quite impressive.

Now getting to the main point: it is possible to use a single TypeScript codebase to create sophisticated apps that target regular web (served from a URL), desktop apps that are often as good as native, and likewise for mobile, deployed within a native app wrapper. Focusing on the desktop part, this is (as of December 2017) not exactly a painfree experience, because the Electron framework is still relatively new, and the various development permutations have not all been fleshed out in great detail. If you’re stuck on how to do something, it’s highly likely that you can’t find a quick answer on StackOverflow.

The example I’m going to describe is the SketchEl2 app (see GitHub), which is currently bare bones (most of the heavy lifting is done by the WebMolKit project – where the interactive molecule sketching is coded up – but that’s another story). I’m not going to describe installing any of the tools because that’s well covered by the internet; you’ll need npm (node package manager), TypeScript and Electron (via npm) and Visual Studio Code (from Microsoft).

The development directory should have several subdirectories in it:

app
dist
src
.vscode

The app directory contains the glue necessary for the Electron framework to wrap your code into a functioning desktop app. This includes the cross-compiled TypeScript output, static libraries, resources like images, and a bunch of rather ugly files that are needed to bootstrap several technologies that were not originally designed to work together. The latter category is currently one of the most awkward parts of getting results out of this new technology.

The dist directory is where the deliverable builds will go. There is one output bundle for each of the three major platforms, and once constructed, these will run as-is on Linux, Mac and Windows.

The src directory is where the TypeScript files go – all the production-quality, highly maintainable code that can be repurposed for a multitude of different platform types.

The .vscode directory contains the launching and debugging details for Visual Studio Code, which is key for making the development experience acceptable. More on that later.

For the benefit of Electron, there needs to be two files called package.json, one of them in the root directory (see GitHub) and the other in the app directory (see GitHub). These provide instructions for building the package, and for executing it once assembled, respectively. For the record, I have assembled these files largely by trial & error, so the examples for SketchEl2 may be under- or overspecified for any given purpose. They are at least sufficient for debugging and building distributable packages that can be executed independently on a different computer.

Since TypeScript is a cross compiled language, the workflow is to take all of the .ts files in the src subdirectory, and combine them into a single JavaScript file. In this example, that file is app/sketchel2.js. Note the destination folder: app is where all the deliverables go; the sketchel2.js file contains all of the TypeScript content, and this gets included directly into the web runtime, just as if the whole project had been coded up into a single gigantic JavaScript source file.

To establish this compilation step, a tsconfig.json file is required, to tell the TypeScript compiler what to do. This is quite a simple and concise format, and for SketchEl2 it is thus:

ts-electron-3

These settings are well documented, so no need to go into too much detail. The outFile is set to app/sketchel2.js, and the sources are defined as everything in the src directory, plus all of the files in the companion WebMolKit. The exclude section is a directive to tell the TypeScript compiler not to opportunistically scrape other files that it might dig out of the directory hierarchy (which seems like more of a bug than a feature).

With this established, the cross-compilation process can be accomplished from the commandline by typing “tsc”, or more conveniently from Visual Studio Code with Ctrl-Shift-B (or Command-Shift-B) to execute the default build process.

As soon as you get started with Visual Studio Code, you will end up with a .vscode subdirectory. Because it is naturally paired with TypeScript, some of the settings don’t need any real attention. There should be two files in this directory: tasks.json and launch.json. The former has defaults that are unlikely to need modification, but the latter needs to be configured to work with Electron:

ts-electron-4

The launch task is the most important one. The program parameter specifies the entrypoint, which is app/main.js, which basically bootstraps everything. The runtimeExecutable is set to use Electron to launch the project.

One of the confusing pieces, which may be resolved by now, is the protocol by which Visual Studio Code communicates with Electron to obtain logging information, stop the app, etc. At a certain point, the various components were defaulting to different protocol versions, which broke the whole development chain. Setting the protocol to legacy fixed this, but by the time you read this article, this particular glitch is quite likely to have been sorted out. This is just one example of the frequent pitfalls of using a heterogeneous collection of open source frameworks, each of them is a fast moving target.

Once this is setup, making changes and running the project can be achieved from Visual Studio Code by pressing Control-Shift-B to do the compilation, and then F5 to execute the thing. If everything is working, the result will be a window with the functioning desktop webapp:

ts-electron-1

But that’s getting slightly ahead – there is still the app directory which contains all of the glue. The important things that go in here are:

  • the main.js file that is loaded first, and sets everything up
  • the package.json file (for Electron’s deployment benefit)
  • index.html: the web page that is loaded into the browser
  • various .js and .css files (including the cross-compiled TypeScript)
  • other resources such as images

The main.js file is an interesting beast, and understanding it requires an introduction to the two threads that are part of a baseline Electron process (described in more detail here). The main.js file is the Main thread, and its responsibility is to create a new BrowserWindow, and tell it what to put in there. That browser window then goes on to create a Renderer thread, which behaves almost indistinguishably from a normal web application inside a normal browser page loaded up from the web.

In the Main thread (handled by main.js) the codebase has control over some high level desktop customisation features: it can open new windows, control their size, add menus, and various other things. The Renderer thread on the other hand is completely confined to its own window, and by default has the same sandboxed characteristics as a normal web page – except that it has the option of communicating with the main thread, and also importing some special Node.js packages for privileged access (such as for local files).

At this point, we have to consider the scope of the application: does it need to be able to be used interchangeably as desktop vs. web vs. mobile app? If so, then the Electron-specific functionality should be confined to specific modules, which need to be avoided for the non-Electron parts. For the SketchEl2 example, this is not a major concern because the bulk of the functionality is already abstracted out into the WebMolKit library, which is designed for a pure web environment, so the project itself provides that modular separation.

The interplay from Main-to-Renderer thread goes something like this:

  • the Main thread (running main.js) opens a BrowserWindow
  • the BrowserWindow is instructed to load index.html
  • index.html includes all the resource files, then immediately calls the runSketchEl function
  • runSketchEl (which is in the Renderer thread) wakes up and tries to figure out where it is…

The waking up process involves importing Electron-specific libraries, and starts by unpacking the command line parameters. The implementation is shown in startup.ts (see GitHub), which is part of the TypeScript section. Things get a little bit more interesting due to the fact that like most desktop apps, SketchEl2 needs to open an extra window from time to time, so there is also a function called openNewWindow, which accesses the appropriate Electron library to recirculate the process, and pass several parameters that are needed to create an additional Renderer thread, with its own frame.

Menu handling is rather awkward, since the menus are defined and handled in the Main thread, whereas the code that needs to have control over them lives in the Renderer thread. If the menu is constant, then this is straightforward enough: menus can be generated in the Main thread and send actions to the Renderer thread:

ts-electron-5

The sendCommand function is rather kludgey, but it works. The Renderer thread has to install a custom event receiver, which can be done early on:

ts-electron-6

So far the configuration steps described should be largely sufficient to execute the project from within Visual Studio Code, or by typing electron app to launch it from the command line. Creating a self-contained package to distribute to someone who does not have all the development tools installed is accomplished by another npm package called electron-packager, and has a number of parameters that are specific to the desired target. In the SketchEl2 GitHub project, these are expressed in the form of three shell scripts: linbuild.sh, macbuild.sh and winbuild.sh. The Linux and macOS versions have been tested, and they include a couple of kludges needed to workaround temporary limitations, but hopefully these will get trimmed down as the platform tools mature.

This overview covers many of the unfamiliar project configuration and glue procedures that are necessary to get up and running using TypeScript, Visual Studio Code and Electron for desktop web development. The rest of the codebase is mostly the same as regular web programming. There are some other key points at which it is necessary to invoke Electron-specific functionality, such as reading/writing from files on the local filesystem, but this is quite well documented: there are plenty of examples to draw from.

All things being considered, this approach to building apps seems to be a big part of future software development. For better for worse, JavaScript is the only runtime that is available on every platform, so if you want to code for regular web, all 3 major desktop operating systems, both mobile platforms, as well as the various company sponsored appstores, then this is your best bet for maintaining a single codebase. The good news is that once you figure out how to work around the rough edges, it’s actually quite nice. And as time goes by, it will no doubt become much more streamlined, and there will be a lot more tutorials and Q&A resources for solving common problems.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s