From 2cbb13b77edd9410704eac6f30ffea30a18c7046 Mon Sep 17 00:00:00 2001 From: Rayan Salhab Date: Fri, 5 Jun 2026 19:57:31 +0300 Subject: [PATCH 1/4] fix: recognize allowScripts for local link targets (#9490) ## Summary - Recognize local directory link targets by their incoming link source when matching `allowScripts` policy entries. - Reuse that source identity when `approve-scripts`/`deny-scripts` derive file dependency policy keys. - Add coverage for reviewed local `file:` dependency link targets. Fixes #9488 ## Testing - `node node_modules/tap/bin/run.js --no-coverage workspaces/arborist/test/script-allowed.js workspaces/arborist/test/unreviewed-scripts.js test/lib/utils/allow-scripts-writer.js test/lib/utils/check-allow-scripts.js test/lib/utils/resolve-allow-scripts.js` - `node node_modules/eslint/bin/eslint.js lib/utils/allow-scripts-writer.js test/lib/utils/allow-scripts-writer.js workspaces/arborist/lib/script-allowed.js workspaces/arborist/test/script-allowed.js workspaces/arborist/test/unreviewed-scripts.js` - `git diff --check` - Manual repro: local `file:` dependency with `allowScripts: { "file:../testdep": false }` no longer emits an `allow-scripts` warning; `npm approve-scripts --all` writes `file:../testdep`. --------- Co-authored-by: cyphercodes --- lib/utils/allow-scripts-writer.js | 19 ++++--- test/lib/utils/allow-scripts-writer.js | 42 ++++++++++++++++ workspaces/arborist/lib/script-allowed.js | 48 +++++++++++++++--- workspaces/arborist/test/script-allowed.js | 50 +++++++++++++++++++ .../arborist/test/unreviewed-scripts.js | 19 +++++++ 5 files changed, 163 insertions(+), 15 deletions(-) diff --git a/lib/utils/allow-scripts-writer.js b/lib/utils/allow-scripts-writer.js index 5f43bbebeedef..310e22412a4a4 100644 --- a/lib/utils/allow-scripts-writer.js +++ b/lib/utils/allow-scripts-writer.js @@ -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`. @@ -12,6 +15,8 @@ const { getTrustedRegistryIdentity } = require('@npmcli/arborist/lib/script-allo // Denying always writes `"": 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 @@ -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) @@ -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) @@ -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 @@ -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 } diff --git a/test/lib/utils/allow-scripts-writer.js b/test/lib/utils/allow-scripts-writer.js index 56314f8eb5a52..f72a84eef113b 100644 --- a/test/lib/utils/allow-scripts-writer.js +++ b/test/lib/utils/allow-scripts-writer.js @@ -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')) diff --git a/workspaces/arborist/lib/script-allowed.js b/workspaces/arborist/lib/script-allowed.js index 66104b274c7c5..89e24339791f1 100644 --- a/workspaces/arborist/lib/script-allowed.js +++ b/workspaces/arborist/lib/script-allowed.js @@ -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 @@ -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) => { @@ -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 diff --git a/workspaces/arborist/test/script-allowed.js b/workspaces/arborist/test/script-allowed.js index 4e20f987d9c87..e708074817402 100644 --- a/workspaces/arborist/test/script-allowed.js +++ b/workspaces/arborist/test/script-allowed.js @@ -118,6 +118,56 @@ t.test('file path — exact resolved match', t => { t.end() }) +t.test('file path — link target matches incoming link source', t => { + const targetPath = require('node:path').resolve('local-pkg') + const target = node({ + name: 'local-pkg', + packageName: 'local-pkg', + version: '1.0.0', + }) + target.resolved = null + target.path = targetPath + target.realpath = targetPath + target.linksIn = new Set([{ resolved: 'file:../local-pkg' }]) + + t.equal(isScriptAllowed(target, { 'file:../local-pkg': true }), true) + t.equal(isScriptAllowed(target, { 'file:local-pkg': true }), true) + t.equal(isScriptAllowed(target, { 'file:../local-pkg': false }), false) + t.equal(isScriptAllowed(target, { 'file:../other': true }), null) + t.end() +}) + +t.test('file path — registry nodes do not match by install path', t => { + const reg = node({ + name: 'sharp', + packageName: 'sharp', + version: '0.33.0', + path: 'node_modules/sharp', + realpath: require('node:path').resolve('node_modules/sharp'), + linksIn: new Set(), + }) + + t.equal(isScriptAllowed(reg, { 'file:node_modules/sharp': true }), null) + t.end() +}) + +t.test('file path — empty link sets do not add install paths', t => { + const targetPath = require('node:path').resolve('local-pkg') + const target = node({ + name: 'local-pkg', + packageName: 'local-pkg', + version: '1.0.0', + }) + target.resolved = null + target.path = targetPath + target.realpath = targetPath + target.linksIn = new Set() + + t.equal(isScriptAllowed(target, { 'file:local-pkg': true }), null) + t.equal(isScriptAllowed(target, { [targetPath]: true }), null) + t.end() +}) + t.test('directory key — npa parses absolute paths as type=directory', t => { // npa treats absolute paths as { type: 'directory' }, which the // matcher shares with the 'file' case. path.resolve produces a diff --git a/workspaces/arborist/test/unreviewed-scripts.js b/workspaces/arborist/test/unreviewed-scripts.js index 9672f0dc228f7..7f26f56db6875 100644 --- a/workspaces/arborist/test/unreviewed-scripts.js +++ b/workspaces/arborist/test/unreviewed-scripts.js @@ -118,6 +118,25 @@ t.test('collectUnreviewedScripts', async t => { t.equal(result[0].node.name, 'pending') }) + t.test('skips reviewed local directory link targets', async t => { + const target = node({ name: 'local', scripts: { install: 'x' } }) + target.resolved = null + target.isRegistryDependency = false + target.path = require('node:path').resolve('local') + target.realpath = target.path + target.linksIn = new Set([{ resolved: 'file:../local' }]) + + t.strictSame(await collectUnreviewedScripts({ + tree: tree([target]), + policy: { 'file:../local': false }, + }), []) + + t.strictSame(await collectUnreviewedScripts({ + tree: tree([target]), + policy: { 'file:local': true }, + }), []) + }) + t.test('detects synthetic node-gyp via binding.gyp runtime check', async t => { const collect = mockCollect(t, async (n) => { if (n.path === '/has-bindings') { From 0e55f97deea0f2395196dd572fac5fd7c9fbee38 Mon Sep 17 00:00:00 2001 From: Zelys Date: Thu, 4 Jun 2026 13:37:45 -0500 Subject: [PATCH 2/4] fix: pass script-shell to publish lifecycle hooks npm run passes script-shell to @npmcli/run-script, but the three runScript() calls in publish.js for prepublishOnly, publish, and postpublish never did. On Windows, where users set script-shell to bash in .npmrc, those lifecycle hooks still ran under cmd.exe while explicit npm run calls used the configured shell. Reads script-shell from config in #publish() and passes it to all three calls, matching the existing pattern in run.js. Adds script-shell to static params so it appears in npm help publish. Fixes #9477 --- lib/commands/publish.js | 4 ++++ test/lib/commands/publish.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/commands/publish.js b/lib/commands/publish.js index bc243dbea68f1..015e4312f6113 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -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)) { @@ -102,6 +103,7 @@ class Publish extends BaseCommand { path: spec.fetchSpec, stdio: 'inherit', pkg: manifest, + scriptShell, }) } @@ -218,6 +220,7 @@ class Publish extends BaseCommand { path: spec.fetchSpec, stdio: 'inherit', pkg: manifest, + scriptShell, }) await runScript({ @@ -225,6 +228,7 @@ class Publish extends BaseCommand { path: spec.fetchSpec, stdio: 'inherit', pkg: manifest, + scriptShell, }) } diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 8765ebea7b8b9..2478cd3fb1ff0 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -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`) + } +}) From 4bcba546420119ebf9ee73e903b00b757f643fdf Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Fri, 5 Jun 2026 22:59:19 +0530 Subject: [PATCH 3/4] fix(arborist): apply registry-tarball allow-remote exemption in linked strategy (#9495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In continuation of our exploration of using `install-strategy=linked` in the [Gutenberg monorepo](https://github.com/WordPress/gutenberg/pull/75814), which powers the WordPress Block Editor. Under `install-strategy=linked`, a fresh install fails with `EALLOWREMOTE` on ordinary registry dependencies whose lockfile `resolved` is a full registry tarball URL, even though `allow-remote=none` is meant to permit registry-mediated tarballs. The standard (hoisted) reifier installs the same dependency fine; only the linked strategy rejects it. ``` npm error code EALLOWREMOTE npm error Fetching packages of type "remote" have been disabled npm error Refusing to fetch "minimatch@https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz" ``` ## Why Both strategies extract through the same `pacote.extract` in `reify.js`, which exempts registry tarballs from the allow-remote gate via `#isRegistryResolvedTarball`. That check first requires `node.isRegistryDependency`. In the linked strategy, store nodes are `IsolatedNode` instances — a standalone class that emulates `lib/node.js` but has no `isRegistryDependency` getter and no edges to recompute it from. So `node.isRegistryDependency` was `undefined`, the exemption short-circuited to `false`, the `allowRemote: 'all'` override was never applied, and pacote rejected the same-origin registry tarball. This is the second half of the allow-remote registry-tarball handling: the URL-matching half was hardened previously (origin + registry-path-prefix); this fixes the `isRegistryDependency` half for the linked path. The origin/path security check still runs unchanged on the linked path — a tampered lockfile pointing at a foreign host is still blocked. ## How Carry the registry-dependency flag from the source tree node onto the store node, rather than weakening the guard: 1. `IsolatedNode` gains an `isRegistryDependency` field (default `false`), settable from constructor options. 2. `#externalProxy` copies `node.isRegistryDependency` from the real tree node onto the proxy. 3. `#generateChild` passes it through to the store `IsolatedNode`. This preserves exact parity with the hoisted reifier: registry deps are exempt, user-pinned off-registry URLs are not. It also makes the linked strategy's `isScriptAllowed` matching more accurate — store nodes now carry the trustworthy edge-based flag instead of falling back to guessing registry-ness from the resolved URL. ## References Fixes #9494 --- .../arborist/lib/arborist/isolated-reifier.js | 4 ++ workspaces/arborist/lib/isolated-classes.js | 4 ++ workspaces/arborist/test/arborist/reify.js | 50 +++++++++++++++++++ workspaces/arborist/test/script-allowed.js | 1 + 4 files changed, 59 insertions(+) diff --git a/workspaces/arborist/lib/arborist/isolated-reifier.js b/workspaces/arborist/lib/arborist/isolated-reifier.js index e782d6125e9fa..e7d985a80a7b9 100644 --- a/workspaces/arborist/lib/arborist/isolated-reifier.js +++ b/workspaces/arborist/lib/arborist/isolated-reifier.js @@ -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, @@ -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 } diff --git a/workspaces/arborist/lib/isolated-classes.js b/workspaces/arborist/lib/isolated-classes.js index 32bf19972d150..007f2609e5feb 100644 --- a/workspaces/arborist/lib/isolated-classes.js +++ b/workspaces/arborist/lib/isolated-classes.js @@ -20,6 +20,7 @@ class IsolatedNode { inventory = new IsolatedInventory() isInStore = false inBundle = false + isRegistryDependency = false linksIn = new Set() meta = { loadedFromDisk: false } optional = false @@ -50,6 +51,9 @@ class IsolatedNode { if (options.inBundle) { this.inBundle = true } + if (options.isRegistryDependency) { + this.isRegistryDependency = true + } if (options.optional) { this.optional = true } diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 6c427dbb2213a..2abe5a5ed09a2 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -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', diff --git a/workspaces/arborist/test/script-allowed.js b/workspaces/arborist/test/script-allowed.js index e708074817402..615ffaba7140e 100644 --- a/workspaces/arborist/test/script-allowed.js +++ b/workspaces/arborist/test/script-allowed.js @@ -452,6 +452,7 @@ t.test('isolated mode (linked): bundled IsolatedNode is blocked', async t => { const store = new IsolatedNode({ isInStore: true, + isRegistryDependency: true, // carried from the source node by #externalProxy location: 'node_modules/.store/store-pkg@1.0.0/node_modules/store-pkg', name: 'store-pkg', package: { name: 'store-pkg', version: '1.0.0' }, From 455aa4a17b92a77ffbd474c4dc17a16f71cfe6b1 Mon Sep 17 00:00:00 2001 From: Liang Date: Sat, 6 Jun 2026 01:41:27 +0800 Subject: [PATCH 4/4] docs: use the latest version for global update and outdated's `wanted` (#9401) For now, `npm outdated`'s document shows `wanted` field for global `npm outdated` is always the same as `current` field, but the runtime behavior shows `npm outdated` prints `wanted` as latest version. Considering `npm update`'s behavior, it should be a document issue. This PR fixes it by saying `the latest version` instead of `currently-installed version`. ## References Close #3612 --- docs/lib/content/commands/npm-outdated.md | 2 +- docs/lib/content/commands/npm-update.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/lib/content/commands/npm-outdated.md b/docs/lib/content/commands/npm-outdated.md index 40da7c3146f28..fb5db63d12d4b 100644 --- a/docs/lib/content/commands/npm-outdated.md +++ b/docs/lib/content/commands/npm-outdated.md @@ -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). diff --git a/docs/lib/content/commands/npm-update.md b/docs/lib/content/commands/npm-update.md index 92c38dbe76d78..088737a53c63d 100644 --- a/docs/lib/content/commands/npm-update.md +++ b/docs/lib/content/commands/npm-update.md @@ -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 [...]` +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_.