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(npm) pass npm context everywhere #2772

Merged
merged 1 commit into from Mar 4, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
285 changes: 157 additions & 128 deletions lib/access.js
Expand Up @@ -3,25 +3,11 @@ const path = require('path')
const libaccess = require('libnpmaccess')
const readPackageJson = require('read-package-json-fast')

const npm = require('./npm.js')
const output = require('./utils/output.js')
const otplease = require('./utils/otplease.js')
const usageUtil = require('./utils/usage.js')
const getIdentity = require('./utils/get-identity.js')

const usage = usageUtil(
'access',
'npm access public [<package>]\n' +
'npm access restricted [<package>]\n' +
'npm access grant <read-only|read-write> <scope:team> [<package>]\n' +
'npm access revoke <scope:team> [<package>]\n' +
'npm access 2fa-required [<package>]\n' +
'npm access 2fa-not-required [<package>]\n' +
'npm access ls-packages [<user>|<scope>|<scope:team>]\n' +
'npm access ls-collaborators [<package> [<user>]]\n' +
'npm access edit [<package>]'
)

const subcommands = [
'public',
'restricted',
Expand All @@ -34,152 +20,195 @@ const subcommands = [
'2fa-not-required',
]

const UsageError = (msg) =>
Object.assign(new Error(`\nUsage: ${msg}\n\n` + usage), {
code: 'EUSAGE',
})

const cmd = (args, cb) =>
access(args)
.then(x => cb(null, x))
.catch(err => err.code === 'EUSAGE'
? cb(err.message)
: cb(err)
class Access {
constructor (npm) {
this.npm = npm
}

get usage () {
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
return usageUtil(
'access',
'npm access public [<package>]\n' +
'npm access restricted [<package>]\n' +
'npm access grant <read-only|read-write> <scope:team> [<package>]\n' +
'npm access revoke <scope:team> [<package>]\n' +
'npm access 2fa-required [<package>]\n' +
'npm access 2fa-not-required [<package>]\n' +
'npm access ls-packages [<user>|<scope>|<scope:team>]\n' +
'npm access ls-collaborators [<package> [<user>]]\n' +
'npm access edit [<package>]'
)
}

const access = async ([cmd, ...args], cb) => {
const fn = subcommands.includes(cmd) && access[cmd]
async completion (opts) {
const argv = opts.conf.argv.remain
if (argv.length === 2)
return subcommands

switch (argv[2]) {
case 'grant':
if (argv.length === 3)
return ['read-only', 'read-write']
else
return []

case 'public':
case 'restricted':
case 'ls-packages':
case 'ls-collaborators':
case 'edit':
case '2fa-required':
case '2fa-not-required':
case 'revoke':
return []
default:
throw new Error(argv[2] + ' not recognized')
}
}

if (!cmd)
throw UsageError('Subcommand is required.')
exec (args, cb) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you're heading in this direction already, but pretty please can we ditch the callbacks entirely? 😆 I'd love to see this.access just be this.exec(), and have npm know how to handle that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That change was left out of this PR because of the amount of testing changes it would require. This PR is mostly code change, with minimal test changes.

Making this function async would be a minimal code change, and a larger test change. It'd be much easier than THIS PR, but still needs to be its own thing.

this.access(args)
.then(x => cb(null, x))
.catch(err => err.code === 'EUSAGE'
? cb(err.message)
: cb(err)
)
}

if (!fn)
throw UsageError(`${cmd} is not a recognized subcommand.`)
async access ([cmd, ...args]) {
if (!cmd)
throw this.usageError('Subcommand is required.')

return fn(args, { ...npm.flatOptions })
}
if (!subcommands.includes(cmd) || !this[cmd])
throw this.usageError(`${cmd} is not a recognized subcommand.`)

const completion = async (opts) => {
const argv = opts.conf.argv.remain
if (argv.length === 2)
return subcommands
return this[cmd](args, { ...this.npm.flatOptions })
}

switch (argv[2]) {
case 'grant':
if (argv.length === 3)
return ['read-only', 'read-write']
else
return []
public ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.public)
}

case 'public':
case 'restricted':
case 'ls-packages':
case 'ls-collaborators':
case 'edit':
case '2fa-required':
case '2fa-not-required':
case 'revoke':
return []
default:
throw new Error(argv[2] + ' not recognized')
restricted ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.restricted)
}
}

access.public = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.public)
async grant ([perms, scopeteam, pkg], opts) {
if (!perms || (perms !== 'read-only' && perms !== 'read-write'))
throw this.usageError('First argument must be either `read-only` or `read-write`.')

access.restricted = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.restricted)
if (!scopeteam)
throw this.usageError('`<scope:team>` argument is required.')

