Skip to content

Commit bab9f1e

Browse files
committed
fix(arborist): use the edge-based registry check on the install path too and cover the mixed registry/file: case
1 parent a32d37f commit bab9f1e

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

lib/commands/patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class Patch extends BaseCommand {
104104
}
105105
}
106106

107-
// a version cannot be patched if a consumer depends on it through a non-registry spec (file:, git:, link:).
107+
// a version cannot be patched if a consumer depends on it through a non-registry spec (file:, git:, http(s)); npm: aliases stay registry.
108108
// checking the edges (not isRegistryDependency) avoids rejecting edgeless store nodes and linked symlinks, which are registry deps.
109109
const ensureRegistry = version => {
110110
const nodes = installed.get(version) || []

test/lib/commands/patch.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,38 @@ t.test('add: an installed file: dependency is rejected as non-registry', async t
355355
)
356356
})
357357

358+
t.test('add: a version installed as both registry and file: is rejected', async t => {
359+
// one consumer pulls the registry copy, another pulls a file: copy of the same version;
360+
// the file: edge must still cause a rejection even though a registry edge also exists
361+
const { npm } = await loadMockNpm(t, {
362+
config: { 'ignore-scripts': true, audit: false },
363+
prefixDir: {
364+
'package.json': JSON.stringify({
365+
name: 'root-project',
366+
version: '1.0.0',
367+
dependencies: { [DEP_NAME]: '1.0.0', b: '1.0.0' },
368+
}),
369+
local: { 'package.json': JSON.stringify({ name: DEP_NAME, version: DEP_VERSION }) },
370+
node_modules: {
371+
[DEP_NAME]: { 'package.json': JSON.stringify({ name: DEP_NAME, version: DEP_VERSION }) },
372+
b: {
373+
'package.json': JSON.stringify({
374+
name: 'b', version: '1.0.0', dependencies: { [DEP_NAME]: 'file:../../local' },
375+
}),
376+
node_modules: {
377+
[DEP_NAME]: { 'package.json': JSON.stringify({ name: DEP_NAME, version: DEP_VERSION }) },
378+
},
379+
},
380+
},
381+
},
382+
})
383+
await t.rejects(
384+
npm.exec('patch', ['add', DEP_NAME]),
385+
{ code: 'EPATCHNONREGISTRY' },
386+
'a version with any file: consumer cannot be patched'
387+
)
388+
})
389+
358390
t.test('add: a range matching multiple installed versions is ambiguous', async t => {
359391
const { npm } = await loadMockNpm(t, {
360392
config: { 'ignore-scripts': true, audit: false },

workspaces/arborist/lib/patched-dependencies.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Attaches node.patched = { path, integrity } to each matched node.
33
// Enforces the failure modes (workspace-member entry, missing file, unused patch, non-registry target, ambiguous selectors) as hard errors.
44
const semver = require('semver')
5+
const npa = require('npm-package-arg')
56
const { resolve, relative, isAbsolute } = require('node:path')
67
const { readFile } = require('node:fs/promises')
78
const { patchIntegrity } = require('./patch.js')
@@ -118,8 +119,9 @@ const resolvePatchedDependencies = async (tree, { path, allowUnusedPatches }) =>
118119
continue
119120
}
120121

121-
// links and other non-registry resolutions cannot be patched
122-
if (node.isLink || !node.isRegistryDependency) {
122+
// a non-registry consumer edge (file:, git:, http(s)) means there is no registry tarball to patch; npm: aliases stay registry.
123+
// checking edges (not isRegistryDependency) avoids rejecting an edgeless node, which is still a registry dep.
124+
if ([...node.edgesIn].some(e => e.spec && !npa(e.spec).registry)) {
123125
throw err(
124126
`Cannot patch non-registry dependency ${node.name}@${node.version} ` +
125127
`(selector "${selector.key}"). Only registry dependencies can be patched.`,

0 commit comments

Comments
 (0)