PR-URL: https://github.com/nodejs/node/pull/63602 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
312 lines
9.5 KiB
JavaScript
312 lines
9.5 KiB
JavaScript
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
|
const { join, basename, resolve } = require('path')
|
|
const transformHTML = require('./transform-html.js')
|
|
const { version } = require('../../lib/npm.js')
|
|
const { aliases } = require('../../lib/utils/cmd-list')
|
|
const { shorthands, definitions } = require('@npmcli/config/lib/definitions')
|
|
|
|
const DOC_EXT = '.md'
|
|
|
|
const TAGS = {
|
|
CONFIG: '<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->',
|
|
USAGE: '<!-- AUTOGENERATED USAGE DESCRIPTIONS -->',
|
|
SHORTHANDS: '<!-- AUTOGENERATED CONFIG SHORTHANDS -->',
|
|
}
|
|
|
|
const assertPlaceholder = (src, path, placeholder) => {
|
|
if (!src.includes(placeholder)) {
|
|
throw new Error(
|
|
`Cannot replace ${placeholder} in ${path} due to missing placeholder`
|
|
)
|
|
}
|
|
return placeholder
|
|
}
|
|
|
|
// Default command loader - loads commands from lib/commands
|
|
const defaultCommandLoader = (name) => {
|
|
return require(`../../lib/commands/${name}`)
|
|
}
|
|
|
|
// Load a command using the provided loader or default
|
|
const getCommand = (name, commandLoader = defaultCommandLoader) => {
|
|
return commandLoader(name)
|
|
}
|
|
|
|
// Resolve definitions for a command - use definitions if present, otherwise build from params
|
|
const resolveDefinitions = (command) => {
|
|
// If command has definitions, use them directly (ignore params)
|
|
if (command.definitions && Object.keys(command.definitions).length > 0) {
|
|
return command.definitions
|
|
}
|
|
|
|
// Otherwise build from params using global definitions
|
|
if (command.params) {
|
|
const resolved = {}
|
|
for (const param of command.params) {
|
|
if (definitions[param]) {
|
|
resolved[param] = definitions[param]
|
|
}
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
return {}
|
|
}
|
|
|
|
const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader) => {
|
|
// Grab the command name from the *.md filename
|
|
// NOTE: We cannot use the name property command file because in the case of
|
|
// `npx` the file being used is `lib/commands/exec.js`
|
|
const name = basename(docFile, docExt).replace('npm-', '')
|
|
|
|
if (name === 'npm') {
|
|
return {
|
|
name,
|
|
definitions: [],
|
|
usage: 'npm',
|
|
}
|
|
}
|
|
|
|
// special case for `npx`:
|
|
// `npx` is not technically a command in and of itself,
|
|
// so it just needs the usage of npm exec
|
|
const srcName = name === 'npx' ? 'exec' : name
|
|
const command = getCommand(srcName, commandLoader)
|
|
const { usage = [''], workspaces } = command
|
|
const usagePrefix = name === 'npx' ? 'npx' : `npm ${name}`
|
|
|
|
// Resolve definitions - handles exclusive params expansion
|
|
const commandDefs = resolveDefinitions(command)
|
|
const resolvedDefs = {}
|
|
for (const [key, def] of Object.entries(commandDefs)) {
|
|
resolvedDefs[key] = def
|
|
// Handle exclusive params
|
|
if (def.exclusive) {
|
|
for (const e of def.exclusive) {
|
|
if (!resolvedDefs[e] && definitions[e]) {
|
|
resolvedDefs[e] = definitions[e]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
name,
|
|
workspaces,
|
|
definitions: name === 'npx' ? {} : resolvedDefs,
|
|
usage: usage?.map(u => `${usagePrefix} ${u}`.trim()).join('\n'),
|
|
}
|
|
}
|
|
|
|
const replaceVersion = (src) => src.replace(/@VERSION@/g, version)
|
|
|
|
const replaceUsage = (src, { path, commandLoader }) => {
|
|
const replacer = assertPlaceholder(src, path, TAGS.USAGE)
|
|
const { usage, name, workspaces } = getCommandByDoc(path, DOC_EXT, commandLoader)
|
|
|
|
const synopsis = []
|
|
|
|
if (usage) {
|
|
synopsis.push('```bash', usage)
|
|
|
|
const cmdAliases = Object.keys(aliases).reduce((p, c) => {
|
|
if (aliases[c] === name) {
|
|
p.push(c)
|
|
}
|
|
return p
|
|
}, [])
|
|
|
|
if (cmdAliases.length === 1) {
|
|
synopsis.push('', `alias: ${cmdAliases[0]}`)
|
|
} else if (cmdAliases.length > 1) {
|
|
synopsis.push('', `aliases: ${cmdAliases.join(', ')}`)
|
|
}
|
|
|
|
synopsis.push('```')
|
|
}
|
|
|
|
if (!workspaces) {
|
|
if (synopsis.length) {
|
|
synopsis.push('')
|
|
}
|
|
synopsis.push('Note: This command is unaware of workspaces.')
|
|
}
|
|
|
|
return src.replace(replacer, synopsis.join('\n'))
|
|
}
|
|
|
|
// Helper to generate a markdown table from definitions
|
|
const generateFlagsTable = (definitionPool) => {
|
|
const rows = Object.keys(definitionPool).map((n) => {
|
|
const def = definitionPool[n]
|
|
const flags = [`\`--${def.key}\``]
|
|
if (def.alias) {
|
|
flags.push(...def.alias.map(a => `\`--${a}\``))
|
|
}
|
|
if (def.short) {
|
|
flags.push(`\`-${def.short}\``)
|
|
}
|
|
const flagsStr = flags.join(', ')
|
|
let defaultVal = def.defaultDescription
|
|
if (!defaultVal) {
|
|
defaultVal = String(def.default)
|
|
}
|
|
defaultVal = defaultVal.replace(/\n/g, ' ').trim()
|
|
let typeVal = def.typeDescription || String(def.type)
|
|
if (def.required) {
|
|
typeVal = `${typeVal} (required)`
|
|
}
|
|
typeVal = typeVal.replace(/\n/g, ' ').trim()
|
|
const desc = (def.description || '').replace(/\n/g, ' ').trim()
|
|
return `| ${flagsStr} | ${defaultVal} | ${typeVal} | ${desc} |`
|
|
})
|
|
|
|
return [
|
|
'| Flag | Default | Type | Description |',
|
|
'| --- | --- | --- | --- |',
|
|
...rows,
|
|
].join('\n')
|
|
}
|
|
|
|
const replaceDefinitions = (src, { path, commandLoader }) => {
|
|
const { definitions: commandDefs, name } = getCommandByDoc(path, DOC_EXT, commandLoader)
|
|
|
|
let subcommands = {}
|
|
try {
|
|
const command = getCommand(name, commandLoader)
|
|
subcommands = command.subcommands || {}
|
|
} catch {
|
|
// Command doesn't exist
|
|
}
|
|
|
|
// If no definitions and no subcommands, nothing to replace
|
|
if (Object.keys(commandDefs).length === 0 && Object.keys(subcommands).length === 0) {
|
|
return src
|
|
}
|
|
|
|
// Assert placeholder is present
|
|
const replacer = assertPlaceholder(src, path, TAGS.CONFIG)
|
|
|
|
// If command has subcommands, generate sections for each subcommand
|
|
if (Object.keys(subcommands).length > 0) {
|
|
const subcommandSections = Object.entries(subcommands).map(([subName, SubCommand]) => {
|
|
const subUsage = SubCommand.usage || []
|
|
const subDefs = resolveDefinitions(SubCommand)
|
|
|
|
const parts = [`### \`npm ${name} ${subName}\``, '']
|
|
|
|
if (SubCommand.description) {
|
|
parts.push(SubCommand.description, '')
|
|
}
|
|
|
|
// Add usage/synopsis
|
|
if (subUsage.length > 0) {
|
|
parts.push('#### Synopsis', '', '```bash')
|
|
subUsage.forEach(u => {
|
|
parts.push(`npm ${name} ${subName} ${u}`.trim())
|
|
})
|
|
parts.push('```', '')
|
|
}
|
|
|
|
// Add flags section if definitions exist
|
|
if (Object.keys(subDefs).length > 0) {
|
|
parts.push('#### Flags', '')
|
|
parts.push(generateFlagsTable(subDefs), '')
|
|
}
|
|
|
|
return parts.join('\n')
|
|
})
|
|
|
|
return src.replace(replacer, subcommandSections.join('\n'))
|
|
}
|
|
|
|
// For commands without subcommands - commandDefs must be non-empty here
|
|
// (we would have returned early at line 175 if both were empty)
|
|
const paramDescriptions = Object.values(commandDefs)
|
|
.map(def => def.describe())
|
|
|
|
return src.replace(replacer, paramDescriptions.join('\n\n'))
|
|
}
|
|
|
|
const replaceConfig = (src, { path }) => {
|
|
const replacer = assertPlaceholder(src, path, TAGS.CONFIG)
|
|
|
|
// sort not-deprecated ones to the top
|
|
/* istanbul ignore next - typically already sorted in the definitions file,
|
|
* but this is here so that our help doc will stay consistent if we decide
|
|
* to move them around. */
|
|
const sort = ([keya, { deprecated: depa }], [keyb, { deprecated: depb }]) => {
|
|
return depa && !depb ? 1
|
|
: !depa && depb ? -1
|
|
: localeCompare(keya, keyb)
|
|
}
|
|
|
|
const allConfig = Object.entries(definitions).sort(sort)
|
|
.map(([, def]) => def.describe())
|
|
.join('\n\n')
|
|
|
|
return src.replace(replacer, allConfig)
|
|
}
|
|
|
|
const replaceShorthands = (src, { path }) => {
|
|
const replacer = assertPlaceholder(src, path, TAGS.SHORTHANDS)
|
|
|
|
const sh = Object.entries(shorthands)
|
|
.sort(([shorta, expansiona], [shortb, expansionb]) =>
|
|
// sort by what they're short FOR
|
|
localeCompare(expansiona.join(' '), expansionb.join(' ')) || localeCompare(shorta, shortb)
|
|
)
|
|
.map(([short, expansion]) => {
|
|
// XXX: this is incorrect. we have multicharacter flags like `-iwr` that
|
|
// can only be set with a single dash
|
|
const dash = short.length === 1 ? '-' : '--'
|
|
return `* \`${dash}${short}\`: \`${expansion.join(' ')}\``
|
|
})
|
|
|
|
return src.replace(replacer, sh.join('\n'))
|
|
}
|
|
|
|
const replaceHelpLinks = (src) => {
|
|
// replaces markdown links with equivalent-ish npm help commands
|
|
return src.replace(
|
|
/\[`?([\w\s-]+)`?\]\(\/(?:commands|configuring-npm|using-npm)\/(?:[\w\s-]+)\)/g,
|
|
(_, p1) => {
|
|
const term = p1.replace(/npm\s/g, '').replace(/\s+/g, ' ').trim()
|
|
const help = `npm help ${term.includes(' ') ? `"${term}"` : term}`
|
|
return help
|
|
}
|
|
)
|
|
}
|
|
|
|
const transformMan = (src, { data, unified, remarkParse, remarkMan }) => unified()
|
|
.use(remarkParse)
|
|
.use(remarkMan, { version: `NPM@${version}` })
|
|
.processSync(`# ${data.title}(${data.section}) - ${data.description}\n\n${src}`)
|
|
.toString()
|
|
|
|
const manPath = (name, { data }) => join(`man${data.section}`, `${name}.${data.section}`)
|
|
|
|
const transformMd = (src, { frontmatter }) => ['---', frontmatter, '---', '', src].join('\n')
|
|
|
|
module.exports = {
|
|
DOC_EXT,
|
|
TAGS,
|
|
paths: {
|
|
content: resolve(__dirname, 'content'),
|
|
nav: resolve(__dirname, 'content', 'nav.yml'),
|
|
template: resolve(__dirname, 'template.html'),
|
|
man: resolve(__dirname, '..', '..', 'man'),
|
|
html: resolve(__dirname, '..', 'output'),
|
|
md: resolve(__dirname, '..', 'content'),
|
|
},
|
|
usage: replaceUsage,
|
|
definitions: replaceDefinitions,
|
|
config: replaceConfig,
|
|
shorthands: replaceShorthands,
|
|
version: replaceVersion,
|
|
helpLinks: replaceHelpLinks,
|
|
man: transformMan,
|
|
manPath: manPath,
|
|
md: transformMd,
|
|
html: transformHTML,
|
|
}
|