From b010c32ac13c07039bc2cf2ce06c1082f1ae7950 Mon Sep 17 00:00:00 2001 From: Finomosec <1665799+Finomosec@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:30:50 +0100 Subject: [PATCH] feat: add replaceProcess option to exec the script in place of npm Refs: npm/cli#10165 --- lib/run-script-pkg.js | 5 +++ test/replace-process.js | 70 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 test/replace-process.js diff --git a/lib/run-script-pkg.js b/lib/run-script-pkg.js index e81396c..8964b4c 100644 --- a/lib/run-script-pkg.js +++ b/lib/run-script-pkg.js @@ -67,6 +67,11 @@ const runScriptPkg = async options => { stdioString, }) + if (options.replaceProcess && stdio === 'inherit' && promiseSpawn.canExecve()) { + inputEnd() + return promiseSpawn.execve(spawnShell, spawnArgs, spawnOpts) + } + const p = promiseSpawn(spawnShell, spawnArgs, spawnOpts, { event, script: cmd, diff --git a/test/replace-process.js b/test/replace-process.js new file mode 100644 index 0000000..89302e8 --- /dev/null +++ b/test/replace-process.js @@ -0,0 +1,70 @@ +const t = require('tap') +const spawk = require('spawk') +const realPromiseSpawn = require('@npmcli/promise-spawn') + +const emptyDir = t.testdir({}) + +const mockRunScript = (t, { canExecve }) => { + const calls = [] + const promiseSpawn = Object.assign((...args) => realPromiseSpawn(...args), { + canExecve: () => canExecve, + execve: async (...args) => { + calls.push(args) + }, + }) + const runScript = t.mock('../lib/run-script.js', { + '../lib/run-script-pkg.js': t.mock('../lib/run-script-pkg.js', { + '@npmcli/promise-spawn': promiseSpawn, + }), + }) + return { runScript, calls } +} + +const options = (overrides) => ({ + event: 'npx', + path: emptyDir, + scriptShell: 'sh', + stdio: 'inherit', + cmd: 'bar', + args: ['baz qux'], + replaceProcess: true, + pkg: { scripts: {} }, + ...overrides, +}) + +t.test('replaces the process when asked to and possible', async t => { + const { runScript, calls } = mockRunScript(t, { canExecve: true }) + await runScript(options()) + t.equal(calls.length, 1) + const [cmd, args, opts] = calls[0] + t.equal(cmd, 'bar') + t.strictSame(args, ['baz qux']) + t.equal(opts.shell, 'sh') + t.equal(opts.cwd, emptyDir) + t.equal(opts.env.npm_lifecycle_event, 'npx') + t.equal(opts.env.npm_lifecycle_script, 'bar') +}) + +t.test('spawns when execve is not available', async t => { + const { runScript, calls } = mockRunScript(t, { canExecve: false }) + spawk.spawn('sh', a => a.includes("bar 'baz qux'")) + await runScript(options()) + t.strictSame(calls, []) + t.ok(spawk.done()) +}) + +t.test('spawns when stdio is not inherited', async t => { + const { runScript, calls } = mockRunScript(t, { canExecve: true }) + spawk.spawn('sh', a => a.includes("bar 'baz qux'")) + await runScript(options({ stdio: 'pipe' })) + t.strictSame(calls, []) + t.ok(spawk.done()) +}) + +t.test('spawns when not asked to replace the process', async t => { + const { runScript, calls } = mockRunScript(t, { canExecve: true }) + spawk.spawn('sh', a => a.includes("bar 'baz qux'")) + await runScript(options({ replaceProcess: false })) + t.strictSame(calls, []) + t.ok(spawk.done()) +})