1const localeCompare = require('@isaacs/string-locale-compare')('en') 2const { join, basename, resolve } = require('path') 3const transformHTML = require('./transform-html.js') 4const { version } = require('../../lib/npm.js') 5const { aliases } = require('../../lib/utils/cmd-list') 6const { shorthands, definitions } = require('@npmcli/config/lib/definitions') 7 8const DOC_EXT = '.md' 9 10const TAGS = { 11 CONFIG: '<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->', 12 USAGE: '<!-- AUTOGENERATED USAGE DESCRIPTIONS -->', 13 SHORTHANDS: '<!-- AUTOGENERATED CONFIG SHORTHANDS -->', 14} 15 16const assertPlaceholder = (src, path, placeholder) => { 17 if (!src.includes(placeholder)) { 18 throw new Error( 19 `Cannot replace ${placeholder} in ${path} due to missing placeholder` 20 ) 21 } 22 return placeholder 23} 24 25const getCommandByDoc = (docFile, docExt) => { 26 // Grab the command name from the *.md filename 27 // NOTE: We cannot use the name property command file because in the case of 28 // `npx` the file being used is `lib/commands/exec.js` 29 const name = basename(docFile, docExt).replace('npm-', '') 30 31 if (name === 'npm') { 32 return { 33 name, 34 params: null, 35 usage: 'npm', 36 } 37 } 38 39 // special case for `npx`: 40 // `npx` is not technically a command in and of itself, 41 // so it just needs the usage of npm exex 42 const srcName = name === 'npx' ? 'exec' : name 43 const { params, usage = [''], workspaces } = require(`../../lib/commands/${srcName}`) 44 const usagePrefix = name === 'npx' ? 'npx' : `npm ${name}` 45 if (params) { 46 for (const param of params) { 47 if (definitions[param].exclusive) { 48 for (const e of definitions[param].exclusive) { 49 if (!params.includes(e)) { 50 params.splice(params.indexOf(param) + 1, 0, e) 51 } 52 } 53 } 54 } 55 } 56 57 return { 58 name, 59 workspaces, 60 params: name === 'npx' ? null : params, 61 usage: usage.map(u => `${usagePrefix} ${u}`.trim()).join('\n'), 62 } 63} 64 65const replaceVersion = (src) => src.replace(/@VERSION@/g, version) 66 67const replaceUsage = (src, { path }) => { 68 const replacer = assertPlaceholder(src, path, TAGS.USAGE) 69 const { usage, name, workspaces } = getCommandByDoc(path, DOC_EXT) 70 71 const synopsis = ['```bash', usage] 72 73 const cmdAliases = Object.keys(aliases).reduce((p, c) => { 74 if (aliases[c] === name) { 75 p.push(c) 76 } 77 return p 78 }, []) 79 80 if (cmdAliases.length === 1) { 81 synopsis.push('', `alias: ${cmdAliases[0]}`) 82 } else if (cmdAliases.length > 1) { 83 synopsis.push('', `aliases: ${cmdAliases.join(', ')}`) 84 } 85 86 synopsis.push('```') 87 88 if (!workspaces) { 89 synopsis.push('', 'Note: This command is unaware of workspaces.') 90 } 91 92 return src.replace(replacer, synopsis.join('\n')) 93} 94 95const replaceParams = (src, { path }) => { 96 const { params } = getCommandByDoc(path, DOC_EXT) 97 const replacer = params && assertPlaceholder(src, path, TAGS.CONFIG) 98 99 if (!params) { 100 return src 101 } 102 103 const paramsConfig = params.map((n) => definitions[n].describe()) 104 105 return src.replace(replacer, paramsConfig.join('\n\n')) 106} 107 108const replaceConfig = (src, { path }) => { 109 const replacer = assertPlaceholder(src, path, TAGS.CONFIG) 110 111 // sort not-deprecated ones to the top 112 /* istanbul ignore next - typically already sorted in the definitions file, 113 * but this is here so that our help doc will stay consistent if we decide 114 * to move them around. */ 115 const sort = ([keya, { deprecated: depa }], [keyb, { deprecated: depb }]) => { 116 return depa && !depb ? 1 117 : !depa && depb ? -1 118 : localeCompare(keya, keyb) 119 } 120 121 const allConfig = Object.entries(definitions).sort(sort) 122 .map(([_, def]) => def.describe()) 123 .join('\n\n') 124 125 return src.replace(replacer, allConfig) 126} 127 128const replaceShorthands = (src, { path }) => { 129 const replacer = assertPlaceholder(src, path, TAGS.SHORTHANDS) 130 131 const sh = Object.entries(shorthands) 132 .sort(([shorta, expansiona], [shortb, expansionb]) => 133 // sort by what they're short FOR 134 localeCompare(expansiona.join(' '), expansionb.join(' ')) || localeCompare(shorta, shortb) 135 ) 136 .map(([short, expansion]) => { 137 // XXX: this is incorrect. we have multicharacter flags like `-iwr` that 138 // can only be set with a single dash 139 const dash = short.length === 1 ? '-' : '--' 140 return `* \`${dash}${short}\`: \`${expansion.join(' ')}\`` 141 }) 142 143 return src.replace(replacer, sh.join('\n')) 144} 145 146const replaceHelpLinks = (src) => { 147 // replaces markdown links with equivalent-ish npm help commands 148 return src.replace( 149 /\[`?([\w\s-]+)`?\]\(\/(?:commands|configuring-npm|using-npm)\/(?:[\w\s-]+)\)/g, 150 (_, p1) => { 151 const term = p1.replace(/npm\s/g, '').replace(/\s+/g, ' ').trim() 152 const help = `npm help ${term.includes(' ') ? `"${term}"` : term}` 153 return help 154 } 155 ) 156} 157 158const transformMan = (src, { data, unified, remarkParse, remarkMan }) => unified() 159 .use(remarkParse) 160 .use(remarkMan) 161 .processSync(`# ${data.title}(${data.section}) - ${data.description}\n\n${src}`) 162 .toString() 163 164const manPath = (name, { data }) => join(`man${data.section}`, `${name}.${data.section}`) 165 166const transformMd = (src, { frontmatter }) => ['---', frontmatter, '---', '', src].join('\n') 167 168module.exports = { 169 DOC_EXT, 170 TAGS, 171 paths: { 172 content: resolve(__dirname, 'content'), 173 nav: resolve(__dirname, 'content', 'nav.yml'), 174 template: resolve(__dirname, 'template.html'), 175 man: resolve(__dirname, '..', '..', 'man'), 176 html: resolve(__dirname, '..', 'output'), 177 md: resolve(__dirname, '..', 'content'), 178 }, 179 usage: replaceUsage, 180 params: replaceParams, 181 config: replaceConfig, 182 shorthands: replaceShorthands, 183 version: replaceVersion, 184 helpLinks: replaceHelpLinks, 185 man: transformMan, 186 manPath: manPath, 187 md: transformMd, 188 html: transformHTML, 189} 190