Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/lib/content/commands/npm-outdated.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Use `--all` to find all outdated meta-dependencies as well.
In the output:

* `wanted` is the maximum version of the package that satisfies the semver range specified in `package.json`.
If there's no available semver range (i.e. you're running `npm outdated --global`, or the package isn't included in `package.json`), then `wanted` shows the currently-installed version.
If there's no available semver range (i.e. you're running `npm outdated --global`, or the package isn't included in `package.json`), then `wanted` shows the latest version.
* `latest` is the version of the package tagged as latest in the registry.
Running `npm publish` with no special configuration will publish the package with a dist-tag of `latest`.
This may or may not be the maximum version of the package, or the most-recently published version of the package, depending on how the package's developer manages the latest [dist-tag](/commands/npm-dist-tag).
Expand Down
3 changes: 1 addition & 2 deletions docs/lib/content/commands/npm-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,7 @@ In this case if you really did need your package to use a newer version you woul

`npm update -g` will apply the `update` action to each globally installed package that is `outdated` -- that is, has a version that is different from `wanted`.

Note: Globally installed packages are treated as if they are installed with a caret semver range specified.
So if you require to update to `latest` you may need to run `npm install -g [<pkg>...]`
Note: Globally installed packages do not have a `package.json` semver range available, so their `wanted` version is `latest`.

NOTE: If a package has been upgraded to a version newer than `latest`, it will be _downgraded_.

Expand Down
4 changes: 4 additions & 0 deletions lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class Publish extends BaseCommand {
const json = this.npm.config.get('json')
const defaultTag = this.npm.config.get('tag')
const ignoreScripts = this.npm.config.get('ignore-scripts')
const scriptShell = this.npm.config.get('script-shell') || undefined
const { silent } = this.npm

if (semver.validRange(defaultTag)) {
Expand All @@ -102,6 +103,7 @@ class Publish extends BaseCommand {
path: spec.fetchSpec,
stdio: 'inherit',
pkg: manifest,
scriptShell,
})
}

Expand Down Expand Up @@ -218,13 +220,15 @@ class Publish extends BaseCommand {
path: spec.fetchSpec,
stdio: 'inherit',
pkg: manifest,
scriptShell,
})

await runScript({
event: 'postpublish',
path: spec.fetchSpec,
stdio: 'inherit',
pkg: manifest,
scriptShell,
})
}

Expand Down
19 changes: 12 additions & 7 deletions lib/utils/allow-scripts-writer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const npa = require('npm-package-arg')
const { log } = require('proc-log')
const { getTrustedRegistryIdentity } = require('@npmcli/arborist/lib/script-allowed.js')
const {
getTrustedRegistryIdentity,
resolvedSourceSpecs,
} = require('@npmcli/arborist/lib/script-allowed.js')

