From d18f367bbcca2151e2ad1632c99263940de6f860 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Wed, 15 Jul 2026 06:15:38 +0300 Subject: [PATCH] fix: refresh npx cache for local bin changes --- workspaces/libnpmexec/lib/index.js | 40 +++++++++++++++++------------ workspaces/libnpmexec/test/local.js | 36 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/workspaces/libnpmexec/lib/index.js b/workspaces/libnpmexec/lib/index.js index 4d7c126654689..8e732e22bf849 100644 --- a/workspaces/libnpmexec/lib/index.js +++ b/workspaces/libnpmexec/lib/index.js @@ -24,14 +24,19 @@ const withLock = require('./with-lock.js') const manifests = new Map() const getManifest = async (spec, flatOptions) => { + const loadManifest = () => pacote.manifest(spec, { + ...flatOptions, + preferOnline: true, + Arborist, + _isRoot: true, + }) + + if (spec.type === 'directory') { + return loadManifest() + } + if (!manifests.has(spec.raw)) { - const manifest = await pacote.manifest(spec, { - ...flatOptions, - preferOnline: true, - Arborist, - _isRoot: true, - }) - manifests.set(spec.raw, manifest) + manifests.set(spec.raw, await loadManifest()) } return manifests.get(spec.raw) } @@ -247,16 +252,19 @@ const exec = async (opts) => { if (!npxCache) { throw new Error('Must provide a valid npxCache path') } + const npxCacheKeyPackages = await Promise.all(packages.map(async p => { + // Keeps the npx directory unique to the resolved directory, not the + // potentially relative one (i.e. "npx ."). Directory specs also include + // their bin map so newly added local bins do not reuse stale shims. + const spec = npa(p) + if (spec.type === 'directory') { + const manifest = await getManifest(spec, flatOptions) + return `${spec.fetchSpec}\n${JSON.stringify(manifest.bin)}` + } + return p + })) const hash = crypto.createHash('sha512') - .update(packages.map(p => { - // Keeps the npx directory unique to the resolved directory, not the - // potentially relative one (i.e. "npx .") - const spec = npa(p) - if (spec.type === 'directory') { - return spec.fetchSpec - } - return p - }).sort((a, b) => a.localeCompare(b, 'en')).join('\n')) + .update(npxCacheKeyPackages.sort((a, b) => a.localeCompare(b, 'en')).join('\n')) .digest('hex') .slice(0, 16) const installDir = resolve(npxCache, hash) diff --git a/workspaces/libnpmexec/test/local.js b/workspaces/libnpmexec/test/local.js index 46700ea535f81..43492dcbc1eae 100644 --- a/workspaces/libnpmexec/test/local.js +++ b/workspaces/libnpmexec/test/local.js @@ -470,6 +470,42 @@ for (const allowDirectory of ['none', 'root']) { }) } +t.test('local package bin changes reinstall the npx cache entry', async t => { + const { pkg, fixtures } = createPkg({ + version: '1.0.0', + name: '@npmcli/local-pkg-bin-change-test', + bin: { + a: 'a.js', + }, + files: { + 'a.js': { key: 'local-bin-a', value: 'A' }, + }, + }) + + const { exec, chmod, readOutput, path } = setup(t, { + pkg, + testdir: fixtures.packages['@npmcli-local-pkg-bin-change-test-1.0.0'], + }) + + await chmod('a.js') + await exec({ args: ['a'] }) + t.match(await readOutput('local-bin-a'), { + value: 'A', + }) + + await fs.writeFile(resolve(path, 'b.js'), '#!/usr/bin/env node\nrequire("fs").writeFileSync("output-local-bin-b", JSON.stringify({ value: "B", args: process.argv.slice(2) }))') + await chmod('b.js') + const packageJson = JSON.parse(await fs.readFile(resolve(path, 'package.json'), 'utf8')) + packageJson.bin.b = 'b.js' + await fs.writeFile(resolve(path, 'package.json'), JSON.stringify(packageJson, null, 2)) + + await exec({ args: ['b', 'argument-b'] }) + t.match(await readOutput('local-bin-b'), { + value: 'B', + args: ['argument-b'], + }) +}) + t.test('npm exec sequential workspace runs with same-named local bins', async t => { t.plan(2) const { path, readOutput, rmOutput, registry } = setup(t, {