APIs
@haetae/utils

@haetae/utils

@haetae/utils provides useful unitlities for general Heatae workflow.

peerDependencies

Note: This might not be exhaustive and lists only Haetae's packages.

Dependents

Installation

💡

Are you developing a library(e.g. plugin) for Haetae?
It might be more suitable to specify @haetae/utils as peerDependencies than dependencies.

To automatically install @haetae/utils and its peerDependencies

You may want to install @haetae/utils and its peerDependencies all at once.
install-peerdeps (opens in a new tab) is a good tool for that.


# As dependencies
npx install-peerdeps @haetae/utils
# As devDependencies
npx install-peerdeps --dev @haetae/utils

To manually handle installation

You might want to manually deal with installation.
First, install @haetae/utils itself.


# As dependencies
npm install @haetae/utils
# As devDependencies
npm install --save-dev @haetae/utils

Then, check out peerDependencies and manually handle them.
(e.g. Install them as dependencies or set them as peerDependencies)

# This does not install, but just show peerDependencies.
npm info @haetae/utils peerDependencies

API

pkg

Refer to introduction#pkg.

RecordData

interface RecordData extends Rec {
  '@haetae/utils': {
    files?: Record<string, string>
    pkgVersion: string
  }
}
💡

Record Data Namespace
Record Data can have arbitrary fields. '@haetae/utils' is a namespace to avoid collision. Haetae uses a package name as a namespace by convention.

RecordDataOptions

An argument interface for recordData

interface RecordDataOptions {
  files?: Record<string, string>
  pkgVersion?: string
}

recordData

A function to form Record Data @haetae/utils manages.

Type

(options?: RecordDataOptions) => Promise<RecordData>

Options?

  • files? : filename-hash pairs.
  • pkgVersion? : Version of @haetae/utils. (default: pkg.version.value)

GlobOptions

A function to add a new record under the given command to store.

GlobbyOptions (opens in a new tab), which is part of GlobOptions, is from globby (opens in a new tab).

interface GlobOptions {
  rootDir?: string // A facade option for `globbyOptions.cwd`
  globbyOptions?: GlobbyOptions
}

glob


Path Principles

A function to find files by a glob pattern.
Internally, the task is delegated to globby (opens in a new tab) (v13 as of writing).
glob is a facade function (opens in a new tab) for globby, providing more handy experience by default options and postprocessing.

Type

(patterns: readonly string[], options?: GlobOptions) => Promise<string[]>

Arguments

  • patterns: Array of glob patterns. (e.g. ['**/*.test.ts', '**/*.test.tsx'])
  • options? :

ExecOptions

An argument interface for exec.

interface ExecOptions {
  uid?: number | undefined
  gid?: number | undefined
  cwd?: string | URL | undefined
  env?: NodeJS.ProcessEnv | undefined
  windowsHide?: boolean | undefined
  timeout?: number | undefined
  shell?: string | undefined
  maxBuffer?: number | undefined
  killSignal?: NodeJS.Signals | number | undefined
  trim?: boolean // An option added from Haetae side. (Not for `childProcess.exec`)
}

exec

A function to execute a script.
Internally, nodejs's childProcess.exec (opens in a new tab) is used.

Type

(command: string, options?: ExecOptions) => Promise<string>

Arguments

  • command : An arbitrary command to execute on shell. This command does NOT mean haetae's command concept.
  • options? : Options for childProcess.exec. Refer to the nodejs official docs (opens in a new tab).
    • options.trim? : Some commands' result (stdout, stderr) ends with whitespace(s) or line terminator character (e.g. \n). If true, the result would be automatically trimmed (opens in a new tab). If false, the result would be returned as-is. options.trim is the only option not a part of childProcess.exec's original options.

$Exec

Type of $.
It's an interface for function, but simultaneously ExecOptions.

Type

interface $Exec extends ExecOptions {
  (
    statics: TemplateStringsArray,
    ...dynamics: readonly PromiseOr<
      string | number | PromiseOr<string | number>[]
    >[]
  ): Promise<string>
}

$

A wrapper of exec as a Tagged Template (opens in a new tab).
It can have properties as options (ExecOptions) of exec.

Type

$Exec

Usage

