4

How to set what would otherwise be command-line arguments to node for a NodeJS process run from a launcher script? (The sh/CMD scripts npm places into node_modules/.bin.)

Plenty of NodeJS libraries / frameworks come with their own runner script, e.g. zeit/micro or moleculer that's usually executed from a npm script. This presents a problem in development, since in my case I want to do the equivalent of:

node --inspect -r ts-node/register -r dotenv-safe/config src/index.ts

(Except, of course, that does nothing since index.ts just exports something for the runner to pick up.)

Is there some "clean", preferably generic (i.e. not specific to a given framework's runner exposing those command line params) way that I'm missing to do this, ideally one that works as a npm script? The only thing that seems like it would work would be for e.g. micro:

node-dev -r ts-node/register ./node_modules/micro-dev/bin/micro-dev.js ./src/index.ts

which is kind of a mouthful from the Redundant Department of Redundancy Department and seems to obviate the point of having those launcher scripts. (It also won't work if the runner spawns other Node processes, but that's not a problem I'm actually having.) I'd like to not have to duplicate what the launcher scripts are already doing. I'm also aware of npx having --node-arg but npx is a whole another can of worms. (On Windows it's five seconds of startup time and one spurious error message just to run a script I already have installed; it also won't find an already installed package if it can't find its .cmd launcher script, e.g. when using Docker to run the dev environment. In short I'd rather not use npx for this.)


To clear up the confusion that seems to crop up in the comments: I want to override the command line parameters that affect the behaviour of the NodeJS runtime itself executing the runner script, not pass parameters to the script itself or to my code. That is, the options listed here: https://nodejs.org/api/cli.html

6
  • I'm not sure I understand, but effectively you want to override the command line params being passed by a child_process.exec or child_process.spawn call within a script you are calling? Commented Apr 11, 2018 at 23:30
  • @Catalyst No, the command line params to the NodeJS runtime that script is being spawned with. Commented Apr 12, 2018 at 10:46
  • so just checking that I get it - an example would be to be able to do node -r overwrite-da-stuff.js sample.js and have sample.js be invoked with some custom node.js command line params specified within overwrite-da-stuff.js? If that is the case it would be similar to being able to turn on --inspect from within application code? If so, I doubt you can beat child process... otherwise it is going to be only allowed for specific flags which do not pose security risks. Commented Apr 13, 2018 at 1:20
  • @Catalyst - I think you're close, but not really aware of what -r does? It makes the node runtime require() a module before it runs the script you specify. In my example, ts-node/register is a module that when required, registers ts-node as a script loader, making it possible to load TypeScript files directly, compiling them on the fly, without having to do so yourself up-front. Similar is dotenv, which loads a .env file in the current folder and adds its contents to process.env. Commented Apr 13, 2018 at 10:53
  • 1
    I get it now, I did check what -r does before saying what I said, I just wasn't sure how it was playing into what you are asking. Commented Apr 14, 2018 at 4:26

3 Answers 3

2

One option is to write a little wrapper script that uses the current process execPath to run child_process.execFile.

So the sample here is to be able to do

node --expose-http2 --zero-fill-buffers -r ./some-module.js ./test.js

but not actually write that out, instead have wrap.js inject the args:

node ./wrap.js ./test.js

I tested running this via npm in a package.json, and it works fine. I tested that it was working by having some-module.js stick a value on the global object, and then logging it in test.js.

Files involved:

wrap.js

const child_process = require('child_process');

const nodeArgs = ['--expose-http2', '--zero-fill-buffers', '-r', './some-module.js'];
const runTarget = process.argv[2];

console.log('going to wrap', runTarget, 'with', nodeArgs);

const finalArgs = nodeArgs.concat(runTarget).concat(process.argv.slice(2));

const child = child_process.execFile(
  process.execPath,
  finalArgs,
  {
    env: process.env,
    cwd: process.cwd(),
    stdio: 'inherit'
  }, (e, stdout, stderr) => {
    console.log('process completed');
    if (e) {
      process.emit('uncaughtException', e);
    }
  });

child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

and

some-module.js

global.testval = 2;

and

test.js

console.log('hi guys, did the wrap work?', global.testval)

EDIT: So upon further thought, this solution really only satisfies wrapping the initial runner. But most tools, such as mocha re-spawn a sub process which would then lose this effect. To really get the job done, you can proxy each of the child process calls and somewhat enforce that calls to spawn and such also include your args.

I rewrote the code to reflect this. Here's a new setup:

package.json

{
  "scripts": {
    "test": "node -r ./ensure-wrapped.js node_modules/mocha/$(npm view mocha bin.mocha) ./test.js"
  },
  "dependencies": {
    "mocha": "^5.1.0"
  }
}

ensure-wrapped.js

const child_process = require('child_process');

// up here we can require code or do whatever we want;

global.testvalue = 'hi there'
const customParams = ['--zero-fill-buffers'];

// the code below injects itself into any child process's spawn/fork/exec calls
// so that it propogates

const matchNodeRe = /((:?\s|^|\/)node(:?(:?\.exe)|(:?\.js)|(:?\s+)|$))/;
const ensureWrappedLocation = __filename;

const injectArgsAndAddToParamsIfPathMatchesNode = (cmd, args, params) => {
  params.unshift(...customParams);
  params.unshift(args);
  if (!Array.isArray(args)) { // all child_proc functions do [] optionally, then other params
    args = []
    params.unshift(args);
  }

  if (!matchNodeRe.test(cmd)) {
    return params;
  }

  args.unshift(ensureWrappedLocation);
  args.unshift('-r');

  return params;
}

child_process._exec = child_process.exec;
child_process.exec = (cmd, ...params) => {
  // replace node.js node.exe or /path/to/node to inject -r ensure-wrapped.js ...args..
  // leaves alone exec if it isn't calling node
  cmd = cmd.replace(matchNodeRe, '$1 -r ' + ensureWrappedLocation + ' ');
  return child_process._exec(cmd, ...params)
}
child_process._execFile = child_process.execFile;
child_process.execFile = (path, args, ...params) => {
  params = injectArgsAndAddToParamsIfPathMatchesNode(path, args, params);
  return child_process._execFile(path, ...params)
}
child_process._execFileSync = child_process.execFileSync;
child_process.execFileSync = (path, args, ...params) => {
  params = injectArgsAndAddToParamsIfPathMatchesNode(path, args, params);
  return child_process._execFileSync(path, ...params);
}
child_process._execSync = child_process.execSync;
child_process.execSync = (cmd, ...params) => {
  cmd = cmd.replace(matchNodeRe, '$1 -r ' + ensureWrappedLocation + ' ');
  return child_process._exec(bin, ...args)
}
child_process._fork = child_process.fork;
child_process.fork = (module, args, ...params) => {
  params = injectArgsAndAddToParamsIfPathMatchesNode(process.execPath, args, params);
  return child_process._fork(module, ...params);
}
child_process._spawn = child_process.spawn;
child_process.spawn = (cmd, args, ...params) => {
  params = injectArgsAndAddToParamsIfPathMatchesNode(cmd, args, params);
  return child_process._spawn(cmd, ...params)
}
child_process._spawnSync = child_process.spawnSync;
child_process.spawnSync = (cmd, args, ...params) => {
  params = injectArgsAndAddToParamsIfPathMatchesNode(cmd, args, params);
  return child_process._spawnSync(cmd, ...params);
}

test.js

describe('test', () => {
  it('should have the global value pulled in by some-module.js', (done) => {
    if (global.testvalue !== 'hi there') {
      done(new Error('test value was not globally set'))
    }
    return done();
  })
})

Please never put code like this into a node module that's published. modifying the global library functions is pretty bad.

Sign up to request clarification or add additional context in comments.

2 Comments

This is actually pretty close if NPM has some sort of API to find out the path to the entrypoint of a package for the script to run, I’ll poke at it later
@millimoose added a more robust sample that should support most runners
1

Everything passed in the command line AFTER your nodejs application is parsed into an array called process.argv. So...

node myapp.js foo bar hello 5000

In your nodejs code...

const args = process.argv;
console.log(args[0]);
console.log(args[1]);
console.log(args[2]);
console.log(args[3]);

would yield...

foo
bar
hello
5000

1 Comment

This is not what I'm trying to do, I'm trying to affect the options of the NodeJS runtime itself, not my code that will ultimately get executed. I want to change these options when I'm not actually using the node executable directly.
1

I didnt get clear scenario of your problem,but as your question title ,we can execute the any cmd command from nodejs using npm libraries like:

import Promise from 'bluebird'
import cmd from 'node-cmd'

const getAsync = Promise.promisify(cmd.get, { multiArgs: true, context: cmd })

getAsync('node -v').then(data => {
  console.log('cmd data', data)
}).catch(err => {
  console.log('cmd err', err)
})  

1 Comment

Not quite. What you described is what the runner scripts I mention might be doing, but I'm not the one doing it. My own code is just a function exported to some framework that handles the plumbing.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.