@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
JavaScript
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