@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?
:options.rootDir?
: A directory to start search. Ifoptions.globbyOptions.cwd
is not provided (undefined
), this value is used. Ignored otherwise. (default:core.getConfigDirname()
)options.globbyOptions?
: Refer to globby v13 docs (opens in a new tab).
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).options.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.options.trim
is the only option 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
}
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 offiles
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
.
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). Thedependents
anddependencies
have to be file path, not directory.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()
)
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 ofdependencies
, directly or transitively.dependencies
: A list of candidates that may be a dependency of dependent, directly or transitively.graph
: A graph. Return value ofgraph
is proper.rootDir?
: Whendependent
or an element ofdependencies
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 offiles
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 tohash
function. (default:false
)reserveRecordData?
: Whether to reserve Record Data. (default:true
)