Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
index.js311 linesDownload Raw Back to lib
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
25// Default command loader - loads commands from lib/commands
26const defaultCommandLoader = (name) => {
27  return require(`../../lib/commands/${name}`)
28}
29
30// Load a command using the provided loader or default
31const getCommand = (name, commandLoader = defaultCommandLoader) => {
32  return commandLoader(name)
33}
34
35// Resolve definitions for a command - use definitions if present, otherwise build from params
36const resolveDefinitions = (command) => {
37  // If command has definitions, use them directly (ignore params)
38  if (command.definitions && Object.keys(command.definitions).length > 0) {
39    return command.definitions
40  }
41
42  // Otherwise build from params using global definitions
43  if (command.params) {
44    const resolved = {}
45    for (const param of command.params) {
46      if (definitions[param]) {
47        resolved[param] = definitions[param]
48      }
49    }
50    return resolved
51  }
52
53  return {}
54}
55
56const getCommandByDoc = (docFile, docExt, commandLoader = defaultCommandLoader) => {
57  // Grab the command name from the *.md filename
58  // NOTE: We cannot use the name property command file because in the case of
59  // `npx` the file being used is `lib/commands/exec.js`
60  const name = basename(docFile, docExt).replace('npm-', '')
61
62  if (name === 'npm') {
63    return {
64      name,
65      definitions: [],
66      usage: 'npm',
67    }
68  }
69
70  // special case for `npx`:
71  // `npx` is not technically a command in and of itself,
72  // so it just needs the usage of npm exec
73  const srcName = name === 'npx' ? 'exec' : name
74  const command = getCommand(srcName, commandLoader)
75  const { usage = [''], workspaces } = command
76  const usagePrefix = name === 'npx' ? 'npx' : `npm ${name}`
77
78  // Resolve definitions - handles exclusive params expansion
79  const commandDefs = resolveDefinitions(command)
80  const resolvedDefs = {}
81  for (const [key, def] of Object.entries(commandDefs)) {
82    resolvedDefs[key] = def
83    // Handle exclusive params
84    if (def.exclusive) {
85      for (const e of def.exclusive) {
86        if (!resolvedDefs[e] && definitions[e]) {
87          resolvedDefs[e] = definitions[e]
88        }
89      }
90    }
91  }
92
93  return {
94    name,
95    workspaces,
96    definitions: name === 'npx' ? {} : resolvedDefs,
97    usage: usage?.map(u => `${usagePrefix} ${u}`.trim()).join('\n'),
98  }
99}
100
101const replaceVersion = (src) => src.replace(/@VERSION@/g, version)
102
103const replaceUsage = (src, { path, commandLoader }) => {
104  const replacer = assertPlaceholder(src, path, TAGS.USAGE)
105  const { usage, name, workspaces } = getCommandByDoc(path, DOC_EXT, commandLoader)
106
107  const synopsis = []
108
109  if (usage) {
110    synopsis.push('```bash', usage)
111
112    const cmdAliases = Object.keys(aliases).reduce((p, c) => {
113      if (aliases[c] === name) {
114        p.push(c)
115      }
116      return p
117    }, [])
118
119    if (cmdAliases.length === 1) {
120      synopsis.push('', `alias: ${cmdAliases[0]}`)
121    } else if (cmdAliases.length > 1) {
122      synopsis.push('', `aliases: ${cmdAliases.join(', ')}`)
123    }
124
125    synopsis.push('```')
126  }
127
128  if (!workspaces) {
129    if (synopsis.length) {
130      synopsis.push('')
131    }
132    synopsis.push('Note: This command is unaware of workspaces.')
133  }
134
135  return src.replace(replacer, synopsis.join('\n'))
136}
137
138// Helper to generate a markdown table from definitions
139const generateFlagsTable = (definitionPool) => {
140  const rows = Object.keys(definitionPool).map((n) => {
141    const def = definitionPool[n]
142    const flags = [`\`--${def.key}\``]
143    if (def.alias) {
144      flags.push(...def.alias.map(a => `\`--${a}\``))
145    }
146    if (def.short) {
147      flags.push(`\`-${def.short}\``)
148    }
149    const flagsStr = flags.join(', ')
150    let defaultVal = def.defaultDescription
151    if (!defaultVal) {
152      defaultVal = String(def.default)
153    }
154    let typeVal = def.typeDescription || String(def.type)
155    if (def.required) {
156      typeVal = `${typeVal} (required)`
157    }
158    const desc = (def.description || '').replace(/\n/g, ' ').trim()
159    return `| ${flagsStr} | ${defaultVal} | ${typeVal} | ${desc} |`
160  })
161
162  return [
163    '| Flag | Default | Type | Description |',
164    '| --- | --- | --- | --- |',
165    ...rows,
166  ].join('\n')
167}
168
169const replaceDefinitions = (src, { path, commandLoader }) => {
170  const { definitions: commandDefs, name } = getCommandByDoc(path, DOC_EXT, commandLoader)
171
172  let subcommands = {}
173  try {
174    const command = getCommand(name, commandLoader)
175    subcommands = command.subcommands || {}
176  } catch {
177    // Command doesn't exist
178  }
179
180  // If no definitions and no subcommands, nothing to replace
181  if (Object.keys(commandDefs).length === 0 && Object.keys(subcommands).length === 0) {
182    return src
183  }
184
185  // Assert placeholder is present
186  const replacer = assertPlaceholder(src, path, TAGS.CONFIG)
187
188  // If command has subcommands, generate sections for each subcommand
189  if (Object.keys(subcommands).length > 0) {
190    const subcommandSections = Object.entries(subcommands).map(([subName, SubCommand]) => {
191      const subUsage = SubCommand.usage || []
192      const subDefs = resolveDefinitions(SubCommand)
193
194      const parts = [`### \`npm ${name} ${subName}\``, '']
195
196      if (SubCommand.description) {
197        parts.push(SubCommand.description, '')
198      }
199
200      // Add usage/synopsis
201      if (subUsage.length > 0) {
202        parts.push('#### Synopsis', '', '```bash')
203        subUsage.forEach(u => {
204          parts.push(`npm ${name} ${subName} ${u}`.trim())
205        })
206        parts.push('```', '')
207      }
208
209      // Add flags section if definitions exist
210      if (Object.keys(subDefs).length > 0) {
211        parts.push('#### Flags', '')
212        parts.push(generateFlagsTable(subDefs), '')
213      }
214
215      return parts.join('\n')
216    })
217
218    return src.replace(replacer, subcommandSections.join('\n'))
219  }
220
221  // For commands without subcommands - commandDefs must be non-empty here
222  // (we would have returned early at line 175 if both were empty)
223  const paramDescriptions = Object.values(commandDefs)
224    .map(def => def.describe())
225
226  return src.replace(replacer, paramDescriptions.join('\n\n'))
227}
228
229const replaceConfig = (src, { path }) => {
230  const replacer = assertPlaceholder(src, path, TAGS.CONFIG)
231
232  // sort not-deprecated ones to the top
233  /* istanbul ignore next - typically already sorted in the definitions file,
234   * but this is here so that our help doc will stay consistent if we decide
235   * to move them around. */
236  const sort = ([keya, { deprecated: depa }], [keyb, { deprecated: depb }]) => {
237    return depa && !depb ? 1
238      : !depa && depb ? -1
239      : localeCompare(keya, keyb)
240  }
241
242  const allConfig = Object.entries(definitions).sort(sort)
243    .map(([, def]) => def.describe())
244    .join('\n\n')
245
246  return src.replace(replacer, allConfig)
247}
248
249const replaceShorthands = (src, { path }) => {
250  const replacer = assertPlaceholder(src, path, TAGS.SHORTHANDS)
251
252  const sh = Object.entries(shorthands)
253    .sort(([shorta, expansiona], [shortb, expansionb]) =>
254      // sort by what they're short FOR
255      localeCompare(expansiona.join(' '), expansionb.join(' ')) || localeCompare(shorta, shortb)
256    )
257    .map(([short, expansion]) => {
258      // XXX: this is incorrect. we have multicharacter flags like `-iwr` that
259      // can only be set with a single dash
260      const dash = short.length === 1 ? '-' : '--'
261      return `* \`${dash}${short}\`: \`${expansion.join(' ')}\``
262    })
263
264  return src.replace(replacer, sh.join('\n'))
265}
266
267const replaceHelpLinks = (src) => {
268  // replaces markdown links with equivalent-ish npm help commands
269  return src.replace(
270    /\[`?([\w\s-]+)`?\]\(\/(?:commands|configuring-npm|using-npm)\/(?:[\w\s-]+)\)/g,
271    (_, p1) => {
272      const term = p1.replace(/npm\s/g, '').replace(/\s+/g, ' ').trim()
273      const help = `npm help ${term.includes(' ') ? `"${term}"` : term}`
274      return help
275    }
276  )
277}
278
279const transformMan = (src, { data, unified, remarkParse, remarkMan }) => unified()
280  .use(remarkParse)
281  .use(remarkMan, { version: `NPM@${version}` })
282  .processSync(`# ${data.title}(${data.section}) - ${data.description}\n\n${src}`)
283  .toString()
284
285const manPath = (name, { data }) => join(`man${data.section}`, `${name}.${data.section}`)
286
287const transformMd = (src, { frontmatter }) => ['---', frontmatter, '---', '', src].join('\n')
288
289module.exports = {
290  DOC_EXT,
291  TAGS,
292  paths: {
293    content: resolve(__dirname, 'content'),
294    nav: resolve(__dirname, 'content', 'nav.yml'),
295    template: resolve(__dirname, 'template.html'),
296    man: resolve(__dirname, '..', '..', 'man'),
297    html: resolve(__dirname, '..', 'output'),
298    md: resolve(__dirname, '..', 'content'),
299  },
300  usage: replaceUsage,
301  definitions: replaceDefinitions,
302  config: replaceConfig,
303  shorthands: replaceShorthands,
304  version: replaceVersion,
305  helpLinks: replaceHelpLinks,
306  man: transformMan,
307  manPath: manPath,
308  md: transformMd,
309  html: transformHTML,
310}
311 
codekingpro/portable-devtools · Team Ai