UNPKG

@redocly/cli

Version:

[@Redocly](https://redocly.com) CLI is your all-in-one OpenAPI utility. It builds, manages, improves, and quality-checks your OpenAPI descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make API g

418 lines 14.6 kB
import { basename, dirname, extname, join, resolve, relative } from 'node:path'; import { blue, gray, green, red, yellow } from 'colorette'; import { performance } from 'perf_hooks'; import { hasMagic, glob } from 'glob'; import * as fs from 'node:fs'; import * as readline from 'node:readline'; import { Writable } from 'node:stream'; import * as process from 'node:process'; import { ResolveError, YamlParseError, parseYaml, stringifyYaml, isAbsoluteUrl, loadConfig, isEmptyObject, isNotEmptyArray, isNotEmptyObject, pluralize, ConfigValidationError, logger, HandledError, } from '@redocly/openapi-core'; import { outputExtensions } from '../types.js'; import { exitWithError } from './error.js'; import { handleLintConfig } from '../commands/lint.js'; export async function getFallbackApisOrExit(argsApis, config) { const shouldFallbackToAllDefinitions = !isNotEmptyArray(argsApis) && isNotEmptyObject(config.resolvedConfig.apis); const res = shouldFallbackToAllDefinitions ? fallbackToAllDefinitions(config) : await expandGlobsInEntrypoints(argsApis, config); const filteredInvalidEntrypoints = res.filter(({ path }) => !isApiPathValid(path)); if (isNotEmptyArray(filteredInvalidEntrypoints)) { for (const { path } of filteredInvalidEntrypoints) { logger.warn(`\n${formatPath(path)} ${red(`does not exist or is invalid.\n\n`)}`); } exitWithError('Please provide a valid path.'); } return res; } function getConfigDirectory(config) { return config.configPath ? dirname(config.configPath) : process.cwd(); } function isApiPathValid(apiPath) { if (!apiPath.trim()) { exitWithError('Path cannot be empty.'); return; } return fs.existsSync(apiPath) || isAbsoluteUrl(apiPath) ? apiPath : undefined; } function fallbackToAllDefinitions(config) { return Object.entries(config.resolvedConfig.apis || {}).map(([alias, { root, output }]) => ({ path: isAbsoluteUrl(root) ? root : resolve(getConfigDirectory(config), root), alias, output: output && resolve(getConfigDirectory(config), output), })); } function getAliasOrPath(config, aliasOrPath) { const configDir = getConfigDirectory(config); const aliasApi = config.resolvedConfig.apis?.[aliasOrPath]; return aliasApi ? { path: isAbsoluteUrl(aliasApi.root) ? aliasApi.root : resolve(configDir, aliasApi.root), alias: aliasOrPath, output: aliasApi.output && resolve(configDir, aliasApi.output), } : { path: aliasOrPath, // find alias by path, take the first match alias: Object.entries(config.resolvedConfig.apis || {}).find(([_alias, api]) => { return resolve(configDir, api.root) === resolve(aliasOrPath); })?.[0] ?? undefined, }; } async function expandGlobsInEntrypoints(argApis, config) { return (await Promise.all(argApis.map(async (aliasOrPath) => { const shouldResolveGlob = hasMagic(aliasOrPath) && !isAbsoluteUrl(aliasOrPath); if (shouldResolveGlob) { const data = await glob(aliasOrPath, { cwd: getConfigDirectory(config), }); return data.map((g) => getAliasOrPath(config, g)); } return getAliasOrPath(config, aliasOrPath); }))).flat(); } export function getExecutionTime(startedAt) { return process.env.NODE_ENV === 'test' ? '<test>ms' : `${Math.ceil(performance.now() - startedAt)}ms`; } export function printExecutionTime(commandName, startedAt, api) { const elapsed = getExecutionTime(startedAt); logger.info(gray(`\n${api}: ${commandName} processed in ${elapsed}\n\n`)); } export function pathToFilename(path, pathSeparator) { return path .replace(/~1/g, '/') .replace(/~0/g, '~') .replace(/^\//, '') .replace(/\//g, pathSeparator); } export function escapeLanguageName(lang) { return lang.replace(/#/g, '_sharp').replace(/\//, '_').replace(/\s/g, ''); } export function langToExt(lang) { const langObj = { php: '.php', 'c#': '.cs', shell: '.sh', curl: '.sh', bash: '.sh', javascript: '.js', js: '.js', python: '.py', c: '.c', 'c++': '.cpp', coffeescript: '.litcoffee', dart: '.dart', elixir: '.ex', go: '.go', groovy: '.groovy', java: '.java', kotlin: '.kt', 'objective-c': '.m', perl: '.pl', powershell: '.ps1', ruby: '.rb', rust: '.rs', scala: '.sc', swift: '.swift', typescript: '.ts', tsx: '.tsx', 'visual basic': '.vb', 'c/al': '.al', }; return langObj[lang.toLowerCase()]; } export class CircularJSONNotSupportedError extends Error { constructor(originalError) { super(originalError.message); this.originalError = originalError; // Set the prototype explicitly. Object.setPrototypeOf(this, CircularJSONNotSupportedError.prototype); } } export function dumpBundle(obj, format, dereference) { if (format === 'json') { try { return JSON.stringify(obj, null, 2); } catch (e) { if (e.message.indexOf('circular') > -1) { throw new CircularJSONNotSupportedError(e); } throw e; } } else { return stringifyYaml(obj, { noRefs: !dereference, lineWidth: -1, }); } } export function saveBundle(filename, output) { fs.mkdirSync(dirname(filename), { recursive: true }); fs.writeFileSync(filename, output); } export async function promptUser(query, hideUserInput = false) { return new Promise((resolve) => { let output = process.stdout; let isOutputMuted = false; if (hideUserInput) { output = new Writable({ write: (chunk, encoding, callback) => { if (!isOutputMuted) { process.stdout.write(chunk, encoding); } callback(); }, }); } const rl = readline.createInterface({ input: process.stdin, output, terminal: true, historySize: hideUserInput ? 0 : 30, }); rl.question(`${query}:\n\n `, (answer) => { rl.close(); resolve(answer); }); isOutputMuted = hideUserInput; }); } export function readYaml(filename) { return parseYaml(fs.readFileSync(filename, 'utf-8'), { filename }); } export function writeToFileByExtension(data, filePath, noRefs) { const ext = getAndValidateFileExtension(filePath); if (ext === 'json') { writeJson(data, filePath); return; } writeYaml(data, filePath, noRefs); } export function writeYaml(data, filename, noRefs = false) { const content = stringifyYaml(data, { noRefs }); if (process.env.NODE_ENV === 'test') { logger.info(content); return; } fs.mkdirSync(dirname(filename), { recursive: true }); fs.writeFileSync(filename, content); } export function writeJson(data, filename) { const content = JSON.stringify(data, null, 2); if (process.env.NODE_ENV === 'test') { logger.info(content); return; } fs.mkdirSync(dirname(filename), { recursive: true }); fs.writeFileSync(filename, content); } export function getAndValidateFileExtension(fileName) { const ext = fileName.split('.').pop(); if (outputExtensions.includes(ext)) { return ext; } logger.warn(`Unsupported file extension: ${ext}. Using yaml.\n`); return 'yaml'; } export function handleError(e, ref) { switch (e.constructor) { case HandledError: { throw e; } case ResolveError: exitWithError(`Failed to resolve API description at ${ref}:\n\n - ${e.message}`); break; case YamlParseError: exitWithError(`Failed to parse API description at ${ref}:\n\n - ${e.message}`); break; case CircularJSONNotSupportedError: { exitWithError(`Detected circular reference which can't be converted to JSON.\n` + `Try to use ${blue('yaml')} output or remove ${blue('--dereferenced')}.`); break; } case SyntaxError: exitWithError(`Syntax error: ${e.message} ${e.stack?.split('\n\n')?.[0]}`); break; case ConfigValidationError: exitWithError(e.message); break; default: { exitWithError(`Something went wrong when processing ${ref}:\n\n - ${e.message}`); } } } export function printLintTotals(totals, definitionsCount) { const ignored = totals.ignored ? yellow(`${totals.ignored} ${pluralize('problem is', totals.ignored)} explicitly ignored.\n\n`) : ''; if (totals.errors > 0) { logger.error(`❌ Validation failed with ${totals.errors} ${pluralize('error', totals.errors)}${totals.warnings > 0 ? ` and ${totals.warnings} ${pluralize('warning', totals.warnings)}` : ''}.\n${ignored}`); } else if (totals.warnings > 0) { logger.info(green(`Woohoo! Your API ${pluralize('description is', definitionsCount)} valid. 🎉\n`)); logger.warn(`You have ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n${ignored}`); } else { logger.info(green(`Woohoo! Your API ${pluralize('description is', definitionsCount)} valid. 🎉\n${ignored}`)); } if (totals.errors > 0) { logger.info(gray(`run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file.\n`)); } logger.info('\n'); } export function printConfigLintTotals(totals, command) { if (totals.errors > 0) { logger.error(`❌ Your config has ${totals.errors} ${pluralize('error', totals.errors)}.\n`); } else if (totals.warnings > 0) { logger.warn(`⚠️ Your config has ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n`); } else if (command === 'check-config') { logger.info(green('✅ Your config is valid.\n')); } } export function getOutputFileName({ entrypoint, output, argvOutput, ext, entries, }) { let outputFile = output || argvOutput; if (!outputFile) { return { ext: ext || 'yaml' }; } if (entries > 1 && argvOutput) { ext = ext || extname(entrypoint).substring(1); if (!outputExtensions.includes(ext)) { throw new Error(`Invalid file extension: ${ext}.`); } outputFile = join(argvOutput, basename(entrypoint, extname(entrypoint))) + '.' + ext; } else { ext = ext || extname(outputFile).substring(1) || extname(entrypoint).substring(1); if (!outputExtensions.includes(ext)) { throw new Error(`Invalid file extension: ${ext}.`); } outputFile = join(dirname(outputFile), basename(outputFile, extname(outputFile))) + '.' + ext; } return { outputFile, ext }; } export function printUnusedWarnings(config) { const { preprocessors, rules, decorators } = config.getUnusedRules(); if (rules.length) { logger.warn(`[WARNING] Unused rules found in ${blue(config.configPath || '')}: ${rules.join(', ')}.\n`); } if (preprocessors.length) { logger.warn(`[WARNING] Unused preprocessors found in ${blue(config.configPath || '')}: ${preprocessors.join(', ')}.\n`); } if (decorators.length) { logger.warn(`[WARNING] Unused decorators found in ${blue(config.configPath || '')}: ${decorators.join(', ')}.\n`); } if (rules.length || preprocessors.length) { logger.warn(`Check the spelling and verify the added plugin prefix.\n`); } } export async function loadConfigAndHandleErrors(argv, version) { try { const config = await loadConfig({ configPath: argv.config, customExtends: argv.extends, }); await handleLintConfig(argv, version, config); return config; } catch (e) { handleError(e, ''); } } export function sortTopLevelKeysForOas(document) { if ('swagger' in document) { return sortOas2Keys(document); } return sortOas3Keys(document); } function sortOas2Keys(document) { const orderedKeys = [ 'swagger', 'info', 'host', 'basePath', 'schemes', 'consumes', 'produces', 'security', 'tags', 'externalDocs', 'paths', 'definitions', 'parameters', 'responses', 'securityDefinitions', ]; const result = {}; for (const key of orderedKeys) { if (document.hasOwnProperty(key)) { result[key] = document[key]; } } // merge any other top-level keys (e.g. vendor extensions) return Object.assign(result, document); } function sortOas3Keys(document) { const orderedKeys = [ 'openapi', 'info', 'jsonSchemaDialect', 'servers', 'security', 'tags', 'externalDocs', 'paths', 'webhooks', 'x-webhooks', 'components', ]; const result = {}; for (const key of orderedKeys) { if (document.hasOwnProperty(key)) { result[key] = document[key]; } } // merge any other top-level keys (e.g. vendor extensions) return Object.assign(result, document); } export function checkIfRulesetExist(rules) { const ruleset = { ...rules.oas2, ...rules.oas3_0, ...rules.oas3_1, ...rules.oas3_2, ...rules.async2, ...rules.async3, ...rules.arazzo1, ...rules.overlay1, }; if (isEmptyObject(ruleset)) { exitWithError('⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/'); } } export function cleanColors(input) { // eslint-disable-next-line no-control-regex return input.replace(/\x1b\[\d+m/g, ''); } export function formatPath(path) { if (isAbsoluteUrl(path)) { return path; } return relative(process.cwd(), path); } export function capitalize(s) { if (s?.length > 0) { return s[0].toUpperCase() + s.slice(1); } return s; } //# sourceMappingURL=miscellaneous.js.map