@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
160 lines (144 loc) • 5.12 kB
text/typescript
import { compile } from 'handlebars';
import * as colorette from 'colorette';
import { getPort } from 'get-port-please';
import { readFileSync, promises as fsPromises } from 'fs';
import * as path from 'path';
import { startHttpServer, startWsServer, respondWithGzip, mimeTypes } from './server';
import { isSubdir } from '../../../utils/miscellaneous';
import type { IncomingMessage } from 'http';
function getPageHTML(
htmlTemplate: string,
redocOptions: object = {},
useRedocPro: boolean,
wsPort: number,
host: string
) {
let templateSrc = readFileSync(htmlTemplate, 'utf-8');
// fix template for backward compatibility
templateSrc = templateSrc
.replace(/{?{{redocHead}}}?/, '{{{redocHead}}}')
.replace('{{redocBody}}', '{{{redocHTML}}}');
const template = compile(templateSrc);
return template({
redocHead: `
<script>
window.__REDOC_EXPORT = '${useRedocPro ? 'RedoclyReferenceDocs' : 'Redoc'}';
window.__OPENAPI_CLI_WS_PORT = ${wsPort};
window.__OPENAPI_CLI_WS_HOST = "${host}";
</script>
<script src="/simplewebsocket.min.js"></script>
<script src="/hot.js"></script>
<script src="${
useRedocPro
? 'https://cdn.redocly.com/reference-docs/latest/redocly-reference-docs.min.js'
: 'https://cdn.redocly.com/redoc/latest/bundles/redoc.standalone.js'
}"></script>
`,
redocHTML: `
<div id="redoc"></div>
<script>
var container = document.getElementById('redoc');
${
useRedocPro
? "window[window.__REDOC_EXPORT].setPublicPath('https://cdn.redocly.com/reference-docs/latest/');"
: ''
}
window[window.__REDOC_EXPORT].init("/openapi.json", ${JSON.stringify(redocOptions)}, container)
</script>`,
});
}
export default async function startPreviewServer(
port: number,
host: string,
{
getBundle,
getOptions,
useRedocPro,
}: // eslint-disable-next-line @typescript-eslint/ban-types
{ getBundle: Function; getOptions: Function; useRedocPro: boolean }
) {
const defaultTemplate = path.join(__dirname, 'default.hbs');
const handler = async (request: IncomingMessage, response: any) => {
console.time(colorette.dim(`GET ${request.url}`));
const { htmlTemplate } = getOptions() || {};
if (request.url?.endsWith('/') || path.extname(request.url!) === '') {
respondWithGzip(
getPageHTML(htmlTemplate || defaultTemplate, getOptions(), useRedocPro, wsPort, host),
request,
response,
{
'Content-Type': 'text/html',
}
);
} else if (request.url === '/openapi.json') {
const bundle = await getBundle();
if (bundle === undefined) {
respondWithGzip(
JSON.stringify({
openapi: '3.0.0',
info: {
description:
'<code> Failed to generate bundle: check out console output for more details </code>',
},
paths: {},
}),
request,
response,
{
'Content-Type': 'application/json',
}
);
} else {
respondWithGzip(JSON.stringify(bundle), request, response, {
'Content-Type': 'application/json',
});
}
} else {
let filePath =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{
'/hot.js': path.join(__dirname, 'hot.js'),
'/oauth2-redirect.html': path.join(__dirname, 'oauth2-redirect.html'),
'/simplewebsocket.min.js': require.resolve('simple-websocket/simplewebsocket.min.js'),
}[request.url || ''];
if (!filePath) {
const basePath = htmlTemplate ? path.dirname(htmlTemplate) : process.cwd();
filePath = path.resolve(basePath, `.${request.url}`);
if (!isSubdir(basePath, filePath)) {
respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
console.timeEnd(colorette.dim(`GET ${request.url}`));
return;
}
}
const extname = String(path.extname(filePath)).toLowerCase() as keyof typeof mimeTypes;
const contentType = mimeTypes[extname] || 'application/octet-stream';
try {
respondWithGzip(await fsPromises.readFile(filePath), request, response, {
'Content-Type': contentType,
});
} catch (e) {
if (e.code === 'ENOENT') {
respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
} else {
respondWithGzip(
`Something went wrong: ${e.code || e.message}...\n`,
request,
response,
{},
500
);
}
}
}
console.timeEnd(colorette.dim(`GET ${request.url}`));
};
const wsPort = await getPort({ port: 32201, portRange: [32201, 32301], host });
const server = startHttpServer(port, host, handler);
server.on('listening', () => {
process.stdout.write(
`\n 🔎 Preview server running at ${colorette.blue(`http://${host}:${port}\n`)}`
);
});
return startWsServer(wsPort, host);
}