GitHub

@@ -7,6 +7,281 @@ const parseFrontMatter = require('front-matter')

77

const checkNav = require('./check-nav.js')

88

const { DOC_EXT, ...transform } = require('./index.js')

9910+

// Helper to check if a directory exists

11+

const dirExists = async (path) => {

12+

try {

13+

const stat = await fs.stat(path)

14+

return stat.isDirectory()

15+

} catch {

16+

return false

17+

}

18+

}

19+20+

// Helper to read docs from a section directory

21+

const readSectionDocs = async (contentPath, section, orderedUrls) => {

22+

const sectionPath = join(contentPath, section)

23+

if (!await dirExists(sectionPath)) {

24+

return []

25+

}

26+27+

const files = await fs.readdir(sectionPath)

28+

const docFiles = files.filter(f => f.endsWith(DOC_EXT))

29+30+

// If no doc files exist, return empty array

31+

/* istanbul ignore if - defensive check for empty directories */

32+

if (docFiles.length === 0) {

33+

return []

34+

}

35+36+

// Parse each doc file to get title and description from frontmatter

37+

const docs = await Promise.all(

38+

docFiles.map(async (file) => {

39+

const content = await fs.readFile(join(sectionPath, file), 'utf-8')

40+

const { attributes } = parseFrontMatter(content)

41+

const name = basename(file, DOC_EXT)

42+43+

return {

44+

title: attributes.title,

45+

url: `/${section}/${name}`,

46+

description: attributes.description,

47+

name,

48+

}

49+

})

50+

)

51+52+

// Preserve order from orderedUrls, append any new files at the end sorted alphabetically

53+

const orderedDocs = []

54+

const docsByUrl = new Map(docs.map(d => [d.url, d]))

55+56+

// First, add docs in the order they appear in orderedUrls

57+

for (const url of orderedUrls) {

58+

const doc = docsByUrl.get(url)

59+

if (doc) {

60+

orderedDocs.push(doc)

61+

docsByUrl.delete(url)

62+

}

63+

}

64+65+

return orderedDocs.map(({ name, ...rest }) => rest)

66+

}

67+68+

// Generate nav.yml from the filesystem

69+

const generateNav = async (contentPath, navPath) => {

70+

const docsCommandsPath = join(contentPath, 'commands')

71+72+

// Read all command files

73+

const commandFiles = await dirExists(docsCommandsPath) ? await fs.readdir(docsCommandsPath) : []

74+

const commandDocs = commandFiles.filter(f => f.endsWith(DOC_EXT))

75+76+

// Parse each command file to get title and description

77+

const allCommands = await Promise.all(

78+

commandDocs.map(async (file) => {

79+

const content = await fs.readFile(join(docsCommandsPath, file), 'utf-8')

80+

const { attributes } = parseFrontMatter(content)

81+

const name = basename(file, DOC_EXT)

82+

const title = (attributes.title || name).replace(/^npm-/, 'npm ')

83+84+

return {

85+

title,

86+

url: `/commands/${name}`,

87+

description: attributes.description || '',

88+

name,

89+

}

90+

})

91+

)

92+93+

// Sort commands: npm first, then alphabetically, npx last

94+

const npm = allCommands.find(c => c.name === 'npm')

95+

const npx = allCommands.find(c => c.name === 'npx')

96+

const others = allCommands

97+

.filter(c => c.name !== 'npm' && c.name !== 'npx')

98+

.sort((a, b) => a.name.localeCompare(b.name))

99+100+

// Remove the name field

101+

const commands = [npm, ...others, npx].filter(Boolean).map(({ name, ...rest }) => rest)

102+103+

// Hardcoded order for configuring-npm section (only urls - title/description come from frontmatter)

104+

const configuringNpmOrder = [

105+

'/configuring-npm/install',

106+

'/configuring-npm/folders',

107+

'/configuring-npm/npmrc',

108+

'/configuring-npm/npm-shrinkwrap-json',

109+

'/configuring-npm/package-json',

110+

'/configuring-npm/package-lock-json',

111+

]

112+113+

// Hardcoded order for using-npm section (only urls - title/description come from frontmatter)

114+

const usingNpmOrder = [

115+

'/using-npm/registry',

116+

'/using-npm/package-spec',

117+

'/using-npm/config',

118+

'/using-npm/logging',

119+

'/using-npm/scope',

120+

'/using-npm/scripts',

121+

'/using-npm/workspaces',

122+

'/using-npm/orgs',

123+

'/using-npm/dependency-selectors',

124+

'/using-npm/developers',

125+

'/using-npm/removal',

126+

]

127+128+

// Read actual docs from configuring-npm and using-npm directories

129+

const configuringNpmDocs = await readSectionDocs(contentPath, 'configuring-npm', configuringNpmOrder)

130+

const usingNpmDocs = await readSectionDocs(contentPath, 'using-npm', usingNpmOrder)

131+132+

// Build the navigation structure - only include sections with content

133+

const navData = []

134+135+

if (commands.length > 0) {

136+

navData.push({

137+

title: 'CLI Commands',

138+

shortName: 'Commands',

139+

url: '/commands',

140+

children: commands,

141+

})

142+

}

143+144+

if (configuringNpmDocs.length > 0) {

145+

navData.push({

146+

title: 'Configuring npm',

147+

shortName: 'Configuring',

148+

url: '/configuring-npm',

149+

children: configuringNpmDocs,

150+

})

151+

}

152+153+

if (usingNpmDocs.length > 0) {

154+

navData.push({

155+

title: 'Using npm',

156+

shortName: 'Using',

157+

url: '/using-npm',

158+

children: usingNpmDocs,

159+

})

160+

}

161+162+

const prefix = `# This is the navigation for the documentation pages; it is not used

163+

# directly within the CLI documentation. Instead, it will be used

164+

# for the https://docs.npmjs.com/ site.

165+

`

166+

await fs.writeFile(navPath, `${prefix}\n${yaml.stringify(navData, { indent: 2, indentSeq: false })}`, 'utf-8')

167+

}

