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 ` + + + ReDoc + + + + ${ + ssr + ? cdn + ? '' + : `` + : `` + } + + ${(ssr && css) || ''} + + + +
${(ssr && html) || ''}
+ + `; +} + +// credits: https://stackoverflow.com/a/9238214/1749888 +function respondWithGzip( + contents: string | ReadStream, + request: ServerRequest, + response: ServerResponse, + headers = {}, +) { + let compressedStream; + const acceptEncoding = (request.headers['accept-encoding'] as string) || ''; + if (acceptEncoding.match(/\bdeflate\b/)) { + response.writeHead(200, { ...headers, 'content-encoding': 'deflate' }); + compressedStream = zlib.createDeflate(); + } else if (acceptEncoding.match(/\bgzip\b/)) { + response.writeHead(200, { ...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 debounce(callback: Function, time: number) { + let interval; + return (...args) => { + clearTimeout(interval); + interval = setTimeout(() => { + interval = null; + callback(...args); + }, time); + }; +} diff --git a/package.json b/package.json index 52f08eaa50..bf008ef523 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.0.0-alpha.12", "description": "ReDoc", "main": "bundles/redoc.lib.js", + "bin": "bin/cli", "scripts": { "start": "webpack-dev-server --hot", "start:benchmark": "webpack-dev-server --env.prod --env.perf", @@ -104,7 +105,8 @@ "slugify": "^1.2.1", "stickyfill": "^1.1.1", "styled-components": "^3.1.0", - "swagger2openapi": "^2.11.0" + "swagger2openapi": "^2.11.0", + "yargs": "^11.0.0" }, "resolutions": { "@types/chai": "4.0.8" diff --git a/src/index.ts b/src/index.ts index 80e3bea675..bb4687878b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './components'; export * from './services'; +export * from './utils';