APIs
Introduction

Introduction

"Haetae" represents its packages, haetae and @haetae/*.

API

Type notations

Types are annotated by typescript syntax.
For example, trailing ? means an optional field.

Common utility types

Some APIs depend on common utility types.

type PromiseOr<T> = Promise<T> | T
type RecScalar = string | number | boolean | null | undefined
interface Rec {
  [key: string]: RecScalar | Rec | (RecScalar | Rec)[]
}

@haetae/common exports these, and they can be used across multiple packages.

Memoization

Memoization (opens in a new tab) is a technique returning the cached result. Some functions (e.g. getConfig, getStore) are memoized. The cache only exists in the same process's memory and is cleared when the process is terminated.

💡

Linked by label
Any functions linked to from here by Memoized label satisfy the principles below.

Cache hit and clear

To clear the memoization cache, you can call <function>.clear(). For example, core.getConfig is a memoized function, and getConfig.clear() would clear its cache.

import { getConfig } from '@haetae/core'
 
// `getConfig` is executed.
const config1 = await getConfig({ filename: '/foo/haetae.config.js' })
 
// `getConfig` is executed. No memoization cache hit, due to the different argument.
const config2 = await getConfig({ filename: '/bar/haetae.config.js' })
 
// Cache hit from the 1st call result, thanks to the same argument.
// `getConfig` is not executed. Just returned from the memoization cache.
const config3 = await getConfig({ filename: '/foo/haetae.config.js' })
 
// Cache hit from the 2nd call result, thanks to the same argument.
const config4 = await getConfig({ filename: '/bar/haetae.config.js' })
 
// Clear the memoization cache entirely
getConfig.clear()
 
// `getConfig` is freshly executed without cache.
// A new cache is created from now on again.
const config5 = await getConfig({ filename: '/foo/haetae.config.js' })

Cache by shallow copy

The memoization cache is based on shallow copy.

// `config1` and `config2` would have same memory address.
const config1 = await getConfig()
const config2 = await getConfig()
 
config1.foo = 'bar'
// `config2` is also modified when `config1` is modified
console.log(config2.foo) // 'bar'
const config3 = await getConfig()
console.log(config3.foo) // 'bar'

If you want to avoid the side effect, you can clear the cache before calling getConfig.
Or deep copy techniques like immer (opens in a new tab) can be a good solution.

Path Principles

Haetae has a few design principles for file and directory paths.

💡

Linked by label
Any functions linked to from here by Path Principles label satisfy the principles below.

1. Absolute Return Value

Returned file or directory path is always absolute path (NOT relative).
This is true even when the path is not a directly returned entity, like even when it's an element of an array or object field. For instance, glob of @haetae/utils and changedFiles of @haetae/git are such functions.

import { utils } from 'haetae'
 
const files = await utils.glob(['**/*.test.ts', '**/*.test.tsx'])
// ['/path/to/foo.test.ts', '/path/to/bar.test.tsx', '/path/to/baz.test.tsx']

Arguments or options, which are not return value, don't have to be/contain an absolute path.
In fact, for arguments or options, relative paths would probably be more suitable for the majority of cases.

2. / As Delimiter

Haetae only officially uses/supports / as a delimiter for a path.

/ is traditionally used in POSIX (e.g. Linux, macOS). However, / works well on Windows in Node.js (and other languages or runtimes as well). Thus, Haetae does NOT officially use/support Windows legacy \ delimiter.

This is a better decision for cross-platform design as well as convenience of usage and development.

  • Return Value: Delimiter is always /. This is true even when the path is not a returned entity itself but a part of it, like an element of an array or a field of object.

    For example,

    import { core } from 'haetae'
     
    const config = await core.configure({ storeFile: '..\..\.haetae\store.json', /* ... */ })
    // Relative path is transformed to an absolute path.
    // `\` is transformed to `/`.
    console.log(config.storeFile) // => '/path/to/.haetae/store.json'
  • Function Argument or Option: It MAY PROBABLY work, while not guaranteed officially, even when an (part of) argument or an option's path delimiter is \ on Windows. But using / is always officially recommended.

pkg

haetae and @haetae/* have this export.

pkg contains the package's meta information, name and version.
For example, let's assume the package version is 1.2.3-beta.4.
Then the value would be like this.

const { pkg } = require('haetae' /* or '@haetae/<package>' */)
 
pkg.name // 'haetae' or '@haetae/<package>'
pkg.version.value // '1.2.3-beta.4'
pkg.version.major // 1 // Integer
pkg.version.minor // 2 // Integer
pkg.version.patch // 3 // Integer
pkg.version.prerelease // ['beta', 4] // 'beta' is string, 4 is integer
pkg.version.untilMinor //  '1.2'
pkg.version.untilPatch // '1.2.3'

pkg.version.* could be a good choice for env.

haetae.config.js
import { core, pkg } from 'haetae'
 
export default core.configure({
  // Other options are omitted for brevity.
  commands: {
    myAwesomeCommand: {
      env: {
        // Different OS are to be treated as different environments.
        os: process.platform
        // Different major versions of `haetae` as well.
        haetae: pkg.version.major
      },
      run: () => { /* ... */ }
    }
  },
})