168+169+

// Auto-generate doc templates for commands without docs

170+

const autoGenerateMissingDocs = async (contentPath, navPath, commandsPath = null) => {

171+

commandsPath = commandsPath || join(__dirname, '../../lib/commands')

172+

const docsCommandsPath = join(contentPath, 'commands')

173+174+

// Get all commands from commandsPath directory

175+

let commands

176+

try {

177+

const cmdListPath = join(commandsPath, '..', 'utils', 'cmd-list.js')

178+

const cmdList = require(cmdListPath)

179+

commands = cmdList.commands

180+

} catch {

181+

// Fall back to reading command files from commandsPath

182+

const cmdFiles = await fs.readdir(commandsPath)

183+

commands = cmdFiles

184+

.filter(f => f.endsWith('.js'))

185+

.map(f => basename(f, '.js'))

186+

}

187+188+

// Get existing doc files

189+

const existingDocs = await fs.readdir(docsCommandsPath)

190+

const documentedCommands = existingDocs

191+

.filter(f => f.startsWith('npm-') && f.endsWith(DOC_EXT))

192+

.map(f => f.replace('npm-', '').replace(DOC_EXT, ''))

193+194+

// Find commands without docs

195+

const missingDocs = commands.filter(cmd => !documentedCommands.includes(cmd))

196+197+

// Generate docs for missing commands

198+

const newEntries = []

199+

for (const cmd of missingDocs) {

200+

const Command = require(join(commandsPath, `${cmd}.js`))

201+

const description = Command.description || `The ${cmd} command`

202+

const docPath = join(docsCommandsPath, `npm-${cmd}${DOC_EXT}`)

203+204+

const template = `---

205+

title: npm-${cmd}

206+

section: 1

207+

description: ${description}

208+

---

209+210+

### Synopsis

211+212+

<!-- AUTOGENERATED USAGE DESCRIPTIONS -->

213+214+

### Description

215+216+

${description}

217+218+

### Configuration

219+220+

<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->

221+222+

### See Also

223+224+

* [npm help config](/commands/npm-config)

225+

`

226+227+

await fs.writeFile(docPath, template, 'utf-8')

228+229+

// Track new entry for nav update

230+

newEntries.push({

231+

title: `npm ${cmd}`,

232+

url: `/commands/npm-${cmd}`,

233+

description,

234+

})

235+

}

236+237+

// Update nav.yml if there are new entries

238+

if (newEntries.length > 0) {

239+

const navContent = await fs.readFile(navPath, 'utf-8')

240+

const navData = yaml.parse(navContent)

241+242+

// Find CLI Commands section

243+

let commandsSection = navData.find(s => s.title === 'CLI Commands')

244+

if (!commandsSection) {

245+

// Create CLI Commands section if it doesn't exist

246+

commandsSection = {

247+

title: 'CLI Commands',

248+

shortName: 'Commands',

249+

url: '/commands',

250+

children: [],

251+

}

252+

navData.unshift(commandsSection)

253+

}

254+255+

if (!commandsSection.children) {

256+

commandsSection.children = []

257+

}

258+259+

// Add new entries that don't already exist

260+

for (const entry of newEntries) {

261+

const exists = commandsSection.children.some(c => c.url === entry.url)

262+

if (!exists) {

263+

commandsSection.children.push(entry)

264+

}

265+

}

266+267+

// Sort children: npm first, then alphabetically, npx last

268+

const npm = commandsSection.children.find(c => c.title === 'npm')

269+

const npx = commandsSection.children.find(c => c.title === 'npx')

270+

const others = commandsSection.children

271+

.filter(c => c.title !== 'npm' && c.title !== 'npx')

272+

.sort((a, b) => a.title.localeCompare(b.title))

273+274+

commandsSection.children = [npm, ...others, npx].filter(Boolean)

275+276+

// Write updated nav

277+

const prefix = `# This is the navigation for the documentation pages; it is not used

278+

# directly within the CLI documentation. Instead, it will be used

279+

# for the https://docs.npmjs.com/ site.

280+

`

281+

await fs.writeFile(navPath, `${prefix}\n${yaml.stringify(navData, { indent: 2, indentSeq: false })}`, 'utf-8')

282+

}

283+

}

