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

feat: make npm owner workspace aware #4835

Merged
merged 1 commit into from
May 3, 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
46 changes: 46 additions & 0 deletions docs/content/commands/npm-owner.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,52 @@ password, npm will prompt on the command line for one.
<!-- automatically generated, do not edit manually -->
<!-- see lib/utils/config/definitions.js -->

#### `workspace`

* Default:
* Type: String (can be set multiple times)

Enable running a command in the context of the configured workspaces of the
current project while filtering by running only the workspaces defined by
this configuration option.

Valid values for the `workspace` config are either:

* Workspace names
* Path to a workspace directory
* Path to a parent workspace directory (will result in selecting all
workspaces within that folder)

When set for the `npm init` command, this may be set to the folder of a
workspace which does not yet exist, to create the folder and set it up as a
brand new workspace within the project.

This value is not exported to the environment for child processes.

<!-- automatically generated, do not edit manually -->
<!-- see lib/utils/config/definitions.js -->

#### `workspaces`

* Default: null
* Type: null or Boolean

Set to true to run the command in the context of **all** configured
workspaces.

Explicitly setting this to false will cause commands like `install` to
ignore workspaces altogether. When not set explicitly:

- Commands that operate on the `node_modules` tree (install, update, etc.)
will link workspaces into the `node_modules` folder. - Commands that do
other things (test, exec, publish, etc.) will operate on the root project,
_unless_ one or more workspaces are specified in the `workspace` config.

This value is not exported to the environment for child processes.

<!-- automatically generated, do not edit manually -->
<!-- see lib/utils/config/definitions.js -->

<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->

### See Also
Expand Down
51 changes: 37 additions & 14 deletions lib/commands/owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class Owner extends BaseCommand {
static params = [
'registry',
'otp',
'workspace',
'workspaces',
]

static usage = [
Expand Down Expand Up @@ -69,22 +71,43 @@ class Owner extends BaseCommand {
}

async exec ([action, ...args]) {
switch (action) {
case 'ls':
case 'list':
return this.ls(args[0])
case 'add':
return this.changeOwners(args[0], args[1], 'add')
case 'rm':
case 'remove':
return this.changeOwners(args[0], args[1], 'rm')
default:
if (action === 'ls' || action === 'list') {
await this.ls(args[0])
} else if (action === 'add') {
await this.changeOwners(args[0], args[1], 'add')
} else if (action === 'rm' || action === 'remove') {
await this.changeOwners(args[0], args[1], 'rm')
} else {
throw this.usageError()
}
}

async execWorkspaces ([action, ...args], filters) {
await this.setWorkspaces(filters)
// ls pkg or owner add/rm package
if ((action === 'ls' && args.length > 0) || args.length > 1) {
const implicitWorkspaces = this.npm.config.get('workspace', 'default')
if (implicitWorkspaces.length === 0) {
log.warn(`Ignoring specified workspace(s)`)
}
return this.exec([action, ...args])
}

for (const [name] of this.workspaces) {
if (action === 'ls' || action === 'list') {
await this.ls(name)
} else if (action === 'add') {
await this.changeOwners(args[0], name, 'add')
} else if (action === 'rm' || action === 'remove') {
await this.changeOwners(args[0], name, 'rm')
} else {
throw this.usageError()
}
}
}

async ls (pkg) {
pkg = await this.getPkg(pkg)
pkg = await this.getPkg(this.npm.prefix, pkg)
const spec = npa(pkg)

try {
Expand All @@ -101,12 +124,12 @@ class Owner extends BaseCommand {
}
}

async getPkg (pkg) {
async getPkg (prefix, pkg) {
if (!pkg) {
if (this.npm.config.get('global')) {
throw this.usageError()
}
const { name } = await readJson(resolve(this.npm.prefix, 'package.json'))
const { name } = await readJson(resolve(prefix, 'package.json'))
if (!name) {
throw this.usageError()
}
Expand All @@ -121,7 +144,7 @@ class Owner extends BaseCommand {
throw this.usageError()
}

pkg = await this.getPkg(pkg)
pkg = await this.getPkg(this.npm.prefix, pkg)
log.verbose(`owner ${addOrRm}`, '%s to %s', user, pkg)

const spec = npa(pkg)
Expand Down
2 changes: 2 additions & 0 deletions tap-snapshots/test/lib/load-all-commands.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ npm owner ls [<@scope>/]<pkg>

Options:
[--registry <registry>] [--otp <otp>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]

alias: author

Expand Down
2 changes: 2 additions & 0 deletions tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,8 @@ All commands:

Options:
[--registry <registry>] [--otp <otp>]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]

alias: author

Expand Down
189 changes: 189 additions & 0 deletions test/lib/commands/owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const t = require('tap')
const { load: loadMockNpm } = require('../../fixtures/mock-npm.js')
const MockRegistry = require('../../fixtures/mock-registry.js')

const path = require('path')
const npa = require('npm-package-arg')
const packageName = '@npmcli/test-package'
const spec = npa(packageName)
Expand All @@ -12,6 +13,42 @@ const maintainers = [
{ email: '[email protected]', name: 'test-user-b' },
]

const workspaceFixture = {
'package.json': JSON.stringify({
name: packageName,
version: '1.2.3-test',
workspaces: ['workspace-a', 'workspace-b', 'workspace-c'],
}),
'workspace-a': {
'package.json': JSON.stringify({
name: 'workspace-a',
version: '1.2.3-a',
}),
},
'workspace-b': {
'package.json': JSON.stringify({
name: 'workspace-b',
version: '1.2.3-n',
}),
},
'workspace-c': JSON.stringify({
'package.json': {
name: 'workspace-n',
version: '1.2.3-n',
},
}),
}

function registryPackage (t, registry, name) {
const mockRegistry = new MockRegistry({ tap: t, registry })

const manifest = mockRegistry.manifest({
name,
packuments: [{ maintainers, version: '1.0.0' }],
})
mockRegistry.package({ manifest })
}

t.test('owner no args', async t => {
const { npm } = await loadMockNpm(t)
await t.rejects(
Expand Down Expand Up @@ -429,6 +466,158 @@ t.test('owner rm <user> no cwd package', async t => {
)
})

t.test('workspaces', async t => {
t.test('owner no args --workspace', async t => {
const { npm } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
})
npm.config.set('workspace', ['workspace-a'])
await t.rejects(
npm.exec('owner', []),
{ code: 'EUSAGE' },
'rejects with usage'
)
})

t.test('owner ls implicit workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
globals: ({ prefix }) => ({
'process.cwd': () => path.join(prefix, 'workspace-a'),
}),
})
registryPackage(t, npm.config.get('registry'), 'workspace-a')
await npm.exec('owner', ['ls'])
t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
})

t.test('owner ls explicit workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
globals: ({ prefix }) => ({
'process.cwd': () => prefix,
}),
})
npm.config.set('workspace', ['workspace-a'])
registryPackage(t, npm.config.get('registry'), 'workspace-a')
await npm.exec('owner', ['ls'])
t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
})

t.test('owner ls <pkg> implicit workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
globals: ({ prefix }) => ({
'process.cwd': () => path.join(prefix, 'workspace-a'),
}),
})
registryPackage(t, npm.config.get('registry'), packageName)
await npm.exec('owner', ['ls', packageName])
t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
})

