Trying Bun for Automation Tools (Part of Daily Work)

Dimas
9 min readJun 4, 2024

--

Bun logo

Node.js has provided a strong reason to stick with one programming language, JavaScript. As a Software Engineer (particularly a Web Developer), I feel that (almost) all needs can be well-handled using JavaScript, except when the focus is on speed, which I rarely pay attention to. The performance of applications made with Node.js isn’t bad, but it’s not the best either.

JavaScript (and its ecosystem) has developed very rapidly. I’m not FOMO, but I will try new things if they are crucial and impactful for my work. Bun is one of the tools I must try because since version 1.1, Bun brings many enticing features and offers more modern API usage.

In this article, I will try Bun to create an automation tool that simulates one of my daily tasks. Is Bun better than Node.js? Should I switch to Bun? Hmm.

Overview of the Automation Application Created

I am an instructor who often evaluates applications (mostly back-end applications) made by Dicoding students. Of course, it’s very tiring to manually check students’ assignments, so to make the assessment process more effective, I created an application to automatically evaluate projects.

In this case study, I want to create an application that can evaluate Node.js projects with criteria roughly as follows:

  • Has package.json.
  • Has a main.js file.
  • The main.js file contains the student's id in the form of a comment.
  • The main.js file is an HTTP server application and must respond with Content-Type HTML.
  • The HTML response from the HTTP server must contain an h1 element with the student's id as content.
  • The port used must be 5000.

The above criteria are quite challenging to create automation to evaluate them. At least I have to play with the filesystem API, create child processes, and interact with the HTTP server to check if the Node.js project has been implemented correctly. It’s interesting to find out if using Bun is suitable for handling this case.

In short, I have finished creating the automation application and it can be accessed in this repository.

https://github.com/dimasmds/bun-sample-automation

From the creation of this application, what are the pros and cons of using Bun? Let’s discuss the pros first!

Pro-1: Native TypeScript Support

Bun supports TypeScript natively. This allows us to write TypeScript code without needing extra packages or configurations at all. We can even write TypeScript code in the REPL mode (bun repl) provided. This makes me use TypeScript by default for both large and small projects because it is truly effortless.

We can also write TypeScript configurations if needed. In the automation application I made, I still wrote configurations to adjust a few things.

{
"compilerOptions": {
"esModuleInterop": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "NodeNext"
}
}

Pro-2: Offers Simple API

Bun offers a simpler and more modern-looking API. In the automation application, I dealt a lot with the file system. For example, when I needed to read a JSON file, Bun handled it in a more elegant way.

export async function readProjectConfig(projectPath: string): Promise<ProjectConfig> {
const filepath = join(projectPath, 'project-config.json');
return await Bun.file(filepath).json();
}

I like how Bun returns a BunFile — which is an extension of Blob — and we can convert it to JSON without needing JSON.parse.

Besides offering a better API for the file system, in Bun 1.1, we can already use Bun Shell. This allows us to execute shell scripts in a much simpler way compared to Node.js. For example, when I run the bun install command, I just do it like this.

async function installDependencies(projectPath: string): Promise<void> {
await $`${Bun.which('bun')} install`
.cwd(projectPath)
.quiet();
}

Compared to when I use Node.js, I might write code like this.

async function installDependencies(projectPath: string): Promise<void> {
const bunPath = execSync('which bun', { encoding: 'utf-8' });
execSync(`${bunPath} install`, { cwd: projectPath, stdio: 'ignore', encoding: 'utf-8' });
}

Bun also adds simple but very useful utilities, for example, in the code above the use of the Bun.which function is very helpful for me to get the location of the Bun binary.

Pro-3: Provides Proper Test Runner

Jest or Vitest (depending on the modular system) has been my favorite test runner in Node.js. Although Node.js 20 already comes with a built-in Test Runner, I feel the API is too different from what I usually use, especially in the assertion process which still uses the assert module.

In Bun, I find the built-in test runner quite enjoyable to use. The code written from Jest or Vitest is the same, even almost replaceable. The test runner API is written in Mocha style and the assertion is written in Chai expect style. Additionally, the Bun test runner already provides code coverage features without needing any extra packages.

Here is an example of test code written with the Bun test runner.

import { describe, beforeEach, test, expect } from 'bun:test';
import { buildChecklist } from './checklists';
import { join } from 'node:path';
import { buildReport } from './reports';
import { readProjectConfig } from './utils';
import { Checklists } from './types';

function getSampleProjectPath(sample: string) {
return join(process.cwd(), 'samples', sample);
}

async function readReport(projectPath: string) {
const filename = join(projectPath, 'report.json');
const file = Bun.file(filename);

return file.json();
}

describe('reports', () => {
describe('buildReport', () => {
let checklists: Checklists;

beforeEach(() => {
checklists = buildChecklist();
});

test('should generate failed report correctly', async () => {
// Arrange
checklists.containPackageJson.completed = false;
checklists.containPackageJson.reason = 'we cannot find the package.json of your project';
const projectPath = getSampleProjectPath('with-project-config');
const projectConfig = await readProjectConfig(projectPath);

// Action
await buildReport(checklists, projectPath, projectConfig);

// Assert
const report = await readReport(projectPath);
expect(report.passed).toEqual(false);
expect(report.checklist.includes(checklists.containPackageJson.key)).toEqual(false);
});

test('should generate success report correctly', async () => {
// Arrange
const projectPath = getSampleProjectPath('with-project-config');
const projectConfig = await readProjectConfig(projectPath);

// Action
await buildReport(checklists, projectPath, projectConfig);

// Assert
const report = await readReport(projectPath);
expect(report.passed).toEqual(true);
});
});
});

Below is an example of a report generated by the Bun test runner when enabling the code coverage feature.

bun test v1.1.12 (43f0913c)

src/grader.test.ts:
✓ graders > main > should reject when not contain main.js [5.36ms]
✓ graders > main > should reject when not contain package.json [0.59ms]
✓ graders > main > should reject when root not showing html [0.48ms]
✓ graders > main > should reject when wrong port [7970.63ms]
✓ graders > main > should reject due no comment [560.05ms]
✓ graders > main > should reject wrong submitter id in comment [557.56ms]
✓ graders > main > should reject wrong submitter id in html [543.06ms]
✓ graders > main > should approve when project is not nested [554.21ms]
✓ graders > main > should approve when project with nested [591.83ms]
✓ graders > main > should approve when project using hapi [559.01ms]

src/checklists.test.ts:
✓ checklists > buildChecklist > should return checklist correctly [0.85ms]

src/utils.test.ts:
✓ utils > readProjectConfig > should throw error when project config is not found [0.34ms]
✓ utils > readProjectConfig > should return ProjectConfig correctly [0.43ms]
✓ utils > findFolderBaseOnFile > should return null when looked up file is not found [0.33ms]
✓ utils > findFolderBaseOnFile > should return folder path when looked up file and is found [0.30ms]
✓ utils > parseArgs > should throw error when argument not contain --path [0.22ms]
✓ utils > parseArgs > should use path value when argument not contain --report [0.12ms]
✓ utils > parseArgs > should return report and path correctly [0.06ms]

src/reports.test.ts:
✓ reports > buildReport > should generate failed report correctly [0.72ms]
✓ reports > buildReport > should generate success report correctly [0.38ms]

-------------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|---------|-------------------
All files | 100.00 | 99.74 |
src/checklists.ts | 100.00 | 100.00 |
src/grader.ts | 100.00 | 98.96 |
src/reports.ts | 100.00 | 100.00 |
src/utils.ts | 100.00 | 100.00 |
-------------------|---------|---------|-------------------

20 pass
0 fail
24 expect() calls
Ran 20 tests across 4 files. [11.42s]

Pro-4: Supporting Node.js API

