Skip to content

Commit f1745ac

Browse files
committed
fix(arborist): honor allow-remote=root for root-direct remote tarballs (npm#9510)
In continuation of our exploration of using `install-strategy=linked` in the [Gutenberg monorepo](WordPress/gutenberg#75814), which powers the WordPress Block Editor. Under `install-strategy=linked` with `allow-remote=root`, a fresh install fails with `EALLOWREMOTE` on a genuine remote (non-registry) tarball that is a direct dependency of the project root or a workspace. The standard (hoisted) reifier installs the same dependency fine under `allow-remote=root`; only the linked strategy rejects it. ``` npm error code EALLOWREMOTE npm error Fetching non-root packages of type "remote" have been disabled npm error Refusing to fetch "@react-native-community/slider@https://raw.githubusercontent.com/wordpress-mobile/react-native-slider/v3.0.2-wp-5/react-native-community-slider-3.0.2-wp-5.tgz" ``` ## Why The `allow-remote=root` gate is enforced at reify time by computing `_isRoot` and passing it to `pacote.extract` in `reify.js`. A node counts as "root" if it satisfies at least one valid dependency edge from the project root or a workspace, which is derived from `node.edgesIn`. In the linked strategy, store nodes are `IsolatedNode` instances with no `edgesIn` to recompute root-ness from, so `_isRoot` was always `false`, every remote tarball was treated as non-root, and pacote refused even a legitimate root/workspace direct dependency. This is the sibling of the registry-tarball fix (npm#9495). That change carried `isRegistryDependency` onto store nodes so the registry-tarball exemption still applied; this change carries the analogous root-ness signal so the `allow-remote=root` gate resolves correctly for genuine remote tarballs, which are not registry-mediated and so do not qualify for the registry exemption. This only widens `allow-remote=root`. `allow-remote=none` still rejects all remote specs (pacote refuses regardless of `_isRoot`), and a genuinely transitive remote dependency still fails the resolution-layer `#checkAllow` gate during ideal-tree construction. Hoisted is unaffected because its nodes retain real `edgesIn`. ## How Carry a root-ness flag from the source tree node onto the store node, rather than weakening the guard: 1. `IsolatedNode` gains an `isRootDependency` field (default `false`), settable from constructor options. 2. `#externalProxy` computes `isRootDependency` from the real tree node's `edgesIn` using the same predicate the reifier applies (a valid edge from the project root or a workspace). 3. `#generateChild` passes it through to the store `IsolatedNode`. 4. The `_isRoot` computation in `reify.js` falls back to `node.isRootDependency`. Hoisted nodes do not have the property, so they fall through to the existing edge-based check unchanged. ## References Fixes npm#9509 Follows-up npm#9495
1 parent bcf01c6 commit f1745ac

4 files changed

Lines changed: 41 additions & 1 deletion

File tree

workspaces/arborist/lib/arborist/isolated-reifier.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module.exports = cls => class IsolatedReifier extends cls {
4343
isInStore,
4444
inBundle,
4545
isRegistryDependency: node.isRegistryDependency,
46+
isRootDependency: node.isRootDependency,
4647
location,
4748
name: node.packageName || node.name,
4849
optional: node.optional,
@@ -157,6 +158,10 @@ module.exports = cls => class IsolatedReifier extends cls {
157158
// Carry the source node's registry-dependency flag so the store node retains it.
158159
// IsolatedNode has no edges to recompute it from, and reify's registry-tarball allow-remote exemption depends on it.
159160
result.isRegistryDependency = node.isRegistryDependency
161+
// Same reasoning for allow-remote=root: the store node has no edgesIn, so capture from the source node whether it satisfies a valid edge from the project root or a workspace.
162+
result.isRootDependency = [...node.edgesIn].some(e =>
163+
e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace)
164+
)
160165
return result
161166
}
162167

workspaces/arborist/lib/arborist/reify.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,8 @@ module.exports = cls => class Reifier extends cls {
707707
integrity: node.integrity,
708708
// A node counts as "root" for allow-* enforcement if it satisfies at least one valid dependency edge declared by the project root or a workspace.
709709
// node.parent is unsafe here: after hoisting, transitive packages can have the project root as their tree parent.
710-
_isRoot: [...node.edgesIn].some(e =>
710+
// In the linked strategy the store node has no edgesIn, so isolated-reifier precomputes isRootDependency from the source node's edges.
711+
_isRoot: node.isRootDependency || [...node.edgesIn].some(e =>
711712
e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace)
712713
),
713714
// pacote's npa re-parses our `name@URL` spec as type=remote, so allowRemote would mis-fire on registry tarballs.

workspaces/arborist/lib/isolated-classes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class IsolatedNode {
2121
isInStore = false
2222
inBundle = false
2323
isRegistryDependency = false
24+
isRootDependency = false
2425
linksIn = new Set()
2526
meta = { loadedFromDisk: false }
2627
optional = false
@@ -54,6 +55,9 @@ class IsolatedNode {
5455
if (options.isRegistryDependency) {
5556
this.isRegistryDependency = true
5657
}
58+
if (options.isRootDependency) {
59+
this.isRootDependency = true
60+
}
5761
if (options.optional) {
5862
this.optional = true
5963
}

workspaces/arborist/test/arborist/reify.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3904,6 +3904,36 @@ t.test('should preserve exact ranges, missing actual tree', async (t) => {
39043904
await t.resolves(arb.reify(), 'registry tarball is allowed under linked strategy')
39053905
})
39063906

3907+
t.test('allowRemote=root allows root-direct remote tarball under linked install strategy', async t => {
3908+
// The linked strategy extracts store nodes as IsolatedNode, which has no edgesIn to recompute root-ness from.
3909+
// isRootDependency must be carried from the source tree node, otherwise allow-remote=root mis-fires on a genuine remote tarball that is a direct dep of the project root.
3910+
const testdir = t.testdir({
3911+
project: {
3912+
'package.json': JSON.stringify({
3913+
name: 'myproject',
3914+
version: '1.0.0',
3915+
dependencies: {
3916+
abbrev: 'https://remote.example.com/abbrev-1.1.1.tgz',
3917+
},
3918+
}),
3919+
},
3920+
})
3921+
3922+
tnock(t, 'https://remote.example.com')
3923+
.get('/abbrev-1.1.1.tgz')
3924+
.reply(200, abbrevTGZ)
3925+
3926+
const arb = new Arborist({
3927+
path: resolve(testdir, 'project'),
3928+
registry: 'https://registry.example.com',
3929+
cache: resolve(testdir, 'cache'),
3930+
allowRemote: 'root',
3931+
installStrategy: 'linked',
3932+
})
3933+
3934+
await t.resolves(arb.reify(), 'root-direct remote tarball is allowed under linked strategy with allow-remote=root')
3935+
})
3936+
39073937
t.test('registry with different protocol should swap protocol', async (t) => {
39083938
const abbrevPackument4 = JSON.stringify({
39093939
_id: 'abbrev',

0 commit comments

Comments
 (0)