Modern ESM + TypeScript ports of a few classic Node CommonJS modules, vendored into a single zero-dependency package.
The originals are stable but unmaintained, CJS-only, and pull in @types/* and transitive deps for code that's now smaller than its dependency tree. Vendoring + rewriting gives us:
- Pure ESM, native to Node 22+ and Bun.
- Real TypeScript types co-located with the implementation.
- No runtime npm dependencies — only Node built-ins.
All of these live in packages/node-utils/src/:
| Original (CJS) | Rewritten as |
|---|---|
proper-lockfile |
lockfile.ts (+ index.ts) |
retry |
retry.ts |
exit-hook |
exit-hook.ts |
ignore |
ignore.ts |
A proper-lockfile-equivalent file lock, with retry and exit-hook bundled in.
bun add @alchemy.run/node-utilsimport { lock, check } from '@alchemy.run/node-utils';
const release = await lock('some/file');
try {
// ...do work
} finally {
await release();
}Also exported: unlock, lockSync, unlockSync, checkSync.
Locks are acquired with mkdir (atomic on every filesystem, including NFS), and the lockfile's mtime is refreshed on an interval so staleness can be detected. If the refresh fails repeatedly, onCompromised fires.
stale— ms before a lock is considered stale. Default10000, min5000.update— ms betweenmtimerefreshes. Defaultstale / 2. Min1000, maxstale / 2.retries— number, or a retry options object. Default0.realpath— resolve symlinks. Defaulttrue(file must exist).fs— custom fs. Defaults to Node'snode:fs.onCompromised— callback. Defaults to throwing.lockfilePath— override the<file>.lockpath.
check and unlock accept the relevant subset.
Sync variants (lockSync / unlockSync / checkSync) don't accept retries.
Locks are removed automatically on process exit, except on SIGKILL or fatal VM errors. Wired up via the vendored exit-hook port.
bun install
bun test
bun run build # tsc -b
bun run format # oxfmt .MIT. Vendored sources retain their original MIT licenses; see file headers.