@haetae/utils
@haetae/utils
provides useful utilities 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 the 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 a more handy experience by sane default rootDir
options and postprocessing.
There're many options, but aren't explained here, except roodDir
.
Refer to globby v13 docs (opens in a new tab) for other options.
Type
(patterns: readonly string[], options?: GlobOptions) => Promise<string[]>
Arguments
patterns
: Array of glob patterns. (e.g.['**/*.test.{ts, tsx}']
)options?
:rootDir?
: A directory to start search. Its role is same as globby'scwd
option, but make sure to userootDir
, notcwd
. (default:core.getConfigDirname()
)
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 forchildProcess.exec
. Refer to the nodejs official docs (opens in a new tab).trim?
: Some commands' result (stdout, stderr) ends with whitespace(s) or line terminator character (e.g.\n
). Iftrue
, the result would be automatically trimmed (opens in a new tab). Iffalse
, the result would be returned as-is.trim
is the only option that is not a part ofchildProcess.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
glob?: boolean
}
hash
A function to hash files.
It reads the content of single or multiple files 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 the 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?
algorithm?
: An hash algorithm to use. (default:'sha256'
)rootDir?
: A directory to start file search. When an element offiles
is relative (not absolute), this value is used. Ignored otherwise. (default:core.getConfigDirname()
)glob?
: Whether to enable glob pattern. (default:true
)
Usage
env
in the config file can be a good place to use hash
.
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 pattern
If you target many files, consider using glob pattern.
await utils.hash(['foo', 'bar/**/*.json'])
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
glob?: boolean
}
DepsGraph
A 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) => Promise<DepsGraph>
Options?
edges
: A single or multiple edge(s). Thedependents
anddependencies
have to be file paths, not directories. Each of them supports glob pattern whenglob
is true.rootDir?
: When an element ofdependents
anddependencies
is given as a relative path,rootDir
is joined to transform it to an absolute path. (default:core.getConfigDirname()
)glob?
: Whether to enable glob pattern. (default:true
)
Basic Usage
Basic usage is guided in Getting Started article.
You can specify any dependency relationship.
This is just a pure function that does not hit the filesystem.
Whether the files actually depend on each other does not matter.
It only works as you specify.
const result = await 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.
mergeGraphs
A function to merge multiple dependency graphs into one single unified graph.
(graphs : DepsGraph[]) => DepsGraph
DepsOptions
An argument interface for deps
.
interface DepsOptions {
entrypoint: string
graph: DepsGraph
rootDir?: string
}
deps
A function to get all of the direct and transitive dependencies of a single entry point.
The searched result keeps the order by breath-first approach, without duplication of elements.
(options: DepsOptions) => string[]
Options
entrypoint
: An entry point to get all of whose direct and transitive dependencies.graph
: A dependency graph. Return value ofgraph
is proper.rootDir?
: A directory to join with whenentrypoint
is given as a relative path. (default:core.getConfigDirname()
)
DependsOnOptions
An argument interface for dependsOn
.
DependOnOptions
vs DependsOnOptions
There're DependOnOptions
(plural) and DependsOnOptions
(singular).
Don't confuse!
interface DependsOnOptions {
dependent: string
dependencies: readonly string[]
graph: DepsGraph
rootDir?: string
glob?: boolean
}
dependsOn
A function to check if a file depends on one of the given files, transitively or directly.
(options: DependsOnOptions) => Promise<boolean>
Options
dependent
: A target to check if it is a dependent of at least one ofdependencies
, directly or transitively.dependencies
: Candidates that may be a dependency of Dependents, directly or transitively.graph
: A dependency graph. Return value ofgraph
is proper.rootDir?
: A directory to join with whendependent
ordependencies
is given as a relative path. (default:core.getConfigDirname()
)glob?
: Whether to enable glob pattern. (default:true
)
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'],
},
],
})
await utils.dependsOn({ dependent: 'a', dependencies: ['f', 'b'], graph }) // true
await utils.dependsOn({ dependent: 'c', dependencies: ['f', 'b'], graph }) // true -> transitively
await utils.dependsOn({ dependent: 'f', dependencies: ['f', 'b'], graph }) // true -> 'f' depends on 'f' itself.
await utils.dependsOn({ dependent: 'non-existent', dependencies: ['f', 'b'], graph }) // false -> `graph[dependent] === undefined`, so false
await utils.dependsOn({ dependent: 'a', dependencies: ['non-existent']), graph }) // false
await utils.dependsOn({ dependent: 'c', dependencies: ['non-existent', 'b']), graph }) // true -> at least one (transitive) dependency is found
DependOnOptions
An argument interface for dependOn
.
DependOnOptions
vs DependsOnOptions
There're DependOnOptions
(plural) and DependsOnOptions
(singular).
Don't confuse!
interface DependOnOptions {
dependents: readonly string[]
dependencies: readonly string[]
graph: DepsGraph
rootDir?: string
glob?: boolean
}
dependOn
A function to check if a file depends on one of the given files, transitively or directly.
(options: DependOnOptions) => Promise<string[]>
Options
dependents
: Targets to filter by whether it's a dependent of at least one ofdependencies
, directly or transitively.dependencies
: Candidates that may be a dependency of dependent, directly or transitively.graph
: A dependency graph. Return value ofgraph
is proper.rootDir?
: A directory to join with whendependents
, ordependencies
are given as relative paths. (default:core.getConfigDirname()
)glob?
: Whether to enable glob pattern. (default:true
)
Usage
Basic usage is very similar to js.dependOn
,
which is guided in the Getting Started article.
ChangedFilesOptions
An argument interface for changedFiles
.
interface ChangedFilesOptions {
rootDir?: string
renew?: readonly string[]
hash?: (filename: string) => PromiseOr<string>
filterByExistence?: boolean
keepRemovedFiles?: boolean
reserveRecordData?: boolean
previousFiles?: Record<string, string>
glob?: boolean
}
changedFiles
Memoized Path Principles
A function to get a list of changed files by hash comparison.
(Getting Started guide explains its basic usage.)
Type
(files: readonly string[], options?: ChangedFilesOptions) => Promise<string[]>
Arguments
files
: Files to detect if changed.options?
rootDir?
: When an element offiles
is given as a relative path, rootDir is used to calculate the path. (default:core.getConfigDirname()
.)renew?
: A list of files that will be renewed by their current hash. If some elements in thefiles
are missing inrenew
, they are just subject to compare current and previous hashes to detect if changed. In such cases, the current hashes are not recorded. Rather, previous hashes are recorded by succession. (default:files
(the argument))hash?
: A function to generate a cryptographic hash for each file. Always an absolute path is given as an argument. (default:(f) => hash([f], { rootDir })
)keepRemovedFiles?
: Whether to succeed hash of a file that is previously recorded but currently non-existent on the filesystem. (default:true
)filterByExistence?
: Whether to filter out non-existent files from the result. By default, removed files are treated as changed, so included in the result. But iffilterByExistence
istrue
, they aren't included. (default:false
)reserveRecordData?
: Whether to reserve Record Data. Iftrue
,core.reserveRecordData
is called internally. If a function, not a boolean, is given, the function is called instead ofcore.reserveRecordData
. (default:true
)previousFiles?
: File-hash pair dictionary that's in the previous Record Data.
(default:(await (await getConfig()).store.getRecord<RecordData>())?.data['@haetae/utils'].files
)glob?
: Whether to enable glob pattern. (default:true
)
Usage
Let's say your project is like this.
<your-project>
# Other directories and files like package.json are omitted for brevity
├── haetae.config.js
└── targets
├── b
├── c
├── e
├── f
└── i
import { $, core, git, utils, js } from 'haetae'
export default core.configure({
commands: {
myCommand: {
run: async () => {
// ...
const changedFiles = await utils.changedFiles(['targets/*'])
console.log('changedFiles:', changedFiles)
// ...
},
},
},
})
If you run myCommand
for the first time, the result would be like this.
$ haetae myCommand
changedFiles: [
'/path/to/targets/b',
'/path/to/targets/c',
'/path/to/targets/e',
'/path/to/targets/f',
'/path/to/targets/i'
]
✔ success Command myCommand is successfully executed.
⎡ 🕗 time: 2023 May 28 11:06:06 (timestamp: 1685239566483)
⎜ 🌱 env: {}
⎜ 💾 data:
⎜ "@haetae/utils":
⎜ files:
⎜ targets/b: d9298e6da7af05e586f751d7970b2c7f24672a8ba6c9ce181dd08d7806d57577
⎜ targets/c: c1da9e80c56455de246bc51f13b08a268cfb18cda6e1cb62aeabe97296be1a96
⎜ targets/e: 68dd4ebaba3b6c6a4de18927efbe62da5ebd1bfd720e2ab73bdb3195773fff9c
⎜ targets/f: d8eb1fc8e0f5d0c6f4a710ee0bfd27eeb43eb3c9d5e57f338715bf5c5a660f36
⎜ targets/i: d7b68040b472acede5847c237f0d5a206caa4f3c4df393ac47ab5f6bd9124a9c
⎣ pkgVersion: 0.0.14
As it's the first time, there're no hashes in the previous Record Data.
So b
, c
, e
, f
and i
are all detected as changed files.
And their hashes are recorded in the new Record Data.
If we run it again immediately, no file is detected as changed. The hashes are recorded the same as well.
$ haetae myCommand
changedFiles: []
✔ success Command myCommand is successfully executed.
⎡ 🕗 time: 2023 Jun 15 00:40:28 (timestamp: 1686757228698)
⎜ 🌱 env: {}
⎜ 💾 data:
⎜ "@haetae/utils":
⎜ files:
⎜ targets/b: d9298e6da7af05e586f751d7970b2c7f24672a8ba6c9ce181dd08d7806d57577
⎜ targets/c: c1da9e80c56455de246bc51f13b08a268cfb18cda6e1cb62aeabe97296be1a96
⎜ targets/e: 68dd4ebaba3b6c6a4de18927efbe62da5ebd1bfd720e2ab73bdb3195773fff9c
⎜ targets/f: d8eb1fc8e0f5d0c6f4a710ee0bfd27eeb43eb3c9d5e57f338715bf5c5a660f36
⎜ targets/i: d7b68040b472acede5847c237f0d5a206caa4f3c4df393ac47ab5f6bd9124a9c
⎣ pkgVersion: 0.0.14
After then, you made some changes on the project.
<your-project>
# Other directories and files like package.json are omitted for brevity
├── haetae.config.js
└── targets
├── a
├── b
├── c
└── d
a
and d
are newly created.
e
, f
, and i
are removed.
You modified the config file as well.
// Other content is omitted for brevity.
const changedFiles = await utils.changedFiles(['targets/{a,b,c,d,e,f,h}'], {
renew: ['c', 'd', 'f', 'g']
})
So, the positional argument files
is ['targets/{a,b,c,d,e,f,h}']
,
and the named option renew
is ['c', 'd', 'f', 'g']
.
What would be the result if we run the command again?
$ haetae myCommand
It will be partially different relying on options
and whether the content is changed.
The rule to determine the result is like this. This example covers every possible scenario.
a
: Hash is not recorded, as it's not a target torenew
. Detected as changed, because it's not in the previous Record Data, but exists on the filesystem currently.b
: Previous hash is recorded as it's not a target torenew
. Detected as changed if the current and previous hashes are different.c
: Current hash is recorded. Detected as changed if the current and previous hashes are different.d
: Current hash is recorded. Detected as changed, because it's not in the previous Record Data, but exists on the filesystem currently.e
: Previous hash is recorded ifoptions.keepRemovedFiles
istrue
(default). Detected as changed ifoptions.filterByExistence
isfalse
(not default).f
: Hash is not recorded, as it's a target torenew
and doesn't exist on the filesystem currently. Detected as changed ifoptions.filterByExistence
isfalse
(not default).g
: Hash is not recorded. Not detected as changed.h
: Hash is not recorded. Not detected as changed.i
: Hash is not recorded. Not detected as changed.