Making a command for a package (`bin`)

cli

(David Piepgrass) #1

What I Wanted to Do

The documentation gives the impression that one can simply add a section like "bin" : { "command" : "./cli.js" } to package.json to associate a package’s command-line interface with "command". So I used:

"bin": {
  "testpack": "./dist/index.js",
  "testpack-cli": "./dist/index.js"
},

This didn’t work for me. During installation npm produces an incorrect shell script to run my .js code. I checked the package.json of one of the other packages I had installed and it seems to use "bin" in the same way, so I don’t know what’s wrong in my case.

What Happened Instead

npm produced this cmd script…

@"%~dp0\..\testpack-cli\dist\index.js"   %*

Whereas other modules get a script like this:

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\typescript\bin\command" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\..\typescript\bin\command" %*
)

Reproduction Steps

My repo is https://github.com/qwertie/testpack.git - I suppose you could repro with the following steps

  • clone it
  • npm pack
  • make a test folder and cd to it
  • npm init -f
  • npm install testpack-cli-1.0.2.tgz
  • Examine the script ./node_modules/.bin/testpack.cmd

Platform Info

$ npm --versions
{ simplertime: '1.0.4',
  npm: '6.1.0',
  ares: '1.14.0',
  cldr: '33.0',
  http_parser: '2.8.0',
  icu: '61.1',
  modules: '64',
  napi: '3',
  nghttp2: '1.32.0',
  node: '10.4.1',
  openssl: '1.1.0h',
  tz: '2018c',
  unicode: '10.0',
  uv: '1.20.3',
  v8: '6.7.288.45-node.7',
  zlib: '1.2.11' }

$ node -p process.platform
win32

(Rebecca Turner) #2

Bins must have a line at the top instructing that the bin be run with node. It should look like this:

#!/usr/bin/env node

This should be the first line of your bin. I know this looks weird, it’s a Unix thing, and on Mac and Linux it makes that js file directly executable. On Windows we read it and generate the script quoted above.

You’re using typescript and I don’t know how you would get that line at the top of your js output from typescript. You may be able to just include that at the top of your .ts source files?

Edited to add: It looks like you can in fact just add it to your .ts files: https://github.com/Microsoft/TypeScript/pull/4120

The other option is wrappers, like typescript itself uses: https://github.com/Microsoft/TypeScript/blob/master/bin/tsc


(David Piepgrass) #3

Well, people expect shebangs to affect shells. it’s not at all obvious that a shebang would change the behavior of npm. Clearly I’m not adding a shebang for the shell’s sake, since the shell is never asked to run the file even on Linux (anyone who inspects the npm-generated scripts can see this).

This is not in the documentation and it should be.

I can confirm that adding the shebang fixed the problem.


(Kat Marchán) #4

Totally a thing that would be great to get a docs patch for!


(Rebecca Turner) #5

Shebangs are pretty misunderstood.

They’re actually a feature of Unix-like kernels, not shells or even libc. When you tell the kernel to exec a file, it looks for the shebang, and if it finds one, executes that program with the file it was told to exec as an argument.

This kind of thing is really convenient because it means you can make any file with source code in it for a language that has an interpreter can be made executable by anything. The underlying system calls do the processing and the calling program doesn’t need to know anything about this. This is how Node.js came to ignore the shebang at the start of a JS file.

But Node.js supports Windows, and Windows does not implement anything like this in its kernel. As a result, we can’t just install the JS file directly as an executable. Windows has no facility for that. So we parse the shebang in your JS file and generate these .cmd wrappers that try to replicate the behavior of Unix-like kernels for Windows.

Fun fact about shebangs, it’s perfectly valid for them to be at the start of binary data. The kernel stops looking after the newline. You can slap #!/usr/bin/env java -jar at the start of a jar to make it directly executable on Mac and Linux, for example.


(David Piepgrass) #6

That’s very interesting, iarna, thank you. But my confusion arose not due to Windows, but because npm doesn’t try to execute js files on Linux either - all scripts in node_modules/.bin invoke node on the js file, giving the impression that a shebang would be superfluous.


(Rebecca Turner) #7

But npm does execute js files on Linux (and Mac), it doesn’t run anything with node explicitly.

Edited to add: You’ll note that they’re just symlinks on Linux and Mac. And if you don’t set them executable and give them a shebang, they don’t work.


(Rebecca Turner) #8

The only time npm will inject node into execution for you is the default start lifecycle script which is generated if you have a server.js file, which defaults to node server.js.


(David Piepgrass) #9

Eh? So why do I see what appear to be bash scripts like this node_modules/.bin/uglifyjs?

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../uglify-js/bin/uglifyjs" "$@"
  ret=$?
else 
  node  "$basedir/../uglify-js/bin/uglifyjs" "$@"
  ret=$?
fi
exit $ret

(Rebecca Turner) #10

Because that’s still only done on Windows. That’s done to support MSYS2 and git bash, because even though MSYS2 provides a full bash environment, it still doesn’t support symlinks for ordinary users. Those wrappers are created by cmd-shim.

(Windows does support symlinks, but you have to use elevated privileges to do so.)


(system) #11

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.