Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(libnpmexec): workspaces support #4643

Merged
merged 1 commit into from
Apr 19, 2022
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
8 changes: 3 additions & 5 deletions lib/commands/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ class Exec extends BaseCommand {
static ignoreImplicitWorkspace = false
static isShellout = true

async exec (_args, { locationMsg, path, runPath } = {}) {
if (!path) {
path = this.npm.localPrefix
}
async exec (_args, { locationMsg, runPath } = {}) {
const path = this.npm.localPrefix

if (!runPath) {
runPath = process.cwd()
Expand Down Expand Up @@ -95,7 +93,7 @@ class Exec extends BaseCommand {

for (const path of this.workspacePaths) {
const locationMsg = await getLocationMsg({ color, path })
await this.exec(args, { locationMsg, path, runPath: path })
await this.exec(args, { locationMsg, runPath: path })
}
}
}
Expand Down
156 changes: 104 additions & 52 deletions test/lib/commands/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1128,10 +1128,11 @@ t.test('forward legacyPeerDeps opt', async t => {
)
})

t.test('workspaces', t => {
t.test('workspaces', async t => {
npm.localPrefix = t.testdir({
node_modules: {
'.bin': {
a: '',
foo: '',
},
},
Expand Down Expand Up @@ -1159,68 +1160,119 @@ t.test('workspaces', t => {
})

PROGRESS_IGNORED = true
npm.localBin = resolve(npm.localPrefix, 'node_modules/.bin')
npm.localBin = resolve(npm.localPrefix, 'node_modules', '.bin')

t.test('with args, run scripts in the context of a workspace', async t => {
await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'])
// with arg matching existing bin, run scripts in the context of a workspace
await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b'])

t.match(RUN_SCRIPTS, [
{
pkg: { scripts: { npx: 'foo' } },
args: ['one arg', 'two arg'],
banner: false,
path: process.cwd(),
stdioString: true,
event: 'npx',
env: {
PATH: [npm.localBin, process.env.PATH].join(delimiter),
},
stdio: 'inherit',
t.match(RUN_SCRIPTS, [
{
pkg: { scripts: { npx: 'foo' } },
args: ['one arg', 'two arg'],
banner: false,
path: npm.localPrefix,
stdioString: true,
event: 'npx',
env: {
PATH: [npm.localBin, process.env.PATH].join(delimiter),
},
])
})
stdio: 'inherit',
},
{
pkg: { scripts: { npx: 'foo' } },
args: ['one arg', 'two arg'],
banner: false,
path: npm.localPrefix,
stdioString: true,
event: 'npx',
env: {
PATH: [npm.localBin, process.env.PATH].join(delimiter),
},
stdio: 'inherit',
},
], 'should run with multiple args across multiple workspaces')

t.test('no args, spawn interactive shell', async t => {
CI_NAME = null
process.stdin.isTTY = true
// clean up
RUN_SCRIPTS.length = 0

await exec.execWorkspaces([], ['a'])
// with packages, run scripts in the context of a workspace
config.package = ['foo']
config.call = 'foo'
config.yes = false

t.strictSame(LOG_WARN, [])
t.strictSame(
npm._mockOutputs,
ARB_ACTUAL_TREE[npm.localPrefix] = {
children: new Map([['foo', { name: 'foo', version: '1.2.3' }]]),
}

await exec.execWorkspaces([], ['a', 'b'])

// path should point to the workspace folder
t.match(RUN_SCRIPTS, [
{
pkg: { scripts: { npx: 'foo' } },
args: [],
banner: false,
path: resolve(npm.localPrefix, 'packages', 'a'),
stdioString: true,
event: 'npx',
stdio: 'inherit',
},
{
pkg: { scripts: { npx: 'foo' } },
args: [],
banner: false,
path: resolve(npm.localPrefix, 'packages', 'b'),
stdioString: true,
event: 'npx',
stdio: 'inherit',
},
], 'should run without args in multiple workspaces')

t.match(ARB_CTOR, [
{ path: npm.localPrefix },
{ path: npm.localPrefix },
])

// no args, spawn interactive shell
CI_NAME = null
config.package = []
config.call = ''
process.stdin.isTTY = true

await exec.execWorkspaces([], ['a'])

t.strictSame(LOG_WARN, [])
t.strictSame(
npm._mockOutputs,
[
[
[
`\nEntering npm script environment in workspace [email protected] at location:\n${resolve(
npm.localPrefix,
'packages/a'
)}\nType 'exit' or ^D when finished\n`,
],
`\nEntering npm script environment in workspace [email protected] at location:\n${resolve(
npm.localPrefix,
'packages/a'
)}\nType 'exit' or ^D when finished\n`,
],
'printed message about interactive shell'
)
],
'printed message about interactive shell'
)

npm.color = true
flatOptions.color = true
npm._mockOutputs.length = 0
await exec.execWorkspaces([], ['a'])
npm.color = true
flatOptions.color = true
npm._mockOutputs.length = 0
await exec.execWorkspaces([], ['a'])

t.strictSame(LOG_WARN, [])
t.strictSame(
npm._mockOutputs,
t.strictSame(LOG_WARN, [])
t.strictSame(
npm._mockOutputs,
[
[
[
/* eslint-disable-next-line max-len */
`\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[[email protected]\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve(
npm.localPrefix,
'packages/a'
/* eslint-disable-next-line max-len */
`\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[[email protected]\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve(
npm.localPrefix,
'packages/a'
/* eslint-disable-next-line max-len */
)}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`,
],
)}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`,
],
'printed message about interactive shell'
)
})

t.end()
],
'printed message about interactive shell'
)
})
76 changes: 76 additions & 0 deletions workspaces/libnpmexec/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,82 @@ t.test('local pkg, must not fetch manifest for avail pkg', async t => {
t.equal(res, 'LOCAL PKG', 'should run local pkg bin script')
})

t.test('multiple local pkgs', async t => {
const foo = {
name: '@ruyadorno/create-foo',
version: '2.0.0',
bin: {
'create-foo': './index.js',
},
}
const bar = {
name: '@ruyadorno/create-bar',
version: '2.0.0',
bin: {
'create-bar': './index.js',
},
}
const path = t.testdir({
cache: {},
npxCache: {},
node_modules: {
'.bin': {},
'@ruyadorno': {
'create-foo': {
'package.json': JSON.stringify(foo),
'index.js': `#!/usr/bin/env node
require('fs').writeFileSync(process.argv.slice(2)[0], 'foo')`,
},
'create-bar': {
'package.json': JSON.stringify(bar),
'index.js': `#!/usr/bin/env node
require('fs').writeFileSync(process.argv.slice(2)[0], 'bar')`,
},
},
},
'package.json': JSON.stringify({
name: 'pkg',
dependencies: {
'@ruyadorno/create-foo': '^2.0.0',
'@ruyadorno/create-bar': '^2.0.0',
},
}),
})
const runPath = path
const cache = resolve(path, 'cache')
const npxCache = resolve(path, 'npxCache')

const setupBins = async (pkg) => {
const executable =
resolve(path, `node_modules/${pkg.name}/index.js`)
fs.chmodSync(executable, 0o775)

await binLinks({
path: resolve(path, `node_modules/${pkg.name}`),
pkg,
})
}

await Promise.all([foo, bar]
.map(setupBins))

await libexec({
...baseOpts,
localBin: resolve(path, 'node_modules/.bin'),
cache,
npxCache,
packages: ['@ruyadorno/create-foo', '@ruyadorno/create-bar'],
call: 'create-foo resfile && create-bar bar',
path,
runPath,
})

const resFoo = fs.readFileSync(resolve(path, 'resfile')).toString()
t.equal(resFoo, 'foo', 'should run local pkg bin script')
const resBar = fs.readFileSync(resolve(path, 'bar')).toString()
t.equal(resBar, 'bar', 'should run local pkg bin script')
})

t.test('local file system path', async t => {
const path = t.testdir({
cache: {},
Expand Down