diff --git a/.npmignore b/.npmignore index 5f87409c6d..4475fa4b70 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,4 @@ !bundles/ !package.json +!README.md +!bin/ \ No newline at end of file diff --git a/bin/cli.ts b/bin/cli.ts new file mode 100644 index 0000000000..8d089164eb --- /dev/null +++ b/bin/cli.ts @@ -0,0 +1,241 @@ +#!/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', + }); + }, + 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, + }); + }, + async argv => { + try { + await bundle(argv.spec, { ssr: true, output: argv.o, cdn: argv.cdn }); + } catch (e) { + console.log(e.message); + } + }, + ).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 ` +
+ +