Skip to content
Open
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
24 changes: 19 additions & 5 deletions lib/base-cmd.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { log } = require('proc-log')
const { definitions } = require('@npmcli/config/lib/definitions')

class BaseCommand {
// these defaults can be overridden by individual commands
Expand All @@ -10,16 +11,24 @@ class BaseCommand {
static name = null
static description = null
static params = null
static definitions = null

// this is a static so that we can read from it without instantiating a command
// which would require loading the config
static get describeUsage () {
const { definitions } = require('@npmcli/config/lib/definitions')
const { aliases: cmdAliases } = require('./utils/cmd-list')
const seenExclusive = new Set()
const wrapWidth = 80
const { description, usage = [''], name, params } = this

if (this.definitions) {
this.definitions = this.definitions
} else {
this.definitions = definitions
}

const definitionsPool = { ...definitions, ...this.definitions }

Comment on lines +24 to +31
Copy link
Member

Choose a reason for hiding this comment

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

This part seems weird to me, we're merging potentially both into definitionsPool, with this.definitions taking predecent. But if there are no this.definitions then they are both the same value so why do we care about both. This probably needs a quick boolean tree to see what to do in all 4 cases (has/doesn't have definitions and has/doesn't have this.definitions)

const fullUsage = [
`${description}`,
'',
Expand All @@ -35,14 +44,14 @@ class BaseCommand {
if (seenExclusive.has(param)) {
continue
}
const { exclusive } = definitions[param]
let paramUsage = `${definitions[param].usage}`
const exclusive = definitionsPool[param]?.exclusive
let paramUsage = definitionsPool[param]?.usage
if (exclusive) {
const exclusiveParams = [paramUsage]
seenExclusive.add(param)
for (const e of exclusive) {
seenExclusive.add(e)
exclusiveParams.push(definitions[e].usage)
exclusiveParams.push(definitionsPool[e].usage)
}
paramUsage = `${exclusiveParams.join('|')}`
}
Expand Down Expand Up @@ -77,7 +86,7 @@ class BaseCommand {
constructor (npm) {
this.npm = npm

const { config } = this.npm
const { config } = this

if (!this.constructor.skipConfigValidation) {
config.validate()
Expand All @@ -88,6 +97,11 @@ class BaseCommand {
}
}

get config () {
// Return command-specific config if it exists, otherwise use npm's config
return this.npm.config
}

get name () {
return this.constructor.name
}
Expand Down
10 changes: 10 additions & 0 deletions lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class Npm {
#runId = new Date().toISOString().replace(/[.:]/g, '_')
#title = 'npm'
#argvClean = []
#argv = undefined
#excludeNpmCwd = undefined
#npmRoot = null

#display = null
Expand Down Expand Up @@ -227,6 +229,14 @@ class Npm {
process.env.npm_command = this.command
}

if (!Command.definitions || Command.definitions === definitions) {
this.config.logWarnings()
} else {
this.config.loadCommand(Command.definitions)
this.config.logWarnings()
this.config.warn = true
}

if (this.config.get('usage')) {
return output.standard(command.usage)
}
Expand Down
18 changes: 9 additions & 9 deletions tap-snapshots/test/lib/commands/install.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ silly logfile done cleaning log files
verbose stack Error: The developer of this package has specified the following through devEngines
verbose stack Invalid devEngines.runtime
verbose stack Invalid name "nondescript" does not match "node" for "runtime"
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:181:27)
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:252:7)
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:208:9)
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:195:27)
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:262:7)
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:210:9)
error code EBADDEVENGINES
error EBADDEVENGINES The developer of this package has specified the following through devEngines
error EBADDEVENGINES Invalid devEngines.runtime
Expand Down Expand Up @@ -199,9 +199,9 @@ warn EBADDEVENGINES }
verbose stack Error: The developer of this package has specified the following through devEngines
verbose stack Invalid devEngines.runtime
verbose stack Invalid name "nondescript" does not match "node" for "runtime"
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:181:27)
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:252:7)
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:208:9)
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:195:27)
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:262:7)
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:210:9)
error code EBADDEVENGINES
error EBADDEVENGINES The developer of this package has specified the following through devEngines
error EBADDEVENGINES Invalid devEngines.runtime
Expand All @@ -225,9 +225,9 @@ silly logfile done cleaning log files
verbose stack Error: The developer of this package has specified the following through devEngines
verbose stack Invalid devEngines.runtime
verbose stack Invalid name "nondescript" does not match "node" for "runtime"
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:181:27)
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:252:7)
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:208:9)
verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:195:27)
verbose stack at MockNpm.#exec ({CWD}/lib/npm.js:262:7)
verbose stack at MockNpm.exec ({CWD}/lib/npm.js:210:9)
error code EBADDEVENGINES
error EBADDEVENGINES The developer of this package has specified the following through devEngines
error EBADDEVENGINES Invalid devEngines.runtime
Expand Down
67 changes: 67 additions & 0 deletions test/lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,70 @@ t.test('print usage if non-command param provided', async t => {
t.match(joinedOutput(), 'Unknown command: "tset"')
t.match(joinedOutput(), 'Did you mean this?')
})

async function testCommandDefinitions (t, { defaultValue, outputValue, type, flags }) {
const path = require('node:path')

// Create a temporary command file
const tsetPath = path.join(__dirname, '../../lib/commands/tset.js')
const tsetContent = `
const Definition = require('@npmcli/config/lib/definitions/definition.js')
const BaseCommand = require('../base-cmd.js')
const { output } = require('proc-log')
const { flatten } = require('@npmcli/config/lib/definitions/index.js')

module.exports = class TestCommand extends BaseCommand {
static description = 'A test command'
static name = 'tset'
static definitions = {
say: new Definition('say', {
default: ${defaultValue},
type: ${type},
description: 'say',
flatten,
}),
}

async exec () {
const say = this.npm.config.get('say')
output.standard(say)
}
}
`
fs.writeFileSync(tsetPath, tsetContent)
t.teardown(() => {
try {
fs.unlinkSync(tsetPath)
delete require.cache[tsetPath]
} catch (e) {
// ignore
}
})

const mockCmdList = require('../../lib/utils/cmd-list.js')
const { npm, joinedOutput } = await loadMockNpm(t, {
argv: ['tset', ...(flags || [])],
mocks: {
'{LIB}/utils/cmd-list.js': {
...mockCmdList,
commands: [...mockCmdList.commands, 'tset'],
deref: (c) => c === 'tset' ? 'tset' : mockCmdList.deref(c),
},
},
})

// Now you can execute the mocked command
await npm.exec('tset', [])

t.match(joinedOutput(), outputValue)
}

const stack = {
boolean_default: (t) => testCommandDefinitions(t, { type: 'Boolean', defaultValue: 'false', outputValue: 'false' }),
string_default: (t) => testCommandDefinitions(t, { type: 'String', defaultValue: `'meow'`, outputValue: 'meow' }),
string_flag: (t) => testCommandDefinitions(t, { type: 'String', defaultValue: `'meow'`, outputValue: 'woof', flags: ['--say=woof'] }),
}

Object.entries(stack).forEach(([name, fn]) => {
t.test(name, fn)
})
Loading
Loading