redoc-cli
Version:
ReDoc's Command Line Interface
393 lines (390 loc) ⢠15.5 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.mimeTypes = void 0;
/* tslint:disable:no-implicit-dependencies */
const react_1 = require("react");
const server_1 = require("react-dom/server");
const styled_components_1 = require("styled-components");
const handlebars_1 = require("handlebars");
const http_1 = require("http");
const path_1 = require("path");
const zlib = require("zlib");
const boxen = require("boxen");
// @ts-ignore
const redoc_1 = require("redoc");
const chokidar_1 = require("chokidar");
const fs_1 = require("fs");
const mkdirp = require("mkdirp");
const YargsParser = require("yargs");
// eslint-disable-next-line import/no-extraneous-dependencies
const openapi_core_1 = require("@redocly/openapi-core");
// eslint-disable-next-line import/no-extraneous-dependencies
const openapi_core_2 = require("@redocly/openapi-core");
exports.mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm',
};
const BUNDLES_DIR = path_1.dirname(require.resolve('redoc'));
const boxenOptions = {
title: 'DEPRECATED',
titleAlignment: 'center',
padding: 1,
margin: 1,
borderColor: 'red',
};
const builderForBuildCommand = yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.option('o', {
describe: 'Output file',
alias: 'output',
type: 'string',
default: 'redoc-static.html',
});
yargs.options('title', {
describe: 'Page Title',
type: 'string',
});
yargs.options('disableGoogleFont', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
});
yargs.option('cdn', {
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
type: 'boolean',
default: false,
});
yargs.demandOption('spec');
return yargs;
};
const handlerForBuildCommand = (argv) => __awaiter(void 0, void 0, void 0, function* () {
const config = {
ssr: true,
output: argv.o,
cdn: argv.cdn,
title: argv.title,
disableGoogleFont: argv.disableGoogleFont,
templateFileName: argv.template,
templateOptions: argv.templateOptions || {},
redocOptions: getObjectOrJSON(argv.options),
};
try {
yield bundle(argv.spec, config);
}
catch (e) {
handleError(e);
}
});
YargsParser.command('serve <spec>', 'start the server [deprecated]', yargs => {
yargs.positional('spec', {
describe: 'path or URL to your spec',
});
yargs.options('title', {
describe: 'Page Title',
type: 'string',
});
yargs.option('s', {
alias: 'ssr',
describe: 'Enable server-side rendering',
type: 'boolean',
});
yargs.option('h', {
alias: 'host',
type: 'string',
default: '127.0.0.1',
});
yargs.option('p', {
alias: 'port',
type: 'number',
default: 8080,
});
yargs.option('w', {
alias: 'watch',
type: 'boolean',
});
yargs.options('disable-google-font', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
});
yargs.demandOption('spec');
return yargs;
}, (argv) => __awaiter(void 0, void 0, void 0, function* () {
const config = {
ssr: argv.ssr,
title: argv.title,
watch: argv.watch,
disableGoogleFont: argv.disableGoogleFont,
templateFileName: argv.template,
templateOptions: argv.templateOptions || {},
redocOptions: getObjectOrJSON(argv.options),
};
try {
yield serve(argv.host, argv.port, argv.spec, config);
}
catch (e) {
handleError(e);
}
}), [
res => {
console.log(`
${boxen('This package is deprecated.\n\nUse `npx @redocly/cli preview-docs <api>` instead.', boxenOptions)}`);
return res;
},
], true)
.command('build <spec>', 'build definition into zero-dependency HTML-file [deprecated]', builderForBuildCommand, handlerForBuildCommand, [notifyDeprecation], true)
.command('bundle <spec>', 'bundle spec into zero-dependency HTML-file [deprecated]', builderForBuildCommand, handlerForBuildCommand, [notifyDeprecation], true)
.demandCommand()
.options('t', {
alias: 'template',
describe: 'Path to handlebars page template, see https://git.io/vh8fP for the example ',
type: 'string',
})
.options('templateOptions', {
describe: 'Additional options that you want pass to template. Use dot notation, e.g. templateOptions.metaDescription',
})
.options('options', {
describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars',
}).argv;
function serve(host, port, pathToSpec, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
let spec = yield redoc_1.loadAndBundleSpec(isURL(pathToSpec) ? pathToSpec : path_1.resolve(pathToSpec));
let pageHTML = yield getPageHTML(spec, pathToSpec, options);
const server = http_1.createServer((request, response) => {
console.time('GET ' + request.url);
if (request.url === '/redoc.standalone.js') {
respondWithGzip(fs_1.createReadStream(path_1.join(BUNDLES_DIR, 'redoc.standalone.js'), 'utf8'), request, response, {
'Content-Type': 'application/javascript',
});
}
else if (request.url === '/') {
respondWithGzip(pageHTML, request, response, {
'Content-Type': 'text/html',
});
}
else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {
'Content-Type': 'application/json',
});
}
else {
try {
const filePath = path_1.join(path_1.dirname(pathToSpec), request.url || '');
const extname = String(path_1.extname(filePath)).toLowerCase();
const contentType = exports.mimeTypes[extname] || 'application/octet-stream';
respondWithGzip(fs_1.createReadStream(filePath), request, response, {
'Content-Type': contentType,
});
}
catch (e) {
response.writeHead(404);
response.write('Not found');
response.end();
}
}
console.timeEnd('GET ' + request.url);
});
console.log();
server.listen(port, host, () => console.log(`Server started: http://${host}:${port}`));
if (options.watch && fs_1.existsSync(pathToSpec)) {
const pathToSpecDirectory = path_1.resolve(path_1.dirname(pathToSpec));
const watchOptions = {
ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/],
ignoreInitial: true,
};
const watcher = chokidar_1.watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console);
const handlePath = (_path) => __awaiter(this, void 0, void 0, function* () {
try {
spec = yield redoc_1.loadAndBundleSpec(path_1.resolve(pathToSpec));
pageHTML = yield getPageHTML(spec, pathToSpec, options);
log('Updated successfully');
}
catch (e) {
console.error('Error while updating: ', e.message);
}
});
watcher
.on('change', (path) => __awaiter(this, void 0, void 0, function* () {
log(`${path} changed, updating docs`);
handlePath(path);
}))
.on('add', (path) => __awaiter(this, void 0, void 0, function* () {
log(`File ${path} added, updating docs`);
handlePath(path);
}))
.on('addDir', path => {
log(`ā Directory ${path} added. Files in here will trigger reload.`);
})
.on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`š Watching ${pathToSpecDirectory} for changes...`));
}
});
}
function bundle(pathToSpec, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const start = Date.now();
const spec = yield redoc_1.loadAndBundleSpec(isURL(pathToSpec) ? pathToSpec : path_1.resolve(pathToSpec));
const pageHTML = yield getPageHTML(spec, pathToSpec, Object.assign(Object.assign({}, options), { ssr: true }));
mkdirp.sync(path_1.dirname(options.output));
fs_1.writeFileSync(options.output, pageHTML);
const sizeInKiB = Math.ceil(Buffer.byteLength(pageHTML) / 1024);
const time = Date.now() - start;
console.log(`\nš bundled successfully in: ${options.output} (${sizeInKiB} KiB) [ā± ${time / 1000}s]`);
});
}
function getPageHTML(spec, pathToSpec, { ssr, cdn, title, disableGoogleFont, templateFileName, templateOptions, redocOptions = {}, }) {
return __awaiter(this, void 0, void 0, function* () {
let html;
let css;
let state;
let redocStandaloneSrc;
if (ssr) {
console.log('Prerendering docs');
const specUrl = redocOptions.specUrl || (isURL(pathToSpec) ? pathToSpec : undefined);
const store = yield redoc_1.createStore(spec, specUrl, redocOptions);
const sheet = new styled_components_1.ServerStyleSheet();
// @ts-ignore
html = server_1.renderToString(sheet.collectStyles(react_1.createElement(redoc_1.Redoc, { store })));
css = sheet.getStyleTags();
state = yield store.toJS();
if (!cdn) {
redocStandaloneSrc = fs_1.readFileSync(path_1.join(BUNDLES_DIR, 'redoc.standalone.js'));
}
}
templateFileName = templateFileName ? templateFileName : path_1.join(__dirname, './template.hbs');
const template = handlebars_1.compile(fs_1.readFileSync(templateFileName).toString());
return template({
redocHTML: `
<div id="redoc">${(ssr && html) || ''}</div>
<script>
${(ssr && `const __redoc_state = ${sanitizeJSONString(JSON.stringify(state))};`) || ''}
var container = document.getElementById('redoc');
Redoc.${ssr
? 'hydrate(__redoc_state, container)'
: `init("spec.json", ${JSON.stringify(redocOptions)}, container)`};
</script>`,
redocHead: ssr
? (cdn
? '<script src="https://unpkg.com/redoc@latest/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>',
title: title || spec.info.title || 'ReDoc documentation',
disableGoogleFont,
templateOptions,
});
});
}
// credits: https://stackoverflow.com/a/9238214/1749888
function respondWithGzip(contents, request, response, headers = {}) {
let compressedStream;
const acceptEncoding = request.headers['accept-encoding'] || '';
if (acceptEncoding.match(/\bdeflate\b/)) {
response.writeHead(200, Object.assign(Object.assign({}, headers), { 'content-encoding': 'deflate' }));
compressedStream = zlib.createDeflate();
}
else if (acceptEncoding.match(/\bgzip\b/)) {
response.writeHead(200, Object.assign(Object.assign({}, headers), { 'content-encoding': 'gzip' }));
compressedStream = zlib.createGzip();
}
else {
response.writeHead(200, headers);
if (typeof contents === 'string') {
response.write(contents);
response.end();
}
else {
contents.pipe(response);
}
return;
}
if (typeof contents === 'string') {
compressedStream.write(contents);
compressedStream.pipe(response);
compressedStream.end();
return;
}
else {
contents.pipe(compressedStream).pipe(response);
}
}
function isURL(str) {
return /^(https?:)\/\//m.test(str);
}
function sanitizeJSONString(str) {
return escapeClosingScriptTag(escapeUnicode(str));
}
// see http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/
function escapeClosingScriptTag(str) {
return str.replace(/<\/script>/g, '<\\/script>');
}
// see http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/
function escapeUnicode(str) {
return str.replace(/\u2028|\u2029/g, m => '\\u202' + (m === '\u2028' ? '8' : '9'));
}
function handleError(error) {
console.error(error.stack);
process.exit(1);
}
function getObjectOrJSON(options) {
switch (typeof options) {
case 'object':
return options;
case 'string':
try {
if (fs_1.existsSync(options) && fs_1.lstatSync(options).isFile()) {
return JSON.parse(fs_1.readFileSync(options, 'utf-8'));
}
else {
return JSON.parse(options);
}
}
catch (e) {
console.log(`Encountered error:\n\n${options}\n\nis neither a file with a valid JSON object neither a stringified JSON object.`);
handleError(e);
}
default:
const configFile = openapi_core_1.findConfig();
if (configFile) {
console.log(`Found ${configFile} and using features.openapi options`);
try {
const config = openapi_core_2.parseYaml(fs_1.readFileSync(configFile, 'utf-8'));
return config['features.openapi'];
}
catch (e) {
console.warn(`Found ${configFile} but failed to parse: ${e.message}`);
}
}
return {};
}
}
function notifyDeprecation(res) {
console.log(boxen('This package is deprecated.\n\nUse `npx @redocly/cli build-docs <api>` instead.', boxenOptions));
return res;
}
;