| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- 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)
- }
- let typeVal = def.typeDescription || String(def.type)
- if (def.required) {
- typeVal = `${typeVal} (required)`
- }
- 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,
- }
|