Using make in frontend projects

Summary: Using make can greatly simplify the maintainance of your project. If any command requires more than a few words to type, consider adding it to a Makefile.

It’s common to use package.json scripts to run commands in node-based projects, for example, npm run dev.

Recently, I came across several articles discussing the use of make and decided to give it a try.

As a result, I’ve started using make in all of my frontend projects, and I really like it. Here’s a simplified example of a Makefile for one of my work projects:

devpod: # start a devcontainer and ssh into it
	devpod up .
	devpod ssh lmst
dev: # run in a dev mode
	npx vite
type:
	npx tsc --noEmit
size:
	npx vite build
	npx size-limit
test:
	npx tap

I think it’s better than scripts in a package.json file, because of:

  • Multi-line commands.
  • Comments: each line can be commented. You can describe why you run one command before another. In a package.json file, all your commands are placed on a single line
  • Works great with non-Node.js commands. In one of our projects, we use Selenium for integration tests, and I no longer struggling to remember how to install all dependencies and run tests

For development I use devcontainers + devpod setup, and connecting to a devcontainer is pretty much to type:

devpod up .
devpod ssh projname

Now I’m just typing make devpod, and after a few seconds I’m inside a devcontainer.

In a team, this approach will work only if each of team’s members uses a platform with a make available. On linux and Mac, it’s already included. On a Windows system you need to install make by yourself.

A small tutorial on how to start using “make”

It’s pretty easy to get started with make -just create a Makefile in the project’s root directory.

Makefile consists of targets. For example, in the make build command, build is a target.

Let’s add a type target, so we’ll be able to use make type to run typescript checks in our project:

type:
	npx tsc --noEmit

Nice! To build our project, we’ll use build target. Typically, we want to check types and then build the project. For this, we can use dependencies:

type:
	npx tsc --noEmit

build: type
	npx vite build

Now, if we run make build, actually these commands will be executed:

npx tsc --noEmit
npx vite build

Variables

Some commands differ by just one flag, such as running typescript type checks mentioned above either single time or in watch mode:

npx tsc --noEmit
npx tsc --noEmit --watch

We can duplicate these commands:

type:
	npx tsc --noEmit
type-watch:
	npx tsc --noEmit --watch

Or we can use variables:

type:
        npx tsc --noEmit $(WATCH)

type-watch: WATCH = --watch
type-watch: type

Here, the type-watch target just sets a WATCH variable, and re-uses type target.

A “docs” Pitfall

If you run a target and there’s a file or directory with the same name exists, make will think that this target is up to date and won’t do anything!

A classic example here is a docs target - most probably you would like to run make docs to generate the documentation, but if your documentation is placed in the docs directory, you’ll see this message:

make: 'docs' target is up to date.

To always run the specified target in such scenarios, we need to use a special .PHONY target:

.PHONY: docs

docs:
	npx vitepress

Links

Movies list, part I

This is the list of movies I’ve watched and liked. I’m starting it right now (2025-03-10).

Paprika

Paprika

I didn’t understand it, but it was very interesting.

Pantheon

Pantheon

This is the great one! I had been under impression from it for a few days after watching.

Долгая ночь

Плохие дети

The day of the Jackal (2024)

The day of the Jackal

The day of the Jackal (1973)

I read the book when I was a teenager and decided to watch 1973 version before starting the 2024 series.

The day of the Jackal 1973

Longlegs

This one is strange. I should have dropped it for sure, but there was something hypnotizing that kept me watch until the end.

Longlegs

How to declare a type for 'onClick'-like events in Typescript

In this article, I’ll tell you how to create a type for all HTML elements’ events, like onClickonDragstart, etc.

Suppose you want to create a wrapper component around some div tag. And you want to accept event handlers as component properties, like this:

const component = CreateMyComponent({
  onClick: (e) => { ...},
  onDragstart: () => {...},
})

For this, we need two things: types for html element events and template literal types.

HtmlElementEventMap

This is the type that maps event names to their callback types.

How I’ve found it? I searched for “typescript dom event types”, and the second link redirected me to the DOM manipulation page in the typescript documentation. There was a link to DOM type definitions, where I searched for string “click”.

interface HTMLElementEventMap extends ElementEventMap, GlobalEventHandlersEventMap {
}

The most interesting part lies in the GlobalEventHandlersEventMap type (the full source of DOM-related types):