284+10285

const mkDirs = async (paths) => {

11286

const uniqDirs = [...new Set(paths.map((p) => dirname(p)))]

12287

return Promise.all(uniqDirs.map((d) => fs.mkdir(d, { recursive: true })))

@@ -28,7 +303,21 @@ const pAll = async (obj) => {

28303

}, {})

29304

}

3030531-

const run = async ({ content, template, nav, man, html, md }) => {

306+

const run = async (opts) => {

307+

const {

308+

content, template, nav, man, html, md,

309+

skipAutoGenerate, skipGenerateNav, commandLoader,

310+

} = opts

311+

// Auto-generate docs for commands without documentation

312+

if (!skipAutoGenerate) {

313+

await autoGenerateMissingDocs(content, nav)

314+

}

315+316+

// Generate nav.yml from filesystem

317+

if (!skipGenerateNav) {

318+

await generateNav(content, nav)

319+

}

320+32321

await rmAll(man, html, md)

33322

const [contentPaths, navFile, options] = await Promise.all([

34323

readDocs(content),

@@ -73,6 +362,7 @@ const run = async ({ content, template, nav, man, html, md }) => {

73362

}) => {

74363

const applyTransforms = makeTransforms({

75364

path: childPath,

365+

commandLoader,

76366

data: {

77367

...data,

78368

github_repo: 'npm/cli',

@@ -86,7 +376,7 @@ const run = async ({ content, template, nav, man, html, md }) => {

86376

const transformedSrc = applyTransforms(body, [

87377

transform.version,

88378

...(fullName.startsWith('commands/')

89-

? [transform.usage, transform.params]

379+

? [transform.usage, transform.definitions]

90380

: []),

91381

...(fullName === 'using-npm/config'

92382

? [transform.shorthands, transform.config]

@@ -145,3 +435,5 @@ const run = async ({ content, template, nav, man, html, md }) => {

145435

}

146436147437

module.exports = run

438+

module.exports.generateNav = generateNav

439+

module.exports.autoGenerateMissingDocs = autoGenerateMissingDocs

Read the original on github.com ↗