Skip to content

Commit cf56a1e

Browse files
authored
feat: npm trust, per-command config
feat: npm trust also implements per-command config
1 parent aae84bf commit cf56a1e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+7760
-344
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/DEPENDENCIES.md text eol=lf
1919
/DEPENDENCIES.json text eol=lf
2020
/AUTHORS text eol=lf
21+
/docs/lib/content/nav.yml text eol=lf
2122

2223
# fixture tarballs should be treated as binary
2324
/workspaces/*/test/fixtures/**/*.tgz binary

docs/lib/build.js

Lines changed: 294 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,281 @@ const parseFrontMatter = require('front-matter')
77
const checkNav = require('./check-nav.js')
88
const { 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(/^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
}
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

147437
module.exports = run
438+
module.exports.generateNav = generateNav
439+
module.exports.autoGenerateMissingDocs = autoGenerateMissingDocs
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: npm-get
3+
section: 1
4+
description: Get a value from the npm configuration
5+
---
6+
7+
### Synopsis
8+
9+
<!-- AUTOGENERATED USAGE DESCRIPTIONS -->
10+
11+
### Description
12+
13+
Get a value from the npm configuration
14+
15+
### Configuration
16+
17+
<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->
18+
19+
### See Also
20+
21+
* [npm help config](/commands/npm-config)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: npm-ll
3+
section: 1
4+
description: List installed packages
5+
---
6+
7+
### Synopsis
8+
9+
<!-- AUTOGENERATED USAGE DESCRIPTIONS -->
10+
11+
### Description
12+
13+
List installed packages
14+
15+
### Configuration
16+
17+
<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->
18+
19+
### See Also
20+
21+
* [npm help config](/commands/npm-config)

0 commit comments

Comments
 (0)