interface GlobalEventHandlersEventMap {
    "abort": UIEvent;
    "animationcancel": AnimationEvent;
    "animationend": AnimationEvent;
    "animationiteration": AnimationEvent;
    "animationstart": AnimationEvent;
    "auxclick": MouseEvent;
    "beforeinput": InputEvent;
    "beforetoggle": Event;
    "blur": FocusEvent;
    "cancel": Event;
    "canplay": Event;
    "canplaythrough": Event;
    "change": Event;
    "click": MouseEvent;
    "close": Event;
    "compositionend": CompositionEvent;
    "compositionstart": CompositionEvent;
    "compositionupdate": CompositionEvent;
    "contextlost": Event;
    "contextmenu": MouseEvent;
    "contextrestored": Event;
    "copy": ClipboardEvent;
    "cuechange": Event;
    "cut": ClipboardEvent;
    "dblclick": MouseEvent;
    "drag": DragEvent;
    "dragend": DragEvent;
    "dragenter": DragEvent;
    "dragleave": DragEvent;
    "dragover": DragEvent;
    "dragstart": DragEvent;
    "drop": DragEvent;
    "durationchange": Event;
    "emptied": Event;
    "ended": Event;
    "error": ErrorEvent;
    "focus": FocusEvent;
    "focusin": FocusEvent;
    "focusout": FocusEvent;
    "formdata": FormDataEvent;
    "gotpointercapture": PointerEvent;
    "input": Event;
    "invalid": Event;
    "keydown": KeyboardEvent;
    "keypress": KeyboardEvent;
    "keyup": KeyboardEvent;
    "load": Event;
    "loadeddata": Event;
    "loadedmetadata": Event;
    "loadstart": Event;
    "lostpointercapture": PointerEvent;
    "mousedown": MouseEvent;
    "mouseenter": MouseEvent;
    "mouseleave": MouseEvent;
    "mousemove": MouseEvent;
    "mouseout": MouseEvent;
    "mouseover": MouseEvent;
    "mouseup": MouseEvent;
    "paste": ClipboardEvent;
    "pause": Event;
    "play": Event;
    "playing": Event;
    "pointercancel": PointerEvent;
    "pointerdown": PointerEvent;
    "pointerenter": PointerEvent;
    "pointerleave": PointerEvent;
    "pointermove": PointerEvent;
    "pointerout": PointerEvent;
    "pointerover": PointerEvent;
    "pointerup": PointerEvent;
    "progress": ProgressEvent;
    "ratechange": Event;
    "reset": Event;
    "resize": UIEvent;
    "scroll": Event;
    "scrollend": Event;
    "securitypolicyviolation": SecurityPolicyViolationEvent;
    "seeked": Event;
    "seeking": Event;
    "select": Event;
    "selectionchange": Event;
    "selectstart": Event;
    "slotchange": Event;
    "stalled": Event;
    "submit": SubmitEvent;
    "suspend": Event;
    "timeupdate": Event;
    "toggle": Event;
    "touchcancel": TouchEvent;
    "touchend": TouchEvent;
    "touchmove": TouchEvent;
    "touchstart": TouchEvent;
    "transitioncancel": TransitionEvent;
    "transitionend": TransitionEvent;
    "transitionrun": TransitionEvent;
    "transitionstart": TransitionEvent;
    "volumechange": Event;
    "waiting": Event;
    "webkitanimationend": Event;
    "webkitanimationiteration": Event;
    "webkitanimationstart": Event;
    "webkittransitionend": Event;
    "wheel": WheelEvent;
}

Template literal types

Template literal types allow us to use string interpolation in type declarations.

For example, this is how we can create a onClick literal type:

type DomEvent = 'Click'

// type DomEventHandlerName = 'onClick'
type DomEventHandlerName = `on${DomEvent}`

Implementation

All we need now is to loop over all events and create a on type for each of them:

export type HTMLEventHandler = {
  [K in keyof HTMLElementEventMap as `on${Capitalize<K>}`]? : (evt: HTMLElementEventMap[K]) => void
}

We used Capitalize helper type, which capitalizes the first character in a string.

Create a tooltip with CSS

Recently I was needed to add a quick tooltip to a span element in a project with vuetify and vue2. My first try was to use vuetify’s v-tooltip, but, you know, it didn’t work after 3 minutes of copy-pasting code samples from docs.

So I just googled something like “HTML native tooltip”, and found an example of a tooltip on stackoverflow, which impressed me with its small amount of code and nice results.