You can execute any shell command.

const stdout = await $`echo hello world`
assert(stdout === 'hello world')

Placeholders can be used. Promise is automatically awaited internally.

const stdout = await $`echo ${123} ${'hello'} ${Promise.resolve('world')}`
assert(stdout === '123 hello world')

When a placeholder is an array, a white space (' ') is joined between the elements.

// Array
let stdout = await $`echo ${[Promise.resolve('hello'), 'world']}`
assert(stdout === 'hello world')
 
// Promise<Array>
stdout = await $`echo ${Promise.resolve([
  Promise.resolve('hello'),
  'world',
])}`
assert(stdout === 'hello world')

It can have properties as options (ExecOptions) of exec.
The state of properties of $ does not take effect when independently calling exec.

$.cwd = '/path/to/somewhere'
const stdout = await $`pwd`
assert(stdout === '/path/to/somewhere')

HashOptions

An argument interface for hash.

interface HashOptions {
  algorithm?: 'md5' | 'sha1' | 'sha256' | 'sha512'
  rootDir?: string
}

hash

A function to hash files.
It reads content of a single or multiple file(s), and returns a cryptographic hash string.

💡

Sorted Merkle Tree
When multiple files are given, they are treated as a single depth Merkle Tree (opens in a new tab). However, the files are sorted by their path before hashed, resulting in same result even when different order is given. For example, hash(['foo.txt', 'bar.txt']) is equal to hash(['bar.txt', 'foo.txt']).

Type

(files: string[], options?: HashOptions) => Promise<string>

Arguments

  • files : Files to hash. (e.g. ['package.json', 'package-lock.json'])
  • options?
    • options.algorithm? : An hash algorithm to use. (default: 'sha256')
    • options.rootDir? : A directory to start file search. When an element of files is relative (not absolute), this value is used. Ignored otherwise. (default: core.getConfigDirname())

Usage

env in the config file can be a good place to use hash.

haetae.config.js
import { core, utils, js } from 'haetae'
 
export default core.configure({
  // Other options are omitted for brevity.
  commands: {
    myTest: {
      env: async () => ({
        hash: await utils.hash([
          'jest.config.js',
          'package-lock.json',
        ])
      }),
      run: async () => { /* ... */ }
    },
    myLint: {
      env: async () => ({
        eslintrc: await utils.hash(['.eslintrc.js']),
        eslint: (await js.version('eslint')).major
      }),
      run: async () => { /* ... */ }
    }
  },
})

Usage with glob

If you target many files, consider using glob with hash.

await utils.hash([
  'foo',
  ...(await utils.glob(['bar/**/*'])),
])

DepsEdge

An interface resolving dependencies edge.
TIP. The prefix Deps stands for 'Dependencies'.

interface DepsEdge {
  dependents: readonly string[]
  dependencies: readonly string[]
}

GraphOptions

An argument interface for graph.

interface GraphOptions {
  edges: readonly DepsEdge[]
  rootDir?: string
}

DepsGraph

An return type of graph.
Its structure is similar to the traditional 'Adjacency List' (opens in a new tab).
TIP. The prefix Deps stands for 'Dependencies'.

interface DepsGraph {
  // key is dependent. Value is Set of dependencies.
  [dependent: string]: Set<string>
}

graph


Path Principles

A function to create a dependency graph.
Unlike js.graph, it's not just for a specific language, but for any dependency graph.

Type

(options?: GraphOptions) => DepsGraph

Options?

  • edges : A single or multiple edge(s). The dependents and dependencies have to be file path, not directory.
  • rootDir? : When an element of dependents and dependencies is given as a relative path, rootDir is joined to transform it to an absolute path. (default: core.getConfigDirname())

Basic Usage

You can specify any dependency relationship.
This is just a pure function. Whether the files depend on each other does not matter.

const result = graph({
  rootDir: '/path/to',
  edges: [
    {
      dependents: ['src/foo.tsx', 'src/bar.ts'],
      dependencies: ['assets/one.png', 'config/another.json'],
    },
    {
      // 'src/bar.ts' appears again, and it's OK!
      dependents: ['src/bar.ts', 'test/qux.ts'],
      // Absolute path is also OK!
      dependencies: ['/somewhere/the-other.txt'],
    },
  ],
})
 
