Node.js, Bash Scripts and Directory-local node binary - node.js

In our environment, we're building a build/util tool to be used by many users without the need for external (or system-wide) dependencies. Namely, we're attempting to build this so that users won't have to be concerned with which version of node.js they have installed globally, as we're packing the node.js binary within the tool directory. (Please refrain from commenting on the merits of such an approach).
Our tool structure is as such:
/web/tool/
/web/tool/bin/ (where the node binary lives)
/web/tool/node_modules
We have /web/tool/bin added to the $PATH variable (first in order) upon execution of any of the scripts or grunt.js tasks to ensure that the local binaries path trumps any other.
Upon execution of any bash scripts or grunt.js tasks, the initial task locates the proper node binary. This was tested in an environment without node installed globally, so we knew it was finding the directory-local node binary. Any subsequent processes spawned from within these scripts/grunt-tasks however, are looking globally for the node binary.
Our specific user-case is running karma and phantomjs via a grunt task. The bash scripts/bins for karma, grunt-cli (grunt), and phantomjs all have the familiar header directive of
#!/usr/bin/env node
How should we go about setting up so that our tool is always looking for the directory-local node binary, even in subsequent child-processes?

Definitely try creating a wrapper script to set environment variables to pass on to child classes:
node ./wrapper.js (you write/execute this one)
#!/usr/bin/env node
console.log(process.env.NODE); // "/usr/local/bin/node"
process.env.NODE = "/web/tool/bin/node";
require('child_process').exec('node ./child.js', {
'cwd': __dirname,
'env': process.env
}, function(err, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (err !== null) {
console.log('exec error: ' + error);
}
});
child.js (or whatever other script you have, note that it does not need to be modified)
#!/usr/bin/env node
console.log(process.env.NODE);
Output:
$ node env.js
/usr/local/bin/node
stdout: /web/tool/bin/node (from child.js)
stderr:

Related

Problem when using process.cwd() in published npm package

I'm dipping my toes into cli tooling by building a simple program for automating Gerrit commits. Everything works locally, but after publishing the package to npm and installing it globally it looks like process.cwd() behaves differently. The program exits, but no console.log(). Even a simple console.log(process.cwd()) is ignored (again works locally). Is it possible to use process.cwd() when running a globally installed npm package?
console.log(process.cwd());
const getCurrentBranchName = (p = process.cwd()) => {
const gitHeadPath = `${p}/.git/HEAD`;
return fs.existsSync(gitHeadPath)
? fs.readFileSync(gitHeadPath, "utf-8").trim().split("/").pop()
: (console.log("not a git repo"), process.exit(0));
}
const currentBranch = getCurrentBranchName();
When ran locally (with node index):
$ /Users/jpap/.npm-packages/lib/node_modules/gittest
$ not a git repo
You haven't proved the problem is with process.cwd()
The code in your question and the results your describe only indicate that the console.log() calls aren't executing.
You could easily test this by adding the following to the top of your code:
console.log('My name is Inigo Montoya. You killed my father. Prepare to die!')
What you are publishing is likely not the same as what you are running locally
For example, you could be using a bundler (e.g. Rollup) configured to strip console.log calls.
This is easy to confirm. Simple look at the code of the npm installed version:
Use npm root -g to find out where your global packages are installed. For non-global packages, look in node_modules.
Find your package's subdir.
Look at the code, or diff it with the source code.
I suspect you will see all console.log statements removed.

How can I execute node cli script from another node.js script/module?

I have a node library with multiple scripts (ES modules to be precise). One of the module, when invoked, uses exec function from built-in child_process module. I need to run ts-node command using the exec function.
The problem is that ts-node is not recognized as exec doesn't do the path manipulation magic that npm scripts do by appending the node_modules/.bin to the path variable. Further, I cannot rely on __dirname and __filename special values as I use ESM and not CJS (I know there is a way to get these values but I would rather prefer if there is any standard approach to execute commands with the semantics of npm scripts).
So, how can I execute ts-node command from my own script which could be installed anywhere (as a nested dependency)?

Install different packages depending on node version

