#!/usr/bin/env node import * as React from 'react'; import { renderToString } from 'react-dom/server'; import { ServerStyleSheet } from 'styled-components'; import { createServer, ServerResponse, ServerRequest } from 'http'; import * as zlib from 'zlib'; import { resolve } from 'path'; // @ts-ignore import { Redoc, loadAndBundleSpec, createStore } from '../'; import { createReadStream, writeFileSync, ReadStream, readFileSync, watch, existsSync } from 'fs'; import * as yargs from 'yargs'; type Options = { ssr?: boolean; watch?: boolean; cdn?: boolean; output?: string; }; yargs .command( 'serve [spec]', 'start the server', yargs => { yargs.positional('spec', { describe: 'path or URL to your spec', }); yargs.option('s', { alias: 'ssr', describe: 'Enable server-side rendering', type: 'boolean', }); yargs.option('p', { alias: 'port', type: 'number', default: 8080, }); yargs.option('w', { alias: 'watch', type: 'boolean', }); yargs.demandOption('spec'); return yargs; }, async argv => { try { await serve(argv.port, argv.spec, { ssr: argv.ssr, watch: argv.watch }); } catch (e) { console.log(e.message); } }, ) .command( 'bundle [spec]', 'bundle spec into zero-dependency HTML-file', yargs => { yargs.positional('spec', { describe: 'path or URL to your spec', }); yargs.option('o', { describe: 'Output file', alias: 'output', type: 'number', default: 'redoc-static.html', }); 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; }, async argv => { try { await bundle(argv.spec, { ssr: true, output: argv.o, cdn: argv.cdn }); } catch (e) { console.log(e.message); } }, ) .demandCommand().argv; async function serve(port: number, pathToSpec: string, options: Options = {}) { let spec = await loadAndBundleSpec(pathToSpec); let pageHTML = await getPageHTML(spec, pathToSpec, options); const server = createServer((request, response) => { console.time('GET ' + request.url); if (request.url === '/redoc.standalone.js') { respondWithGzip(createReadStream('bundles/redoc.standalone.js', 'utf8'), request, response, { 'Content-Type': 'application/javascript', }); } else if (request.url === '/') { respondWithGzip(pageHTML, request, response); } else if (request.url === '/spec.json') { const specStr = JSON.stringify(spec, null, 2); respondWithGzip(specStr, request, response, { 'Content-Type': 'application/json', }); } else { response.writeHead(404); response.write('Not found'); response.end(); } console.timeEnd('GET ' + request.url); }); console.log(); server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`)); if (options.watch && existsSync(pathToSpec)) { watch( pathToSpec, debounce(async (event, filename) => { if (event === 'change' || (event === 'rename' && existsSync(filename))) { console.log(`${pathToSpec} changed, updating docs`); try { spec = await loadAndBundleSpec(pathToSpec); pageHTML = await getPageHTML(spec, pathToSpec, options); console.log('Updated successfully'); } catch (e) { console.error('Error while updating: ', e.message); } } }, 2200), ); console.log(`š Watching ${pathToSpec} for changes...`); } } async function bundle(pathToSpec, options: Options = {}) { const spec = await loadAndBundleSpec(pathToSpec); const pageHTML = await getPageHTML(spec, pathToSpec, { ...options, ssr: true }); writeFileSync(options.output!, pageHTML); const sizeInKb = Math.ceil(Buffer.byteLength(pageHTML) / 1024); console.log(`\nš bundled successfully in: ${options.output!} (${sizeInKb} kB)`); } async function getPageHTML(spec: any, pathToSpec: string, { ssr, cdn }: Options) { let html, css, state; let redocStandaloneSrc; if (ssr) { console.log('Prerendering docs'); let store = await createStore(spec, pathToSpec); const sheet = new ServerStyleSheet(); html = renderToString(sheet.collectStyles(React.createElement(Redoc, { store }))); css = sheet.getStyleTags(); state = await store.toJS(); if (!cdn) { redocStandaloneSrc = readFileSync(resolve(__dirname, '../bundles/redoc.standalone.js')); } } return `