UNPKG

bump-cli

Version:

The Bump CLI is used to interact with your API documentation hosted on Bump.sh by using the API of developers.bump.sh

150 lines (149 loc) 6.83 kB
import * as p from '@clack/prompts'; import { ux } from '@oclif/core'; import { CLIError, ExitError } from '@oclif/core/errors'; import chalk from 'chalk'; import debug from 'debug'; import { rename } from 'node:fs'; import { basename, dirname, extname, join, resolve } from 'node:path'; import { confirm as promptConfirm } from '../core/utils/prompts.js'; import { API } from '../definition.js'; import { File } from './utils/file.js'; export class DefinitionDirectory { buildNewFilename; definitions; filenamePattern; humanFilenamePattern; path; constructor(directory, filenamePattern) { this.path = resolve(directory); this.definitions = []; // // Transform basic patterns '*' or '{text}' into a real RegExp this.filenamePattern = new RegExp('^' + filenamePattern.replace('*', '.*?').replace(/{.*?}/, '(?<slug>.+?)') + '$'); this.buildNewFilename = (slug) => filenamePattern.replace('*', '').replace(/{.*?}/, slug); this.humanFilenamePattern = filenamePattern.replace(/{(.*?)}/, `${chalk.inverse('{$1}')}`); } // Function signature type taken from @types/debug // Debugger(formatter: any, ...args: any[]): void; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ d(formatter, ...args) { return debug(`bump-cli:core:interactive`)(formatter, ...args); } definitionsExists() { return this.definitions.length > 0; } async interactiveSelection() { p.intro(`This interactive form will help you rename your API contrat files to follow the expected naming convention.\n${chalk.gray('│ ')}Once finished, the selected files will be deployed to Bump.sh.\n${chalk.gray('│ ')}\n${chalk.gray('│ ')}File naming convention: ${this.humanFilenamePattern}${chalk.dim('.[json|yml|yaml]')}\n`); let fileOptions = File.listInvalidConventionFiles(this.path, this.filenamePattern); if (fileOptions.length === 0) { throw new CLIError(`No JSON or YAML files needing a rename were found in ${this.path}.\nAre you sure you need the ${chalk.dim('--interactive')} flag?`); } let shouldContinue = true; while (shouldContinue) { fileOptions = fileOptions.filter(({ label }) => { // keep file only if it's NOT already present in the directory return (this.definitions.findIndex(({ file }) => { return basename(file) === label; }) === -1); }); const filePrompt = { fileName: () => p.select({ message: `Which file do you want to deploy from ${chalk.dim(this.path)}?`, options: fileOptions, }), }; const groupPrompt = { /* Results type should be taken from the previous prompts * defined with clack/prompt */ slug: ({ results }) => p.text({ message: `What is the ${chalk.inverse('documentation slug')} for this ${chalk.dim(results.fileName)} file?`, }), }; // eslint-disable-next-line no-await-in-loop const prompt = await p.group({ ...filePrompt, ...groupPrompt, shouldContinue: () => p.confirm({ message: 'Do you want to select another file?' }), }, { onCancel() { p.cancel('Deploy cancelled.'); throw new ExitError(1); }, }); const file = join(this.path, prompt.fileName); // eslint-disable-next-line no-await-in-loop const definition = await API.load(file); this.d(`${file} looks like an ${definition.specName} spec version ${definition.version}`); this.definitions.push({ definition, file, slug: prompt.slug, }); shouldContinue = prompt.shouldContinue; } p.outro(`You're all set. Your deployments will start soon.`); return this.definitions; } async map(callback) { return Promise.all(this.definitions.map((definition) => callback(definition))); } async readDefinitions() { for await (const { filename, value } of File.listValidConventionFiles(this.path, this.filenamePattern)) { const file = join(this.path, value); /* We already check the filenamePattern match inside the `File.listValidConventionFiles` method so we are sure the group matched exists. */ const slug = filename.match(this.filenamePattern).groups.slug; const definition = await API.load(file); this.definitions.push({ definition, file, slug, }); } return this.definitions; } async renameToConvention(documentation) { const { file, slug } = documentation; if (this.filenamePattern.test(basename(file, extname(file)))) return; // Default convention is defined in the flags.ts file for the // 'filenamePattern' flag. const newFilename = this.buildNewFilename(slug); const newFile = `${dirname(file)}/${newFilename}${extname(file)}`; const confirm = await promptConfirm(`Do you want to rename ${file} to ${newFile} (for later deployments)?`); if (confirm) { await rename(file, newFile, (err) => { if (err) throw err; ux.stdout(ux.colorize('green', `Renamed ${file} to ${newFile}.`)); }); } } async sequentialMap(callback) { for (const definition of this.definitions) { // We explicitly need a sequential run of promises, so the await // in loop is needed. /* eslint-disable-next-line no-await-in-loop */ await callback(definition); } return this.definitions; } stdoutDefinitions() { if (this.definitions.length > 0) { ux.stdout(chalk.underline(`We've found ${this.definitions.length} valid API definitions to deploy`)); ux.stdout(`└─ ${this.path}`); let iterations = this.definitions.length; for (const { definition, file } of this.definitions) { const filename = `${basename(file)} (${definition.specName} spec version ${definition.version})`; iterations -= 1; if (iterations) { ux.stdout(` ├─ ${filename}`); } else { ux.stdout(` └─ ${filename}`); } } ux.stdout(''); } } }