Skip to content

Commit 450e50f

Browse files
committed
fix: evaluate configs in command class
### Refactor Config Reading During `exec` This removes the previous workarounds that were required due to tests setting configs that were not available in the constructor of the commands. This also made the fix for nodejs/node#45881 easier since the config checks for workspaces can now all be made in the command. The fix was to move the package.json detection to `@npmcli/config` and use that boolean instead of setting `location=project` which affected all commands such as `config set` saving to the wrong location. Some notable changes from this refactor include: - `execWorkspaces` no longer being passed the raw workspace filters and instead requiring `this.setWorkspaces()` to be called which was already being done in most commands - `static workspaces = [true|false]` on all commands to indicate workspaces support. This is used in docs generation and whether to throw when `execWorkspaces` is called or not. ### Drop `fakeMockNpm` and refactor `mockNpm` This refactor also drops `fakeMockNpm` in favor of `realMockNpm` (now just `mockNpm`), and adds new features to `mockNpm`. Some new features of `mockNpm`: - sets all configs via argv so they are parsed before `npm.load()`. This is the most important change as it more closely resembles real usage. - automatically resets `process.exitCode` - automatically puts global `node_modules` in correct testdir based on platform - more helpful error messages when used in unsupported ways - ability to preload a command for execution - sets `cwd` automatically to `prefix` and sets `globalPrefix` to the specified testdir Note that this commit does not include the actual test changes, which are included in the following commits for readability reasons. ### Linting This also removes some of the one off linting rules that were set in the past to reduce churn and fixes all remaining errors.
1 parent c52cf6b commit 450e50f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+986
-923
lines changed

.eslintrc.local.json

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
{
22
"rules": {
3-
"no-shadow": "off",
43
"no-console": "error"
5-
},
6-
"overrides": [{
7-
"files": [
8-
"test/**"
9-
],
10-
"rules": {
11-
"no-console": "off"
12-
}
13-
}]
4+
}
145
}

lib/arborist-cmd.js

+22-9
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,35 @@ class ArboristCmd extends BaseCommand {
1717
'install-links',
1818
]
1919

20+
static workspaces = true
2021
static ignoreImplicitWorkspace = false
2122

