homeprojectstemplates
 
   
🔍

Extending outdated TypeScript package declarations

November 2, 2023

Extending package declarations locally is one of the options for outdated package typings.

Create a declaration file .d.ts (e.g., handlebars.d.ts), and put it inside the src directories.

Find the exact name of the package namespace inside the node_modules types file (e.g. handlebars/types/index.d.ts).

Extend the found namespace with your needed properties, like classes, functions, etc.

// handlebars.d.ts
declare namespace Handlebars {
export class JavaScriptCompiler {
public nameLookup(parent: string, name: string, type: string): string | string[];
}
export function doSomething(name: string): void;
// ...
}

Bun overview

September 11, 2023

Bun is a JavaScript runtime environment that extends JavaScriptCore engine built for Safari. Bun is designed for speed and developer experience (DX), which includes many features out of the box.

Some of the features include

  • Built-in bundler
  • Built-in test runner
  • Node.js-compatible package manager, compatible with existing npm packages
  • Compatibility with Node.js native modules like fs, path, etc.
  • TypeScript support, run TypeScript files with no extra configuration
  • Built-in watch mode
  • Support for both ES modules and CommonJS modules, both can be used in the same file
  • Native SQLite driver

Installation

Let's start by installing it with the following command

curl -fsSL https://bun.sh/install | bash

Update the current version with bun upgrade command and check the current version with bun --version command.

Run bun --help to see what CLI options are available.

Initialize empty project via the bun init command

The init command will bootstrap the "hello world" example with configured package.json, binary lock file (bun.lockb), and tsconfig.

Bundler

Bundler can be used via CLI command (bun build) or Bun.build() API

await Bun.build({
entrypoints: ['./index.ts'],
outdir: './build',
});

Below is the example for CLI command usage. Run bun build --help to see all of the available options

bun build --target=bun ./index.ts --outdir=./build

Package manager

Install packages from package.json via the bun install command.

Install additional npm packages via the bun add command (e.g., bun add zod). To install dev dependencies, run bun add with --dev option (e.g., bun add zod --dev)

Remove dependencies via the bun remove command (e.g., bun remove zod)

Running scripts

  • Run specific script via the bun <SCRIPT PATH>.ts command
  • Auto-install and run packages locally via the bunx command (e.g., bunx cowsay "Hello world")
  • Run a custom npm script from package.json via the bun run <SCRIPT NAME> command

Watch mode

  • hot reloading mode via bun --hot index.ts command without restarting the process
  • watch mode via bun --watch index.ts command with restarting the process

File system

Write into the file using Bun.write method

await Bun.write('./output.txt', 'Lorem ipsum');

Environment variables

  • Access environment variables via Bun.env or process.env objects
  • Store variables in .env files, like .env, .env.production, .env.local
  • Print all current environment variables via bun run env command

HTTP server

Create a server with the following code

const server = Bun.serve({
port: Bun.env.PORT,
fetch (request) {
return new Response('Welcome to Bun!');
},
});
console.log(`Listening to port ${server.port}`);

Frameworks

Elysia (Bun framework)

Install packages via the bun add elysia @elysiajs/swagger command, write the initial server, and run it via the bun server.ts command.

// server.ts
import { Elysia } from 'elysia';
import swagger from '@elysiajs/swagger';
const port = Bun.env.PORT || 8081;
new Elysia()
.use(swagger({
path: '/api-docs',
}))
.get('/posts/:id', (({ params: { id }}) => id))
.listen(port);

Express

Install the express package via the bun add express command, write the initial server, and run it via the bun server.ts command

// server.ts
import express from 'express';
const app = express();
const port = Bun.env.PORT || 3001;
app.get('/', (req, res) => {
res.send('Hello world');
});
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});

Debugging

Install the extension Bun for Visual Studio Code by Oven and Run the Bun: Debug file command from the command palette. Execution will pause at the breakpoint.

Testing

Bun supports basic mocking and assertion functions. Run existing tests via the bun run <TEST SCRIPT NAME> (e.g., bun run test:unit) command.

Below is an example of a basic test assertion and mocking using bun:test module.

import { describe, expect, it, mock } from 'bun:test';
import { add } from './addition-service';
import { calculate } from './calculation-service';
describe('Calculation service', () => {
it('should return calculated value', async () => {
const result = calculate();
expect(result).toEqual(5);
});
it('should return mocked value', async () => {
mock.module('./addition-service', () => {
return {
add: () => 3,
};
});
const result = add();
expect(result).toEqual(3);
});
});

Run unit tests via the bun test command. Re-run tests when files change via the bun test --watch command.

SQLite database

Below is a basic example of SQLite driver usage.

import { Database } from 'bun:sqlite';
const database = new Database('database.sqlite');
const query = database.query("SELECT 'hello world' as message;");
console.log(query.get());
database.close();

