Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/lib/content/commands/npm-approve-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ it with `--global` (`-g`) fails with an `EGLOBAL` error, since global
installs (`npm install -g`) and one-off executions (`npm exec` / `npx`) have
no project `package.json` to write to. To allow install scripts in those
contexts, use the `--allow-scripts` flag at install time (for example
`npm install -g --allow-scripts=canvas,sharp`) or persist the setting with
`npm config set allow-scripts=canvas,sharp --location=user`.
`npm install -g canvas sharp --allow-scripts=canvas,sharp`) or persist the
setting with `npm config set allow-scripts=canvas,sharp --location=user`.

There are three modes:

Expand Down
4 changes: 2 additions & 2 deletions docs/lib/content/commands/npm-install-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ it with `--global` (`-g`) fails with an `EGLOBAL` error, since global
installs (`npm install -g`) and one-off executions (`npm exec` / `npx`) have
no project `package.json` to write to. To allow install scripts in those
contexts, use the `--allow-scripts` flag at install time (for example
`npm install -g --allow-scripts=canvas,sharp`) or persist the setting with
`npm config set allow-scripts=canvas,sharp --location=user`.
`npm install -g canvas sharp --allow-scripts=canvas,sharp`) or persist the
setting with `npm config set allow-scripts=canvas,sharp --location=user`.

There are four subcommands:

Expand Down
13 changes: 12 additions & 1 deletion lib/utils/allow-scripts-remediation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,15 @@
const configSetAllowScripts = (names) =>
`npm config set allow-scripts=${names.join(',')} --location=user`

module.exports = { configSetAllowScripts }
// Builds the one-off `npm <cmd> -g ... --allow-scripts=<names>` command
// suggested to global users. The specs the user asked for have to be
// repeated: `npm install -g --allow-scripts=foo` with no specs installs the
// current directory, which global users usually are not sitting in, so the
// suggestion would fail with ENOENT reading package.json.
const globalAllowScripts = (npm, names) => {
const command = npm.command || 'install'
const specs = npm.argv?.length ? ` ${npm.argv.join(' ')}` : ''
return `npm ${command} -g${specs} --allow-scripts=${names.join(',')}`
}

module.exports = { configSetAllowScripts, globalAllowScripts }
5 changes: 2 additions & 3 deletions lib/utils/reify-output.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const npmAuditReport = require('npm-audit-report')
const { readTree: getFundingInfo } = require('libnpmfund')
const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js')
const auditError = require('./audit-error.js')
const { configSetAllowScripts } = require('./allow-scripts-remediation.js')
const { configSetAllowScripts, globalAllowScripts } = require('./allow-scripts-remediation.js')

const reifyOutput = (npm, arb, extras = {}) => {
const { diff, actualTree } = arb
Expand Down Expand Up @@ -276,9 +276,8 @@ const unreviewedScriptsMessage = (npm, unreviewedScripts) => {
// one-off, or `npm config set allow-scripts` to persist it.
const remediationLines = (npm, names) => {
if (npm.global) {
const list = names.join(',')
return [
`Run \`npm install -g --allow-scripts=${list}\` to allow these scripts ` +
`Run \`${globalAllowScripts(npm, names)}\` to allow these scripts ` +
`once, or \`${configSetAllowScripts(names)}\` to allow them for ` +
'all global installs.',
]
Expand Down
52 changes: 52 additions & 0 deletions test/lib/utils/reify-output.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,58 @@ t.test('global install suggests --allow-scripts, not approve-scripts', async t =
t.notMatch(warn, /approve-scripts/)
})

t.test('global install repeats the requested specs in the suggestion', async t => {
const mock = await mockNpm(t, {
command: 'install',
argv: ['esbuild', 'canvas@2'],
config: { global: true },
})
Object.defineProperty(mock.npm, 'command', {
get () {
return 'install'
},
enumerable: true,
})

reifyOutput(mock.npm, {
actualTree: { name: 'host', inventory: { has: () => false } },
diff: { children: [] },
}, {
unreviewedScripts: [{
node: { packageName: 'esbuild', name: 'esbuild', version: '0.28.1', path: '/x/esbuild' },
scripts: { postinstall: 'node install.js' },
}],
})
mock.npm.finish()

const warn = mock.logs.warn.byTitle('install-scripts').join('\n')
t.match(warn, /npm install -g esbuild canvas@2 --allow-scripts=esbuild/)
})

t.test('global command without specs suggests that command', async t => {
const mock = await mockNpm(t, { command: 'update', config: { global: true } })
Object.defineProperty(mock.npm, 'command', {
get () {
return 'update'
},
enumerable: true,
})

reifyOutput(mock.npm, {
actualTree: { name: 'host', inventory: { has: () => false } },
diff: { children: [] },
}, {
unreviewedScripts: [{
node: { packageName: 'esbuild', name: 'esbuild', version: '0.28.1', path: '/x/esbuild' },
scripts: { postinstall: 'node install.js' },
}],
})
mock.npm.finish()

const warn = mock.logs.warn.byTitle('install-scripts').join('\n')
t.match(warn, /npm update -g --allow-scripts=esbuild/)
})

t.test('single unreviewed script uses singular wording', async t => {
const mockReifyWithExtras = async (t, reify, extras) => {
const mock = await mockNpm(t, {})
Expand Down