Bun is still in its early stages, so the available API isn’t as extensive as Node.js. Thus, when there’s an operation that Bun’s API doesn’t support yet, we can confidently use the Node.js API to accomplish it. Many Node.js APIs have already been implemented by Bun. This is also why Bun can be considered a replacement for Node.js, whose code is widely used (making it difficult to replace).

Personally, I always try to write in Bun’s way first. If it’s not available, then I use Node.js methods. In this automation project, I still use the Node.js API, for instance, when I need to read a folder in the file system.

import { join } from 'node:path';
import { promises as fsPromises } from 'node:fs';

const { readdir } = fsPromises;

/* .. */

export async function findFolderBasedOnFile(folder: string, filename: string): Promise<string> {
const files = await readdir(folder);
const filteredFiles = files.filter((f) => f !== 'node_modules');

if (filteredFiles.includes(filename)) {
return folder;
}

return Promise.any(
filteredFiles.map((fileOrDir) => findFolderBasedOnFile(join(folder, fileOrDir), filename))
).catch(() => Promise.resolve(null));
}

As you can see, I still rely on the Node.js API for paths because it can still be used effectively.

Pro-5: Simple SEA Creation

Lastly (though there’s more), I love Bun because it allows us to create Single Executable Applications (SEA) very simply. SEA isn’t new in JavaScript, but so far, creating SEA in Node.js isn’t straightforward. Meanwhile, Bun abstracts the complicated process with just a single argument.

In this automation application I created, I utilized this feature. I could create an SEA with the following command.

bun build src/app.ts --target=bun --compile --outfile app

The result is an SEA ready to use without even installing the Bun runtime. The downside is that the binary can only run on the same OS as the build process and its large size (I assume the Bun runtime is bundled as well). 😆

➜ la | grep app
-rwxrwxrwx 1 dimas dimas 94M Jun 4 09:50 app

Con-1: Bugs?

The first drawback I experienced is the presence of bugs when using the Bun API to create a child process with Bun.spawn(). The child process created with Bun.spawn() is difficult to stop using the subProcess.kill() method. To address this, I had to revert to using the Node API, and the child process created by it can be stopped properly. Whether this is a bug or not, I haven't filed an issue yet.

import { spawn } from 'node:child_process';
import { join } from 'node:path';

function runApp(mainJsPath: string) {
const filepath = join(mainJsPath, 'main.js');
/**
* Bug?
* not using Bun.spawn because child hard to kill
*/
return spawn(Bun.which('bun'), ['run', filepath]);
}

Such bugs or issues might occur in other cases as well, given Bun’s early stage.

Con-2: Documentation

Another drawback is the lack of detailed information in the documentation. For example, I couldn’t get detailed information about methods in SubProcess. Much of the information still directs us to read the type definition.

However, I expected this due Bun’s early stage.

Con-3: IDE Support

WebStorm is my favorite IDE for creating JavaScript applications. As the default runtime, the experience of developing JavaScript applications with Bun isn’t as comfortable as with Node.js. This is because test runners and debugging tools in WebStorm can’t be used with the Bun runtime yet.

WebStorm itself doesn’t support Bun yet. However, from this issue, we can see that WebStorm will likely add support for Bun. 😁

Conclusion

After trying Bun to create an automation application, I found that Bun has great potential.

Bun’s advantages, such as faster performance and a more modern API, are very enticing. Using Bun for day-to-day applications shows that it can provide more efficient solutions in some cases. However, Bun’s early stage brings some drawbacks, such as bugs and incomplete documentation.

Additionally, the lack of optimal IDE support, like in WebStorm, is another consideration. Developing applications with Bun in WebStorm isn’t as smooth as using Node.js due to the lack of support for test runners and debugging tools, which I heavily rely on.

I’m optimistic about Bun’s development in the future. With continuous updates and improvements, Bun has the potential to become a strong alternative to Node.js. For now, I’ll use Bun for small-scale projects. If you enjoy experimenting and seek better performance, trying Bun can be an interesting experience.

--

--

No responses yet