2223
constructor (npm) {
2324
super(npm)
24-
if (this.npm.config.isDefault('audit')
25-
&& (this.npm.global || this.npm.config.get('location') !== 'project')
26-
) {
27-
this.npm.config.set('audit', false)
28-
} else if (this.npm.global && this.npm.config.get('audit')) {
29-
log.warn('config',
30-
'includes both --global and --audit, which is currently unsupported.')
25+
26+
const { config } = this.npm
27+
28+
// when location isn't set and global isn't true check for a package.json at
29+
// the localPrefix and set the location to project if found
30+
const locationProject = config.get('location') === 'project' || (
31+
config.isDefault('location')
32+
// this is different then `npm.global` which falls back to checking
33+
// location which we do not want to use here
34+
&& !config.get('global')
35+
&& npm.localPackage
36+
)
37+
38+
// if audit is not set and we are in global mode and location is not project
39+
// and we assume its not a project related context, then we set audit=false
40+
if (config.isDefault('audit') && (this.npm.global || !locationProject)) {
41+
config.set('audit', false)
42+
} else if (this.npm.global && config.get('audit')) {
43+
log.warn('config', 'includes both --global and --audit, which is currently unsupported.')
3144
}
3245
}
3346

34-
async execWorkspaces (args, filters) {
35-
await this.setWorkspaces(filters)
47+
async execWorkspaces (args) {
48+
await this.setWorkspaces()
3649
return this.exec(args)
3750
}
3851
}

lib/base-command.js

+54-27
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ const getWorkspaces = require('./workspaces/get-workspaces.js')
88
const cmdAliases = require('./utils/cmd-list').aliases
99

1010
class BaseCommand {
11+
static workspaces = false
12+
static ignoreImplicitWorkspace = true
13+
1114
constructor (npm) {
1215
this.wrapWidth = 80
1316
this.npm = npm
1417

15-
if (!this.skipConfigValidation) {
16-
this.npm.config.validate()
18+
const { config } = this.npm
19+
20+
if (!this.constructor.skipConfigValidation) {
21+
config.validate()
22+
}
23+
24+
if (config.get('workspaces') === false && config.get('workspace').length) {
25+
throw new Error('Can not use --no-workspaces and --workspace at the same time')
1726
}
1827
}
1928

@@ -25,35 +34,31 @@ class BaseCommand {
2534
return this.constructor.description
2635
}
2736

28-
get ignoreImplicitWorkspace () {
29-
return this.constructor.ignoreImplicitWorkspace
30-
}
31-
32-
get skipConfigValidation () {
33-
return this.constructor.skipConfigValidation
37+
get params () {
38+
return this.constructor.params
3439
}
3540

3641
get usage () {
3742
const usage = [
38-
`${this.constructor.description}`,
43+
`${this.description}`,
3944
'',
4045
'Usage:',
4146
]
4247

4348
if (!this.constructor.usage) {
44-
usage.push(`npm ${this.constructor.name}`)
49+
usage.push(`npm ${this.name}`)
4550
} else {
46-
usage.push(...this.constructor.usage.map(u => `npm ${this.constructor.name} ${u}`))
51+
usage.push(...this.constructor.usage.map(u => `npm ${this.name} ${u}`))
4752
}
4853

49-
if (this.constructor.params) {
54+
if (this.params) {
5055
usage.push('')
5156
usage.push('Options:')
5257
usage.push(this.wrappedParams)
5358
}
5459

5560
const aliases = Object.keys(cmdAliases).reduce((p, c) => {
56-
if (cmdAliases[c] === this.constructor.name) {
61+
if (cmdAliases[c] === this.name) {
5762
p.push(c)
5863
}
5964
return p
@@ -68,7 +73,7 @@ class BaseCommand {
6873
}
6974

7075
usage.push('')
71-
usage.push(`Run "npm help ${this.constructor.name}" for more info`)
76+
usage.push(`Run "npm help ${this.name}" for more info`)
7277

7378
return usage.join('\n')
7479
}
@@ -77,7 +82,7 @@ class BaseCommand {
7782
let results = ''
7883
let line = ''
7984

80-
for (const param of this.constructor.params) {
85+
for (const param of this.params) {
8186
const usage = `[${ConfigDefinitions[param].usage}]`
8287
if (line.length && line.length + usage.length > this.wrapWidth) {
8388
results = [results, line].filter(Boolean).join('\n')
@@ -98,26 +103,48 @@ class BaseCommand {
98103
})
99104
}
100105

101-
async execWorkspaces (args, filters) {
102-
throw Object.assign(new Error('This command does not support workspaces.'), {
103-
code: 'ENOWORKSPACES',
104-
})
105-
}
106+
async cmdExec (args) {
107+
const { config } = this.npm
106108

107-
async setWorkspaces (filters) {
108-
if (this.isArboristCmd) {
109-
this.includeWorkspaceRoot = false
109+
if (config.get('usage')) {
110+
return this.npm.output(this.usage)
110111
}
111112

112-
const relativeFrom = relative(this.npm.localPrefix, process.cwd()).startsWith('..')
113-
? this.npm.localPrefix
114-
: process.cwd()
113+
const hasWsConfig = config.get('workspaces') || config.get('workspace').length
114+
// if cwd is a workspace, the default is set to [that workspace]
115+
const implicitWs = config.get('workspace', 'default').length
115116

117+
// (-ws || -w foo) && (cwd is not a workspace || command is not ignoring implicit workspaces)
118+
if (hasWsConfig && (!implicitWs || !this.constructor.ignoreImplicitWorkspace)) {
119+
if (this.npm.global) {
120+
throw new Error('Workspaces not supported for global packages')
121+
}
122+
if (!this.constructor.workspaces) {
123+
throw Object.assign(new Error('This command does not support workspaces.'), {
124+
code: 'ENOWORKSPACES',
125+
})
126+
}
127+
return this.execWorkspaces(args)
128+
}
129+
130+
return this.exec(args)
131+
}
132+
133+
async setWorkspaces () {
134+
const includeWorkspaceRoot = this.isArboristCmd
135+
? false
136+
: this.npm.config.get('include-workspace-root')
137+
138+
const prefixInsideCwd = relative(this.npm.localPrefix, process.cwd()).startsWith('..')
139+
const relativeFrom = prefixInsideCwd ? this.npm.localPrefix : process.cwd()
140+
141+
const filters = this.npm.config.get('workspace')
116142
const ws = await getWorkspaces(filters, {
117143
path: this.npm.localPrefix,
118-
includeWorkspaceRoot: this.includeWorkspaceRoot,
144+
includeWorkspaceRoot,
119145
relativeFrom,
120146
})
147+
121148
this.workspaces = ws
122149
this.workspaceNames = [...ws.keys()]
123150
this.workspacePaths = [...ws.values()]

lib/cli.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ module.exports = async process => {
6868
// leak any private CLI configs to other programs
6969
process.title = 'npm'
7070

71+
// if npm is called as "npmg" or "npm_g", then run in global mode.
72+
if (process.argv[1][process.argv[1].length - 1] === 'g') {
73+
process.argv.splice(1, 1, 'npm', '-g')
74+
}
75+
7176
// Nothing should happen before this line if we can't guarantee it will
7277
// not have syntax errors in some version of node
7378
const validateEngines = createEnginesValidation()
@@ -78,11 +83,6 @@ module.exports = async process => {
7883
const npm = new Npm()
7984
exitHandler.setNpm(npm)
8085

81-
// if npm is called as "npmg" or "npm_g", then run in global mode.
82-
if (process.argv[1][process.argv[1].length - 1] === 'g') {
83-
process.argv.splice(1, 1, 'npm', '-g')
84-
}
85-
8686
// only log node and npm paths in argv initially since argv can contain
8787
// sensitive info. a cleaned version will be logged later
8888
const log = require('./utils/log-shim.js')
@@ -112,6 +112,7 @@ module.exports = async process => {
112112
// this is how to use npm programmatically:
113113
try {
114114
await npm.load()
115+
115116
if (npm.config.get('version', 'cli')) {
116117
npm.output(npm.version)
117118
return exitHandler()
@@ -130,7 +131,7 @@ module.exports = async process => {
130131
return exitHandler()
131132
}
132133

133-
await npm.exec(cmd, npm.argv)
134+
await npm.exec(cmd)
134135
return exitHandler()
135136
} catch (err) {
136137
if (err.code === 'EUNKNOWNCOMMAND') {

lib/commands/access.js

-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ class Access extends BaseCommand {
3737
'registry',
3838
]
3939

40-
static ignoreImplicitWorkspace = true
41-
4240
static usage = [
4341
'list packages [<user>|<scope>|<scope:team> [<package>]',
4442
'list collaborators [<package> [<user>]]',

lib/commands/adduser.js

-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ class AddUser extends BaseCommand {
1313
'auth-type',
1414
]
1515

16-
static ignoreImplicitWorkspace = true
17-
1816
async exec (args) {
1917
const scope = this.npm.config.get('scope')
2018
let registry = this.npm.config.get('registry')

lib/commands/audit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class VerifySignatures {
152152
const keys = await fetch.json('/-/npm/v1/keys', {
153153
...this.npm.flatOptions,
154154
registry,
155-
}).then(({ keys }) => keys.map((key) => ({
155+
}).then(({ keys: ks }) => ks.map((key) => ({
156156
...key,
157157
pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`,
158158
}))).catch(err => {

lib/commands/cache.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const cacache = require('cacache')
22
const Arborist = require('@npmcli/arborist')
33
const pacote = require('pacote')
44
const fs = require('fs/promises')
5-
const path = require('path')
5+
const { join } = require('path')
66
const semver = require('semver')
77
const BaseCommand = require('../base-command.js')
88
const npa = require('npm-package-arg')
@@ -74,8 +74,6 @@ class Cache extends BaseCommand {
7474
'verify',
7575
]
7676

77-
static ignoreImplicitWorkspace = true
78-
7977
async completion (opts) {
8078
const argv = opts.conf.argv.remain
8179
if (argv.length === 2) {
@@ -111,7 +109,7 @@ class Cache extends BaseCommand {
111109

112110
// npm cache clean [pkg]*
113111
async clean (args) {
114-
const cachePath = path.join(this.npm.cache, '_cacache')
112+
const cachePath = join(this.npm.cache, '_cacache')
115113
if (args.length === 0) {
116114
if (!this.npm.config.get('force')) {
117115
throw new Error(`As of npm@5, the npm cache self-heals from corruption issues
@@ -169,7 +167,7 @@ class Cache extends BaseCommand {
169167
}
170168

171169
async verify () {
172-
const cache = path.join(this.npm.cache, '_cacache')
170+
const cache = join(this.npm.cache, '_cacache')
173171
const prefix = cache.indexOf(process.env.HOME) === 0
174172
? `~${cache.slice(process.env.HOME.length)}`
175173
: cache
@@ -192,7 +190,7 @@ class Cache extends BaseCommand {
192190

193191
// npm cache ls [--package <spec> ...]
194192
async ls (specs) {
195-
const cachePath = path.join(this.npm.cache, '_cacache')
193+
const cachePath = join(this.npm.cache, '_cacache')
196194
const cacheKeys = Object.keys(await cacache.ls(cachePath))
197195
if (specs.length > 0) {
198196
// get results for each package spec specified

lib/commands/completion.js

+7-18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
const fs = require('fs/promises')
3333
const nopt = require('nopt')
34+
const { resolve } = require('path')
3435

3536
const { definitions, shorthands } = require('../utils/config/index.js')
3637
const { aliases, commands, plumbing } = require('../utils/cmd-list.js')
@@ -40,29 +41,20 @@ const configNames = Object.keys(definitions)
4041
const shorthandNames = Object.keys(shorthands)
4142
const allConfs = configNames.concat(shorthandNames)
4243
const { isWindowsShell } = require('../utils/is-windows.js')
43-
const fileExists = async (file) => {
44-
try {
45-
const stat = await fs.stat(file)
46-
return stat.isFile()
47-
} catch {
48-
return false
49-
}
50-
}
44+
const fileExists = (file) => fs.stat(file).then(s => s.isFile()).catch(() => false)
5145

5246
const BaseCommand = require('../base-command.js')
5347

5448
class Completion extends BaseCommand {
5549
static description = 'Tab Completion for npm'
5650
static name = 'completion'
57-
static ignoreImplicitWorkspace = true
5851

5952
// completion for the completion command
6053
async completion (opts) {
6154
if (opts.w > 2) {
6255
return
6356
}
6457

65-
const { resolve } = require('path')
6658
const [bashExists, zshExists] = await Promise.all([
6759
fileExists(resolve(process.env.HOME, '.bashrc')),
6860
fileExists(resolve(process.env.HOME, '.zshrc')),
@@ -93,7 +85,7 @@ class Completion extends BaseCommand {
9385
if (COMP_CWORD === undefined ||
9486
COMP_LINE === undefined ||
9587
COMP_POINT === undefined) {
96-
return dumpScript()
88+
return dumpScript(resolve(this.npm.npmRoot, 'lib', 'utils', 'completion.sh'))
9789
}
9890

9991
// ok we're actually looking at the envs and outputting the suggestions
@@ -150,9 +142,9 @@ class Completion extends BaseCommand {
150142
// take a little shortcut and use npm's arg parsing logic.
151143
// don't have to worry about the last arg being implicitly
152144
// boolean'ed, since the last block will catch that.
153-
const types = Object.entries(definitions).reduce((types, [key, def]) => {
154-
types[key] = def.type
155-
return types
145+
const types = Object.entries(definitions).reduce((acc, [key, def]) => {
146+
acc[key] = def.type
147+
return acc
156148
}, {})
157149
const parsed = opts.conf =
158150
nopt(types, shorthands, partialWords.slice(0, -1), 0)
@@ -196,10 +188,7 @@ class Completion extends BaseCommand {
196188
}
197189
}
198190

199-
const dumpScript = async () => {
200-
const { resolve } = require('path')
201-
const p = resolve(__dirname, '..', 'utils', 'completion.sh')
202-
191+
const dumpScript = async (p) => {
203192
const d = (await fs.readFile(p, 'utf8')).replace(/^#!.*?\n/, '')
204193
await new Promise((res, rej) => {
205194
let done = false

0 commit comments

Comments
 (0)