289 lines
7.2 KiB
TypeScript
289 lines
7.2 KiB
TypeScript
import { execSync } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { kebabCase, startCase } from 'lodash-es'
|
|
|
|
import { commandDefines, CommandSchema, OptionSchema } from './commands.js'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const defFolder = path.join(__filename, '../../src/def')
|
|
const docsTargetFolder = path.join(__filename, '../../docs')
|
|
|
|
function PascalCase(str: string) {
|
|
return startCase(str).replace(/\s/g, '')
|
|
}
|
|
|
|
/**
|
|
* convert command definition to command options interface
|
|
*/
|
|
function generateOptionsDef(command: CommandSchema) {
|
|
const optionsName = `${PascalCase(command.name)}Options`
|
|
|
|
const optLines: string[] = []
|
|
|
|
optLines.push('/**')
|
|
optLines.push(` * ${command.description}`)
|
|
optLines.push(' */')
|
|
optLines.push(`export interface ${optionsName} {`)
|
|
command.args.forEach((arg) => {
|
|
optLines.push(' /**')
|
|
optLines.push(` * ${arg.description}`)
|
|
optLines.push(' */')
|
|
optLines.push(` ${arg.name}${arg.required ? '' : '?'}: ${arg.type}`)
|
|
})
|
|
|
|
command.options.forEach((opt) => {
|
|
optLines.push(' /**')
|
|
optLines.push(` * ${opt.description}`)
|
|
if (typeof opt.default !== 'undefined') {
|
|
optLines.push(' *')
|
|
optLines.push(` * @default ${opt.default}`)
|
|
}
|
|
optLines.push(' */')
|
|
optLines.push(` ${opt.name}${opt.required ? '' : '?'}: ${opt.type}`)
|
|
})
|
|
|
|
optLines.push('}\n')
|
|
|
|
if (command.options.some((opt) => typeof opt.default !== 'undefined')) {
|
|
optLines.push(
|
|
`export function applyDefault${optionsName}(options: ${optionsName}) {`,
|
|
)
|
|
optLines.push(` return {`)
|
|
command.options.forEach((opt) => {
|
|
if (typeof opt.default !== 'undefined') {
|
|
optLines.push(` ${opt.name}: ${opt.default},`)
|
|
}
|
|
})
|
|
optLines.push(' ...options,')
|
|
optLines.push(' }')
|
|
optLines.push('}\n')
|
|
}
|
|
|
|
return optLines.join('\n')
|
|
}
|
|
|
|
function getOptionDescriptor(opt: OptionSchema) {
|
|
let desc = `--${opt.long ?? kebabCase(opt.name)}`
|
|
if (opt.alias) {
|
|
desc += `,${opt.alias.map((alias) => `--${alias}`).join(',')}`
|
|
}
|
|
if (opt.short) {
|
|
desc += `,${opt.short.map((short) => `-${short}`).join(',')}`
|
|
}
|
|
|
|
return desc
|
|
}
|
|
|
|
function generateCommandDef(command: CommandSchema) {
|
|
const commandPath = kebabCase(command.name)
|
|
const avoidList = ['path', 'name']
|
|
|
|
const avoidName = (name: string) => {
|
|
return avoidList.includes(name) ? '$$' + name : name
|
|
}
|
|
|
|
const prepare: string[] = []
|
|
const cmdLines: string[] = []
|
|
|
|
let paths = `[['${commandPath}']]`
|
|
|
|
if (command.alias) {
|
|
command.alias.unshift(commandPath)
|
|
paths = `[${command.alias.map((alias) => `['${alias}']`).join(', ')}]`
|
|
}
|
|
|
|
cmdLines.push(`
|
|
export abstract class Base${PascalCase(command.name)}Command extends Command {
|
|
static paths = ${paths}
|
|
|
|
static usage = Command.Usage({
|
|
description: '${command.description}',
|
|
})\n`)
|
|
|
|
command.args.forEach((arg) => {
|
|
cmdLines.push(
|
|
` ${avoidName(arg.name)} = Option.String({ required: ${
|
|
arg.required ?? false
|
|
} })`,
|
|
)
|
|
})
|
|
|
|
cmdLines.push('')
|
|
|
|
command.options.forEach((opt) => {
|
|
const optName = avoidName(opt.name)
|
|
let optionType = ''
|
|
|
|
switch (opt.type) {
|
|
case 'number':
|
|
optionType = 'String'
|
|
prepare.push("import * as typanion from 'typanion'")
|
|
break
|
|
case 'boolean':
|
|
optionType = 'Boolean'
|
|
break
|
|
case 'string[]':
|
|
optionType = 'Array'
|
|
break
|
|
case 'string':
|
|
default:
|
|
optionType = 'String'
|
|
}
|
|
|
|
const optionDesc = getOptionDescriptor(opt)
|
|
|
|
if (opt.required) {
|
|
cmdLines.push(` ${optName} = Option.${optionType}('${optionDesc}', {`)
|
|
cmdLines.push(' required: true,')
|
|
} else if (typeof opt.default !== 'undefined') {
|
|
const defaultValue =
|
|
typeof opt.default === 'number'
|
|
? `'${opt.default.toString()}'`
|
|
: opt.default
|
|
cmdLines.push(` ${optName} = Option.${optionType}(`)
|
|
cmdLines.push(` '${optionDesc}',`)
|
|
cmdLines.push(` ${defaultValue},`)
|
|
cmdLines.push(` {`)
|
|
} else {
|
|
cmdLines.push(
|
|
` ${optName}?: ${opt.type} = Option.${optionType}('${optionDesc}', {`,
|
|
)
|
|
}
|
|
|
|
if (opt.type === 'number') {
|
|
cmdLines.push(' validator: typanion.isNumber(),')
|
|
}
|
|
|
|
cmdLines.push(` description: '${opt.description}'`)
|
|
cmdLines.push(' })\n')
|
|
})
|
|
|
|
cmdLines.push(` getOptions() {`)
|
|
cmdLines.push(` return {`)
|
|
command.args
|
|
.map(({ name }) => name)
|
|
.concat(command.options.map(({ name }) => name))
|
|
.forEach((name) => {
|
|
cmdLines.push(` ${name}: this.${avoidName(name)},`)
|
|
})
|
|
cmdLines.push(' }')
|
|
cmdLines.push(' }')
|
|
|
|
cmdLines.push('}\n')
|
|
|
|
return prepare.join('\n') + '\n' + cmdLines.join('\n')
|
|
}
|
|
|
|
function generateDocs(command: CommandSchema, targetFolder: string): string {
|
|
const docsFileName = kebabCase(command.name)
|
|
const docsFile = path.join(targetFolder, `${docsFileName}.md`)
|
|
|
|
const options: string[] = []
|
|
|
|
command.args.forEach((arg) => {
|
|
options.push(
|
|
[
|
|
'',
|
|
arg.name,
|
|
`<${kebabCase(arg.name)}>`,
|
|
arg.required ? 'true' : 'false',
|
|
arg.type,
|
|
'',
|
|
arg.description,
|
|
'',
|
|
].join('|'),
|
|
)
|
|
})
|
|
|
|
command.options.forEach((opt) => {
|
|
options.push(
|
|
[
|
|
'',
|
|
opt.name,
|
|
getOptionDescriptor(opt),
|
|
opt.type.replace(/\|/g, '\\|'),
|
|
opt.required ? 'true' : 'false',
|
|
opt.default ?? '',
|
|
opt.description,
|
|
'',
|
|
].join('|'),
|
|
)
|
|
})
|
|
|
|
const content = `# ${startCase(command.name)}
|
|
|
|
> This file is generated by cli/codegen. Do not edit this file manually.
|
|
|
|
${command.description}
|
|
|
|
## Usage
|
|
|
|
\`\`\`sh
|
|
# CLI
|
|
napi ${kebabCase(command.name)}${command.args.reduce(
|
|
(h, arg) => h + ` <${arg.name}>`,
|
|
'',
|
|
)} [--options]
|
|
\`\`\`
|
|
|
|
\`\`\`typescript
|
|
// Programatically
|
|
import { NapiCli } from '@napi-rs/cli'
|
|
|
|
new NapiCli().${command.name}({
|
|
// options
|
|
})
|
|
\`\`\`
|
|
|
|
## Options
|
|
|
|
| Options | CLI Options | type | required | default | description |
|
|
| ------- | ----------- | ---- | -------- | ------- | ----------- |
|
|
| | --help,-h | | | | get help |
|
|
${options.join('\n')}
|
|
`
|
|
|
|
// make sure the target folder exists
|
|
fs.mkdirSync(targetFolder, { recursive: true })
|
|
// write file
|
|
fs.writeFileSync(docsFile, content)
|
|
|
|
return docsFile
|
|
}
|
|
|
|
function generateDef(cmd: CommandSchema, folder: string): string {
|
|
const defFileName = kebabCase(cmd.name)
|
|
const defFilePath = path.join(folder, `${defFileName}.ts`)
|
|
|
|
const def = `// This file is generated by codegen/index.ts
|
|
// Do not edit this file manually
|
|
import { Command, Option } from 'clipanion'
|
|
${generateCommandDef(cmd)}
|
|
|
|
${generateOptionsDef(cmd)}
|
|
`
|
|
|
|
// make sure the target folder exists
|
|
fs.mkdirSync(folder, { recursive: true })
|
|
// write file
|
|
fs.writeFileSync(defFilePath, def)
|
|
|
|
return defFilePath
|
|
}
|
|
|
|
function codegen() {
|
|
const outputs: string[] = []
|
|
commandDefines.forEach((command) => {
|
|
outputs.push(generateDef(command, defFolder))
|
|
outputs.push(generateDocs(command, docsTargetFolder))
|
|
})
|
|
|
|
outputs.forEach((output) => {
|
|
execSync(`yarn prettier -w ${output}`)
|
|
})
|
|
}
|
|
|
|
codegen()
|