const t = require('tap')
const { join } = require('path')
const walk = require('ignore-walk')
const fs = require('fs/promises')
const yaml = require('yaml')
const {
paths: { content: CONTENT_DIR, nav: NAV, template: TEMPLATE },
} = require('../lib/index.js')
// Helper to generate nav entries from content structure
const generateNavFromContent = (content, prefix = '') => {
const entries = []
for (const [key, value] of Object.entries(content)) {
if (key.endsWith('.md')) {
const name = key.replace('.md', '')
const url = prefix ? `${prefix}/${name}` : `/${name}`
entries.push({ url })
} else if (typeof value === 'object') {
const children = generateNavFromContent(value, `/${key}`)
if (children.length > 0) {
entries.push(...children)
}
}
}
return entries
}
const testBuildDocs = async (t, { verify, commandLoader, ...opts } = {}) => {
const mockedBuild = require('../lib/build.js')
const fixtures = {
man: {},
html: {},
md: {},
...opts,
}
// Ensure commands directory exists if content is provided
if (fixtures.content && !fixtures.content.commands) {
fixtures.content.commands = {}
}
// If custom content is provided but not custom nav, auto-generate nav from content
if (fixtures.content && !fixtures.nav) {
const navEntries = generateNavFromContent(fixtures.content)
fixtures.nav = yaml.stringify(navEntries)
}
const root = t.testdir(fixtures)
const paths = {
content: fixtures.content ? join(root, 'content') : CONTENT_DIR,
template: fixtures.template ? join(root, 'template') : TEMPLATE,
nav: fixtures.nav ? join(root, 'nav') : NAV,
man: join(root, 'man'),
html: join(root, 'html'),
md: join(root, 'md'),
// Skip auto-generation of missing docs when using test fixtures
skipAutoGenerate: !!fixtures.content,
// Skip nav generation when using test fixtures with custom content
skipGenerateNav: !!fixtures.content,
// Custom command loader for testing
commandLoader,
}
return {
results: await mockedBuild({ ...paths, verify }),
root,
...paths,
}
}
// Helper to create a standard command doc with placeholders
const createCommandDoc = (title, description) => `---
title: ${title}
section: 1
description: ${description}
---
### Synopsis
<!-- AUTOGENERATED USAGE DESCRIPTIONS -->
### Configuration
<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->
`
// Helper to read and return HTML content from a built doc
const readHtmlDoc = async (htmlPath, commandName) => {
const htmlFile = join(htmlPath, `commands/${commandName}.html`)
return await fs.readFile(htmlFile, 'utf-8')
}
// Helper to test a command doc with common assertions
const testCommandDoc = async (t, commandName, description, assertions = {}) => {
const doc = createCommandDoc(commandName, description)
const { html } = await testBuildDocs(t, {
content: {
commands: { [`${commandName}.md`]: doc },
},
nav: `- url: /commands/${commandName}`,
})
const htmlContent = await readHtmlDoc(html, commandName)
// Default assertions
t.ok(htmlContent.length > 0, `generates HTML for ${commandName} command`)
// Custom assertions
if (assertions.match) {
for (const pattern of assertions.match) {
t.match(htmlContent, pattern, `contains expected pattern: ${pattern}`)
}
}
return { html, htmlContent }
}
// Helper to create test directory structure for autoGenerateMissingDocs tests
const createAutoGenTestDir = (t, { existingDocs = {}, navEntries = [], commandFiles = {} }) => {
const navYml = `
- title: CLI Commands
children:
- title: npm
url: /commands/npm
${navEntries.map(entry => ` - title: ${entry.title}\n url: ${entry.url}\n description: ${entry.description}`).join('\n')}
`
return t.testdir({
content: {
commands: existingDocs,
},
'nav.yml': navYml,
lib: {
commands: commandFiles,
},
})
}
// Helper to verify nav structure after auto-generation
const verifyNavStructure = async (navPath) => {
const navContent = await fs.readFile(navPath, 'utf-8')
const navData = yaml.parse(navContent)
const commandsSection = navData.find(s => s.title === 'CLI Commands')
return { navContent, navData, commandsSection }
}
t.test('builds and verifies the real docs', async (t) => {
const { man, html, md, results } = await testBuildDocs(t, { verify: true })
const allFiles = (await Promise.all([
walk({ path: man }).then(r => r.length),
walk({ path: html }).then(r => r.length),
walk({ path: md }).then(r => r.length),
])).reduce((a, b) => a + b, 0)
t.equal(allFiles, results.length)
})
t.test('fails on mismatched nav', async t => {
await t.rejects(() => testBuildDocs(t, {
content: { 'test.md': '' },
nav: '- url: /test2',
}), 'Documentation navigation (nav.yml) does not match filesystem')
})
t.test('missing placeholders', async t => {
t.test('command', async t => {
await t.rejects(testBuildDocs(t, {
content: {
commands: { 'npm-access.md': '<!-- AUTOGENERATED USAGE DESCRIPTIONS -->' },
},
}), /npm-access\.md/)
await t.rejects(testBuildDocs(t, {
content: {
commands: { 'npm-access.md': '<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->' },
},
}), /npm-access\.md/)
})
t.test('definitions', async t => {
await t.rejects(testBuildDocs(t, {
content: {
'using-npm': { 'config.md': '<!-- AUTOGENERATED CONFIG SHORTHANDS -->' },
},
}), /config\.md/)
await t.rejects(testBuildDocs(t, {
content: {
'using-npm': { 'config.md': '<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->' },
},
}), /config\.md/)
})
})
t.test('html', async t => {
// these don't happen anywhere in the docs so test this for coverage
// but we test for coverage
t.test('files can link to root pages', async t => {
await testBuildDocs(t, {
content: { 'test.md': '[Test](/test)' },
nav: '- url: /test',
})
})
t.test('succeeds with empty content', async t => {
await testBuildDocs(t, {
content: { 'test.md': '' },
nav: '- url: /test',
template: '{{ content }}',
})
})
t.test('fails on missing vars in template', async t => {
await t.rejects(() => testBuildDocs(t, {
template: '{{ hello }}',
}), /\{\{ hello \}\}/)
})
t.test('rewrites img src', async t => {
await testBuildDocs(t, {
content: { 'test.md': '' },
nav: '- url: /test',
})
})
})
t.test('command-specific definitions and exclusive parameters', async t => {
// Test through the actual doc building process with real commands
t.test('config command uses params correctly', async t => {
await testCommandDoc(t, 'npm-config', 'Manage the npm configuration files')
})
t.test('install command includes exclusive save parameters', async t => {
const { htmlContent } = await testCommandDoc(t, 'npm-install', 'Install a package', {
match: [/save/],
})
// The install command should have save-related params due to exclusive expansion
t.match(htmlContent, /save/, 'includes save-related configuration')
})
})
t.test('autoGenerateMissingDocs', async t => {
const { autoGenerateMissingDocs } = require('../lib/build.js')
t.test('generates docs for missing commands', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {
'npm-access.md': createCommandDoc('npm-access', 'Set access level on published packages'),
},
navEntries: [
{ title: 'npm access', url: '/commands/npm-access', description: 'Set access level on published packages' },
],
commandFiles: {
'access.js': `
class AccessCommand {
static description = 'Set access level on published packages'
}
module.exports = AccessCommand
`,
'testcmd.js': `
class TestCommand {
static description = 'A test command'
}
module.exports = TestCommand
`,
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Verify the doc was created
const testcmdDocPath = join(contentPath, 'commands', 'npm-testcmd.md')
const docExists = await fs.access(testcmdDocPath).then(() => true).catch(() => false)
t.ok(docExists, 'creates documentation file for missing command')
// Verify the doc has correct content
const docContent = await fs.readFile(testcmdDocPath, 'utf-8')
t.match(docContent, /title: npm-testcmd/, 'doc has correct title')
t.match(docContent, /description: A test command/, 'doc has correct description')
t.match(docContent, /<!-- AUTOGENERATED USAGE DESCRIPTIONS -->/, 'doc has usage placeholder')
t.match(docContent, /<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->/, 'doc has config placeholder')
t.match(docContent, /A test command/, 'doc has description in body')
})
t.test('updates nav.yml for new commands', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {
'npm-existing.md': `---
title: npm-existing
section: 1
description: Existing command
---`,
},
navEntries: [
{ title: 'npm existing', url: '/commands/npm-existing', description: 'Existing command' },
],
commandFiles: {
'existing.js': `
class ExistingCommand {
static description = 'Existing command'
}
module.exports = ExistingCommand
`,
'newcmd.js': `
class NewCommand {
static description = 'New command'
}
module.exports = NewCommand
`,
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Read and verify nav.yml was updated
const { commandsSection } = await verifyNavStructure(navPath)
t.ok(commandsSection, 'nav has CLI Commands section')
const newCmdEntry = commandsSection.children.find(c => c.url === '/commands/npm-newcmd')
t.ok(newCmdEntry, 'nav has entry for new command')
t.equal(newCmdEntry.title, 'npm newcmd', 'nav entry has correct title')
t.equal(newCmdEntry.description, 'New command', 'nav entry has correct description')
})
t.test('sorts nav children alphabetically', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {},
navEntries: [
{ title: 'npm zebra', url: '/commands/npm-zebra', description: 'Zebra command' },
{ title: 'npm alpha', url: '/commands/npm-alpha', description: 'Alpha command' },
],
commandFiles: {
'zebra.js': `module.exports = { description: 'Zebra command' }`,
'alpha.js': `module.exports = { description: 'Alpha command' }`,
'beta.js': `module.exports = { description: 'Beta command' }`,
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Verify sorting
const { commandsSection } = await verifyNavStructure(navPath)
const titles = commandsSection.children.map(c => c.title)
// npm should be first
t.equal(titles[0], 'npm', 'npm command is first')
// Rest should be alphabetically sorted
const rest = titles.slice(1)
const sorted = [...rest].sort()
t.same(rest, sorted, 'remaining commands are alphabetically sorted')
t.ok(titles.includes('npm alpha'), 'includes alpha')
t.ok(titles.includes('npm beta'), 'includes beta')
t.ok(titles.includes('npm zebra'), 'includes zebra')
})
t.test('handles commands without description', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {},
navEntries: [],
commandFiles: {
'nodesc.js': `module.exports = {}`,
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Verify fallback description
const docPath = join(contentPath, 'commands', 'npm-nodesc.md')
const docContent = await fs.readFile(docPath, 'utf-8')
t.match(docContent, /description: The nodesc command/, 'uses fallback description in frontmatter')
t.match(docContent, /The nodesc command/, 'uses fallback description in body')
})
t.test('does not add duplicate entries to nav', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {},
navEntries: [
{ title: 'npm duplicate', url: '/commands/npm-duplicate', description: 'Already exists' },
],
commandFiles: {
'duplicate.js': `module.exports = { description: 'Already exists' }`,
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Verify no duplicate
const { commandsSection } = await verifyNavStructure(navPath)
const duplicateEntries = commandsSection.children.filter(c => c.url === '/commands/npm-duplicate')
t.equal(duplicateEntries.length, 1, 'does not create duplicate nav entries')
})
t.test('skips update when no missing docs', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {
'npm-complete.md': `---
title: npm-complete
section: 1
description: Complete command
---`,
},
navEntries: [
{ title: 'npm complete', url: '/commands/npm-complete', description: 'Complete command' },
],
commandFiles: {
'complete.js': `module.exports = { description: 'Complete command' }`,
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
const navBefore = await fs.readFile(navPath, 'utf-8')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
const navAfter = await fs.readFile(navPath, 'utf-8')
t.equal(navBefore, navAfter, 'does not modify nav when no missing docs')
})
t.test('handles nav without CLI Commands section', async t => {
const testDir = t.testdir({
content: {
commands: {},
},
'nav.yml': `
- title: Other Section
children: []
`,
lib: {
commands: {
'test.js': `module.exports = { description: 'Test command' }`,
},
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
// Should not throw, just skip nav update
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Doc should still be created
const docPath = join(contentPath, 'commands', 'npm-test.md')
const docExists = await fs.access(docPath).then(() => true).catch(() => false)
t.ok(docExists, 'creates doc even when nav section missing')
})
t.test('handles nav with CLI Commands but no children', async t => {
const testDir = t.testdir({
content: {
commands: {},
},
'nav.yml': `
- title: CLI Commands
`,
lib: {
commands: {
'test.js': `module.exports = { description: 'Test command' }`,
},
},
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const commandsPath = join(testDir, 'lib', 'commands')
// Should not throw, just skip nav children update
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Doc should still be created
const docPath = join(contentPath, 'commands', 'npm-test.md')
const docExists = await fs.access(docPath).then(() => true).catch(() => false)
t.ok(docExists, 'creates doc even when children missing')
})
t.test('handles npm command not in first position', async t => {
const testDir = createAutoGenTestDir(t, {
existingDocs: {},
navEntries: [
{ title: 'npm alpha', url: '/commands/npm-alpha', description: 'Alpha command' },
],
commandFiles: {
'alpha.js': `module.exports = { description: 'Alpha command' }`,
'beta.js': `module.exports = { description: 'Beta command' }`,
},
})
// Manually adjust nav to put npm not first
const navPath = join(testDir, 'nav.yml')
await fs.writeFile(navPath, `
- title: CLI Commands
children:
- title: npm alpha
url: /commands/npm-alpha
- title: npm
url: /commands/npm
`)
const contentPath = join(testDir, 'content')
const commandsPath = join(testDir, 'lib', 'commands')
await autoGenerateMissingDocs(contentPath, navPath, commandsPath)
// Verify npm moved to first position
const { commandsSection } = await verifyNavStructure(navPath)
const titles = commandsSection.children.map(c => c.title)
t.equal(titles[0], 'npm', 'npm command moved to first position')
})
t.test('calls autoGenerateMissingDocs via run with default skipAutoGenerate', async t => {
// This test ensures the default parameter path is covered
const build = require('../lib/build.js')
const testDir = t.testdir({
content: {
commands: {
'npm-test.md': createCommandDoc('npm-test', 'Test'),
},
},
'nav.yml': `
- title: CLI Commands
url: /commands/npm-test
`,
lib: {
commands: {
'test.js': `module.exports = { description: 'Test' }`,
},
},
})
const template = '<html>{{ content }}</html>'
await fs.writeFile(join(testDir, 'template.html'), template)
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
const templatePath = join(testDir, 'template.html')
const manPath = join(testDir, 'man')
const htmlPath = join(testDir, 'html')
const mdPath = join(testDir, 'md')
// Call run with skipAutoGenerate set to true to avoid hitting real commands
const results = await build({
content: contentPath,
template: templatePath,
nav: navPath,
man: manPath,
html: htmlPath,
md: mdPath,
skipAutoGenerate: true,
})
t.ok(results.length > 0, 'build runs successfully')
})
})
t.test('command-specific definitions with missing command file', async t => {
// This test targets the catch block in index.js lines 110-113
// Use a command that exists and has params - the catch block is for safety
// when command-specific definitions can't be loaded
await testCommandDoc(t, 'npm-install', 'Install a package', {
match: [/install/],
})
})
t.test('generateNav', async t => {
const { generateNav } = require('../lib/build.js')
t.test('commands directory does not exist', async t => {
// Tests line 63: await dirExists(docsCommandsPath) ? await fs.readdir(docsCommandsPath) : []
// When commands directory doesn't exist, should return empty array
const testDir = t.testdir({
content: {
// No commands directory
'configuring-npm': {
'install.md': `---
title: Install
section: 5
description: Download and install node and npm
---`,
},
},
'nav.yml': '',
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
await generateNav(contentPath, navPath)
const navContent = await fs.readFile(navPath, 'utf-8')
const navData = yaml.parse(navContent)
// Should NOT have CLI Commands section since commands dir doesn't exist
const commandsSection = navData.find(s => s.title === 'CLI Commands')
t.notOk(commandsSection, 'no CLI Commands section when commands directory is missing')
// Should still have configuring-npm section
const configuringSection = navData.find(s => s.title === 'Configuring npm')
t.ok(configuringSection, 'has configuring-npm section')
})
t.test('command title fallback to name', async t => {
// Tests line 72: (attributes.title || name).replace(/^npm-/, 'npm ')
// When command doc has no title, should use filename as title
const testDir = t.testdir({
content: {
commands: {
// Command doc WITHOUT title in frontmatter - should use name
'npm-test-cmd.md': `---
section: 1
description: A test command
---
Content here`,
},
},
'nav.yml': '',
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
await generateNav(contentPath, navPath)
const navContent = await fs.readFile(navPath, 'utf-8')
const navData = yaml.parse(navContent)
const commandsSection = navData.find(s => s.title === 'CLI Commands')
t.ok(commandsSection, 'has CLI Commands section')
const testCmdEntry = commandsSection.children.find(c => c.url === '/commands/npm-test-cmd')
t.ok(testCmdEntry, 'has test-cmd entry')
// Should use name ('npm-test-cmd') and replace 'npm-' with 'npm '
t.equal(testCmdEntry.title, 'npm test-cmd', 'uses name with npm- replaced to npm space')
})
t.test('command description fallback to empty string', async t => {
// Tests line 77: description: attributes.description || ''
// When command doc has no description, should use empty string
const testDir = t.testdir({
content: {
commands: {
// Command doc WITHOUT description in frontmatter - should use ''
'npm-no-desc.md': `---
title: npm-no-desc
section: 1
---
Content here`,
},
},
'nav.yml': '',
})
const contentPath = join(testDir, 'content')
const navPath = join(testDir, 'nav.yml')
await generateNav(contentPath, navPath)
const navContent = await fs.readFile(navPath, 'utf-8')
const navData = yaml.parse(navContent)
const commandsSection = navData.find(s => s.title === 'CLI Commands')
t.ok(commandsSection, 'has CLI Commands section')
const noDescEntry = commandsSection.children.find(c => c.url === '/commands/npm-no-desc')
t.ok(noDescEntry, 'has no-desc entry')
// Should use empty string since no description in frontmatter
t.equal(noDescEntry.description, '', 'uses empty string when no description')
})
})
t.test('replaceParams with name edge cases', async t => {
// Test the conditions around the catch block more explicitly
t.test('npm command (no params)', async t => {
await testCommandDoc(t, 'npm', 'javascript package manager')
})
t.test('npx command (special case)', async t => {
await testCommandDoc(t, 'npx', 'Run a command from a local or remote npm package', {
match: [/package/],
})
})
t.test('regular command with params (access)', async t => {
// Tests line 110: name && name !== 'npm' && name !== 'npx'
await testCommandDoc(t, 'npm-access', 'Set access level on published packages', {
match: [/registry/],
})
})
t.test('command with subcommands and aliases (trust)', async t => {
// Tests subcommand code path including line 184 (aliases in subcommand definitions)
// npm trust has subcommands with definitions that include aliases (repo, env)
await testCommandDoc(t, 'npm-trust', 'Create a trusted relationship between a package and a OIDC provider', {
match: [/--repo/, /--env/],
})
})
})
// Helper to create a command loader from a map of command names to command objects
const createCommandLoader = (commands) => (name) => {
if (commands[name]) {
return commands[name]
}
// Fall back to real commands
return require(`../../lib/commands/${name}`)
}
// Test harness for custom commands to test edge cases
t.test('custom command loader tests', async t => {
t.test('command without description', async t => {
const commandLoader = createCommandLoader({
'testcmd-nodesc': {
usage: ['<pkg>'],
params: ['registry'],
},
})
const doc = createCommandDoc('npm-testcmd-nodesc', 'Test command without description')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-nodesc.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-nodesc')
t.ok(htmlContent.length > 0, 'generates HTML for command without description')
t.match(htmlContent, /registry/, 'includes registry param')
})
t.test('command without usage', async t => {
const commandLoader = createCommandLoader({
'testcmd-nousage': {
params: ['registry'],
},
})
const doc = createCommandDoc('npm-testcmd-nousage', 'Test command without usage')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-nousage.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-nousage')
t.ok(htmlContent.length > 0, 'generates HTML for command without usage')
t.match(htmlContent, /npm testcmd-nousage/, 'includes command name in usage')
})
t.test('command without params (no definitions)', async t => {
const commandLoader = createCommandLoader({
'testcmd-noparams': {
usage: ['<pkg>'],
},
})
const doc = `---
title: npm-testcmd-noparams
section: 1
description: Test command without params
---
### Synopsis
<!-- AUTOGENERATED USAGE DESCRIPTIONS -->
`
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-noparams.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-noparams')
t.ok(htmlContent.length > 0, 'generates HTML for command without params')
t.match(htmlContent, /npm testcmd-noparams/, 'includes command name')
})
t.test('command with empty definitions (has config placeholder)', async t => {
const commandLoader = createCommandLoader({
'testcmd-empty-defs': {
usage: ['<pkg>'],
definitions: [],
},
})
const doc = createCommandDoc('npm-testcmd-empty-defs', 'Test command with empty definitions')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-empty-defs.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-empty-defs')
t.ok(htmlContent.length > 0, 'generates HTML for command with empty definitions')
t.match(htmlContent, /npm testcmd-empty-defs/, 'includes command name')
})
t.test('command with params referencing non-existent global definition', async t => {
const commandLoader = createCommandLoader({
'testcmd-missing-param': {
usage: ['<pkg>'],
params: ['non-existent-param', 'registry'],
},
})
const doc = createCommandDoc('npm-testcmd-missing-param', 'Test command with missing param')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-missing-param.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-missing-param')
t.ok(htmlContent.length > 0, 'generates HTML even with missing param definition')
t.match(htmlContent, /registry/, 'includes valid param')
})
t.test('command with exclusive param already resolved', async t => {
const commandLoader = createCommandLoader({
'testcmd-exclusive-resolved': {
usage: ['<pkg>'],
definitions: {
'save-dev': {
key: 'save-dev',
default: false,
type: Boolean,
description: 'Save as devDependency',
exclusive: ['save-optional'],
describe: () => '#### `save-dev`\n\n* Default: false\n* Type: Boolean\n\nSave as devDependency',
},
'save-optional': {
key: 'save-optional',
default: false,
type: Boolean,
description: 'Save as optionalDependency',
describe: () => '#### `save-optional`\n\n* Default: false\n* Type: Boolean\n\nSave as optionalDependency',
},
},
},
})
const doc = createCommandDoc('npm-testcmd-exclusive-resolved', 'Test command with exclusive already resolved')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-exclusive-resolved.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-exclusive-resolved')
t.ok(htmlContent.length > 0, 'generates HTML with exclusive params')
t.match(htmlContent, /save-dev/, 'includes save-dev')
t.match(htmlContent, /save-optional/, 'includes save-optional')
})
t.test('command with exclusive param that does not exist in global definitions', async t => {
const commandLoader = createCommandLoader({
'testcmd-exclusive-missing': {
usage: ['<pkg>'],
definitions: {
'my-flag': {
key: 'my-flag',
default: false,
type: Boolean,
description: 'A flag with non-existent exclusive',
exclusive: ['non-existent-global-param'],
describe: () => '#### `my-flag`\n\n* Default: false\n* Type: Boolean\n\nA flag with non-existent exclusive',
},
},
},
})
const doc = createCommandDoc('npm-testcmd-exclusive-missing', 'Test command with missing exclusive')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-exclusive-missing.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-exclusive-missing')
t.ok(htmlContent.length > 0, 'generates HTML even with missing exclusive param')
t.match(htmlContent, /my-flag/, 'includes the defined flag')
})
t.test('command with one definition with short flag', async t => {
const commandLoader = createCommandLoader({
'testcmd-short': {
usage: ['<pkg>'],
params: ['custom-flag'],
definitions: {
'custom-flag': {
key: 'custom-flag',
default: false,
type: Boolean,
short: 'c',
description: 'A custom flag with a short version',
describe: () => '#### `custom-flag`\n\n* Default: false\n* Type: Boolean\n\nA custom flag with a short version',
},
},
},
})
const doc = createCommandDoc('npm-testcmd-short', 'Test command with short flag')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-short.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-short')
t.ok(htmlContent.length > 0, 'generates HTML for command with short flag')
t.match(htmlContent, /custom-flag/, 'includes custom flag')
t.match(htmlContent, /-c/, 'includes short flag')
})
t.test('command with definition with aliases', async t => {
const commandLoader = createCommandLoader({
'testcmd-alias': {
usage: ['<pkg>'],
definitions: {
'aliased-flag': {
key: 'aliased-flag',
default: '',
type: String,
alias: ['af', 'alias-flag'],
description: 'A flag with aliases',
describe: () => '#### `aliased-flag`\n\n* Default: ""\n* Type: String\n* Alias: --af, --alias-flag\n\nA flag with aliases',
},
},
},
})
const doc = createCommandDoc('npm-testcmd-alias', 'Test command with aliased flag')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-alias.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-alias')
t.ok(htmlContent.length > 0, 'generates HTML for command with aliases')
t.match(htmlContent, /aliased-flag/, 'includes aliased flag')
t.match(htmlContent, /--af/, 'includes first alias')
t.match(htmlContent, /--alias-flag/, 'includes second alias')
})
t.test('command with subcommands', async t => {
class SubA {
static description = 'Subcommand A description'
static usage = ['<arg>']
static definitions = {
'sub-a-flag': {
key: 'sub-a-flag',
default: false,
type: Boolean,
describe: () => '#### `sub-a-flag`\n\n* Default: false\n* Type: Boolean\n\nFlag for subcommand A',
},
}
}
class SubB {
static description = 'Subcommand B description'
static usage = ['[options]']
static params = ['registry']
}
const commandLoader = createCommandLoader({
'testcmd-subs': {
usage: ['<subcommand>'],
params: null,
subcommands: {
'sub-a': SubA,
'sub-b': SubB,
},
},
})
const doc = createCommandDoc('npm-testcmd-subs', 'Test command with subcommands')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-subs.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-subs')
t.ok(htmlContent.length > 0, 'generates HTML for command with subcommands')
t.match(htmlContent, /npm testcmd-subs sub-a/, 'includes sub-a subcommand')
t.match(htmlContent, /npm testcmd-subs sub-b/, 'includes sub-b subcommand')
t.match(htmlContent, /Subcommand A description/, 'includes sub-a description')
t.match(htmlContent, /sub-a-flag/, 'includes sub-a specific flag')
})
t.test('command with exclusive params', async t => {
const commandLoader = createCommandLoader({
'testcmd-exclusive': {
usage: ['<pkg>'],
params: ['save'],
},
})
const doc = createCommandDoc('npm-testcmd-exclusive', 'Test command with exclusive params')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-exclusive.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-exclusive')
t.ok(htmlContent.length > 0, 'generates HTML for command with exclusive params')
t.match(htmlContent, /save/, 'includes save param')
})
t.test('command without workspaces', async t => {
const commandLoader = createCommandLoader({
'testcmd-noworkspaces': {
usage: ['<pkg>'],
params: ['registry'],
workspaces: false,
},
})
const doc = createCommandDoc('npm-testcmd-noworkspaces', 'Test command without workspaces')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-noworkspaces.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-noworkspaces')
t.ok(htmlContent.length > 0, 'generates HTML for command without workspaces')
t.match(htmlContent, /unaware of workspaces/, 'includes workspaces note')
})
t.test('command with workspaces enabled', async t => {
const commandLoader = createCommandLoader({
'testcmd-workspaces': {
usage: ['<pkg>'],
params: ['registry'],
workspaces: true,
},
})
const doc = createCommandDoc('npm-testcmd-workspaces', 'Test command with workspaces')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-workspaces.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-workspaces')
t.ok(htmlContent.length > 0, 'generates HTML for command with workspaces')
t.notMatch(htmlContent, /unaware of workspaces/, 'does NOT include workspaces note')
})
t.test('subcommand without description', async t => {
class SubNoDesc {
static usage = ['<arg>']
static definitions = {
flag: {
key: 'flag',
default: false,
type: Boolean,
describe: () => '#### `flag`\n\n* Default: false\n* Type: Boolean\n\nA flag',
},
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-nodesc': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubNoDesc,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-nodesc', 'Test command with subcommand without description')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-nodesc.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-nodesc')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand without description')
t.match(htmlContent, /npm testcmd-sub-nodesc mysub/, 'includes subcommand')
})
t.test('subcommand without usage', async t => {
class SubNoUsage {
static description = 'Subcommand without usage'
static definitions = {
flag: {
key: 'flag',
default: false,
type: Boolean,
describe: () => '#### `flag`\n\n* Default: false\n* Type: Boolean\n\nA flag',
},
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-nousage': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubNoUsage,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-nousage', 'Test command with subcommand without usage')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-nousage.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-nousage')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand without usage')
t.match(htmlContent, /Subcommand without usage/, 'includes subcommand description')
})
t.test('subcommand with short flag and alias', async t => {
class SubWithShortAlias {
static description = 'Subcommand with short and alias'
static usage = ['<arg>']
static definitions = {
'complex-flag': {
key: 'complex-flag',
default: '',
type: String,
short: 'x',
alias: ['cf', 'cflag'],
describe: () => '#### `complex-flag`\n\n* Default: ""\n* Type: String\n\nA complex flag',
},
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-complex': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubWithShortAlias,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-complex', 'Test command with complex subcommand')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-complex.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-complex')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with short and alias')
t.match(htmlContent, /complex-flag/, 'includes complex flag')
t.match(htmlContent, /-x/, 'includes short flag')
t.match(htmlContent, /--cf/, 'includes first alias')
t.match(htmlContent, /--cflag/, 'includes second alias')
})
t.test('subcommand with explicit params (not derived from definitions)', async t => {
class SubWithParams {
static description = 'Subcommand with explicit params'
static usage = ['<arg>']
static params = ['registry', 'tag']
}
const commandLoader = createCommandLoader({
'testcmd-sub-params': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubWithParams,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-params', 'Test command with subcommand with explicit params')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-params.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-params')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with explicit params')
t.match(htmlContent, /registry/, 'includes registry param')
t.match(htmlContent, /tag/, 'includes tag param')
})
t.test('command with mixed command-specific and global params', async t => {
const { definitions: globalDefs } = require('@npmcli/config/lib/definitions')
const commandLoader = createCommandLoader({
'testcmd-mixed': {
usage: ['<pkg>'],
definitions: {
'custom-only': {
key: 'custom-only',
default: false,
type: Boolean,
description: 'A command-specific flag',
describe: () => '#### `custom-only`\n\n* Default: false\n* Type: Boolean\n\nA command-specific flag',
},
registry: globalDefs.registry,
},
},
})
const doc = createCommandDoc('npm-testcmd-mixed', 'Test command with mixed params')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-mixed.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-mixed')
t.ok(htmlContent.length > 0, 'generates HTML for command with mixed params')
t.match(htmlContent, /custom-only/, 'includes command-specific flag')
t.match(htmlContent, /registry/, 'includes global registry param')
})
t.test('subcommand with command-specific and global params', async t => {
const { definitions: globalDefs } = require('@npmcli/config/lib/definitions')
class SubMixed {
static description = 'Subcommand with mixed params'
static usage = ['<arg>']
static definitions = {
'sub-flag': {
key: 'sub-flag',
default: false,
type: Boolean,
description: 'A subcommand-specific flag',
describe: () => '#### `sub-flag`\n\n* Default: false\n* Type: Boolean\n\nA subcommand-specific flag',
},
registry: globalDefs.registry,
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-mixed': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubMixed,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-mixed', 'Test command with subcommand with mixed params')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-mixed.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-mixed')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with mixed params')
t.match(htmlContent, /sub-flag/, 'includes subcommand-specific flag')
t.match(htmlContent, /registry/, 'includes global registry param')
})
t.test('subcommand with global config param that has alias in subDefinitions', async t => {
const { definitions: globalDefs } = require('@npmcli/config/lib/definitions')
class SubWithGlobalAlias {
static description = 'Subcommand using global param with alias override'
static usage = ['<pkg>']
static params = ['registry']
static definitions = {
registry: {
...globalDefs.registry,
alias: ['reg', 'r'],
describe: () => globalDefs.registry.describe(),
},
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-global-alias': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubWithGlobalAlias,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-global-alias', 'Test subcommand with global aliased param')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-global-alias.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-global-alias')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with global aliased param')
t.match(htmlContent, /registry/, 'includes registry param')
t.match(htmlContent, /--reg/, 'includes first alias')
t.match(htmlContent, /--r/, 'includes second alias')
})
t.test('subcommand with flags without describe method (fallback table format)', async t => {
class SubWithoutDescribe {
static description = 'Subcommand with simple flags'
static usage = ['<arg>']
static definitions = {
'simple-flag': {
key: 'simple-flag',
default: false,
type: Boolean,
description: 'A simple flag without describe method',
},
'aliased-flag': {
key: 'aliased-flag',
default: 'default-val',
type: String,
description: 'A flag with aliases and short form',
alias: ['af', 'alias-f'],
short: 'a',
},
'custom-default': {
key: 'custom-default',
default: 'value',
type: String,
defaultDescription: 'custom description of default',
typeDescription: 'Custom Type',
description: 'A flag with custom descriptions',
},
'falsy-default-desc': {
key: 'falsy-default-desc',
default: 'another-value',
type: String,
defaultDescription: '',
description: 'A flag with empty defaultDescription',
},
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-nodescribe': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubWithoutDescribe,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-nodescribe', 'Test command with subcommand without describe')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-nodescribe.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-nodescribe')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with simple flags')
t.match(htmlContent, /simple-flag/, 'includes simple flag')
t.match(htmlContent, /aliased-flag/, 'includes aliased flag')
t.match(htmlContent, /-a/, 'includes short form')
t.match(htmlContent, /af/, 'includes alias')
t.match(htmlContent, /custom description of default/, 'includes custom default description')
t.match(htmlContent, /Custom Type/, 'includes custom type description')
t.match(htmlContent, /falsy-default-desc/, 'includes flag with falsy defaultDescription')
})
t.test('subcommand with command-specific params and table format (no describe)', async t => {
class SubMixedNoDescribe {
static description = 'Subcommand with command-specific params but no describe'
static usage = ['<arg>']
static definitions = {
'sub-custom': {
key: 'sub-custom',
default: 'default-val',
type: String,
description: 'A command-specific flag without describe',
},
registry: {
key: 'registry',
default: 'https://registry.npmjs.org/',
type: String,
description: 'The base URL of the npm registry',
},
}
}
const commandLoader = createCommandLoader({
'testcmd-sub-mixed-nodesc': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubMixedNoDescribe,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-mixed-nodesc', 'Test command with subcommand with mixed params no describe')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-mixed-nodesc.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-mixed-nodesc')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with command-specific params no describe')
t.match(htmlContent, /sub-custom/, 'includes command-specific flag')
t.match(htmlContent, /registry/, 'includes global registry param')
})
t.test('subcommand with no params', async t => {
class SubNoParams {
static description = 'Subcommand with no params'
static usage = ['<arg>']
}
const commandLoader = createCommandLoader({
'testcmd-sub-noparams': {
usage: ['<subcommand>'],
params: null,
subcommands: {
mysub: SubNoParams,
},
},
})
const doc = createCommandDoc('npm-testcmd-sub-noparams', 'Test command with subcommand with no params')
const { html } = await testBuildDocs(t, {
commandLoader,
content: {
commands: { 'npm-testcmd-sub-noparams.md': doc },
},
})
const htmlContent = await readHtmlDoc(html, 'npm-testcmd-sub-noparams')
t.ok(htmlContent.length > 0, 'generates HTML for subcommand with no params')
t.match(htmlContent, /mysub/, 'includes subcommand')
})
})