Skip to content

Commit

Permalink
fix(npm) pass npm context everywhere
Browse files Browse the repository at this point in the history
Instead of files randomly requiring the npm singleton,
we pass it where it needs to go so that tests don't need
to do so much require mocking everywhere

PR-URL: #2772
Credit: @wraithgar
Close: #2772
Reviewed-by: @ruyadorno
  • Loading branch information
wraithgar authored and ruyadorno committed Mar 4, 2021
1 parent b33c760 commit 4a5dd3a
Show file tree
Hide file tree
Showing 165 changed files with 8,445 additions and 7,469 deletions.
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 () {
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) {
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

0 comments on commit 4a5dd3a

Please sign in to comment.