Skip to content

Commit

Permalink
feat: ReDoc CLI ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
RomanHotsiy committed Mar 13, 2018
1 parent 21447d2 commit 390f6c1
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
!bundles/
!package.json
!README.md
!bin/
241 changes: 241 additions & 0 deletions bin/cli.ts
Original file line number Diff line number Diff line change
@@ -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 `<html>
<head>
<meta charset="utf8" />
<title>ReDoc</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 0;
margin: 0;
}
</style>
${
ssr
? cdn
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`
: `<script src="redoc.standalone.js"></script>`
}
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
${(ssr && css) || ''}
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', function() {
${(ssr && `const state = ${JSON.stringify(state)};`) || ''}
var container = document.getElementById('redoc');
Redoc.${ssr ? 'hydrate(state, container);' : 'init("spec.json", {}, container)'};
});
</script>
<div id="redoc">${(ssr && html) || ''}</div>
</body>
</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);
};
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './components';
export * from './services';
export * from './utils';

0 comments on commit 390f6c1

Please sign in to comment.