A lot of people claim that you can build anything with JavaScript. There are countless frameworks for web development, from backend tools such as Node, to mobile-oriented libraries like React. You can even build desktop applications using Electron. But is it possible to perform desktop automation with JavaScript?
It sure is!
Using node add-ons, it’s possible to use OS APIs to simulate inputs or retrieve screen content.
However, building and shipping a ready-to-use desktop automation framework for three major platforms requires an elaborate development setup. In this tutorial, I’ll show you how I organize my work on nut.js. We’ll touch upon the following topics.
Let’s get started!
When it comes to tooling, you should also address repository setups. While it is possible to do all development on a single branch, I’m a huge fan of GitFlow. I’m employed full-time in software development, so the amount of time I’m able to spend on my personal projects depends heavily on my workload. Whenever I have some energy left in the evening or when I find time for coding on a weekend, I’ll pick up some old task or start a new one if I feel like it. Following the GitFlow, all development happens on feature branches, so no matter what I’m currently working on, I’m always able to seamlessly switch to another task. Develop and master branches only change when:
This is a huge benefit for contributors since they will always be able to check out either master or develop to get going. I don’t need to tell you how frustrating it is when you clone a project only to realize it’s in an inconsistent state and won’t build.
This part may be a little opinionated, but TypeScript turned into my default when starting new projects. Its type system is really helpful, yet it’s unintrusive — a great combination for everyday use.
While not required, I tend to adjust the default tsconfig.json
.
{ "compilerOptions": { "outDir": "./dist", "declaration": true, "declarationMap": true, "sourceMap": true, "strict": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, … }, “include”: [ “lib/**/*.ts”, “index.ts” ], “exclude”: [ “node_modules” ] }
The TypeScript config is accompanied by the following scripts.
… “scripts”: { “clean”: “rm -rf dist”, “compile”: “npm run clean && tsc -p .”, “prepublishOnly”: “npm run compile”, … }, …
The end result is a setup that fits my needs perfectly. Simple, yet powerful! đź’Ş
When developing a framework, you might be tempted to focus solely on features, but if you want people to use your work, you’ll have to provide documentation and samples. Fortunately, there are multiple solutions available that allow you to host your samples and/or documentation.
nut.js uses TypeDoc to automatically generate API documentation, including short examples from doc comments:
/** * {@link type} types a sequence of strings or single {@link Key}s via system keyboard * @example * ```typescript * await keyboard.type(Key.A, Key.S, Key.D, Key.F); * await keyboard.type("Hello, world!"); * ``` * @param input Sequence of strings or {@link Key}s to type */
Typedoc is configured to output generated documentation to a docs
folder in our repository.
"scripts": { ... "typedoc": "typedoc --options ./typedoc.js --out ./docs lib/" },
GitHub Pages is able to use this docs
folder on the master branch as a source. With just a few clicks, the documentation is live at https://nut-tree.github.io/nut.js/.
For samples, I’m using a separate repository at nut-tree/trailmix. It’s a Lerna monorepo that contains packages for every (current) main aspect of nut.js. Following the monorepo approach makes it easy to update and test against a new release of nut.js since dependencies are managed at root level, which helps keeping samples up to date. And since all samples are designed to be Jest tests, it also enables you to detect errors early on.
Testing is the most crucial part of nut.js development. We need to make sure nut.js builds and runs on multiple platforms. The only feasible way to do that is to rely heavily on automation. And the only way to reliably automate processes is to have a proper test set up to verify our system’s behaviour.
The testing framework I’m most comfortable with is Jest. It has all the features I’m looking for in a testing framework and plays along nicely with TypeScript using the ts-jest preset.
module.exports = { collectCoverageFrom: [ "index.ts", "lib/**/*.ts", "!lib/**/*.spec.ts", "!<rootDir>/node_modules/", ], preset: "ts-jest", testEnvironment: "node", testMatch: process.env.E2E_TEST ? ["**/__tests__/?(e2e)/**/*.[jt]s?(x)", "**/?(*.)?(e2e.)+(spec|test).[jt]s?(x)"] : ["**/__tests__/!(e2e)/**/*.[jt]s?(x)", "**/!(*.e2e.*)+(spec|test).[jt]s?(x)"], testPathIgnorePatterns: [ "/node_modules/", "/dist/", ], };
This single config file enables TypeScript support for Jest, allows me to collect coverage for files I’m interested in, and separates two kinds of tests. nut.js includes unit tests, which can be run every time, and E2E tests, which are meant to be run in a Docker container featuring a certain UI. E2E tests will only be included in a test run if the E2E_TEST
environment variable is set.
Tests are simply distinguished by their file name. feature.class.spec.ts
contains unit tests for a feature, while feature.class.e2e.spec.ts
contains a full E2E test, which depends on a fixed UI.
On CI, all tests are executed in a Docker container to run all available tests.
nut.js currently uses two CI systems:
In total, 16 CI jobs run tests against five supported node versions (10, 11, 12, 13, 14) on three supported platforms (Windows, macOS, and Linux). The 16th job publishes snapshot and stable releases.
On Travis, nut.js imposes a three-stage setup. The first stage builds and tests against the current node LTS release. If sucessful, SonarCloud is used for static code analysis.
In the next stage, we’ll test against the remaining combinations of platform and node versions.
We’ll run a final deploy
stage in case of a tagged commit or a build on the develop
branch. If we push a new tag, a stable release is published under the default @latest
tag. Builds for the develop
branch will do a snapshot release under the @next
tag. So whenever a feature is finished and merged into develop
, a new snapshot release is published. Snapshots are great for fast feedback since users don’t have to wait for the next stable release to test new features.
A nice benefit of having a proper test and CI setup is branch protection. We want to keep our code clean and we don’t want any surprises after merging a pull request. With branch protection enabled, we can enforce certain status checks to pass before merging a PR.
Sonarcloud and Travis/Appveyor are among the available status checks, so if the pull request build fails or our quality gate is missed, a PR won’t be mergeable (unless forced by a repo owner or admin).
You might be thinking to yourself that these settings are only relevant when collaborating with others, but solo developers can benefit from these features too.
The setup I just walked you through has evolved over time. It allows me to maintain nut.js with confidence. Refactorings are covered by tests, releases are automated, and documentation is version controlled and automated.
Every developer has their own setup, but if you’re just getting acquainted with nut.js or looking for a way to get better organized, I hope this walkthrough will give you some inspiration and a solid foundation.
If you’re already experienced in nut.js, what does your setup look like?
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.