From ce8a8e01311992ba4ea2059b41b4eb757374599d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Fl=C3=B6gel?= Date: Mon, 17 Nov 2025 10:54:12 +0100 Subject: [PATCH] fix(arborist): fix usage of path of custom registry Properly handle the resolution of a path via a custom registry with a path element in the URL, when the path element is a prefix of a package name. When a custom registry with a path element is used (e.g. http://localhost:3080/npm/), then the resolution of download URLs for packages starting with the path element (in this case `npm`, so e.g. `npm-run-path`) will fail as the path element gets omitted. ## References Fixes #8736 --- workspaces/arborist/lib/arborist/reify.js | 14 ++++-- workspaces/arborist/test/arborist/reify.js | 56 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index abc2c8a5dd0bd..ff71044536d8c 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -813,9 +813,17 @@ module.exports = cls => class Reifier extends cls { // Make sure we don't double-include the path if it's already there const registryPath = registryURL.pathname.replace(/\/$/, '') - if (registryPath && registryPath !== '/' && !resolvedURL.pathname.startsWith(registryPath)) { - // Since hostname is changed, we need to ensure the registry path is included - resolvedURL.pathname = registryPath + resolvedURL.pathname + if (registryPath && registryPath !== '/') { + // Check if the resolved pathname already starts with the registry path + // We need to ensure it's a proper path prefix, not just a string prefix + // e.g., registry path '/npm' should not match '/npm-run-path' + const hasRegistryPath = resolvedURL.pathname === registryPath || + resolvedURL.pathname.startsWith(registryPath + '/') + + if (!hasRegistryPath) { + // Since hostname is changed, we need to ensure the registry path is included + resolvedURL.pathname = registryPath + resolvedURL.pathname + } } return resolvedURL.toString() diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 4209c605a9791..9d407d5c8033f 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -3617,6 +3617,62 @@ t.test('should preserve exact ranges, missing actual tree', async (t) => { await t.resolves(arb.reify(), 'reify should complete successfully') }) + t.test('registry path prepending with registry path being a package name prefix', async t => { + // A registry path is prepended to resolved URLs that don't already have it + const abbrevPackument4 = 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: { + // Note: This URL has no path component that matches our registry path + tarball: 'https://external-registry.example.com/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', + }, + }), + }, + }) + + // Set up the registry with a deep path + const registryHost = 'https://registry.example.com' + // Note: This path is a prefix of the package name 'abbrev' + const registryPath = '/abb' + const registry = `${registryHost}${registryPath}` + + tnock(t, registryHost) + .get(`${registryPath}/abbrev`) + .reply(200, abbrevPackument4) + + // This is the critical test - the tarball URL in the packument doesn't have our registry path, but when replaceRegistryHost is 'always', we should get a request to this URL which includes the registry path + tnock(t, registryHost) + .get(`${registryPath}/abbrev-1.1.1.tgz`) + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry, + cache: resolve(testdir, 'cache'), + replaceRegistryHost: 'always', + }) + + await t.resolves(arb.reify(), 'reify should complete successfully') + }) + t.test('registry with different protocol should swap protocol', async (t) => { const abbrevPackument4 = JSON.stringify({ _id: 'abbrev',