@cogs/node-pkg
v0.1.0
Published
Resolve and spawn a node_modules/.bin binary from a package.json bin field, walking the directory tree up and down — makes hoisted-binary proxy scripts work correctly in a monorepo
Readme
@cogs/node-pkg
Resolve and spawn a node_modules/.bin binary from a package's package.json
bin field, walking the node_modules directory tree up and down the same
way require() does.
Why
In a pnpm/turbo monorepo (or any setup with hoisted node_modules), a
package's real binary can end up several directories away from the package
that wants to run it — hoisted to the workspace root, nested under a scoped
dependency, or resolved differently depending on where the calling script
lives on disk. A naive require.resolve('some-cli/package.json') or a
hardcoded ../../node_modules/.bin/some-cli path breaks the moment the
hoisting layout shifts.
This package hooks into Node's own internal module-resolution logic
(Module._findPath / Module._nodeModulePaths) to resolve a package
correctly regardless of how deep it's nested, then (via pkgBin) spawns its
bin entry as a child process with stdio: 'inherit' and a NODE_PATH
pointed at the resolved package's own node_modules, forwarding the current
process's CLI arguments and signals.
This is exactly what makes a bin-proxy script like a bin/wdio.js shim work
correctly: instead of shelling out to a binary path that only works from a
predictable location, the proxy script resolves the real, currently-hoisted
binary and re-executes it in-place.
Usage
pkgBin — resolve and spawn a binary
#!/usr/bin/env node
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { pkgBin } from '@cogs/node-pkg/pkg-bin'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
pkgBin('@wdio/cli/package', 'wdio', {
baseDir: path.join(__dirname, '../'),
})pkgBin(pkgRoute, binName, opts?) resolves pkgRoute (a path to a
package.json, e.g. '@wdio/cli/package'), reads its bin field (a string
or a map of bin-name to script path), and spawns the resolved script as a
child process — forwarding process.argv.slice(2), SIGINT, and the child's
exit code/signal back to the parent process.
pkgUp — resolve a package's absolute location
import { pkgUp } from '@cogs/node-pkg/pkg-up'
const { path, base, cwd } = pkgUp('@wdio/cli/package')pkgUp(pkgRoute, opts?) resolves pkgRoute and returns
{ cwd, base, path } — the absolute path to the resolved file plus its
directory and containing base path. Throws (with a .fileName property set
to pkgRoute) when the route can't be resolved anywhere in the lookup chain.
ModuleResolver — the underlying resolver
import { ModuleResolver } from '@cogs/node-pkg/module-resolver'
const resolver = new ModuleResolver({ lookupPaths: [...] })
const resolved = resolver.resolve('some-package', '/extra/priority/path')Both pkgBin and pkgUp are built on ModuleResolver, which wraps Node's
internal Module._findPath to search a list of lookupPaths (highest
priority first, with an optional extraLookupPath searched before all of
them) and returns the resolved absolute file path, or null if nothing
matched anywhere in the chain.
Both pkgBin and pkgUp accept:
paths— module lookup paths (defaults to the calling module's ownnode_modulesresolution chain, walking up from its directory).baseDir— an absolute path searched with the highest priority.
