如何将命令行参数传递给从可执行脚本启动的NodeJS

4
我如何为从启动器脚本运行的NodeJS进程设置本应是命令行参数传递给node的内容?(sh/CMD脚本npm放置在node_modules/.bin中。)
许多NodeJS库/框架都带有自己的运行器脚本,例如zeit/micromoleculer,通常从npm脚本执行。这在开发中会出现问题,因为在我的情况下,我想要执行相当于:
node --inspect -r ts-node/register -r dotenv-safe/config src/index.ts

(当然,那样做没有任何作用,因为index.ts只是导出一些内容供运行程序使用。)
有没有一种“干净”的、最好是通用的(即不特定于给定框架的运行器公开这些命令行参数)方法可以做到这一点,理想情况下是一种作为npm脚本工作的方法?唯一似乎可行的事情是例如micro:
node-dev -r ts-node/register ./node_modules/micro-dev/bin/micro-dev.js ./src/index.ts

这个来自“多余的冗余部门”的说法有点啰嗦,似乎使得拥有那些启动器脚本的意义丧失了。 (如果运行程序会产生其他Node进程,则也无法使用它,但我实际上没有这个问题。)我不想重复启动器脚本已经在做的事情。 我也知道npx--node-arg,但npx是一个完全不同的问题。(在Windows上,它需要五秒钟的启动时间和一个虚假的错误消息才能运行我已经安装的脚本;如果找不到其.cmd启动器脚本,例如在使用Docker运行dev环境时,它也无法找到已安装的软件包。总之,我宁愿不使用npx来解决这个问题。)


为了澄清评论中似乎会出现的混淆:我想要覆盖影响执行运行脚本时NodeJS运行时本身行为的命令行参数,而不是将参数传递给脚本本身或我的代码。也就是说,这里列出的选项:https://nodejs.org/api/cli.html。请注意,保留HTML标签。

我不确定我理解了,但实际上您想要覆盖在调用脚本时通过child_process.execchild_process.spawn调用传递的命令行参数吗? - Catalyst
@Catalyst 不是指脚本被生成的命令行参数,而是NodeJS运行时的命令行参数。 - millimoose
所以我只是在确认一下 - 一个例子就是能够执行 node -r overwrite-da-stuff.js sample.js 并且在 overwrite-da-stuff.js 中指定一些自定义的 node.js 命令行参数来调用 sample.js?如果是这样的话,那么它类似于能够从应用程序代码中启用 --inspect 吗?如果是这样的话,我怀疑你无法打败子进程...否则它只会允许特定的标志,这些标志不会造成安全风险。 - Catalyst
@Catalyst - 我认为你很接近答案,但似乎不太明白 -r 的作用?它会在执行指定脚本之前让 node 运行时 require() 一个模块。在我的示例中,ts-node/register 是一个模块,当被 require 时,注册 ts-node 为脚本加载器,使得直接加载 TypeScript 文件成为可能,动态编译它们,而无需提前手动完成这些。类似的还有 dotenv,它会加载当前文件夹中的 .env 文件,并将其内容添加到 process.env 中。 - millimoose
我想要的是不必重复NPM的启动脚本所做的事情,而通过命令行或脚本自己生成一个节点进程就可以解决这个问题。 - millimoose
1
我现在明白了,我在说话之前确实查看了-r的含义,只是不确定它如何与你所问的问题相关。 - Catalyst
3个回答

2

一种选择是编写一个小的包装脚本,使用当前进程的 execPath 来运行 child_process.execFile。

因此,这里的示例是能够执行以下命令:

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

但实际上不需要将其写出来,而是让 wrap.js 注入参数:

node ./wrap.js ./test.js

我在 package.json 中通过 npm 运行它,并且它可以正常工作。我通过让 some-module.js 在全局对象上设置一个值,然后在 test.js 中记录它来测试它是否正常工作。

涉及的文件:

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)

编辑:经过进一步思考,这个解决方案只适用于包装初始运行程序。但是大多数工具(如mocha)会重新生成子进程,从而失去此效果。要真正完成工作,您可以代理每个子进程调用,并在某种程度上强制调用spawn等也包括您的参数。

我重写了代码以反映这一点。这里是一个新的设置:

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();
  })
})

请勿将这样的代码放入已发布的节点模块中。修改全局库函数非常糟糕。

如果NPM有某种API可以找到包的入口路径以便运行脚本,那么这实际上非常接近,我稍后会尝试一下。 - millimoose
@millimoose 添加了一个更健壮的示例,应该支持大多数运行器。 - Catalyst

1
在您的Node.js应用程序之后通过命令行传递的所有内容都会被解析为一个名为process.argv的数组。因此...
node myapp.js foo bar hello 5000

在你的Node.js代码中...
const args = process.argv;
console.log(args[0]);
console.log(args[1]);
console.log(args[2]);
console.log(args[3]);

将产生...

foo
bar
hello
5000

这不是我想做的事情,我想影响NodeJS运行时本身的选项,而不是最终将被执行的代码。当我实际上没有直接使用node可执行文件时,我想要更改这些选项 - millimoose

1

我不清楚您的问题场景,但根据您的问题标题,我们可以使用npm库执行任何cmd命令。

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)
})  

不完全正确。你所描述的是我提到的运行脚本可能正在做的事情,但我并不是在做这个。我的代码只是一个导出到某个处理管道的框架的函数。 - millimoose

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接