@@ -7,6 +7,281 @@ const parseFrontMatter = require('front-matter')
77const checkNav = require ( './check-nav.js' )
88const { DOC_EXT , ...transform } = require ( './index.js' )
99
10+ // 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 ( / ^ n p m - / , '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+
10285const 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}
30305
31- 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}
146436
147437module . exports = run
438+ module . exports . generateNav = generateNav
439+ module . exports . autoGenerateMissingDocs = autoGenerateMissingDocs
0 commit comments