UNPKG

npm

Version:

a package manager for JavaScript

304 lines (255 loc) 9.49 kB
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 = ['```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) { 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, }