Demo

Hover me

The solution is to use data-attribute and attr() function:

[data-tooltip]:hover::after {
  display: block;
  position: absolute;
  content: attr(data-tooltip);
  border: 1px solid black;
  background: #eee;
  padding: .25em;
}
<div data-tooltip="Hello, World!">Hello, World!</div>

Often CSS-only solutions look very tricky, but this one is very good, I like it.

More info

NIH syndrome

Not invented here (NIH) is the tendency to avoid using or buying products, research, standards, or knowledge from external origins. It is usually adopted by social, corporate, or institutional cultures. Research illustrates a strong bias against ideas from the outside.

Wikipedia

Oracle: nvl is not lazy

We all use nvl. It’s so common and easy(3 chars among 8 in coalesce and 6 in decode) to write it in queries, but there is one thing you should keep in mind when using NVL.

First, let’s face the problem:

create package test_pck is

    function get_user_id return number;
end;
/

create or replace package body test_pck is

    function get_user_id return number
    is
        l_res number;
    begin
        <<lbl>>
        if 1 < 2 then
            goto lbl;
        end if;    end;
end;
/

Here we have created a package with a function that never returns a value, because it contains an infinity loop.

I picked this method for demonstration to be sure that examples will show you the same result despite IDE and environment settings you have.

So, let’s run this query:

select nvl(1, test_pck.get_user_id)
from dual

It has hung.

It tells us that the test_pck.get_user_id function was called despite the fact that the first parameter is 1, which is not null.

Generally, it’s not a problem. But it may be, if your second argument in nvl is a heavy function. In this case SQL query might work slower than you expect.

Do all functions that work with null values work the same? Let’s see:

COALESCE:

-- Lazy
select coalesce(1, test_pck.get_user_id)
from dual

DECODE:

-- Lazy
select decode(1, 1, 1, test_pck.get_user_id)
from dual

CASE:

-- Lazy
select case
           when 1 = 1 then 1
           else test_pck.get_user_id
       end
from dual

We forgot about nvl’s brother - NVL2

-- Not Lazy
select nvl2(1, 2, test_pck.get_user_id)
from dual

Yes, it’s not lazy, like nvl.

What to choose

nvl meme

I think it’s not a problem when you use nvl with “static” values. But if you use functions as one (or both) arguments to nvl, it’s better to replace it with a call to something lazier.

nvl meme

Using Typescript to force errors handling

Summary: Typescript’s discriminated unions may be used to force you to handle exceptions.

The problem

If a function can throw an error, it should be wrapped in try/catch block to be properly handled.

However, error handling can be ignored, which adds more room for potential errors.

function getUserFromStorage(id: string) {
  return undefined
}

function getUser(id: string) {
  const user = getUserFromStorage(id)
  if (user === undefined)
    throw new Error('Specified user does not exist')

  return user
}

function printUserInfo(id: string) {
  const user = getUser(id)

  console.log(`User name is ${user.name}`)
}

The problem here is that we didn’t check for errors when we retrieve user from our storage. Moreover, TS compiler don’t actually care.

Ideally, printUserInfo should handle possible errors:

function printUserInfo(id: string) {
  try {
    const user = getUser(id)

    console.log(`User name is ${user.name}`)
  }
  catch {
    console.error('Some error')
  }
}

Discriminated unions

A union type is a type that combines multiple types into one. A variable of a union type can store only values with those types:

type mode = 'edit' | 'view' | number

// ok, type ='edit'
const a = 'edit'

// ok, type = number
const b = 24

// ok, type = 'view'
const c = 'view'

// type error: type string is unassignable to type mode
const d = 'randomstring'

Union types and type narrowing

When dealing with union types, we often need to narrow them before use:

type StringOrNumber = string | number

function getStringOrNumber(): StringOrNumber {
    return 2
}

let v = getStringOrNumber()

// Error!
// The left-hand side of an arithmetic operation must be of 
// type 'any', 'number', 'bigint' or an enum type.
console.log(v ** 3)

See, typescript complains because it doesn’t know whether v is string or number, Therefore, we need to make sure that v is a number:

type StringOrNumber = string | number

function getStringOrNumber(): StringOrNumber {
    return 2
}
let v = getStringOrNumber()

if (typeof v === 'number')
    console.log(v ** 3)
else
    console.log(v.toLowerCase())