// Pure helpers that implement the RFC's pin-mismatch table for
// `npm approve-scripts` and `npm deny-scripts`.
Expand All @@ -12,6 +15,8 @@ const { getTrustedRegistryIdentity } = require('@npmcli/arborist/lib/script-allo
// Denying always writes `"<name>": false`, regardless of `--allow-scripts-pin`, per the
// RFC's asymmetric-pin rule.

const primaryResolvedSource = (node) => resolvedSourceSpecs(node)[0] || ''

// Convert an arborist Node into the spec string used for a versioned policy
// entry. Returns `null` if the node cannot be represented as a versioned key
// derived from trusted sources (lockfile URL for registry, hosted shortcut
Expand All @@ -21,8 +26,7 @@ const versionedKeyFor = (node) => {
if (!node) {
return null
}
/* istanbul ignore next: callers guarantee a string resolved */
const resolved = typeof node.resolved === 'string' ? node.resolved : ''
const resolved = primaryResolvedSource(node)
if (resolved.startsWith('git')) {
try {
const parsed = npa(resolved)
Expand Down Expand Up @@ -69,8 +73,7 @@ const nameKeyFor = (node) => {
if (!node) {
return null
}
/* istanbul ignore next: callers guarantee a string resolved */
const resolved = typeof node.resolved === 'string' ? node.resolved : ''
const resolved = primaryResolvedSource(node)
if (resolved.startsWith('git')) {
try {
const parsed = npa(resolved)
Expand Down Expand Up @@ -164,7 +167,8 @@ const keyTargetsNode = (key, node) => {
case 'git': {
let resolvedParsed
try {
resolvedParsed = node.resolved ? npa(node.resolved) : null
const resolved = primaryResolvedSource(node)
resolvedParsed = resolved ? npa(resolved) : null
} catch {
/* istanbul ignore next */
return false
Expand All @@ -176,7 +180,8 @@ const keyTargetsNode = (key, node) => {
case 'file':
case 'directory':
case 'remote':
return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec
return resolvedSourceSpecs(node)
.some(resolved => resolved === parsed.saveSpec || resolved === parsed.fetchSpec)
default:
return false
}
Expand Down
34 changes: 34 additions & 0 deletions test/lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -1624,3 +1624,37 @@ t.test('oidc token exchange - provenance', (t) => {

t.end()
})

t.test('passes script-shell config to lifecycle hooks', async t => {
const CAPTURED = []
const { npm, registry } = await loadNpmWithRegistry(t, {
config: {
...auth,
'script-shell': '/bin/bash',
},
prefixDir: {
'package.json': JSON.stringify({
...pkgJson,
scripts: {
prepublishOnly: 'exit 0',
publish: 'exit 0',
postpublish: 'exit 0',
},
}),
},
mocks: {
'@npmcli/run-script': async (opts) => {
CAPTURED.push(opts)
},
},
})

registry.publish(pkg, {})
await npm.exec('publish', [])

for (const event of ['prepublishOnly', 'publish', 'postpublish']) {
const rs = CAPTURED.find(r => r.event === event)
t.ok(rs, `ran ${event}`)
t.equal(rs?.scriptShell, '/bin/bash', `${event} receives scriptShell`)
}
})
42 changes: 42 additions & 0 deletions test/lib/utils/allow-scripts-writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,48 @@ t.test('nameKeyFor / versionedKeyFor — file', async t => {
t.equal(versionedKeyFor(n), 'file:../local')
})

t.test('nameKeyFor / versionedKeyFor — local directory link target', async t => {
const targetPath = path.resolve('local')
const n = {
name: 'local',
packageName: 'local',
version: '1.0.0',
resolved: null,
path: targetPath,
realpath: targetPath,
linksIn: new Set([{ resolved: 'file:../local' }]),
}

t.equal(nameKeyFor(n), 'file:../local')
t.equal(versionedKeyFor(n), 'file:../local')

t.strictSame(
applyApprovalForPackage({}, [n], { pin: true }).allowScripts,
{ 'file:../local': true }
)
t.match(
applyApprovalForPackage({ 'file:local': false }, [n], { pin: true }).warning,
/denied|versioned deny/
)
})

t.test('nameKeyFor / versionedKeyFor — empty link target has no portable file key', async t => {
const targetPath = path.resolve('local')
const n = {
name: 'local',
packageName: 'local',
version: '1.0.0',
resolved: null,
path: targetPath,
realpath: targetPath,
linksIn: new Set(),
}

t.equal(nameKeyFor(n), null)
t.equal(versionedKeyFor(n), null)
t.strictSame(applyApprovalForPackage({}, [n], { pin: true }).allowScripts, {})
})

t.test('isSingleVersionPin', async t => {
t.ok(isSingleVersionPin('pkg@1.2.3'))
t.notOk(isSingleVersionPin('pkg'))
Expand Down
4 changes: 4 additions & 0 deletions workspaces/arborist/lib/arborist/isolated-reifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = cls => class IsolatedReifier extends cls {
const newChild = new IsolatedNode({
isInStore,
inBundle,
isRegistryDependency: node.isRegistryDependency,
location,
name: node.packageName || node.name,
optional: node.optional,
Expand Down Expand Up @@ -153,6 +154,9 @@ module.exports = cls => class IsolatedReifier extends cls {
result.optional = node.optional
result.resolved = node.resolved
result.version = node.version
// Carry the source node's registry-dependency flag so the store node retains it.
// IsolatedNode has no edges to recompute it from, and reify's registry-tarball allow-remote exemption depends on it.
result.isRegistryDependency = node.isRegistryDependency
return result
}

Expand Down
4 changes: 4 additions & 0 deletions workspaces/arborist/lib/isolated-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class IsolatedNode {
inventory = new IsolatedInventory()
isInStore = false
inBundle = false
isRegistryDependency = false
linksIn = new Set()
meta = { loadedFromDisk: false }
optional = false
Expand Down Expand Up @@ -50,6 +51,9 @@ class IsolatedNode {
if (options.inBundle) {
this.inBundle = true
}
if (options.isRegistryDependency) {
this.isRegistryDependency = true
}
if (options.optional) {
this.optional = true
}
Expand Down
48 changes: 40 additions & 8 deletions workspaces/arborist/lib/script-allowed.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,41 @@ const matches = (node, key) => {
}
}

const resolvedSourceSpecs = (node) => {
const specs = []
const seen = new Set()
const add = (spec) => {
if (typeof spec !== 'string' || spec === '' || seen.has(spec)) {
return
}
seen.add(spec)
specs.push(spec)
}

add(node?.resolved)

if (!node?.resolved && node?.linksIn && typeof node.linksIn[Symbol.iterator] === 'function') {
let hasIncomingLink = false
for (const link of node.linksIn) {
hasIncomingLink = true
add(link.resolved)
}

if (hasIncomingLink) {
// Link targets for local directory deps are separate inventory nodes
// whose own `resolved` is null. The incoming Link carries the saved spec
// (for example `file:../pkg`, relative to node_modules), while policy
// entries written by hand often use the dependency spec from package.json
// (for example `file:pkg`, resolved by npa to this target path). Include
// the real target paths so both forms can match the same local dep.
add(node.realpath)
add(node.path)
}
}

return specs
}

const matchRegistry = (node, parsed) => {
// If this node is not a registry dep, refuse the match. A registry-style
// key (`pkg`, `pkg@1`, `pkg@1 || 2`) must not match a tarball or git node
Expand Down Expand Up @@ -282,17 +317,13 @@ const matchGit = (node, parsed) => {
}

const matchFileOrDir = (node, parsed) => {
if (!node.resolved) {
return false
}
return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec
return resolvedSourceSpecs(node)
.some(resolved => resolved === parsed.saveSpec || resolved === parsed.fetchSpec)
}

const matchRemote = (node, parsed) => {
if (!node.resolved) {
return false
}
return node.resolved === parsed.fetchSpec || node.resolved === parsed.saveSpec
return resolvedSourceSpecs(node)
.some(resolved => resolved === parsed.fetchSpec || resolved === parsed.saveSpec)
}

const isRegistryNode = (node) => {
Expand Down Expand Up @@ -337,4 +368,5 @@ module.exports = isScriptAllowed
module.exports.isScriptAllowed = isScriptAllowed
module.exports.isExactVersionDisjunction = isExactVersionDisjunction
module.exports.getTrustedRegistryIdentity = getTrustedRegistryIdentity
module.exports.resolvedSourceSpecs = resolvedSourceSpecs
module.exports.trustedDisplay = trustedDisplay
50 changes: 50 additions & 0 deletions workspaces/arborist/test/arborist/reify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3854,6 +3854,56 @@ t.test('should preserve exact ranges, missing actual tree', async (t) => {
await t.resolves(arb.reify(), 'same-origin tarball is allowed for registry root')
})

t.test('allowRemote=none allows registry tarball under linked install strategy', async t => {
// The linked strategy extracts store nodes as IsolatedNode, which has no edges to recompute isRegistryDependency from.
// The flag must be carried from the source tree node so the registry-tarball allow-remote exemption still applies.
const abbrevPackument5 = JSON.stringify({
_id: 'abbrev',
_rev: 'lkjadflkjasdf',
name: 'abbrev',
'dist-tags': { latest: '1.1.1' },
versions: {
'1.1.1': {
name: 'abbrev',
version: '1.1.1',
dist: {
tarball: 'https://registry.example.com/npm/abbrev/-/abbrev-1.1.1.tgz',
},
},
},
})

const testdir = t.testdir({
project: {
'package.json': JSON.stringify({
name: 'myproject',
version: '1.0.0',
dependencies: {
abbrev: '1.1.1',
},
}),
},
})

tnock(t, 'https://registry.example.com')
.get('/npm/abbrev')
.reply(200, abbrevPackument5)

tnock(t, 'https://registry.example.com')
.get('/npm/abbrev/-/abbrev-1.1.1.tgz')
.reply(200, abbrevTGZ)

const arb = new Arborist({
path: resolve(testdir, 'project'),
registry: 'https://registry.example.com/npm',
cache: resolve(testdir, 'cache'),
allowRemote: 'none',
installStrategy: 'linked',
})

await t.resolves(arb.reify(), 'registry tarball is allowed under linked strategy')
})

t.test('registry with different protocol should swap protocol', async (t) => {
const abbrevPackument4 = JSON.stringify({
_id: 'abbrev',
Expand Down
Loading
Loading