Formatting Node.js codebase with Prettier

July 3, 2023

Formatting helps to stay consistent with code style throughout the whole codebase. Include format script in pre-hooks (pre-commit or pre-push). This post covers Prettier setup with JavaScript and TypeScript code.

Start by installing the prettier package as a dev dependency.

npm i prettier -D

Specify rules inside the .prettierrc config file.

{
"singleQuote": true,
"trailingComma": "all"
}

Add format script in the package.json file.

{
"scripts": {
// ...
"format": "prettier --write \"{src,test}/**/*.{js,ts}\""
}
}

Notes

If you use Eslint, install the eslint-config-prettier package as a dev dependency and update the Eslint configuration to use the Prettier config.

{
// ...
"extends": [
// ...
"prettier"
]
}

Using Visual Studio Code, you can install a prettier-vscode extension and activate formatting when file changes are saved.

Boilerplate

Here is the link to the boilerplate I use for the development.

Spies and mocking with Node test runner (node:test)

June 24, 2023

Node.js version 20 brings a stable test runner so you can run tests inside *.test.js files with node --test command. This post covers the primary usage of it regarding spies and mocking for the unit tests.

Spies are functions that let you spy on the behavior of functions called indirectly by some other code while mocking injects test values into the code during the tests.

mock.method can create spies and mock async, rejected async, sync, chained methods, and external and built-in modules.

  • Async function
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
const calculationService = {
calculate: () => // implementation
};
describe('mocking resolved value', () => {
it('should resolve mocked value', async () => {
const value = 2;
mock.method(calculationService, 'calculate', async () => value);
const result = await calculationService.calculate();
assert.equal(result, value);
});
});
  • Rejected async function
const error = new Error('some error message');
mock.method(calculationService, 'calculate', async () => Promise.reject(error));
await assert.rejects(async () => calculateSomething(calculationService), error);
  • Sync function
mock.method(calculationService, 'calculate', () => value);
  • Chained methods
mock.method(calculationService, 'get', () => calculationService);
mock.method(calculationService, 'calculate', async () => value);
const result = await calculationService.get().calculate();
  • External modules
import axios from 'axios';
mock.method(axios, 'get', async () => ({ data: value }));
  • Built-in modules
import fs from 'fs/promises';
mock.method(fs, 'readFile', async () => fileContent);
  • Async and sync functions called multiple times can be mocked with different values using context.mock.fn and mockedFunction.mock.mockImplementationOnce.
describe('mocking same method multiple times with different values', () => {
it('should resolve mocked values', async (context) => {
const firstValue = 2;
const secondValue = 3;
const calculateMock = context.mock.fn(calculationService.calculate);
calculateMock.mock.mockImplementationOnce(async () => firstValue, 0);
calculateMock.mock.mockImplementationOnce(async () => secondValue, 1);
const firstResult = await calculateMock();
const secondResult = await calculateMock();
assert.equal(firstResult, firstValue);
assert.equal(secondResult, secondValue);
});
});
  • To assert called arguments for a spy, use mockedFunction.mock.calls[0] value.
mock.method(calculationService, 'calculate');
await calculateSomething(calculationService, firstValue, secondValue);
const call = calculationService.calculate.mock.calls[0];
assert.deepEqual(call.arguments, [firstValue, secondValue]);
  • To assert skipped call for a spy, use mockedFunction.mock.calls.length value.
mock.method(calculationService, 'calculate');
assert.equal(calculationService.calculate.mock.calls.length, 0);
  • To assert how many times mocked function is called, use mockedFunction.mock.calls.length value.
mock.method(calculationService, 'calculate');
calculationService.calculate(3);
calculationService.calculate(2);
assert.equal(calculationService.calculate.mock.calls.length, 2);
  • To assert called arguments for the exact call when a mocked function is called multiple times, an assertion can be done using mockedFunction.mock.calls[index] and call.arguments values.
const calculateMock = context.mock.fn(calculationService.calculate);
calculateMock.mock.mockImplementationOnce((a) => a + 2, 0);
calculateMock.mock.mockImplementationOnce((a) => a + 3, 1);
calculateMock(firstValue);
calculateMock(secondValue);
[firstValue, secondValue].forEach((argument, index) => {
const call = calculateMock.mock.calls[index];
assert.deepEqual(call.arguments, [argument]);
});

Running TypeScript tests

Add a new test script

{
"type": "module",
"scripts": {
"test": "node --test",
"test:ts": "glob -c \"node --loader tsx --no-warnings --test\" \"./src/**/*.{spec,test}.ts\""
},
"devDependencies": {
// ...
"glob": "^10.3.1",
"tsx": "^3.12.7"
}
}

Demo

The demo with the mentioned examples is available here.

Boilerplate

Here is the link to the boilerplate I use for the development.

2020