[Feature] Support non-JavaScript executables in "bin"
- [ ] I'd be willing to implement this feature (contributing guide)
- [x] This feature is important to have in this repository; a contrib plugin wouldn't do
Describe the user story
It's useful to be able to use npm to distribute executables that are not strictly written in JavaScript. My specific case is the sass-embedded package, which wraps a native sass executable in a JavaScript API. Providing direct, efficient access to that executable is valuable, but if I need to wrap it in a JavaScript library that produces substantial overhead both in terms of efficiency (I measure about 450ms of pure overhead on my laptop) and in terms of accuracy (Node.js is not very graceful about wrapping executables, so edge cases like signal handling often end up working weirdly).
Describe the solution you'd like
npm already supports this by allowing any executable file in the "bin" section of a package.json file. Yarn currently breaks compatibility by always attempting to run these files as Node.js source. Matching npm's behavior is the minimal solution I'd like to see.
However, npm doesn't gracefully support cross-platform packages in this way (https://github.com/npm/cmd-shim/issues/152). Having additional support for distinguishing between bin/executable and bin/executable.cmd, or explicitly specifying per-platform executables, would be a substantial value-add.
Describe the drawbacks of your solution
This does likely involve maintaining some amount of shim code for Windows, which is always a pain in the ass because of how weird cmd.exe is.
Describe alternatives you've considered
This can't be a plugin because it's intrinsic to the way Yarn installs and exposes executables in a variety of contexts. The current workaround is to write a custom JavaScript shim, but this poses the efficiency and correctness issues I outlined above. Another option could be to add a post-install script to manually set up executables in the user's directories, but this is likely to substantially step on the package manager's toes.
AFAIK we support some native executable formats as bins
Native binaries can be helpful, but without also supporting shell scripts, the value is severely limited. In my case, we're trying to invoke a pre-existing binary with a specific set of flags.
Hi, I ran into similar issues trying to run node with flags like --experimental-strip-types via Yarn bin.
In my case, I wanted to leverage Node’s native type stripping feature introduced in v22.6.0.
I don't know if this is my setup problem, but.. I found that shebangs don’t work properly when the executable exposed as bin ends with .*js extension.
Renaming the executable to use .bin extension did the trick — but then I ran into working directory issue.
I expected this script to run correctly, but it didn’t because my setup uses Yarn workspaces. The linked shell script lives in a nested workspace directory rather than the root (the cli is installed as deps of the workspace root), so the relative paths didn’t resolve as intended:
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning --experimental-strip-types src/index.ts
This resolves to
"$(pwd)/src/index.ts", which is not relative to working directory of the project.
For now, I’ve managed to get it working — regardless of the current working directory — by using a .bin shell script that delegates to the node command:
bin/cli.bin
#!/usr/bin/env sh
set -eu
BASEDIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
find_up() {
target="$1"; dir="${2:-$BASEDIR}"
while [ "$dir" != "/" ]; do
[ -e "$dir/$target" ] && { echo "$dir/$target"; return 0; }
dir=$(dirname "$dir")
done
return 1
}
node \
--disable-warning=ExperimentalWarning \
--experimental-strip-types \
--require "$(find_up .pnp.cjs)" \
--import "data:text/javascript,import{register}from'node:module';import{pathToFileURL}from'node:url';register('./.pnp.loader.mjs',pathToFileURL('$(find_up .pnp.loader.mjs)'));" \
$BASEDIR/../src/cli.ts \
"$@"
- Yarn v4.9.2
- Node.js v22.11.0