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