Take a look at the last example one more time; did you notice console.log(v.toLowerCase())?

It works because this statement is placed in the else branch of our check, so typescript knows that v is a string.

Fixing the problem

We can use union types and required type narrowing to make result checking mandatory - or our program won’t be compiled.

This is how it can be implemenented.

First of all, we create two main types and their union:

interface OkResult<T> {
  ok: true
  value: T
}

interface FailResult {
  ok: false
  error: string
}

type OkOrFail<T> = OkResult<T> | FailResult

Each of these types has ok field, which we can use to narrow OkOrFail type to OkResult or FailResult:

type User = {
    name: string
    email: string
}

function getUser(id: number): OkOrFail<User> {
  try {
    const user = getUserFromDb(id)

    return {
      ok: true,
      value: user,
    }
  }
  catch {
    return {
      ok: false,
      error: 'can\'t find a user'
    }
  }
}

We used try/catch to return OkResult or FailResult, so our function doesn’t throw errors.

It returns union type instead, and in order to work with the returned error we need to narrow result to OkResult type:

const u = getUser(1)

// Type Error!
// If u is `FailResult`, it doesn't have the `value` field!
console.log(u.value)

// Now everything is ok, we narrow result down to `OkResult` type
if (u.ok)
  console.log(u.value)

Always returning literal objects is too much to type, so we can create helper functions for this:

function ok<T>(value: T): OkResult<T> {
  return {
    ok: true,
    value,
  }
}

function fail(error: string): FailResult {
  return {
    ok: false,
    error,
  }
}

And then our getUser can be refactored:

function getUser(id: number): OkOrFail<User> {
  try {
    const user = getUserFromDb(id)

    return ok(user)
  }
  catch {
    return fail('can\'t find a user')
  }
}

That’s it, I hope you liked this article, good luck!

Create signals in plain JS

In this article, we’ll implement our own signals.

What are signals

Signals are objects that incapsulate access to their original value and track dependencies that use these signals.

Yes but why?

The goal of this post is to have fun and to create the basic understanding through practice of how such things can work.

Real world signals examples

Vue

In vue3, there are two main reactivity primitives: ref and reactive. They’re not called signals in vue, but they’re the same. From the documentation:

Fundamentally, signals are the same kind of reactivity primitive as Vue refs. It’s a value container that provides dependency tracking on access, and side-effect triggering on mutation.

Vue automatically tracks dependencies that use refs or reactive objects, and triggers re-render or re-computation (if we talk about such thing as computed) when their value is changed.

Additionally, vue provides ‘watchers’(watchwatchEffect), which run callback functions any time their ‘watched’ value has been changed. In code it looks this way:

const name = ref('name')

watch(name, (newName) => {
  console.log(newName)
})

S.js

In S.js, signals are created with the S.data function. To change the signal’s value, you call signal as a function with an argument, and you call signal without arguments to get its current value:

const name = S.data('Andrew')
name('Saphir')
console.log(name()) // prints 'Saphir'

S.js has the on method, which is identical to vue’s watch:

const counter = S.data(0)

S.on(counter, () => {
  console.log('The counter has been changed')
})

Design signals

Our signals will work as in the S.js library - createSignal() function will return a function that can be used as a setter when we provide a new value, and as a getter when called without arguments:

const userName = createSignal('User13')

console.log(userName()) // prints 'User13'

userName('User')
console.log(userName()) // prints 'User'

Update value with function

For simple values like strings or numbers changing values by simply passing them as arguments is ok, but for more complex types it may be too verbose. Imagine a user object with a complex structure like this:

const user = {
  name: 'User13',
  age: 44,
  email: 'user13@mail.com',
  created_at: '2024-01-01',
}

To update one property, we have to pass a new value to a signal:

const signal = createSignal(user)

signal({
  name: 'User02',
  age: 44,
  email: 'user13@mail.com',
  created_at: '2024-01-01',
})

console.log(signal().name) // 'User02'

We can, hovever, simplify this by using spread operator and signal as a getter:

const signal = createSignal(user)

signal({
  ...signal(), // Will return the current object
  name: 'User02',
})

Looks better, yet we can make it better - pass a callback function with one parameter - current signal’s value, and assign result as a new value:

const signal = createSignal(user)

signal(u => ({
  ...u,
  name: 'User02',
}))

Or with simple types:

const signal = createSignal(3)

signal(v => ++v)

console.log(signal()) // prints '4'