t.test('owner ls <pkg> explicit workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
globals: ({ prefix }) => ({
'process.cwd': () => prefix,
}),
})
npm.config.set('workspace', ['workspace-a'])
registryPackage(t, npm.config.get('registry'), packageName)
await npm.exec('owner', ['ls', packageName])
t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
})

t.test('owner add implicit workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
globals: ({ prefix }) => ({
'process.cwd': () => path.join(prefix, 'workspace-a'),
}),
})
const username = 'foo'
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })

const manifest = registry.manifest({
name: 'workspace-a',
packuments: [{ maintainers, version: '1.0.0' }],
})
registry.package({ manifest })
registry.couchuser({ username })
registry.nock.put(`/workspace-a/-rev/${manifest._rev}`, body => {
t.match(body, {
_id: manifest._id,
_rev: manifest._rev,
maintainers: [
...manifest.maintainers,
{ name: username, email: '' },
],
})
return true
}).reply(200, {})
await npm.exec('owner', ['add', username])
t.equal(joinedOutput(), `+ ${username} (workspace-a)`)
})

t.test('owner add --workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
})
npm.config.set('workspace', ['workspace-a'])
const username = 'foo'
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })

const manifest = registry.manifest({
name: 'workspace-a',
packuments: [{ maintainers, version: '1.0.0' }],
})
registry.package({ manifest })
registry.couchuser({ username })
registry.nock.put(`/workspace-a/-rev/${manifest._rev}`, body => {
t.match(body, {
_id: manifest._id,
_rev: manifest._rev,
maintainers: [
...manifest.maintainers,
{ name: username, email: '' },
],
})
return true
}).reply(200, {})
await npm.exec('owner', ['add', username])
t.equal(joinedOutput(), `+ ${username} (workspace-a)`)
})

t.test('owner rm --workspace', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: workspaceFixture,
globals: ({ prefix }) => ({
'process.cwd': () => path.join(prefix, 'workspace-a'),
}),
})
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })

const username = maintainers[0].name
const manifest = registry.manifest({
name: 'workspace-a',
packuments: [{ maintainers, version: '1.0.0' }],
})
registry.package({ manifest })
registry.couchuser({ username })
registry.nock.put(`/workspace-a/-rev/${manifest._rev}`, body => {
t.match(body, {
_id: manifest._id,
_rev: manifest._rev,
maintainers: maintainers.slice(1),
})
return true
}).reply(200, {})
await npm.exec('owner', ['rm', username])
t.equal(joinedOutput(), `- ${username} (workspace-a)`)
})
})

t.test('completion', async t => {
t.test('basic commands', async t => {
const { npm } = await loadMockNpm(t)
Expand Down
2 changes: 1 addition & 1 deletion test/lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ t.test('implicit workspace rejection', async t => {
}),
})
await t.rejects(
mock.npm.exec('owner', []),
mock.npm.exec('team', []),
/This command does not support workspaces/
)
})
Expand Down