I have a project that depends on the websocket package. However, for node 10.x, the latest version (1.0.31) of websocket works, while on node 4.x version 10.0.24 works but the 10.0.31 does not. Is it possible to specify different package (versions) per nodejs version to handle cases like this, e.g. like so
node 4.x and older -> websockets 1.0.24
all other node versions -> websockets 1.0.31
Preferable it should work in both npm and yarn, but if it only works in either that's fine as well.
The node 10 version is used in dev setups, while the node 4.x is used in a legacy embedded platform that cannot run docker or be upgraded.
Consider utilizing a postinstall script in the scripts section of your projects package.json. For instance:
package.json
"scripts": {
"postinstall": "node install-websocket"
},
As you can see, the postinstall script invokes a nodejs script, arbitrarily named install-websocket.js.
install-websocket.js
const execSync = require('child_process').execSync;
const nodeMajorVersion = process.version.replace(/^v/, '').split('.')[0];
const websocketVersion = nodeMajorVersion <= '4' ? '1.0.24' : '1.0.31';
execSync('npm install websocket#' + websocketVersion, {
cwd: __dirname,
stdio: 'inherit'
});
The install-websocket.js script essentially performs the following tasks:
Gets the version of node.js using process.version which returns a string, e.g. v13.10.1
To obtain the Major version from that string (i.e. 13 in that aforementioned example) we use a combination of the replace() and split() methods.
The conditional (ternary) operator ascertains
which version of websocket to subsequently install - based on whether the value of nodeMajorVersion is <= 4.
Finally we "shell out" the appropriate npm install websocket#x.x.x command using execSync.
Note: If you're concerned about execSync being synchronous, then utilize the asynchronous exec instead.
Additional Notes:
Given the code shown above it assumes the install-websocket.js file resides in the root of your project directory, i.e. at the same level as package.json.
my-project
├── package.json
├── install-websocket.js
└── ...
It's important for the install-websocket.js file to exist at this location for following two reasons:
Primarily, and most importantly, because you'll have noticed that we specify __dirname for the value of execSync's cwd option. The value of __dirname in this context is the pathame to the parent directory of wherever the install-websocket.js file resides.
Essentially by setting the cwd option to this specific pathname, (i.e. the path to the project directory), we ensure that when the npm install websocket#x.x.x command is run it gets installed in the same location as where your project resides - regardless of whether it's installed locally or globally.
The postinstall script in package.json expects the install-websocket.js file to reside there too. Note how it currently runs; node install-websocket, and doesn't assume the file exists elsewhere, i.e. it's not running something like: node ./some/path/to/install-websocket
If consumers of your package have npm configured to ignore-scripts then websocket's simply will not be installed because the postinstall script will not be invoked.

Using nodejs's spawn causes "unknown option -- " and "[Error: spawn ENOENT]" errors

