Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions workspaces/libnpmexec/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ const runScript = require('./run-script.js')
const isWindows = require('./is-windows.js')
const withLock = require('./with-lock.js')

const binPaths = []

// when checking the local tree we look up manifests, cache those results by
// spec.raw so we don't have to fetch again when we check npxCache
const manifests = new Map()
Expand Down Expand Up @@ -113,6 +111,8 @@ const exec = async (opts) => {
...flatOptions
} = opts

const binPaths = []

let pkgPaths = opts.pkgPath
if (typeof pkgPaths === 'string') {
pkgPaths = [pkgPaths]
Expand Down Expand Up @@ -196,7 +196,11 @@ const exec = async (opts) => {
let commandManifest
await Promise.all(packages.map(async (pkg, i) => {
const spec = npa(pkg, path)
const { manifest, node } = await missingFromTree({ spec, tree: localTree, flatOptions })
const { manifest, node } = await missingFromTree({
spec,
tree: localTree,
flatOptions,
})
if (manifest) {
// Package does not exist in the local tree
needInstall.push({ spec, manifest })
Expand Down
63 changes: 63 additions & 0 deletions workspaces/libnpmexec/test/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,66 @@ for (const allowDirectory of ['none', 'root']) {
})
})
}

t.test('npm exec sequential workspace runs with same-named local bins', async t => {
t.plan(2)
const { path, readOutput, rmOutput, registry } = setup(t, {
testdir: {
packages: {
a: {
'package.json': {
name: 'workspace-a',
version: '1.0.0',
bin: { 'shared-bin': 'cli.js' },
},
'cli.js': { key: 'shared-bin', value: 'A' },
},
b: {
'package.json': {
name: 'workspace-b',
version: '1.0.0',
bin: { 'shared-bin': 'cli.js' },
},
'cli.js': { key: 'shared-bin', value: 'B' },
},
},
},
})

// We mock the module exactly once. This simulates two sequential calls
// hitting the same module instance, allowing us to prove that state
// (like binPaths) is correctly cleared between runs and NOT preserved.
const libnpmexec = t.mock('../lib/index.js')

const baseOpts = {
path,
runPath: path,
npxCache: resolve(path, 'npxCache'),
registry: registry.origin + '/',
localBin: resolve(path, 'node_modules', '.bin'),
call: '',
scriptShell: 'sh',
yes: true, // skip interactive prompt for npxCache install
}

// Workspace A
await libnpmexec({
...baseOpts,
pkgPath: resolve(path, 'packages/a'),
args: ['shared-bin'],
})

const outputA = await readOutput('shared-bin')
t.equal(outputA.value, 'A', 'should run workspace A bin')
await rmOutput('shared-bin')

// Workspace B
await libnpmexec({
...baseOpts,
pkgPath: resolve(path, 'packages/b'),
args: ['shared-bin'],
})

const outputB = await readOutput('shared-bin')
t.equal(outputB.value, 'B', 'should run workspace B bin, not cached workspace A bin')
})
Loading