@npmcli/run-script
Advanced tools
Comparing version
/* eslint camelcase: "off" */ | ||
const isWindows = require('./is-windows.js') | ||
const setPATH = require('./set-path.js') | ||
const {resolve} = require('path') | ||
const { resolve } = require('path') | ||
const npm_config_node_gyp = require.resolve('node-gyp/bin/node-gyp.js') | ||
@@ -11,31 +10,32 @@ | ||
path, | ||
scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh', | ||
scriptShell = true, | ||
binPaths, | ||
env = {}, | ||
stdio, | ||
cmd, | ||
stdioString = false, | ||
args = [], | ||
stdioString, | ||
} = options | ||
const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(scriptShell) | ||
const args = isCmd ? ['/d', '/s', '/c', cmd] : ['-c', cmd] | ||
const spawnEnv = setPATH(path, binPaths, { | ||
// we need to at least save the PATH environment var | ||
...process.env, | ||
...env, | ||
npm_package_json: resolve(path, 'package.json'), | ||
npm_lifecycle_event: event, | ||
npm_lifecycle_script: cmd, | ||
npm_config_node_gyp, | ||
}) | ||
const spawnOpts = { | ||
env: setPATH(path, { | ||
// we need to at least save the PATH environment var | ||
...process.env, | ||
...env, | ||
npm_package_json: resolve(path, 'package.json'), | ||
npm_lifecycle_event: event, | ||
npm_lifecycle_script: cmd, | ||
npm_config_node_gyp, | ||
}), | ||
env: spawnEnv, | ||
stdioString, | ||
stdio, | ||
cwd: path, | ||
...(isCmd ? { windowsVerbatimArguments: true } : {}), | ||
shell: scriptShell, | ||
} | ||
return [scriptShell, args, spawnOpts] | ||
return [cmd, args, spawnOpts] | ||
} | ||
module.exports = makeSpawnArgs |
@@ -9,8 +9,9 @@ // https://github.com/npm/rfcs/pull/183 | ||
for (const [key, val] of Object.entries(vals)) { | ||
if (val === undefined) | ||
if (val === undefined) { | ||
continue | ||
else if (val && !Array.isArray(val) && typeof val === 'object') | ||
} else if (val && !Array.isArray(val) && typeof val === 'object') { | ||
packageEnvs(env, val, `${prefix}${key}_`) | ||
else | ||
} else { | ||
env[`${prefix}${key}`] = envVal(val) | ||
} | ||
} | ||
@@ -17,0 +18,0 @@ return env |
@@ -9,4 +9,13 @@ const makeSpawnArgs = require('./make-spawn-args.js') | ||
// you wouldn't like me when I'm angry... | ||
const bruce = (id, event, cmd) => | ||
`\n> ${id ? id + ' ' : ''}${event}\n> ${cmd.trim().replace(/\n/g, '\n> ')}\n` | ||
const bruce = (id, event, cmd, args) => { | ||
let banner = id | ||
? `\n> ${id} ${event}\n` | ||
: `\n> ${event}\n` | ||
banner += `> ${cmd.trim().replace(/\n/g, '\n> ')}` | ||
if (args.length) { | ||
banner += ` ${args.join(' ')}` | ||
} | ||
banner += '\n' | ||
return banner | ||
} | ||
@@ -18,2 +27,3 @@ const runScriptPkg = async options => { | ||
scriptShell, | ||
binPaths = false, | ||
env = {}, | ||
@@ -23,3 +33,3 @@ stdio = 'pipe', | ||
args = [], | ||
stdioString = false, | ||
stdioString, | ||
// note: only used when stdio:inherit | ||
@@ -32,9 +42,10 @@ banner = true, | ||
const {scripts = {}, gypfile} = pkg | ||
const { scripts = {}, gypfile } = pkg | ||
let cmd = null | ||
if (options.cmd) | ||
if (options.cmd) { | ||
cmd = options.cmd | ||
else if (pkg.scripts && pkg.scripts[event]) | ||
cmd = pkg.scripts[event] + args.map(a => ` ${JSON.stringify(a)}`).join('') | ||
else if ( // If there is no preinstall or install script, default to rebuilding node-gyp packages. | ||
} else if (pkg.scripts && pkg.scripts[event]) { | ||
cmd = pkg.scripts[event] | ||
} else if ( | ||
// If there is no preinstall or install script, default to rebuilding node-gyp packages. | ||
event === 'install' && | ||
@@ -45,24 +56,30 @@ !scripts.install && | ||
await isNodeGypPackage(path) | ||
) | ||
) { | ||
cmd = defaultGypInstallScript | ||
else if (event === 'start' && await isServerPackage(path)) | ||
cmd = 'node server.js' + args.map(a => ` ${JSON.stringify(a)}`).join('') | ||
} else if (event === 'start' && await isServerPackage(path)) { | ||
cmd = 'node server.js' | ||
} | ||
if (!cmd) | ||
if (!cmd) { | ||
return { code: 0, signal: null } | ||
} | ||
if (stdio === 'inherit' && banner !== false) { | ||
// we're dumping to the parent's stdout, so print the banner | ||
console.log(bruce(pkg._id, event, cmd)) | ||
console.log(bruce(pkg._id, event, cmd, args)) | ||
} | ||
const p = promiseSpawn(...makeSpawnArgs({ | ||
const [spawnShell, spawnArgs, spawnOpts] = makeSpawnArgs({ | ||
event, | ||
path, | ||
scriptShell, | ||
binPaths, | ||
env: packageEnvs(env, pkg), | ||
stdio, | ||
cmd, | ||
args, | ||
stdioString, | ||
}), { | ||
}) | ||
const p = promiseSpawn(spawnShell, spawnArgs, spawnOpts, { | ||
event, | ||
@@ -74,7 +91,9 @@ script: cmd, | ||
if (stdio === 'inherit') | ||
if (stdio === 'inherit') { | ||
signalManager.add(p.process) | ||
} | ||
if (p.stdin) | ||
if (p.stdin) { | ||
p.stdin.end() | ||
} | ||
@@ -84,3 +103,7 @@ return p.catch(er => { | ||
if (stdio === 'inherit' && signal) { | ||
// by the time we reach here, the child has already exited. we send the | ||
// signal back to ourselves again so that npm will exit with the same | ||
// status as the child | ||
process.kill(process.pid, signal) | ||
// just in case we don't die, reject after 500ms | ||
@@ -90,4 +113,5 @@ // this also keeps the node process open long enough to actually | ||
return new Promise((res, rej) => setTimeout(() => rej(er), signalTimeout)) | ||
} else | ||
} else { | ||
throw er | ||
} | ||
}) | ||
@@ -94,0 +118,0 @@ } |
@@ -8,7 +8,8 @@ const rpj = require('read-package-json-fast') | ||
validateOptions(options) | ||
const {pkg, path} = options | ||
const { pkg, path } = options | ||
return pkg ? runScriptPkg(options) | ||
: rpj(path + '/package.json').then(pkg => runScriptPkg({...options, pkg})) | ||
: rpj(path + '/package.json') | ||
.then(readPackage => runScriptPkg({ ...options, pkg: readPackage })) | ||
} | ||
module.exports = Object.assign(runScript, { isServerPackage }) |
@@ -1,3 +0,2 @@ | ||
const {resolve, dirname} = require('path') | ||
const isWindows = require('./is-windows.js') | ||
const { resolve, dirname, delimiter } = require('path') | ||
// the path here is relative, even though it does not need to be | ||
@@ -10,14 +9,15 @@ // in order to make the posix tests pass in windows | ||
// all together in the order they appear in the object. | ||
const setPATH = (projectPath, env) => { | ||
// not require('path').delimiter, because we fake this for testing | ||
const delimiter = isWindows ? ';' : ':' | ||
const setPATH = (projectPath, binPaths, env) => { | ||
const PATH = Object.keys(env).filter(p => /^path$/i.test(p) && env[p]) | ||
.map(p => env[p].split(delimiter)) | ||
.reduce((set, p) => set.concat(p.filter(p => !set.includes(p))), []) | ||
.reduce((set, p) => set.concat(p.filter(concatted => !set.includes(concatted))), []) | ||
.join(delimiter) | ||
const pathArr = [] | ||
if (binPaths) { | ||
pathArr.push(...binPaths) | ||
} | ||
// unshift the ./node_modules/.bin from every folder | ||
// walk up until dirname() does nothing, at the root | ||
// XXX should we specify a cwd that we don't go above? | ||
// XXX we should specify a cwd that we don't go above | ||
let p = projectPath | ||
@@ -38,4 +38,5 @@ let pp | ||
for (const key of Object.keys(env)) { | ||
if (/^path$/i.test(key)) | ||
if (/^path$/i.test(key)) { | ||
env[key] = pathVal | ||
} | ||
} | ||
@@ -42,0 +43,0 @@ |
const runningProcs = new Set() | ||
let handlersInstalled = false | ||
// NOTE: these signals aren't actually forwarded anywhere. they're trapped and | ||
// ignored until all child processes have exited. in our next breaking change | ||
// we should rename this | ||
const forwardedSignals = [ | ||
'SIGINT', | ||
'SIGTERM' | ||
'SIGTERM', | ||
] | ||
const handleSignal = signal => { | ||
for (const proc of runningProcs) { | ||
proc.kill(signal) | ||
} | ||
} | ||
// no-op, this is so receiving the signal doesn't cause us to exit immediately | ||
// instead, we exit after all children have exited when we re-send the signal | ||
// to ourselves. see the catch handler at the bottom of run-script-pkg.js | ||
// istanbul ignore next - this function does nothing | ||
const handleSignal = () => {} | ||
const setupListeners = () => { | ||
@@ -33,4 +35,5 @@ for (const signal of forwardedSignals) { | ||
runningProcs.add(proc) | ||
if (!handlersInstalled) | ||
if (!handlersInstalled) { | ||
setupListeners() | ||
} | ||
@@ -46,3 +49,3 @@ proc.once('exit', () => { | ||
handleSignal, | ||
forwardedSignals | ||
forwardedSignals, | ||
} |
const validateOptions = options => { | ||
if (typeof options !== 'object' || !options) | ||
if (typeof options !== 'object' || !options) { | ||
throw new TypeError('invalid options object provided to runScript') | ||
} | ||
@@ -15,18 +16,25 @@ const { | ||
if (!event || typeof event !== 'string') | ||
if (!event || typeof event !== 'string') { | ||
throw new TypeError('valid event not provided to runScript') | ||
if (!path || typeof path !== 'string') | ||
} | ||
if (!path || typeof path !== 'string') { | ||
throw new TypeError('valid path not provided to runScript') | ||
if (scriptShell !== undefined && typeof scriptShell !== 'string') | ||
} | ||
if (scriptShell !== undefined && typeof scriptShell !== 'string') { | ||
throw new TypeError('invalid scriptShell option provided to runScript') | ||
if (typeof env !== 'object' || !env) | ||
} | ||
if (typeof env !== 'object' || !env) { | ||
throw new TypeError('invalid env option provided to runScript') | ||
if (typeof stdio !== 'string' && !Array.isArray(stdio)) | ||
} | ||
if (typeof stdio !== 'string' && !Array.isArray(stdio)) { | ||
throw new TypeError('invalid stdio option provided to runScript') | ||
if (!Array.isArray(args) || args.some(a => typeof a !== 'string')) | ||
} | ||
if (!Array.isArray(args) || args.some(a => typeof a !== 'string')) { | ||
throw new TypeError('invalid args option provided to runScript') | ||
if (cmd !== undefined && typeof cmd !== 'string') | ||
} | ||
if (cmd !== undefined && typeof cmd !== 'string') { | ||
throw new TypeError('invalid cmd option provided to runScript') | ||
} | ||
} | ||
module.exports = validateOptions |
{ | ||
"name": "@npmcli/run-script", | ||
"version": "1.8.6", | ||
"version": "7.0.2", | ||
"description": "Run a lifecycle script for a package (descendant of npm-lifecycle)", | ||
"author": "Isaac Z. Schlueter <i@izs.me> (https://izs.me)", | ||
"author": "GitHub Inc.", | ||
"license": "ISC", | ||
"scripts": { | ||
"test": "tap", | ||
"preversion": "npm test", | ||
"postversion": "npm publish", | ||
"prepublishOnly": "git push origin --follow-tags", | ||
"eslint": "eslint", | ||
"lint": "npm run eslint -- \"lib/**/*.js\"", | ||
"lintfix": "npm run lint -- --fix" | ||
"lint": "eslint \"**/*.js\"", | ||
"lintfix": "npm run lint -- --fix", | ||
"postlint": "template-oss-check", | ||
"snap": "tap", | ||
"posttest": "npm run lint", | ||
"template-oss-apply": "template-oss-apply --force" | ||
}, | ||
"tap": { | ||
"check-coverage": true, | ||
"coverage-map": "map.js" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^7.19.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-promise": "^4.2.1", | ||
"eslint-plugin-standard": "^5.0.0", | ||
"minipass": "^3.1.1", | ||
"@npmcli/eslint-config": "^4.0.0", | ||
"@npmcli/template-oss": "4.19.0", | ||
"require-inject": "^1.4.4", | ||
"tap": "^15.0.4" | ||
"tap": "^16.0.1" | ||
}, | ||
"dependencies": { | ||
"@npmcli/node-gyp": "^1.0.2", | ||
"@npmcli/promise-spawn": "^1.3.2", | ||
"node-gyp": "^7.1.0", | ||
"read-package-json-fast": "^2.0.1" | ||
"@npmcli/node-gyp": "^3.0.0", | ||
"@npmcli/promise-spawn": "^7.0.0", | ||
"node-gyp": "^10.0.0", | ||
"read-package-json-fast": "^3.0.0", | ||
"which": "^4.0.0" | ||
}, | ||
"files": [ | ||
"lib/**/*.js", | ||
"lib/node-gyp-bin" | ||
"bin/", | ||
"lib/" | ||
], | ||
@@ -43,4 +37,18 @@ "main": "lib/run-script.js", | ||
"type": "git", | ||
"url": "git+https://github.com/npm/run-script.git" | ||
"url": "https://github.com/npm/run-script.git" | ||
}, | ||
"engines": { | ||
"node": "^16.14.0 || >=18.0.0" | ||
}, | ||
"templateOSS": { | ||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", | ||
"version": "4.19.0", | ||
"publish": "true" | ||
}, | ||
"tap": { | ||
"nyc-arg": [ | ||
"--exclude", | ||
"tap-snapshots/**" | ||
] | ||
} | ||
} |
@@ -20,8 +20,18 @@ # @npmcli/run-script | ||
// optional, these paths will be put at the beginning of `$PATH`, even | ||
// after run-script adds the node_modules/.bin folder(s) from | ||
// `process.cwd()`. This is for commands like `npm init`, `npm exec`, | ||
// and `npx` to make sure manually installed packages come before | ||
// anything that happens to be in the tree in `process.cwd()`. | ||
binPaths: [ | ||
'/path/to/npx/node_modules/.bin', | ||
'/path/to/npm/prefix/node_modules/.bin', | ||
], | ||
// optional, defaults to /bin/sh on unix, or cmd.exe on windows | ||
scriptShell: '/bin/bash', | ||
// optional, defaults to false | ||
// optional, passed directly to `@npmcli/promise-spawn` which defaults it to true | ||
// return stdout and stderr as strings rather than buffers | ||
stdioString: true, | ||
stdioString: false, | ||
@@ -115,4 +125,5 @@ // optional, additional environment variables to add | ||
something else, which will be run in an otherwise matching environment. | ||
- `stdioString` Optional, defaults to `false`. Return string values for | ||
`stderr` and `stdout` rather than Buffers. | ||
- `stdioString` Optional, passed directly to `@npmcli/promise-spawn` which | ||
defaults it to `true`. Return string values for `stderr` and `stdout` rather | ||
than Buffers. | ||
- `banner` Optional, defaults to `true`. If the `stdio` option is set to | ||
@@ -152,1 +163,16 @@ `'inherit'`, then print a banner with the package name and version, event | ||
script. | ||
## Escaping | ||
In order to ensure that arguments are handled consistently, this module | ||
writes a temporary script file containing the command as it exists in | ||
the package.json, followed by the user supplied arguments having been | ||
escaped to ensure they are processed as literal strings. We then instruct | ||
the shell to execute the script file, and when the process exits we remove | ||
the temporary file. | ||
In Windows, when the shell is cmd, and when the initial command in the script | ||
is a known batch file (i.e. `something.cmd`) we double escape additional | ||
arguments so that the shim scripts npm installs work correctly. | ||
The actual implementation of the escaping is in `lib/escape.js`. |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
18483
11.89%4
-50%302
13.96%176
17.33%2
-33.33%0
-100%5
25%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated