182 lines
5.8 KiB
TypeScript
182 lines
5.8 KiB
TypeScript
import pc from 'picocolors'
|
|
import type { Arg } from '../../utils/args'
|
|
import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer'
|
|
|
|
export function help({
|
|
render = true,
|
|
invalid,
|
|
usage,
|
|
options,
|
|
}: {
|
|
render?: boolean
|
|
invalid?: string
|
|
usage?: string[]
|
|
options?: Arg
|
|
}) {
|
|
// Available terminal width
|
|
let width = process.stdout.columns ?? 80
|
|
let lines: string[] = []
|
|
let writeLine = render ? println : (value = '') => void lines.push(value)
|
|
|
|
// Render header
|
|
writeLine(header())
|
|
|
|
// Render the invalid command
|
|
if (invalid) {
|
|
writeLine()
|
|
writeLine(`${pc.dim('Invalid command:')} ${invalid}`)
|
|
}
|
|
|
|
// Render usage
|
|
if (usage && usage.length > 0) {
|
|
writeLine()
|
|
writeLine(pc.dim('Usage:'))
|
|
for (let [idx, example] of usage.entries()) {
|
|
// Split the usage example into the command and its options. This allows
|
|
// us to wrap the options based on the available width of the terminal.
|
|
let command = example.slice(0, example.indexOf('['))
|
|
let options = example.slice(example.indexOf('['))
|
|
|
|
// Make the options dimmed, to make them stand out less than the command
|
|
// itself.
|
|
options = options.replace(/\[.*?\]/g, (option) => pc.dim(option))
|
|
|
|
// The space between the command and the options.
|
|
let space = 1
|
|
|
|
// Wrap the options based on the available width of the terminal.
|
|
let lines = wordWrap(options, width - UI.indent - command.length - space)
|
|
|
|
// Print an empty line between the usage examples if we need to split due
|
|
// to width constraints. This ensures that the usage examples are visually
|
|
// separated.
|
|
//
|
|
// E.g.: when enough space is available
|
|
//
|
|
// ```
|
|
// Usage:
|
|
// tailwindcss build [--input input.css] [--output output.css] [--watch] [options...]
|
|
// tailwindcss other [--watch] [options...]
|
|
// ```
|
|
//
|
|
// E.g.: when not enough space is available
|
|
//
|
|
// ```
|
|
// Usage:
|
|
// tailwindcss build [--input input.css] [--output output.css]
|
|
// [--watch] [options...]
|
|
//
|
|
// tailwindcss other [--watch] [options...]
|
|
// ```
|
|
if (lines.length > 1 && idx !== 0) {
|
|
writeLine()
|
|
}
|
|
|
|
// Print the usage examples based on available width of the terminal.
|
|
//
|
|
// E.g.: when enough space is available
|
|
//
|
|
// ```
|
|
// Usage:
|
|
// tailwindcss [--input input.css] [--output output.css] [--watch] [options...]
|
|
// ```
|
|
//
|
|
// E.g.: when not enough space is available
|
|
//
|
|
// ```
|
|
// Usage:
|
|
// tailwindcss [--input input.css] [--output output.css]
|
|
// [--watch] [options...]
|
|
// ```
|
|
//
|
|
// > Note how the second line is indented to align with the first line.
|
|
writeLine(indent(`${command}${lines.shift()}`))
|
|
for (let line of lines) {
|
|
writeLine(indent(line, command.length))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render options
|
|
if (options) {
|
|
// Track the max alias length, this is used to indent the options that don't
|
|
// have an alias such that everything is aligned properly.
|
|
let maxAliasLength = 0
|
|
for (let { alias } of Object.values(options)) {
|
|
if (alias) {
|
|
maxAliasLength = Math.max(maxAliasLength, alias.length)
|
|
}
|
|
}
|
|
|
|
// The option strings, which are the combination of the `alias` and the
|
|
// `flag`, with the correct spacing.
|
|
let optionStrings: string[] = []
|
|
|
|
// Track the max option length, which is the longest combination of an
|
|
// `alias` followed by `, ` and followed by the `flag`.
|
|
let maxOptionLength = 0
|
|
|
|
for (let [flag, { alias, values }] of Object.entries(options)) {
|
|
if (values?.length) {
|
|
flag += `[=${values.join(', ')}]`
|
|
}
|
|
|
|
// The option string, which is the combination of the alias and the flag
|
|
// but already properly indented based on the other aliases to ensure
|
|
// everything is aligned properly.
|
|
let option = [
|
|
alias ? `${alias.padStart(maxAliasLength)}` : alias,
|
|
alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag,
|
|
]
|
|
.filter(Boolean)
|
|
.join(', ')
|
|
|
|
optionStrings.push(option)
|
|
maxOptionLength = Math.max(maxOptionLength, option.length)
|
|
}
|
|
|
|
writeLine()
|
|
writeLine(pc.dim('Options:'))
|
|
|
|
// The minimum amount of dots between the option and the description.
|
|
let minimumGap = 8
|
|
|
|
for (let { description, default: defaultValue = null } of Object.values(options)) {
|
|
// The option to render
|
|
let option = optionStrings.shift() as string
|
|
|
|
// The amount of dots to show between the option and the description.
|
|
let dotCount = minimumGap + (maxOptionLength - option.length)
|
|
|
|
// To account for the space before and after the dots.
|
|
let spaces = 2
|
|
|
|
// The available width remaining for the description.
|
|
let availableWidth = width - option.length - dotCount - spaces - UI.indent
|
|
|
|
// Wrap the description and the default value (if present), based on the
|
|
// available width.
|
|
let lines = wordWrap(
|
|
defaultValue !== null
|
|
? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}`
|
|
: description,
|
|
availableWidth,
|
|
)
|
|
|
|
// Print the option, the spacer dots and the start of the description.
|
|
writeLine(
|
|
indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`),
|
|
)
|
|
|
|
// Print the remaining lines of the description, indenting them to align
|
|
// with the start of the description.
|
|
for (let line of lines) {
|
|
writeLine(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`))
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!render) {
|
|
return lines.join('\n')
|
|
}
|
|
}
|