Skip to content

Commit 4bdfdea

Browse files
committed
docs fixes
1 parent 66c7528 commit 4bdfdea

File tree

4 files changed

+175
-50
lines changed

4 files changed

+175
-50
lines changed

docs/lib/build.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ const run = async (opts) => {
376376
const transformedSrc = applyTransforms(body, [
377377
transform.version,
378378
...(fullName.startsWith('commands/')
379-
? [transform.usage, transform.params]
379+
? [transform.usage, transform.definitions]
380380
: []),
381381
...(fullName === 'using-npm/config'
382382
? [transform.shorthands, transform.config]

docs/lib/index.js

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ const getCommand = (name, commandLoader = defaultCommandLoader) => {
3232
return commandLoader(name)
3333
}
3434

35+
// Resolve definitions for a command - use definitions if present, otherwise build from params
36+
const resolveDefinitions = (command) => {
37+
// If command has definitions, use them directly (ignore params)
38+
if (command.definitions && Object.keys(command.definitions).length > 0) {
39+
return command.definitions
40+
}
41+
42+
// Otherwise build from params using global definitions
43+
if (command.params) {
44+
const resolved = {}
45+
for (const param of command.params) {
46+
if (definitions[param]) {
47+
resolved[param] = definitions[param]
48+
}
49+
}
50+
return resolved
51+
}
52+
53+
return {}
54+
}
55+
3556
const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader) => {
3657
// Grab the command name from the *.md filename
3758
// NOTE: We cannot use the name property command file because in the case of
@@ -41,7 +62,7 @@ const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader)
4162
if (name === 'npm') {
4263
return {
4364
name,
44-
params: null,
65+
definitions: {},
4566
usage: 'npm',
4667
}
4768
}
@@ -51,19 +72,19 @@ const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader)
5172
// so it just needs the usage of npm exec
5273
const srcName = name === 'npx' ? 'exec' : name
5374
const command = getCommand(srcName, commandLoader)
54-
const { params, usage = [''], workspaces } = command
55-
const commandDefinitions = command.definitions || {}
56-
const definitionPool = { ...definitions, ...commandDefinitions }
75+
const { usage = [''], workspaces } = command
5776
const usagePrefix = name === 'npx' ? 'npx' : `npm ${name}`
58-
if (params) {
59-
for (const param of params) {
60-
// Check command-specific definitions first, fall back to global definitions
61-
const paramDef = definitionPool[param]
62-
if (paramDef && paramDef.exclusive) {
63-
for (const e of paramDef.exclusive) {
64-
if (!params.includes(e)) {
65-
params.splice(params.indexOf(param) + 1, 0, e)
66-
}
77+
78+
// Resolve definitions - handles exclusive params expansion
79+
const commandDefs = resolveDefinitions(command)
80+
const resolvedDefs = {}
81+
for (const [key, def] of Object.entries(commandDefs)) {
82+
resolvedDefs[key] = def
83+
// Handle exclusive params
84+
if (def.exclusive) {
85+
for (const e of def.exclusive) {
86+
if (!resolvedDefs[e] && definitions[e]) {
87+
resolvedDefs[e] = definitions[e]
6788
}
6889
}
6990
}
@@ -72,7 +93,7 @@ const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader)
7293
return {
7394
name,
7495
workspaces,
75-
params: name === 'npx' ? null : params,
96+
definitions: name === 'npx' ? {} : resolvedDefs,
7697
usage: usage.map(u => `${usagePrefix} ${u}`.trim()).join('\n'),
7798
}
7899
}
@@ -138,33 +159,30 @@ const generateFlagsTable = (definitionPool) => {
138159
].join('\n')
139160
}
140161

141-
const replaceParams = (src, { path, commandLoader }) => {
142-
const { params, name } = getCommandByDoc(path, DOC_EXT, commandLoader)
162+
const replaceDefinitions = (src, { path, commandLoader }) => {
163+
const { definitions: commandDefs, name } = getCommandByDoc(path, DOC_EXT, commandLoader)
143164

144-
// Load command to get command-specific definitions and subcommands if they exist
145-
let commandDefinitions = {}
146165
let subcommands = {}
147166
try {
148167
const command = getCommand(name, commandLoader)
149-
commandDefinitions = command.definitions || {}
150168
subcommands = command.subcommands || {}
151169
} catch {
152-
// If command doesn't exist or has no definitions, continue with global definitions only
170+
// Command doesn't exist
153171
}
154172

155-
// If no params and no subcommands, nothing to replace
156-
if (!params && Object.keys(subcommands).length === 0) {
173+
// If no definitions and no subcommands, nothing to replace
174+
if (Object.keys(commandDefs).length === 0 && Object.keys(subcommands).length === 0) {
157175
return src
158176
}
159177

160-
// Assert placeholder is present - commands with params must have the config placeholder
178+
// Assert placeholder is present
161179
const replacer = assertPlaceholder(src, path, TAGS.CONFIG)
162180

163181
// If command has subcommands, generate sections for each subcommand
164182
if (Object.keys(subcommands).length > 0) {
165183
const subcommandSections = Object.entries(subcommands).map(([subName, SubCommand]) => {
166184
const subUsage = SubCommand.usage || []
167-
const subDefinitions = SubCommand.definitions || {}
185+
const subDefs = resolveDefinitions(SubCommand)
168186

169187
const parts = [`### \`npm ${name} ${subName}\``, '']
170188

@@ -181,10 +199,10 @@ const replaceParams = (src, { path, commandLoader }) => {
181199
parts.push('```', '')
182200
}
183201

184-
// Add flags section with all parameters combined
185-
if (Object.keys(subDefinitions).length > 0) {
202+
// Add flags section if definitions exist
203+
if (Object.keys(subDefs).length > 0) {
186204
parts.push('#### Flags', '')
187-
parts.push(generateFlagsTable(subDefinitions), '')
205+
parts.push(generateFlagsTable(subDefs), '')
188206
}
189207

190208
return parts.join('\n')
@@ -193,21 +211,10 @@ const replaceParams = (src, { path, commandLoader }) => {
193211
return src.replace(replacer, subcommandSections.join('\n'))
194212
}
195213

196-
// Original behavior for commands without subcommands but with params
197-
/* istanbul ignore if - all commands with no subcommands have params */
198-
if (!params) {
199-
return src
200-
}
201-
202-
// Build definitions object from params using global definitions pool
203-
const allDefinitions = { ...definitions, ...commandDefinitions }
204-
const paramDescriptions = []
205-
for (const param of params) {
206-
if (allDefinitions[param]) {
207-
const def = allDefinitions[param]
208-
paramDescriptions.push(def.describe())
209-
}
210-
}
214+
// For commands without subcommands - commandDefs must be non-empty here
215+
// (we would have returned early at line 175 if both were empty)
216+
const paramDescriptions = Object.values(commandDefs)
217+
.map(def => def.describe())
211218

212219
return src.replace(replacer, paramDescriptions.join('\n\n'))
213220
}
@@ -284,7 +291,7 @@ module.exports = {
284291
md: resolve(__dirname, '..', 'content'),
285292
},
286293
usage: replaceUsage,
287-
params: replaceParams,
294+
definitions: replaceDefinitions,
288295
config: replaceConfig,
289296
shorthands: replaceShorthands,
290297
version: replaceVersion,

docs/test/index.js

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,116 @@ description: Test command without params
808808
t.match(htmlContent, /npm testcmd-noparams/, 'includes command name')
809809
})
810810

811+
t.test('command with empty definitions (has config placeholder)', async t => {
812+
const commandLoader = createCommandLoader({
813+
'testcmd-empty-defs': {
814+
usage: ['<pkg>'],
815+
definitions: {},
816+
},
817+
})
818+
819+
const doc = createCommandDoc('npm-testcmd-empty-defs', 'Test command with empty definitions')
820+
const { html } = await testBuildDocs(t, {
821+
commandLoader,
822+
content: {
823+
commands: { 'npm-testcmd-empty-defs.md': doc },
824+
},
825+
})
826+
827+
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-empty-defs')
828+
t.ok(htmlContent.length > 0, 'generates HTML for command with empty definitions')
829+
t.match(htmlContent, /npm testcmd-empty-defs/, 'includes command name')
830+
})
831+
832+
t.test('command with params referencing non-existent global definition', async t => {
833+
const commandLoader = createCommandLoader({
834+
'testcmd-missing-param': {
835+
usage: ['<pkg>'],
836+
params: ['non-existent-param', 'registry'],
837+
},
838+
})
839+
840+
const doc = createCommandDoc('npm-testcmd-missing-param', 'Test command with missing param')
841+
const { html } = await testBuildDocs(t, {
842+
commandLoader,
843+
content: {
844+
commands: { 'npm-testcmd-missing-param.md': doc },
845+
},
846+
})
847+
848+
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-missing-param')
849+
t.ok(htmlContent.length > 0, 'generates HTML even with missing param definition')
850+
t.match(htmlContent, /registry/, 'includes valid param')
851+
})
852+
853+
t.test('command with exclusive param already resolved', async t => {
854+
const commandLoader = createCommandLoader({
855+
'testcmd-exclusive-resolved': {
856+
usage: ['<pkg>'],
857+
definitions: {
858+
'save-dev': {
859+
key: 'save-dev',
860+
default: false,
861+
type: Boolean,
862+
description: 'Save as devDependency',
863+
exclusive: ['save-optional'],
864+
describe: () => '#### `save-dev`\n\n* Default: false\n* Type: Boolean\n\nSave as devDependency',
865+
},
866+
'save-optional': {
867+
key: 'save-optional',
868+
default: false,
869+
type: Boolean,
870+
description: 'Save as optionalDependency',
871+
describe: () => '#### `save-optional`\n\n* Default: false\n* Type: Boolean\n\nSave as optionalDependency',
872+
},
873+
},
874+
},
875+
})
876+
877+
const doc = createCommandDoc('npm-testcmd-exclusive-resolved', 'Test command with exclusive already resolved')
878+
const { html } = await testBuildDocs(t, {
879+
commandLoader,
880+
content: {
881+
commands: { 'npm-testcmd-exclusive-resolved.md': doc },
882+
},
883+
})
884+
885+
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-exclusive-resolved')
886+
t.ok(htmlContent.length > 0, 'generates HTML with exclusive params')
887+
t.match(htmlContent, /save-dev/, 'includes save-dev')
888+
t.match(htmlContent, /save-optional/, 'includes save-optional')
889+
})
890+
891+
t.test('command with exclusive param that does not exist in global definitions', async t => {
892+
const commandLoader = createCommandLoader({
893+
'testcmd-exclusive-missing': {
894+
usage: ['<pkg>'],
895+
definitions: {
896+
'my-flag': {
897+
key: 'my-flag',
898+
default: false,
899+
type: Boolean,
900+
description: 'A flag with non-existent exclusive',
901+
exclusive: ['non-existent-global-param'],
902+
describe: () => '#### `my-flag`\n\n* Default: false\n* Type: Boolean\n\nA flag with non-existent exclusive',
903+
},
904+
},
905+
},
906+
})
907+
908+
const doc = createCommandDoc('npm-testcmd-exclusive-missing', 'Test command with missing exclusive')
909+
const { html } = await testBuildDocs(t, {
910+
commandLoader,
911+
content: {
912+
commands: { 'npm-testcmd-exclusive-missing.md': doc },
913+
},
914+
})
915+
916+
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-exclusive-missing')
917+
t.ok(htmlContent.length > 0, 'generates HTML even with missing exclusive param')
918+
t.match(htmlContent, /my-flag/, 'includes the defined flag')
919+
})
920+
811921
t.test('command with one definition with short flag', async t => {
812922
const commandLoader = createCommandLoader({
813923
'testcmd-short': {
@@ -844,15 +954,14 @@ description: Test command without params
844954
const commandLoader = createCommandLoader({
845955
'testcmd-alias': {
846956
usage: ['<pkg>'],
847-
params: ['aliased-flag'],
848957
definitions: {
849958
'aliased-flag': {
850959
key: 'aliased-flag',
851960
default: '',
852961
type: String,
853962
alias: ['af', 'alias-flag'],
854963
description: 'A flag with aliases',
855-
describe: () => '#### `aliased-flag`\n\n* Default: ""\n* Type: String\n\nA flag with aliases',
964+
describe: () => '#### `aliased-flag`\n\n* Default: ""\n* Type: String\n* Alias: --af, --alias-flag\n\nA flag with aliases',
856965
},
857966
},
858967
},
@@ -1131,10 +1240,11 @@ description: Test command without params
11311240
})
11321241

11331242
t.test('command with mixed command-specific and global params', async t => {
1243+
const { definitions: globalDefs } = require('@npmcli/config/lib/definitions')
1244+
11341245
const commandLoader = createCommandLoader({
11351246
'testcmd-mixed': {
11361247
usage: ['<pkg>'],
1137-
params: ['custom-only', 'registry'],
11381248
definitions: {
11391249
'custom-only': {
11401250
key: 'custom-only',
@@ -1143,6 +1253,7 @@ description: Test command without params
11431253
description: 'A command-specific flag',
11441254
describe: () => '#### `custom-only`\n\n* Default: false\n* Type: Boolean\n\nA command-specific flag',
11451255
},
1256+
registry: globalDefs.registry,
11461257
},
11471258
},
11481259
})
@@ -1162,10 +1273,11 @@ description: Test command without params
11621273
})
11631274

11641275
t.test('subcommand with command-specific and global params', async t => {
1276+
const { definitions: globalDefs } = require('@npmcli/config/lib/definitions')
1277+
11651278
class SubMixed {
11661279
static description = 'Subcommand with mixed params'
11671280
static usage = ['<arg>']
1168-
static params = ['sub-flag', 'registry']
11691281
static definitions = {
11701282
'sub-flag': {
11711283
key: 'sub-flag',
@@ -1174,6 +1286,7 @@ description: Test command without params
11741286
description: 'A subcommand-specific flag',
11751287
describe: () => '#### `sub-flag`\n\n* Default: false\n* Type: Boolean\n\nA subcommand-specific flag',
11761288
},
1289+
registry: globalDefs.registry,
11771290
}
11781291
}
11791292

@@ -1312,14 +1425,19 @@ description: Test command without params
13121425
class SubMixedNoDescribe {
13131426
static description = 'Subcommand with command-specific params but no describe'
13141427
static usage = ['<arg>']
1315-
static params = ['sub-custom', 'registry']
13161428
static definitions = {
13171429
'sub-custom': {
13181430
key: 'sub-custom',
13191431
default: 'default-val',
13201432
type: String,
13211433
description: 'A command-specific flag without describe',
13221434
},
1435+
registry: {
1436+
key: 'registry',
1437+
default: 'https://registry.npmjs.org/',
1438+
type: String,
1439+
description: 'The base URL of the npm registry',
1440+
},
13231441
}
13241442
}
13251443

workspaces/config/test/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1793,4 +1793,4 @@ t.test('before and min-release-age', async t => {
17931793
await config.load()
17941794
// Simple gut check to make sure we didn't do + instead of -
17951795
t.ok(config.flat.before < Date.now(), 'before date is in the past not the future')
1796-
})
1796+
})

0 commit comments

Comments
 (0)