Building a template with Bun

JonathanJonathan
Jul 09, 2024ยท
5 min read

Hihi! Lately i've been using a lot of bun in my side projects, why? because it cames with a lot of things that I use already solved

You can check the repository i'm using for example here

  • Typescript? โœ…
  • Single Ejecutable File? โœ…
  • Multiple Architectures? โœ…
  • Test runner? โœ…
  • Coverage? โœ…
  • Pipelines? โœ…

With all of these already we can make sure to build a suitable product, giving quality and speed, since all of these things are really easy to setup, let's dive in.

Typing ๐ŸŸฆ

We have two ways, use typescript or jsdocs, since I'm already confortable with TS and I know what i'm doing I'll always use JS instead but never lost the habit of adding types thats when jsdocs comes to save the day.

Here is an example of a function using jsdocs in javascript

/**
 * @description Notify the user that the message is too long
 * @param { Object } options - Options object
 * @param { string } options.notification - Message to notify the user
 * @param { string } options.std - Content to be evaluated
 * @param { number } options.maxLength - Max length of the message
 * @param { import("#types").sendMessage } options.fn - Function to send the message
 * @returns { undefined }
 */
function sendMessageIfLong(options) {
  const { notification, std, maxLength, fn } = options;
  const notificationMessage = notification + 'sending in parts...';
  const delay = 2000;
  const isStdValid = std !== null;

  if (isStdValid) {
    const isLargeMessage = std.length > maxLength;
    if (isLargeMessage) {
      fn(notificationMessage || noOutputMessage + 'sendMessageIfLong');
      sleep(delay);
      const splitMessage = splitString(std, maxLength);
      splitMessage.forEach(part => fn(part));
    }
  }
}

Did I mention that we can create documentation with the jsdocs typing? with esdocs we can create something like this! https://bun-template.jonathan.com.ar/

Binary ๐Ÿ”ข

But why binarys tho? well not everything is a web!! (you could use it with web anyways if you are lazy and run it with ./binary & if is for a sample project) but now being serious, there are some projects where a binary comes handy and I love to have a easy way to do it, or to not have to install nothing more.

With something like this in our package.json "build": "bun build ./src/index.js --compile --outfile lib/app_name" we are up to go and whats better than having binarys at our disposal?

Architecture ๐Ÿš

Having multiple architecture to built in! Yes I love this as a fan of ARM64 or ARMV7 I always had to do work arounds to build docker images and transform apps (mostly for ARMV7 not for ARM64) but anyways, this is so cool. You should give it a try

bun build --compile --target=bun-linux-arm64 ./index.ts --outfile myapp

Testing ๐Ÿงช

Now here we have two things, the hability to do unit testing with some sort of jest (or also import jest), integration test if we install supertest and add coverage if we configure our bunfig.toml

To add coverage to our projects is something like this

[test]

coverage = true
coverageThreshold = { line = 0.7, function = 0.5 }

It is true that at the time i'm writing this bun do not have any sort of test reporter, which is really needed on enterprise level to create junit files for example and add graphics to platforms like github or azure devops for example. You can follow this thread to get in touch about it here and contribute to make it real

By the way tests are extremely fast!

1

Here are some of the test that are running

import { SampleApp } from "src";
import { describe, it, expect, beforeAll } from 'bun:test';

describe("SampleApp Class", () => {

    /**
     * @type {SampleApp}
     */
    let sampleApp;

    beforeAll(() => {
        sampleApp = new SampleApp();
    });

    it("should be an instance of SampleApp", () => {
        expect(sampleApp).toBeInstanceOf(SampleApp);
    });

    it("should have a method called 'sampleMethod'", () => {
        expect(sampleApp.sampleMethod).toBeInstanceOf(Function);
        expect(sampleApp.sampleMethod()).toBe("Hello World!");
    });
});

Yes this second one is running against a deployed database and still is running like if it was mocked, hell so fast haha

import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { LibsqlDialect } from '@libsql/kysely-libsql';
import { Kysely } from 'kysely';

import { db } from '#db';
import { config } from 'src/config/config';
import { createTable, dropTable } from './utils/sql';

describe('db file', () => {

    /** @type { import("kysely").Kysely<any> } */
    let newDb;

    beforeAll(async () => {
        newDb = new Kysely({
            dialect: new LibsqlDialect({
                url: config.db.url,
                authToken: config.db.authToken
            })
        });
    });

    it('should be an instance of Kysely', () => {
        expect(db).toBeInstanceOf(Kysely);
    });

    it('should be an instance of Kysely', () => {
        expect(newDb).toBeInstanceOf(Kysely);
    });

    it('should create a table with the name of users', async () => {
        await createTable(newDb, 'sampleUsers');
        const usersTable = await newDb.selectFrom('sampleUsers').selectAll().execute();
        expect(usersTable).toEqual([]);
    });

    afterAll(async () => {
        await dropTable(newDb, 'sampleUsers');
    });

});

Pipelines ๐Ÿšง

Bun already made not only a dockerimage but an action in github, which comes handly to generate our documention, do testing and more things!

Here is a complete example

name: build, test and run

on:
  workflow_dispatch:
  push:
    branches: [ "develop", "master" ]
  pull_request:
    branches: [ "develop", "master" ]

jobs:
  build:
    name: build and test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop'
    steps:

      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: set environment variables
        run: |
          touch .env
          echo "DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}" >> .env
          echo "TURSO_URL=${{ secrets.TURSO_URL }}" >> .env
          echo "TURSO_TOKEN=${{ secrets.TURSO_TOKEN }}" >> .env

      - name: install dependencies, build and test
        run: |
          bun install
          bun run test
          bun run build:arm

      - uses: actions/upload-artifact@v2
        with:
          name: executor
          path: lib/executor_arm64
          if-no-files-found: error

  run:
    name: run
    runs-on: self-hosted
    if: github.ref == 'refs/heads/master'
    needs: build
    steps:
      - uses: actions/download-artifact@v2
        with:
          name: executor

      - name: move to $HOME/apps
        run: mv executor_arm64 $HOME/apps/executor_arm64

      - name: run executor
        run: |
          chmod +x $HOME/apps/executor_arm64
          $HOME/apps/executor_arm64 &

Conclusion ๐Ÿ

Bun is awesome and it makes javascript fun outside the browser without shooting yourself in the leg.

22
Subscribe to my newsletter

Read articles from Jonathan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jonathan
Jonathan