const expected = {
  '/path/to/src/foo.tsx': new Set([
    '/path/to/assets/one.png',
    '/path/to/config/another.json',
  ]),
  '/path/to/src/bar.ts': new Set([
    '/path/to/assets/one.png',
    '/path/to/config/another.json',
    '/somewhere/the-other.txt',
  ]),
  '/path/to/test/qux.ts': new Set([
    '/somewhere/the-other.txt', // Absolute path is preserved.
  ]),
  '/path/to/assets/one.png': new Set([]),
  '/path/to/config/another.json': new Set([]),
  '/somewhere/the-other.txt': new Set([]),
}
 
assert(deepEqual(result, expected)) // They are same.

Usage With glob

glob is a good friend when you want to specify chunk-level dependency relationships.

Let's say you have multiple Python projects (packages) in a single monorepo.
For example, packages 'foo' and 'bar' depend on a package 'qux'.
Then you can create a normalized dependency graph like the snippet below.

graph({
  edges: [
    {
      dependents: await glob(['packages/foo/**/*.py', 'packages/bar/**/*.py']),
      dependencies: await glob(['packages/qux/**/*.py']),
    },
  ],
})

mergeGraphs

A function to merge multiple dependency graphs into one single unified graph.

(graphs : DepsGraph[]) => DepsGraph

DependsOnOptions

An argument interface for dependsOn.

interface DependsOnOptions {
  dependent: string
  dependencies: readonly string[] | Set<string>
  graph: DepsGraph
  rootDir?: string
}

dependsOn

A function to check if a file depends on one of different files, transitively or directly.

(options: DependsOnOptions) => boolean

Options

  • dependent : A target to check if it is a dependent of at least one of dependencies, directly or transitively.
  • dependencies : A list of candidates that may be a dependency of dependent, directly or transitively.
  • graph : A graph. Return value of graph is proper.
  • rootDir? : When dependent or an element of dependencies is given as a relative path, rootDir is joined to transform it to an absolute path. (default: core.getConfigDirname())

Basic Usage

Let's say,

  • a depends on b.
  • c depends on a, which depends on b
  • e does not (even transitively) depend on neither f nor b.
  • f does not (even transitively) depend on b.

then the result would be like this.

const graph = utils.graph({
  edges: [
    {
      dependents: ['a'],
      dependencies: ['b'],
    },
    {
      dependents: ['c'],
      dependencies: ['a'],
    },
    {
      dependents: ['f'],
      dependencies: ['another', 'another2'],
    },
  ],
})
 
utils.dependsOn({ dependent: 'a', dependencies: ['f', 'b'], graph }) // true
utils.dependsOn({ dependent: 'c', dependencies: ['f', 'b'], graph }) // true -> transitively
utils.dependsOn({ dependent: 'f', dependencies: ['f', 'b'], graph }) // true -> 'f' depends on 'f' itself.
utils.dependsOn({ dependent: 'non-existent', dependencies: ['f', 'b'], graph }) // false -> `graph[dependent] === undefined`, so false
utils.dependsOn({ dependent: 'a', dependencies: ['non-existent']), graph }) // false
utils.dependsOn({ dependent: 'c', dependencies: ['non-existent', 'b']), graph }) // true -> at least one (transitive) dependency is found

ChangedFilesOptions

An argument interface for changedFiles.

interface ChangedFilesOptions {
  rootDir?: string
  hash?: (filename: string) => PromiseOr<string>
  filterByExistence?: boolean
  reserveRecordData?: boolean
}

changedFiles


Memoized Path Principles

A function to get a list of changed files.
Getting Started guide explains its principles.

Type

(files: readonly string[], options?: ChangedFilesOptions) => Promise<string[]>

Options?

  • rootDir? : When an element of files is given as a relative path, rootDir is used to calculate the path. (default: core.getConfigDirname().)
  • hash? : A commit ID as a starting point of comparison. (default: (f) => _hash([f], { rootDir }))
  • filterByExistence? : Whether to filter out non-existent files before passing to hash function. (default: false)
  • reserveRecordData? : Whether to reserve Record Data. (default: true)