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 ### Configuration ` // 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': '' }, }, }), /npm-access\.md/) await t.rejects(testBuildDocs(t, { content: { commands: { 'npm-access.md': '' }, }, }), /npm-access\.md/) }) t.test('definitions', async t => { await t.rejects(testBuildDocs(t, { content: { 'using-npm': { 'config.md': '' }, }, }), /config\.md/) await t.rejects(testBuildDocs(t, { content: { 'using-npm': { 'config.md': '' }, }, }), /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': '![](/src)' }, 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, //, 'doc has usage placeholder') t.match(docContent, //, '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 = '{{ content }}' 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: [''], 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: [''], }, }) const doc = `--- title: npm-testcmd-noparams section: 1 description: Test command without params --- ### Synopsis ` 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: [''], 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: [''], 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: [''], 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: [''], 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: [''], 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: [''], 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 = [''] 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: [''], 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: [''], 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: [''], 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: [''], 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 = [''] 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: [''], 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: [''], 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 = [''] 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: [''], 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 = [''] static params = ['registry', 'tag'] } const commandLoader = createCommandLoader({ 'testcmd-sub-params': { usage: [''], 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: [''], 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 = [''] 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: [''], 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 = [''] static params = ['registry'] static definitions = { registry: { ...globalDefs.registry, alias: ['reg', 'r'], describe: () => globalDefs.registry.describe(), }, } } const commandLoader = createCommandLoader({ 'testcmd-sub-global-alias': { usage: [''], 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 = [''] 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: [''], 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 = [''] 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: [''], 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 = [''] } const commandLoader = createCommandLoader({ 'testcmd-sub-noparams': { usage: [''], 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') }) })