@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
184 lines (154 loc) • 5.54 kB
text/typescript
import * as colorette from 'colorette';
import * as chockidar from 'chokidar';
import { bundle, RedoclyClient, getTotals, getMergedConfig, Config } from '@redocly/openapi-core';
import {
getFallbackApisOrExit,
handleError,
loadConfigAndHandleErrors,
} from '../../utils/miscellaneous';
import startPreviewServer from './preview-server/preview-server';
import type { Skips, VerifyConfigOptions } from '../../types';
import type { CommandArgs } from '../../wrapper';
export type PreviewDocsOptions = {
port: number;
host: string;
'use-community-edition'?: boolean;
config?: string;
api?: string;
force?: boolean;
} & Omit<Skips, 'skip-rule'> &
VerifyConfigOptions;
export async function previewDocs({
argv,
config: configFromFile,
}: CommandArgs<PreviewDocsOptions>) {
let isAuthorizedWithRedocly = false;
let redocOptions: any = {};
let config = await reloadConfig(configFromFile);
const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
const api = apis[0];
let cachedBundle: any;
const deps = new Set<string>();
async function getBundle() {
return cachedBundle;
}
async function updateBundle() {
process.stdout.write('\nBundling...\n\n');
try {
const {
bundle: openapiBundle,
problems,
fileDependencies,
} = await bundle({
ref: api.path,
config,
});
const removed = [...deps].filter((x) => !fileDependencies.has(x));
watcher.unwatch(removed);
watcher.add([...fileDependencies]);
deps.clear();
fileDependencies.forEach(deps.add, deps);
const fileTotals = getTotals(problems);
if (fileTotals.errors === 0) {
process.stdout.write(
fileTotals.errors === 0
? `Created a bundle for ${api.alias || api.path} ${
fileTotals.warnings > 0 ? 'with warnings' : 'successfully'
}\n`
: colorette.yellow(
`Created a bundle for ${
api.alias || api.path
} with errors. Docs may be broken or not accurate\n`
)
);
}
return openapiBundle.parsed;
} catch (e) {
handleError(e, api.path);
}
}
setImmediate(() => {
cachedBundle = updateBundle();
}); // initial cache
const isAuthorized = isAuthorizedWithRedocly || redocOptions.licenseKey;
if (!isAuthorized) {
process.stderr.write(
`Using Redoc community edition.\nLogin with redocly ${colorette.blue(
'login'
)} or use an enterprise license key to preview with the premium docs.\n\n`
);
}
const hotClients = await startPreviewServer(argv.port, argv.host, {
getBundle,
getOptions: () => redocOptions,
useRedocPro: isAuthorized && !redocOptions.useCommunityEdition,
});
const watchPaths = [api.path, config.configFile!].filter((e) => !!e);
const watcher = chockidar.watch(watchPaths, {
disableGlobbing: true,
ignoreInitial: true,
});
const debouncedUpdatedBundle = debounce(async () => {
cachedBundle = updateBundle();
await cachedBundle;
hotClients.broadcast('{"type": "reload", "bundle": true}');
}, 2000);
const changeHandler = async (type: string, file: string) => {
process.stdout.write(`${colorette.green('watch')} ${type} ${colorette.blue(file)}\n`);
if (file === config.configFile) {
config = await reloadConfig();
hotClients.broadcast(JSON.stringify({ type: 'reload' }));
return;
}
debouncedUpdatedBundle();
};
watcher.on('change', changeHandler.bind(undefined, 'changed'));
watcher.on('add', changeHandler.bind(undefined, 'added'));
watcher.on('unlink', changeHandler.bind(undefined, 'removed'));
watcher.on('ready', () => {
process.stdout.write(
`\n 👀 Watching ${colorette.blue(api.path)} and all related resources for changes\n\n`
);
});
async function reloadConfig(config?: Config) {
if (!config) {
try {
config = (await loadConfigAndHandleErrors({ configPath: argv.config })) as Config;
} catch (err) {
config = new Config({ apis: {}, styleguide: {} });
}
}
const redoclyClient = new RedoclyClient();
isAuthorizedWithRedocly = await redoclyClient.isAuthorizedWithRedocly();
const resolvedConfig = getMergedConfig(config, argv.api);
const { styleguide } = resolvedConfig;
styleguide.skipPreprocessors(argv['skip-preprocessor']);
styleguide.skipDecorators(argv['skip-decorator']);
const referenceDocs = resolvedConfig.theme?.openapi;
redocOptions = {
...referenceDocs,
useCommunityEdition: argv['use-community-edition'] || referenceDocs?.useCommunityEdition,
licenseKey: process.env.REDOCLY_LICENSE_KEY || referenceDocs?.licenseKey,
whiteLabel: true,
};
return resolvedConfig;
}
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function debounce(func: Function, wait: number, immediate?: boolean) {
let timeout: NodeJS.Timeout | null;
return function executedFunction(...args: any[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
const later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}