@actionforge/file-utils
v0.1.0
Published
Small, dependency-light filesystem helpers for build tooling — safe JSON reading, safe stat, relative-path and glob-pattern detection, and glob/directory/file partitioning
Maintainers
Readme
@actionforge/file-utils
Small, dependency-light filesystem helpers for build tooling: safe JSON
reading, safe stat, relative-path and glob detection, and turning a mixed
list of files/directories/globs into something a matcher can consume.
Why this exists
These helpers started life inside a set of GitHub Actions, where every "read
the repo's package.json" or "figure out which paths the user actually meant"
step re-implemented the same handful of edge cases. None of that logic is
actually GitHub-specific — it is the boring plumbing every build script needs.
This package has zero coupling to GitHub Actions: no @actions/* imports,
no reliance on GITHUB_* environment variables. It works the same in a CI
runner, a local CLI, or a plain Node script.
Install
npm install @actionforge/file-utilsAPI
readJSON<T>({ filepath, cwd? })
Read and parse a JSON file. Relative filepath values resolve against cwd
(default process.cwd()). Throws with a descriptive message when the file is
missing, empty, or malformed.
import { readJSON } from '@actionforge/file-utils'
const pkg = readJSON<{ name: string; version: string }>({
filepath: 'package.json',
})readJSONIfExists<T>({ filepath, cwd? })
Same as readJSON, but returns undefined when the file is absent. A file
that exists but is malformed still throws — a missing file is a normal
outcome, broken JSON is a bug worth surfacing.
const config = readJSONIfExists<MyConfig>({ filepath: '.myrc.json' }) ?? {}statSafeSync(filePath)
fs.statSync that returns null instead of throwing for a missing path. Any
other error (permissions, and so on) still propagates.
if (statSafeSync('dist')?.isDirectory()) {
// ...
}isRelative(filePath?)
true when the path is written explicitly relative to the current directory
(./… or ../…). Deliberately narrower than "not absolute": a bare
src/index.ts returns false.
isGlobPattern(pattern)
true when the string is a glob rather than a literal path. Windows backslash
separators are normalised first, so src\*.ts is recognised.
matchGlobs(globs?, { cwd? })
Partition a mixed list into cwd-relative glob patterns and cwd-relative
concrete files.
import { matchGlobs } from '@actionforge/file-utils'
matchGlobs(['src/*.ts', 'lib', 'package.json'], { cwd: process.cwd() })
// { patterns: ['src/*.ts', 'lib/**'], files: ['package.json'] }Each entry is classified independently, so an entry that is both glob-like
and an existing path (a file literally named report[1].txt) lands in both
buckets. That is intentional — callers get every interpretation.
checkWorkingDir({ workingDirectory, defaultWorkingDirectory?, onWarn? })
process.chdir into workingDirectory, unless it is the default sentinel
(no-op) or does not exist (warn and stay put). Returns whether the directory
actually changed.
checkWorkingDir({ workingDirectory: 'apps/web' })onWarn defaults to writing to stdout so this behaves sensibly outside a CI
runner. Pass your own to route it into a logger, or a no-op to silence it.
resolvePackagePaths({ names, workspaceRoot, run, onUnresolved? })
Resolve pnpm workspace package names to their working-folder paths, with a
single pnpm ls --recursive --depth=-1 --json call. Built for actions that
accept a packages input of names — e.g. an affected-packages output — but
operate on folder paths internally.
The command runner is injected, so this has no direct dependency on
child_process or any Actions runtime; a caller wires in execFile,
@actions/exec, or anything else that can run pnpm.
A name that does not match any workspace package is collected into
unresolved (and reported to onUnresolved, if given) instead of throwing —
one bad name should not fail every other package in the same run.
import { resolvePackagePaths } from '@actionforge/file-utils'
import { getExecOutput } from '@actions/exec'
const { paths, unresolved } = await resolvePackagePaths({
names: ['@scope/api', '@scope/web'],
workspaceRoot: process.cwd(),
run: (command, args) =>
getExecOutput(command, [...args], { ignoreReturnCode: true, silent: true }),
onUnresolved: (name) => core.warning(`unknown package: ${name}`),
})
// { paths: ['packages/api', 'apps/web'], unresolved: [] }License
MIT © Andrew Cates