I'm trying to get spawn to effect an rm -rf node_modules followed by npm install (on windows 7; nx commands courtesy of a transparently installed CygWin. All nx commands resolve on a commandline just fine).
I initially had this using exec, but wanted to catch the stdout/stderr information as it occurred, so I figured I'd use spawn, and rewrote the code to use that. However, that breaks everything.
The rm command, rewritten, became this:
var spawn = require("child_process").spawn,
child = spawn("rm", ["-rf", "node_modules"]);
child.stdout.on('data', function (data) { console.log(data.toString()); });
child.stderr.on('data', function (data) { console.log(data.toString()); });
child.on('error', function() { console.log(arguments); });
However, running this generates the following error:
rm: unknown option -- ,
Try `rm --help' for more information.
The npm command, rewritten, became this:
var spawn = require("child_process").spawn,
child = spawn("npm", ["install"]);
child.stdout.on('data', function (data) { console.log(data.toString()); });
child.stderr.on('data', function (data) { console.log(data.toString()); });
child.on('error', function() { console.log(arguments); });
However, running this generates the following error:
{
'0': {
[Error: spawn ENOENT]
code: 'ENOENT',
errno: 'ENOENT',
syscall: 'spawn'
}
}
How do I make spawn run the same commands that worked fine using exec without it throwing up errors all over the place? And why does this not work? Reading the API, http://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options, seems to suggest this is precisely how one is supposed to use spawn...
After lots of trying different things, I finally had a look at what "npm" actually is on windows, and it turns out to be a bash script called npm, as well as a windows-native batch script called npm.cmd (no idea why it's .cmd, that should be .bat, but there you have it). Windows's command resolver will see npm, notice that it's not an executable, see npm.cmd, and then notice that IS an executable, and will then use that instead. This is helpful when you're in a terminal, but spawn() will not do any such resolution: passing it npm will make it fail because it's not an executable. Passing it npm.cmd as command, however, works just fine.
(Also, not sure why rm was failing earlier, since that actually works correctly without any changes that I can tell. Probably misread that as part of the problem when in fact it wasn't.)
So: if you run into spawn saying ENOENT in windows, when the command you're trying to trigger works in a plain command prompt, find out if the command you're calling is a true executable, or whether there's a .bat/.cmd file that the command prompt will "helpfully" run for you instead. If so, spawn that.
edit
since this post is still getting upvotes, a good way to ensure the command always works is to bootstrap it based on process.platform, which will be win32 for windows.
var npm = (process.platform === "win32" ? "npm.cmd" : "npm"),
child = spawn(npm, ["install", ...]);
...
edit specific to the use-case that triggered this error
since posting this question (and its answer), several packages have been released that allow running npm tasks without having to rely on exec or spawn, and you should use them instead.
Probably the most popular is npm-run-all which doesn't just give you the power to run any npm task from other npm scripts as well as from Node, but also adds commands to run multiple npm scripts in series or in parallel, with or without wildcards.
In the context of the original question, where the error got thrown because I was trying to run npm as an exec/spawn in order to effect a cleanup and reinstall, the modern solution is to have a dedicated cleaning task in package.json:
{
...
"scripts": {
"clean": "rimraf ./node_modules",
...
},
...
}
And to then invoke that clean task followed by the install command on the command line as
> npm run clean && npm install
Or, from inside some Node script, using:
const runAll = require("npm-run-all");
...
runAll(["clean", "install"])
.then(() => {
console.log("done!");
})
.catch(err => {
console.log("failed!");
});
(Or of course, as a compound script in package.js, e.g. "redo": "run-s clean install" and then using runAll(["redo"]))
I think this may be some sort of cygwin gotcha. I'm running Ubuntu 12.04 and tried to duplicate your problem, but it works just fine for me. In short, I don't see anything you are doing wrong.
If it is complaining about the option, maybe split it up into multiple options like so:
child = spawn("rm", ["-r", "-f", "node_modules"]);
That's kind of a hail mary, but that works on my Ubuntu 12.04 as well. You might try to just delete a single file and see if you get the same thing.
child = spawn("rm", ["/home/username/Desktop/TestFile"]);
If that still fails, then you know you are working against some crazy stuff.
You could even try to just execute a command with no parameters like so:
child = spawn("ls");
If that still fails, you aren't likely to get spawn to work at all would be my guess and be grateful that at least exec is working.
Not much in the realm of answers for you, but like I said, I can't see anything you are doing incorrectly.
Furthermore, I don't see how your npm command is going to work because you aren't specifying what to install, but that being said, it fails in a different way than I'm seeing it fail here if I use the same command. . . I see lots of stderr output, not an overall error.
BTW, I'm running node v0.8.21. You can query that by node -v. If you are running another version, maybe give 0.8.21 a try.
Use full path for the process, like:
var cmd = require('child_process').spawn("C:\\windows\\system32\\cmd.exe");

How to install npm package from nodejs script?

How to install npm package from nodejs script?
Question is not about simple installation npm packages via terminal,
it is about installation via nodejs script:
Not about this: npm install express, but about having install.js file with content npm install express, which I will execute like node install.js and after this it will locally install express module in this folder.
Sorry, but Google and DuckDuckGo are not my friends today(
The main problem is in automatic local installation required packages for my small utility, because global packages are not working in windows.
Check out commander.js it allows you to write command line apps using node.
Then you can use the exec module.
Assuming you put the following in install.js, you just have to do: ./install.js and it will run npm install for you.
#!/usr/bin/env node
var program = require('commander');
var exec = require('child_process').exec;
var run = function(cmd){
var child = exec(cmd, function (error, stdout, stderr) {
if (stderr !== null) {
console.log('' + stderr);
}
if (stdout !== null) {
console.log('' + stdout);
}
if (error !== null) {
console.log('' + error);
}
});
};
program
.version('0.1.3')
.option('i, --install ', 'install packages')
.parse(process.argv);
if (program.install) {
run('npm install');
}
var count = 0;
// If parameter is missing or not supported, display help
program.options.filter(function (option) {
if(!(option.short == process.argv[2]))
count++
});
if(count == program.options.length)
program.help();
Hope this helps!
NOTE: I don't think this fulfills all the requirements of your question, because at the end you state that you can't find npm...so maybe your question would be better titled "How to install npm package without npm?"--yikes! But it addresses the title, "How to install npm package from nodejs script?"
I've just been shown another alternative to do this: the module npmi. While this is still another module dependency, it does at least work without a *nix shell script environment, which I think the other answer here (about commander.js) does. And, if you look inside the code for npmi.js, you'll find it's very short and merely uses the npm module directly in the node script--which you can do yourself if you don't want to add the npmi module.
So in our case we needed a way to install modules without requiring a *nix shell script (to support Windows users), and this fits the bill nicely.
That still doesn't help you if you can't require('npm'). Only thing I can think of there is trying likely absolute paths...you can require('C:\Program Files\Node\packages\x)`, I think--or wherever node's global packages are stored (per user?). Wrap a couple of attempts in try/catch or test for the file's existence first and try to require the npm module whenever you find where the global packages are actually installed? You might tick off a malware scanner :-), but it might work.

Resources