Track signal changes

Signals as containers for values are not very useful, though. We want to be able to perform some actions when signal’s value changed. For this, we’ll create a function named on:

const s = createSignal(12)

on(s, (newVal) => {
  console.log(`New value is ${newVal}`)
})

s(13)
s(v => v++)

What we expect from this code? It should print these two lines:

New value is 13
New value is 14

Implementation

First thing that we’ll create is a function that acts differently depending whether it’s called with arguments or without them. For this, we need the arguments object.

function signal(param) {
  if (arguments.length === 0)
    console.log('Without arguments')
  else
    console.log('With arguments')
}

signal() // 'Without arguments'
signal(23) // 'With arguments'

Note that we can’t check param for undefined, because in this case we won’t be able to distinct signal() and signal(undefined) calls:

function signal(param) {
  if (param === undefined)
    console.log('Without arguments')
  else
    console.log('With arguments')
}

signal() // 'Without arguments'
signal(2) // 'With arguments'

// Oh no! It prints 'Without arguments'!
signal(undefined) // 'Without arguments'

createSignal

For keeping the same state between signal function calls we’ll wrap our state variable in a closure.

function createSignal(value) {
  let _value = value

  function signal(v) {
    const isSetter = arguments.length > 0

    if (isSetter) {
      if (typeof v === 'function') {
        _value = v(_value)
      } else {
        _value = v
      }
    } else {
      return _value
    }
  }

  return signal
}

const name = createSignal('Anna')
console.log(name()) // 'Anna'
console.log(name('Tanya'))
console.log(name()) // 'Tanya'

And that’s it, our signal is ready. It’s simple, but it works! And it also works with functions, as we planned:

const user = createSignal({ name: 'Anna', age: 41 })
user(u => ({...u, name: 'Tanya'}))
console.log(user().name) // 'Tanya'
console.log(user().age) // 41

on function

This function takes a signal and a callback that should be executed when the signal is changed. At this point, we should think about few things:

  1. We need some data structure to store callbacks
  2. We need to modify the signals’ implementation and execute required callbacks when a signal is executed as a “setter”.

Data structures go first. We’ll use Map with signals as its keys and array of callbacks as its values. It will allow us to quiclky find required callbacks. This map should be global for the whole module:

const effects = new Map()
function on(signal, cb) {
  const signalEffects = effects.get(signal)

  if (signalEffects) {
    signalEffects.push(cb)
  } else {
    effects.set(signal, [cb])
  }
}

Very simple, isn’t it? We just push the callback to the array of already existed callbacks if it exists. If not, we create a brand new array with just one value - our callback.

Finally, we need to find these callbacks and execute them in our signals. This is the code that implements it:

// Find registered callbacks
const signalEffects = effects.get(signal)
if (signalEffects) {
  for (const cb of signalEffects) {
    cb(newSignalValue)
  }
}

What’s next?

I plan to write one more post which will introduce a few improvements to our current signals, so stay tuned in!

Full source

const effects = new Map()

function ss(value) {
  let _value = value

  function signal(v) {
    const isSetter = arguments.length > 0

    if (isSetter) {
      if (typeof v === 'function') {
        _value = v(_value)
      } else {
        _value = v
      }

      // Find registered callbacks
      const signalEffects = effects.get(signal)
      if (signalEffects) {
        for (const cb of signalEffects) {
          cb(_value)
        }
      }
    } else {
      return _value
    }
  }

  return signal
}

function on(signal, cb) {
  const signalEffects = effects.get(signal)

  if (signalEffects) {
    signalEffects.push(cb)
  } else {
    effects.set(signal, [cb])
  }
}

Let’s do something with our signals

For demonstration purposes, let’s create a simple web page with a button and a counter for the number of times the button was clicked.

<html>
	<head>
		<script type="text/javascript" src="index.js"></script>
		<script type="text/javascript">
			const counter = createSignal(0)

			function onClick() {
				counter(v => ++v)
			}

			window.addEventListener('load', () => {
				const btn = document.querySelector('button')
				// Display initial counter's value in the button
				btn.innerText = counter()

				// On each click, increment counter value
				btn.addEventListener('click', onClick)

				// Every time when the counter was changed,
				// update button text with the new value
				on(counter, (newValue) => {
					btn.innerText = newValue
				})
			})
		</script>
	</head>
	<body>
		<button> </button>
	</body>
</html>

Links