access.grant = async ([perms, scopeteam, pkg], opts) => {
if (!perms || (perms !== 'read-only' && perms !== 'read-write'))
throw UsageError('First argument must be either `read-only` or `read-write`.')
const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []

if (!scopeteam)
throw UsageError('`<scope:team>` argument is required.')
if (!scope && !team) {
throw this.usageError(
'Second argument used incorrect format.\n' +
'Example: @example:developers'
)
}

const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []
return this.modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.grant(pkgName, scopeteam, perms, opts), false)
}

if (!scope && !team) {
throw UsageError(
'Second argument used incorrect format.\n' +
'Example: @example:developers'
)
async revoke ([scopeteam, pkg], opts) {
if (!scopeteam)
throw this.usageError('`<scope:team>` argument is required.')

const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []

if (!scope || !team) {
throw this.usageError(
'First argument used incorrect format.\n' +
'Example: @example:developers'
)
}

return this.modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.revoke(pkgName, scopeteam, opts))
}

return modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.grant(pkgName, scopeteam, perms, opts), false)
}
get ['2fa-required'] () {
return this.tfaRequired
}

access.revoke = async ([scopeteam, pkg], opts) => {
if (!scopeteam)
throw UsageError('`<scope:team>` argument is required.')
tfaRequired ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.tfaRequired, false)
}

const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []
get ['2fa-not-required'] () {
return this.tfaNotRequired
}

if (!scope || !team) {
throw UsageError(
'First argument used incorrect format.\n' +
'Example: @example:developers'
)
tfaNotRequired ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.tfaNotRequired, false)
}

return modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.revoke(pkgName, scopeteam, opts))
}
get ['ls-packages'] () {
return this.lsPackages
}

access['2fa-required'] = access.tfaRequired = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.tfaRequired, false)
async lsPackages ([owner], opts) {
if (!owner)
owner = await getIdentity(this.npm, opts)

access['2fa-not-required'] = access.tfaNotRequired = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.tfaNotRequired, false)
const pkgs = await libaccess.lsPackages(owner, opts)

access['ls-packages'] = access.lsPackages = async ([owner], opts) => {
if (!owner)
owner = await getIdentity(opts)
// TODO - print these out nicely (breaking change)
output(JSON.stringify(pkgs, null, 2))
}

const pkgs = await libaccess.lsPackages(owner, opts)
get ['ls-collaborators'] () {
return this.lsCollaborators
}

// TODO - print these out nicely (breaking change)
output(JSON.stringify(pkgs, null, 2))
}
async lsCollaborators ([pkg, usr], opts) {
const pkgName = await this.getPackage(pkg, false)
const collabs = await libaccess.lsCollaborators(pkgName, usr, opts)

access['ls-collaborators'] = access.lsCollaborators = async ([pkg, usr], opts) => {
const pkgName = await getPackage(pkg, false)
const collabs = await libaccess.lsCollaborators(pkgName, usr, opts)
// TODO - print these out nicely (breaking change)
output(JSON.stringify(collabs, null, 2))
}

// TODO - print these out nicely (breaking change)
output(JSON.stringify(collabs, null, 2))
}
async edit () {
throw new Error('edit subcommand is not implemented yet')
}

access.edit = () =>
Promise.reject(new Error('edit subcommand is not implemented yet'))

const modifyPackage = (pkg, opts, fn, requireScope = true) =>
getPackage(pkg, requireScope)
.then(pkgName => otplease(opts, opts => fn(pkgName, opts)))

const getPackage = async (name, requireScope) => {
if (name && name.trim())
return name.trim()
else {
try {
const pkg = await readPackageJson(path.resolve(npm.prefix, 'package.json'))
name = pkg.name
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(
'no package name passed to command and no package.json found'
)
} else
throw err
modifyPackage (pkg, opts, fn, requireScope = true) {
return this.getPackage(pkg, requireScope)
.then(pkgName => otplease(opts, opts => fn(pkgName, opts)))
}

async getPackage (name, requireScope) {
if (name && name.trim())
return name.trim()
else {
try {
const pkg = await readPackageJson(path.resolve(this.npm.prefix, 'package.json'))
name = pkg.name
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(
'no package name passed to command and no package.json found'
)
} else
throw err
}

if (requireScope && !name.match(/^@[^/]+\/.*$/))
throw this.usageError('This command is only available for scoped packages.')
else
return name
}
}

if (requireScope && !name.match(/^@[^/]+\/.*$/))
throw UsageError('This command is only available for scoped packages.')
else
return name
usageError (msg) {
return Object.assign(new Error(`\nUsage: ${msg}\n\n` + this.usage), {
code: 'EUSAGE',
})
}
}

module.exports = Object.assign(cmd, { usage, completion, subcommands })
module.exports = Access